feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가

자재관리:
- 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장
- 재고현황 컴포넌트 리팩토링

출고관리:
- 출하관리 생성/수정/목록/상세 개선
- 차량배차관리 상세/수정/목록 기능 보강

생산관리:
- 작업지시서 WIP 생산 모달 신규 추가
- 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가
- 작업자화면 기능 대폭 확장 (카드/목록 개선)
- 검사성적서 모달 개선

품질관리:
- 실적보고서 관리 페이지 신규 추가
- 검사관리 문서/타입/목데이터 개선

단가관리:
- 단가배포 페이지 및 컴포넌트 신규 추가
- 단가표 관리 페이지 및 컴포넌트 신규 추가

공통:
- 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard)
- 메뉴 폴링 훅 개선, 레이아웃 수정
- 모바일 줌/패닝 CSS 수정
- locale 유틸 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-04 12:46:19 +09:00
parent 17c16028b1
commit c1b63b850a
70 changed files with 6832 additions and 384 deletions

View File

@@ -9,6 +9,14 @@ import { useState, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { vehicleDispatchConfig } from './vehicleDispatchConfig';
import { getVehicleDispatchById } from './actions';
@@ -87,7 +95,7 @@ export function VehicleDispatchDetail({ id }: VehicleDispatchDetailProps) {
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{renderInfoField('배차번호', detail.dispatchNo)}
{renderInfoField('출고번호', detail.shipmentNo)}
{renderInfoField('로트번호', detail.lotNo || detail.shipmentNo)}
{renderInfoField('현장명', detail.siteName)}
{renderInfoField('수주처', detail.orderCustomer)}
{renderInfoField(
@@ -107,20 +115,34 @@ export function VehicleDispatchDetail({ id }: VehicleDispatchDetailProps) {
</CardContent>
</Card>
{/* 카드 2: 배차 정보 */}
{/* 카드 2: 배차 정보 (테이블 형태) */}
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
{renderInfoField('물류업체', detail.logisticsCompany)}
{renderInfoField('입차일시', detail.arrivalDateTime)}
{renderInfoField('톤수', detail.tonnage)}
{renderInfoField('차량번호', detail.vehicleNo)}
{renderInfoField('기사연락처', detail.driverContact)}
{renderInfoField('비고', detail.remarks || '-')}
</div>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>{detail.logisticsCompany || '-'}</TableCell>
<TableCell>{detail.arrivalDateTime || '-'}</TableCell>
<TableCell>{detail.tonnage || '-'}</TableCell>
<TableCell>{detail.vehicleNo || '-'}</TableCell>
<TableCell>{detail.driverContact || '-'}</TableCell>
<TableCell>{detail.remarks || '-'}</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>

View File

@@ -202,8 +202,8 @@ export function VehicleDispatchEdit({ id }: VehicleDispatchEditProps) {
<div className="font-medium">{detail.dispatchNo}</div>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
<div className="font-medium">{detail.shipmentNo}</div>
<Label className="text-muted-foreground"></Label>
<div className="font-medium">{detail.lotNo || detail.shipmentNo}</div>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
@@ -275,11 +275,11 @@ export function VehicleDispatchEdit({ id }: VehicleDispatchEditProps) {
/>
</div>
<div className="space-y-2">
<Label></Label>
<Label></Label>
<Input
value={formData.tonnage}
onChange={(e) => handleInputChange('tonnage', e.target.value)}
placeholder="예: 3.5톤"
placeholder="예: 3.5 톤"
disabled={isSubmitting}
/>
</div>

View File

@@ -163,23 +163,19 @@ export function VehicleDispatchList() {
onEndDateChange: setEndDate,
},
// 테이블 컬럼
// 테이블 컬럼 (13개)
columns: [
{ key: 'no', label: '번호', className: 'w-[50px] text-center' },
{ key: 'no', label: 'No.', className: 'w-[50px] text-center' },
{ key: 'dispatchNo', label: '배차번호', className: 'min-w-[130px]' },
{ key: 'shipmentNo', label: '출고번호', className: 'min-w-[130px]' },
{ key: 'lotNo', label: '로트번호', className: 'min-w-[120px]' },
{ key: 'siteName', label: '현장명', className: 'min-w-[100px]' },
{ key: 'orderCustomer', label: '수주처', className: 'min-w-[100px]' },
{ key: 'logisticsCompany', label: '물류업체', className: 'min-w-[90px]' },
{ key: 'tonnage', label: '톤수', className: 'w-[70px] text-center' },
{ key: 'supplyAmount', label: '공급가액', className: 'w-[100px] text-right' },
{ key: 'vat', label: '부가세', className: 'w-[90px] text-right' },
{ key: 'totalAmount', label: '합계', className: 'w-[100px] text-right' },
{ key: 'freightCostType', label: '선/착불', className: 'w-[70px] text-center' },
{ key: 'vehicleNo', label: '차량번호', className: 'min-w-[100px]' },
{ key: 'driverContact', label: '기사연락처', className: 'min-w-[110px]' },
{ key: 'writer', label: '작성자', className: 'w-[80px] text-center' },
{ key: 'arrivalDateTime', label: '입차일시', className: 'w-[130px] text-center' },
{ key: 'status', label: '상태', className: 'w-[80px] text-center' },
{ key: 'remarks', label: '비고', className: 'min-w-[100px]' },
],
@@ -201,15 +197,14 @@ export function VehicleDispatchList() {
itemsPerPage: ITEMS_PER_PAGE,
// 검색
searchPlaceholder: '배차번호, 출고번호, 현장명, 수주처, 차량번호 검색...',
searchPlaceholder: '배차번호, 로트번호, 현장명, 수주처 검색...',
searchFilter: (item: VehicleDispatchItem, search: string) => {
const s = search.toLowerCase();
return (
item.dispatchNo.toLowerCase().includes(s) ||
item.shipmentNo.toLowerCase().includes(s) ||
(item.lotNo || item.shipmentNo).toLowerCase().includes(s) ||
item.siteName.toLowerCase().includes(s) ||
item.orderCustomer.toLowerCase().includes(s) ||
item.vehicleNo.toLowerCase().includes(s)
item.orderCustomer.toLowerCase().includes(s)
);
},
@@ -235,31 +230,29 @@ export function VehicleDispatchList() {
onCheckedChange={handlers.onToggle}
/>
</TableCell>
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
<TableCell className="font-medium">{item.dispatchNo}</TableCell>
<TableCell>{item.shipmentNo}</TableCell>
<TableCell className="max-w-[100px] truncate">{item.siteName}</TableCell>
<TableCell>{item.orderCustomer}</TableCell>
<TableCell>{item.logisticsCompany}</TableCell>
<TableCell className="text-center">{item.tonnage}</TableCell>
<TableCell className="text-right">{formatAmount(item.supplyAmount)}</TableCell>
<TableCell className="text-right">{formatAmount(item.vat)}</TableCell>
<TableCell className="text-right font-medium">{formatAmount(item.totalAmount)}</TableCell>
<TableCell className="text-center">
<Badge className={`text-xs ${FREIGHT_COST_STYLES[item.freightCostType]}`}>
{FREIGHT_COST_LABELS[item.freightCostType]}
</Badge>
<TableCell className="w-[50px] text-center text-muted-foreground">{globalIndex}</TableCell>
<TableCell className="min-w-[130px] font-medium">{item.dispatchNo}</TableCell>
<TableCell className="min-w-[120px]">{item.lotNo || item.shipmentNo}</TableCell>
<TableCell className="min-w-[100px] truncate">{item.siteName}</TableCell>
<TableCell className="min-w-[100px]">{item.orderCustomer}</TableCell>
<TableCell className="min-w-[90px]">{item.logisticsCompany}</TableCell>
<TableCell className="w-[100px] text-right">{formatAmount(item.supplyAmount || 0)}</TableCell>
<TableCell className="w-[90px] text-right">{formatAmount(item.vat || 0)}</TableCell>
<TableCell className="w-[100px] text-right font-medium">{formatAmount(item.totalAmount || 0)}</TableCell>
<TableCell className="w-[70px] text-center">
{item.freightCostType ? (
<Badge className={`text-xs ${FREIGHT_COST_STYLES[item.freightCostType]}`}>
{FREIGHT_COST_LABELS[item.freightCostType]}
</Badge>
) : '-'}
</TableCell>
<TableCell>{item.vehicleNo}</TableCell>
<TableCell>{item.driverContact}</TableCell>
<TableCell className="text-center">{item.writer}</TableCell>
<TableCell className="text-center">{item.arrivalDateTime}</TableCell>
<TableCell className="text-center">
<TableCell className="w-[80px] text-center">{item.writer || '-'}</TableCell>
<TableCell className="w-[80px] text-center">
<Badge className={`text-xs ${VEHICLE_DISPATCH_STATUS_STYLES[item.status]}`}>
{VEHICLE_DISPATCH_STATUS_LABELS[item.status]}
</Badge>
</TableCell>
<TableCell className="max-w-[100px] truncate">{item.remarks || '-'}</TableCell>
<TableCell className="min-w-[100px] truncate">{item.remarks || '-'}</TableCell>
</TableRow>
);
},
@@ -296,17 +289,16 @@ export function VehicleDispatchList() {
}
infoGrid={
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<InfoField label="출고번호" value={item.shipmentNo} />
<InfoField label="로트번호" value={item.lotNo || item.shipmentNo} />
<InfoField label="수주처" value={item.orderCustomer} />
<InfoField label="물류업체" value={item.logisticsCompany} />
<InfoField label="톤수" value={item.tonnage} />
<InfoField label="공급가액" value={`${formatAmount(item.supplyAmount)}`} />
<InfoField label="합계" value={`${formatAmount(item.totalAmount)}`} />
<InfoField
label="선/착불"
value={FREIGHT_COST_LABELS[item.freightCostType]}
/>
<InfoField label="차량번호" value={item.vehicleNo} />
<InfoField label="입차일시" value={item.arrivalDateTime} />
<InfoField label="작성자" value={item.writer} />
</div>
}
actions={

View File

@@ -35,6 +35,7 @@ export interface VehicleDispatchItem {
id: string;
dispatchNo: string; // 배차번호
shipmentNo: string; // 출고번호
lotNo?: string; // 로트번호
siteName: string; // 현장명
orderCustomer: string; // 수주처
logisticsCompany: string; // 물류업체
@@ -57,6 +58,7 @@ export interface VehicleDispatchDetail {
// 기본 정보
dispatchNo: string; // 배차번호
shipmentNo: string; // 출고번호
lotNo?: string; // 로트번호
siteName: string; // 현장명
orderCustomer: string; // 수주처
freightCostType: FreightCostType; // 운임비용