차량관리(기초데이터) 구현

This commit is contained in:
dohyeons 2025-12-01 18:41:02 +09:00
parent cea2421899
commit 9c3f1d26ad
10 changed files with 914 additions and 3 deletions

View File

@ -72,6 +72,7 @@ import numberingRuleRoutes from "./routes/numberingRuleRoutes"; // 채번 규칙
import entitySearchRoutes from "./routes/entitySearchRoutes"; // 엔티티 검색
import orderRoutes from "./routes/orderRoutes"; // 수주 관리
import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
import { BatchSchedulerService } from "./services/batchSchedulerService";
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
@ -237,6 +238,7 @@ app.use("/api/code-merge", codeMergeRoutes); // 코드 병합
app.use("/api/numbering-rules", numberingRuleRoutes); // 채번 규칙 관리
app.use("/api/entity-search", entitySearchRoutes); // 엔티티 검색
app.use("/api/orders", orderRoutes); // 수주 관리
app.use("/api/driver", driverRoutes); // 공차중계 운전자 관리
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
// app.use("/api/collections", collectionRoutes); // 임시 주석
// app.use("/api/batch", batchRoutes); // 임시 주석

View File

@ -384,4 +384,69 @@ export class AuthController {
});
}
}
/**
* POST /api/auth/signup
* API
*/
static async signup(req: Request, res: Response): Promise<void> {
try {
const { userId, password, userName, phoneNumber, licenseNumber, vehicleNumber, vehicleType } = req.body;
logger.info(`=== 공차중계 회원가입 API 호출 ===`);
logger.info(`userId: ${userId}, vehicleNumber: ${vehicleNumber}`);
// 입력값 검증
if (!userId || !password || !userName || !phoneNumber || !licenseNumber || !vehicleNumber) {
res.status(400).json({
success: false,
message: "필수 입력값이 누락되었습니다.",
error: {
code: "INVALID_INPUT",
details: "아이디, 비밀번호, 이름, 연락처, 면허번호, 차량번호는 필수입니다.",
},
});
return;
}
// 회원가입 처리
const signupResult = await AuthService.signupDriver({
userId,
password,
userName,
phoneNumber,
licenseNumber,
vehicleNumber,
vehicleType,
});
if (signupResult.success) {
logger.info(`공차중계 회원가입 성공: ${userId}`);
res.status(201).json({
success: true,
message: "회원가입이 완료되었습니다.",
});
} else {
logger.warn(`공차중계 회원가입 실패: ${userId} - ${signupResult.message}`);
res.status(400).json({
success: false,
message: signupResult.message || "회원가입에 실패했습니다.",
error: {
code: "SIGNUP_FAILED",
details: signupResult.message,
},
});
}
} catch (error) {
logger.error("공차중계 회원가입 API 오류:", error);
res.status(500).json({
success: false,
message: "회원가입 처리 중 오류가 발생했습니다.",
error: {
code: "SIGNUP_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
});
}
}
}

View File

