268 lines
8.2 KiB
TypeScript
268 lines
8.2 KiB
TypeScript
import { logger } from "../utils/logger";
|
|
import { query } from "../database/db";
|
|
|
|
// 환경 변수로 데이터 소스 선택
|
|
const DATA_SOURCE = process.env.MAINTENANCE_DATA_SOURCE || "memory";
|
|
|
|
export interface MaintenanceSchedule {
|
|
id: string;
|
|
vehicleNumber: string;
|
|
vehicleType: string;
|
|
maintenanceType: "정기점검" | "수리" | "타이어교체" | "오일교환" | "기타";
|
|
scheduledDate: string;
|
|
status: "scheduled" | "in_progress" | "completed" | "overdue";
|
|
notes?: string;
|
|
estimatedCost?: number;
|
|
actualCost?: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
startedAt?: string;
|
|
completedAt?: string;
|
|
mechanicName?: string;
|
|
location?: string;
|
|
}
|
|
|
|
// 메모리 목 데이터
|
|
const mockSchedules: MaintenanceSchedule[] = [
|
|
{
|
|
id: "maint-1",
|
|
vehicleNumber: "서울12가3456",
|
|
vehicleType: "1톤 트럭",
|
|
maintenanceType: "정기점검",
|
|
scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
status: "scheduled",
|
|
notes: "6개월 정기점검",
|
|
estimatedCost: 300000,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
location: "본사 정비소",
|
|
},
|
|
{
|
|
id: "maint-2",
|
|
vehicleNumber: "경기34나5678",
|
|
vehicleType: "2.5톤 트럭",
|
|
maintenanceType: "오일교환",
|
|
scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
status: "scheduled",
|
|
estimatedCost: 150000,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
location: "본사 정비소",
|
|
},
|
|
{
|
|
id: "maint-3",
|
|
vehicleNumber: "인천56다7890",
|
|
vehicleType: "라보",
|
|
maintenanceType: "타이어교체",
|
|
scheduledDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
status: "overdue",
|
|
notes: "긴급",
|
|
estimatedCost: 400000,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
location: "외부 정비소",
|
|
},
|
|
{
|
|
id: "maint-4",
|
|
vehicleNumber: "부산78라1234",
|
|
vehicleType: "1톤 트럭",
|
|
maintenanceType: "수리",
|
|
scheduledDate: new Date().toISOString(),
|
|
status: "in_progress",
|
|
notes: "엔진 점검 중",
|
|
estimatedCost: 800000,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
startedAt: new Date().toISOString(),
|
|
location: "본사 정비소",
|
|
},
|
|
];
|
|
|
|
/**
|
|
* 정비 일정 관리 서비스 (Memory/DB 하이브리드)
|
|
*/
|
|
export class MaintenanceService {
|
|
private static instance: MaintenanceService;
|
|
|
|
private constructor() {
|
|
logger.info(`🔧 정비 일정 데이터 소스: ${DATA_SOURCE.toUpperCase()}`);
|
|
}
|
|
|
|
public static getInstance(): MaintenanceService {
|
|
if (!MaintenanceService.instance) {
|
|
MaintenanceService.instance = new MaintenanceService();
|
|
}
|
|
return MaintenanceService.instance;
|
|
}
|
|
|
|
public async getAllSchedules(filter?: {
|
|
status?: string;
|
|
vehicleNumber?: string;
|
|
}): Promise<MaintenanceSchedule[]> {
|
|
try {
|
|
const schedules = DATA_SOURCE === "database"
|
|
? await this.loadSchedulesFromDB(filter)
|
|
: this.loadSchedulesFromMemory(filter);
|
|
|
|
// 자동으로 overdue 상태 업데이트
|
|
const now = new Date();
|
|
schedules.forEach((s) => {
|
|
if (s.status === "scheduled" && new Date(s.scheduledDate) < now) {
|
|
s.status = "overdue";
|
|
}
|
|
});
|
|
|
|
// 정렬: 지연 > 진행중 > 예정 > 완료
|
|
schedules.sort((a, b) => {
|
|
const statusOrder = { overdue: 0, in_progress: 1, scheduled: 2, completed: 3 };
|
|
if (a.status !== b.status) {
|
|
return statusOrder[a.status] - statusOrder[b.status];
|
|
}
|
|
return new Date(a.scheduledDate).getTime() - new Date(b.scheduledDate).getTime();
|
|
});
|
|
|
|
return schedules;
|
|
} catch (error) {
|
|
logger.error("❌ 정비 일정 조회 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async updateScheduleStatus(
|
|
id: string,
|
|
status: MaintenanceSchedule["status"]
|
|
): Promise<MaintenanceSchedule> {
|
|
try {
|
|
if (DATA_SOURCE === "database") {
|
|
return await this.updateScheduleStatusDB(id, status);
|
|
} else {
|
|
return this.updateScheduleStatusMemory(id, status);
|
|
}
|
|
} catch (error) {
|
|
logger.error("❌ 정비 상태 업데이트 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ==================== DATABASE 메서드 ====================
|
|
|
|
private async loadSchedulesFromDB(filter?: {
|
|
status?: string;
|
|
vehicleNumber?: string;
|
|
}): Promise<MaintenanceSchedule[]> {
|
|
let sql = `
|
|
SELECT
|
|
id, vehicle_number as "vehicleNumber", vehicle_type as "vehicleType",
|
|
maintenance_type as "maintenanceType", scheduled_date as "scheduledDate",
|
|
status, notes, estimated_cost as "estimatedCost", actual_cost as "actualCost",
|
|
created_at as "createdAt", updated_at as "updatedAt",
|
|
started_at as "startedAt", completed_at as "completedAt",
|
|
mechanic_name as "mechanicName", location
|
|
FROM maintenance_schedules
|
|
WHERE 1=1
|
|
`;
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (filter?.status) {
|
|
sql += ` AND status = $${paramIndex++}`;
|
|
params.push(filter.status);
|
|
}
|
|
if (filter?.vehicleNumber) {
|
|
sql += ` AND vehicle_number = $${paramIndex++}`;
|
|
params.push(filter.vehicleNumber);
|
|
}
|
|
|
|
const rows = await query(sql, params);
|
|
return rows.map((row: any) => ({
|
|
...row,
|
|
scheduledDate: new Date(row.scheduledDate).toISOString(),
|
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
startedAt: row.startedAt ? new Date(row.startedAt).toISOString() : undefined,
|
|
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
|
|
}));
|
|
}
|
|
|
|
private async updateScheduleStatusDB(
|
|
id: string,
|
|
status: MaintenanceSchedule["status"]
|
|
): Promise<MaintenanceSchedule> {
|
|
let additionalSet = "";
|
|
if (status === "in_progress") {
|
|
additionalSet = ", started_at = NOW()";
|
|
} else if (status === "completed") {
|
|
additionalSet = ", completed_at = NOW()";
|
|
}
|
|
|
|
const rows = await query(
|
|
`UPDATE maintenance_schedules
|
|
SET status = $1, updated_at = NOW() ${additionalSet}
|
|
WHERE id = $2
|
|
RETURNING
|
|
id, vehicle_number as "vehicleNumber", vehicle_type as "vehicleType",
|
|
maintenance_type as "maintenanceType", scheduled_date as "scheduledDate",
|
|
status, notes, estimated_cost as "estimatedCost",
|
|
created_at as "createdAt", updated_at as "updatedAt",
|
|
started_at as "startedAt", completed_at as "completedAt",
|
|
mechanic_name as "mechanicName", location`,
|
|
[status, id]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
throw new Error(`정비 일정을 찾을 수 없습니다: ${id}`);
|
|
}
|
|
|
|
const row = rows[0];
|
|
return {
|
|
...row,
|
|
scheduledDate: new Date(row.scheduledDate).toISOString(),
|
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
startedAt: row.startedAt ? new Date(row.startedAt).toISOString() : undefined,
|
|
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
|
|
};
|
|
}
|
|
|
|
// ==================== MEMORY 메서드 ====================
|
|
|
|
private loadSchedulesFromMemory(filter?: {
|
|
status?: string;
|
|
vehicleNumber?: string;
|
|
}): MaintenanceSchedule[] {
|
|
let schedules = [...mockSchedules];
|
|
|
|
if (filter?.status) {
|
|
schedules = schedules.filter((s) => s.status === filter.status);
|
|
}
|
|
if (filter?.vehicleNumber) {
|
|
schedules = schedules.filter((s) => s.vehicleNumber === filter.vehicleNumber);
|
|
}
|
|
|
|
return schedules;
|
|
}
|
|
|
|
private updateScheduleStatusMemory(
|
|
id: string,
|
|
status: MaintenanceSchedule["status"]
|
|
): MaintenanceSchedule {
|
|
const schedule = mockSchedules.find((s) => s.id === id);
|
|
|
|
if (!schedule) {
|
|
throw new Error(`정비 일정을 찾을 수 없습니다: ${id}`);
|
|
}
|
|
|
|
schedule.status = status;
|
|
schedule.updatedAt = new Date().toISOString();
|
|
|
|
if (status === "in_progress") {
|
|
schedule.startedAt = new Date().toISOString();
|
|
} else if (status === "completed") {
|
|
schedule.completedAt = new Date().toISOString();
|
|
}
|
|
|
|
return schedule;
|
|
}
|
|
}
|
|
|