feat(WEB): 수입검사 관리 대폭 개선, 캘린더 DayTimeView 추가 및 출고 기능 보완
- 수입검사: InspectionCreate/Detail/List 대폭 개선, OrderSelectModal/문서 컴포넌트 신규 추가 - 수입검사: actions/types/mockData/inspectionConfig 전면 리팩토링 - QMS: InspectionModalV2/ImportInspectionDocument 개선 - 캘린더: DayTimeView 신규 추가, CalendarHeader/ScheduleCalendar/utils 확장 - 출고: ShipmentDetail/List/actions 개선, ShipmentOrderDocument/ShippingSlip 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,6 @@ import {
|
||||
ArrowRight,
|
||||
Loader2,
|
||||
Trash2,
|
||||
X,
|
||||
Printer,
|
||||
ChevronDown,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -42,7 +40,7 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { PhoneInput } from '@/components/ui/phone-input';
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
import { DocumentViewer } from '@/components/document-system/viewer/DocumentViewer';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -73,7 +71,6 @@ import type {
|
||||
} from './types';
|
||||
import { ShippingSlip } from './documents/ShippingSlip';
|
||||
import { DeliveryConfirmation } from './documents/DeliveryConfirmation';
|
||||
import { printArea } from '@/lib/print-utils';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
interface ShipmentDetailProps {
|
||||
@@ -167,11 +164,6 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
}
|
||||
}, [id, router]);
|
||||
|
||||
const handlePrint = useCallback(() => {
|
||||
const docName = previewDocument === 'shipping' ? '출고증' : '납품확인서';
|
||||
printArea({ title: `${docName} 인쇄` });
|
||||
}, [previewDocument]);
|
||||
|
||||
const handleOpenStatusDialog = useCallback((status: ShipmentStatus) => {
|
||||
setTargetStatus(status);
|
||||
setStatusFormData({
|
||||
@@ -541,57 +533,21 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
headerActions={renderHeaderActions()}
|
||||
/>
|
||||
|
||||
{/* 문서 미리보기 다이얼로그 */}
|
||||
<Dialog open={previewDocument !== null} onOpenChange={() => setPreviewDocument(null)}>
|
||||
<DialogContent className="sm:max-w-5xl max-h-[90vh] overflow-y-auto p-0 gap-0 bg-gray-100">
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>
|
||||
{previewDocument === 'shipping' && '출고증'}
|
||||
{previewDocument === 'delivery' && '납품확인서'}
|
||||
</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
|
||||
<div className="print-hidden flex items-center justify-between px-6 py-4 border-b bg-white sticky top-0 z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-semibold text-lg">
|
||||
{previewDocument === 'shipping' && '출고증 미리보기'}
|
||||
{previewDocument === 'delivery' && '납품확인서'}
|
||||
</span>
|
||||
{detail && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{detail.customerName}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({detail.shipmentNo})
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handlePrint}>
|
||||
<Printer className="w-4 h-4 mr-1.5" />
|
||||
인쇄
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setPreviewDocument(null)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detail && (
|
||||
<div className="print-area m-6 p-6 bg-white rounded-lg shadow-sm">
|
||||
{previewDocument === 'shipping' && <ShippingSlip data={detail} />}
|
||||
{previewDocument === 'delivery' && <DeliveryConfirmation data={detail} />}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* 문서 미리보기 (DocumentViewer 통일 패턴) */}
|
||||
<DocumentViewer
|
||||
title={previewDocument === 'shipping' ? '출고증' : '납품확인서'}
|
||||
subtitle={detail ? `${detail.customerName} (${detail.shipmentNo})` : undefined}
|
||||
preset="readonly"
|
||||
open={previewDocument !== null}
|
||||
onOpenChange={() => setPreviewDocument(null)}
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
{previewDocument === 'shipping' && <ShippingSlip data={detail} />}
|
||||
{previewDocument === 'delivery' && <DeliveryConfirmation data={detail} />}
|
||||
</>
|
||||
)}
|
||||
</DocumentViewer>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
import { ScheduleCalendar } from '@/components/common/ScheduleCalendar/ScheduleCalendar';
|
||||
import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types';
|
||||
import type { ScheduleEvent, CalendarView } from '@/components/common/ScheduleCalendar/types';
|
||||
import { getShipments, getShipmentStats } from './actions';
|
||||
import {
|
||||
SHIPMENT_STATUS_LABELS,
|
||||
@@ -68,20 +68,9 @@ export function ShipmentList() {
|
||||
|
||||
// ===== 캘린더 상태 =====
|
||||
const [calendarDate, setCalendarDate] = useState(new Date());
|
||||
const [calendarDateInitialized, setCalendarDateInitialized] = useState(false);
|
||||
const [scheduleView, setScheduleView] = useState<CalendarView>('day-time');
|
||||
const [shipmentData, setShipmentData] = useState<ShipmentItem[]>([]);
|
||||
|
||||
// 데이터 로드 후 캘린더를 데이터 날짜로 이동
|
||||
useEffect(() => {
|
||||
if (!calendarDateInitialized && shipmentData.length > 0) {
|
||||
const firstDate = shipmentData[0].scheduledDate;
|
||||
if (firstDate) {
|
||||
setCalendarDate(new Date(firstDate));
|
||||
setCalendarDateInitialized(true);
|
||||
}
|
||||
}
|
||||
}, [shipmentData, calendarDateInitialized]);
|
||||
|
||||
// 초기 통계 로드
|
||||
useEffect(() => {
|
||||
const loadStats = async () => {
|
||||
@@ -414,23 +403,27 @@ export function ShipmentList() {
|
||||
);
|
||||
},
|
||||
|
||||
// 하단 캘린더 (시간축 주간 뷰)
|
||||
// 하단 캘린더 (일/주 토글)
|
||||
afterTableContent: (
|
||||
<ScheduleCalendar
|
||||
events={scheduleEvents}
|
||||
currentDate={calendarDate}
|
||||
view="week-time"
|
||||
view={scheduleView}
|
||||
onDateClick={handleCalendarDateClick}
|
||||
onEventClick={handleCalendarEventClick}
|
||||
onMonthChange={setCalendarDate}
|
||||
onViewChange={setScheduleView}
|
||||
titleSlot="출고 스케줄"
|
||||
weekStartsOn={0}
|
||||
availableViews={[]}
|
||||
availableViews={[
|
||||
{ value: 'day-time', label: '일' },
|
||||
{ value: 'week-time', label: '주' },
|
||||
]}
|
||||
timeRange={{ start: 1, end: 12 }}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[stats, startDate, endDate, scheduleEvents, calendarDate, handleRowClick, handleCreate, handleCalendarDateClick, handleCalendarEventClick]
|
||||
[stats, startDate, endDate, scheduleEvents, calendarDate, scheduleView, handleRowClick, handleCreate, handleCalendarDateClick, handleCalendarEventClick]
|
||||
);
|
||||
|
||||
return <UniversalListPage config={config} />;
|
||||
|
||||
@@ -178,12 +178,12 @@ function transformApiToDetail(data: ShipmentApiData): ShipmentDetail {
|
||||
shipmentNo: data.shipment_no,
|
||||
lotNo: data.lot_no || '',
|
||||
scheduledDate: data.scheduled_date,
|
||||
shipmentDate: (data as Record<string, unknown>).shipment_date as string | undefined,
|
||||
shipmentDate: (data as unknown as Record<string, unknown>).shipment_date as string | undefined,
|
||||
status: data.status,
|
||||
priority: data.priority,
|
||||
deliveryMethod: data.delivery_method,
|
||||
freightCost: (data as Record<string, unknown>).freight_cost as FreightCostType | undefined,
|
||||
freightCostLabel: (data as Record<string, unknown>).freight_cost_label as string | undefined,
|
||||
freightCost: (data as unknown as Record<string, unknown>).freight_cost as FreightCostType | undefined,
|
||||
freightCostLabel: (data as unknown as Record<string, unknown>).freight_cost_label as string | undefined,
|
||||
depositConfirmed: data.deposit_confirmed,
|
||||
invoiceIssued: data.invoice_issued,
|
||||
customerGrade: data.customer_grade || '',
|
||||
@@ -197,11 +197,21 @@ function transformApiToDetail(data: ShipmentApiData): ShipmentDetail {
|
||||
deliveryAddress: data.order_info?.delivery_address || data.delivery_address || '',
|
||||
receiver: data.receiver,
|
||||
receiverContact: data.order_info?.contact || data.receiver_contact,
|
||||
zipCode: (data as Record<string, unknown>).zip_code as string | undefined,
|
||||
address: (data as Record<string, unknown>).address as string | undefined,
|
||||
addressDetail: (data as Record<string, unknown>).address_detail as string | undefined,
|
||||
// 배차 정보 (다중 행) - API 준비 후 연동
|
||||
vehicleDispatches: [],
|
||||
zipCode: (data as unknown as Record<string, unknown>).zip_code as string | undefined,
|
||||
address: (data as unknown as Record<string, unknown>).address as string | undefined,
|
||||
addressDetail: (data as unknown as Record<string, unknown>).address_detail as string | undefined,
|
||||
// 배차 정보 - 기존 단일 필드에서 구성 (다중 행 API 준비 전까지)
|
||||
vehicleDispatches: data.vehicle_no || data.logistics_company || data.driver_contact
|
||||
? [{
|
||||
id: `vd-${data.id}`,
|
||||
logisticsCompany: data.logistics_company || '-',
|
||||
arrivalDateTime: data.confirmed_arrival || data.expected_arrival || '-',
|
||||
tonnage: data.vehicle_tonnage || '-',
|
||||
vehicleNo: data.vehicle_no || '-',
|
||||
driverContact: data.driver_contact || '-',
|
||||
remarks: '',
|
||||
}]
|
||||
: [],
|
||||
// 제품내용 (그룹핑) - 프론트엔드에서 그룹핑 처리
|
||||
productGroups: [],
|
||||
otherParts: [],
|
||||
|
||||
@@ -13,9 +13,10 @@ import { ConstructionApprovalTable } from '@/components/document-system';
|
||||
interface ShipmentOrderDocumentProps {
|
||||
title: string;
|
||||
data: ShipmentDetail;
|
||||
showDispatchInfo?: boolean;
|
||||
}
|
||||
|
||||
export function ShipmentOrderDocument({ title, data }: ShipmentOrderDocumentProps) {
|
||||
export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }: ShipmentOrderDocumentProps) {
|
||||
// 스크린 제품 필터링 (productGroups 기반)
|
||||
const screenProducts = data.productGroups.filter(g =>
|
||||
g.productName?.includes('스크린') ||
|
||||
@@ -188,6 +189,38 @@ export function ShipmentOrderDocument({ title, data }: ShipmentOrderDocumentProp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 배차정보 (출고증에서만 표시) */}
|
||||
{showDispatchInfo && (() => {
|
||||
const dispatch = data.vehicleDispatches[0];
|
||||
return (
|
||||
<div className="border border-gray-400 mb-4">
|
||||
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400">배차정보</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-1 py-1 w-[1%] font-medium border-r border-gray-300 whitespace-nowrap">물류업체</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{dispatch?.logisticsCompany || data.logisticsCompany || '-'}</td>
|
||||
<td className="bg-gray-100 px-1 py-1 w-[1%] font-medium border-r border-gray-300 whitespace-nowrap">입차일시</td>
|
||||
<td className="px-2 py-1" colSpan={3}>{dispatch?.arrivalDateTime || '-'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-1 py-1 w-[1%] font-medium border-r border-gray-300 whitespace-nowrap">톤수</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{dispatch?.tonnage || data.vehicleTonnage || '-'}</td>
|
||||
<td className="bg-gray-100 px-1 py-1 w-[1%] font-medium border-r border-gray-300 whitespace-nowrap">차량번호</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{dispatch?.vehicleNo || data.vehicleNo || '-'}</td>
|
||||
<td className="bg-gray-100 px-1 py-1 w-[1%] font-medium border-r border-gray-300 whitespace-nowrap">기사연락처</td>
|
||||
<td className="px-2 py-1 whitespace-nowrap">{dispatch?.driverContact || data.driverContact || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="bg-gray-100 px-1 py-1 w-[1%] font-medium border-r border-gray-300 whitespace-nowrap">비고</td>
|
||||
<td className="px-2 py-1" colSpan={5}>{dispatch?.remarks || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<p className="text-[10px] mb-4">아래와 같이 주문하오니 품질 및 납기일을 준수하여 주시기 바랍니다.</p>
|
||||
|
||||
{/* 1. 스크린 테이블 */}
|
||||
|
||||
@@ -13,5 +13,5 @@ interface ShippingSlipProps {
|
||||
}
|
||||
|
||||
export function ShippingSlip({ data }: ShippingSlipProps) {
|
||||
return <ShipmentOrderDocument title="출 고 증" data={data} />;
|
||||
return <ShipmentOrderDocument title="출 고 증" data={data} showDispatchInfo />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user