ERP-node/backend-node/src/services/bookingService.ts

335 lines
11 KiB
TypeScript

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<BookingRequest> {
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<BookingRequest> {
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<BookingRequest[]> {
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<BookingRequest> {
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<BookingRequest> {
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;
}
}