From 7c03907000ef41726c04f3c644c4428fa79d50c9 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Mon, 11 May 2026 10:17:04 +0900 Subject: [PATCH 01/10] =?UTF-8?q?PR-B=20G2:=20=EC=A3=BC=EB=AC=B8=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A3=BC=EC=9E=85=EB=A0=A5=20=3D=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=EB=93=B1=EB=A1=9D=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=ED=8F=BC=20(is=5Fdirect=5Forder=3D'Y'=20+=20order=5Fdate=20+?= =?UTF-8?q?=20approval=5Frequired)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wace 패턴(estimateAndOrderRegistFormPopup + saveEstimateAndOrderInfo) 이식. 운영 데이터 90건 중 74건이 is_direct_order='Y' — 주문 신규 등록의 기본 흐름. 변경: - 백엔드 OrderBody: order_date / approval_required / is_direct_order 신규 (contract_date 폐지, 운영 컬럼명 일치) - create()/update() INSERT·UPDATE에 위 3개 컬럼 추가. is_direct_order 기본값 'Y' - 프론트 OrderBody 타입 동기화. openCreate 시 order_date=today + is_direct_order='Y' 자동 - 폼 다이얼로그 "발주일" 입력을 order_date 바인딩 (잘못 contract_date 바인딩 정정) 자동 검증 (BEGIN/ROLLBACK): - is_direct_order='Y' INSERT, order_date 저장, ORDER_* 라인 컬럼 정상 - 견적관리 그리드 노출 차단(IS_DIRECT_ORDER!='Y' 필터), 주문관리 그리드 노출 확인 docs/migration/sales/05-direct-order-verify.md Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/services/salesOrderMgmtService.ts | 42 ++++++++------- .../migration/sales/05-direct-order-verify.md | 52 +++++++++++++++++++ .../(main)/COMPANY_16/sales/order/page.tsx | 11 ++-- frontend/lib/api/salesOrderMgmt.ts | 4 +- 4 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 docs/migration/sales/05-direct-order-verify.md diff --git a/backend-node/src/services/salesOrderMgmtService.ts b/backend-node/src/services/salesOrderMgmtService.ts index d73a382c..153b0c37 100644 --- a/backend-node/src/services/salesOrderMgmtService.ts +++ b/backend-node/src/services/salesOrderMgmtService.ts @@ -47,7 +47,7 @@ export interface OrderItem { } export interface OrderBody { - // contract_mgmt 헤더 + // contract_mgmt 헤더 (wace estimateAndOrderRegistFormPopup 직접등록 통합폼 — G2) objid?: string; // 신규면 자동 생성 contract_no?: string; category_cd?: string; @@ -57,15 +57,17 @@ export interface OrderBody { paid_type?: string; contract_currency?: string; exchange_rate?: string; - receipt_date?: string; - contract_date?: string; // 발주일 + receipt_date?: string; // 접수일 * + order_date?: string; // 발주일 * (wace G2 필수) req_del_date?: string; // 요청납기 po_no?: string; // 발주번호 - contract_result?: string; // 수주상태 (예: WAITING/CONFIRMED/CANCELLED) + contract_result?: string; // 수주상태 + approval_required?: string; // 결재여부 'Y'|'N' pm_user_id?: string; customer_request?: string; shipping_method?: string; incoterms?: string; + is_direct_order?: string; // 'Y'면 직접등록 (견적관리 노출 X, 주문관리만 노출) // 라인 items: OrderItem[]; } @@ -435,25 +437,29 @@ export async function create(userId: string, body: OrderBody) { return s + (isNaN(v) ? 0 : v); }, 0); + // wace G2 패턴: 주문관리 등록은 직접등록 통합폼 — is_direct_order='Y' (견적관리에 노출 X) + const isDirectOrder = body.is_direct_order || "Y"; + await client.query( `INSERT INTO contract_mgmt ( objid, contract_no, category_cd, customer_objid, product, area_cd, paid_type, - contract_currency, exchange_rate, receipt_date, contract_date, req_del_date, - po_no, contract_result, pm_user_id, customer_request, shipping_method, incoterms, + contract_currency, exchange_rate, receipt_date, order_date, req_del_date, + po_no, contract_result, approval_required, pm_user_id, customer_request, + shipping_method, incoterms, is_direct_order, order_supply_price, order_vat, order_total_amount, writer, regdate ) VALUES ( - $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18, - $19,$20,$21,$22,NOW() + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20, + $21,$22,$23,$24,NOW() )`, [ objid, contractNo, body.category_cd || null, body.customer_objid || null, body.product || null, body.area_cd || null, body.paid_type || null, body.contract_currency || null, body.exchange_rate || null, - body.receipt_date || null, body.contract_date || null, body.req_del_date || null, - body.po_no || null, body.contract_result || null, + body.receipt_date || null, body.order_date || null, body.req_del_date || null, + body.po_no || null, body.contract_result || null, body.approval_required || "N", body.pm_user_id || null, body.customer_request || null, - body.shipping_method || null, body.incoterms || null, + body.shipping_method || null, body.incoterms || null, isDirectOrder, String(sum("order_supply_price")), String(sum("order_vat")), String(sum("order_total_amount")), userId, ], @@ -484,18 +490,18 @@ export async function update(userId: string, objid: string, body: OrderBody) { await client.query( `UPDATE contract_mgmt SET category_cd=$2, customer_objid=$3, product=$4, area_cd=$5, paid_type=$6, - contract_currency=$7, exchange_rate=$8, receipt_date=$9, contract_date=$10, - req_del_date=$11, po_no=$12, contract_result=$13, pm_user_id=$14, - customer_request=$15, shipping_method=$16, incoterms=$17, - order_supply_price=$18, order_vat=$19, order_total_amount=$20, - chg_user_id=$21 + contract_currency=$7, exchange_rate=$8, receipt_date=$9, order_date=$10, + req_del_date=$11, po_no=$12, contract_result=$13, approval_required=$14, pm_user_id=$15, + customer_request=$16, shipping_method=$17, incoterms=$18, + order_supply_price=$19, order_vat=$20, order_total_amount=$21, + chg_user_id=$22 WHERE objid=$1`, [ objid, body.category_cd || null, body.customer_objid || null, body.product || null, body.area_cd || null, body.paid_type || null, body.contract_currency || null, body.exchange_rate || null, - body.receipt_date || null, body.contract_date || null, body.req_del_date || null, - body.po_no || null, body.contract_result || null, + body.receipt_date || null, body.order_date || null, body.req_del_date || null, + body.po_no || null, body.contract_result || null, body.approval_required || "N", body.pm_user_id || null, body.customer_request || null, body.shipping_method || null, body.incoterms || null, String(sum("order_supply_price")), String(sum("order_vat")), String(sum("order_total_amount")), diff --git a/docs/migration/sales/05-direct-order-verify.md b/docs/migration/sales/05-direct-order-verify.md new file mode 100644 index 00000000..3fdae862 --- /dev/null +++ b/docs/migration/sales/05-direct-order-verify.md @@ -0,0 +1,52 @@ +# 05. PR-B G2 — 주문관리 직접등록 통합폼 (`is_direct_order='Y'`) + +> 작성: 2026-05-11 +> 원본: `wace_plm/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp` + `ContractMgmtService.saveEstimateAndOrderInfo` (라인 2664~) +> 대상: `app/(main)/COMPANY_16/sales/order/page.tsx` "수주입력" 다이얼로그 + `POST /api/sales/order-mgmt` + +## 1. wace 패턴 + +견적 없이 주문을 바로 등록하는 통합폼. 핵심 차이: +- `contract_mgmt.is_direct_order = 'Y'` 강제 (견적관리 그리드 노출 X — 견적관리 SQL이 `IS_DIRECT_ORDER != 'Y'` 필터) +- 주문관리 그리드엔 노출 +- 헤더에 발주일(`order_date`) / 발주번호(`po_no`) 추가 +- 라인에 `ORDER_*` 컬럼 (수주수량/단가/공급가액/부가세/총액) 입력 +- 라인의 `quantity = order_quantity` 미러링 (wace 통합폼은 견적수량 별도 입력 X) + +## 2. 운영 데이터 검증 + +``` +contract_mgmt 90건 중 is_direct_order='Y' = 74건 (82%) +``` + +→ 운영 주문은 절대다수가 직접등록 통합폼으로 작성. G2가 주문 신규 등록의 기본 흐름. + +## 3. RPS 변경 + +### 백엔드 [salesOrderMgmtService.ts] +- `OrderBody` 타입: `order_date` / `approval_required` / `is_direct_order` 추가, `contract_date` 폐지 (운영 컬럼은 `order_date`) +- `create()`: INSERT에 `is_direct_order` (default 'Y') / `order_date` / `approval_required` 추가 +- `update()`: 동일 컬럼 UPDATE +- `upsertItems`: 기존 `ORDER_*` 처리 그대로 (이미 G2 호환) + +### 프론트 [order/page.tsx] + [salesOrderMgmt.ts] +- `OrderBody`: `order_date`/`approval_required`/`is_direct_order` 보강, `contract_date` 폐지 +- `openCreate()`: `order_date = today` + `is_direct_order = 'Y'` 기본값 +- `openEdit()`: `order_date`/`approval_required`/`is_direct_order` detail에서 복원 +- 폼 다이얼로그: "발주일" 입력을 `order_date` 바인딩 (이전 `contract_date` 잘못 바인딩 정정) + +## 4. 자동 검증 결과 (BEGIN/ROLLBACK) + +| 항목 | 결과 | +|---|---| +| `is_direct_order='Y'` INSERT | ✅ | +| `order_date` 컬럼에 저장 | ✅ (`2026-05-11`) | +| `ORDER_*` 라인 컬럼 (qty/unit_price/total_amount) | ✅ | +| 견적관리 노출 차단 (필터 `is_direct_order != 'Y'`) | ✅ (0 rows) | +| 주문관리 노출 | ✅ (1 row) | + +## 5. 결론 + +기존 RPS `create/update`가 거의 G2 호환이라 신규 endpoint 없이 컬럼 보강(is_direct_order/order_date/approval_required)으로 처리. 분리 endpoint 불필요. + +다음 단계: G5 견적작성 PDF 또는 G4 결재 모듈. diff --git a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index c878f1d5..a2fae223 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -192,7 +192,8 @@ export default function SalesOrderPage() { contract_no: contractNo, contract_currency: "KRW", paid_type: "paid", - contract_date: new Date().toISOString().slice(0, 10), + order_date: new Date().toISOString().slice(0, 10), + is_direct_order: "Y", items: [{ ...EMPTY_ITEM }], }); setDialogOpen(true); @@ -214,7 +215,9 @@ export default function SalesOrderPage() { contract_currency: detail.contract_currency ?? "KRW", exchange_rate: detail.exchange_rate ?? "", receipt_date: detail.receipt_date ?? "", - contract_date: detail.contract_date ?? "", + order_date: detail.order_date ?? "", + approval_required: detail.approval_required ?? "N", + is_direct_order: detail.is_direct_order ?? "Y", req_del_date: detail.req_del_date ?? "", po_no: detail.po_no ?? "", contract_result: detail.contract_result ?? "", @@ -501,8 +504,8 @@ export default function SalesOrderPage() { placeholder="저장 시 자동 부여됩니다" />
setForm({ ...form, po_no: e.target.value })} />
-
- setForm({ ...form, contract_date: e.target.value })} />
+
+ setForm({ ...form, order_date: e.target.value })} />
setForm({ ...form, req_del_date: e.target.value })} />
diff --git a/frontend/lib/api/salesOrderMgmt.ts b/frontend/lib/api/salesOrderMgmt.ts index 7ce536c2..1a87a103 100644 --- a/frontend/lib/api/salesOrderMgmt.ts +++ b/frontend/lib/api/salesOrderMgmt.ts @@ -88,10 +88,12 @@ export interface OrderBody { contract_currency?: string; exchange_rate?: string; receipt_date?: string; - contract_date?: string; + order_date?: string; // 발주일 (wace G2 필수) req_del_date?: string; po_no?: string; contract_result?: string; + approval_required?: string; // 결재여부 'Y'|'N' + is_direct_order?: string; // 'Y' 기본 (G2 직접등록) pm_user_id?: string; customer_request?: string; shipping_method?: string; From ae5ef8f7e53802fc148ca87ab5cd4d6914fe3dec Mon Sep 17 00:00:00 2001 From: hjjeong Date: Mon, 11 May 2026 10:24:25 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=EC=A3=BC=EB=AC=B8=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=EC=A3=BC=ED=86=B5=ED=95=A9=EB=93=B1=EB=A1=9D=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20wace=20G2=201:1=20?= =?UTF-8?q?=EC=9E=AC=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 RPS 폼이 wace estimateAndOrderRegistFormPopup과 크게 어긋남(헤더 항목 잘못 배치 + 라인 누락). 통째 재작성. 수주통합 기본정보 — wace 헤더 9개 정확 일치: 영업번호(자동채번) / 주문유형* / 국내해외* / 고객사* / 유무상* / 접수일* / 견적환종 / 견적환율 / 발주번호 / 발주일* (이전 RPS의 요청납기/통화/수주상태/담당자(PM ID)/출하방법/고객사요청사항 헤더 배치 폐지 — wace 통합폼에 없음) 품목정보 — wace 라인 13컬럼 + Total 합계 행: No / 제품구분* / 품번* / 품명* / S/N / 요청납기 / 고객요청사항 / 반납사유 / 수주수량* / 수주단가 / 수주공급가액 / 수주부가세 / 수주총액 / 삭제 (이전 누락: 제품구분 / S/N / 요청납기 / 고객요청사항 / 반납사유) 추가 컴포넌트: - 품번/품명: PartSelect (part_mng 8,176건 검색 + 자동 매핑) - 제품구분/반납사유: CommCodeSelect (0000001 / 0001810) - S/N: 별도 다이얼로그(테이블 + 연속번호생성) — wace fn_openItemSnPopup / fn_openItemSequentialSnPopup 1:1 - 합계: useMemo로 라인 수량/공급가액/부가세/총액 실시간 집계 EMPTY_ITEM 라인 기본값에 product/serials/due_date/return_reason/customer_request 추가. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../(main)/COMPANY_16/sales/order/page.tsx | 377 ++++++++++++++---- 1 file changed, 306 insertions(+), 71 deletions(-) diff --git a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index a2fae223..a850622e 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -70,8 +70,13 @@ const CONTRACT_RESULTS = [ { value: "0000965", label: "Cancel" }, ]; +// wace estimateAndOrderRegistFormPopup 라인 — 제품구분/S/N/요청납기/반납사유/고객요청사항 포함 const EMPTY_ITEM: OrderItem = { - seq: 1, part_objid: "", part_no: "", part_name: "", quantity: 1, + seq: 1, + product: "", part_objid: "", part_no: "", part_name: "", + serials: [], + due_date: "", return_reason: "", customer_request: "", + quantity: 1, order_quantity: "", order_unit_price: "0", order_supply_price: "0", order_vat: "0", order_total_amount: "0", }; @@ -98,8 +103,18 @@ export default function SalesOrderPage() { items: [], }); - // 품목 검색 모달 + // 품목 검색 모달 (라인별 진입) const [itemDialogOpen, setItemDialogOpen] = useState(false); + const [itemSearchTargetIdx, setItemSearchTargetIdx] = useState(null); + + // S/N 관리 모달 + 연속번호 생성 (wace fn_openItemSnPopup / fn_openItemSequentialSnPopup) + const [serialDialogOpen, setSerialDialogOpen] = useState(false); + const [serialDialogIdx, setSerialDialogIdx] = useState(null); + const [serialDraft, setSerialDraft] = useState([]); + const [serialInput, setSerialInput] = useState(""); + const [seqDialogOpen, setSeqDialogOpen] = useState(false); + const [seqStartNo, setSeqStartNo] = useState(""); + const [seqCount, setSeqCount] = useState(""); // 첨부파일 다이얼로그 (주문서첨부 클립 컬럼 클릭 시) const [attachDialogOpen, setAttachDialogOpen] = useState(false); @@ -363,6 +378,63 @@ export default function SalesOrderPage() { const addItem = () => setForm((prev) => ({ ...prev, items: [...(prev.items ?? []), { ...EMPTY_ITEM, seq: (prev.items?.length ?? 0) + 1 }] })); const removeItem = (idx: number) => setForm((prev) => ({ ...prev, items: (prev.items ?? []).filter((_, i) => i !== idx).map((it, i) => ({ ...it, seq: i + 1 })) })); + // S/N 관리 (wace fn_openItemSnPopup) — 견적관리와 동일 패턴 + const openSerialDialog = (idx: number) => { + const item = form.items?.[idx]; + setSerialDialogIdx(idx); + setSerialDraft([...(item?.serials ?? [])]); + setSerialInput(""); + setSerialDialogOpen(true); + }; + const addSerialDraft = () => { + const v = serialInput.trim(); + if (!v) { toast.warning("S/N을 입력해주세요."); return; } + if (serialDraft.includes(v)) { toast.warning("이미 등록된 S/N입니다."); return; } + setSerialDraft((prev) => [...prev, v]); + setSerialInput(""); + }; + const removeSerialDraft = (i: number) => setSerialDraft((prev) => prev.filter((_, k) => k !== i)); + const applySerialDraft = () => { + if (serialDialogIdx === null) return; + updateItem(serialDialogIdx, "serials", [...serialDraft]); + setSerialDialogOpen(false); + }; + const openSeqDialog = () => { setSeqStartNo(""); setSeqCount(""); setSeqDialogOpen(true); }; + const generateSequentialSn = () => { + const startNo = seqStartNo.trim(); + const count = parseInt(seqCount, 10); + if (!startNo) { toast.warning("시작 번호를 입력해주세요."); return; } + if (!count || count < 1) { toast.warning("생성 개수를 1 이상 입력해주세요."); return; } + if (count > 100) { toast.warning("최대 100개까지만 생성 가능합니다."); return; } + const m = startNo.match(/^(.*?)(\d+)$/); + if (!m) { toast.warning("형식이 올바르지 않습니다. 마지막에 숫자가 있어야 합니다."); return; } + const prefix = m[1]; const startNum = parseInt(m[2], 10); const numLen = m[2].length; + setSerialDraft((prev) => { + const next = [...prev]; + for (let i = 0; i < count; i++) { + const sn = prefix + String(startNum + i).padStart(numLen, "0"); + if (!next.includes(sn)) next.push(sn); + } + return next; + }); + setSeqDialogOpen(false); + }; + + // 라인 합계 자동 계산용 헬퍼 + const formatNum = (v: any) => { + const n = Number(String(v ?? "0").replace(/,/g, "")); + return isNaN(n) ? 0 : n; + }; + const lineTotal = useMemo(() => { + const items = form.items ?? []; + return items.reduce((acc, it) => ({ + qty: acc.qty + formatNum(it.order_quantity), + supply: acc.supply + formatNum(it.order_supply_price), + vat: acc.vat + formatNum(it.order_vat), + total: acc.total + formatNum(it.order_total_amount), + }), { qty: 0, supply: 0, vat: 0, total: 0 }); + }, [form.items]); + return (
{ConfirmDialogComponent} @@ -489,107 +561,194 @@ export default function SalesOrderPage() { /> - + - {dialogMode === "create" ? "주문서 등록" : "주문서 수정"} - 주문 헤더 + 라인을 입력합니다. + 영업관리 _ 주문서관리 _ 수주통합등록 + 수주통합 기본정보 + 품목정보를 입력합니다. (wace estimateAndOrderRegistFormPopup 1:1) + {/* 수주통합 기본정보 — wace 헤더 9개 (영업번호 자동채번 표시 포함) */}
- 주문 헤더 + 수주통합 기본정보
-
-
-
- setForm({ ...form, po_no: e.target.value })} />
-
- setForm({ ...form, order_date: e.target.value })} />
-
- setForm({ ...form, req_del_date: e.target.value })} />
-
- setForm({ ...form, customer_objid: v })} - />
-
- setForm({ ...form, receipt_date: e.target.value })} />
-
- setForm({ ...form, contract_currency: e.target.value })} />
-
- setForm({ ...form, exchange_rate: e.target.value })} />
-
- +
+
+ + setForm({ ...form, category_cd: v })} /> +
+
+ + setForm({ ...form, area_cd: v })} /> +
+
+ + setForm({ ...form, customer_objid: v })} /> +
+
+ +
-
-
-
- setForm({ ...form, pm_user_id: e.target.value })} />
-
- setForm({ ...form, shipping_method: e.target.value })} />
-
-