From 5c085dc69e940f1f753ae85ba09fe6675a1b7f3b Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 12 May 2026 13:44:58 +0900 Subject: [PATCH] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=20wace=20=EC=9A=B4=EC=98=81?= =?UTF-8?q?=ED=8C=90=201:1=20+=20=EC=98=81=EC=97=85=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=8C=90=EB=A7=A4=C2=B7=EB=A7=A4=EC=B6=9C=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit · 다이얼로그 디자인: 프로젝트정보 박스(주문유형/제품구분/국내해외/고객사·유무상/접수일/견적환종/견적환율) + 품목정보 테이블(No/품번/품명/S/N/요청납기/고객요청사항/반납사유) · ProjectInfoData 정규화 props — 진행관리/판매관리/매출관리 각각 toProjectInfo 매핑으로 호출 · backend SQL 3곳 보강: exchange_rate (contract_mgmt) + customer_request (CI→CM fallback) + return_reason_name (CI CODE_NAME) · 판매관리/매출관리 page에 columns useMemo + project_no 셀 onClick + ProjectInfoDialog state 추가 · wace 운영 URL: /salesMgmt/salesRegForm.do?saleNo=detail 1:1 매핑 (새 창 → 같은 탭 다이얼로그) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/services/projectMgmtService.ts | 10 ++ backend-node/src/services/salesSaleService.ts | 8 ++ .../COMPANY_16/project/progress/page.tsx | 26 +++- .../(main)/COMPANY_16/sales/revenue/page.tsx | 37 ++++- .../app/(main)/COMPANY_16/sales/sale/page.tsx | 37 ++++- .../components/project/ProjectInfoDialog.tsx | 126 ++++++++++++------ frontend/lib/api/projectMgmt.ts | 5 + frontend/lib/api/salesSale.ts | 14 ++ 8 files changed, 215 insertions(+), 48 deletions(-) diff --git a/backend-node/src/services/projectMgmtService.ts b/backend-node/src/services/projectMgmtService.ts index 7421f062..593a6194 100644 --- a/backend-node/src/services/projectMgmtService.ts +++ b/backend-node/src/services/projectMgmtService.ts @@ -148,6 +148,16 @@ export async function listProgress(filter: ProgressListFilter) { ,T.CONTRACT_PRICE_CURRENCY ,T.CONTRACT_CURRENCY ,CODE_NAME(T.CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME + ,(SELECT CM.exchange_rate FROM contract_mgmt CM WHERE CM.objid = T.CONTRACT_OBJID) AS EXCHANGE_RATE + ,COALESCE( + (SELECT CI.customer_request FROM contract_item CI + WHERE CI.contract_objid = T.CONTRACT_OBJID AND CI.part_objid = T.PART_OBJID + AND CI.status='ACTIVE' ORDER BY CI.objid DESC LIMIT 1), + (SELECT CM.customer_request FROM contract_mgmt CM WHERE CM.objid = T.CONTRACT_OBJID) + ) AS CUSTOMER_REQUEST + ,(SELECT CODE_NAME(CI.return_reason) FROM contract_item CI + WHERE CI.contract_objid = T.CONTRACT_OBJID AND CI.part_objid = T.PART_OBJID + AND CI.status='ACTIVE' ORDER BY CI.objid DESC LIMIT 1) AS RETURN_REASON_NAME ,T.REGDATE ,TO_CHAR(T.REGDATE,'YYYY-MM-DD') AS REG_DATE ,T.WRITER diff --git a/backend-node/src/services/salesSaleService.ts b/backend-node/src/services/salesSaleService.ts index 14217df5..de0d569e 100644 --- a/backend-node/src/services/salesSaleService.ts +++ b/backend-node/src/services/salesSaleService.ts @@ -143,7 +143,11 @@ export async function getSaleList(filter: SaleListFilter) { ,COALESCE(CC_CUR_S.code_name, CC_CUR.code_name) AS sales_currency_name ,T.contract_currency ,CC_CUR.code_name AS contract_currency_name + ,CM.exchange_rate AS exchange_rate ,SR.sales_exchange_rate + ,(SELECT CODE_NAME(CI2.return_reason) FROM contract_item CI2 + WHERE CI2.contract_objid = T.contract_objid AND CI2.part_objid = T.part_objid + AND CI2.status='ACTIVE' ORDER BY CI2.objid DESC LIMIT 1) AS return_reason_name ,SR.shipping_date ,SR.shipping_method ,SR.shipping_order_status @@ -289,9 +293,13 @@ export async function getRevenueList(filter: SaleListFilter) { END AS sales_total_amount_krw ,T.contract_currency ,CC_CUR.code_name AS contract_currency_name + ,CM.exchange_rate AS exchange_rate ,SR.sales_currency ,CC_CUR_S.code_name AS sales_currency_name ,SR.sales_exchange_rate + ,(SELECT CODE_NAME(CI2.return_reason) FROM contract_item CI2 + WHERE CI2.contract_objid = T.contract_objid AND CI2.part_objid = T.part_objid + AND CI2.status='ACTIVE' ORDER BY CI2.objid DESC LIMIT 1) AS return_reason_name ,SR.shipping_date ,SR.shipping_method ,SR.serial_no diff --git a/frontend/app/(main)/COMPANY_16/project/progress/page.tsx b/frontend/app/(main)/COMPANY_16/project/progress/page.tsx index 521037f6..e7984bab 100644 --- a/frontend/app/(main)/COMPANY_16/project/progress/page.tsx +++ b/frontend/app/(main)/COMPANY_16/project/progress/page.tsx @@ -21,9 +21,27 @@ import { CommCodeSelect } from "@/components/common/CommCodeSelect"; import { CustomerSelect } from "@/components/common/CustomerSelect"; import { PartSelect } from "@/components/common/PartSelect"; import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect"; -import { ProjectInfoDialog } from "@/components/project/ProjectInfoDialog"; +import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog"; import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt"; +// 진행관리 row → 정규화된 ProjectInfoData 매핑 +const toProjectInfo = (r: ProgressRow): ProjectInfoData => ({ + orderType: r.category_name, + productType: r.product_name, + area: r.area_name, + customer: r.customer_name, + paidType: r.free_of_charge, + regDate: r.reg_date, + currency: r.contract_currency_name, + exchangeRate: r.exchange_rate, + partNo: r.product_item_code, + partName: r.product_item_name, + serialNo: r.serial_no, + reqDelDate: r.req_del_date, + customerRequest: r.customer_request, + returnReason: r.return_reason_name, +}); + // wace projectMgmtWbsList3.jsp 컬럼 정의 1:1 (8그룹 → 평탄화, 그룹명은 라벨 prefix) const GRID_COLUMNS: DataGridColumn[] = [ { key: "project_no", label: "프로젝트번호", width: "w-[160px]", frozen: true }, @@ -82,13 +100,13 @@ export default function ProjectProgressPage() { // wace `fn_openSaleRegPopup(PROJECT_NO, "detail")` 대응 — PROJECT_NO 셀 클릭 시 프로젝트 정보 다이얼로그 const [infoOpen, setInfoOpen] = useState(false); - const [infoRow, setInfoRow] = useState(null); + const [infoData, setInfoData] = useState(null); // GRID_COLUMNS의 project_no 셀에만 onClick 주입 (DataGrid 컬럼별 onClick 패턴) const columns = useMemo( () => GRID_COLUMNS.map((col) => col.key === "project_no" - ? { ...col, onClick: (row: any) => { setInfoRow(row as ProgressRow); setInfoOpen(true); } } + ? { ...col, onClick: (row: any) => { setInfoData(toProjectInfo(row as ProgressRow)); setInfoOpen(true); } } : col, ), [], @@ -240,7 +258,7 @@ export default function ProjectProgressPage() { /> - + ); } diff --git a/frontend/app/(main)/COMPANY_16/sales/revenue/page.tsx b/frontend/app/(main)/COMPANY_16/sales/revenue/page.tsx index a2adb7de..cdfe6c07 100644 --- a/frontend/app/(main)/COMPANY_16/sales/revenue/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/revenue/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -19,8 +19,27 @@ import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { CustomerSelect } from "@/components/common/CustomerSelect"; import { PartSelect } from "@/components/common/PartSelect"; import { CommCodeSelect } from "@/components/common/CommCodeSelect"; +import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog"; import { salesSaleApi, RevenueListRow, DeadlineInfoBody } from "@/lib/api/salesSale"; +// RevenueListRow → 정규화된 ProjectInfoData (wace 운영판 다이얼로그 1:1) +const toProjectInfo = (r: RevenueListRow): ProjectInfoData => ({ + orderType: r.order_type_name, + productType: r.product_type_name, + area: r.nation_name, + customer: r.customer, + paidType: r.payment_type_name, + regDate: r.receipt_date, + currency: r.contract_currency_name, + exchangeRate: r.exchange_rate, + partNo: r.product_no, + partName: r.product_name, + serialNo: r.serial_no, + reqDelDate: r.request_date, + customerRequest: r.customer_request, + returnReason: r.return_reason_name, +}); + // wace_plm revenueMgmtList.jsp 컬럼 순서/라벨에 맞춤 const GRID_COLUMNS: DataGridColumn[] = [ { key: "project_no", label: "프로젝트번호", width: "w-[170px]", frozen: true }, @@ -70,6 +89,18 @@ export default function SalesRevenuePage() { const [loading, setLoading] = useState(false); const [selected, setSelected] = useState(null); const [checkedIds, setCheckedIds] = useState([]); + + // 프로젝트번호 셀 클릭 → 프로젝트 상세 정보 다이얼로그 (wace 운영판 1:1) + const [infoOpen, setInfoOpen] = useState(false); + const [infoData, setInfoData] = useState(null); + const columns = useMemo( + () => GRID_COLUMNS.map((col) => + col.key === "project_no" + ? { ...col, onClick: (row: any) => { setInfoData(toProjectInfo(row as RevenueListRow)); setInfoOpen(true); } } + : col, + ), + [], + ); // wace revenueMgmtList.jsp 활성 11개 const [searchForm, setSearchForm] = useState({ orderType: "", poNo: "", customer_objid: "", @@ -268,7 +299,7 @@ export default function SalesRevenuePage() { setSelected(id ? rows.find((r) => r.id === id) ?? null : null)} @@ -280,6 +311,8 @@ export default function SalesRevenuePage() { loading={loading} /> + + {/* 마감정보 입력 Dialog */} diff --git a/frontend/app/(main)/COMPANY_16/sales/sale/page.tsx b/frontend/app/(main)/COMPANY_16/sales/sale/page.tsx index 3f18ed14..20463668 100644 --- a/frontend/app/(main)/COMPANY_16/sales/sale/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/sale/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -17,8 +17,27 @@ import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { CustomerSelect } from "@/components/common/CustomerSelect"; import { PartSelect } from "@/components/common/PartSelect"; import { CommCodeSelect } from "@/components/common/CommCodeSelect"; +import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog"; import { salesSaleApi, SaleListRow, SaleRegisterBody } from "@/lib/api/salesSale"; +// SaleListRow → 정규화된 ProjectInfoData (wace 운영판 다이얼로그 1:1) +const toProjectInfo = (r: SaleListRow): ProjectInfoData => ({ + orderType: r.order_type_name, + productType: r.product_type_name, + area: r.nation_name, + customer: r.customer, + paidType: r.payment_type_name, + regDate: r.receipt_date, + currency: r.contract_currency_name, + exchangeRate: r.exchange_rate, + partNo: r.product_no, + partName: r.product_name, + serialNo: r.serial_no, + reqDelDate: r.request_date, + customerRequest: r.customer_request, + returnReason: r.return_reason_name, +}); + // wace_plm salesMgmtList.jsp 컬럼 순서/라벨에 맞춤 const GRID_COLUMNS: DataGridColumn[] = [ { key: "project_no", label: "프로젝트번호", width: "w-[170px]", frozen: true }, @@ -75,6 +94,18 @@ export default function SalesSalePage() { shippingDateFrom: "", shippingDateTo: "", }); + // 프로젝트번호 셀 클릭 → 프로젝트 상세 정보 다이얼로그 (wace 운영판 1:1) + const [infoOpen, setInfoOpen] = useState(false); + const [infoData, setInfoData] = useState(null); + const columns = useMemo( + () => GRID_COLUMNS.map((col) => + col.key === "project_no" + ? { ...col, onClick: (row: any) => { setInfoData(toProjectInfo(row as SaleListRow)); setInfoOpen(true); } } + : col, + ), + [], + ); + const [registerOpen, setRegisterOpen] = useState(false); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ @@ -248,7 +279,7 @@ export default function SalesSalePage() { setSelected(id ? rows.find((r) => r.id === id) ?? null : null)} @@ -257,6 +288,8 @@ export default function SalesSalePage() { loading={loading} /> + + {/* 출하지시/판매등록 Dialog */} diff --git a/frontend/components/project/ProjectInfoDialog.tsx b/frontend/components/project/ProjectInfoDialog.tsx index 874af21f..f73f0a81 100644 --- a/frontend/components/project/ProjectInfoDialog.tsx +++ b/frontend/components/project/ProjectInfoDialog.tsx @@ -1,54 +1,99 @@ "use client"; -// 진행관리 PROJECT_NO 셀 클릭 시 표시되는 프로젝트 정보 다이얼로그 (read-only). -// wace `fn_openSaleRegPopup(PROJECT_NO, "detail")` 대응 — 새 창 대신 같은 탭 내 다이얼로그. -// list SQL 응답(ProgressRow)에 필요 데이터가 모두 포함돼 있어 별도 detail API 호출 불필요. +// 프로젝트 상세 정보 다이얼로그 — wace 운영판 (salesRegForm.do?saleNo=detail) 1:1 +// 사용처: 진행관리 / 영업관리 판매관리 / 매출관리 그리드의 프로젝트번호 셀 클릭 시. +// 페이지마다 row 키가 다르므로 ProjectInfoData 정규화 객체를 호출 측에서 매핑해 전달. import React from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { ProgressRow } from "@/lib/api/projectMgmt"; + +export interface ProjectInfoData { + // 프로젝트정보 박스 + orderType: string | null; // 주문유형 + productType: string | null; // 제품구분 + area: string | null; // 국내/해외 + customer: string | null; // 고객사 + paidType: string | null; // 유/무상 + regDate: string | null; // 접수일 + currency: string | null; // 견적환종 + exchangeRate: string | null; // 견적환율 + // 품목정보 테이블 (라인 1건) + partNo: string | null; // 품번 + partName: string | null; // 품명 + serialNo: string | null; // S/N + reqDelDate: string | null; // 요청납기 + customerRequest: string | null; // 고객요청사항 + returnReason: string | null; // 반납사유 +} interface Props { open: boolean; onOpenChange: (open: boolean) => void; - row: ProgressRow | null; + data: ProjectInfoData | null; } -const fmtQty = (v: unknown) => { - if (v == null || v === "") return ""; - const n = Number(String(v).replace(/,/g, "")); - return isNaN(n) ? String(v) : n.toLocaleString(); -}; +const dash = (v: string | null | undefined) => + v == null || v === "" ? "-" : v; -export function ProjectInfoDialog({ open, onOpenChange, row }: Props) { +export function ProjectInfoDialog({ open, onOpenChange, data }: Props) { return ( - + - 프로젝트 정보 + 프로젝트 상세 정보 - {row ? ( -
- {row.project_no} - {row.contract_no} - {row.category_name} - {row.product_name} - {row.area_name} - {row.customer_name} - {row.free_of_charge} - {row.product_item_code} - {row.product_item_name} - {row.serial_no} - {fmtQty(row.contract_qty)} - {row.reg_date} - {row.req_del_date} - {row.order_date} - {row.project_name ?? ""} - {row.writer_name} + {data ? ( +
+ {/* 프로젝트정보 */} +
+
프로젝트정보
+
+ 주문유형{dash(data.orderType)} + 제품구분{dash(data.productType)} + 국내/해외{dash(data.area)} + 고객사{dash(data.customer)} + + 유/무상{dash(data.paidType)} + 접수일{dash(data.regDate)} + 견적환종{dash(data.currency)} + 견적환율{dash(data.exchangeRate)} +
+
+ + {/* 품목정보 */} +
+
품목정보
+
+ + + + + + + + + + + + + + + + + + + + + + + +
No품번품명S/N요청납기고객요청사항반납사유
1{dash(data.partNo)}{dash(data.partName)}{dash(data.serialNo)}{dash(data.reqDelDate)}{dash(data.customerRequest)}{dash(data.returnReason)}
+
+
) : null} @@ -60,14 +105,15 @@ export function ProjectInfoDialog({ open, onOpenChange, row }: Props) { ); } -function Label({ children }: { children: React.ReactNode }) { - return
{children}
; +function K({ children }: { children: React.ReactNode }) { + return
{children}
; } -function Value({ children, className }: { children: React.ReactNode; className?: string }) { - const empty = children == null || children === ""; - return ( -
- {empty ? - : children} -
- ); +function V({ children }: { children: React.ReactNode }) { + return
{children}
; +} +function Th({ children, className }: { children: React.ReactNode; className?: string }) { + return {children}; +} +function Td({ children, className }: { children: React.ReactNode; className?: string }) { + return {children}; } diff --git a/frontend/lib/api/projectMgmt.ts b/frontend/lib/api/projectMgmt.ts index f102aff5..fa300ab2 100644 --- a/frontend/lib/api/projectMgmt.ts +++ b/frontend/lib/api/projectMgmt.ts @@ -62,6 +62,11 @@ export interface ProgressRow { writer_name: string | null; cu01_cnt: number | null; cu02_cnt: number | null; + // 다이얼로그 표시용 (wace 운영판 1:1) + contract_currency_name: string | null; + exchange_rate: string | null; + customer_request: string | null; + return_reason_name: string | null; } export interface ProgressDetail { diff --git a/frontend/lib/api/salesSale.ts b/frontend/lib/api/salesSale.ts index cd6a30e7..8a969476 100644 --- a/frontend/lib/api/salesSale.ts +++ b/frontend/lib/api/salesSale.ts @@ -62,6 +62,11 @@ export interface SaleListRow { manager_name: string | null; cu01_cnt: number | null; serial_no: string | null; + // 다이얼로그 표시용 (wace 운영판 1:1) + order_type_name: string | null; + contract_currency_name: string | null; + exchange_rate: string | null; + return_reason_name: string | null; } export interface RevenueListRow { @@ -106,6 +111,15 @@ export interface RevenueListRow { manager_name: string | null; incoterms: string | null; cu01_cnt: number | null; + // 다이얼로그 표시용 (wace 운영판 1:1) + order_type_name: string | null; + product_type_name: string | null; + nation_name: string | null; + product_no: string | null; + product_name: string | null; + contract_currency_name: string | null; + exchange_rate: string | null; + return_reason_name: string | null; } export interface SaleRegisterBody {