From d12618f3200e28e8c0631f1bd0dc15a34c8aa7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 16 Jan 2026 21:59:06 +0900 Subject: [PATCH] =?UTF-8?q?fix(WEB):=20=EC=88=98=EC=A3=BC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=95=84=EB=93=9C=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=9C=ED=92=88-=EB=B6=80=ED=92=88=20=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ApiClient 인터페이스: representative → manager_name, contact_person 변경 - transformApiToFrontend: client.representative → client.manager_name 수정 - ApiOrderItem에 floor_code, symbol_code 필드 추가 (제품-부품 매핑) - ApiOrder에 options 타입 정의 추가 - ApiQuote에 calculation_inputs 타입 정의 추가 - 수주 상세 페이지 제품-부품 트리 구조 UI 개선 --- .../order-management-sales/[id]/edit/page.tsx | 572 ++++++++- .../order-management-sales/[id]/page.tsx | 1125 ++++++++++++++++- .../estimates/EstimateDetailForm.tsx | 3 +- src/components/orders/actions.ts | 194 ++- .../orders/documents/ContractDocument.tsx | 174 +-- .../orders/documents/OrderDocumentModal.tsx | 99 +- .../documents/PurchaseOrderDocument.tsx | 18 +- .../orders/documents/TransactionDocument.tsx | 18 +- src/components/orders/index.ts | 2 + tsconfig.tsbuildinfo | 2 +- 10 files changed, 2043 insertions(+), 164 deletions(-) diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx index 1205ca29..0fdf3cfe 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx @@ -1,26 +1,562 @@ -'use client'; - -import { use, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; - -interface PageProps { - params: Promise<{ id: string }>; -} +"use client"; /** - * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + * 수주 수정 페이지 + * + * - 기본 정보 (읽기전용) + * - 수주/배송 정보 (편집 가능) + * - 비고 (편집 가능) + * - 품목 내역 (생산 시작 후 수정 불가) */ -export default function OrderEditPage({ params }: PageProps) { - const { id } = use(params); - const router = useRouter(); - useEffect(() => { - router.replace(`/sales/order-management-sales/${id}?mode=edit`); - }, [id, router]); +import { useState, useEffect } from "react"; +import { useRouter, useParams } from "next/navigation"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { FileText, AlertTriangle } from "lucide-react"; +import { toast } from "sonner"; +import { PageLayout } from "@/components/organisms/PageLayout"; +import { PageHeader } from "@/components/organisms/PageHeader"; +import { FormActions } from "@/components/organisms/FormActions"; +import { BadgeSm } from "@/components/atoms/BadgeSm"; +import { formatAmount } from "@/utils/formatAmount"; +import { + OrderItem, + getOrderById, + updateOrder, + type OrderStatus, +} from "@/components/orders"; +// 수정 폼 데이터 +interface EditFormData { + // 읽기전용 정보 + lotNumber: string; + quoteNumber: string; + client: string; + siteName: string; + manager: string; + contact: string; + status: OrderStatus; + + // 수정 가능 정보 + expectedShipDate: string; + expectedShipDateUndecided: boolean; + deliveryRequestDate: string; + deliveryMethod: string; + shippingCost: string; + receiver: string; + receiverContact: string; + address: string; + addressDetail: string; + remarks: string; + + // 품목 (수정 제한) + items: OrderItem[]; + canEditItems: boolean; + subtotal: number; + discountRate: number; + totalAmount: number; +} + +// 배송방식 옵션 +const DELIVERY_METHODS = [ + { value: "direct", label: "직접배차" }, + { value: "pickup", label: "상차" }, + { value: "courier", label: "택배" }, +]; + +// 운임비용 옵션 +const SHIPPING_COSTS = [ + { value: "free", label: "무료" }, + { value: "prepaid", label: "선불" }, + { value: "collect", label: "착불" }, + { value: "negotiable", label: "협의" }, +]; + + +// 상태 뱃지 헬퍼 +function getOrderStatusBadge(status: OrderStatus) { + const statusConfig: Record = { + order_registered: { label: "수주등록", className: "bg-gray-100 text-gray-700 border-gray-200" }, + order_confirmed: { label: "수주확정", className: "bg-gray-100 text-gray-700 border-gray-200" }, + production_ordered: { label: "생산지시완료", className: "bg-blue-100 text-blue-700 border-blue-200" }, + in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" }, + rework: { label: "재작업중", className: "bg-orange-100 text-orange-700 border-orange-200" }, + work_completed: { label: "작업완료", className: "bg-blue-600 text-white border-blue-600" }, + shipped: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" }, + cancelled: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" }, + }; + const config = statusConfig[status]; return ( -
-
리다이렉트 중...
-
+ + {config.label} + ); } + +export default function OrderEditPage() { + const router = useRouter(); + const params = useParams(); + const orderId = params.id as string; + + const [form, setForm] = useState(null); + const [loading, setLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + + // 데이터 로드 (API) + useEffect(() => { + async function loadOrder() { + try { + setLoading(true); + const result = await getOrderById(orderId); + if (result.success && result.data) { + const order = result.data; + // 상태에 따라 품목 수정 가능 여부 결정 + const canEditItems = !["in_production", "rework", "work_completed", "shipped"].includes( + order.status + ); + // Order 데이터를 EditFormData로 변환 + setForm({ + lotNumber: order.lotNumber, + quoteNumber: order.quoteNumber || "", + client: order.client, + siteName: order.siteName, + manager: order.manager || "", + contact: order.contact || "", + status: order.status, + expectedShipDate: order.expectedShipDate || "", + expectedShipDateUndecided: !order.expectedShipDate, + deliveryRequestDate: order.deliveryRequestDate || "", + deliveryMethod: order.deliveryMethod || "", + shippingCost: order.shippingCost || "", + receiver: order.receiver || "", + receiverContact: order.receiverContact || "", + address: order.address || "", + addressDetail: order.addressDetail || "", + remarks: order.remarks || "", + items: order.items || [], + canEditItems, + subtotal: order.subtotal || order.amount, + discountRate: order.discountRate || 0, + totalAmount: order.amount, + }); + } else { + toast.error(result.error || "수주 정보를 불러오는데 실패했습니다."); + router.push("/sales/order-management-sales"); + } + } catch (error) { + console.error("Error loading order:", error); + toast.error("수주 정보를 불러오는 중 오류가 발생했습니다."); + router.push("/sales/order-management-sales"); + } finally { + setLoading(false); + } + } + loadOrder(); + }, [orderId, router]); + + const handleCancel = () => { + router.push(`/sales/order-management-sales/${orderId}`); + }; + + const handleSave = async () => { + if (!form) return; + + // 유효성 검사 + if (!form.deliveryRequestDate) { + toast.error("납품요청일을 입력해주세요."); + return; + } + if (!form.receiver.trim()) { + toast.error("수신(반장/업체)을 입력해주세요."); + return; + } + if (!form.receiverContact.trim()) { + toast.error("수신처 연락처를 입력해주세요."); + return; + } + + setIsSaving(true); + try { + // API 연동 + // 주의: clientId를 보내지 않으면 기존 값 유지됨 + // clientName과 clientContact는 반드시 보내야 기존 값이 유지됨 + const result = await updateOrder(orderId, { + clientName: form.client, + clientContact: form.contact, + siteName: form.siteName, + expectedShipDate: form.expectedShipDateUndecided ? undefined : form.expectedShipDate, + deliveryRequestDate: form.deliveryRequestDate, + deliveryMethod: form.deliveryMethod, + shippingCost: form.shippingCost, + receiver: form.receiver, + receiverContact: form.receiverContact, + address: form.address, + addressDetail: form.addressDetail, + remarks: form.remarks, + items: form.items.map((item) => ({ + itemId: item.id ? parseInt(item.id, 10) : undefined, + itemCode: item.itemCode, + itemName: item.itemName, + specification: item.spec, + quantity: item.quantity, + unit: item.unit, + unitPrice: item.unitPrice, + })), + }); + + if (result.success) { + toast.success("수주가 수정되었습니다."); + router.push(`/sales/order-management-sales/${orderId}`); + } else { + toast.error(result.error || "수주 수정에 실패했습니다."); + } + } catch (error) { + console.error("Error updating order:", error); + toast.error("수주 수정 중 오류가 발생했습니다."); + } finally { + setIsSaving(false); + } + }; + + if (loading || !form) { + return ( + +
+
+
+ + ); + } + + return ( + + {/* 헤더 */} + + + {form.lotNumber} + + {getOrderStatusBadge(form.status)} +
+ } + /> + +
+ {/* 기본 정보 (읽기전용) */} + + + + 기본 정보 + (읽기전용) + + + +
+
+ +

