Files
vexplor_dev/backend-node/src/services/workHistoryService.ts
kjs c618283306 feat: Refactor work process and item inspection logic
- Updated SQL queries in `popProductionController` to separate work order process and result handling, ensuring batch_id is now managed in the work_order_process_result table.
- Enhanced `workInstructionController` to include id generation for work items and details, preventing NULL values during insertion.
- Implemented case-insensitive search functionality across various services, improving data retrieval accuracy.
- Added sorting functionality in the item inspection page, allowing users to sort by different columns with visual indicators for sort direction.

This refactor aims to improve data integrity and user experience across the production and inspection workflows.
2026-04-23 17:36:04 +09:00

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 ILIKE $${paramIndex}`;
params.push(`%${filters.vehicle_number}%`);
paramIndex++;
}
if (filters?.driver_name) {
query += ` AND driver_name ILIKE $${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 ILIKE $${paramIndex} OR
vehicle_number ILIKE $${paramIndex} OR
driver_name ILIKE $${paramIndex} OR
cargo_name ILIKE $${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;
}
}