335 lines
11 KiB
TypeScript
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;
|
|
}
|
|
}
|