From 73199876fd2ea533720576e4dcbe0859a0f80d07 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 16:30:53 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20POP=20=EB=B0=B0=EB=84=88=EB=A5=BC?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=EC=84=A4=EC=A0=95=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PopShell에서 하드코딩 배너 → popConfig.bannerEnabled/bannerText 읽기 - 설정에서 배너 OFF → 배너 숨김 - 설정에서 텍스트 입력 → 해당 텍스트 표시 - 설정 없으면 기존 기본 문구 유지 (폴백) --- frontend/components/pop/hardcoded/PopShell.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/components/pop/hardcoded/PopShell.tsx b/frontend/components/pop/hardcoded/PopShell.tsx index 3d4b6ac6..7f538aef 100644 --- a/frontend/components/pop/hardcoded/PopShell.tsx +++ b/frontend/components/pop/hardcoded/PopShell.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef, ReactNode } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; interface PopShellProps { children: ReactNode; @@ -98,8 +99,12 @@ export function PopShell({ children, showBanner = true, title, showBack = false, logout(); }; - const marqueeText = - "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. \u00a0\u00a0|\u00a0\u00a0 [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 \u00a0\u00a0|\u00a0\u00a0 [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings("/pop/home"); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. \u00a0\u00a0|\u00a0\u00a0 [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 \u00a0\u00a0|\u00a0\u00a0 [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; return (
@@ -296,7 +301,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false, {/* ===== NOTICE BANNER (Marquee) ===== */} - {showBanner &&
+ {showBanner && bannerEnabled &&
📢 공지 From 2f675660b42939ab424cf0a20da13a5893f95d62 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 16:50:17 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20POP=20=ED=99=94=EB=A9=B4=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=E2=80=94=20=EC=B1=84=EB=B2=88=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20+=20=EB=AF=B8?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=ED=95=AD=EB=AA=A9=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입고/출고 설정에 채번규칙(numberingRuleId) 텍스트 필드 추가 - 하드코딩 POP에 미연동된 설정 항목에 (미구현) 라벨 추가 - 구현 완료: 배너(ON/OFF+텍스트), 자재투입, 그룹별사진 - 미구현: 바코드, 검사필수, 사진첨부, 포장, PLC, 날짜필터 등 --- .../admin/screenMng/popSettingsMng/page.tsx | 50 ++++++++++--------- sonar-project.properties | 7 +++ 2 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 sonar-project.properties diff --git a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx index b0d0b9be..80d8b7dd 100644 --- a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx @@ -173,53 +173,55 @@ interface SettingField { const SETTINGS_SCHEMA: Record = { inbound: [ - { key: "barcodeEnabled", label: "바코드 스캔", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, - { key: "inspectionRequired", label: "검사 필수", description: "입고 시 검사 항목을 필수로 표시합니다", type: "toggle" }, - { key: "photoUpload", label: "사진 첨부", description: "입고 확정 시 사진 첨부를 허용합니다", type: "toggle" }, - { key: "packagingRecord", label: "포장 기록", description: "포장/적재 상세 기록을 사용합니다", type: "toggle" }, - { key: "defectSeparation", label: "불량 분리", description: "양품/불량 수량을 분리 입력합니다", type: "toggle" }, + { key: "numberingRuleId", label: "📋 입고번호 채번규칙", description: "입고 확정 시 사용할 채번규칙을 선택합니다", type: "text" }, + { key: "barcodeEnabled", label: "바코드 스캔 (미구현)", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, + { key: "inspectionRequired", label: "검사 필수 (미구현)", description: "입고 시 검사 항목을 필수로 표시합니다", type: "toggle" }, + { key: "photoUpload", label: "사진 첨부 (미구현)", description: "입고 확정 시 사진 첨부를 허용합니다", type: "toggle" }, + { key: "packagingRecord", label: "포장 기록 (미구현)", description: "포장/적재 상세 기록을 사용합니다", type: "toggle" }, + { key: "defectSeparation", label: "불량 분리 (미구현)", description: "양품/불량 수량을 분리 입력합니다", type: "toggle" }, ], outbound: [ - { key: "barcodeEnabled", label: "바코드 스캔", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, - { key: "photoUpload", label: "사진 첨부", description: "출고 시 사진 첨부를 허용합니다", type: "toggle" }, + { key: "numberingRuleId", label: "📋 출고번호 채번규칙", description: "출고 확정 시 사용할 채번규칙을 선택합니다", type: "text" }, + { key: "barcodeEnabled", label: "바코드 스캔 (미구현)", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, + { key: "photoUpload", label: "사진 첨부 (미구현)", description: "출고 시 사진 첨부를 허용합니다", type: "toggle" }, ], processExecution: [ { key: "materialInput", label: "자재 투입", description: "BOM 기반 자재 투입 탭을 표시합니다", type: "toggle" }, - { key: "bomFlexible", label: "BOM 유동 투입", description: "기준과 다른 수량 투입을 허용합니다", type: "toggle" }, - { key: "photoUpload", label: "사진 첨부", description: "실적 입력 시 사진 첨부를 허용합니다", type: "toggle" }, + { key: "bomFlexible", label: "BOM 유동 투입 (미구현)", description: "기준과 다른 수량 투입을 허용합니다", type: "toggle" }, + { key: "photoUpload", label: "사진 첨부 (미구현)", description: "실적 입력 시 사진 첨부를 허용합니다", type: "toggle" }, { key: "groupPhotoEnabled", label: "그룹별 사진", description: "체크리스트 그룹마다 사진을 첨부합니다", type: "toggle" }, - { key: "plcEnabled", label: "PLC 연동", description: "설비 PLC 데이터를 자동 연동합니다", type: "toggle" }, - { key: "reworkTargetSelection", label: "재작업 공정 지정", description: "불량 처리 시 특정 공정을 선택할 수 있습니다", type: "toggle" }, - { key: "dateFilter", label: "날짜 필터", description: "작업지시 목록에 날짜 필터를 표시합니다", type: "toggle" }, + { key: "plcEnabled", label: "PLC 연동 (미구현)", description: "설비 PLC 데이터를 자동 연동합니다", type: "toggle" }, + { key: "reworkTargetSelection", label: "재작업 공정 지정 (미구현)", description: "불량 처리 시 특정 공정을 선택할 수 있습니다", type: "toggle" }, + { key: "dateFilter", label: "날짜 필터 (미구현)", description: "작업지시 목록에 날짜 필터를 표시합니다", type: "toggle" }, { - key: "lastProcessInventory", label: "마지막 공정 입고", description: "마지막 공정 완료 시 재고 입고 방식", type: "select", options: [ + key: "lastProcessInventory", label: "마지막 공정 입고 (미구현)", description: "마지막 공정 완료 시 재고 입고 방식", type: "select", options: [ { value: "auto", label: "자동 입고" }, { value: "manual", label: "수동 선택" }, { value: "button", label: "버튼 활성화" }, ], }, - { key: "defaultWarehouse", label: "기본 창고 기억", description: "선택한 창고를 다음에도 자동 선택합니다", type: "toggle" }, + { key: "defaultWarehouse", label: "기본 창고 기억 (미구현)", description: "선택한 창고를 다음에도 자동 선택합니다", type: "toggle" }, { - key: "inspectionAutoJudge", label: "검사 자동 판정", description: "수치 검사 시 상/하한 초과 처리 방식", type: "select", options: [ + key: "inspectionAutoJudge", label: "검사 자동 판정 (미구현)", description: "수치 검사 시 상/하한 초과 처리 방식", type: "select", options: [ { value: "off", label: "사용 안 함" }, { value: "warn", label: "경고만 표시" }, { value: "fail", label: "자동 불량" }, ], }, - { key: "standardTimeDisplay", label: "표준시간 비교", description: "표준시간 대비 실제시간을 표시합니다", type: "toggle" }, - { key: "progressDisplay", label: "진행률 표시", description: "작업지시 전체 진행률을 표시합니다", type: "toggle" }, - { key: "packagingOptions", label: "포장 옵션", description: "포장 단위 선택지를 관리합니다", type: "tags" }, - { key: "defectTypes", label: "불량 유형", description: "불량 유형 선택지를 관리합니다", type: "tags" }, + { key: "standardTimeDisplay", label: "표준시간 비교 (미구현)", description: "표준시간 대비 실제시간을 표시합니다", type: "toggle" }, + { key: "progressDisplay", label: "진행률 표시 (미구현)", description: "작업지시 전체 진행률을 표시합니다", type: "toggle" }, + { key: "packagingOptions", label: "포장 옵션 (미구현)", description: "포장 단위 선택지를 관리합니다", type: "tags" }, + { key: "defectTypes", label: "불량 유형 (미구현)", description: "불량 유형 선택지를 관리합니다", type: "tags" }, ], home: [ - { key: "kpiCarousel", label: "KPI 캐러셀", description: "오늘의 현황 캐러셀을 표시합니다", type: "toggle" }, - { key: "recentActivity", label: "최근 활동", description: "최근 입출고 활동을 표시합니다", type: "toggle" }, + { key: "kpiCarousel", label: "KPI 캐러셀 (미구현)", description: "오늘의 현황 캐러셀을 표시합니다", type: "toggle" }, + { key: "recentActivity", label: "최근 활동 (미구현)", description: "최근 입출고 활동을 표시합니다", type: "toggle" }, { key: "bannerEnabled", label: "공지 배너", description: "상단에 공지 배너를 표시합니다", type: "toggle" }, { key: "bannerText", label: "배너 텍스트", description: "공지 배너에 표시할 텍스트", type: "text" }, - { key: "iconThemeColor", label: "아이콘 테마색", description: "메뉴 아이콘의 테마 색상", type: "color" }, - { key: "iconCustomImages", label: "아이콘 커스텀", description: "메뉴 아이콘 이미지를 커스터마이즈합니다", type: "toggle" }, + { key: "iconThemeColor", label: "아이콘 테마색 (미구현)", description: "메뉴 아이콘의 테마 색상", type: "color" }, + { key: "iconCustomImages", label: "아이콘 커스텀 (미구현)", description: "메뉴 아이콘 이미지를 커스터마이즈합니다", type: "toggle" }, { - key: "dashboardLayout", label: "대시보드 구성", description: "홈 대시보드 레이아웃", type: "select", options: [ + key: "dashboardLayout", label: "대시보드 구성 (미구현)", description: "홈 대시보드 레이아웃", type: "select", options: [ { value: "default", label: "기본" }, { value: "compact", label: "컴팩트" }, { value: "detailed", label: "상세" }, diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..4a233136 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,7 @@ +sonar.projectKey=vexplor +sonar.projectName=vexplor +sonar.sources=backend-node/src,frontend/src +sonar.exclusions=**/node_modules/**,**/dist/**,**/*.test.*,**/test-scenarios/** +sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.host.url=http://localhost:9000 +sonar.sourceEncoding=UTF-8 From 31e225f6d3691c91a0cafffba3fc13126ccd3b76 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 16:59:25 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20POP=20=ED=99=94=EB=A9=B4=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B1=84=EB=B2=88=EA=B7=9C=EC=B9=99=20=EC=85=80?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=20=EB=B0=95=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 type "numbering-rule" 추가 - NumberingRuleSelect 컴포넌트: 회사별 채번규칙 목록 자동 로드 - 입고/출고 설정에서 inbound/outbound 키워드로 필터링 - 등록된 채번규칙이 없으면 안내 메시지 표시 --- .scannerwork/.sonar_lock | 0 .scannerwork/report-task.txt | 6 ++ .../admin/screenMng/popSettingsMng/page.tsx | 85 ++++++++++++++++++- sonar-project.properties | 6 +- 4 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 .scannerwork/.sonar_lock create mode 100644 .scannerwork/report-task.txt diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock new file mode 100644 index 00000000..e69de29b diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt new file mode 100644 index 00000000..363424f2 --- /dev/null +++ b/.scannerwork/report-task.txt @@ -0,0 +1,6 @@ +projectKey=vexplor +serverUrl=http://localhost:9000 +serverVersion=26.3.0.120487 +dashboardUrl=http://localhost:9000/dashboard?id=vexplor +ceTaskId=f2c72369-4d50-4483-bf76-b03788385757 +ceTaskUrl=http://localhost:9000/api/ce/task?id=f2c72369-4d50-4483-bf76-b03788385757 diff --git a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx index 80d8b7dd..1b5ee89d 100644 --- a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx @@ -165,15 +165,16 @@ interface SettingField { key: string; label: string; description: string; - type: "toggle" | "text" | "number" | "select" | "color" | "tags" | "array-object"; + type: "toggle" | "text" | "number" | "select" | "color" | "tags" | "array-object" | "numbering-rule"; defaultValue?: unknown; options?: { value: string; label: string }[]; fields?: { key: string; label: string; type: string }[]; + tableFilter?: string; // numbering-rule용: inbound/outbound 등 } const SETTINGS_SCHEMA: Record = { inbound: [ - { key: "numberingRuleId", label: "📋 입고번호 채번규칙", description: "입고 확정 시 사용할 채번규칙을 선택합니다", type: "text" }, + { key: "numberingRuleId", label: "📋 입고번호 채번규칙", description: "입고 확정 시 사용할 채번규칙을 선택합니다", type: "numbering-rule", tableFilter: "inbound" }, { key: "barcodeEnabled", label: "바코드 스캔 (미구현)", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, { key: "inspectionRequired", label: "검사 필수 (미구현)", description: "입고 시 검사 항목을 필수로 표시합니다", type: "toggle" }, { key: "photoUpload", label: "사진 첨부 (미구현)", description: "입고 확정 시 사진 첨부를 허용합니다", type: "toggle" }, @@ -181,7 +182,7 @@ const SETTINGS_SCHEMA: Record = { { key: "defectSeparation", label: "불량 분리 (미구현)", description: "양품/불량 수량을 분리 입력합니다", type: "toggle" }, ], outbound: [ - { key: "numberingRuleId", label: "📋 출고번호 채번규칙", description: "출고 확정 시 사용할 채번규칙을 선택합니다", type: "text" }, + { key: "numberingRuleId", label: "📋 출고번호 채번규칙", description: "출고 확정 시 사용할 채번규칙을 선택합니다", type: "numbering-rule", tableFilter: "outbound" }, { key: "barcodeEnabled", label: "바코드 스캔 (미구현)", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, { key: "photoUpload", label: "사진 첨부 (미구현)", description: "출고 시 사진 첨부를 허용합니다", type: "toggle" }, ], @@ -256,6 +257,82 @@ const SETTINGS_SCHEMA: Record = { ], }; +// ============================================================ +// 채번규칙 셀렉트 컴포넌트 +// ============================================================ + +function NumberingRuleSelect({ + field, + value, + onChange, +}: { + field: SettingField; + value: unknown; + onChange: (value: unknown) => void; +}) { + const { user } = useAuth(); + const [rules, setRules] = useState<{ value: string; label: string }[]>([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const loadRules = async () => { + setLoading(true); + try { + const companyCode = user?.companyCode || "COMPANY_7"; + const res = await apiClient.get(`/numbering-rules?company_code=${companyCode}`); + const data = res.data?.data || res.data || []; + const allRules = Array.isArray(data) ? data : (data.rules || []); + // tableFilter로 필터링 (inbound/outbound 등) + const filtered = field.tableFilter + ? allRules.filter((r: any) => + (r.table_name || "").toLowerCase().includes(field.tableFilter!) || + (r.rule_name || r.column_name || "").toLowerCase().includes(field.tableFilter!) + ) + : allRules; + setRules( + filtered.length > 0 + ? filtered.map((r: any) => ({ + value: r.id || r.rule_id, + label: `${r.rule_name || r.table_name + "." + r.column_name} (${r.prefix || ""}${r.separator || ""}...)`, + })) + : allRules.map((r: any) => ({ + value: r.id || r.rule_id, + label: `${r.rule_name || r.table_name + "." + r.column_name}`, + })) + ); + } catch { + setRules([]); + } + setLoading(false); + }; + loadRules(); + }, [user?.companyCode, field.tableFilter]); + + return ( +
+ +

{field.description}

+ {loading ? ( +

채번규칙 로드 중...

+ ) : rules.length === 0 ? ( +

등록된 채번규칙이 없습니다. PC에서 먼저 채번규칙을 등록해주세요.

+ ) : ( + + )} +
+ ); +} + // ============================================================ // Sub-components: TagEditor, ArrayObjectEditor // ============================================================ @@ -484,6 +561,8 @@ function SettingRow({
); + case "numbering-rule": + return ; case "color": return (
diff --git a/sonar-project.properties b/sonar-project.properties index 4a233136..b08e2bd3 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ sonar.projectKey=vexplor sonar.projectName=vexplor -sonar.sources=backend-node/src,frontend/src -sonar.exclusions=**/node_modules/**,**/dist/**,**/*.test.*,**/test-scenarios/** -sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.sources=backend-node/src +sonar.exclusions=**/node_modules/**,**/dist/**,**/*.test.*,**/test-scenarios/**,**/build/**,**/.next/** sonar.host.url=http://localhost:9000 sonar.sourceEncoding=UTF-8 +sonar.scm.disabled=true From 1abc6645fbb280f5961c255438f5d8c444ac4b1e Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 17:01:11 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20=EC=B1=84=EB=B2=88=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=85=80=EB=A0=89=ED=8A=B8=20=E2=80=94=20camelCase?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EA=B5=AC=EC=A1=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/screenMng/popSettingsMng/page.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx index 1b5ee89d..aa42b1a1 100644 --- a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx @@ -281,24 +281,23 @@ function NumberingRuleSelect({ const companyCode = user?.companyCode || "COMPANY_7"; const res = await apiClient.get(`/numbering-rules?company_code=${companyCode}`); const data = res.data?.data || res.data || []; - const allRules = Array.isArray(data) ? data : (data.rules || []); + const allRules: any[] = Array.isArray(data) ? data : (data.rules || []); // tableFilter로 필터링 (inbound/outbound 등) const filtered = field.tableFilter - ? allRules.filter((r: any) => - (r.table_name || "").toLowerCase().includes(field.tableFilter!) || - (r.rule_name || r.column_name || "").toLowerCase().includes(field.tableFilter!) - ) + ? allRules.filter((r: any) => { + const t = (r.tableName || "").toLowerCase(); + const c = (r.columnName || "").toLowerCase(); + const n = (r.ruleName || "").toLowerCase(); + const f = field.tableFilter!.toLowerCase(); + return t.includes(f) || c.includes(f) || n.includes(f); + }) : allRules; + const finalRules = filtered.length > 0 ? filtered : allRules; setRules( - filtered.length > 0 - ? filtered.map((r: any) => ({ - value: r.id || r.rule_id, - label: `${r.rule_name || r.table_name + "." + r.column_name} (${r.prefix || ""}${r.separator || ""}...)`, - })) - : allRules.map((r: any) => ({ - value: r.id || r.rule_id, - label: `${r.rule_name || r.table_name + "." + r.column_name}`, - })) + finalRules.map((r: any) => ({ + value: r.ruleId, + label: `${r.ruleName || r.tableName + "." + r.columnName}`, + })) ); } catch { setRules([]); From d795c74dbd40b2ad96216e5411b32504221cc9df Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 17:11:39 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20POP=20=EC=9E=85=EA=B3=A0/?= =?UTF-8?q?=EC=B6=9C=EA=B3=A0=20=EC=B1=84=EB=B2=88=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=EC=9D=84=20=ED=99=94=EB=A9=B4=EC=84=A4=EC=A0=95=EA=B3=BC=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드: - receiving/generate-number: ?ruleId 쿼리 받아 numberingRuleService.allocateCode 사용 - outbound/generate-number: 동일 - ruleId 없거나 실패 시 기존 하드코딩 채번으로 폴백 프론트: - InboundCartPage: 확정 시 화면설정의 popConfig.inbound.numberingRuleId 읽어 ruleId 전달 - OutboundCartPage: 확정 시 화면설정의 popConfig.outbound.numberingRuleId 읽어 ruleId 전달 POP 화면설정에서 채번규칙 선택 → 입고/출고 확정 시 자동 적용 --- .../src/controllers/outboundController.ts | 15 ++++++++++++++- .../src/controllers/receivingController.ts | 16 +++++++++++++++- .../pop/hardcoded/inbound/InboundCartPage.tsx | 8 +++++++- .../pop/hardcoded/outbound/OutboundCartPage.tsx | 8 +++++++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/backend-node/src/controllers/outboundController.ts b/backend-node/src/controllers/outboundController.ts index b4b942a0..7e77974c 100644 --- a/backend-node/src/controllers/outboundController.ts +++ b/backend-node/src/controllers/outboundController.ts @@ -477,8 +477,21 @@ export async function getItems(req: AuthenticatedRequest, res: Response) { export async function generateNumber(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; - const pool = getPool(); + const ruleId = (req.query.ruleId as string) || (req.query.rule_id as string); + // 1순위: POP 화면설정에서 선택한 채번규칙 사용 + if (ruleId && ruleId !== "__none__") { + try { + const { numberingRuleService } = await import("../services/numberingRuleService"); + const newNumber = await numberingRuleService.allocateCode(ruleId, companyCode); + return res.json({ success: true, data: newNumber }); + } catch (e: any) { + logger.warn("선택한 채번규칙 사용 실패, 기본 채번으로 폴백", { ruleId, error: e.message }); + } + } + + // 2순위: 기본 하드코딩 채번 (OUT-YYYY-XXXX) + const pool = getPool(); const today = new Date(); const yyyy = today.getFullYear(); const prefix = `OUT-${yyyy}-`; diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index 8f9c9863..fb358a06 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -881,8 +881,22 @@ export async function getItems(req: AuthenticatedRequest, res: Response) { export async function generateNumber(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; - const pool = getPool(); + const ruleId = (req.query.ruleId as string) || (req.query.rule_id as string); + // 1순위: POP 화면설정에서 선택한 채번규칙 사용 + if (ruleId && ruleId !== "__none__") { + try { + const { numberingRuleService } = await import("../services/numberingRuleService"); + const newNumber = await numberingRuleService.allocateCode(ruleId, companyCode); + return res.json({ success: true, data: newNumber }); + } catch (e: any) { + logger.warn("선택한 채번규칙 사용 실패, 기본 채번으로 폴백", { ruleId, error: e.message }); + // 폴백 + } + } + + // 2순위: 기본 하드코딩 채번 (RCV-YYYY-XXXX) + const pool = getPool(); const today = new Date(); const yyyy = today.getFullYear(); const prefix = `RCV-${yyyy}-`; diff --git a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx index 960794b4..06d142c2 100644 --- a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx @@ -310,9 +310,15 @@ export function InboundCartPage() { try { // 확정 시점에 채번 (동시접속 충돌 방지) + // POP 화면설정에서 선택한 채번규칙 사용 (없으면 기본) let finalNumber = ""; try { - const numRes = await apiClient.get("/receiving/generate-number"); + const settingsRes: any = await apiClient.get("/screen-management/screens/6527/layout-pop").catch(() => null); + const ruleId = settingsRes?.data?.data?.settings?.popConfig?.inbound?.numberingRuleId; + const url = ruleId && ruleId !== "__none__" + ? `/receiving/generate-number?ruleId=${encodeURIComponent(ruleId)}` + : "/receiving/generate-number"; + const numRes = await apiClient.get(url); if (numRes.data?.success && numRes.data?.data) { finalNumber = numRes.data.data; setInboundNumber(finalNumber); diff --git a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx index 918e0510..05fd095f 100644 --- a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx @@ -305,9 +305,15 @@ export function OutboundCartPage() { try { // Generate outbound number at confirm time + // POP 화면설정에서 선택한 채번규칙 사용 (없으면 기본) let finalNumber = ""; try { - const numRes = await apiClient.get("/outbound/generate-number"); + const settingsRes: any = await apiClient.get("/screen-management/screens/5/layout-pop").catch(() => null); + const ruleId = settingsRes?.data?.data?.settings?.popConfig?.outbound?.numberingRuleId; + const url = ruleId && ruleId !== "__none__" + ? `/outbound/generate-number?ruleId=${encodeURIComponent(ruleId)}` + : "/outbound/generate-number"; + const numRes = await apiClient.get(url); if (numRes.data?.success && numRes.data?.data) { finalNumber = numRes.data.data; setOutboundNumber(finalNumber); From 444f0f95f3b981c7bb7264b4bd2a40232ace636f Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 17:55:40 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20POP=20=ED=99=94=EB=A9=B4=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=97=90=20=EC=B6=9C=EA=B3=A0=20=EC=9E=A5=EB=B0=94?= =?UTF-8?q?=EA=B5=AC=EB=8B=88=20=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - screen_definitions에 7010 (POP_OUTBOUND_CART) 신규 등록 - popSettingsMng SCREEN_GROUPS의 outbound에 outbound-cart 추가 - OutboundCartPage 채번규칙 조회 screen_id를 5 → 7010으로 변경 이로써 POP 설정 페이지에서 출고 장바구니 화면도 미리보기 + 채번규칙 설정 가능 --- frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx | 1 + .../components/pop/hardcoded/outbound/OutboundCartPage.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx index aa42b1a1..58085f8f 100644 --- a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx @@ -121,6 +121,7 @@ const SCREEN_GROUPS: ScreenGroup[] = [ screens: [ { id: "sales-outbound", name: "판매출고", url: "/pop/outbound/sales", settingsKey: "outbound", screenId: 5 }, { id: "outbound-type", name: "출고유형선택", url: "/pop/outbound", settingsKey: "outbound", screenId: 6 }, + { id: "outbound-cart", name: "출고 장바구니", url: "/pop/outbound/cart", settingsKey: "outbound", screenId: 7010 }, ], }, { diff --git a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx index 05fd095f..a54bed23 100644 --- a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx @@ -306,9 +306,10 @@ export function OutboundCartPage() { try { // Generate outbound number at confirm time // POP 화면설정에서 선택한 채번규칙 사용 (없으면 기본) + // 출고 장바구니 전용 screen_id 7010 let finalNumber = ""; try { - const settingsRes: any = await apiClient.get("/screen-management/screens/5/layout-pop").catch(() => null); + const settingsRes: any = await apiClient.get("/screen-management/screens/7010/layout-pop").catch(() => null); const ruleId = settingsRes?.data?.data?.settings?.popConfig?.outbound?.numberingRuleId; const url = ruleId && ruleId !== "__none__" ? `/outbound/generate-number?ruleId=${encodeURIComponent(ruleId)}` From dad8df7b10d836cb1ae377c10f259e852568b18a Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 18:23:36 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix:=20POP=20=ED=99=94=EB=A9=B4=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20iframe=20=EB=A7=A4=EC=B9=AD=20=E2=80=94=20=EC=A0=95?= =?UTF-8?q?=ED=99=95/=EA=B8=B8=EC=9D=B4=20=EA=B8=B4=20url=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문제: /pop/outbound/cart 진입 시 startsWith 매칭으로 /pop/outbound(출고유형선택)가 먼저 잡혀 selectedScreen이 잘못 설정됨 해결: 정확 일치 1순위 + url 길이 긴 항목 startsWith 우선 --- .../admin/screenMng/popSettingsMng/page.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx index 58085f8f..b00c69ad 100644 --- a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx @@ -830,13 +830,24 @@ export default function PopSettingsMngPage() { const path = iframeRef.current?.contentWindow?.location.pathname; if (path && path !== lastPath) { setLastPath(path); + // 1순위: 정확 일치, 2순위: 길이 긴 url부터 startsWith 매칭 (구체적 경로 우선) + let bestMatch: ScreenItem | null = null; + let bestUrlLength = -1; for (const group of SCREEN_GROUPS) { - const found = group.screens.find((s) => path === s.url || path.startsWith(s.url + "/")); - if (found) { - setSelectedScreen(found); - break; + for (const s of group.screens) { + if (path === s.url) { + bestMatch = s; + bestUrlLength = Infinity; + break; + } + if (path.startsWith(s.url + "/") && s.url.length > bestUrlLength) { + bestMatch = s; + bestUrlLength = s.url.length; + } } + if (bestUrlLength === Infinity) break; } + if (bestMatch) setSelectedScreen(bestMatch); } } catch { // cross-origin: silently ignore From a7b914407cc8c06321fcb8889eb208379aef4bbb Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 7 Apr 2026 18:46:18 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=EC=B1=84=EB=B2=88=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=ED=95=84=EB=93=9C=EB=A5=BC=20=EC=9E=85=EA=B3=A0/?= =?UTF-8?q?=EC=B6=9C=EA=B3=A0=20=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=EB=A7=8C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SettingField에 showOnlyForScreens 옵션 추가 - 입고번호 채번: inbound-cart에서만 표시 - 출고번호 채번: outbound-cart에서만 표시 - 구매입고/판매출고/유형선택 등 다른 화면에서는 채번 필드 숨김 이로써 채번 설정의 컨텍스트가 명확해짐 --- .../(main)/admin/screenMng/popSettingsMng/page.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx index b00c69ad..619c3675 100644 --- a/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/popSettingsMng/page.tsx @@ -171,11 +171,12 @@ interface SettingField { options?: { value: string; label: string }[]; fields?: { key: string; label: string; type: string }[]; tableFilter?: string; // numbering-rule용: inbound/outbound 등 + showOnlyForScreens?: string[]; // 특정 화면 ID에서만 표시 (예: ["inbound-cart"]) } const SETTINGS_SCHEMA: Record = { inbound: [ - { key: "numberingRuleId", label: "📋 입고번호 채번규칙", description: "입고 확정 시 사용할 채번규칙을 선택합니다", type: "numbering-rule", tableFilter: "inbound" }, + { key: "numberingRuleId", label: "📋 입고번호 채번규칙", description: "입고 확정 시 사용할 채번규칙을 선택합니다", type: "numbering-rule", tableFilter: "inbound", showOnlyForScreens: ["inbound-cart"] }, { key: "barcodeEnabled", label: "바코드 스캔 (미구현)", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, { key: "inspectionRequired", label: "검사 필수 (미구현)", description: "입고 시 검사 항목을 필수로 표시합니다", type: "toggle" }, { key: "photoUpload", label: "사진 첨부 (미구현)", description: "입고 확정 시 사진 첨부를 허용합니다", type: "toggle" }, @@ -183,7 +184,7 @@ const SETTINGS_SCHEMA: Record = { { key: "defectSeparation", label: "불량 분리 (미구현)", description: "양품/불량 수량을 분리 입력합니다", type: "toggle" }, ], outbound: [ - { key: "numberingRuleId", label: "📋 출고번호 채번규칙", description: "출고 확정 시 사용할 채번규칙을 선택합니다", type: "numbering-rule", tableFilter: "outbound" }, + { key: "numberingRuleId", label: "📋 출고번호 채번규칙", description: "출고 확정 시 사용할 채번규칙을 선택합니다", type: "numbering-rule", tableFilter: "outbound", showOnlyForScreens: ["outbound-cart"] }, { key: "barcodeEnabled", label: "바코드 스캔 (미구현)", description: "바코드/QR 스캔 기능을 사용합니다", type: "toggle" }, { key: "photoUpload", label: "사진 첨부 (미구현)", description: "출고 시 사진 첨부를 허용합니다", type: "toggle" }, ], @@ -944,7 +945,12 @@ export default function PopSettingsMngPage() { // ---- Current screen schema values ---- const currentSettingsKey = selectedScreen?.settingsKey || "inbound"; - const currentFields = SETTINGS_SCHEMA[currentSettingsKey] || []; + const allFields = SETTINGS_SCHEMA[currentSettingsKey] || []; + // showOnlyForScreens 옵션이 있으면 현재 화면 ID와 일치할 때만 표시 + const currentFields = allFields.filter((f) => { + if (!f.showOnlyForScreens) return true; + return selectedScreen?.id ? f.showOnlyForScreens.includes(selectedScreen.id) : false; + }); const currentValues = (settings.screens as Record>)[currentSettingsKey] || {}; return ( From 126f11354e10f98ebef72615aa3be76bd7c823e8 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 8 Apr 2026 09:57:57 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20POP=20=EC=B6=9C=EA=B3=A0=20?= =?UTF-8?q?=ED=99=95=EC=A0=95=20=EC=8B=9C=20outbound=5Fstatus=EB=A5=BC=20'?= =?UTF-8?q?=EC=B6=9C=EA=B3=A0=EC=99=84=EB=A3=8C'=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존: '대기' 하드코딩 → 출고 확정해도 상태 미갱신 수정: '출고완료'로 → 출고 확정 시 정상 상태 반영 --- frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx index a54bed23..32bcc462 100644 --- a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx @@ -344,7 +344,7 @@ export function OutboundCartPage() { customer_name: item.customer_name, source_type: "shipment_instruction_detail", source_id: item.source_id || item.id, - outbound_status: "대기", + outbound_status: "출고완료", })), }; From 71abfebb51fe1e5a4668d90bbdc29755d113e647 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 8 Apr 2026 10:04:44 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20POP=20=EC=9E=85=EA=B3=A0=20?= =?UTF-8?q?=ED=99=95=EC=A0=95=20=EC=8B=9C=20inbound=5Fstatus=EB=A5=BC=20'?= =?UTF-8?q?=EC=9E=85=EA=B3=A0=EC=99=84=EB=A3=8C'=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존: inbound_status 미전달 → 백엔드 기본값 '대기' 수정: '입고완료' 명시적 전달 --- frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx index 06d142c2..d6a90ec3 100644 --- a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx @@ -350,6 +350,7 @@ export function InboundCartPage() { reference_number: item.purchase_no, supplier_code: item.supplier_code, supplier_name: item.supplier_name, + inbound_status: "입고완료", inspection_status: inspResult?.completed ? "검사완료" : item.inspection_required