@ -0,0 +1,301 @@
// 공차중계 운전자 컨트롤러
import { Request, Response } from "express";
import { query } from "../database/db";
import { logger } from "../utils/logger";
export class DriverController {
/**
* GET /api/driver/profile
*
*/
static async getProfile(req: Request, res: Response): Promise<void> {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
message: "인증이 필요합니다.",
});
return;
}
// 사용자 정보 조회
const userResult = await query<any>(
`SELECT
user_id, user_name, cell_phone, license_number, vehicle_number, signup_type
FROM user_info
WHERE user_id = $1`,
[userId]
);
if (userResult.length === 0) {
res.status(404).json({
success: false,
message: "사용자를 찾을 수 없습니다.",
});
return;
}
const user = userResult[0];
// 공차중계 사용자가 아닌 경우
if (user.signup_type !== "DRIVER") {
res.status(403).json({
success: false,
message: "공차중계 사용자만 접근할 수 있습니다.",
});
return;
}
// 차량 정보 조회
const vehicleResult = await query<any>(
`SELECT
vehicle_number, vehicle_type, driver_name, driver_phone, status
FROM vehicles
WHERE user_id = $1`,
[userId]
);
const vehicle = vehicleResult.length > 0 ? vehicleResult[0] : null;
res.status(200).json({
success: true,
data: {
userId: user.user_id,
userName: user.user_name,
phoneNumber: user.cell_phone,
licenseNumber: user.license_number,
vehicleNumber: user.vehicle_number,
vehicleType: vehicle?.vehicle_type || null,
vehicleStatus: vehicle?.status || null,
},
});
} catch (error) {
logger.error("운전자 프로필 조회 오류:", error);
res.status(500).json({
success: false,
message: "프로필 조회 중 오류가 발생했습니다.",
});
}
}
/**
* PUT /api/driver/profile
* (, , , , )
*/
static async updateProfile(req: Request, res: Response): Promise<void> {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
message: "인증이 필요합니다.",
});
return;
}
const { userName, phoneNumber, licenseNumber, vehicleNumber, vehicleType } = req.body;
// 공차중계 사용자 확인
const userCheck = await query<any>(
`SELECT signup_type, vehicle_number FROM user_info WHERE user_id = $1`,
[userId]
);
if (userCheck.length === 0) {
res.status(404).json({
success: false,
message: "사용자를 찾을 수 없습니다.",
});
return;
}
if (userCheck[0].signup_type !== "DRIVER") {
res.status(403).json({
success: false,
message: "공차중계 사용자만 접근할 수 있습니다.",
});
return;
}
const oldVehicleNumber = userCheck[0].vehicle_number;
// 차량번호 변경 시 중복 확인
if (vehicleNumber && vehicleNumber !== oldVehicleNumber) {
const duplicateCheck = await query<any>(
`SELECT vehicle_number FROM vehicles WHERE vehicle_number = $1 AND user_id != $2`,
[vehicleNumber, userId]
);
if (duplicateCheck.length > 0) {
res.status(400).json({
success: false,
message: "이미 등록된 차량번호입니다.",
});
return;
}
}
// user_info 업데이트
await query(
`UPDATE user_info SET
user_name = COALESCE($1, user_name),
cell_phone = COALESCE($2, cell_phone),
license_number = COALESCE($3, license_number),
vehicle_number = COALESCE($4, vehicle_number)
WHERE user_id = $5`,
[userName || null, phoneNumber || null, licenseNumber || null, vehicleNumber || null, userId]
);
// vehicles 테이블 업데이트
await query(
`UPDATE vehicles SET
vehicle_number = COALESCE($1, vehicle_number),
vehicle_type = COALESCE($2, vehicle_type),
driver_name = COALESCE($3, driver_name),
driver_phone = COALESCE($4, driver_phone),
updated_at = NOW()
WHERE user_id = $5`,
[vehicleNumber || null, vehicleType || null, userName || null, phoneNumber || null, userId]
);
logger.info(`운전자 프로필 수정 완료: ${userId}`);
res.status(200).json({
success: true,
message: "프로필이 수정되었습니다.",
});
} catch (error) {
logger.error("운전자 프로필 수정 오류:", error);
res.status(500).json({
success: false,
message: "프로필 수정 중 오류가 발생했습니다.",
});
}
}
/**
* PUT /api/driver/status
* (/ )
*/
static async updateStatus(req: Request, res: Response): Promise<void> {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
message: "인증이 필요합니다.",
});
return;
}
const { status } = req.body;
// 허용된 상태값만 (대기: off, 정비: maintenance)
const allowedStatuses = ["off", "maintenance"];
if (!status || !allowedStatuses.includes(status)) {
res.status(400).json({
success: false,
message: "유효하지 않은 상태값입니다. (off: 대기, maintenance: 정비)",
});
return;
}
// 공차중계 사용자 확인
const userCheck = await query<any>(
`SELECT signup_type FROM user_info WHERE user_id = $1`,
[userId]
);
if (userCheck.length === 0 || userCheck[0].signup_type !== "DRIVER") {
res.status(403).json({
success: false,
message: "공차중계 사용자만 접근할 수 있습니다.",
});
return;
}
// vehicles 테이블 상태 업데이트
const updateResult = await query(
`UPDATE vehicles SET status = $1, updated_at = NOW() WHERE user_id = $2`,
[status, userId]
);
logger.info(`차량 상태 변경: ${userId} -> ${status}`);
res.status(200).json({
success: true,
message: `차량 상태가 ${status === "off" ? "대기" : "정비"}로 변경되었습니다.`,
});
} catch (error) {
logger.error("차량 상태 변경 오류:", error);
res.status(500).json({
success: false,
message: "상태 변경 중 오류가 발생했습니다.",
});
}
}
/**
* DELETE /api/driver/account
* ( )
*/
static async deleteAccount(req: Request, res: Response): Promise<void> {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
message: "인증이 필요합니다.",
});
return;
}
// 공차중계 사용자 확인
const userCheck = await query<any>(
`SELECT signup_type FROM user_info WHERE user_id = $1`,
[userId]
);
if (userCheck.length === 0) {
res.status(404).json({
success: false,
message: "사용자를 찾을 수 없습니다.",
});
return;
}
if (userCheck[0].signup_type !== "DRIVER") {
res.status(403).json({
success: false,
message: "공차중계 사용자만 탈퇴할 수 있습니다.",
});
return;
}
// vehicles 테이블에서 삭제
await query(`DELETE FROM vehicles WHERE user_id = $1`, [userId]);
// user_info 테이블에서 삭제
await query(`DELETE FROM user_info WHERE user_id = $1`, [userId]);
logger.info(`회원 탈퇴 완료: ${userId}`);
res.status(200).json({
success: true,
message: "회원 탈퇴가 완료되었습니다.",
});
} catch (error) {
logger.error("회원 탈퇴 오류:", error);
res.status(500).json({
success: false,
message: "회원 탈퇴 처리 중 오류가 발생했습니다.",
});
}
}
}

