336 lines
8.7 KiB
TypeScript
336 lines
8.7 KiB
TypeScript
/**
|
|
* 작업 이력 관리 서비스
|
|
*/
|
|
|
|
import pool from '../database/db';
|
|
import {
|
|
WorkHistory,
|
|
CreateWorkHistoryDto,
|
|
UpdateWorkHistoryDto,
|
|
WorkHistoryFilters,
|
|
WorkHistoryStats,
|
|
MonthlyTrend,
|
|
TopRoute,
|
|
} from '../types/workHistory';
|
|
|
|
/**
|
|
* 작업 이력 목록 조회
|
|
*/
|
|
export async function getWorkHistories(filters?: WorkHistoryFilters): Promise<WorkHistory[]> {
|
|
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<WorkHistory | null> {
|
|
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<WorkHistory> {
|
|
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<WorkHistory> {
|
|
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<void> {
|
|
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<WorkHistoryStats> {
|
|
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<MonthlyTrend[]> {
|
|
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<TopRoute[]> {
|
|
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;
|
|
}
|
|
}
|
|
|