From 915b5d9a75d66d0f0cbfe6349a14f1a023b51547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 18 Mar 2026 09:21:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[shipment]=20=EC=B6=9C=EA=B3=A0?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 출고증/납품확인서 목업 데이터 → API 실데이터 전환 - 출고 등록(수동) 기능 제거 (자동생성만 유지) - 출고로트/수주로트 분리 표시, 로트번호 폴백 처리 - 출고 목록 카드뷰 불일치 수정 --- .../outbound/shipments/new/page.tsx | 10 - .../(protected)/outbound/shipments/page.tsx | 15 +- .../ShipmentManagement/ShipmentCreate.tsx | 760 --------------- .../ShipmentManagement/ShipmentDetail.tsx | 25 +- .../ShipmentManagement/ShipmentList.tsx | 26 +- .../outbound/ShipmentManagement/actions.ts | 99 +- .../documents/DeliveryConfirmation.tsx | 9 +- .../documents/ShipmentOrderDocument.tsx | 878 +++++++++--------- .../documents/ShippingSlip.tsx | 9 +- .../outbound/ShipmentManagement/index.ts | 1 - .../ShipmentManagement/shipmentConfig.ts | 18 +- .../outbound/ShipmentManagement/types.ts | 7 +- 12 files changed, 545 insertions(+), 1312 deletions(-) delete mode 100644 src/app/[locale]/(protected)/outbound/shipments/new/page.tsx delete mode 100644 src/components/outbound/ShipmentManagement/ShipmentCreate.tsx diff --git a/src/app/[locale]/(protected)/outbound/shipments/new/page.tsx b/src/app/[locale]/(protected)/outbound/shipments/new/page.tsx deleted file mode 100644 index f6d3cac9..00000000 --- a/src/app/[locale]/(protected)/outbound/shipments/new/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/** - * 출하관리 - 등록 페이지 - * URL: /outbound/shipments/new - */ - -import { ShipmentCreate } from '@/components/outbound/ShipmentManagement'; - -export default function NewShipmentPage() { - return ; -} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/outbound/shipments/page.tsx b/src/app/[locale]/(protected)/outbound/shipments/page.tsx index bdd88380..d8dabd30 100644 --- a/src/app/[locale]/(protected)/outbound/shipments/page.tsx +++ b/src/app/[locale]/(protected)/outbound/shipments/page.tsx @@ -1,21 +1,12 @@ 'use client'; /** - * 출하관리 - 목록/등록 페이지 + * 출하관리 - 목록 페이지 * URL: /outbound/shipments - * URL: /outbound/shipments?mode=new */ -import { useSearchParams } from 'next/navigation'; -import { ShipmentList, ShipmentCreate } from '@/components/outbound/ShipmentManagement'; +import { ShipmentList } from '@/components/outbound/ShipmentManagement'; export default function ShipmentsPage() { - const searchParams = useSearchParams(); - const mode = searchParams.get('mode'); - - if (mode === 'new') { - return ; - } - return ; -} +} \ No newline at end of file diff --git a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx b/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx deleted file mode 100644 index 226a442a..00000000 --- a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx +++ /dev/null @@ -1,760 +0,0 @@ -'use client'; - -/** - * 출고 등록 페이지 - * 4개 섹션 구조: 기본정보, 수주/배송정보, 배차정보, 제품내용 - */ - -import { useState, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { Plus, Trash2, ChevronDown, Search } from 'lucide-react'; -import { getTodayString } from '@/lib/utils/date'; -import { Input } from '@/components/ui/input'; -import { DateTimePicker } from '@/components/ui/date-time-picker'; -import { DatePicker } from '@/components/ui/date-picker'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -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 { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from '@/components/ui/accordion'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; -import { shipmentCreateConfig } from './shipmentConfig'; -import { - createShipment, - getLotOptions, - getLogisticsOptions, - getVehicleTonnageOptions, -} from './actions'; -import type { - ShipmentCreateFormData, - DeliveryMethod, - FreightCostType, - VehicleDispatch, - LotOption, - LogisticsOption, - VehicleTonnageOption, - ProductGroup, - ProductPart, -} from './types'; -import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { toast } from 'sonner'; -import { useDaumPostcode } from '@/hooks/useDaumPostcode'; -import { useDevFill } from '@/components/dev'; -import { generateShipmentData } from '@/components/dev/generators/shipmentData'; -import { mockProductGroups, mockOtherParts } from './mockData'; - -// 배송방식 옵션 -const deliveryMethodOptions: { value: DeliveryMethod; label: string }[] = [ - { value: 'direct_dispatch', label: '직접배차' }, - { value: 'loading', label: '상차' }, - { value: 'kyungdong_delivery', label: '경동택배' }, - { value: 'daesin_delivery', label: '대신택배' }, - { value: 'kyungdong_freight', label: '경동화물' }, - { value: 'daesin_freight', label: '대신화물' }, - { value: 'self_pickup', label: '직접수령' }, -]; - -// 운임비용 옵션 (선불, 착불, 없음) -const freightCostOptions: { value: FreightCostType; label: string }[] = [ - { value: 'prepaid', label: '선불' }, - { value: 'collect', label: '착불' }, - { value: 'none', label: '없음' }, -]; - -// 빈 배차 행 생성 -function createEmptyDispatch(): VehicleDispatch { - return { - id: `vd-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, - logisticsCompany: '', - arrivalDateTime: '', - tonnage: '', - vehicleNo: '', - driverContact: '', - remarks: '', - }; -} - -export function ShipmentCreate() { - const router = useRouter(); - - // 폼 상태 - const [formData, setFormData] = useState({ - lotNo: '', - scheduledDate: getTodayString(), - priority: 'normal', - deliveryMethod: 'direct_dispatch', - shipmentDate: '', - freightCost: 'none', - receiver: '', - receiverContact: '', - zipCode: '', - address: '', - addressDetail: '', - vehicleDispatches: [createEmptyDispatch()], - logisticsCompany: '', - vehicleTonnage: '', - loadingTime: '', - loadingManager: '', - remarks: '', - }); - - // API 옵션 데이터 상태 - const [lotOptions, setLotOptions] = useState([]); - const [logisticsOptions, setLogisticsOptions] = useState([]); - const [vehicleTonnageOptions, setVehicleTonnageOptions] = useState([]); - - // 제품 데이터 (LOT 선택 시 표시) - const [productGroups, setProductGroups] = useState([]); - const [otherParts, setOtherParts] = useState([]); - - // 로딩/에러 상태 - const [isLoading, setIsLoading] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); - const [validationErrors, setValidationErrors] = useState>({}); - - // 아코디언 상태 - const [accordionValue, setAccordionValue] = useState([]); - - // 우편번호 찾기 - const { openPostcode } = useDaumPostcode({ - onComplete: (result) => { - setFormData(prev => ({ - ...prev, - zipCode: result.zonecode, - address: result.address, - })); - }, - }); - - // 옵션 데이터 로드 - const loadOptions = useCallback(async () => { - setIsLoading(true); - setError(null); - - try { - const [lotsResult, logisticsResult, tonnageResult] = await Promise.all([ - getLotOptions(), - getLogisticsOptions(), - getVehicleTonnageOptions(), - ]); - - if (lotsResult.success && lotsResult.data) { - setLotOptions(lotsResult.data); - } - if (logisticsResult.success && logisticsResult.data) { - setLogisticsOptions(logisticsResult.data); - } - if (tonnageResult.success && tonnageResult.data) { - setVehicleTonnageOptions(tonnageResult.data); - } - } catch (err) { - if (isNextRedirectError(err)) throw err; - console.error('[ShipmentCreate] loadOptions error:', err); - setError('옵션 데이터를 불러오는 중 오류가 발생했습니다.'); - } finally { - setIsLoading(false); - } - }, []); - - useEffect(() => { - loadOptions(); - }, [loadOptions]); - - // DevToolbar 자동 채우기 - useDevFill( - 'shipment', - useCallback(() => { - const lotOptionsForGenerator = lotOptions.map(o => ({ - lotNo: o.value, - customerName: o.customerName, - siteName: o.siteName, - })); - const logisticsOptionsForGenerator = logisticsOptions.map(o => ({ - id: o.value, - name: o.label, - })); - const tonnageOptionsForGenerator = vehicleTonnageOptions.map(o => ({ - value: o.value, - label: o.label, - })); - const sampleData = generateShipmentData({ - lotOptions: lotOptionsForGenerator as unknown as LotOption[], - logisticsOptions: logisticsOptionsForGenerator as unknown as LogisticsOption[], - tonnageOptions: tonnageOptionsForGenerator, - }); - setFormData(prev => ({ ...prev, ...sampleData })); - toast.success('[Dev] 출고 폼이 자동으로 채워졌습니다.'); - }, [lotOptions, logisticsOptions, vehicleTonnageOptions]) - ); - - // LOT 선택 시 현장명/수주처 자동 매핑 + 목데이터 제품 표시 - const handleLotChange = useCallback((lotNo: string) => { - setFormData(prev => ({ ...prev, lotNo })); - if (lotNo) { - // 목데이터로 제품 그룹 표시 - setProductGroups(mockProductGroups); - setOtherParts(mockOtherParts); - } else { - setProductGroups([]); - setOtherParts([]); - } - if (validationErrors.lotNo) { - setValidationErrors(prev => { const { lotNo: _, ...rest } = prev; return rest; }); - } - }, [validationErrors]); - - // 배송방식에 따라 운임비용 '없음' 고정 여부 판단 - const isFreightCostLocked = (method: DeliveryMethod) => - method === 'direct_dispatch' || method === 'self_pickup'; - - // 폼 입력 핸들러 - const handleInputChange = (field: keyof ShipmentCreateFormData, value: string) => { - if (field === 'deliveryMethod') { - const method = value as DeliveryMethod; - if (isFreightCostLocked(method)) { - setFormData(prev => ({ ...prev, deliveryMethod: method, freightCost: 'none' as FreightCostType })); - } else { - setFormData(prev => ({ ...prev, deliveryMethod: method })); - } - } else { - setFormData(prev => ({ ...prev, [field]: value })); - } - if (validationErrors[field]) { - setValidationErrors(prev => { - const next = { ...prev }; - delete next[field]; - return next; - }); - } - }; - - // 배차 정보 핸들러 - const handleDispatchChange = (index: number, field: keyof VehicleDispatch, value: string) => { - setFormData(prev => { - const newDispatches = [...prev.vehicleDispatches]; - newDispatches[index] = { ...newDispatches[index], [field]: value }; - return { ...prev, vehicleDispatches: newDispatches }; - }); - }; - - const handleAddDispatch = () => { - setFormData(prev => ({ - ...prev, - vehicleDispatches: [...prev.vehicleDispatches, createEmptyDispatch()], - })); - }; - - const handleRemoveDispatch = (index: number) => { - setFormData(prev => ({ - ...prev, - vehicleDispatches: prev.vehicleDispatches.filter((_, i) => i !== index), - })); - }; - - // 아코디언 제어 - const handleExpandAll = useCallback(() => { - const allIds = [ - ...productGroups.map(g => g.id), - ...(otherParts.length > 0 ? ['other-parts'] : []), - ]; - setAccordionValue(allIds); - }, [productGroups, otherParts]); - - const handleCollapseAll = useCallback(() => { - setAccordionValue([]); - }, []); - - const handleCancel = useCallback(() => { - router.push('/ko/outbound/shipments'); - }, [router]); - - const validateForm = (): boolean => { - const errors: Record = {}; - if (!formData.lotNo) errors.lotNo = '로트번호는 필수 선택 항목입니다.'; - if (!formData.scheduledDate) errors.scheduledDate = '출고예정일은 필수 입력 항목입니다.'; - if (!formData.deliveryMethod) errors.deliveryMethod = '배송방식은 필수 선택 항목입니다.'; - setValidationErrors(errors); - if (Object.keys(errors).length > 0) { - const firstError = Object.values(errors)[0]; - toast.error(firstError); - } - return Object.keys(errors).length === 0; - }; - - const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { - if (!validateForm()) return { success: false, error: '' }; - - setIsSubmitting(true); - try { - const result = await createShipment(formData); - if (!result.success) { - return { success: false, error: result.error || '출고 등록에 실패했습니다.' }; - } - return { success: true }; - } catch (err) { - if (isNextRedirectError(err)) throw err; - console.error('[ShipmentCreate] handleSubmit error:', err); - const errorMessage = err instanceof Error ? err.message : '저장 중 오류가 발생했습니다.'; - return { success: false, error: errorMessage }; - } finally { - setIsSubmitting(false); - } - }, [formData]); - - // 제품 부품 테이블 렌더링 - const renderPartsTable = (parts: ProductPart[]) => ( - - - - 순번 - 품목명 - 규격 - 수량 - 단위 - - - - {parts.map((part) => ( - - {part.seq} - {part.itemName} - {part.specification} - {part.quantity} - {part.unit} - - ))} - -
- ); - - // LOT에서 선택한 정보 표시 - const selectedLot = lotOptions.find(o => o.value === formData.lotNo); - - // 폼 컨텐츠 렌더링 - const renderFormContent = useCallback((_props: { formData: Record; onChange: (key: string, value: unknown) => void; mode: string; errors: Record }) => ( -
- {/* 카드 1: 기본 정보 */} - - - 기본 정보 - - -
- {/* 출고번호 - 자동생성 */} -
-
출고번호
-
자동생성
-
- {/* 로트번호 - Select */} -
-
로트번호 *
- - {validationErrors.lotNo &&

{validationErrors.lotNo}

} -
- {/* 현장명 - LOT 선택 시 자동 매핑 */} -
-
현장명
-
{selectedLot?.siteName || '-'}
-
- {/* 수주처 - LOT 선택 시 자동 매핑 */} -
-
수주처
-
{selectedLot?.customerName || '-'}
-
-
-
-
- - {/* 카드 2: 수주/배송 정보 */} - - - 수주/배송 정보 - - -
-
- - handleInputChange('scheduledDate', date)} - disabled={isSubmitting} - className={validationErrors.scheduledDate ? 'border-red-500' : ''} - /> - {validationErrors.scheduledDate &&

{validationErrors.scheduledDate}

} -
-
- - handleInputChange('shipmentDate', date)} - disabled={isSubmitting} - /> -
-
- - - {validationErrors.deliveryMethod &&

{validationErrors.deliveryMethod}

} -
-
- - -
-
-
-
- - handleInputChange('receiver', e.target.value)} - placeholder="수신자명" - disabled={isSubmitting} - /> -
-
- - handleInputChange('receiverContact', e.target.value)} - placeholder="수신처" - disabled={isSubmitting} - /> -
-
- {/* 주소 */} -
- -
- - -
- - handleInputChange('addressDetail', e.target.value)} - placeholder="상세주소" - disabled={isSubmitting} - /> -
-
-
- - {/* 카드 3: 배차 정보 */} - - - 배차 정보 - - - - - - - 물류업체 - 입차일시 - 구분 - 차량번호 - 기사연락처 - 비고 - - - - - {formData.vehicleDispatches.map((dispatch, index) => ( - - - - - - handleDispatchChange(index, 'arrivalDateTime', val)} - size="sm" - disabled={isSubmitting} - /> - - - - - - handleDispatchChange(index, 'vehicleNo', e.target.value)} - placeholder="차량번호" - className="h-8" - disabled={isSubmitting} - /> - - - handleDispatchChange(index, 'driverContact', e.target.value)} - placeholder="연락처" - className="h-8" - disabled={isSubmitting} - /> - - - handleDispatchChange(index, 'remarks', e.target.value)} - placeholder="비고" - className="h-8" - disabled={isSubmitting} - /> - - - {formData.vehicleDispatches.length > 1 && ( - - )} - - - ))} - -
-
-
- - {/* 카드 4: 제품내용 (읽기전용 - LOT 선택 시 표시) */} - - - 제품내용 - {productGroups.length > 0 && ( - - - - - - - 모두 펼치기 - - - 모두 접기 - - - - )} - - - {productGroups.length > 0 || otherParts.length > 0 ? ( - - {productGroups.map((group: ProductGroup) => ( - - -
- {group.productName} - - ({group.specification}) - - - {group.partCount}개 부품 - -
-
- - {renderPartsTable(group.parts)} - -
- ))} - {otherParts.length > 0 && ( - - -
- 기타부품 - - {otherParts.length}개 부품 - -
-
- - {renderPartsTable(otherParts)} - -
- )} -
- ) : ( -
- {formData.lotNo ? '제품 정보를 불러오는 중...' : '로트를 선택하면 제품 목록이 표시됩니다.'} -
- )} -
-
-
- ), [ - formData, validationErrors, isSubmitting, lotOptions, logisticsOptions, - vehicleTonnageOptions, selectedLot, productGroups, otherParts, accordionValue, - handleLotChange, handleExpandAll, handleCollapseAll, openPostcode, - ]); - - if (error) { - return ( - ; onChange: (key: string, value: unknown) => void; mode: string; errors: Record }) => ( -
{error}
- )} - /> - ); - } - - return ( - { - return await handleSubmit(); - }} - renderForm={renderFormContent} - /> - ); -} diff --git a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx index b4a61b11..a6214004 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx @@ -72,6 +72,8 @@ import type { } from './types'; import { ShippingSlip } from './documents/ShippingSlip'; import { DeliveryConfirmation } from './documents/DeliveryConfirmation'; +import type { OrderDocumentDetail } from './documents/ShipmentOrderDocument'; +import { getOrderDocumentDetail } from '@/components/orders/actions'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { toast } from 'sonner'; @@ -90,6 +92,7 @@ const STATUS_TRANSITIONS: Record = { export function ShipmentDetail({ id }: ShipmentDetailProps) { const router = useRouter(); const [previewDocument, setPreviewDocument] = useState<'shipping' | 'delivery' | null>(null); + const [orderDetail, setOrderDetail] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -139,6 +142,21 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) { loadData(); }, [loadData]); + // 문서 모달 열 때 수주 BOM 데이터 로드 + useEffect(() => { + if (!previewDocument || !detail?.orderId) { + if (!previewDocument) setOrderDetail(null); + return; + } + getOrderDocumentDetail(String(detail.orderId)).then((result) => { + if (result.success && result.data) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const raw = result.data as Record; + setOrderDetail((raw?.data ?? raw) as OrderDocumentDetail); + } + }); + }, [previewDocument, detail?.orderId]); + const _handleGoBack = useCallback(() => { router.push('/ko/outbound/shipments'); }, [router]); @@ -340,7 +358,8 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
- {renderInfoField('로트번호', detail.lotNo)} + {renderInfoField('출고로트', detail.lotNo || detail.shipmentNo)} + {renderInfoField('수주로트', detail.orderLotNo)} {renderInfoField('현장명', detail.siteName)} {renderInfoField('수주처', detail.customerName)} {renderInfoField('작성자', detail.registrant)} @@ -543,8 +562,8 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) { > {detail && ( <> - {previewDocument === 'shipping' && } - {previewDocument === 'delivery' && } + {previewDocument === 'shipping' && } + {previewDocument === 'delivery' && } )} diff --git a/src/components/outbound/ShipmentManagement/ShipmentList.tsx b/src/components/outbound/ShipmentManagement/ShipmentList.tsx index d9e4a478..1105f072 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentList.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentList.tsx @@ -21,7 +21,6 @@ import { Clock, CheckCircle2, Eye, - Plus, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -92,11 +91,6 @@ export function ShipmentList() { [router] ); - // ===== 등록 핸들러 ===== - const handleCreate = useCallback(() => { - router.push('/ko/outbound/shipments?mode=new'); - }, [router]); - // ===== 통계 카드 (3개: 당일 출고대기, 출고대기, 출고완료) ===== const stats: StatCard[] = useMemo( () => [ @@ -218,17 +212,11 @@ export function ShipmentList() { onEndDateChange: setEndDate, }, - // 등록 버튼 - createButton: { - label: '출고 등록', - onClick: handleCreate, - icon: Plus, - }, - - // 테이블 컬럼 (11개) + // 테이블 컬럼 (12개) columns: [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, - { key: 'lotNo', label: '로트번호', className: 'min-w-[120px]', copyable: true }, + { key: 'lotNo', label: '출고로트', className: 'min-w-[120px]', copyable: true }, + { key: 'orderLotNo', label: '수주로트', className: 'min-w-[120px]', copyable: true }, { key: 'siteName', label: '현장명', className: 'min-w-[100px]', copyable: true }, { key: 'orderCustomer', label: '수주처', className: 'min-w-[100px]', copyable: true }, { key: 'receiver', label: '수신자', className: 'w-[80px] text-center', copyable: true }, @@ -300,7 +288,8 @@ export function ShipmentList() { /> {globalIndex} - {item.lotNo || item.shipmentNo || '-'} + {item.lotNo?.trim() || item.shipmentNo || '-'} + {item.orderLotNo || '-'} {item.siteName} {item.orderCustomer || item.customerName || '-'} {item.receiver || '-'} @@ -350,7 +339,8 @@ export function ShipmentList() { } infoGrid={
- + + @@ -403,7 +393,7 @@ export function ShipmentList() { /> ), }), - [stats, startDate, endDate, scheduleEvents, calendarDate, scheduleView, handleRowClick, handleCreate, handleCalendarDateClick, handleCalendarEventClick] + [stats, startDate, endDate, scheduleEvents, calendarDate, scheduleView, handleRowClick, handleCalendarDateClick, handleCalendarEventClick] ); return ; diff --git a/src/components/outbound/ShipmentManagement/actions.ts b/src/components/outbound/ShipmentManagement/actions.ts index 9536b316..d07ac3e0 100644 --- a/src/components/outbound/ShipmentManagement/actions.ts +++ b/src/components/outbound/ShipmentManagement/actions.ts @@ -31,9 +31,7 @@ import type { ShipmentPriority, DeliveryMethod, FreightCostType, - ShipmentCreateFormData, ShipmentEditFormData, - LotOption, LogisticsOption, VehicleTonnageOption, } from './types'; @@ -151,6 +149,7 @@ function transformApiToListItem(data: ShipmentApiData): ShipmentItem { id: String(data.id), shipmentNo: data.shipment_no, lotNo: data.lot_no || '', + orderLotNo: data.order_info?.order_no || '', scheduledDate: data.scheduled_date, status: data.status, priority: data.priority, @@ -189,10 +188,40 @@ function transformApiToProduct(data: ShipmentItemApiData): ShipmentProduct { // ===== API → Frontend 변환 (상세용) ===== function transformApiToDetail(data: ShipmentApiData): ShipmentDetail { + // items를 floor_unit 기준으로 productGroups 자동 그룹핑 + const rawItems = data.items || []; + const items = rawItems.map(transformApiToProduct); + const groupMap = new Map(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const raw = rawItems[i]; + const key = item.floorUnit || `item-${item.id}`; + if (!groupMap.has(key)) { + groupMap.set(key, { productName: key, specification: '', parts: [] }); + } + groupMap.get(key)!.parts.push({ product: item, unit: raw.unit || '' }); + } + const productGroups = Array.from(groupMap.entries()).map(([key, g]) => ({ + id: key, + productName: g.productName, + specification: g.parts[0]?.product.specification || '', + partCount: g.parts.length, + parts: g.parts.map((p, i) => ({ + id: p.product.id, + seq: i + 1, + itemName: p.product.itemName, + specification: p.product.specification, + quantity: p.product.quantity, + unit: p.unit, + })), + })); + return { id: String(data.id), + orderId: data.order_id ?? data.order_info?.order_id, shipmentNo: data.shipment_no, lotNo: data.lot_no || '', + orderLotNo: data.order_info?.order_no || '', scheduledDate: data.scheduled_date, shipmentDate: (data as unknown as Record).shipment_date as string | undefined, status: data.status, @@ -238,10 +267,10 @@ function transformApiToDetail(data: ShipmentApiData): ShipmentDetail { remarks: '', }] : []), - // 제품내용 (그룹핑) - 프론트엔드에서 그룹핑 처리 - productGroups: [], + // 제품내용 (그룹핑) - floor_unit 기준 자동 그룹핑 + productGroups, otherParts: [], - products: (data.items || []).map(transformApiToProduct), + products: items, logisticsCompany: data.logistics_company, vehicleTonnage: data.vehicle_tonnage, shippingCost: data.shipping_cost ? parseFloat(String(data.shipping_cost)) : undefined, @@ -253,11 +282,12 @@ function transformApiToDetail(data: ShipmentApiData): ShipmentDetail { } // ===== API → Frontend 변환 (통계용) ===== -function transformApiToStats(data: ShipmentApiStatsResponse & { total_count?: number }): ShipmentStats { +function transformApiToStats(data: ShipmentApiStatsResponse & { total_count?: number; completed_count?: number; ready_count?: number }): ShipmentStats { return { todayShipmentCount: data.today_shipment_count, scheduledCount: data.scheduled_count, shippingCount: data.shipping_count, + completedCount: data.completed_count || 0, urgentCount: data.urgent_count, totalCount: data.total_count || 0, }; @@ -285,37 +315,6 @@ function transformApiToStatsByStatus(data: ShipmentApiStatsByStatusResponse): Sh return result; } -// ===== Frontend → API 변환 (등록용) ===== -function transformCreateFormToApi( - data: ShipmentCreateFormData -): Record { - const result: Record = { - lot_no: data.lotNo, - scheduled_date: data.scheduledDate, - priority: data.priority, - delivery_method: data.deliveryMethod, - logistics_company: data.logisticsCompany, - vehicle_tonnage: data.vehicleTonnage, - loading_time: data.loadingTime, - loading_manager: data.loadingManager, - remarks: data.remarks, - }; - - if (data.vehicleDispatches && data.vehicleDispatches.length > 0) { - result.vehicle_dispatches = data.vehicleDispatches.map((vd, idx) => ({ - seq: idx + 1, - logistics_company: vd.logisticsCompany || null, - arrival_datetime: vd.arrivalDateTime || null, - tonnage: vd.tonnage || null, - vehicle_no: vd.vehicleNo || null, - driver_contact: vd.driverContact || null, - remarks: vd.remarks || null, - })); - } - - return result; -} - // ===== Frontend → API 변환 (수정용) ===== function transformEditFormToApi( data: Partial @@ -423,22 +422,6 @@ export async function getShipmentById(id: string): Promise<{ success: boolean; d return { success: result.success, data: result.data, error: result.error }; } -// ===== 출고 등록 ===== -export async function createShipment( - data: ShipmentCreateFormData -): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> { - const apiData = transformCreateFormToApi(data); - const result = await executeServerAction({ - url: buildApiUrl('/api/v1/shipments'), - method: 'POST', - body: apiData, - transform: (d: ShipmentApiData) => transformApiToDetail(d), - errorMessage: '출고 등록에 실패했습니다.', - }); - if (result.__authError) return { success: false, __authError: true }; - return { success: result.success, data: result.data, error: result.error }; -} - // ===== 출고 수정 ===== export async function updateShipment( id: string, data: Partial @@ -493,16 +476,6 @@ export async function deleteShipment(id: string): Promise<{ success: boolean; er return { success: result.success, error: result.error }; } -// ===== LOT 옵션 조회 ===== -export async function getLotOptions(): Promise<{ success: boolean; data: LotOption[]; error?: string; __authError?: boolean }> { - const result = await executeServerAction({ - url: buildApiUrl('/api/v1/shipments/options/lots'), - errorMessage: 'LOT 옵션 조회에 실패했습니다.', - }); - if (result.__authError) return { success: false, data: [], __authError: true }; - return { success: result.success, data: result.data || [], error: result.error }; -} - // ===== 물류사 옵션 조회 ===== export async function getLogisticsOptions(): Promise<{ success: boolean; data: LogisticsOption[]; error?: string; __authError?: boolean }> { const result = await executeServerAction({ diff --git a/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx b/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx index e3db4f54..6355f492 100644 --- a/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx +++ b/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx @@ -6,12 +6,13 @@ */ import type { ShipmentDetail } from '../types'; -import { ShipmentOrderDocument } from './ShipmentOrderDocument'; +import { ShipmentOrderDocument, type OrderDocumentDetail } from './ShipmentOrderDocument'; interface DeliveryConfirmationProps { data: ShipmentDetail; + orderDetail?: OrderDocumentDetail | null; } -export function DeliveryConfirmation({ data }: DeliveryConfirmationProps) { - return ; -} +export function DeliveryConfirmation({ data, orderDetail }: DeliveryConfirmationProps) { + return ; +} \ No newline at end of file diff --git a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx index 8d9c9161..34a732a2 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx @@ -4,6 +4,7 @@ * 출고 문서 공통 컴포넌트 (기획서 D1.8 기준 리디자인) * - 출고증: showDispatchInfo + showLotColumn * - 납품확인서: 기본값 (배차정보 없음, LOT 컬럼 없음) + * - BOM 데이터는 orderDetail props로 전달 (수주서와 동일 API 활용) */ import { useState } from 'react'; @@ -12,78 +13,127 @@ import { DELIVERY_METHOD_LABELS } from '../types'; import { ConstructionApprovalTable } from '@/components/document-system'; import { formatNumber } from '@/lib/utils/amount'; +// ===== BOM 데이터 타입 (수주서와 동일) ===== + +interface ProductRow { + no: number; + floor?: string; + symbol?: string; + product_name?: string; + product_code?: string; + product_type?: string; + open_width?: number | string; + open_height?: number | string; + made_width?: number | string; + made_height?: number | string; + guide_rail?: string; + shaft?: string | number; + case_inch?: string | number; + bracket?: string; + capacity?: string | number; + finish?: string; + joint_bar?: number | null; +} + +interface MotorRow { + item: string; + type: string; + spec: string; + qty: number; + lot?: string; +} + +interface BendingItem { + name: string; + spec: string; + qty: number; +} + +interface BendingGroup { + group: string; + items: BendingItem[]; +} + +interface SubsidiaryItem { + name: string; + spec: string; + qty: number; +} + +export interface OrderDocumentDetail { + products?: ProductRow[]; + motors?: { left?: MotorRow[]; right?: MotorRow[] }; + bending_parts?: BendingGroup[]; + subsidiary_parts?: SubsidiaryItem[]; + category_code?: string; + // 수주 기본 정보 + order_no?: string; + received_at?: string; + delivery_date?: string; + client_name?: string; + client_contact?: string; + manager_name?: string; + receiver?: string; + receiver_contact?: string; + shipping_address?: string; + shipping_address_detail?: string; +} + interface ShipmentOrderDocumentProps { title: string; data: ShipmentDetail; + orderDetail?: OrderDocumentDetail | null; showDispatchInfo?: boolean; showLotColumn?: boolean; } -// ===== 문서 전용 목데이터 ===== - -const MOCK_SCREEN_ROWS = [ - { no: 1, type: '이(마)', code: 'FA123', openW: 4300, openH: 4300, madeW: 4300, madeH: 3000, guideRail: '백면형', shaft: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS마감' }, - { no: 2, type: '이(마)', code: 'FA123', openW: 4300, openH: 4300, madeW: 4300, madeH: 3000, guideRail: '백면형', shaft: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS마감' }, -]; - -const MOCK_STEEL_ROWS = [ - { no: 1, code: 'FA123', openW: 4300, openH: 3000, madeW: 4300, madeH: 3000, guideRail: '백면형', shaft: 5, jointBar: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS마감' }, - { no: 2, code: 'FA123', openW: 4300, openH: 3000, madeW: 4300, madeH: 3000, guideRail: '백면형', shaft: 5, jointBar: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS마감' }, -]; - -const MOCK_MOTOR_LEFT = [ - { item: '모터', type: '380V 단상', spec: 'KD-150K', qty: 6, lot: '123123' }, - { item: '브라켓트', type: '-', spec: '380X180', qty: 6, lot: '123123' }, - { item: '앵글', type: '밑침통 영금', spec: '40*40*380', qty: 4, lot: '123123' }, -]; - -const MOCK_MOTOR_RIGHT = [ - { item: '전동개폐기', type: '릴박스', spec: '-', qty: 1, lot: '123123' }, - { item: '전동개폐기', type: '매입', spec: '-', qty: 1, lot: '123123' }, -]; - -const MOCK_GUIDE_RAIL_ITEMS = [ - { name: '항목명', spec: 'L: 3,000', qty: 22 }, - { name: '하부BASE', spec: '130X80', qty: 22 }, -]; - -const MOCK_GUIDE_SMOKE = { name: '연기차단재(W50)', spec: '2,438', qty: 4 }; - -const MOCK_CASE_ITEMS = [ - { name: '500X330', spec: 'L: 4,000', qty: 3 }, - { name: '500X330', spec: 'L: 5,000', qty: 4 }, - { name: '상부덮개', spec: '1219X389', qty: 55 }, - { name: '측면부 (마구리)', spec: '500X355', qty: '500X355' }, -]; - -const MOCK_CASE_SMOKE = { name: '연기차단재(W80)', spec: '3,000', qty: 4 }; - -const MOCK_BOTTOM_SCREEN = [ - { name: '하단마감재', spec: '60X40', l1: 'L: 3,000', q1: 6, name2: '하단마감재', spec2: '60X40', l2: 'L: 4,000', q2: 6 }, - { name: '하단보강엘비', spec: '60X17', l1: 'L: 3,000', q1: 6, name2: '하단보강엘비', spec2: '60X17', l2: 'L: 4,000', q2: 6 }, - { name: '하단보강평철', spec: '-', l1: 'L: 3,000', q1: 6, name2: '하단보강평철', spec2: '-', l2: 'L: 4,000', q2: 6 }, - { name: '하단무게평철', spec: '50X12T', l1: 'L: 3,000', q1: 6, name2: '하단무게평철', spec2: '50X12T', l2: 'L: 4,000', q2: 6 }, -]; - -const MOCK_BOTTOM_STEEL = { spec: '60X40', length: 'L: 3,000', qty: 22 }; - -const MOCK_SUBSIDIARY = [ - { leftItem: '감기사프트', leftSpec: '4인치 4500', leftQty: 6, rightItem: '각파이프', rightSpec: '6000', rightQty: 4 }, - { leftItem: '조인트바', leftSpec: '300', leftQty: 6, rightItem: '환봉', rightSpec: '3000', rightQty: 5 }, -]; - // ===== 공통 스타일 ===== const thBase = 'border-r border-gray-400 px-1 py-1'; const tdBase = 'border-r border-gray-300 px-1 py-1'; const tdCenter = `${tdBase} text-center`; const imgPlaceholder = 'flex items-center justify-center border border-dashed border-gray-300 text-gray-400'; -export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, showLotColumn = false }: ShipmentOrderDocumentProps) { +export function ShipmentOrderDocument({ title, data, orderDetail, showDispatchInfo = false, showLotColumn = false }: ShipmentOrderDocumentProps) { const [bottomFinishView, setBottomFinishView] = useState<'screen' | 'steel'>('screen'); const deliveryMethodLabel = DELIVERY_METHOD_LABELS[data.deliveryMethod] || '-'; const fullAddress = [data.address, data.addressDetail].filter(Boolean).join(' ') || data.deliveryAddress || '-'; - const motorRows = Math.max(MOCK_MOTOR_LEFT.length, MOCK_MOTOR_RIGHT.length); + + // BOM 데이터 (orderDetail에서 추출) + const productRows = orderDetail?.products || []; + const motorsLeft = orderDetail?.motors?.left || []; + const motorsRight = orderDetail?.motors?.right || []; + const bendingParts = orderDetail?.bending_parts || []; + const subsidiaryParts = orderDetail?.subsidiary_parts || []; + + const motorRows = Math.max(motorsLeft.length, motorsRight.length); + + // 스크린/철재 제품 분리 + const screenProducts = productRows.filter(p => p.product_type !== 'steel'); + const steelProducts = productRows.filter(p => p.product_type === 'steel'); + + // 절곡물 그룹 데이터 추출 + const guideRailItems = bendingParts.find(g => g.group === '가이드레일')?.items ?? []; + const caseItems = bendingParts.find(g => g.group === '케이스')?.items ?? []; + const bottomItems = bendingParts.find(g => g.group === '하단마감')?.items ?? []; + const smokeItems = bendingParts.find(g => g.group === '연기차단재')?.items ?? []; + const guideSmokeItems = smokeItems.filter(i => i.name.includes('레일') || i.name.includes('가이드')); + const caseSmokeItems = smokeItems.filter(i => i.name.includes('케이스')); + const otherSmokeItems = smokeItems.filter(i => + !i.name.includes('레일') && !i.name.includes('가이드') && !i.name.includes('케이스') + ); + + // 부자재 좌/우 2열 변환 + const subsidiaryRows = []; + for (let i = 0; i < subsidiaryParts.length; i += 2) { + subsidiaryRows.push({ + left: subsidiaryParts[i], + right: subsidiaryParts[i + 1] ?? null, + }); + } + + // 셔터 수량: productRows 기준 (없으면 productGroups fallback) + const shutterCount = productRows.length || data.productGroups.length; return (
@@ -98,7 +148,7 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
- + {/* ========== 로트번호 / 제품명 / 제품코드 / 인정번호 ========== */} @@ -106,13 +156,13 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s 로트번호 - {data.lotNo} + {data.orderLotNo || data.lotNo} 제품명 - {data.productGroups[0]?.productName || '-'} + {productRows[0]?.product_name || data.productGroups[0]?.productName || '-'} 제품코드 - KWS01 + {productRows[0]?.product_code || '-'} 인정번호 - ABC1234 + {orderDetail?.category_code || '-'} @@ -127,7 +177,7 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s 수주일 - {data.scheduledDate} + {orderDetail?.received_at || data.scheduledDate} 수주처 @@ -135,11 +185,11 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s 수주 담당자 - {data.registrant || '-'} + {orderDetail?.manager_name || data.registrant || '-'} 담당자 연락처 - {data.driverContact || '-'} + {orderDetail?.client_contact || data.driverContact || '-'} @@ -156,7 +206,7 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s 납기요청일 - {data.scheduledDate} + {orderDetail?.delivery_date || data.scheduledDate} 출고일 @@ -164,7 +214,7 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s 셔터출수량 - {data.productGroups.length}개소 + {shutterCount}개소 @@ -234,404 +284,390 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s ); })()} - {/* ========== 자재 및 철거 내역 헤더 ========== */} -
- 자재 및 철거 내역 -
+