View File

@ -41,4 +41,10 @@ router.post("/logout", AuthController.logout);
*/
router.post("/refresh", AuthController.refreshToken);
/**
* POST /api/auth/signup
* API
*/
router.post("/signup", AuthController.signup);
export default router;

View File

@ -0,0 +1,36 @@
// 공차중계 운전자 API 라우터
import { Router } from "express";
import { DriverController } from "../controllers/driverController";
import { authenticateToken } from "../middleware/authMiddleware";
const router = Router();
// 모든 라우트에 인증 필요
router.use(authenticateToken);
/**
* GET /api/driver/profile
*
*/
router.get("/profile", DriverController.getProfile);
/**
* PUT /api/driver/profile
* (, , , , )
*/
router.put("/profile", DriverController.updateProfile);
/**
* PUT /api/driver/status
* (/)
*/
router.put("/status", DriverController.updateStatus);
/**
* DELETE /api/driver/account
*
*/
router.delete("/account", DriverController.deleteAccount);
export default router;

View File

@ -342,4 +342,130 @@ export class AuthService {
);
}
}
/**
*
* - user_info
* - vehicles
*/
static async signupDriver(data: {
userId: string;
password: string;
userName: string;
phoneNumber: string;
licenseNumber: string;
vehicleNumber: string;
vehicleType?: string;
}): Promise<{ success: boolean; message?: string }> {
try {
const {
userId,
password,
userName,
phoneNumber,
licenseNumber,
vehicleNumber,
vehicleType,
} = data;
// 1. 중복 사용자 확인
const existingUser = await query<any>(
`SELECT user_id FROM user_info WHERE user_id = $1`,
[userId]
);
if (existingUser.length > 0) {
return {
success: false,
message: "이미 존재하는 아이디입니다.",
};
}
// 2. 중복 차량번호 확인
const existingVehicle = await query<any>(
`SELECT vehicle_number FROM vehicles WHERE vehicle_number = $1`,
[vehicleNumber]
);
if (existingVehicle.length > 0) {
return {
success: false,
message: "이미 등록된 차량번호입니다.",
};
}
// 3. 비밀번호 암호화 (MD5 - 기존 시스템 호환)
const crypto = require("crypto");
const hashedPassword = crypto
.createHash("md5")
.update(password)
.digest("hex");
// 4. 사용자 정보 저장 (user_info)
await query(
`INSERT INTO user_info (
user_id,
user_password,
user_name,
cell_phone,
license_number,
vehicle_number,
company_code,
user_type,
signup_type,
status,
regdate
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())`,
[
userId,
hashedPassword,
userName,
phoneNumber,
licenseNumber,
vehicleNumber,
"COMPANY_13", // 기본 회사 코드
null, // user_type: null
"DRIVER", // signup_type: 공차중계 회원가입 사용자
"active", // status: active
]
);
// 5. 차량 정보 저장 (vehicles)
await query(
`INSERT INTO vehicles (
vehicle_number,
vehicle_type,
driver_name,
driver_phone,
status,
company_code,
user_id,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())`,
[
vehicleNumber,
vehicleType || null,
userName,
phoneNumber,
"off", // 초기 상태: off (대기)
"COMPANY_13", // 기본 회사 코드
userId, // 사용자 ID 연결
]
);
logger.info(`공차중계 회원가입 성공: ${userId}, 차량번호: ${vehicleNumber}`);
return {
success: true,
message: "회원가입이 완료되었습니다.",
};
} catch (error: any) {
logger.error("공차중계 회원가입 오류:", error);
return {
success: false,
message: error.message || "회원가입 중 오류가 발생했습니다.",
};
}
}
}

