diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 104a7fbe..d214c19a 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -73,6 +73,7 @@ import entitySearchRoutes from "./routes/entitySearchRoutes"; // 엔티티 검 import orderRoutes from "./routes/orderRoutes"; // 수주 관리 import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달 import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리 +import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리 import { BatchSchedulerService } from "./services/batchSchedulerService"; // import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석 // import batchRoutes from "./routes/batchRoutes"; // 임시 주석 @@ -238,6 +239,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/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리 // app.use("/api/collections", collectionRoutes); // 임시 주석 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..a448d9c0 --- /dev/null +++ b/backend-node/src/controllers/driverController.ts @@ -0,0 +1,458 @@ +// 공차중계 운전자 컨트롤러 +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, branch_name + 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, + branchName: user.branch_name || 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, branchName } = 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), + branch_name = COALESCE($5, branch_name) + WHERE user_id = $6`, + [userName || null, phoneNumber || null, licenseNumber || null, vehicleNumber || null, branchName || 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), + branch_name = COALESCE($5, branch_name), + updated_at = NOW() + WHERE user_id = $6`, + [vehicleNumber || null, vehicleType || null, userName || null, phoneNumber || null, branchName || 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/vehicle + * 차량 삭제 (user_id = NULL 처리, 기록 보존) + */ + static async deleteVehicle(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, vehicle_number 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 테이블에서 user_id를 NULL로 변경하고 status를 disabled로 (기록 보존) + await query( + `UPDATE vehicles SET user_id = NULL, status = 'disabled', updated_at = NOW() WHERE user_id = $1`, + [userId] + ); + + // user_info에서 vehicle_number를 NULL로 변경 + await query( + `UPDATE user_info SET vehicle_number = NULL 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: "차량 삭제 중 오류가 발생했습니다.", + }); + } + } + + /** + * POST /api/driver/vehicle + * 새 차량 등록 + */ + static async registerVehicle(req: Request, res: Response): Promise { + try { + const userId = req.user?.userId; + const companyCode = req.user?.companyCode; + + if (!userId) { + res.status(401).json({ + success: false, + message: "인증이 필요합니다.", + }); + return; + } + + const { vehicleNumber, vehicleType, branchName } = req.body; + + if (!vehicleNumber) { + res.status(400).json({ + success: false, + message: "차량번호는 필수입니다.", + }); + return; + } + + // 공차중계 사용자 확인 + const userCheck = await query( + `SELECT signup_type, user_name, cell_phone, vehicle_number, company_code 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; + } + + // 이미 차량이 있는지 확인 + if (userCheck[0].vehicle_number) { + res.status(400).json({ + success: false, + message: "이미 등록된 차량이 있습니다. 먼저 기존 차량을 삭제해주세요.", + }); + return; + } + + // 차량번호 중복 확인 + const duplicateCheck = await query( + `SELECT vehicle_number FROM vehicles WHERE vehicle_number = $1 AND user_id IS NOT NULL`, + [vehicleNumber] + ); + + if (duplicateCheck.length > 0) { + res.status(400).json({ + success: false, + message: "이미 등록된 차량번호입니다.", + }); + return; + } + + const userName = userCheck[0].user_name; + const userPhone = userCheck[0].cell_phone; + // 사용자의 company_code 사용 (req.user에서 가져오거나 DB에서 조회한 값 사용) + const userCompanyCode = companyCode || userCheck[0].company_code; + + // vehicles 테이블에 새 차량 등록 (company_code 포함, status는 'off') + await query( + `INSERT INTO vehicles (vehicle_number, vehicle_type, user_id, driver_name, driver_phone, branch_name, status, company_code, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, 'off', $7, NOW(), NOW())`, + [vehicleNumber, vehicleType || null, userId, userName, userPhone, branchName || null, userCompanyCode] + ); + + // user_info에 vehicle_number 업데이트 + await query( + `UPDATE user_info SET vehicle_number = $1 WHERE user_id = $2`, + [vehicleNumber, userId] + ); + + logger.info(`새 차량 등록 완료: ${userId} -> ${vehicleNumber} (company_code: ${userCompanyCode})`); + + res.status(200).json({ + success: true, + message: "차량이 등록되었습니다.", + }); + } 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..b46cca1b --- /dev/null +++ b/backend-node/src/routes/driverRoutes.ts @@ -0,0 +1,48 @@ +// 공차중계 운전자 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/vehicle + * 차량 삭제 (기록 보존) + */ +router.delete("/vehicle", DriverController.deleteVehicle); + +/** + * POST /api/driver/vehicle + * 새 차량 등록 + */ +router.post("/vehicle", DriverController.registerVehicle); + +/** + * DELETE /api/driver/account + * 회원 탈퇴 + */ +router.delete("/account", DriverController.deleteAccount); + +export default router; + diff --git a/backend-node/src/routes/externalRestApiConnectionRoutes.ts b/backend-node/src/routes/externalRestApiConnectionRoutes.ts index 48813575..14fd17d0 100644 --- a/backend-node/src/routes/externalRestApiConnectionRoutes.ts +++ b/backend-node/src/routes/externalRestApiConnectionRoutes.ts @@ -97,6 +97,8 @@ router.post( const data: ExternalRestApiConnection = { ...req.body, created_by: req.user?.userId || "system", + // 로그인 사용자의 company_code 사용 (프론트에서 안 보내도 자동 설정) + company_code: req.body.company_code || req.user?.companyCode || "*", }; const result = 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/backend-node/src/services/externalDbConnectionService.ts b/backend-node/src/services/externalDbConnectionService.ts index 99164ae1..410e8daf 100644 --- a/backend-node/src/services/externalDbConnectionService.ts +++ b/backend-node/src/services/externalDbConnectionService.ts @@ -28,39 +28,39 @@ export class ExternalDbConnectionService { // 회사별 필터링 (최고 관리자가 아닌 경우 필수) if (userCompanyCode && userCompanyCode !== "*") { - whereConditions.push(`company_code = $${paramIndex++}`); + whereConditions.push(`e.company_code = $${paramIndex++}`); params.push(userCompanyCode); logger.info(`회사별 외부 DB 연결 필터링: ${userCompanyCode}`); } else if (userCompanyCode === "*") { logger.info(`최고 관리자: 모든 외부 DB 연결 조회`); // 필터가 있으면 적용 if (filter.company_code) { - whereConditions.push(`company_code = $${paramIndex++}`); + whereConditions.push(`e.company_code = $${paramIndex++}`); params.push(filter.company_code); } } else { // userCompanyCode가 없는 경우 (하위 호환성) if (filter.company_code) { - whereConditions.push(`company_code = $${paramIndex++}`); + whereConditions.push(`e.company_code = $${paramIndex++}`); params.push(filter.company_code); } } // 필터 조건 적용 if (filter.db_type) { - whereConditions.push(`db_type = $${paramIndex++}`); + whereConditions.push(`e.db_type = $${paramIndex++}`); params.push(filter.db_type); } if (filter.is_active) { - whereConditions.push(`is_active = $${paramIndex++}`); + whereConditions.push(`e.is_active = $${paramIndex++}`); params.push(filter.is_active); } // 검색 조건 적용 (연결명 또는 설명에서 검색) if (filter.search && filter.search.trim()) { whereConditions.push( - `(connection_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})` + `(e.connection_name ILIKE $${paramIndex} OR e.description ILIKE $${paramIndex})` ); params.push(`%${filter.search.trim()}%`); paramIndex++; @@ -72,9 +72,12 @@ export class ExternalDbConnectionService { : ""; const connections = await query( - `SELECT * FROM external_db_connections + `SELECT e.*, + COALESCE(c.company_name, CASE WHEN e.company_code = '*' THEN '전체' ELSE e.company_code END) AS company_name + FROM external_db_connections e + LEFT JOIN company_mng c ON e.company_code = c.company_code ${whereClause} - ORDER BY is_active DESC, connection_name ASC`, + ORDER BY e.is_active DESC, e.connection_name ASC`, params ); diff --git a/backend-node/src/services/externalRestApiConnectionService.ts b/backend-node/src/services/externalRestApiConnectionService.ts index 89096fbb..2632a6e6 100644 --- a/backend-node/src/services/externalRestApiConnectionService.ts +++ b/backend-node/src/services/externalRestApiConnectionService.ts @@ -31,15 +31,17 @@ export class ExternalRestApiConnectionService { try { let query = ` SELECT - id, connection_name, description, base_url, endpoint_path, default_headers, - default_method, + e.id, e.connection_name, e.description, e.base_url, e.endpoint_path, e.default_headers, + e.default_method, -- DB 스키마의 컬럼명은 default_request_body 기준이고 -- 코드에서는 default_body 필드로 사용하기 위해 alias 처리 - default_request_body AS default_body, - auth_type, auth_config, timeout, retry_count, retry_delay, - company_code, is_active, created_date, created_by, - updated_date, updated_by, last_test_date, last_test_result, last_test_message - FROM external_rest_api_connections + e.default_request_body AS default_body, + e.auth_type, e.auth_config, e.timeout, e.retry_count, e.retry_delay, + e.company_code, e.is_active, e.created_date, e.created_by, + e.updated_date, e.updated_by, e.last_test_date, e.last_test_result, e.last_test_message, + COALESCE(c.company_name, CASE WHEN e.company_code = '*' THEN '전체' ELSE e.company_code END) AS company_name + FROM external_rest_api_connections e + LEFT JOIN company_mng c ON e.company_code = c.company_code WHERE 1=1 `; @@ -48,7 +50,7 @@ export class ExternalRestApiConnectionService { // 회사별 필터링 (최고 관리자가 아닌 경우 필수) if (userCompanyCode && userCompanyCode !== "*") { - query += ` AND company_code = $${paramIndex}`; + query += ` AND e.company_code = $${paramIndex}`; params.push(userCompanyCode); paramIndex++; logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`); @@ -56,14 +58,14 @@ export class ExternalRestApiConnectionService { logger.info(`최고 관리자: 모든 REST API 연결 조회`); // 필터가 있으면 적용 if (filter.company_code) { - query += ` AND company_code = $${paramIndex}`; + query += ` AND e.company_code = $${paramIndex}`; params.push(filter.company_code); paramIndex++; } } else { // userCompanyCode가 없는 경우 (하위 호환성) if (filter.company_code) { - query += ` AND company_code = $${paramIndex}`; + query += ` AND e.company_code = $${paramIndex}`; params.push(filter.company_code); paramIndex++; } @@ -71,14 +73,14 @@ export class ExternalRestApiConnectionService { // 활성 상태 필터 if (filter.is_active) { - query += ` AND is_active = $${paramIndex}`; + query += ` AND e.is_active = $${paramIndex}`; params.push(filter.is_active); paramIndex++; } // 인증 타입 필터 if (filter.auth_type) { - query += ` AND auth_type = $${paramIndex}`; + query += ` AND e.auth_type = $${paramIndex}`; params.push(filter.auth_type); paramIndex++; } @@ -86,9 +88,9 @@ export class ExternalRestApiConnectionService { // 검색어 필터 (연결명, 설명, URL) if (filter.search) { query += ` AND ( - connection_name ILIKE $${paramIndex} OR - description ILIKE $${paramIndex} OR - base_url ILIKE $${paramIndex} + e.connection_name ILIKE $${paramIndex} OR + e.description ILIKE $${paramIndex} OR + e.base_url ILIKE $${paramIndex} )`; params.push(`%${filter.search}%`); paramIndex++; @@ -233,6 +235,7 @@ export class ExternalRestApiConnectionService { // 디버깅: 저장하려는 데이터 로깅 logger.info(`REST API 연결 생성 요청 데이터:`, { connection_name: data.connection_name, + company_code: data.company_code, default_method: data.default_method, endpoint_path: data.endpoint_path, base_url: data.base_url, diff --git a/frontend/app/(main)/admin/external-connections/page.tsx b/frontend/app/(main)/admin/external-connections/page.tsx index 88754ac4..0ab2fbeb 100644 --- a/frontend/app/(main)/admin/external-connections/page.tsx +++ b/frontend/app/(main)/admin/external-connections/page.tsx @@ -317,6 +317,7 @@ export default function ExternalConnectionsPage() { 연결명 + 회사 DB 타입 호스트:포트 데이터베이스 @@ -333,6 +334,9 @@ export default function ExternalConnectionsPage() {
{connection.connection_name}
+ + {(connection as any).company_name || connection.company_code} + {DB_TYPE_LABELS[connection.db_type] || connection.db_type} diff --git a/frontend/components/admin/RestApiConnectionList.tsx b/frontend/components/admin/RestApiConnectionList.tsx index ad57eb01..8ed5ea58 100644 --- a/frontend/components/admin/RestApiConnectionList.tsx +++ b/frontend/components/admin/RestApiConnectionList.tsx @@ -284,6 +284,7 @@ export function RestApiConnectionList() { 연결명 + 회사 기본 URL 인증 타입 헤더 수 @@ -308,6 +309,9 @@ export function RestApiConnectionList() { )} + + {(connection as any).company_name || connection.company_code} +
{connection.base_url} diff --git a/frontend/components/admin/RestApiConnectionModal.tsx b/frontend/components/admin/RestApiConnectionModal.tsx index aa7d79d8..3de34800 100644 --- a/frontend/components/admin/RestApiConnectionModal.tsx +++ b/frontend/components/admin/RestApiConnectionModal.tsx @@ -232,7 +232,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }: timeout, retry_count: retryCount, retry_delay: retryDelay, - company_code: "*", + // company_code는 백엔드에서 로그인 사용자의 company_code로 자동 설정 is_active: isActive ? "Y" : "N", }; diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index d26ac0b7..b9346ba7 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -545,8 +545,8 @@ export function DashboardViewer({ {/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
- {/* 다운로드 버튼 */} -
+ {/* 다운로드 버튼 - 비활성화 */} + {/*
+
*/}
- {/* 다운로드 버튼 */} -
+ {/* 다운로드 버튼 - 비활성화 */} + {/*
+
*/}
{sortedElements.map((element) => ( diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index faba6df5..0d23a88e 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -245,6 +245,21 @@ function AppLayoutInner({ children }: AppLayoutProps) { selectImage, removeImage, saveProfile, + // 운전자 관련 + isDriver, + hasVehicle, + driverInfo, + driverFormData, + updateDriverFormData, + handleDriverStatusChange, + handleDriverAccountDelete, + handleDeleteVehicle, + openVehicleRegisterModal, + closeVehicleRegisterModal, + isVehicleRegisterModalOpen, + newVehicleData, + updateNewVehicleData, + handleRegisterVehicle, } = useProfile(user, refreshUserData, refreshMenus); // 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려) @@ -564,6 +579,20 @@ function AppLayoutInner({ children }: AppLayoutProps) { isSaving={isSaving} departments={departments} alertModal={alertModal} + isDriver={isDriver} + hasVehicle={hasVehicle} + driverInfo={driverInfo} + driverFormData={driverFormData} + onDriverFormChange={updateDriverFormData} + onDriverStatusChange={handleDriverStatusChange} + onDriverAccountDelete={handleDriverAccountDelete} + onDeleteVehicle={handleDeleteVehicle} + onOpenVehicleRegisterModal={openVehicleRegisterModal} + isVehicleRegisterModalOpen={isVehicleRegisterModalOpen} + newVehicleData={newVehicleData} + onCloseVehicleRegisterModal={closeVehicleRegisterModal} + onNewVehicleDataChange={updateNewVehicleData} + onRegisterVehicle={handleRegisterVehicle} onClose={closeProfileModal} onFormChange={updateFormData} onImageSelect={selectImage} diff --git a/frontend/components/layout/ProfileModal.tsx b/frontend/components/layout/ProfileModal.tsx index 9dce16a0..e79d3357 100644 --- a/frontend/components/layout/ProfileModal.tsx +++ b/frontend/components/layout/ProfileModal.tsx @@ -11,8 +11,20 @@ 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, Plus, Trash2 } from "lucide-react"; import { ProfileFormData } from "@/types/profile"; +import { Separator } from "@/components/ui/separator"; +import { VehicleRegisterData } from "@/lib/api/driver"; + +// 운전자 정보 타입 +export interface DriverInfo { + vehicleNumber: string; + vehicleType: string | null; + licenseNumber: string; + phoneNumber: string; + vehicleStatus: string | null; + branchName: string | null; +} // 알림 모달 컴포넌트 interface AlertModalProps { @@ -54,6 +66,15 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod ); } +// 운전자 폼 데이터 타입 +export interface DriverFormData { + vehicleNumber: string; + vehicleType: string; + licenseNumber: string; + phoneNumber: string; + branchName: string; +} + interface ProfileModalProps { isOpen: boolean; user: any; @@ -70,6 +91,23 @@ interface ProfileModalProps { message: string; type: "success" | "error" | "info"; }; + // 운전자 관련 props (선택적) + isDriver?: boolean; + hasVehicle?: boolean; + driverInfo?: DriverInfo | null; + driverFormData?: DriverFormData; + onDriverFormChange?: (field: keyof DriverFormData, value: string) => void; + onDriverStatusChange?: (status: "off" | "maintenance") => void; + onDriverAccountDelete?: () => void; + // 차량 삭제/등록 관련 props + onDeleteVehicle?: () => void; + onOpenVehicleRegisterModal?: () => void; + // 새 차량 등록 모달 관련 props + isVehicleRegisterModalOpen?: boolean; + newVehicleData?: VehicleRegisterData; + onCloseVehicleRegisterModal?: () => void; + onNewVehicleDataChange?: (field: keyof VehicleRegisterData, value: string) => void; + onRegisterVehicle?: () => void; onClose: () => void; onFormChange: (field: keyof ProfileFormData, value: string) => void; onImageSelect: (event: React.ChangeEvent) => void; @@ -89,6 +127,20 @@ export function ProfileModal({ isSaving, departments, alertModal, + isDriver = false, + hasVehicle = false, + driverInfo, + driverFormData, + onDriverFormChange, + onDriverStatusChange, + onDriverAccountDelete, + onDeleteVehicle, + onOpenVehicleRegisterModal, + isVehicleRegisterModalOpen = false, + newVehicleData, + onCloseVehicleRegisterModal, + onNewVehicleDataChange, + onRegisterVehicle, onClose, onFormChange, onImageSelect, @@ -96,6 +148,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 +301,152 @@ export function ProfileModal({
+ + {/* 운전자 정보 섹션 (공차중계 사용자만) */} + {isDriver && ( + <> + +
+
+
+ +

차량/운전자 정보

+
+ {/* 차량 유무에 따른 버튼 표시 */} + {hasVehicle ? ( + + ) : ( + + )} +
+ + {/* 운전자 정보 (항상 수정 가능) */} + {driverFormData && onDriverFormChange && ( + <> + {/* 차량 정보 - 차량이 있을 때만 수정 가능 */} + {hasVehicle ? ( +
+
+ + 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" + /> +
+
+ +
+ + onDriverFormChange("branchName", e.target.value)} + placeholder="서울 본점" + /> +
+ + {/* 차량 상태 - 차량이 있을 때만 표시 */} + {hasVehicle && driverInfo && onDriverStatusChange && ( +
+ +
+ + {getStatusLabel(driverInfo.vehicleStatus)} + +
+ + +
+
+

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

+
+ )} + + )} +
+ + )}
@@ -255,6 +468,50 @@ export function ProfileModal({ message={alertModal.message} type={alertModal.type} /> + + {/* 새 차량 등록 모달 */} + {isVehicleRegisterModalOpen && newVehicleData && onNewVehicleDataChange && onRegisterVehicle && onCloseVehicleRegisterModal && ( + + + + 새 차량 등록 + + 새로운 차량 정보를 입력해주세요. + + + +
+
+ + onNewVehicleDataChange("vehicleNumber", e.target.value)} + placeholder="12가1234" + /> +
+
+ + onNewVehicleDataChange("vehicleType", e.target.value)} + placeholder="1톤 카고" + /> +
+
+ + + + + +
+
+ )} ); } diff --git a/frontend/hooks/useProfile.ts b/frontend/hooks/useProfile.ts index 0498eb13..f96593f5 100644 --- a/frontend/hooks/useProfile.ts +++ b/frontend/hooks/useProfile.ts @@ -4,6 +4,17 @@ 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, + deleteDriverVehicle, + registerDriverVehicle, + DriverProfile, + VehicleRegisterData, +} from "@/lib/api/driver"; +import { DriverInfo, DriverFormData } from "@/components/layout/ProfileModal"; // 알림 모달 상태 타입 interface AlertModalState { @@ -48,6 +59,26 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr }> >([]); + // 운전자 정보 상태 + const [isDriver, setIsDriver] = useState(false); + const [hasVehicle, setHasVehicle] = useState(false); // 차량 보유 여부 + const [driverInfo, setDriverInfo] = useState(null); + const [driverFormData, setDriverFormData] = useState({ + vehicleNumber: "", + vehicleType: "", + licenseNumber: "", + phoneNumber: "", + branchName: "", + }); + + // 새 차량 등록 모달 상태 + const [isVehicleRegisterModalOpen, setIsVehicleRegisterModalOpen] = useState(false); + const [newVehicleData, setNewVehicleData] = useState({ + vehicleNumber: "", + vehicleType: "", + branchName: "", + }); + // 알림 모달 표시 함수 const showAlert = useCallback((title: string, message: string, type: "success" | "error" | "info" = "info") => { setAlertModal({ @@ -75,6 +106,41 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr } }, []); + // 운전자 정보 로드 함수 + const loadDriverInfo = useCallback(async () => { + try { + const response = await getDriverProfile(); + if (response.success && response.data) { + setIsDriver(true); + // 차량 보유 여부 확인 + const vehicleExists = !!response.data.vehicleNumber; + setHasVehicle(vehicleExists); + setDriverInfo({ + vehicleNumber: response.data.vehicleNumber, + vehicleType: response.data.vehicleType, + licenseNumber: response.data.licenseNumber, + phoneNumber: response.data.phoneNumber, + vehicleStatus: response.data.vehicleStatus, + branchName: response.data.branchName, + }); + setDriverFormData({ + vehicleNumber: response.data.vehicleNumber || "", + vehicleType: response.data.vehicleType || "", + licenseNumber: response.data.licenseNumber || "", + phoneNumber: response.data.phoneNumber || "", + branchName: response.data.branchName || "", + }); + } else { + setIsDriver(false); + setHasVehicle(false); + setDriverInfo(null); + } + } catch (error) { + console.error("운전자 정보 로드 실패:", error); + setIsDriver(false); + } + }, []); + /** * 프로필 모달 열기 */ @@ -82,6 +148,8 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr if (user) { // 부서 목록 로드 loadDepartments(); + // 운전자 정보 로드 + loadDriverInfo(); setModalState((prev) => ({ ...prev, @@ -98,7 +166,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr isSaving: false, })); } - }, [user, loadDepartments]); + }, [user, loadDepartments, loadDriverInfo]); /** * 프로필 모달 닫기 @@ -125,6 +193,138 @@ 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]); + + /** + * 차량 삭제 + */ + const handleDeleteVehicle = useCallback(async () => { + if (!confirm("이 차량을 더 이상 사용하지 않습니까?\n차량 정보가 삭제됩니다.")) { + return; + } + + try { + const response = await deleteDriverVehicle(); + if (response.success) { + showAlert("삭제 완료", "차량이 삭제되었습니다.", "success"); + // 운전자 정보 새로고침 + await loadDriverInfo(); + } else { + showAlert("삭제 실패", response.message || "차량 삭제에 실패했습니다.", "error"); + } + } catch (error) { + console.error("차량 삭제 실패:", error); + showAlert("오류", "차량 삭제 중 오류가 발생했습니다.", "error"); + } + }, [showAlert, loadDriverInfo]); + + /** + * 새 차량 등록 모달 열기 + */ + const openVehicleRegisterModal = useCallback(() => { + setNewVehicleData({ + vehicleNumber: "", + vehicleType: "", + branchName: driverFormData.branchName || "", // 기존 소속 지점 유지 + }); + setIsVehicleRegisterModalOpen(true); + }, [driverFormData.branchName]); + + /** + * 새 차량 등록 모달 닫기 + */ + const closeVehicleRegisterModal = useCallback(() => { + setIsVehicleRegisterModalOpen(false); + }, []); + + /** + * 새 차량 데이터 변경 + */ + const updateNewVehicleData = useCallback((field: keyof VehicleRegisterData, value: string) => { + setNewVehicleData((prev) => ({ + ...prev, + [field]: value, + })); + }, []); + + /** + * 새 차량 등록 처리 + */ + const handleRegisterVehicle = useCallback(async () => { + if (!newVehicleData.vehicleNumber) { + showAlert("입력 오류", "차량번호는 필수입니다.", "error"); + return; + } + + try { + const response = await registerDriverVehicle(newVehicleData); + if (response.success) { + showAlert("등록 완료", "차량이 등록되었습니다.", "success"); + setIsVehicleRegisterModalOpen(false); + // 운전자 정보 새로고침 + await loadDriverInfo(); + } else { + showAlert("등록 실패", response.message || "차량 등록에 실패했습니다.", "error"); + } + } catch (error) { + console.error("차량 등록 실패:", error); + showAlert("오류", "차량 등록 중 오류가 발생했습니다.", "error"); + } + }, [newVehicleData, showAlert, loadDriverInfo]); + /** * 이미지 선택 처리 */ @@ -229,6 +429,22 @@ 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, + branchName: driverFormData.branchName, + }); + + 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 +481,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 +495,16 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr alertModal, closeAlert, + // 운전자 관련 상태 + isDriver, + hasVehicle, + driverInfo, + driverFormData, + + // 새 차량 등록 모달 상태 + isVehicleRegisterModalOpen, + newVehicleData, + // 액션 openProfileModal, closeProfileModal, @@ -286,5 +512,15 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr selectImage, removeImage, saveProfile, + + // 운전자 관련 액션 + updateDriverFormData, + handleDriverStatusChange, + handleDriverAccountDelete, + handleDeleteVehicle, + openVehicleRegisterModal, + closeVehicleRegisterModal, + updateNewVehicleData, + handleRegisterVehicle, }; }; diff --git a/frontend/lib/api/driver.ts b/frontend/lib/api/driver.ts new file mode 100644 index 00000000..8074660a --- /dev/null +++ b/frontend/lib/api/driver.ts @@ -0,0 +1,135 @@ +// 공차중계 운전자 API +import { apiClient } from "./client"; + +export interface DriverProfile { + userId: string; + userName: string; + phoneNumber: string; + licenseNumber: string; + vehicleNumber: string; + vehicleType: string | null; + vehicleStatus: string | null; + branchName: string | null; +} + +export interface DriverProfileUpdateData { + userName?: string; + phoneNumber?: string; + licenseNumber?: string; + vehicleNumber?: string; + vehicleType?: string; + branchName?: 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 deleteDriverVehicle(): Promise<{ + success: boolean; + message?: string; +}> { + try { + const response = await apiClient.delete("/driver/vehicle"); + return response.data; + } catch (error: any) { + return { + success: false, + message: error.response?.data?.message || "차량 삭제에 실패했습니다.", + }; + } +} + +/** + * 새 차량 등록 + */ +export interface VehicleRegisterData { + vehicleNumber: string; + vehicleType?: string; + branchName?: string; +} + +export async function registerDriverVehicle( + data: VehicleRegisterData +): Promise<{ success: boolean; message?: string }> { + try { + const response = await apiClient.post("/driver/vehicle", data); + 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 || "회원 탈퇴에 실패했습니다.", + }; + } +} +