아래와 같이 주문하오니 품질 및 납기일을 준수하여 주시기 바랍니다.

{/* ========== 1. 스크린 ========== */} -
-

1. 스크린

-
- - - - - - - - - - - - - - - - - - - - - - - - - {MOCK_SCREEN_ROWS.map((row) => ( - - - - - - - - - - - - - - + {screenProducts.length > 0 && ( +
+

1. 스크린

+
+
No품류부호오픈사이즈제작사이즈가이드
레일
사프트
(인치)
케이스
(인치)
모터마감
가로세로가로세로브라켓트용량Kg
{row.no}{row.type}{row.code}{formatNumber(row.openW)}{formatNumber(row.openH)}{formatNumber(row.madeW)}{formatNumber(row.madeH)}{row.guideRail}{row.shaft}{row.caseInch}{row.bracket}{row.capacity}{row.finish}
+ + + + + + + + + + + + - ))} - -
No품류부호오픈사이즈제작사이즈가이드
레일
사프트
(인치)
케이스
(인치)
모터마감
+ + 가로 + 세로 + 가로 + 세로 + 브라켓트 + 용량Kg + + + + {screenProducts.map((row) => ( + + {row.no} + {row.floor ?? '-'} + {row.symbol ?? '-'} + {row.open_width ? formatNumber(Number(row.open_width)) : '-'} + {row.open_height ? formatNumber(Number(row.open_height)) : '-'} + {row.made_width ? formatNumber(Number(row.made_width)) : '-'} + {row.made_height ? formatNumber(Number(row.made_height)) : '-'} + {row.guide_rail ?? '-'} + {row.shaft ?? '-'} + {row.case_inch ?? '-'} + {row.bracket ?? '-'} + {row.capacity ?? '-'} + {row.finish ?? '-'} + + ))} + + +
- + )} - {/* ========== 2. 절재 ========== */} -
-

