Merge pull request 'common/feat/dashboard-map' (#240) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/240
This commit is contained in:
commit
cb9c90fcdb
|
|
@ -73,6 +73,7 @@ import entitySearchRoutes from "./routes/entitySearchRoutes"; // 엔티티 검
|
||||||
import orderRoutes from "./routes/orderRoutes"; // 수주 관리
|
import orderRoutes from "./routes/orderRoutes"; // 수주 관리
|
||||||
import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
|
import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
|
||||||
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
|
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
|
||||||
|
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
|
||||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||||
|
|
@ -238,6 +239,7 @@ app.use("/api/code-merge", codeMergeRoutes); // 코드 병합
|
||||||
app.use("/api/numbering-rules", numberingRuleRoutes); // 채번 규칙 관리
|
app.use("/api/numbering-rules", numberingRuleRoutes); // 채번 규칙 관리
|
||||||
app.use("/api/entity-search", entitySearchRoutes); // 엔티티 검색
|
app.use("/api/entity-search", entitySearchRoutes); // 엔티티 검색
|
||||||
app.use("/api/orders", orderRoutes); // 수주 관리
|
app.use("/api/orders", orderRoutes); // 수주 관리
|
||||||
|
app.use("/api/driver", driverRoutes); // 공차중계 운전자 관리
|
||||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||||
|
|
|
||||||
|
|
@ -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 : "알 수 없는 오류",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<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, 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<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,
|
||||||
|
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<void> {
|
||||||
|
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<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),
|
||||||
|
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<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/vehicle
|
||||||
|
* 차량 삭제 (user_id = NULL 처리, 기록 보존)
|
||||||
|
*/
|
||||||
|
static async deleteVehicle(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, 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<void> {
|
||||||
|
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<any>(
|
||||||
|
`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<any>(
|
||||||
|
`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<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: "회원 탈퇴 처리 중 오류가 발생했습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -41,4 +41,10 @@ router.post("/logout", AuthController.logout);
|
||||||
*/
|
*/
|
||||||
router.post("/refresh", AuthController.refreshToken);
|
router.post("/refresh", AuthController.refreshToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/signup
|
||||||
|
* 공차중계 회원가입 API
|
||||||
|
*/
|
||||||
|
router.post("/signup", AuthController.signup);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -97,6 +97,8 @@ router.post(
|
||||||
const data: ExternalRestApiConnection = {
|
const data: ExternalRestApiConnection = {
|
||||||
...req.body,
|
...req.body,
|
||||||
created_by: req.user?.userId || "system",
|
created_by: req.user?.userId || "system",
|
||||||
|
// 로그인 사용자의 company_code 사용 (프론트에서 안 보내도 자동 설정)
|
||||||
|
company_code: req.body.company_code || req.user?.companyCode || "*",
|
||||||
};
|
};
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
|
|
|
||||||
|
|
@ -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 || "회원가입 중 오류가 발생했습니다.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,39 +28,39 @@ export class ExternalDbConnectionService {
|
||||||
|
|
||||||
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
|
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
|
||||||
if (userCompanyCode && userCompanyCode !== "*") {
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
whereConditions.push(`company_code = $${paramIndex++}`);
|
whereConditions.push(`e.company_code = $${paramIndex++}`);
|
||||||
params.push(userCompanyCode);
|
params.push(userCompanyCode);
|
||||||
logger.info(`회사별 외부 DB 연결 필터링: ${userCompanyCode}`);
|
logger.info(`회사별 외부 DB 연결 필터링: ${userCompanyCode}`);
|
||||||
} else if (userCompanyCode === "*") {
|
} else if (userCompanyCode === "*") {
|
||||||
logger.info(`최고 관리자: 모든 외부 DB 연결 조회`);
|
logger.info(`최고 관리자: 모든 외부 DB 연결 조회`);
|
||||||
// 필터가 있으면 적용
|
// 필터가 있으면 적용
|
||||||
if (filter.company_code) {
|
if (filter.company_code) {
|
||||||
whereConditions.push(`company_code = $${paramIndex++}`);
|
whereConditions.push(`e.company_code = $${paramIndex++}`);
|
||||||
params.push(filter.company_code);
|
params.push(filter.company_code);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// userCompanyCode가 없는 경우 (하위 호환성)
|
// userCompanyCode가 없는 경우 (하위 호환성)
|
||||||
if (filter.company_code) {
|
if (filter.company_code) {
|
||||||
whereConditions.push(`company_code = $${paramIndex++}`);
|
whereConditions.push(`e.company_code = $${paramIndex++}`);
|
||||||
params.push(filter.company_code);
|
params.push(filter.company_code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필터 조건 적용
|
// 필터 조건 적용
|
||||||
if (filter.db_type) {
|
if (filter.db_type) {
|
||||||
whereConditions.push(`db_type = $${paramIndex++}`);
|
whereConditions.push(`e.db_type = $${paramIndex++}`);
|
||||||
params.push(filter.db_type);
|
params.push(filter.db_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.is_active) {
|
if (filter.is_active) {
|
||||||
whereConditions.push(`is_active = $${paramIndex++}`);
|
whereConditions.push(`e.is_active = $${paramIndex++}`);
|
||||||
params.push(filter.is_active);
|
params.push(filter.is_active);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검색 조건 적용 (연결명 또는 설명에서 검색)
|
// 검색 조건 적용 (연결명 또는 설명에서 검색)
|
||||||
if (filter.search && filter.search.trim()) {
|
if (filter.search && filter.search.trim()) {
|
||||||
whereConditions.push(
|
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()}%`);
|
params.push(`%${filter.search.trim()}%`);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
|
|
@ -72,9 +72,12 @@ export class ExternalDbConnectionService {
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const connections = await query<any>(
|
const connections = await query<any>(
|
||||||
`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}
|
${whereClause}
|
||||||
ORDER BY is_active DESC, connection_name ASC`,
|
ORDER BY e.is_active DESC, e.connection_name ASC`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,15 +31,17 @@ export class ExternalRestApiConnectionService {
|
||||||
try {
|
try {
|
||||||
let query = `
|
let query = `
|
||||||
SELECT
|
SELECT
|
||||||
id, connection_name, description, base_url, endpoint_path, default_headers,
|
e.id, e.connection_name, e.description, e.base_url, e.endpoint_path, e.default_headers,
|
||||||
default_method,
|
e.default_method,
|
||||||
-- DB 스키마의 컬럼명은 default_request_body 기준이고
|
-- DB 스키마의 컬럼명은 default_request_body 기준이고
|
||||||
-- 코드에서는 default_body 필드로 사용하기 위해 alias 처리
|
-- 코드에서는 default_body 필드로 사용하기 위해 alias 처리
|
||||||
default_request_body AS default_body,
|
e.default_request_body AS default_body,
|
||||||
auth_type, auth_config, timeout, retry_count, retry_delay,
|
e.auth_type, e.auth_config, e.timeout, e.retry_count, e.retry_delay,
|
||||||
company_code, is_active, created_date, created_by,
|
e.company_code, e.is_active, e.created_date, e.created_by,
|
||||||
updated_date, updated_by, last_test_date, last_test_result, last_test_message
|
e.updated_date, e.updated_by, e.last_test_date, e.last_test_result, e.last_test_message,
|
||||||
FROM external_rest_api_connections
|
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
|
WHERE 1=1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -48,7 +50,7 @@ export class ExternalRestApiConnectionService {
|
||||||
|
|
||||||
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
|
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
|
||||||
if (userCompanyCode && userCompanyCode !== "*") {
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
query += ` AND company_code = $${paramIndex}`;
|
query += ` AND e.company_code = $${paramIndex}`;
|
||||||
params.push(userCompanyCode);
|
params.push(userCompanyCode);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`);
|
logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`);
|
||||||
|
|
@ -56,14 +58,14 @@ export class ExternalRestApiConnectionService {
|
||||||
logger.info(`최고 관리자: 모든 REST API 연결 조회`);
|
logger.info(`최고 관리자: 모든 REST API 연결 조회`);
|
||||||
// 필터가 있으면 적용
|
// 필터가 있으면 적용
|
||||||
if (filter.company_code) {
|
if (filter.company_code) {
|
||||||
query += ` AND company_code = $${paramIndex}`;
|
query += ` AND e.company_code = $${paramIndex}`;
|
||||||
params.push(filter.company_code);
|
params.push(filter.company_code);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// userCompanyCode가 없는 경우 (하위 호환성)
|
// userCompanyCode가 없는 경우 (하위 호환성)
|
||||||
if (filter.company_code) {
|
if (filter.company_code) {
|
||||||
query += ` AND company_code = $${paramIndex}`;
|
query += ` AND e.company_code = $${paramIndex}`;
|
||||||
params.push(filter.company_code);
|
params.push(filter.company_code);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
@ -71,14 +73,14 @@ export class ExternalRestApiConnectionService {
|
||||||
|
|
||||||
// 활성 상태 필터
|
// 활성 상태 필터
|
||||||
if (filter.is_active) {
|
if (filter.is_active) {
|
||||||
query += ` AND is_active = $${paramIndex}`;
|
query += ` AND e.is_active = $${paramIndex}`;
|
||||||
params.push(filter.is_active);
|
params.push(filter.is_active);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 인증 타입 필터
|
// 인증 타입 필터
|
||||||
if (filter.auth_type) {
|
if (filter.auth_type) {
|
||||||
query += ` AND auth_type = $${paramIndex}`;
|
query += ` AND e.auth_type = $${paramIndex}`;
|
||||||
params.push(filter.auth_type);
|
params.push(filter.auth_type);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
@ -86,9 +88,9 @@ export class ExternalRestApiConnectionService {
|
||||||
// 검색어 필터 (연결명, 설명, URL)
|
// 검색어 필터 (연결명, 설명, URL)
|
||||||
if (filter.search) {
|
if (filter.search) {
|
||||||
query += ` AND (
|
query += ` AND (
|
||||||
connection_name ILIKE $${paramIndex} OR
|
e.connection_name ILIKE $${paramIndex} OR
|
||||||
description ILIKE $${paramIndex} OR
|
e.description ILIKE $${paramIndex} OR
|
||||||
base_url ILIKE $${paramIndex}
|
e.base_url ILIKE $${paramIndex}
|
||||||
)`;
|
)`;
|
||||||
params.push(`%${filter.search}%`);
|
params.push(`%${filter.search}%`);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
|
|
@ -233,6 +235,7 @@ export class ExternalRestApiConnectionService {
|
||||||
// 디버깅: 저장하려는 데이터 로깅
|
// 디버깅: 저장하려는 데이터 로깅
|
||||||
logger.info(`REST API 연결 생성 요청 데이터:`, {
|
logger.info(`REST API 연결 생성 요청 데이터:`, {
|
||||||
connection_name: data.connection_name,
|
connection_name: data.connection_name,
|
||||||
|
company_code: data.company_code,
|
||||||
default_method: data.default_method,
|
default_method: data.default_method,
|
||||||
endpoint_path: data.endpoint_path,
|
endpoint_path: data.endpoint_path,
|
||||||
base_url: data.base_url,
|
base_url: data.base_url,
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,7 @@ export default function ExternalConnectionsPage() {
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-background">
|
<TableRow className="bg-background">
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
||||||
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">회사</TableHead>
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">DB 타입</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">DB 타입</TableHead>
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">호스트:포트</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">호스트:포트</TableHead>
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">데이터베이스</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">데이터베이스</TableHead>
|
||||||
|
|
@ -333,6 +334,9 @@ export default function ExternalConnectionsPage() {
|
||||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="font-medium">{connection.connection_name}</div>
|
<div className="font-medium">{connection.connection_name}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
|
{(connection as any).company_name || connection.company_code}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{DB_TYPE_LABELS[connection.db_type] || connection.db_type}
|
{DB_TYPE_LABELS[connection.db_type] || connection.db_type}
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,7 @@ export function RestApiConnectionList() {
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-background">
|
<TableRow className="bg-background">
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
||||||
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">회사</TableHead>
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">기본 URL</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">기본 URL</TableHead>
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">인증 타입</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">인증 타입</TableHead>
|
||||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">헤더 수</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">헤더 수</TableHead>
|
||||||
|
|
@ -308,6 +309,9 @@ export function RestApiConnectionList() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
|
{(connection as any).company_name || connection.company_code}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||||
<div className="max-w-[300px] truncate" title={connection.base_url}>
|
<div className="max-w-[300px] truncate" title={connection.base_url}>
|
||||||
{connection.base_url}
|
{connection.base_url}
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
timeout,
|
timeout,
|
||||||
retry_count: retryCount,
|
retry_count: retryCount,
|
||||||
retry_delay: retryDelay,
|
retry_delay: retryDelay,
|
||||||
company_code: "*",
|
// company_code는 백엔드에서 로그인 사용자의 company_code로 자동 설정
|
||||||
is_active: isActive ? "Y" : "N",
|
is_active: isActive ? "Y" : "N",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -545,8 +545,8 @@ export function DashboardViewer({
|
||||||
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
|
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
|
||||||
<div className="bg-muted hidden min-h-screen py-8 lg:block" style={{ backgroundColor }}>
|
<div className="bg-muted hidden min-h-screen py-8 lg:block" style={{ backgroundColor }}>
|
||||||
<div className="mx-auto px-4" style={{ width: "100%", maxWidth: "none" }}>
|
<div className="mx-auto px-4" style={{ width: "100%", maxWidth: "none" }}>
|
||||||
{/* 다운로드 버튼 */}
|
{/* 다운로드 버튼 - 비활성화 */}
|
||||||
<div className="mb-4 flex justify-end">
|
{/* <div className="mb-4 flex justify-end">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
|
@ -559,7 +559,7 @@ export function DashboardViewer({
|
||||||
<DropdownMenuItem onClick={() => handleDownload("pdf")}>PDF 문서로 저장</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => handleDownload("pdf")}>PDF 문서로 저장</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="dashboard-viewer-canvas relative rounded-lg"
|
className="dashboard-viewer-canvas relative rounded-lg"
|
||||||
|
|
@ -588,8 +588,8 @@ export function DashboardViewer({
|
||||||
{/* 태블릿 이하: 반응형 세로 정렬 */}
|
{/* 태블릿 이하: 반응형 세로 정렬 */}
|
||||||
<div className="bg-muted block min-h-screen p-4 lg:hidden" style={{ backgroundColor }}>
|
<div className="bg-muted block min-h-screen p-4 lg:hidden" style={{ backgroundColor }}>
|
||||||
<div className="mx-auto max-w-3xl space-y-4">
|
<div className="mx-auto max-w-3xl space-y-4">
|
||||||
{/* 다운로드 버튼 */}
|
{/* 다운로드 버튼 - 비활성화 */}
|
||||||
<div className="flex justify-end">
|
{/* <div className="flex justify-end">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
|
@ -602,7 +602,7 @@ export function DashboardViewer({
|
||||||
<DropdownMenuItem onClick={() => handleDownload("pdf")}>PDF 문서로 저장</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => handleDownload("pdf")}>PDF 문서로 저장</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="dashboard-viewer-canvas">
|
<div className="dashboard-viewer-canvas">
|
||||||
{sortedElements.map((element) => (
|
{sortedElements.map((element) => (
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,21 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
selectImage,
|
selectImage,
|
||||||
removeImage,
|
removeImage,
|
||||||
saveProfile,
|
saveProfile,
|
||||||
|
// 운전자 관련
|
||||||
|
isDriver,
|
||||||
|
hasVehicle,
|
||||||
|
driverInfo,
|
||||||
|
driverFormData,
|
||||||
|
updateDriverFormData,
|
||||||
|
handleDriverStatusChange,
|
||||||
|
handleDriverAccountDelete,
|
||||||
|
handleDeleteVehicle,
|
||||||
|
openVehicleRegisterModal,
|
||||||
|
closeVehicleRegisterModal,
|
||||||
|
isVehicleRegisterModalOpen,
|
||||||
|
newVehicleData,
|
||||||
|
updateNewVehicleData,
|
||||||
|
handleRegisterVehicle,
|
||||||
} = useProfile(user, refreshUserData, refreshMenus);
|
} = useProfile(user, refreshUserData, refreshMenus);
|
||||||
|
|
||||||
// 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려)
|
// 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려)
|
||||||
|
|
@ -564,6 +579,20 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
departments={departments}
|
departments={departments}
|
||||||
alertModal={alertModal}
|
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}
|
onClose={closeProfileModal}
|
||||||
onFormChange={updateFormData}
|
onFormChange={updateFormData}
|
||||||
onImageSelect={selectImage}
|
onImageSelect={selectImage}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,20 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
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 { 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 {
|
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 {
|
interface ProfileModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
user: any;
|
user: any;
|
||||||
|
|
@ -70,6 +91,23 @@ interface ProfileModalProps {
|
||||||
message: string;
|
message: string;
|
||||||
type: "success" | "error" | "info";
|
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;
|
onClose: () => void;
|
||||||
onFormChange: (field: keyof ProfileFormData, value: string) => void;
|
onFormChange: (field: keyof ProfileFormData, value: string) => void;
|
||||||
onImageSelect: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onImageSelect: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
|
@ -89,6 +127,20 @@ export function ProfileModal({
|
||||||
isSaving,
|
isSaving,
|
||||||
departments,
|
departments,
|
||||||
alertModal,
|
alertModal,
|
||||||
|
isDriver = false,
|
||||||
|
hasVehicle = false,
|
||||||
|
driverInfo,
|
||||||
|
driverFormData,
|
||||||
|
onDriverFormChange,
|
||||||
|
onDriverStatusChange,
|
||||||
|
onDriverAccountDelete,
|
||||||
|
onDeleteVehicle,
|
||||||
|
onOpenVehicleRegisterModal,
|
||||||
|
isVehicleRegisterModalOpen = false,
|
||||||
|
newVehicleData,
|
||||||
|
onCloseVehicleRegisterModal,
|
||||||
|
onNewVehicleDataChange,
|
||||||
|
onRegisterVehicle,
|
||||||
onClose,
|
onClose,
|
||||||
onFormChange,
|
onFormChange,
|
||||||
onImageSelect,
|
onImageSelect,
|
||||||
|
|
@ -96,6 +148,21 @@ export function ProfileModal({
|
||||||
onSave,
|
onSave,
|
||||||
onAlertClose,
|
onAlertClose,
|
||||||
}: ProfileModalProps) {
|
}: ProfileModalProps) {
|
||||||
|
// 차량 상태 한글 변환
|
||||||
|
const getStatusLabel = (status: string | null) => {
|
||||||
|
switch (status) {
|
||||||
|
case "off":
|
||||||
|
return "대기";
|
||||||
|
case "active":
|
||||||
|
return "운행중";
|
||||||
|
case "inactive":
|
||||||
|
return "공차";
|
||||||
|
case "maintenance":
|
||||||
|
return "정비";
|
||||||
|
default:
|
||||||
|
return status || "-";
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
|
@ -234,6 +301,152 @@ export function ProfileModal({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 운전자 정보 섹션 (공차중계 사용자만) */}
|
||||||
|
{isDriver && (
|
||||||
|
<>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Car className="h-5 w-5 text-primary" />
|
||||||
|
<h3 className="text-sm font-semibold">차량/운전자 정보</h3>
|
||||||
|
</div>
|
||||||
|
{/* 차량 유무에 따른 버튼 표시 */}
|
||||||
|
{hasVehicle ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={onDeleteVehicle}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
차량 삭제
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={onOpenVehicleRegisterModal}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
새 차량 등록
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 운전자 정보 (항상 수정 가능) */}
|
||||||
|
{driverFormData && onDriverFormChange && (
|
||||||
|
<>
|
||||||
|
{/* 차량 정보 - 차량이 있을 때만 수정 가능 */}
|
||||||
|
{hasVehicle ? (
|
||||||
|
<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="text-center py-4 text-muted-foreground border rounded-md bg-muted/30">
|
||||||
|
<Car className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
||||||
|
<p className="text-sm">등록된 차량이 없습니다.</p>
|
||||||
|
<p className="text-xs mt-1">새 차량 등록 버튼을 눌러 차량을 등록하세요.</p>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="branchName">소속 지점</Label>
|
||||||
|
<Input
|
||||||
|
id="branchName"
|
||||||
|
value={driverFormData.branchName}
|
||||||
|
onChange={(e) => onDriverFormChange("branchName", e.target.value)}
|
||||||
|
placeholder="서울 본점"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 차량 상태 - 차량이 있을 때만 표시 */}
|
||||||
|
{hasVehicle && 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>
|
</div>
|
||||||
|
|
||||||
<ResizableDialogFooter>
|
<ResizableDialogFooter>
|
||||||
|
|
@ -255,6 +468,50 @@ export function ProfileModal({
|
||||||
message={alertModal.message}
|
message={alertModal.message}
|
||||||
type={alertModal.type}
|
type={alertModal.type}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 새 차량 등록 모달 */}
|
||||||
|
{isVehicleRegisterModalOpen && newVehicleData && onNewVehicleDataChange && onRegisterVehicle && onCloseVehicleRegisterModal && (
|
||||||
|
<ResizableDialog open={isVehicleRegisterModalOpen} onOpenChange={onCloseVehicleRegisterModal}>
|
||||||
|
<ResizableDialogContent className="sm:max-w-[400px]">
|
||||||
|
<ResizableDialogHeader>
|
||||||
|
<ResizableDialogTitle>새 차량 등록</ResizableDialogTitle>
|
||||||
|
<ResizableDialogDescription>
|
||||||
|
새로운 차량 정보를 입력해주세요.
|
||||||
|
</ResizableDialogDescription>
|
||||||
|
</ResizableDialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newVehicleNumber">차량번호 *</Label>
|
||||||
|
<Input
|
||||||
|
id="newVehicleNumber"
|
||||||
|
value={newVehicleData.vehicleNumber}
|
||||||
|
onChange={(e) => onNewVehicleDataChange("vehicleNumber", e.target.value)}
|
||||||
|
placeholder="12가1234"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newVehicleType">차종</Label>
|
||||||
|
<Input
|
||||||
|
id="newVehicleType"
|
||||||
|
value={newVehicleData.vehicleType || ""}
|
||||||
|
onChange={(e) => onNewVehicleDataChange("vehicleType", e.target.value)}
|
||||||
|
placeholder="1톤 카고"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResizableDialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={onCloseVehicleRegisterModal}>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onRegisterVehicle}>
|
||||||
|
등록
|
||||||
|
</Button>
|
||||||
|
</ResizableDialogFooter>
|
||||||
|
</ResizableDialogContent>
|
||||||
|
</ResizableDialog>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,17 @@ import { useState, useCallback, useEffect } from "react";
|
||||||
import { ProfileFormData, ProfileModalState } from "@/types/profile";
|
import { ProfileFormData, ProfileModalState } from "@/types/profile";
|
||||||
import { LAYOUT_CONFIG, MESSAGES } from "@/constants/layout";
|
import { LAYOUT_CONFIG, MESSAGES } from "@/constants/layout";
|
||||||
import { apiCall } from "@/lib/api/client";
|
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 {
|
interface AlertModalState {
|
||||||
|
|
@ -48,6 +59,26 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
// 운전자 정보 상태
|
||||||
|
const [isDriver, setIsDriver] = useState(false);
|
||||||
|
const [hasVehicle, setHasVehicle] = useState(false); // 차량 보유 여부
|
||||||
|
const [driverInfo, setDriverInfo] = useState<DriverInfo | null>(null);
|
||||||
|
const [driverFormData, setDriverFormData] = useState<DriverFormData>({
|
||||||
|
vehicleNumber: "",
|
||||||
|
vehicleType: "",
|
||||||
|
licenseNumber: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
branchName: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 새 차량 등록 모달 상태
|
||||||
|
const [isVehicleRegisterModalOpen, setIsVehicleRegisterModalOpen] = useState(false);
|
||||||
|
const [newVehicleData, setNewVehicleData] = useState<VehicleRegisterData>({
|
||||||
|
vehicleNumber: "",
|
||||||
|
vehicleType: "",
|
||||||
|
branchName: "",
|
||||||
|
});
|
||||||
|
|
||||||
// 알림 모달 표시 함수
|
// 알림 모달 표시 함수
|
||||||
const showAlert = useCallback((title: string, message: string, type: "success" | "error" | "info" = "info") => {
|
const showAlert = useCallback((title: string, message: string, type: "success" | "error" | "info" = "info") => {
|
||||||
setAlertModal({
|
setAlertModal({
|
||||||
|
|
@ -75,6 +106,41 @@ 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);
|
||||||
|
// 차량 보유 여부 확인
|
||||||
|
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<void>, refr
|
||||||
if (user) {
|
if (user) {
|
||||||
// 부서 목록 로드
|
// 부서 목록 로드
|
||||||
loadDepartments();
|
loadDepartments();
|
||||||
|
// 운전자 정보 로드
|
||||||
|
loadDriverInfo();
|
||||||
|
|
||||||
setModalState((prev) => ({
|
setModalState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -98,7 +166,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [user, loadDepartments]);
|
}, [user, loadDepartments, loadDriverInfo]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 프로필 모달 닫기
|
* 프로필 모달 닫기
|
||||||
|
|
@ -125,6 +193,138 @@ 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]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 차량 삭제
|
||||||
|
*/
|
||||||
|
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<void>, refr
|
||||||
// API 호출 (JWT 토큰 자동 포함)
|
// API 호출 (JWT 토큰 자동 포함)
|
||||||
const response = await apiCall("PUT", "/admin/profile", updateData);
|
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) {
|
if (response.success || (response as any).result) {
|
||||||
// locale이 변경된 경우 전역 변수와 localStorage 업데이트
|
// locale이 변경된 경우 전역 변수와 localStorage 업데이트
|
||||||
const localeChanged = modalState.formData.locale && modalState.formData.locale !== user.locale;
|
const localeChanged = modalState.formData.locale && modalState.formData.locale !== user.locale;
|
||||||
|
|
@ -265,7 +481,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
} finally {
|
} finally {
|
||||||
setModalState((prev) => ({ ...prev, isSaving: false }));
|
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 {
|
return {
|
||||||
// 상태
|
// 상태
|
||||||
|
|
@ -279,6 +495,16 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
alertModal,
|
alertModal,
|
||||||
closeAlert,
|
closeAlert,
|
||||||
|
|
||||||
|
// 운전자 관련 상태
|
||||||
|
isDriver,
|
||||||
|
hasVehicle,
|
||||||
|
driverInfo,
|
||||||
|
driverFormData,
|
||||||
|
|
||||||
|
// 새 차량 등록 모달 상태
|
||||||
|
isVehicleRegisterModalOpen,
|
||||||
|
newVehicleData,
|
||||||
|
|
||||||
// 액션
|
// 액션
|
||||||
openProfileModal,
|
openProfileModal,
|
||||||
closeProfileModal,
|
closeProfileModal,
|
||||||
|
|
@ -286,5 +512,15 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
selectImage,
|
selectImage,
|
||||||
removeImage,
|
removeImage,
|
||||||
saveProfile,
|
saveProfile,
|
||||||
|
|
||||||
|
// 운전자 관련 액션
|
||||||
|
updateDriverFormData,
|
||||||
|
handleDriverStatusChange,
|
||||||
|
handleDriverAccountDelete,
|
||||||
|
handleDeleteVehicle,
|
||||||
|
openVehicleRegisterModal,
|
||||||
|
closeVehicleRegisterModal,
|
||||||
|
updateNewVehicleData,
|
||||||
|
handleRegisterVehicle,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 || "회원 탈퇴에 실패했습니다.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue