/** * 작업 이력 관리 서비스 */ import pool from '../database/db'; import { WorkHistory, CreateWorkHistoryDto, UpdateWorkHistoryDto, WorkHistoryFilters, WorkHistoryStats, MonthlyTrend, TopRoute, } from '../types/workHistory'; /** * 작업 이력 목록 조회 */ export async function getWorkHistories(filters?: WorkHistoryFilters): Promise { try { let query = ` SELECT * FROM work_history WHERE deleted_at IS NULL `; const params: (string | Date)[] = []; let paramIndex = 1; // 필터 적용 if (filters?.work_type) { query += ` AND work_type = $${paramIndex}`; params.push(filters.work_type); paramIndex++; } if (filters?.status) { query += ` AND status = $${paramIndex}`; params.push(filters.status); paramIndex++; } if (filters?.vehicle_number) { query += ` AND vehicle_number LIKE $${paramIndex}`; params.push(`%${filters.vehicle_number}%`); paramIndex++; } if (filters?.driver_name) { query += ` AND driver_name LIKE $${paramIndex}`; params.push(`%${filters.driver_name}%`); paramIndex++; } if (filters?.start_date) { query += ` AND work_date >= $${paramIndex}`; params.push(filters.start_date); paramIndex++; } if (filters?.end_date) { query += ` AND work_date <= $${paramIndex}`; params.push(filters.end_date); paramIndex++; } if (filters?.search) { query += ` AND ( work_number LIKE $${paramIndex} OR vehicle_number LIKE $${paramIndex} OR driver_name LIKE $${paramIndex} OR cargo_name LIKE $${paramIndex} )`; params.push(`%${filters.search}%`); paramIndex++; } query += ` ORDER BY work_date DESC`; const result: any = await pool.query(query, params); return result.rows; } catch (error) { console.error('작업 이력 조회 실패:', error); throw error; } } /** * 작업 이력 단건 조회 */ export async function getWorkHistoryById(id: number): Promise { try { const result: any = await pool.query( 'SELECT * FROM work_history WHERE id = $1 AND deleted_at IS NULL', [id] ); return result.rows[0] || null; } catch (error) { console.error('작업 이력 조회 실패:', error); throw error; } } /** * 작업 이력 생성 */ export async function createWorkHistory(data: CreateWorkHistoryDto): Promise { try { const result: any = await pool.query( `INSERT INTO work_history ( work_type, vehicle_number, driver_name, origin, destination, cargo_name, cargo_weight, cargo_unit, distance, distance_unit, status, scheduled_time, estimated_arrival, notes, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *`, [ data.work_type, data.vehicle_number, data.driver_name, data.origin, data.destination, data.cargo_name, data.cargo_weight, data.cargo_unit || 'ton', data.distance, data.distance_unit || 'km', data.status || 'pending', data.scheduled_time, data.estimated_arrival, data.notes, data.created_by, ] ); return result.rows[0]; } catch (error) { console.error('작업 이력 생성 실패:', error); throw error; } } /** * 작업 이력 수정 */ export async function updateWorkHistory(id: number, data: UpdateWorkHistoryDto): Promise { try { const fields: string[] = []; const values: any[] = []; let paramIndex = 1; Object.entries(data).forEach(([key, value]) => { if (value !== undefined) { fields.push(`${key} = $${paramIndex}`); values.push(value); paramIndex++; } }); if (fields.length === 0) { throw new Error('수정할 데이터가 없습니다'); } values.push(id); const query = ` UPDATE work_history SET ${fields.join(', ')} WHERE id = $${paramIndex} AND deleted_at IS NULL RETURNING * `; const result: any = await pool.query(query, values); if (result.rows.length === 0) { throw new Error('작업 이력을 찾을 수 없습니다'); } return result.rows[0]; } catch (error) { console.error('작업 이력 수정 실패:', error); throw error; } } /** * 작업 이력 삭제 (소프트 삭제) */ export async function deleteWorkHistory(id: number): Promise { try { const result: any = await pool.query( 'UPDATE work_history SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL', [id] ); if (result.rowCount === 0) { throw new Error('작업 이력을 찾을 수 없습니다'); } } catch (error) { console.error('작업 이력 삭제 실패:', error); throw error; } } /** * 작업 이력 통계 조회 */ export async function getWorkHistoryStats(): Promise { try { // 오늘 작업 통계 const todayResult: any = await pool.query(` SELECT COUNT(*) as today_total, COUNT(*) FILTER (WHERE status = 'completed') as today_completed FROM work_history WHERE DATE(work_date) = CURRENT_DATE AND deleted_at IS NULL `); // 총 운송량 및 거리 const totalResult: any = await pool.query(` SELECT COALESCE(SUM(cargo_weight), 0) as total_weight, COALESCE(SUM(distance), 0) as total_distance FROM work_history WHERE deleted_at IS NULL AND status = 'completed' `); // 정시 도착률 const onTimeResult: any = await pool.query(` SELECT COUNT(*) FILTER (WHERE is_on_time = true) * 100.0 / NULLIF(COUNT(*), 0) as on_time_rate FROM work_history WHERE deleted_at IS NULL AND status = 'completed' AND is_on_time IS NOT NULL `); // 작업 유형별 분포 const typeResult: any = await pool.query(` SELECT work_type, COUNT(*) as count FROM work_history WHERE deleted_at IS NULL GROUP BY work_type `); const typeDistribution = { inbound: 0, outbound: 0, transfer: 0, maintenance: 0, }; typeResult.rows.forEach((row: any) => { typeDistribution[row.work_type as keyof typeof typeDistribution] = parseInt(row.count); }); return { today_total: parseInt(todayResult.rows[0].today_total), today_completed: parseInt(todayResult.rows[0].today_completed), total_weight: parseFloat(totalResult.rows[0].total_weight), total_distance: parseFloat(totalResult.rows[0].total_distance), on_time_rate: parseFloat(onTimeResult.rows[0]?.on_time_rate || '0'), type_distribution: typeDistribution, }; } catch (error) { console.error('작업 이력 통계 조회 실패:', error); throw error; } } /** * 월별 추이 조회 */ export async function getMonthlyTrend(months: number = 6): Promise { try { const result: any = await pool.query( ` SELECT TO_CHAR(work_date, 'YYYY-MM') as month, COUNT(*) as total, COUNT(*) FILTER (WHERE status = 'completed') as completed, COALESCE(SUM(cargo_weight), 0) as weight, COALESCE(SUM(distance), 0) as distance FROM work_history WHERE deleted_at IS NULL AND work_date >= CURRENT_DATE - INTERVAL '${months} months' GROUP BY TO_CHAR(work_date, 'YYYY-MM') ORDER BY month DESC `, [] ); return result.rows.map((row: any) => ({ month: row.month, total: parseInt(row.total), completed: parseInt(row.completed), weight: parseFloat(row.weight), distance: parseFloat(row.distance), })); } catch (error) { console.error('월별 추이 조회 실패:', error); throw error; } } /** * 주요 운송 경로 조회 */ export async function getTopRoutes(limit: number = 5): Promise { try { const result: any = await pool.query( ` SELECT origin, destination, COUNT(*) as count, COALESCE(SUM(cargo_weight), 0) as total_weight FROM work_history WHERE deleted_at IS NULL AND origin IS NOT NULL AND destination IS NOT NULL AND work_type IN ('inbound', 'outbound', 'transfer') GROUP BY origin, destination ORDER BY count DESC LIMIT $1 `, [limit] ); return result.rows.map((row: any) => ({ origin: row.origin, destination: row.destination, count: parseInt(row.count), total_weight: parseFloat(row.total_weight), })); } catch (error) { console.error('주요 운송 경로 조회 실패:', error); throw error; } }