2. 절재

-
- - - - - - - - - - - - - - - - - - - - - - - - - {MOCK_STEEL_ROWS.map((row) => ( - - - - - - - - - - - - - - + {/* ========== 2. 철재 ========== */} + {steelProducts.length > 0 && ( +
+

2. 철재

+
+
No.부호오픈사이즈제작사이즈가이드
레일
사프트
(인치)
조인트바
(규격)
케이스
(인치)
모터마감
가로세로가로세로브라켓트용량Kg
{row.no}{row.code}{formatNumber(row.openW)}{formatNumber(row.openH)}{formatNumber(row.madeW)}{formatNumber(row.madeH)}{row.guideRail}{row.shaft}{row.jointBar}{row.caseInch}{row.bracket}{row.capacity}{row.finish}
+ + + + + + + + + + + + - ))} - -
No.부호오픈사이즈제작사이즈가이드
레일
사프트
(인치)
조인트바
(규격)
케이스
(인치)
모터마감
+ + 가로 + 세로 + 가로 + 세로 + 브라켓트 + 용량Kg + + + + {steelProducts.map((row) => ( + + {row.no} + {row.symbol ?? '-'} + {row.open_width ? formatNumber(Number(row.open_width)) : '-'} + {row.open_height ? formatNumber(Number(row.open_height)) : '-'} + {row.made_width ? formatNumber(Number(row.made_width)) : '-'} + {row.made_height ? formatNumber(Number(row.made_height)) : '-'} + {row.guide_rail ?? '-'} + {row.shaft ?? '-'} + {row.joint_bar ?? '-'} + {row.case_inch ?? '-'} + {row.bracket ?? '-'} + {row.capacity ?? '-'} + {row.finish ?? '-'} + + ))} + + +
- + )} {/* ========== 3. 모터 ========== */} -
-

