From c77c6290d3d686c8c2d4ad4f5d97cf54f13d3c98 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 15 Jan 2026 16:53:27 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=B5=9C=EC=86=8C?= =?UTF-8?q?=ED=95=9C=EC=9D=98=20=EC=BD=94=EB=93=9C=EB=A7=8C=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../split-panel-layout/SplitPanelLayoutComponent.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index a84c90fd..9609f4aa 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -84,7 +84,14 @@ export const SplitPanelLayoutComponent: React.FC // "테이블명.컬럼명" 형식을 "원본컬럼_조인컬럼명" 형식으로 변환하여 데이터 접근 const getEntityJoinValue = useCallback( (item: any, columnName: string, entityColumnMap?: Record): any => { - // 직접 매칭 시도 + // 🆕 백엔드가 제공하는 _label 필드 우선 사용 + // 백엔드는 "표시 컬럼"이 설정된 경우 columnName_label을 자동 생성 + const labelKey = `${columnName}_label`; + if (item[labelKey] !== undefined && item[labelKey] !== "" && item[labelKey] !== null) { + return item[labelKey]; + } + + // 직접 매칭 시도 (JOIN된 값이 없으면 원본 값 반환) if (item[columnName] !== undefined) { return item[columnName]; } From a36802ab10f3d6e8e0927f00e95ff9ea01c7c6e0 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 15 Jan 2026 16:54:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20RepeaterFieldGroup=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B2=BD=EB=A1=9C=EC=97=90=20=EC=B1=84=EB=B2=88=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=ED=95=A0=EB=8B=B9=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/utils/buttonActions.ts | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index a54850ee..debf58b2 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -708,6 +708,47 @@ export class ButtonActionExecutor { if (repeaterJsonKeys.length > 0) { console.log("🔄 [handleSave] RepeaterFieldGroup JSON 문자열 감지:", repeaterJsonKeys); + // 🎯 채번 규칙 할당 처리 (RepeaterFieldGroup 저장 전에 실행) + console.log("🔍 [handleSave-RepeaterFieldGroup] 채번 규칙 할당 체크 시작"); + + const fieldsWithNumberingRepeater: Record = {}; + + // formData에서 채번 규칙이 설정된 필드 찾기 + for (const [key, value] of Object.entries(context.formData)) { + if (key.endsWith("_numberingRuleId") && value) { + const fieldName = key.replace("_numberingRuleId", ""); + fieldsWithNumberingRepeater[fieldName] = value as string; + console.log(`🎯 [handleSave-RepeaterFieldGroup] 채번 필드 발견: ${fieldName} → 규칙 ${value}`); + } + } + + console.log("📋 [handleSave-RepeaterFieldGroup] 채번 규칙이 설정된 필드:", fieldsWithNumberingRepeater); + + // 채번 규칙이 있는 필드에 대해 allocateCode 호출 + if (Object.keys(fieldsWithNumberingRepeater).length > 0) { + console.log("🎯 [handleSave-RepeaterFieldGroup] 채번 규칙 할당 시작 (allocateCode 호출)"); + const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); + + for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) { + try { + console.log(`🔄 [handleSave-RepeaterFieldGroup] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`); + const allocateResult = await allocateNumberingCode(ruleId); + + if (allocateResult.success && allocateResult.data?.generatedCode) { + const newCode = allocateResult.data.generatedCode; + console.log(`✅ [handleSave-RepeaterFieldGroup] ${fieldName} 새 코드 할당: ${context.formData[fieldName]} → ${newCode}`); + context.formData[fieldName] = newCode; + } else { + console.warn(`⚠️ [handleSave-RepeaterFieldGroup] ${fieldName} 코드 할당 실패:`, allocateResult.error); + } + } catch (allocateError) { + console.error(`❌ [handleSave-RepeaterFieldGroup] ${fieldName} 코드 할당 오류:`, allocateError); + } + } + } + + console.log("✅ [handleSave-RepeaterFieldGroup] 채번 규칙 할당 완료"); + // 🆕 상단 폼 데이터(마스터 정보) 추출 // RepeaterFieldGroup JSON과 컴포넌트 키를 제외한 나머지가 마스터 정보 const masterFields: Record = {}; From e8bdcbb95cbe35a2a1e2fd6558f4eded2e2c18cc Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 15 Jan 2026 14:36:00 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EB=B0=9C=EC=A3=BC/=EC=9E=85?= =?UTF-8?q?=EA=B3=A0=EA=B4=80=EB=A6=AC=20=EA=B7=B8=EB=A3=B9=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EC=8B=9C=20=EB=8B=A8=EA=B1=B4=EB=A7=8C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20EditModal.tsx:=20conditional-container=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=8B=9C=20onSave=20=EB=AF=B8=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20ModalRepeaterTableCom?= =?UTF-8?q?ponent.tsx:=20groupedData=20prop=20=EC=9A=B0=EC=84=A0=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=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/screen/EditModal.tsx | 36 ++++++------------- .../ModalRepeaterTableComponent.tsx | 16 +++++++-- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index b3c94ade..7c722ad6 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -309,17 +309,10 @@ export const EditModal: React.FC = ({ className }) => { // 🆕 그룹 데이터 조회 함수 const loadGroupData = async () => { if (!modalState.tableName || !modalState.groupByColumns || modalState.groupByColumns.length === 0) { - // console.warn("테이블명 또는 그룹핑 컬럼이 없습니다."); return; } try { - // console.log("🔍 그룹 데이터 조회 시작:", { - // tableName: modalState.tableName, - // groupByColumns: modalState.groupByColumns, - // editData: modalState.editData, - // }); - // 그룹핑 컬럼 값 추출 (예: order_no = "ORD-20251124-001") const groupValues: Record = {}; modalState.groupByColumns.forEach((column) => { @@ -329,15 +322,9 @@ export const EditModal: React.FC = ({ className }) => { }); if (Object.keys(groupValues).length === 0) { - // console.warn("그룹핑 컬럼 값이 없습니다:", modalState.groupByColumns); return; } - // console.log("🔍 그룹 조회 요청:", { - // tableName: modalState.tableName, - // groupValues, - // }); - // 같은 그룹의 모든 레코드 조회 (entityJoinApi 사용) const { entityJoinApi } = await import("@/lib/api/entityJoin"); const response = await entityJoinApi.getTableDataWithJoins(modalState.tableName, { @@ -347,23 +334,19 @@ export const EditModal: React.FC = ({ className }) => { enableEntityJoin: true, }); - // console.log("🔍 그룹 조회 응답:", response); - // entityJoinApi는 배열 또는 { data: [] } 형식으로 반환 const dataArray = Array.isArray(response) ? response : response?.data || []; if (dataArray.length > 0) { - // console.log("✅ 그룹 데이터 조회 성공:", dataArray.length, "건"); setGroupData(dataArray); setOriginalGroupData(JSON.parse(JSON.stringify(dataArray))); // Deep copy toast.info(`${dataArray.length}개의 관련 품목을 불러왔습니다.`); } else { - console.warn("그룹 데이터가 없습니다:", response); setGroupData([modalState.editData]); // 기본값: 선택된 행만 setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]); } } catch (error: any) { - console.error("❌ 그룹 데이터 조회 오류:", error); + console.error("그룹 데이터 조회 오류:", error); toast.error("관련 데이터를 불러오는 중 오류가 발생했습니다."); setGroupData([modalState.editData]); // 기본값: 선택된 행만 setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]); @@ -1043,17 +1026,18 @@ export const EditModal: React.FC = ({ className }) => { const groupedDataProp = groupData.length > 0 ? groupData : undefined; // 🆕 UniversalFormModal이 있는지 확인 (자체 저장 로직 사용) - // 최상위 컴포넌트 또는 조건부 컨테이너 내부 화면에 universal-form-modal이 있는지 확인 + // 최상위 컴포넌트에 universal-form-modal이 있는지 확인 + // ⚠️ 수정: conditional-container는 제외 (groupData가 있으면 EditModal.handleSave 사용) const hasUniversalFormModal = screenData.components.some( (c) => { - // 최상위에 universal-form-modal이 있는 경우 + // 최상위에 universal-form-modal이 있는 경우만 자체 저장 로직 사용 if (c.componentType === "universal-form-modal") return true; - // 조건부 컨테이너 내부에 universal-form-modal이 있는 경우 - // (조건부 컨테이너가 있으면 내부 화면에서 universal-form-modal을 사용하는 것으로 가정) - if (c.componentType === "conditional-container") return true; return false; } ); + + // 🆕 그룹 데이터가 있으면 EditModal.handleSave 사용 (일괄 저장) + const shouldUseEditModalSave = groupData.length > 0 || !hasUniversalFormModal; // 🔑 첨부파일 컴포넌트가 행(레코드) 단위로 파일을 저장할 수 있도록 tableName 추가 const enrichedFormData = { @@ -1095,9 +1079,9 @@ export const EditModal: React.FC = ({ className }) => { id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} - // 🆕 UniversalFormModal이 있으면 onSave 전달 안 함 (자체 저장 로직 사용) - // ModalRepeaterTable만 있으면 기존대로 onSave 전달 (호환성 유지) - onSave={hasUniversalFormModal ? undefined : handleSave} + // 🆕 그룹 데이터가 있거나 UniversalFormModal이 없으면 EditModal.handleSave 사용 + // groupData가 있으면 일괄 저장을 위해 반드시 EditModal.handleSave 사용 + onSave={shouldUseEditModalSave ? handleSave : undefined} isInModal={true} // 🆕 그룹 데이터를 ModalRepeaterTable에 전달 groupedData={groupedDataProp} diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 2caf1332..153cebdf 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -180,8 +180,11 @@ export function ModalRepeaterTableComponent({ filterCondition: propFilterCondition, companyCode: propCompanyCode, + // 🆕 그룹 데이터 (EditModal에서 전달, 같은 그룹의 여러 품목) + groupedData, + ...props -}: ModalRepeaterTableComponentProps) { +}: ModalRepeaterTableComponentProps & { groupedData?: Record[] }) { // ✅ config 또는 component.config 또는 개별 prop 우선순위로 병합 const componentConfig = { ...config, @@ -208,9 +211,16 @@ export function ModalRepeaterTableComponent({ // 모달 필터 설정 const modalFilters = componentConfig?.modalFilters || []; - // ✅ value는 formData[columnName] 우선, 없으면 prop 사용 + // ✅ value는 groupedData 우선, 없으면 formData[columnName], 없으면 prop 사용 const columnName = component?.columnName; - const externalValue = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; + + // 🆕 groupedData가 전달되면 (EditModal에서 그룹 조회 결과) 우선 사용 + const externalValue = (() => { + if (groupedData && groupedData.length > 0) { + return groupedData; + } + return (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; + })(); // 빈 객체 판단 함수 (수정 모달의 실제 데이터는 유지) const isEmptyRow = (item: any): boolean => {