작업 이력 통계 위젯 추가

백스페이스 안먹는 오류 수정
그리드 컴포넌트 수정
등등
This commit is contained in:
leeheejin
2025-10-20 14:07:08 +09:00
parent 2b7519519a
commit 5b503edfa8
19 changed files with 1447 additions and 2 deletions

View File

@@ -56,6 +56,7 @@ import todoRoutes from "./routes/todoRoutes"; // To-Do 관리
import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리
import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리
import yardLayoutRoutes from "./routes/yardLayoutRoutes"; // 야드 관리 3D
import workHistoryRoutes from "./routes/workHistoryRoutes"; // 작업 이력 관리
import { BatchSchedulerService } from "./services/batchSchedulerService";
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
@@ -206,6 +207,7 @@ app.use("/api/todos", todoRoutes); // To-Do 관리
app.use("/api/bookings", bookingRoutes); // 예약 요청 관리
app.use("/api/map-data", mapDataRoutes); // 지도 데이터 조회
app.use("/api/yard-layouts", yardLayoutRoutes); // 야드 관리 3D
app.use("/api/work-history", workHistoryRoutes); // 작업 이력 관리
// app.use("/api/collections", collectionRoutes); // 임시 주석
// app.use("/api/batch", batchRoutes); // 임시 주석
// app.use('/api/users', userRoutes);

View File