3. 모터

-
- - - - - - - - {showLotColumn && } - - - - - {showLotColumn && } - - - - {Array.from({ length: motorRows }).map((_, i) => { - const left = MOCK_MOTOR_LEFT[i]; - const right = MOCK_MOTOR_RIGHT[i]; - return ( - - - - - - {showLotColumn && } - - - - - {showLotColumn && } - - ); - })} - -
항목구분규격수량입고 LOT항목구분규격수량입고 LOT
{left?.item || ''}{left?.type || ''}{left?.spec || ''}{left?.qty ?? ''}{left?.lot || ''}{right?.item || ''}{right?.type || ''}{right?.spec || ''}{right?.qty ?? ''}{right?.lot || ''}
+ {motorRows > 0 && ( +
+

3. 모터

+
+ + + + + + + + {showLotColumn && } + + + + + {showLotColumn && } + + + + {Array.from({ length: motorRows }).map((_, i) => { + const left = motorsLeft[i]; + const right = motorsRight[i]; + return ( + + + + + + {showLotColumn && } + + + + + {showLotColumn && } + + ); + })} + +
항목구분규격수량입고 LOT항목구분규격수량입고 LOT
{left?.item || ''}{left?.type || ''}{left?.spec || ''}{left?.qty ?? ''}{left?.lot || ''}{right?.item || ''}{right?.type || ''}{right?.spec || ''}{right?.qty ?? ''}{right?.lot || ''}
+
-
+ )} {/* ========== 4. 절곡물 ========== */} -
-

