diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx index 046bd585..e7f000d8 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx @@ -30,6 +30,7 @@ import { Circle, Activity, Play, + ChevronDown, } from "lucide-react"; import { PageLayout } from "@/components/organisms/PageLayout"; import { PageHeader } from "@/components/organisms/PageHeader"; @@ -47,143 +48,17 @@ import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { toast } from "sonner"; import { ServerErrorPage } from "@/components/common/ServerErrorPage"; import { formatNumber } from '@/lib/utils/amount'; - -// 생산지시 상태 타입 -type ProductionOrderStatus = "waiting" | "in_progress" | "completed"; - -// 작업지시 상태 타입 -type WorkOrderStatus = "pending" | "in_progress" | "completed"; - -// 작업지시 데이터 타입 -interface WorkOrder { - id: string; - workOrderNumber: string; // KD-WO-XXXXXX-XX - process: string; // 공정명 - quantity: number; - status: WorkOrderStatus; - assignee: string; -} - -// 생산지시 상세 데이터 타입 -interface ProductionOrderDetail { - id: string; - productionOrderNumber: string; - orderNumber: string; - productionOrderDate: string; - dueDate: string; - quantity: number; - status: ProductionOrderStatus; - client: string; - siteName: string; - productType: string; - pendingWorkOrderCount: number; // 생성 예정 작업지시 수 - workOrders: WorkOrder[]; -} - -// 샘플 생산지시 상세 데이터 -const SAMPLE_PRODUCTION_ORDER_DETAILS: Record = { - "PO-001": { - id: "PO-001", - productionOrderNumber: "PO-KD-TS-251217-07", - orderNumber: "KD-TS-251217-07", - productionOrderDate: "2025-12-22", - dueDate: "2026-02-15", - quantity: 2, - status: "completed", // 생산완료 상태 - 목록 버튼만 표시 - client: "호반건설(주)", - siteName: "씨밋 광교 센트럴시티", - productType: "", - pendingWorkOrderCount: 0, // 작업지시 이미 생성됨 - workOrders: [ - { - id: "WO-001", - workOrderNumber: "KD-WO-251217-07", - process: "재단", - quantity: 2, - status: "completed", - assignee: "-", - }, - { - id: "WO-002", - workOrderNumber: "KD-WO-251217-08", - process: "조립", - quantity: 2, - status: "completed", - assignee: "-", - }, - { - id: "WO-003", - workOrderNumber: "KD-WO-251217-09", - process: "검수", - quantity: 2, - status: "completed", - assignee: "-", - }, - ], - }, - "PO-002": { - id: "PO-002", - productionOrderNumber: "PO-KD-TS-251217-09", - orderNumber: "KD-TS-251217-09", - productionOrderDate: "2025-12-22", - dueDate: "2026-02-10", - quantity: 10, - status: "waiting", - client: "태영건설(주)", - siteName: "데시앙 동탄 파크뷰", - productType: "", - pendingWorkOrderCount: 5, // 생성 예정 작업지시 5개 (일괄생성) - workOrders: [], - }, - "PO-003": { - id: "PO-003", - productionOrderNumber: "PO-KD-TS-251217-06", - orderNumber: "KD-TS-251217-06", - productionOrderDate: "2025-12-22", - dueDate: "2026-02-10", - quantity: 1, - status: "waiting", - client: "롯데건설(주)", - siteName: "예술 검실 푸르지오", - productType: "", - pendingWorkOrderCount: 1, // 생성 예정 작업지시 1개 (단일 생성) - workOrders: [], - }, - "PO-004": { - id: "PO-004", - productionOrderNumber: "PO-KD-BD-251220-35", - orderNumber: "KD-BD-251220-35", - productionOrderDate: "2025-12-20", - dueDate: "2026-02-03", - quantity: 3, - status: "in_progress", - client: "현대건설(주)", - siteName: "[코레타스프] 판교 물류센터 철거현장", - productType: "", - pendingWorkOrderCount: 0, - workOrders: [ - { - id: "WO-004", - workOrderNumber: "KD-WO-251220-01", - process: "재단", - quantity: 3, - status: "completed", - assignee: "-", - }, - { - id: "WO-005", - workOrderNumber: "KD-WO-251220-02", - process: "조립", - quantity: 3, - status: "in_progress", - assignee: "-", - }, - ], - }, -}; +import { getProductionOrderDetail } from "@/components/production/ProductionOrders/actions"; +import { createProductionOrder } from "@/components/orders/actions"; +import type { + ProductionOrderDetail, + ProductionStatus, + ProductionWorkOrder, + BomProcessGroup, +} from "@/components/production/ProductionOrders/types"; // 공정 진행 현황 컴포넌트 -function ProcessProgress({ workOrders }: { workOrders: WorkOrder[] }) { +function ProcessProgress({ workOrders }: { workOrders: ProductionWorkOrder[] }) { if (workOrders.length === 0) { return ( @@ -202,7 +77,9 @@ function ProcessProgress({ workOrders }: { workOrders: WorkOrder[] }) { ); } - const completedCount = workOrders.filter((w) => w.status === "completed").length; + const completedCount = workOrders.filter( + (w) => w.status === "completed" || w.status === "shipped" + ).length; const totalCount = workOrders.length; const progressPercent = Math.round((completedCount / totalCount) * 100); @@ -237,25 +114,27 @@ function ProcessProgress({ workOrders }: { workOrders: WorkOrder[] }) {
- {wo.status === "completed" ? ( + {wo.status === "completed" || wo.status === "shipped" ? ( ) : ( )}
- {wo.process} + {wo.processName}
{index < workOrders.length - 1 && (
)} @@ -269,13 +148,13 @@ function ProcessProgress({ workOrders }: { workOrders: WorkOrder[] }) { } // 상태 배지 헬퍼 -function getStatusBadge(status: ProductionOrderStatus) { - const config: Record = { +function getStatusBadge(status: ProductionStatus) { + const config: Record = { waiting: { label: "생산대기", className: "bg-yellow-100 text-yellow-700 border-yellow-200", }, - in_progress: { + in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200", }, @@ -289,22 +168,16 @@ function getStatusBadge(status: ProductionOrderStatus) { } // 작업지시 상태 배지 헬퍼 -function getWorkOrderStatusBadge(status: WorkOrderStatus) { - const config: Record = { - pending: { - label: "대기", - className: "bg-gray-100 text-gray-700 border-gray-200", - }, - in_progress: { - label: "작업중", - className: "bg-blue-100 text-blue-700 border-blue-200", - }, - completed: { - label: "완료", - className: "bg-green-100 text-green-700 border-green-200", - }, +function getWorkOrderStatusBadge(status: string) { + const config: Record = { + unassigned: { label: "미배정", className: "bg-gray-100 text-gray-700 border-gray-200" }, + pending: { label: "대기", className: "bg-gray-100 text-gray-700 border-gray-200" }, + waiting: { label: "준비중", className: "bg-yellow-100 text-yellow-700 border-yellow-200" }, + in_progress: { label: "작업중", className: "bg-blue-100 text-blue-700 border-blue-200" }, + completed: { label: "완료", className: "bg-green-100 text-green-700 border-green-200" }, + shipped: { label: "출하", className: "bg-purple-100 text-purple-700 border-purple-200" }, }; - const c = config[status]; + const c = config[status] || { label: status, className: "bg-gray-100 text-gray-700 border-gray-200" }; return {c.label}; } @@ -318,99 +191,33 @@ function InfoItem({ label, value }: { label: string; value: string }) { ); } -// 샘플 공정 목록 (작업지시 생성 팝업에 표시용) -const SAMPLE_PROCESSES = [ - { id: "P1", name: "1.1 백판필름", quantity: 10 }, - { id: "P2", name: "2. 하안마감재", quantity: 10 }, - { id: "P3", name: "3.1 케이스", quantity: 10 }, - { id: "P4", name: "4. 연기단자", quantity: 10 }, - { id: "P5", name: "5. 가이드레일 하부브라켓", quantity: 10 }, -]; - -// BOM 품목 타입 -interface BomItem { - id: string; - itemCode: string; - itemName: string; - spec: string; - lotNo: string; - requiredQty: number; - qty: number; -} - -// BOM 공정 분류 타입 -interface BomProcessGroup { - processName: string; - sizeSpec?: string; - items: BomItem[]; -} - -// BOM 품목별 공정 분류 목데이터 -const SAMPLE_BOM_PROCESS_GROUPS: BomProcessGroup[] = [ - { - processName: "1.1 백판필름", - sizeSpec: "[20-70]", - items: [ - { id: "B1", itemCode: "①", itemName: "아연판", spec: "EGI.6T", lotNo: "LOT-M-2024-001", requiredQty: 4300, qty: 8 }, - { id: "B2", itemCode: "②", itemName: "가이드레일", spec: "EGI.6T", lotNo: "LOT-M-2024-002", requiredQty: 3000, qty: 4 }, - { id: "B3", itemCode: "③", itemName: "C형", spec: "EGI.6ST", lotNo: "LOT-M-2024-003", requiredQty: 4300, qty: 4 }, - { id: "B4", itemCode: "④", itemName: "D형", spec: "EGI.6T", lotNo: "LOT-M-2024-004", requiredQty: 4300, qty: 4 }, - { id: "B5", itemCode: "⑤", itemName: "행거마감재", spec: "SUS1.2T", lotNo: "LOT-M-2024-005", requiredQty: 4300, qty: 2 }, - { id: "B6", itemCode: "⑥", itemName: "R/RBASE", spec: "EGI.5ST", lotNo: "LOT-M-2024-006", requiredQty: 0, qty: 4 }, - ], - }, - { - processName: "2. 하안마감재", - sizeSpec: "[60-40]", - items: [ - { id: "B7", itemCode: "①", itemName: "하안마감재", spec: "EGI.5T", lotNo: "LOT-M-2024-007", requiredQty: 3000, qty: 4 }, - { id: "B8", itemCode: "②", itemName: "하안보강재판", spec: "EGI.5T", lotNo: "LOT-M-2024-009", requiredQty: 3000, qty: 4 }, - { id: "B9", itemCode: "③", itemName: "하안보강멈틀", spec: "EGI.1T", lotNo: "LOT-M-2024-010", requiredQty: 0, qty: 2 }, - { id: "B10", itemCode: "④", itemName: "행거마감재", spec: "SUS1.2T", lotNo: "LOT-M-2024-008", requiredQty: 3000, qty: 2 }, - ], - }, - { - processName: "3.1 케이스", - sizeSpec: "[500*330]", - items: [ - { id: "B11", itemCode: "①", itemName: "전판", spec: "EGI.5ST", lotNo: "LOT-M-2024-011", requiredQty: 0, qty: 2 }, - { id: "B12", itemCode: "②", itemName: "전멈틀", spec: "EGI.5T", lotNo: "LOT-M-2024-012", requiredQty: 0, qty: 4 }, - { id: "B13", itemCode: "③⑤", itemName: "타부멈틀", spec: "", lotNo: "LOT-M-2024-013", requiredQty: 0, qty: 2 }, - { id: "B14", itemCode: "④", itemName: "좌우판-HW", spec: "", lotNo: "LOT-M-2024-014", requiredQty: 0, qty: 2 }, - { id: "B15", itemCode: "⑥", itemName: "상부판", spec: "", lotNo: "LOT-M-2024-015", requiredQty: 1220, qty: 5 }, - { id: "B16", itemCode: "⑦", itemName: "측판(전산판)", spec: "", lotNo: "LOT-M-2024-016", requiredQty: 0, qty: 2 }, - ], - }, - { - processName: "4. 연기단자", - sizeSpec: "", - items: [ - { id: "B17", itemCode: "-", itemName: "레일홀 (W50)", spec: "EGI.8T + 화이버 금사", lotNo: "LOT-M-2024-017", requiredQty: 0, qty: 4 }, - { id: "B18", itemCode: "-", itemName: "카이드홀 (W60)", spec: "EGI.8T + 화이버 금사", lotNo: "LOT-M-2024-018", requiredQty: 0, qty: 4 }, - ], - }, -]; - export default function ProductionOrderDetailPage() { const router = useRouter(); const params = useParams(); - const productionOrderId = params.id as string; + const orderId = params.id as string; - const [productionOrder, setProductionOrder] = useState(null); + const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [isCreateWorkOrderDialogOpen, setIsCreateWorkOrderDialogOpen] = useState(false); const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false); - const [createdWorkOrders, setCreatedWorkOrders] = useState([]); const [isCreating, setIsCreating] = useState(false); + const [bomOpen, setBomOpen] = useState(false); // 데이터 로드 + const loadDetail = async () => { + setLoading(true); + const result = await getProductionOrderDetail(orderId); + if (result.success && result.data) { + setDetail(result.data); + } else { + setDetail(null); + } + setLoading(false); + }; + useEffect(() => { - setTimeout(() => { - const found = SAMPLE_PRODUCTION_ORDER_DETAILS[productionOrderId]; - setProductionOrder(found || null); - setLoading(false); - }, 300); - }, [productionOrderId]); + loadDetail(); + }, [orderId]); const handleBack = () => { router.push("/sales/order-management-sales/production-orders"); @@ -423,19 +230,13 @@ export default function ProductionOrderDetailPage() { const handleConfirmCreateWorkOrder = async () => { setIsCreating(true); try { - // API 호출 시뮬레이션 - await new Promise((resolve) => setTimeout(resolve, 500)); - - // 생성된 작업지시서 목록 (실제로는 API 응답에서 받음) - const workOrderCount = productionOrder?.pendingWorkOrderCount || 0; - const created = Array.from({ length: workOrderCount }, (_, i) => - `KD-WO-251223-${String(i + 1).padStart(2, "0")}` - ); - setCreatedWorkOrders(created); - - // 확인 팝업 닫고 성공 팝업 열기 - setIsCreateWorkOrderDialogOpen(false); - setIsSuccessDialogOpen(true); + const result = await createProductionOrder(orderId); + if (result.success) { + setIsCreateWorkOrderDialogOpen(false); + setIsSuccessDialogOpen(true); + } else { + toast.error(result.error || "작업지시 생성에 실패했습니다."); + } } finally { setIsCreating(false); } @@ -457,7 +258,7 @@ export default function ProductionOrderDetailPage() { ); } - if (!productionOrder) { + if (!detail) { return ( 0; + const canCreateWorkOrders = detail.productionStatus === "waiting" && !hasWorkOrders; + return ( {/* 헤더 */} @@ -476,9 +280,9 @@ export default function ProductionOrderDetailPage() {
생산지시 상세 - {productionOrder.productionOrderNumber} + {detail.orderNumber} - {getStatusBadge(productionOrder.status)} + {getStatusBadge(detail.productionStatus)}
} icon={Factory} @@ -488,10 +292,7 @@ export default function ProductionOrderDetailPage() { 목록 - {/* 작업지시 생성 버튼 - 완료 아님 + 작업지시 없음 + 생성 예정 있음 */} - {productionOrder.status !== "completed" && - productionOrder.workOrders.length === 0 && - productionOrder.pendingWorkOrderCount > 0 && ( + {canCreateWorkOrders && ( )} - {productionOrder.workOrders.length === 0 ? ( + {!hasWorkOrders ? (

아직 작업지시서가 생성되지 않았습니다.

- {productionOrder.pendingWorkOrderCount > 0 && ( + {canCreateWorkOrders && (

위 버튼을 클릭하여 BOM 기반 작업지시서를 자동 생성하세요.

@@ -649,23 +445,23 @@ export default function ProductionOrderDetailPage() { 작업지시번호 공정 - 수량 + 개소 상태 담당자 - {productionOrder.workOrders.map((wo) => ( + {detail.workOrders.map((wo) => ( - {wo.workOrderNumber} + {wo.workOrderNo} - {wo.process} - {wo.quantity}개 + {wo.processName} + {wo.quantity}개소 {getWorkOrderStatusBadge(wo.status)} - {wo.assignee} + {wo.assignees.length > 0 ? wo.assignees.join(", ") : "-"} ))} @@ -676,7 +472,7 @@ export default function ProductionOrderDetailPage() {
- {/* 팝업1: 작업지시 생성 확인 다이얼로그 */} + {/* 작업지시 생성 확인 다이얼로그 */}

- 다음 공정에 대한 작업지시서가 생성됩니다: + 이 수주에 대한 작업지시서를 자동 생성합니다.

- {productionOrder?.pendingWorkOrderCount && productionOrder.pendingWorkOrderCount > 0 && ( -
    - {SAMPLE_PROCESSES.slice(0, productionOrder.pendingWorkOrderCount).map((process) => ( -
  • - - {process.name} ({process.quantity}개) -
  • - ))} -
- )}

+ BOM 기반으로 공정별 작업지시서가 생성됩니다. 생성된 작업지시서는 생산팀에서 확인하고 작업을 진행할 수 있습니다.

@@ -706,7 +493,7 @@ export default function ProductionOrderDetailPage() { loading={isCreating} /> - {/* 팝업2: 작업지시 생성 성공 다이얼로그 */} + {/* 작업지시 생성 성공 다이얼로그 */} @@ -716,24 +503,9 @@ export default function ProductionOrderDetailPage() {
- {createdWorkOrders.length}개의 작업지시서가 공정별로 자동 생성되었습니다. + 작업지시서가 자동 생성되었습니다.
-
-

생성된 작업지시서:

- {createdWorkOrders.length > 0 ? ( -
    - {createdWorkOrders.map((wo, idx) => ( -
  • - - {wo} -
  • - ))} -
- ) : ( -

-

- )} -

작업지시 관리 페이지로 이동합니다.

@@ -749,4 +521,4 @@ export default function ProductionOrderDetailPage() {
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx index dce9c551..cf874358 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx @@ -4,24 +4,20 @@ * 생산지시 목록 페이지 * * - 수주관리 > 생산지시 보기에서 접근 - * - 진행 단계 바 + * - 진행 단계 바 (Order 상태 기반 동적) * - 필터 탭: 전체, 생산대기, 생산중, 생산완료 (TabChip 사용) * - IntegratedListTemplateV2 템플릿 적용 + * - 서버사이드 페이지네이션 */ -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { DeleteConfirmDialog } from "@/components/ui/confirm-dialog"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, TableRow, + TableCell, } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -29,7 +25,6 @@ import { ArrowLeft, CheckCircle2, Eye, - Trash2, } from "lucide-react"; import { UniversalListPage, @@ -39,136 +34,63 @@ import { } from "@/components/templates/UniversalListPage"; import { BadgeSm } from "@/components/atoms/BadgeSm"; import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard"; - -// 생산지시 상태 타입 -type ProductionOrderStatus = - | "waiting" // 생산대기 - | "in_progress" // 생산중 - | "completed"; // 생산완료 - -// 생산지시 데이터 타입 -interface ProductionOrder { - id: string; - productionOrderNumber: string; // PO-KD-TS-XXXXXX-XX - orderNumber: string; // KD-TS-XXXXXX-XX - siteName: string; - client: string; - quantity: number; - dueDate: string; - productionOrderDate: string; - status: ProductionOrderStatus; - workOrderCount: number; -} - -// 샘플 생산지시 데이터 -const SAMPLE_PRODUCTION_ORDERS: ProductionOrder[] = [ - { - id: "PO-001", - productionOrderNumber: "PO-KD-TS-251217-07", - orderNumber: "KD-TS-251217-07", - siteName: "씨밋 광교 센트럴시티", - client: "호반건설(주)", - quantity: 2, - dueDate: "2026-02-15", - productionOrderDate: "2025-12-22", - status: "waiting", - workOrderCount: 3, - }, - { - id: "PO-002", - productionOrderNumber: "PO-KD-TS-251217-09", - orderNumber: "KD-TS-251217-09", - siteName: "데시앙 동탄 파크뷰", - client: "태영건설(주)", - quantity: 10, - dueDate: "2026-02-10", - productionOrderDate: "2025-12-22", - status: "waiting", - workOrderCount: 0, - }, - { - id: "PO-003", - productionOrderNumber: "PO-KD-TS-251217-06", - orderNumber: "KD-TS-251217-06", - siteName: "예술 검실 푸르지오", - client: "롯데건설(주)", - quantity: 1, - dueDate: "2026-02-10", - productionOrderDate: "2025-12-22", - status: "waiting", - workOrderCount: 0, - }, - { - id: "PO-004", - productionOrderNumber: "PO-KD-BD-251220-35", - orderNumber: "KD-BD-251220-35", - siteName: "[코레타스프] 판교 물류센터 철거현장", - client: "현대건설(주)", - quantity: 3, - dueDate: "2026-02-03", - productionOrderDate: "2025-12-20", - status: "in_progress", - workOrderCount: 2, - }, - { - id: "PO-005", - productionOrderNumber: "PO-KD-BD-251219-34", - orderNumber: "KD-BD-251219-34", - siteName: "[코레타스프1] 김포 6차 필라테스장", - client: "신성플랜(주)", - quantity: 2, - dueDate: "2026-01-15", - productionOrderDate: "2025-12-19", - status: "in_progress", - workOrderCount: 3, - }, - { - id: "PO-006", - productionOrderNumber: "PO-KD-TS-250401-29", - orderNumber: "KD-TS-250401-29", - siteName: "포레나 전주", - client: "한화건설(주)", - quantity: 2, - dueDate: "2025-05-16", - productionOrderDate: "2025-04-01", - status: "completed", - workOrderCount: 3, - }, - { - id: "PO-007", - productionOrderNumber: "PO-KD-BD-250331-28", - orderNumber: "KD-BD-250331-28", - siteName: "포레나 수원", - client: "포레나건설(주)", - quantity: 4, - dueDate: "2025-05-15", - productionOrderDate: "2025-03-31", - status: "completed", - workOrderCount: 3, - }, - { - id: "PO-008", - productionOrderNumber: "PO-KD-TS-250314-23", - orderNumber: "KD-TS-250314-23", - siteName: "자이 흑산파크", - client: "GS건설(주)", - quantity: 3, - dueDate: "2025-04-28", - productionOrderDate: "2025-03-14", - status: "completed", - workOrderCount: 3, - }, -]; +import { + getProductionOrders, + getProductionOrderStats, +} from "@/components/production/ProductionOrders/actions"; +import type { + ProductionOrder, + ProductionStatus, + ProductionOrderStats, +} from "@/components/production/ProductionOrders/types"; +import { formatNumber } from '@/lib/utils/amount'; // 진행 단계 컴포넌트 -function ProgressSteps() { - const steps = [ - { label: "수주확정", active: true, completed: true }, - { label: "생산지시", active: true, completed: false }, - { label: "작업지시", active: false, completed: false }, - { label: "생산", active: false, completed: false }, - { label: "검사출하", active: false, completed: false }, - ]; +function ProgressSteps({ statusCode }: { statusCode?: string }) { + const getSteps = () => { + // 기본: 생산지시 목록에 있으면 수주확정, 생산지시는 이미 완료 + const steps = [ + { label: "수주확정", completed: true, active: false }, + { label: "생산지시", completed: true, active: false }, + { label: "작업지시", completed: false, active: false }, + { label: "생산", completed: false, active: false }, + { label: "검사출하", completed: false, active: false }, + ]; + + if (!statusCode) return steps; + + // IN_PROGRESS = 생산대기 (작업지시 배정 진행 중) + if (statusCode === "IN_PROGRESS") { + steps[2].active = true; + } + // IN_PRODUCTION = 생산중 + if (statusCode === "IN_PRODUCTION") { + steps[2].completed = true; + steps[3].active = true; + } + // PRODUCED = 생산완료 + if (statusCode === "PRODUCED") { + steps[2].completed = true; + steps[3].completed = true; + steps[4].active = true; + } + // SHIPPING = 출하중 + if (statusCode === "SHIPPING") { + steps[2].completed = true; + steps[3].completed = true; + steps[4].active = true; + } + // SHIPPED = 출하완료 + if (statusCode === "SHIPPED") { + steps[2].completed = true; + steps[3].completed = true; + steps[4].completed = true; + } + + return steps; + }; + + const steps = getSteps(); return (
@@ -214,16 +136,16 @@ function ProgressSteps() { } // 상태 배지 헬퍼 -function getStatusBadge(status: ProductionOrderStatus) { +function getStatusBadge(status: ProductionStatus) { const config: Record< - ProductionOrderStatus, + ProductionStatus, { label: string; className: string } > = { waiting: { label: "생산대기", className: "bg-yellow-100 text-yellow-700 border-yellow-200", }, - in_progress: { + in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200", }, @@ -239,13 +161,12 @@ function getStatusBadge(status: ProductionOrderStatus) { // 테이블 컬럼 정의 const TABLE_COLUMNS: TableColumn[] = [ { key: "no", label: "번호", className: "w-[60px] text-center" }, - { key: "productionOrderNumber", label: "생산지시번호", className: "min-w-[150px]" }, - { key: "orderNumber", label: "수주번호", className: "min-w-[140px]" }, + { key: "orderNumber", label: "수주번호", className: "min-w-[150px]" }, { key: "siteName", label: "현장명", className: "min-w-[180px]" }, - { key: "client", label: "거래처", className: "min-w-[120px]" }, - { key: "quantity", label: "수량", className: "w-[80px] text-center" }, - { key: "dueDate", label: "납기", className: "w-[110px]" }, - { key: "productionOrderDate", label: "생산지시일", className: "w-[110px]" }, + { key: "clientName", label: "거래처", className: "min-w-[120px]" }, + { key: "nodeCount", label: "개소", className: "w-[80px] text-center" }, + { key: "deliveryDate", label: "납기", className: "w-[110px]" }, + { key: "productionOrderedAt", label: "생산지시일", className: "w-[110px]" }, { key: "status", label: "상태", className: "w-[100px]" }, { key: "workOrderCount", label: "작업지시", className: "w-[80px] text-center" }, { key: "actions", label: "작업", className: "w-[100px] text-center" }, @@ -253,65 +174,21 @@ const TABLE_COLUMNS: TableColumn[] = [ export default function ProductionOrdersListPage() { const router = useRouter(); - const [orders, setOrders] = useState(SAMPLE_PRODUCTION_ORDERS); - const [searchTerm, setSearchTerm] = useState(""); - const [activeTab, setActiveTab] = useState("all"); - const [selectedItems, setSelectedItems] = useState>(new Set()); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 20; - - // 삭제 확인 다이얼로그 상태 - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [deleteTargetId, setDeleteTargetId] = useState(null); // 개별 삭제 시 사용 - - // 필터링된 데이터 - const filteredData = orders.filter((item) => { - // 탭 필터 - if (activeTab !== "all") { - const statusMap: Record = { - waiting: "waiting", - in_progress: "in_progress", - completed: "completed", - }; - if (item.status !== statusMap[activeTab]) return false; - } - - // 검색 필터 - if (searchTerm) { - const term = searchTerm.toLowerCase(); - return ( - item.productionOrderNumber.toLowerCase().includes(term) || - item.orderNumber.toLowerCase().includes(term) || - item.siteName.toLowerCase().includes(term) || - item.client.toLowerCase().includes(term) - ); - } - - return true; + const [stats, setStats] = useState({ + total: 0, + waiting: 0, + in_production: 0, + completed: 0, }); - // 페이지네이션 - const totalPages = Math.ceil(filteredData.length / itemsPerPage); - const paginatedData = filteredData.slice( - (currentPage - 1) * itemsPerPage, - currentPage * itemsPerPage - ); - - // 탭별 건수 - const tabCounts = { - all: orders.length, - waiting: orders.filter((i) => i.status === "waiting").length, - in_progress: orders.filter((i) => i.status === "in_progress").length, - completed: orders.filter((i) => i.status === "completed").length, - }; - - // 탭 옵션 - const tabs: TabOption[] = [ - { value: "all", label: "전체", count: tabCounts.all }, - { value: "waiting", label: "생산대기", count: tabCounts.waiting, color: "yellow" }, - { value: "in_progress", label: "생산중", count: tabCounts.in_progress, color: "green" }, - { value: "completed", label: "생산완료", count: tabCounts.completed, color: "gray" }, - ]; + // 통계 로드 + useEffect(() => { + getProductionOrderStats().then((result) => { + if (result.success && result.data) { + setStats(result.data); + } + }); + }, []); const handleBack = () => { router.push("/sales/order-management-sales"); @@ -325,57 +202,13 @@ export default function ProductionOrdersListPage() { router.push(`/sales/order-management-sales/production-orders/${item.id}?mode=view`); }; - // 개별 삭제 다이얼로그 열기 - const handleDelete = (item: ProductionOrder) => { - setDeleteTargetId(item.id); - setShowDeleteDialog(true); - }; - - // 체크박스 선택 - const toggleSelection = (id: string) => { - const newSelection = new Set(selectedItems); - if (newSelection.has(id)) { - newSelection.delete(id); - } else { - newSelection.add(id); - } - setSelectedItems(newSelection); - }; - - const toggleSelectAll = () => { - if (selectedItems.size === paginatedData.length) { - setSelectedItems(new Set()); - } else { - setSelectedItems(new Set(paginatedData.map((item) => item.id))); - } - }; - - // 일괄 삭제 다이얼로그 열기 - const handleBulkDelete = () => { - if (selectedItems.size > 0) { - setDeleteTargetId(null); // 일괄 삭제 - setShowDeleteDialog(true); - } - }; - - // 삭제 개수 계산 (개별 삭제 시 1, 일괄 삭제 시 selectedItems.size) - const deleteCount = deleteTargetId ? 1 : selectedItems.size; - - // 실제 삭제 실행 - const handleConfirmDelete = () => { - if (deleteTargetId) { - // 개별 삭제 - setOrders(orders.filter((o) => o.id !== deleteTargetId)); - setSelectedItems(new Set([...selectedItems].filter(id => id !== deleteTargetId))); - } else { - // 일괄 삭제 - const selectedIds = Array.from(selectedItems); - setOrders(orders.filter((o) => !selectedIds.includes(o.id))); - setSelectedItems(new Set()); - } - setShowDeleteDialog(false); - setDeleteTargetId(null); - }; + // 탭 옵션 (통계 기반 동적 카운트) + const tabs: TabOption[] = [ + { value: "all", label: "전체", count: stats.total }, + { value: "waiting", label: "생산대기", count: stats.waiting, color: "yellow" }, + { value: "in_production", label: "생산중", count: stats.in_production, color: "green" }, + { value: "completed", label: "생산완료", count: stats.completed, color: "gray" }, + ]; // 테이블 행 렌더링 const renderTableRow = ( @@ -402,22 +235,17 @@ export default function ProductionOrdersListPage() { - {item.productionOrderNumber} - - - - {item.orderNumber} {item.siteName} - {item.client} - {item.quantity}개 - {item.dueDate} - {item.productionOrderDate} - {getStatusBadge(item.status)} + {item.clientName} + {formatNumber(item.nodeCount)}개소 + {item.deliveryDate} + {item.productionOrderedAt} + {getStatusBadge(item.productionStatus)} {item.workOrderCount > 0 ? ( {item.workOrderCount}건 @@ -431,9 +259,6 @@ export default function ProductionOrdersListPage() { -
)} @@ -463,19 +288,19 @@ export default function ProductionOrdersListPage() { variant="outline" className="bg-blue-50 text-blue-700 font-mono text-xs" > - {item.productionOrderNumber} + {item.orderNumber} - {getStatusBadge(item.status)} + {getStatusBadge(item.productionStatus)} } infoGrid={
- - - - + + + + 0 ? `${item.workOrderCount}건` : "-"} @@ -497,18 +322,6 @@ export default function ProductionOrdersListPage() { 상세 -
) : undefined } @@ -516,6 +329,43 @@ export default function ProductionOrdersListPage() { ); }; + // getList API 호출 + const getList = useCallback(async (params?: { page?: number; pageSize?: number; search?: string; tab?: string }) => { + const productionStatus = params?.tab && params.tab !== "all" + ? (params.tab as ProductionStatus) + : undefined; + + const result = await getProductionOrders({ + search: params?.search, + productionStatus, + page: params?.page, + perPage: params?.pageSize, + }); + + if (result.success) { + // 통계 새로고침 + getProductionOrderStats().then((statsResult) => { + if (statsResult.success && statsResult.data) { + setStats(statsResult.data); + } + }); + + return { + success: true, + data: result.data, + totalCount: result.pagination?.total || 0, + totalPages: result.pagination?.lastPage || 1, + }; + } + + return { + success: false, + data: [], + totalCount: 0, + error: result.error, + }; + }, []); + // ===== UniversalListPage 설정 ===== const productionOrderConfig: UniversalListConfig = { title: "생산지시 목록", @@ -525,43 +375,19 @@ export default function ProductionOrdersListPage() { idField: "id", actions: { - getList: async () => ({ - success: true, - data: orders, - totalCount: orders.length, - }), + getList, }, columns: TABLE_COLUMNS, tabs: tabs, - defaultTab: activeTab, + defaultTab: "all", - searchPlaceholder: "생산지시번호, 수주번호, 현장명 검색...", + searchPlaceholder: "수주번호, 현장명, 거래처 검색...", - itemsPerPage, + itemsPerPage: 20, - clientSideFiltering: true, - - searchFilter: (item, searchValue) => { - const term = searchValue.toLowerCase(); - return ( - item.productionOrderNumber.toLowerCase().includes(term) || - item.orderNumber.toLowerCase().includes(term) || - item.siteName.toLowerCase().includes(term) || - item.client.toLowerCase().includes(term) - ); - }, - - tabFilter: (item, tabValue) => { - if (tabValue === "all") return true; - const statusMap: Record = { - waiting: "waiting", - in_progress: "in_progress", - completed: "completed", - }; - return item.status === statusMap[tabValue]; - }, + clientSideFiltering: false, headerActions: () => ( + )} +
@@ -344,13 +497,22 @@ export function MaterialInputModal({
) : (
- {materialGroups.map((group) => { + {materialGroups.map((group, groupIdx) => { + // 같은 카테고리 내 순번 계산 (①②③...) + const categoryIndex = group.category + ? materialGroups.slice(0, groupIdx).filter(g => g.category === group.category).length + : -1; + const circledNumbers = ['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩']; + const circledNum = categoryIndex >= 0 && categoryIndex < circledNumbers.length + ? circledNumbers[categoryIndex] : ''; + + const targetQty = getGroupTargetQty(group); const groupAllocated = group.lots.reduce( - (sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0), + (sum, lot) => sum + (allocations.get(getLotKey(lot, group.groupKey)) || 0), 0 ); - const isAlreadyComplete = group.effectiveRequiredQty <= 0; - const isFulfilled = isAlreadyComplete || groupAllocated >= group.effectiveRequiredQty; + const isGroupComplete = targetQty <= 0 && group.alreadyInputted <= 0; + const isFulfilled = isGroupComplete || groupAllocated >= targetQty; return (
@@ -372,6 +534,11 @@ export function MaterialInputModal({ group.category} )} + {group.partType && ( + + {circledNum}{group.partType} + + )} {group.materialName} @@ -387,10 +554,10 @@ export function MaterialInputModal({ <> 필요:{' '} - {fmtQty(group.effectiveRequiredQty)} + {fmtQty(group.requiredQty)} {' '} {group.unit} - + (기투입: {fmtQty(group.alreadyInputted)}) @@ -406,27 +573,20 @@ export function MaterialInputModal({ 0 - ? 'bg-amber-100 text-amber-700' - : 'bg-gray-100 text-gray-500' + : groupAllocated > 0 + ? 'bg-amber-100 text-amber-700' + : 'bg-gray-100 text-gray-500' }`} > - {isAlreadyComplete ? ( - <> - - 투입 완료 - - ) : isFulfilled ? ( + {isFulfilled ? ( <> 배정 완료 ) : ( - `${fmtQty(groupAllocated)} / ${fmtQty(group.effectiveRequiredQty)}` + `${fmtQty(groupAllocated)} / ${fmtQty(targetQty)}` )}
@@ -437,7 +597,7 @@ export function MaterialInputModal({ - 선택 + 선택 로트번호 가용수량 단위 @@ -446,20 +606,24 @@ export function MaterialInputModal({ {group.lots.map((lot, idx) => { - const lotKey = getLotKey(lot); + const lotKey = getLotKey(lot, group.groupKey); const hasStock = lot.stockLotId !== null; const isSelected = selectedLotKeys.has(lotKey); const allocated = allocations.get(lotKey) || 0; - const canSelect = hasStock && !isAlreadyComplete && (!isFulfilled || isSelected); + const itemInput = lot as unknown as MaterialForItemInput; + const lotInputted = itemInput.lotInputtedQty ?? 0; + const isPreInputted = lotInputted > 0; + // 가용수량 = 현재 가용 + 기투입분 (replace 시 복원되므로) + const effectiveAvailable = lot.lotAvailableQty + lotInputted; + const canSelect = hasStock && (!isFulfilled || isSelected); return ( 0 - ? 'bg-blue-50/50' - : '' - } + className={cn( + isSelected && allocated > 0 ? 'bg-blue-50/50' : '', + isPreInputted && isSelected ? 'bg-blue-50/70' : '' + )} > {hasStock ? ( @@ -467,7 +631,7 @@ export function MaterialInputModal({ onClick={() => toggleLot(lotKey)} disabled={!canSelect} className={cn( - 'min-w-[56px] px-3 py-1.5 rounded-lg text-xs font-semibold transition-all', + 'min-w-[64px] px-3 py-1.5 rounded-lg text-xs font-semibold transition-all', isSelected ? 'bg-blue-600 text-white shadow-sm' : canSelect @@ -475,20 +639,34 @@ export function MaterialInputModal({ : 'bg-gray-100 text-gray-400 cursor-not-allowed' )} > - {isSelected ? '선택됨' : '선택'} + {isSelected ? '선택완료' : '선택'} ) : null} - {lot.lotNo || ( - - 재고 없음 - - )} +
+ {lot.lotNo || ( + + 재고 없음 + + )} + {isPreInputted && ( + + 기투입 + + )} +
{hasStock ? ( - fmtQty(lot.lotAvailableQty) + isPreInputted ? ( + + {fmtQty(lot.lotAvailableQty)} + (+{fmtQty(lotInputted)}) + + ) : ( + fmtQty(lot.lotAvailableQty) + ) ) : ( 0 )} @@ -497,7 +675,19 @@ export function MaterialInputModal({ {lot.unit} - {allocated > 0 ? ( + {isSelected && hasStock ? ( + { + const val = parseFloat(e.target.value) || 0; + handleAllocationChange(lotKey, val, effectiveAvailable); + }} + className="w-20 text-center text-blue-600 font-semibold border border-blue-200 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + min={0} + max={effectiveAvailable} + /> + ) : allocated > 0 ? ( {fmtQty(allocated)} @@ -529,7 +719,7 @@ export function MaterialInputModal({