@@ -0,0 +1,199 @@
/**
* 작업 이력 관리 컨트롤러
*/
import { Request, Response } from 'express';
import * as workHistoryService from '../services/workHistoryService';
import { CreateWorkHistoryDto, UpdateWorkHistoryDto, WorkHistoryFilters } from '../types/workHistory';
/**
* 작업 이력 목록 조회
*/
export async function getWorkHistories(req: Request, res: Response): Promise<void> {
try {
const filters: WorkHistoryFilters = {
work_type: req.query.work_type as any,
status: req.query.status as any,
vehicle_number: req.query.vehicle_number as string,
driver_name: req.query.driver_name as string,
start_date: req.query.start_date ? new Date(req.query.start_date as string) : undefined,
end_date: req.query.end_date ? new Date(req.query.end_date as string) : undefined,
search: req.query.search as string,
};
const histories = await workHistoryService.getWorkHistories(filters);
res.json({
success: true,
data: histories,
});
} catch (error) {
console.error('작업 이력 목록 조회 실패:', error);
res.status(500).json({
success: false,
message: '작업 이력 목록 조회에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* 작업 이력 단건 조회
*/
export async function getWorkHistoryById(req: Request, res: Response): Promise<void> {
try {
const id = parseInt(req.params.id);
const history = await workHistoryService.getWorkHistoryById(id);
if (!history) {
res.status(404).json({
success: false,
message: '작업 이력을 찾을 수 없습니다',
});
return;
}
res.json({
success: true,
data: history,
});
} catch (error) {
console.error('작업 이력 조회 실패:', error);
res.status(500).json({
success: false,
message: '작업 이력 조회에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* 작업 이력 생성
*/
export async function createWorkHistory(req: Request, res: Response): Promise<void> {
try {
const data: CreateWorkHistoryDto = req.body;
const history = await workHistoryService.createWorkHistory(data);
res.status(201).json({
success: true,
data: history,
message: '작업 이력이 생성되었습니다',
});
} catch (error) {
console.error('작업 이력 생성 실패:', error);
res.status(500).json({
success: false,
message: '작업 이력 생성에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* 작업 이력 수정
*/
export async function updateWorkHistory(req: Request, res: Response): Promise<void> {
try {
const id = parseInt(req.params.id);
const data: UpdateWorkHistoryDto = req.body;
const history = await workHistoryService.updateWorkHistory(id, data);
res.json({
success: true,
data: history,
message: '작업 이력이 수정되었습니다',
});
} catch (error) {
console.error('작업 이력 수정 실패:', error);
res.status(500).json({
success: false,
message: '작업 이력 수정에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* 작업 이력 삭제
*/
export async function deleteWorkHistory(req: Request, res: Response): Promise<void> {
try {
const id = parseInt(req.params.id);
await workHistoryService.deleteWorkHistory(id);
res.json({
success: true,
message: '작업 이력이 삭제되었습니다',
});
} catch (error) {
console.error('작업 이력 삭제 실패:', error);
res.status(500).json({
success: false,
message: '작업 이력 삭제에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* 작업 이력 통계 조회
*/
export async function getWorkHistoryStats(req: Request, res: Response): Promise<void> {
try {
const stats = await workHistoryService.getWorkHistoryStats();
res.json({
success: true,
data: stats,
});
} catch (error) {
console.error('작업 이력 통계 조회 실패:', error);
res.status(500).json({
success: false,
message: '작업 이력 통계 조회에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* 월별 추이 조회
*/
export async function getMonthlyTrend(req: Request, res: Response): Promise<void> {
try {
const months = parseInt(req.query.months as string) || 6;
const trend = await workHistoryService.getMonthlyTrend(months);
res.json({
success: true,
data: trend,
});
} catch (error) {
console.error('월별 추이 조회 실패:', error);
res.status(500).json({
success: false,
message: '월별 추이 조회에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}
/**
* 주요 운송 경로 조회
*/
export async function getTopRoutes(req: Request, res: Response): Promise<void> {
try {
const limit = parseInt(req.query.limit as string) || 5;
const routes = await workHistoryService.getTopRoutes(limit);
res.json({
success: true,
data: routes,
});
} catch (error) {
console.error('주요 운송 경로 조회 실패:', error);
res.status(500).json({
success: false,
message: '주요 운송 경로 조회에 실패했습니다',
error: error instanceof Error ? error.message : String(error),
});
}
}

View File

@@ -0,0 +1,35 @@
/**
* 작업 이력 관리 라우트
*/
import express from 'express';
import * as workHistoryController from '../controllers/workHistoryController';
const router = express.Router();
// 작업 이력 목록 조회
router.get('/', workHistoryController.getWorkHistories);
// 작업 이력 통계 조회
router.get('/stats', workHistoryController.getWorkHistoryStats);
// 월별 추이 조회
router.get('/trend', workHistoryController.getMonthlyTrend);
// 주요 운송 경로 조회
router.get('/routes', workHistoryController.getTopRoutes);
// 작업 이력 단건 조회
router.get('/:id', workHistoryController.getWorkHistoryById);
// 작업 이력 생성
router.post('/', workHistoryController.createWorkHistory);
// 작업 이력 수정
router.put('/:id', workHistoryController.updateWorkHistory);
// 작업 이력 삭제
router.delete('/:id', workHistoryController.deleteWorkHistory);
export default router;

View File

@@ -53,6 +53,8 @@ const ALLOWED_TABLES = [
"table_labels",
"column_labels",
"dynamic_form_data",
"work_history", // 작업 이력 테이블
"delivery_status", // 배송 현황 테이블
];
/**

View File

@@ -0,0 +1,335 @@
/**
* 작업 이력 관리 서비스
*/
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;
}
}

View File

@@ -0,0 +1,114 @@
/**
* 작업 이력 관리 타입 정의
*/
export type WorkType = 'inbound' | 'outbound' | 'transfer' | 'maintenance';
export type WorkStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
export interface WorkHistory {
id: number;
work_number: string;
work_date: Date;
work_type: WorkType;
vehicle_number?: string;
driver_name?: string;
origin?: string;
destination?: string;
cargo_name?: string;
cargo_weight?: number;
cargo_unit?: string;
distance?: number;
distance_unit?: string;
status: WorkStatus;
scheduled_time?: Date;
start_time?: Date;
end_time?: Date;
estimated_arrival?: Date;
actual_arrival?: Date;
is_on_time?: boolean;
delay_reason?: string;
notes?: string;
created_by?: string;
created_at: Date;
updated_at: Date;
deleted_at?: Date;
}
export interface CreateWorkHistoryDto {
work_type: WorkType;
vehicle_number?: string;
driver_name?: string;
origin?: string;
destination?: string;
cargo_name?: string;
cargo_weight?: number;
cargo_unit?: string;
distance?: number;
distance_unit?: string;
status?: WorkStatus;
scheduled_time?: Date;
estimated_arrival?: Date;
notes?: string;
created_by?: string;
}
export interface UpdateWorkHistoryDto {
work_type?: WorkType;
vehicle_number?: string;
driver_name?: string;
origin?: string;
destination?: string;
cargo_name?: string;
cargo_weight?: number;
cargo_unit?: string;
distance?: number;
distance_unit?: string;
status?: WorkStatus;
scheduled_time?: Date;
start_time?: Date;
end_time?: Date;
estimated_arrival?: Date;
actual_arrival?: Date;
delay_reason?: string;
notes?: string;
}
export interface WorkHistoryFilters {
work_type?: WorkType;
status?: WorkStatus;
vehicle_number?: string;
driver_name?: string;
start_date?: Date;
end_date?: Date;
search?: string;
}
export interface WorkHistoryStats {
today_total: number;
today_completed: number;
total_weight: number;
total_distance: number;
on_time_rate: number;
type_distribution: {
inbound: number;
outbound: number;
transfer: number;
maintenance: number;
};
}
export interface MonthlyTrend {
month: string;
total: number;
completed: number;
weight: number;
distance: number;
}
export interface TopRoute {
origin: string;
destination: string;
count: number;
total_weight: number;
}