4. 절곡물

+ {bendingParts.length > 0 && ( +
+

4. 절곡물

- {/* 4-1. 가이드레일 */} -
-

4-1. 가이드레일 - EGI 1.5ST + 마감재 EGI 1.1ST + 별도마감재 SUS 1.1ST

- - {/* 메인 테이블 */} -
- - - - - - - - - - - {MOCK_GUIDE_RAIL_ITEMS.map((item, i) => ( - - {i === 0 && ( - - )} - - - - - ))} - -
백면형 (120X70)항목규격수량
-
IMG
-
{item.name}{item.spec}{item.qty}
-
- - {/* 연기차단재 */} -
- - - - - - - - - - - - - - - - - -
 항목규격수량
-
IMG
-
{MOCK_GUIDE_SMOKE.name}{MOCK_GUIDE_SMOKE.spec}{MOCK_GUIDE_SMOKE.qty}
-
- -

- * 가이드레일 마감재 양측에 설치 - EGI 0.8T + 화이버글라스코팅직물 -

-
- - {/* 4-2. 케이스(셔터박스) */} -
-

4-2. 케이스(셔터박스) - EGI 1.5ST

- - {/* 메인 테이블 */} -
- - - - - - - - - - - {MOCK_CASE_ITEMS.map((item, i) => ( - - {i === 0 && ( - - )} - - - - - ))} - -
 항목규격수량
-
IMG
-
{item.name}{item.spec}{item.qty}
-
- - {/* 연기차단재 */} -
- - - - - - - - - - - - - - - - - -
 항목규격수량
-
IMG
-
{MOCK_CASE_SMOKE.name}{MOCK_CASE_SMOKE.spec}{MOCK_CASE_SMOKE.qty}
-
- -

- * 전면부, 판넬부 양측에 설치 - EGI 0.8T + 화이버글라스코팅직물 -

-
- - {/* 4-3. 하단마감재 (토글: 스크린 / 절재) */} -
- {/* 토글 버튼 */} -
- - -
- - {bottomFinishView === 'screen' ? ( - <> -

- 4-3. 하단마감재 - 하단마감재(EGI 1.5ST) + 하단보강엘비(EGI 1.5ST) + 하단 보강평철(EGI 1.1ST) + 하단 무게평철(50X12T) -

+ {/* 4-1. 가이드레일 */} + {guideRailItems.length > 0 && ( +
+

4-1. 가이드레일

- - - - - - - - + + + + - {MOCK_BOTTOM_SCREEN.map((row, i) => ( + {guideRailItems.map((item, i) => ( - - - - - - - - + {i === 0 && ( + + )} + + + ))}
항목규격길이수량항목규격길이수량 항목규격수량
{row.name}{row.spec}{row.l1}{row.q1}{row.name2}{row.spec2}{row.l2}{row.q2} +
IMG
+
{item.name}{item.spec}{item.qty}
- - ) : ( - <> -

- 4-3. 하단마감재 -EGI 1.5ST + + {/* 가이드레일 연기차단재 */} + {guideSmokeItems.length > 0 && ( +

+ + + + + + + + + + + {guideSmokeItems.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))} + +
 항목규격수량
+
IMG
+
{item.name}{item.spec}{item.qty}
+
+ )} + +

+ * 가이드레일 마감재 양측에 설치 - EGI 0.8T + 화이버글라스코팅직물

+
+ )} + + {/* 4-2. 케이스(셔터박스) */} + {caseItems.length > 0 && ( +
+

4-2. 케이스(셔터박스)

- - - - + + + + - - - - - - + {caseItems.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))}
하단마감재규격길이수량 항목규격수량
-
IMG
-
{MOCK_BOTTOM_STEEL.spec}{MOCK_BOTTOM_STEEL.length}{MOCK_BOTTOM_STEEL.qty}
+
IMG
+
{item.name}{item.spec}{item.qty}
- + + {/* 케이스 연기차단재 */} + {caseSmokeItems.length > 0 && ( +
+ + + + + + + + + + + {caseSmokeItems.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))} + +
 항목규격수량
+
IMG
+
{item.name}{item.spec}{item.qty}
+
+ )} + +

+ * 전면부, 판넬부 양측에 설치 - EGI 0.8T + 화이버글라스코팅직물 +

+
+ )} + + {/* 4-3. 하단마감재 */} + {bottomItems.length > 0 && ( +
+

4-3. 하단마감재

+
+ + + + + + + + + + + {bottomItems.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))} + +
 항목규격수량
+
IMG
+
{item.name}{item.spec}{item.qty}
+
+
+ )} + + {/* 연기차단재 (구분 불가) */} + {otherSmokeItems.length > 0 && ( +
+

연기차단재

+
+ + + + + + + + + + {otherSmokeItems.map((item, i) => ( + + + + + + ))} + +
항목규격수량
{item.name}{item.spec}{item.qty}
+
+
)}
-
+ )} {/* ========== 5. 부자재 ========== */} -
-