{form.lotNumber}

+
+
+ +

{form.quoteNumber}

+
+
+ +

{form.manager}

+
+
+ +

{form.client}

+
+
+ +

{form.siteName}

+
+
+ +

{form.contact}

+
+
+
+
+ + {/* 수주/배송 정보 (편집 가능) */} + + + 수주/배송 정보 + + +
+ {/* 출고예정일 */} +
+ +
+ + setForm({ ...form, expectedShipDate: e.target.value }) + } + disabled={form.expectedShipDateUndecided} + className="flex-1" + /> +
+ + setForm({ + ...form, + expectedShipDateUndecided: checked as boolean, + expectedShipDate: checked ? "" : form.expectedShipDate, + }) + } + /> + +
+
+
+ + {/* 납품요청일 */} +
+ + + setForm({ ...form, deliveryRequestDate: e.target.value }) + } + /> +
+ + {/* 배송방식 */} +
+ + +
+ + {/* 운임비용 */} +
+ + +
+ + {/* 수신(반장/업체) */} +
+ + + setForm({ ...form, receiver: e.target.value }) + } + /> +
+ + {/* 수신처 연락처 */} +
+ + + setForm({ ...form, receiverContact: e.target.value }) + } + /> +
+ + {/* 수신처 주소 */} +
+ + + setForm({ ...form, address: e.target.value }) + } + placeholder="주소" + className="mb-2" + /> + + setForm({ ...form, addressDetail: e.target.value }) + } + placeholder="상세주소" + /> +
+
+
+
+ + {/* 비고 */} + + + 비고 + + +