From 5e97a3a5e97b5f7d0b74ebc0df0f64e1e1c45d1d Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 9 Dec 2025 16:11:04 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=99=94=EB=A9=B4=20=EB=B3=B5=EC=82=AC?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20UniversalFormModal=20before?= =?UTF-8?q?FormSave=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - screenManagementService: PostgreSQL regexp_replace로 정확한 최대 번호 조회 - CopyScreenModal: linkedScreens 의존성 추가로 모달 코드 생성 보장 - UniversalFormModal: beforeFormSave 이벤트 리스너로 ButtonPrimary 연동 - 설정된 필드만 병합하여 의도치 않은 덮어쓰기 방지 --- .../src/services/screenManagementService.ts | 41 +++++++++------- .../components/screen/CopyScreenModal.tsx | 14 +++++- .../ModalRepeaterTableConfigPanel.tsx | 2 +- .../modal-repeater-table/RepeaterTable.tsx | 16 +++--- .../components/modal-repeater-table/types.ts | 8 +-- .../UniversalFormModalComponent.tsx | 49 +++++++++++++++++++ .../UniversalFormModalConfigPanel.tsx | 16 +++--- 7 files changed, 104 insertions(+), 42 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 6628cf4c..9fc0f079 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -2360,30 +2360,33 @@ export class ScreenManagementService { const lockId = Buffer.from(companyCode).reduce((acc, byte) => acc + byte, 0); await client.query('SELECT pg_advisory_xact_lock($1)', [lockId]); - // 현재 최대 번호 조회 - const existingScreens = await client.query<{ screen_code: string }>( - `SELECT screen_code FROM screen_definitions - WHERE company_code = $1 AND screen_code LIKE $2 - ORDER BY screen_code DESC - LIMIT 10`, - [companyCode, `${companyCode}%`] + // 현재 최대 번호 조회 (숫자 추출 후 정렬) + // 패턴: COMPANY_CODE_XXX 또는 COMPANY_CODEXXX + const existingScreens = await client.query<{ screen_code: string; num: number }>( + `SELECT screen_code, + COALESCE( + NULLIF( + regexp_replace(screen_code, $2, '\\1'), + screen_code + )::integer, + 0 + ) as num + FROM screen_definitions + WHERE company_code = $1 + AND screen_code ~ $2 + AND deleted_date IS NULL + ORDER BY num DESC + LIMIT 1`, + [companyCode, `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[_]?(\\d+)$`] ); let maxNumber = 0; - const pattern = new RegExp( - `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:_)?(\\d+)$` - ); - - for (const screen of existingScreens.rows) { - const match = screen.screen_code.match(pattern); - if (match) { - const number = parseInt(match[1], 10); - if (number > maxNumber) { - maxNumber = number; - } - } + if (existingScreens.rows.length > 0 && existingScreens.rows[0].num) { + maxNumber = existingScreens.rows[0].num; } + console.log(`🔢 현재 최대 화면 코드 번호: ${companyCode} → ${maxNumber}`); + // count개의 코드를 순차적으로 생성 const codes: string[] = []; for (let i = 0; i < count; i++) { diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx index c37603c5..f5e71c4c 100644 --- a/frontend/components/screen/CopyScreenModal.tsx +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -166,18 +166,28 @@ export default function CopyScreenModal({ // linkedScreens 로딩이 완료되면 화면 코드 생성 useEffect(() => { + // 모달 화면들의 코드가 모두 설정되었는지 확인 + const allModalCodesSet = linkedScreens.length === 0 || + linkedScreens.every(screen => screen.newScreenCode); + console.log("🔍 코드 생성 조건 체크:", { targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreensCount: linkedScreens.length, + allModalCodesSet, }); - if (targetCompanyCode && !loadingLinkedScreens && !screenCode) { + // 조건: 회사 코드가 있고, 로딩이 완료되고, (메인 코드가 없거나 모달 코드가 없을 때) + const needsCodeGeneration = targetCompanyCode && + !loadingLinkedScreens && + (!screenCode || (linkedScreens.length > 0 && !allModalCodesSet)); + + if (needsCodeGeneration) { console.log("✅ 화면 코드 생성 시작 (linkedScreens 개수:", linkedScreens.length, ")"); generateScreenCodes(); } - }, [targetCompanyCode, loadingLinkedScreens, screenCode]); + }, [targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreens]); // 회사 목록 조회 const loadCompanies = async () => { diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx index 914e34f1..7a11bdb1 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx @@ -1438,7 +1438,7 @@ export function ModalRepeaterTableConfigPanel({ checked={col.dynamicDataSource?.enabled || false} onCheckedChange={(checked) => toggleDynamicDataSource(index, checked)} /> - +

컬럼 헤더 클릭으로 데이터 소스 전환 (예: 거래처별 단가, 품목별 단가)

diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 869884a7..410fd9a6 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -161,11 +161,11 @@ export function RepeaterTable({ : null; return ( - + {hasDynamicSource ? ( ) : ( <> - {col.label} - {col.required && *} + {col.label} + {col.required && *} )} - + ); })} diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index 028a892b..6097aaf3 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -49,7 +49,7 @@ export interface RepeaterColumnConfig { required?: boolean; // 필수 입력 여부 defaultValue?: string | number | boolean; // 기본값 selectOptions?: { value: string; label: string }[]; // select일 때 옵션 - + // 컬럼 매핑 설정 mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정 @@ -142,16 +142,16 @@ export interface MultiTableJoinStep { export interface ColumnMapping { /** 매핑 타입 */ type: "source" | "reference" | "manual"; - + /** 매핑 타입별 설정 */ // type: "source" - 소스 테이블 (모달에서 선택한 항목)의 컬럼에서 가져오기 sourceField?: string; // 소스 테이블의 컬럼명 (예: "item_name") - + // type: "reference" - 외부 테이블 참조 (조인) referenceTable?: string; // 참조 테이블명 (예: "customer_item_mapping") referenceField?: string; // 참조 테이블에서 가져올 컬럼 (예: "basic_price") joinCondition?: JoinCondition[]; // 조인 조건 - + // type: "manual" - 사용자가 직접 입력 } diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 3fa8f623..7598d3a8 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -144,6 +144,55 @@ export function UniversalFormModalComponent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); + // 🆕 beforeFormSave 이벤트 리스너 - ButtonPrimary 저장 시 formData를 전달 + // 설정된 필드(columnName)만 병합하여 의도치 않은 덮어쓰기 방지 + useEffect(() => { + const handleBeforeFormSave = (event: Event) => { + if (!(event instanceof CustomEvent) || !event.detail?.formData) return; + + // 설정에 정의된 필드 columnName 목록 수집 + const configuredFields = new Set(); + config.sections.forEach((section) => { + section.fields.forEach((field) => { + if (field.columnName) { + configuredFields.add(field.columnName); + } + }); + }); + + console.log("[UniversalFormModal] beforeFormSave 이벤트 수신"); + console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields)); + + // UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함) + // 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀 + // (UniversalFormModal이 해당 필드의 주인이므로) + for (const [key, value] of Object.entries(formData)) { + // 설정에 정의된 필드만 병합 + if (configuredFields.has(key)) { + if (value !== undefined && value !== null && value !== "") { + event.detail.formData[key] = value; + console.log(`[UniversalFormModal] 필드 병합: ${key} =`, value); + } + } + } + + // 반복 섹션 데이터도 병합 (필요한 경우) + if (Object.keys(repeatSections).length > 0) { + for (const [sectionId, items] of Object.entries(repeatSections)) { + const sectionKey = `_repeatSection_${sectionId}`; + event.detail.formData[sectionKey] = items; + console.log(`[UniversalFormModal] 반복 섹션 병합: ${sectionKey}`, items); + } + } + }; + + window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener); + + return () => { + window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener); + }; + }, [formData, repeatSections, config.sections]); + // 필드 레벨 linkedFieldGroup 데이터 로드 useEffect(() => { const loadData = async () => { diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index 3ce7477a..48542342 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -413,14 +413,14 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor ButtonPrimary 컴포넌트로 저장 버튼을 별도 구성할 경우 끄세요 {config.modal.showSaveButton !== false && ( -
- - updateModalConfig({ saveButtonText: e.target.value })} - className="h-7 text-xs mt-1" - /> -
+
+ + updateModalConfig({ saveButtonText: e.target.value })} + className="h-7 text-xs mt-1" + /> +
)}