5. 부자재

-
- - - - - - - - - - - - - {MOCK_SUBSIDIARY.map((row, i) => ( - - - - - - - + {subsidiaryParts.length > 0 && ( +
+

5. 부자재

+
+
항목규격수량항목규격수량
{row.leftItem}{row.leftSpec}{row.leftQty}{row.rightItem}{row.rightSpec}{row.rightQty}
+ + + + + + + + - ))} - -
항목규격수량항목규격수량
+ + + {subsidiaryRows.map((row, i) => ( + + {row.left?.name ?? ''} + {row.left?.spec ?? ''} + {row.left?.qty ?? ''} + {row.right?.name ?? ''} + {row.right?.spec ?? ''} + {row.right?.qty ?? ''} + + ))} + + +
-
+ )} {/* ========== 특이사항 ========== */} {data.remarks && ( @@ -644,4 +680,4 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s )}
); -} +} \ No newline at end of file diff --git a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx index f76d7e80..1cf0ba74 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx @@ -6,12 +6,13 @@ */ import type { ShipmentDetail } from '../types'; -import { ShipmentOrderDocument } from './ShipmentOrderDocument'; +import { ShipmentOrderDocument, type OrderDocumentDetail } from './ShipmentOrderDocument'; interface ShippingSlipProps { data: ShipmentDetail; + orderDetail?: OrderDocumentDetail | null; } -export function ShippingSlip({ data }: ShippingSlipProps) { - return ; -} +export function ShippingSlip({ data, orderDetail }: ShippingSlipProps) { + return ; +} \ No newline at end of file diff --git a/src/components/outbound/ShipmentManagement/index.ts b/src/components/outbound/ShipmentManagement/index.ts index 7aed8230..ffa8d522 100644 --- a/src/components/outbound/ShipmentManagement/index.ts +++ b/src/components/outbound/ShipmentManagement/index.ts @@ -3,7 +3,6 @@ */ export { ShipmentList } from './ShipmentList'; -export { ShipmentCreate } from './ShipmentCreate'; export { ShipmentDetail } from './ShipmentDetail'; export { ShipmentEdit } from './ShipmentEdit'; diff --git a/src/components/outbound/ShipmentManagement/shipmentConfig.ts b/src/components/outbound/ShipmentManagement/shipmentConfig.ts index 4afb9ccb..0b318272 100644 --- a/src/components/outbound/ShipmentManagement/shipmentConfig.ts +++ b/src/components/outbound/ShipmentManagement/shipmentConfig.ts @@ -37,12 +37,11 @@ export const shipmentConfig: DetailConfig = { }; /** - * 출고 등록 페이지 Config - * IntegratedDetailTemplate 마이그레이션 (2025-01-20) + * 출고 수정 페이지 Config */ -export const shipmentCreateConfig: DetailConfig = { +export const shipmentEditConfig: DetailConfig = { title: '출고', - description: '새로운 출고를 등록합니다', + description: '출고 정보를 수정합니다', icon: Truck, basePath: '/outbound/shipments', fields: [], @@ -53,13 +52,4 @@ export const shipmentCreateConfig: DetailConfig = { showSave: true, submitLabel: '저장', }, -}; - -/** - * 출고 수정 페이지 Config - */ -export const shipmentEditConfig: DetailConfig = { - ...shipmentCreateConfig, - title: '출고', - description: '출고 정보를 수정합니다', -}; +}; \ No newline at end of file diff --git a/src/components/outbound/ShipmentManagement/types.ts b/src/components/outbound/ShipmentManagement/types.ts index f0306f65..1a30a24d 100644 --- a/src/components/outbound/ShipmentManagement/types.ts +++ b/src/components/outbound/ShipmentManagement/types.ts @@ -111,7 +111,8 @@ export const DELIVERY_METHOD_LABELS: Record = { export interface ShipmentItem { id: string; shipmentNo: string; // 출고번호 - lotNo: string; // 로트번호 + lotNo: string; // 출고로트 + orderLotNo?: string; // 수주로트 (수주번호) scheduledDate: string; // 출고예정일 status: ShipmentStatus; // 상태 priority: ShipmentPriority; // 우선순위 @@ -158,8 +159,10 @@ export interface ShipmentProduct { export interface ShipmentDetail { // 기본 정보 (읽기전용) id: string; + orderId?: number; // 수주 ID (문서 BOM 데이터 조회용) shipmentNo: string; // 출고번호 - lotNo: string; // 로트번호 + lotNo: string; // 출고로트 + orderLotNo?: string; // 수주로트 (수주번호) siteName: string; // 현장명 customerName: string; // 수주처 customerGrade: string; // 거래등급