View File

@ -234,6 +234,13 @@ function AppLayoutInner({ children }: AppLayoutProps) {
selectImage,
removeImage,
saveProfile,
// 운전자 관련
isDriver,
driverInfo,
driverFormData,
updateDriverFormData,
handleDriverStatusChange,
handleDriverAccountDelete,
} = useProfile(user, refreshUserData, refreshMenus);
// 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려)
@ -483,6 +490,12 @@ function AppLayoutInner({ children }: AppLayoutProps) {
isSaving={isSaving}
departments={departments}
alertModal={alertModal}
isDriver={isDriver}
driverInfo={driverInfo}
driverFormData={driverFormData}
onDriverFormChange={updateDriverFormData}
onDriverStatusChange={handleDriverStatusChange}
onDriverAccountDelete={handleDriverAccountDelete}
onClose={closeProfileModal}
onFormChange={updateFormData}
onImageSelect={selectImage}

View File

@ -11,8 +11,18 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Camera, X } from "lucide-react";
import { Camera, X, Car, Wrench, Clock } from "lucide-react";
import { ProfileFormData } from "@/types/profile";
import { Separator } from "@/components/ui/separator";
// 운전자 정보 타입
export interface DriverInfo {
vehicleNumber: string;
vehicleType: string | null;
licenseNumber: string;
phoneNumber: string;
vehicleStatus: string | null;
}
// 알림 모달 컴포넌트
interface AlertModalProps {
@ -54,6 +64,14 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod
);
}
// 운전자 폼 데이터 타입
export interface DriverFormData {
vehicleNumber: string;
vehicleType: string;
licenseNumber: string;
phoneNumber: string;
}
interface ProfileModalProps {
isOpen: boolean;
user: any;
@ -70,6 +88,13 @@ interface ProfileModalProps {
message: string;
type: "success" | "error" | "info";
};
// 운전자 관련 props (선택적)
isDriver?: boolean;
driverInfo?: DriverInfo | null;
driverFormData?: DriverFormData;
onDriverFormChange?: (field: keyof DriverFormData, value: string) => void;
onDriverStatusChange?: (status: "off" | "maintenance") => void;
onDriverAccountDelete?: () => void;
onClose: () => void;
onFormChange: (field: keyof ProfileFormData, value: string) => void;
onImageSelect: (event: React.ChangeEvent<HTMLInputElement>) => void;
@ -89,6 +114,12 @@ export function ProfileModal({
isSaving,
departments,
alertModal,
isDriver = false,
driverInfo,
driverFormData,
onDriverFormChange,
onDriverStatusChange,
onDriverAccountDelete,
onClose,
onFormChange,
onImageSelect,
@ -96,6 +127,21 @@ export function ProfileModal({
onSave,
onAlertClose,
}: ProfileModalProps) {
// 차량 상태 한글 변환
const getStatusLabel = (status: string | null) => {
switch (status) {
case "off":
return "대기";
case "active":
return "운행중";
case "inactive":
return "공차";
case "maintenance":
return "정비";
default:
return status || "-";
}
};
return (
<>
<ResizableDialog open={isOpen} onOpenChange={onClose}>
@ -234,6 +280,101 @@ export function ProfileModal({
</Select>
</div>
</div>
{/* 운전자 정보 섹션 (공차중계 사용자만) */}
{isDriver && driverFormData && onDriverFormChange && (
<>
<Separator className="my-4" />
<div className="space-y-4">
<div className="flex items-center gap-2">
<Car className="h-5 w-5 text-primary" />
<h3 className="text-sm font-semibold">/ </h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="vehicleNumber"></Label>
<Input
id="vehicleNumber"
value={driverFormData.vehicleNumber}
onChange={(e) => onDriverFormChange("vehicleNumber", e.target.value)}
placeholder="12가1234"
/>
</div>
<div className="space-y-2">
<Label htmlFor="vehicleType"></Label>
<Input
id="vehicleType"
value={driverFormData.vehicleType}
onChange={(e) => onDriverFormChange("vehicleType", e.target.value)}
placeholder="1톤 카고"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="driverPhone"></Label>
<Input
id="driverPhone"
value={driverFormData.phoneNumber}
onChange={(e) => onDriverFormChange("phoneNumber", e.target.value)}
placeholder="010-1234-5678"
/>
</div>
<div className="space-y-2">
<Label htmlFor="licenseNumber"></Label>
<Input
id="licenseNumber"
value={driverFormData.licenseNumber}
onChange={(e) => onDriverFormChange("licenseNumber", e.target.value)}
placeholder="12-34-567890-12"
/>
</div>
</div>
{/* 차량 상태 */}
{driverInfo && onDriverStatusChange && (
<div className="space-y-2">
<Label> </Label>
<div className="flex items-center gap-4">
<span className="text-sm font-medium px-3 py-1 rounded-full bg-muted">
{getStatusLabel(driverInfo.vehicleStatus)}
</span>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onDriverStatusChange("off")}
disabled={driverInfo.vehicleStatus === "off"}
className="flex items-center gap-1"
>
<Clock className="h-3 w-3" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onDriverStatusChange("maintenance")}
disabled={driverInfo.vehicleStatus === "maintenance"}
className="flex items-center gap-1"
>
<Wrench className="h-3 w-3" />
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
* /
</p>
</div>
)}
</div>
</>
)}
</div>
<ResizableDialogFooter>

View File

@ -4,6 +4,14 @@ import { useState, useCallback, useEffect } from "react";
import { ProfileFormData, ProfileModalState } from "@/types/profile";
import { LAYOUT_CONFIG, MESSAGES } from "@/constants/layout";
import { apiCall } from "@/lib/api/client";
import {
getDriverProfile,
updateDriverProfile,
updateDriverStatus,
deleteDriverAccount,
DriverProfile,
} from "@/lib/api/driver";
import { DriverInfo, DriverFormData } from "@/components/layout/ProfileModal";
// 알림 모달 상태 타입
interface AlertModalState {
@ -48,6 +56,16 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
}>
>([]);
// 운전자 정보 상태
const [isDriver, setIsDriver] = useState(false);
const [driverInfo, setDriverInfo] = useState<DriverInfo | null>(null);
const [driverFormData, setDriverFormData] = useState<DriverFormData>({
vehicleNumber: "",
vehicleType: "",
licenseNumber: "",
phoneNumber: "",
});
// 알림 모달 표시 함수
const showAlert = useCallback((title: string, message: string, type: "success" | "error" | "info" = "info") => {
setAlertModal({
@ -75,6 +93,35 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
}
}, []);
// 운전자 정보 로드 함수
const loadDriverInfo = useCallback(async () => {
try {
const response = await getDriverProfile();
if (response.success && response.data) {
setIsDriver(true);
setDriverInfo({
vehicleNumber: response.data.vehicleNumber,
vehicleType: response.data.vehicleType,
licenseNumber: response.data.licenseNumber,
phoneNumber: response.data.phoneNumber,
vehicleStatus: response.data.vehicleStatus,
});
setDriverFormData({
vehicleNumber: response.data.vehicleNumber || "",
vehicleType: response.data.vehicleType || "",
licenseNumber: response.data.licenseNumber || "",
phoneNumber: response.data.phoneNumber || "",
});
} else {
setIsDriver(false);
setDriverInfo(null);
}
} catch (error) {
console.error("운전자 정보 로드 실패:", error);
setIsDriver(false);
}
}, []);
/**
*
*/
@ -82,6 +129,8 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
if (user) {
// 부서 목록 로드
loadDepartments();
// 운전자 정보 로드
loadDriverInfo();
setModalState((prev) => ({
...prev,
@ -98,7 +147,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
isSaving: false,
}));
}
}, [user, loadDepartments]);
}, [user, loadDepartments, loadDriverInfo]);
/**
*
@ -125,6 +174,61 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
}));
}, []);
/**
*
*/
const updateDriverFormData = useCallback((field: keyof DriverFormData, value: string) => {
setDriverFormData((prev) => ({
...prev,
[field]: value,
}));
}, []);
/**
* (/)
*/
const handleDriverStatusChange = useCallback(
async (status: "off" | "maintenance") => {
try {
const response = await updateDriverStatus(status);
if (response.success) {
showAlert("상태 변경", response.message || "차량 상태가 변경되었습니다.", "success");
// 운전자 정보 새로고침
await loadDriverInfo();
} else {
showAlert("상태 변경 실패", response.message || "상태 변경에 실패했습니다.", "error");
}
} catch (error) {
console.error("차량 상태 변경 실패:", error);
showAlert("오류", "상태 변경 중 오류가 발생했습니다.", "error");
}
},
[showAlert, loadDriverInfo]
);
/**
*
*/
const handleDriverAccountDelete = useCallback(async () => {
if (!confirm("정말로 탈퇴하시겠습니까?\n차량 정보가 함께 삭제되며, 이 작업은 되돌릴 수 없습니다.")) {
return;
}
try {
const response = await deleteDriverAccount();
if (response.success) {
showAlert("탈퇴 완료", "회원 탈퇴가 완료되었습니다.", "success");
// 로그아웃 처리
window.location.href = "/login";
} else {
showAlert("탈퇴 실패", response.message || "회원 탈퇴에 실패했습니다.", "error");
}
} catch (error) {
console.error("회원 탈퇴 실패:", error);
showAlert("오류", "회원 탈퇴 중 오류가 발생했습니다.", "error");
}
}, [showAlert]);
/**
*
*/
@ -229,6 +333,21 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
// API 호출 (JWT 토큰 자동 포함)
const response = await apiCall("PUT", "/admin/profile", updateData);
// 운전자 정보도 저장 (운전자인 경우)
if (isDriver) {
const driverResponse = await updateDriverProfile({
userName: modalState.formData.userName,
phoneNumber: driverFormData.phoneNumber,
licenseNumber: driverFormData.licenseNumber,
vehicleNumber: driverFormData.vehicleNumber,
vehicleType: driverFormData.vehicleType,
});
if (!driverResponse.success) {
console.warn("운전자 정보 저장 실패:", driverResponse.message);
}
}
if (response.success || (response as any).result) {
// locale이 변경된 경우 전역 변수와 localStorage 업데이트
const localeChanged = modalState.formData.locale && modalState.formData.locale !== user.locale;
@ -265,7 +384,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
} finally {
setModalState((prev) => ({ ...prev, isSaving: false }));
}
}, [user, modalState.selectedFile, modalState.selectedImage, modalState.formData, refreshUserData, showAlert]);
}, [user, modalState.selectedFile, modalState.selectedImage, modalState.formData, refreshUserData, showAlert, isDriver, driverFormData]);
return {
// 상태
@ -279,6 +398,11 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
alertModal,
closeAlert,
// 운전자 관련 상태
isDriver,
driverInfo,
driverFormData,
// 액션
openProfileModal,
closeProfileModal,
@ -286,5 +410,10 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
selectImage,
removeImage,
saveProfile,
// 운전자 관련 액션
updateDriverFormData,
handleDriverStatusChange,
handleDriverAccountDelete,
};
};

