From 9c3f1d26adb08f5d87869225fa4b49f43681b0d5 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 1 Dec 2025 18:41:02 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B0=A8=EB=9F=89=EA=B4=80=EB=A6=AC(=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=EB=8D=B0=EC=9D=B4=ED=84=B0)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/app.ts | 2 + .../src/controllers/authController.ts | 65 ++++ .../src/controllers/driverController.ts | 301 ++++++++++++++++++ backend-node/src/routes/authRoutes.ts | 6 + backend-node/src/routes/driverRoutes.ts | 36 +++ backend-node/src/services/authService.ts | 126 ++++++++ frontend/components/layout/AppLayout.tsx | 13 + frontend/components/layout/ProfileModal.tsx | 143 ++++++++- frontend/hooks/useProfile.ts | 133 +++++++- frontend/lib/api/driver.ts | 92 ++++++ 10 files changed, 914 insertions(+), 3 deletions(-) create mode 100644 backend-node/src/controllers/driverController.ts create mode 100644 backend-node/src/routes/driverRoutes.ts create mode 100644 frontend/lib/api/driver.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 87470dd6..3b5e74da 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -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); // 임시 주석 diff --git a/backend-node/src/controllers/authController.ts b/backend-node/src/controllers/authController.ts index 374015ee..6f72eb10 100644 --- a/backend-node/src/controllers/authController.ts +++ b/backend-node/src/controllers/authController.ts @@ -384,4 +384,69 @@ export class AuthController { }); } } + + /** + * POST /api/auth/signup + * 공차중계 회원가입 API + */ + static async signup(req: Request, res: Response): Promise { + 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 : "알 수 없는 오류", + }, + }); + } + } } diff --git a/backend-node/src/controllers/driverController.ts b/backend-node/src/controllers/driverController.ts new file mode 100644 index 00000000..61a8a010 --- /dev/null +++ b/backend-node/src/controllers/driverController.ts @@ -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 { + try { + const userId = req.user?.userId; + + if (!userId) { + res.status(401).json({ + success: false, + message: "인증이 필요합니다.", + }); + return; + } + + // 사용자 정보 조회 + const userResult = await query( + `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( + `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 { + 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( + `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( + `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 { + 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( + `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 { + try { + const userId = req.user?.userId; + + if (!userId) { + res.status(401).json({ + success: false, + message: "인증이 필요합니다.", + }); + return; + } + + // 공차중계 사용자 확인 + const userCheck = await query( + `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: "회원 탈퇴 처리 중 오류가 발생했습니다.", + }); + } + } +} + diff --git a/backend-node/src/routes/authRoutes.ts b/backend-node/src/routes/authRoutes.ts index 29bc7944..adba86e6 100644 --- a/backend-node/src/routes/authRoutes.ts +++ b/backend-node/src/routes/authRoutes.ts @@ -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; diff --git a/backend-node/src/routes/driverRoutes.ts b/backend-node/src/routes/driverRoutes.ts new file mode 100644 index 00000000..29a68244 --- /dev/null +++ b/backend-node/src/routes/driverRoutes.ts @@ -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; + diff --git a/backend-node/src/services/authService.ts b/backend-node/src/services/authService.ts index 11e34576..e5d6aa97 100644 --- a/backend-node/src/services/authService.ts +++ b/backend-node/src/services/authService.ts @@ -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( + `SELECT user_id FROM user_info WHERE user_id = $1`, + [userId] + ); + + if (existingUser.length > 0) { + return { + success: false, + message: "이미 존재하는 아이디입니다.", + }; + } + + // 2. 중복 차량번호 확인 + const existingVehicle = await query( + `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 || "회원가입 중 오류가 발생했습니다.", + }; + } + } } diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 8394cd6d..51b939a4 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -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} diff --git a/frontend/components/layout/ProfileModal.tsx b/frontend/components/layout/ProfileModal.tsx index 9dce16a0..8fd2190a 100644 --- a/frontend/components/layout/ProfileModal.tsx +++ b/frontend/components/layout/ProfileModal.tsx @@ -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) => 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 ( <> @@ -234,6 +280,101 @@ export function ProfileModal({ + + {/* 운전자 정보 섹션 (공차중계 사용자만) */} + {isDriver && driverFormData && onDriverFormChange && ( + <> + +
+
+ +

차량/운전자 정보

+
+ +
+
+ + onDriverFormChange("vehicleNumber", e.target.value)} + placeholder="12가1234" + /> +
+
+ + onDriverFormChange("vehicleType", e.target.value)} + placeholder="1톤 카고" + /> +
+
+ +
+
+ + onDriverFormChange("phoneNumber", e.target.value)} + placeholder="010-1234-5678" + /> +
+
+ + onDriverFormChange("licenseNumber", e.target.value)} + placeholder="12-34-567890-12" + /> +
+
+ + {/* 차량 상태 */} + {driverInfo && onDriverStatusChange && ( +
+ +
+ + {getStatusLabel(driverInfo.vehicleStatus)} + +
+ + +
+
+

+ * 운행/공차 상태는 공차등록 화면에서 변경하세요 +

+
+ )} + +
+ + )} diff --git a/frontend/hooks/useProfile.ts b/frontend/hooks/useProfile.ts index 0498eb13..bae5e9d7 100644 --- a/frontend/hooks/useProfile.ts +++ b/frontend/hooks/useProfile.ts @@ -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, refr }> >([]); + // 운전자 정보 상태 + const [isDriver, setIsDriver] = useState(false); + const [driverInfo, setDriverInfo] = useState(null); + const [driverFormData, setDriverFormData] = useState({ + 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, 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, refr if (user) { // 부서 목록 로드 loadDepartments(); + // 운전자 정보 로드 + loadDriverInfo(); setModalState((prev) => ({ ...prev, @@ -98,7 +147,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr isSaving: false, })); } - }, [user, loadDepartments]); + }, [user, loadDepartments, loadDriverInfo]); /** * 프로필 모달 닫기 @@ -125,6 +174,61 @@ export const useProfile = (user: any, refreshUserData: () => Promise, 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, 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, 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, refr alertModal, closeAlert, + // 운전자 관련 상태 + isDriver, + driverInfo, + driverFormData, + // 액션 openProfileModal, closeProfileModal, @@ -286,5 +410,10 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr selectImage, removeImage, saveProfile, + + // 운전자 관련 액션 + updateDriverFormData, + handleDriverStatusChange, + handleDriverAccountDelete, }; }; diff --git a/frontend/lib/api/driver.ts b/frontend/lib/api/driver.ts new file mode 100644 index 00000000..8600c5cb --- /dev/null +++ b/frontend/lib/api/driver.ts @@ -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 || "회원 탈퇴에 실패했습니다.", + }; + } +} +