import * as fs from "fs"; import * as path from "path"; import { v4 as uuidv4 } from "uuid"; import { logger } from "../utils/logger"; import { query } from "../database/db"; const BOOKING_DIR = path.join(__dirname, "../../data/bookings"); const BOOKING_FILE = path.join(BOOKING_DIR, "bookings.json"); // 환경 변수로 데이터 소스 선택 const DATA_SOURCE = process.env.BOOKING_DATA_SOURCE || "file"; export interface BookingRequest { id: string; customerName: string; customerPhone: string; pickupLocation: string; dropoffLocation: string; scheduledTime: string; vehicleType: "truck" | "van" | "car"; cargoType?: string; weight?: number; status: "pending" | "accepted" | "rejected" | "completed"; priority: "normal" | "urgent"; createdAt: string; updatedAt: string; acceptedAt?: string; rejectedAt?: string; completedAt?: string; notes?: string; estimatedCost?: number; } /** * 예약 요청 관리 서비스 (File/DB 하이브리드) */ export class BookingService { private static instance: BookingService; private constructor() { if (DATA_SOURCE === "file") { this.ensureDataDirectory(); this.generateMockData(); } logger.info(`📋 예약 요청 데이터 소스: ${DATA_SOURCE.toUpperCase()}`); } public static getInstance(): BookingService { if (!BookingService.instance) { BookingService.instance = new BookingService(); } return BookingService.instance; } private ensureDataDirectory(): void { if (!fs.existsSync(BOOKING_DIR)) { fs.mkdirSync(BOOKING_DIR, { recursive: true }); logger.info(`📁 예약 데이터 디렉토리 생성: ${BOOKING_DIR}`); } if (!fs.existsSync(BOOKING_FILE)) { fs.writeFileSync(BOOKING_FILE, JSON.stringify([], null, 2)); logger.info(`📄 예약 파일 생성: ${BOOKING_FILE}`); } } private generateMockData(): void { const bookings = this.loadBookingsFromFile(); if (bookings.length > 0) return; const mockBookings: BookingRequest[] = [ { id: uuidv4(), customerName: "김철수", customerPhone: "010-1234-5678", pickupLocation: "서울시 강남구 역삼동 123", dropoffLocation: "경기도 성남시 분당구 정자동 456", scheduledTime: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(), vehicleType: "truck", cargoType: "전자제품", weight: 500, status: "pending", priority: "urgent", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), estimatedCost: 150000, }, { id: uuidv4(), customerName: "이영희", customerPhone: "010-9876-5432", pickupLocation: "서울시 송파구 잠실동 789", dropoffLocation: "인천시 남동구 구월동 321", scheduledTime: new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(), vehicleType: "van", cargoType: "가구", weight: 300, status: "pending", priority: "normal", createdAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(), updatedAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(), estimatedCost: 80000, }, ]; this.saveBookingsToFile(mockBookings); logger.info(`✅ 예약 목 데이터 생성: ${mockBookings.length}개`); } public async getAllBookings(filter?: { status?: string; priority?: string; }): Promise<{ bookings: BookingRequest[]; newCount: number }> { try { const bookings = DATA_SOURCE === "database" ? await this.loadBookingsFromDB(filter) : this.loadBookingsFromFile(filter); bookings.sort((a, b) => { if (a.priority !== b.priority) return a.priority === "urgent" ? -1 : 1; return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); }); const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); const newCount = bookings.filter( (b) => b.status === "pending" && new Date(b.createdAt) > fiveMinutesAgo ).length; return { bookings, newCount }; } catch (error) { logger.error("❌ 예약 목록 조회 오류:", error); throw error; } } public async acceptBooking(id: string): Promise { try { if (DATA_SOURCE === "database") { return await this.acceptBookingDB(id); } else { return this.acceptBookingFile(id); } } catch (error) { logger.error("❌ 예약 수락 오류:", error); throw error; } } public async rejectBooking(id: string, reason?: string): Promise { try { if (DATA_SOURCE === "database") { return await this.rejectBookingDB(id, reason); } else { return this.rejectBookingFile(id, reason); } } catch (error) { logger.error("❌ 예약 거절 오류:", error); throw error; } } // ==================== DATABASE 메서드 ==================== private async loadBookingsFromDB(filter?: { status?: string; priority?: string; }): Promise { let sql = ` SELECT id, customer_name as "customerName", customer_phone as "customerPhone", pickup_location as "pickupLocation", dropoff_location as "dropoffLocation", scheduled_time as "scheduledTime", vehicle_type as "vehicleType", cargo_type as "cargoType", weight, status, priority, created_at as "createdAt", updated_at as "updatedAt", accepted_at as "acceptedAt", rejected_at as "rejectedAt", completed_at as "completedAt", notes, estimated_cost as "estimatedCost" FROM booking_requests WHERE 1=1 `; const params: any[] = []; let paramIndex = 1; if (filter?.status) { sql += ` AND status = $${paramIndex++}`; params.push(filter.status); } if (filter?.priority) { sql += ` AND priority = $${paramIndex++}`; params.push(filter.priority); } const rows = await query(sql, params); return rows.map((row: any) => ({ ...row, scheduledTime: new Date(row.scheduledTime).toISOString(), createdAt: new Date(row.createdAt).toISOString(), updatedAt: new Date(row.updatedAt).toISOString(), acceptedAt: row.acceptedAt ? new Date(row.acceptedAt).toISOString() : undefined, rejectedAt: row.rejectedAt ? new Date(row.rejectedAt).toISOString() : undefined, completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined, })); } private async acceptBookingDB(id: string): Promise { const rows = await query( `UPDATE booking_requests SET status = 'accepted', accepted_at = NOW(), updated_at = NOW() WHERE id = $1 RETURNING id, customer_name as "customerName", customer_phone as "customerPhone", pickup_location as "pickupLocation", dropoff_location as "dropoffLocation", scheduled_time as "scheduledTime", vehicle_type as "vehicleType", cargo_type as "cargoType", weight, status, priority, created_at as "createdAt", updated_at as "updatedAt", accepted_at as "acceptedAt", notes, estimated_cost as "estimatedCost"`, [id] ); if (rows.length === 0) { throw new Error(`예약을 찾을 수 없습니다: ${id}`); } const row = rows[0]; logger.info(`✅ 예약 수락: ${id} - ${row.customerName}`); return { ...row, scheduledTime: new Date(row.scheduledTime).toISOString(), createdAt: new Date(row.createdAt).toISOString(), updatedAt: new Date(row.updatedAt).toISOString(), acceptedAt: new Date(row.acceptedAt).toISOString(), }; } private async rejectBookingDB(id: string, reason?: string): Promise { const rows = await query( `UPDATE booking_requests SET status = 'rejected', rejected_at = NOW(), updated_at = NOW(), rejection_reason = $2 WHERE id = $1 RETURNING id, customer_name as "customerName", customer_phone as "customerPhone", pickup_location as "pickupLocation", dropoff_location as "dropoffLocation", scheduled_time as "scheduledTime", vehicle_type as "vehicleType", cargo_type as "cargoType", weight, status, priority, created_at as "createdAt", updated_at as "updatedAt", rejected_at as "rejectedAt", notes, estimated_cost as "estimatedCost"`, [id, reason] ); if (rows.length === 0) { throw new Error(`예약을 찾을 수 없습니다: ${id}`); } const row = rows[0]; logger.info(`✅ 예약 거절: ${id} - ${row.customerName}`); return { ...row, scheduledTime: new Date(row.scheduledTime).toISOString(), createdAt: new Date(row.createdAt).toISOString(), updatedAt: new Date(row.updatedAt).toISOString(), rejectedAt: new Date(row.rejectedAt).toISOString(), }; } // ==================== FILE 메서드 ==================== private loadBookingsFromFile(filter?: { status?: string; priority?: string; }): BookingRequest[] { try { const data = fs.readFileSync(BOOKING_FILE, "utf-8"); let bookings: BookingRequest[] = JSON.parse(data); if (filter?.status) { bookings = bookings.filter((b) => b.status === filter.status); } if (filter?.priority) { bookings = bookings.filter((b) => b.priority === filter.priority); } return bookings; } catch (error) { logger.error("❌ 예약 파일 로드 오류:", error); return []; } } private saveBookingsToFile(bookings: BookingRequest[]): void { try { fs.writeFileSync(BOOKING_FILE, JSON.stringify(bookings, null, 2)); } catch (error) { logger.error("❌ 예약 파일 저장 오류:", error); throw error; } } private acceptBookingFile(id: string): BookingRequest { const bookings = this.loadBookingsFromFile(); const booking = bookings.find((b) => b.id === id); if (!booking) { throw new Error(`예약을 찾을 수 없습니다: ${id}`); } booking.status = "accepted"; booking.acceptedAt = new Date().toISOString(); booking.updatedAt = new Date().toISOString(); this.saveBookingsToFile(bookings); logger.info(`✅ 예약 수락: ${id} - ${booking.customerName}`); return booking; } private rejectBookingFile(id: string, reason?: string): BookingRequest { const bookings = this.loadBookingsFromFile(); const booking = bookings.find((b) => b.id === id); if (!booking) { throw new Error(`예약을 찾을 수 없습니다: ${id}`); } booking.status = "rejected"; booking.rejectedAt = new Date().toISOString(); booking.updatedAt = new Date().toISOString(); if (reason) { booking.notes = reason; } this.saveBookingsToFile(bookings); logger.info(`✅ 예약 거절: ${id} - ${booking.customerName}`); return booking; } }