View File

@ -0,0 +1,92 @@
// 공차중계 운전자 API
import { apiClient } from "./client";
export interface DriverProfile {
userId: string;
userName: string;
phoneNumber: string;
licenseNumber: string;
vehicleNumber: string;
vehicleType: string | null;
vehicleStatus: string | null;
}
export interface DriverProfileUpdateData {
userName?: string;
phoneNumber?: string;
licenseNumber?: string;
vehicleNumber?: string;
vehicleType?: string;
}
/**
*
*/
export async function getDriverProfile(): Promise<{
success: boolean;
data?: DriverProfile;
message?: string;
}> {
try {
const response = await apiClient.get("/driver/profile");
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "프로필 조회에 실패했습니다.",
};
}
}
/**
*
*/
export async function updateDriverProfile(
data: DriverProfileUpdateData
): Promise<{ success: boolean; message?: string }> {
try {
const response = await apiClient.put("/driver/profile", data);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "프로필 수정에 실패했습니다.",
};
}
}
/**
* (/)
*/
export async function updateDriverStatus(
status: "off" | "maintenance"
): Promise<{ success: boolean; message?: string }> {
try {
const response = await apiClient.put("/driver/status", { status });
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "상태 변경에 실패했습니다.",
};
}
}
/**
*
*/
export async function deleteDriverAccount(): Promise<{
success: boolean;
message?: string;
}> {
try {
const response = await apiClient.delete("/driver/account");
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "회원 탈퇴에 실패했습니다.",
};
}
}