From 85f8637ce06e2b5938ec3ea7c9b2224d8fb5702d Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 8 Jan 2026 17:06:28 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=B1=84=EB=B2=88=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=20=ED=95=A0=EB=8B=B9=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20-=20=EB=B3=B5=EC=82=AC=20=EC=8B=9C=20=ED=92=88=EB=AA=A9?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=90=EB=8F=99=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/numberingRuleController.ts | 5 +- frontend/components/screen/EditModal.tsx | 66 ++++- .../pivot-grid/PivotGridRenderer.tsx | 262 ++++-------------- .../registry/components/pivot-grid/index.ts | 12 +- .../text-input/TextInputComponent.tsx | 21 +- frontend/lib/utils/buttonActions.ts | 85 +++++- 6 files changed, 211 insertions(+), 240 deletions(-) diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index 031a1506..ab7114a5 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -217,11 +217,14 @@ router.post("/:ruleId/allocate", authenticateToken, async (req: AuthenticatedReq const companyCode = req.user!.companyCode; const { ruleId } = req.params; + logger.info("코드 할당 요청", { ruleId, companyCode }); + try { const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode); + logger.info("코드 할당 성공", { ruleId, allocatedCode }); return res.json({ success: true, data: { generatedCode: allocatedCode } }); } catch (error: any) { - logger.error("코드 할당 실패", { error: error.message }); + logger.error("코드 할당 실패", { ruleId, companyCode, error: error.message }); return res.status(500).json({ success: false, error: error.message }); } }); diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 32451d18..c1b644cc 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -761,10 +761,74 @@ export const EditModal: React.FC = ({ className }) => { // INSERT 모드 console.log("[EditModal] INSERT 모드 - 새 데이터 생성:", formData); + // 🆕 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가) + const dataToSave = { ...formData }; + const fieldsWithNumbering: Record = {}; + + // formData에서 채번 규칙이 설정된 필드 찾기 + for (const [key, value] of Object.entries(formData)) { + if (key.endsWith("_numberingRuleId") && value) { + const fieldName = key.replace("_numberingRuleId", ""); + fieldsWithNumbering[fieldName] = value as string; + console.log(`🎯 [EditModal] 채번 규칙 발견: ${fieldName} → 규칙 ${value}`); + } + } + + // 채번 규칙이 있는 필드에 대해 allocateCode 호출 + if (Object.keys(fieldsWithNumbering).length > 0) { + console.log("🎯 [EditModal] 채번 규칙 할당 시작"); + const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); + + let hasAllocationFailure = false; + const failedFields: string[] = []; + + for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { + try { + console.log(`🔄 [EditModal] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`); + const allocateResult = await allocateNumberingCode(ruleId); + + if (allocateResult.success && allocateResult.data?.generatedCode) { + const newCode = allocateResult.data.generatedCode; + console.log(`✅ [EditModal] ${fieldName} 새 코드 할당: ${dataToSave[fieldName]} → ${newCode}`); + dataToSave[fieldName] = newCode; + } else { + console.warn(`⚠️ [EditModal] ${fieldName} 코드 할당 실패:`, allocateResult.error); + if (!dataToSave[fieldName] || dataToSave[fieldName] === "") { + hasAllocationFailure = true; + failedFields.push(fieldName); + } + } + } catch (allocateError) { + console.error(`❌ [EditModal] ${fieldName} 코드 할당 오류:`, allocateError); + if (!dataToSave[fieldName] || dataToSave[fieldName] === "") { + hasAllocationFailure = true; + failedFields.push(fieldName); + } + } + } + + // 채번 규칙 할당 실패 시 저장 중단 + if (hasAllocationFailure) { + const fieldNames = failedFields.join(", "); + toast.error(`채번 규칙 할당에 실패했습니다 (${fieldNames}). 화면 설정에서 채번 규칙을 확인해주세요.`); + console.error(`❌ [EditModal] 채번 규칙 할당 실패로 저장 중단. 실패 필드: ${fieldNames}`); + return; + } + + // _numberingRuleId 필드 제거 (실제 저장하지 않음) + for (const key of Object.keys(dataToSave)) { + if (key.endsWith("_numberingRuleId")) { + delete dataToSave[key]; + } + } + } + + console.log("[EditModal] 최종 저장 데이터:", dataToSave); + const response = await dynamicFormApi.saveFormData({ screenId: modalState.screenId!, tableName: screenData.screenInfo.tableName, - data: formData, + data: dataToSave, }); if (response.success) { diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx index 826ec1db..7c34192a 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx @@ -1,218 +1,23 @@ "use client"; -/** - * PivotGrid 렌더러 - * 화면 관리 시스템에서 PivotGrid를 렌더링하는 컴포넌트 - */ - -import React, { useState, useEffect, useMemo } from "react"; -import { ComponentRegistry } from "../../ComponentRegistry"; +import React from "react"; +import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; import { PivotGridComponent } from "./PivotGridComponent"; import { PivotGridConfigPanel } from "./PivotGridConfigPanel"; -import { - PivotGridComponentConfig, - PivotFieldConfig, - PivotCellData, -} from "./types"; -import { apiClient } from "@/lib/api/client"; -// ==================== 타입 ==================== - -interface PivotGridRendererProps { - // 위젯 ID - id?: string; - - // 컴포넌트 설정 - config?: PivotGridComponentConfig; - - // 외부 데이터 (formData 등에서 주입) - data?: Record[]; - - // 화면 관리 컨텍스트 - formData?: Record; - - // 이벤트 핸들러 - onCellClick?: (cellData: PivotCellData) => void; - onDataLoad?: (data: Record[]) => void; - - // 제어관리 연동 - buttonControlOptions?: { - buttonId?: string; - actionType?: string; - }; - - // 자동 필터 (멀티테넌시) - autoFilter?: { - companyCode?: string; - }; -} - -// ==================== 메인 컴포넌트 ==================== - -export const PivotGridRenderer: React.FC = ({ - id, - config, - data: externalData, - formData, - onCellClick, - onDataLoad, - buttonControlOptions, - autoFilter, -}) => { - const [data, setData] = useState[]>([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - // 데이터 로드 - useEffect(() => { - const loadData = async () => { - // 외부 데이터가 있으면 사용 - if (externalData && externalData.length > 0) { - setData(externalData); - onDataLoad?.(externalData); - return; - } - - // 데이터 소스 설정 확인 - if (!config?.dataSource?.tableName) { - setData([]); - return; - } - - setLoading(true); - setError(null); - - try { - // 테이블 데이터 조회 - const params: any = { - tableName: config.dataSource.tableName, - }; - - // 멀티테넌시 필터 적용 - if (autoFilter?.companyCode) { - params.companyCode = autoFilter.companyCode; - } - - // 필터 조건 적용 - if (config.dataSource.filterConditions) { - const filters: Record = {}; - config.dataSource.filterConditions.forEach((cond) => { - if (cond.valueFromField && formData) { - filters[cond.field] = formData[cond.valueFromField]; - } else if (cond.value !== undefined) { - filters[cond.field] = cond.value; - } - }); - params.filters = JSON.stringify(filters); - } - - const response = await apiClient.get( - `/api/table-management/data/${config.dataSource.tableName}`, - { params } - ); - - if (response.data.success) { - const loadedData = response.data.data || []; - setData(loadedData); - onDataLoad?.(loadedData); - } else { - throw new Error(response.data.message || "데이터 로드 실패"); - } - } catch (err: any) { - console.error("PivotGrid 데이터 로드 실패:", err); - setError(err.message || "데이터를 불러오는데 실패했습니다"); - setData([]); - } finally { - setLoading(false); - } - }; - - loadData(); - }, [ - config?.dataSource?.tableName, - config?.dataSource?.filterConditions, - externalData, - formData, - autoFilter?.companyCode, - onDataLoad, - ]); - - // 필드 설정에서 formData 값 적용 - const processedFields = useMemo(() => { - if (!config?.fields) return []; - - return config.fields.map((field) => { - // 필터 값에 formData 적용 - if (field.filterValues && formData) { - return { - ...field, - filterValues: field.filterValues.map((v) => { - if (typeof v === "string" && v.startsWith("{{") && v.endsWith("}}")) { - const key = v.slice(2, -2).trim(); - return formData[key] ?? v; - } - return v; - }), - }; - } - return field; - }); - }, [config?.fields, formData]); - - // 로딩 상태 - if (loading) { - return ( -
-
-
- 데이터 로딩 중... -
-
- ); - } - - // 에러 상태 - if (error) { - return ( -
-
-

데이터 로드 실패

-

{error}

-
-
- ); - } - - return ( - - ); -}; - -// ==================== 컴포넌트 등록 ==================== - -ComponentRegistry.register({ - type: "pivot-grid", - label: "피벗 그리드", - category: "data", - icon: "BarChart3", +/** + * PivotGrid 컴포넌트 정의 + */ +const PivotGridDefinition = createComponentDefinition({ + id: "pivot-grid", + name: "피벗 그리드", + nameEng: "PivotGrid Component", description: "다차원 데이터 분석을 위한 피벗 테이블 컴포넌트", + category: ComponentCategory.DISPLAY, + webType: "text", + component: PivotGridComponent, defaultConfig: { dataSource: { type: "table", @@ -239,8 +44,41 @@ ComponentRegistry.register({ }, height: "400px", }, - Renderer: PivotGridRenderer, - ConfigPanel: PivotGridConfigPanel, + defaultSize: { width: 800, height: 500 }, + configPanel: PivotGridConfigPanel, + icon: "BarChart3", + tags: ["피벗", "분석", "집계", "그리드", "데이터"], + version: "1.0.0", + author: "개발팀", + documentation: "", }); -export default PivotGridRenderer; +/** + * PivotGrid 렌더러 + * 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록 + */ +export class PivotGridRenderer extends AutoRegisteringComponentRenderer { + static componentDefinition = PivotGridDefinition; + + render(): React.ReactElement { + return ( + + ); + } +} + +// 자동 등록 실행 +PivotGridRenderer.registerSelf(); + +// 강제 등록 (디버깅용) +if (typeof window !== "undefined") { + setTimeout(() => { + try { + PivotGridRenderer.registerSelf(); + } catch (error) { + console.error("❌ PivotGrid 강제 등록 실패:", error); + } + }, 1000); +} diff --git a/frontend/lib/registry/components/pivot-grid/index.ts b/frontend/lib/registry/components/pivot-grid/index.ts index 821815bf..16044dbc 100644 --- a/frontend/lib/registry/components/pivot-grid/index.ts +++ b/frontend/lib/registry/components/pivot-grid/index.ts @@ -3,12 +3,7 @@ * 다차원 데이터 분석을 위한 피벗 테이블 */ -// 메인 컴포넌트 -export { PivotGridComponent } from "./PivotGridComponent"; -export { PivotGridRenderer } from "./PivotGridRenderer"; -export { PivotGridConfigPanel } from "./PivotGridConfigPanel"; - -// 타입 +// 타입 내보내기 export type { // 기본 타입 PivotAreaType, @@ -45,6 +40,10 @@ export type { PivotGridComponentConfig, } from "./types"; +// 컴포넌트 내보내기 +export { PivotGridComponent } from "./PivotGridComponent"; +export { PivotGridConfigPanel } from "./PivotGridConfigPanel"; + // 유틸리티 export { aggregate, @@ -60,4 +59,3 @@ export { } from "./utils/aggregation"; export { processPivotData, pathToKey, keyToPath } from "./utils/pivotEngine"; - diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 8ffa8afe..a101efaf 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -104,6 +104,20 @@ export const TextInputComponent: React.FC = ({ const currentFormValue = formData?.[component.columnName]; const currentComponentValue = component.value; + // 🆕 채번 규칙이 설정되어 있으면 항상 _numberingRuleId를 formData에 설정 + // (값 생성 성공 여부와 관계없이, 저장 시점에 allocateCode를 호출하기 위함) + if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) { + const ruleId = testAutoGeneration.options.numberingRuleId; + if (ruleId && ruleId !== "undefined" && ruleId !== "null" && ruleId !== "") { + const ruleIdKey = `${component.columnName}_numberingRuleId`; + // formData에 아직 설정되지 않은 경우에만 설정 + if (isInteractive && onFormDataChange && !formData?.[ruleIdKey]) { + onFormDataChange(ruleIdKey, ruleId); + console.log("📝 채번 규칙 ID 사전 설정:", ruleIdKey, ruleId); + } + } + } + // 자동생성된 값이 없고, 현재 값도 없을 때만 생성 if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) { isGeneratingRef.current = true; // 생성 시작 플래그 @@ -144,13 +158,6 @@ export const TextInputComponent: React.FC = ({ if (isInteractive && onFormDataChange && component.columnName) { console.log("📝 formData 업데이트:", component.columnName, generatedValue); onFormDataChange(component.columnName, generatedValue); - - // 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함) - if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) { - const ruleIdKey = `${component.columnName}_numberingRuleId`; - onFormDataChange(ruleIdKey, testAutoGeneration.options.numberingRuleId); - console.log("📝 채번 규칙 ID 저장:", ruleIdKey, testAutoGeneration.options.numberingRuleId); - } } } } else if (!autoGeneratedValue && testAutoGeneration.type !== "none") { diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 4c9d638b..a1f53285 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -801,6 +801,9 @@ export class ButtonActionExecutor { console.log("🎯 채번 규칙 할당 시작 (allocateCode 호출)"); const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); + let hasAllocationFailure = false; + const failedFields: string[] = []; + for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { console.log(`🔄 ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`); @@ -811,13 +814,31 @@ export class ButtonActionExecutor { console.log(`✅ ${fieldName} 새 코드 할당: ${formData[fieldName]} → ${newCode}`); formData[fieldName] = newCode; } else { - console.warn(`⚠️ ${fieldName} 코드 할당 실패, 기존 값 유지:`, allocateResult.error); + console.warn(`⚠️ ${fieldName} 코드 할당 실패:`, allocateResult.error); + // 🆕 기존 값이 빈 문자열이면 실패로 표시 + if (!formData[fieldName] || formData[fieldName] === "") { + hasAllocationFailure = true; + failedFields.push(fieldName); + } } } catch (allocateError) { console.error(`❌ ${fieldName} 코드 할당 오류:`, allocateError); - // 오류 시 기존 값 유지 + // 🆕 기존 값이 빈 문자열이면 실패로 표시 + if (!formData[fieldName] || formData[fieldName] === "") { + hasAllocationFailure = true; + failedFields.push(fieldName); + } } } + + // 🆕 채번 규칙 할당 실패 시 저장 중단 + if (hasAllocationFailure) { + const fieldNames = failedFields.join(", "); + toast.error(`채번 규칙 할당에 실패했습니다 (${fieldNames}). 화면 설정에서 채번 규칙을 확인해주세요.`); + console.error(`❌ 채번 규칙 할당 실패로 저장 중단. 실패 필드: ${fieldNames}`); + console.error("💡 해결 방법: 화면관리에서 해당 필드의 채번 규칙 설정을 확인하세요."); + return false; + } } console.log("✅ 채번 규칙 할당 완료"); @@ -3039,6 +3060,7 @@ export class ButtonActionExecutor { config: ButtonActionConfig, rowData: any, context: ButtonActionContext, + isCreateMode: boolean = false, // 🆕 복사 모드에서 true로 전달 ): Promise { const { groupByColumns = [] } = config; @@ -3112,10 +3134,11 @@ export class ButtonActionExecutor { const modalEvent = new CustomEvent("openEditModal", { detail: { screenId: config.targetScreenId, - title: config.editModalTitle || "데이터 수정", + title: isCreateMode ? (config.editModalTitle || "데이터 복사") : (config.editModalTitle || "데이터 수정"), description: description, modalSize: config.modalSize || "lg", editData: rowData, + isCreateMode: isCreateMode, // 🆕 복사 모드에서 INSERT로 처리되도록 groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달 tableName: context.tableName, // 🆕 테이블명 전달 buttonConfig: config, // 🆕 버튼 설정 전달 (제어로직 실행용) @@ -3230,23 +3253,61 @@ export class ButtonActionExecutor { "code", ]; + // 🆕 화면 설정에서 채번 규칙 가져오기 + let screenNumberingRules: Record = {}; + if (config.targetScreenId) { + try { + const { screenApi } = await import("@/lib/api/screen"); + const layout = await screenApi.getLayout(config.targetScreenId); + + // 레이아웃에서 채번 규칙이 설정된 컴포넌트 찾기 + const findNumberingRules = (components: any[]): void => { + for (const comp of components) { + const compConfig = comp.componentConfig || {}; + // text-input 컴포넌트의 채번 규칙 확인 + if (compConfig.autoGeneration?.type === "numbering_rule" && compConfig.autoGeneration?.options?.numberingRuleId) { + const columnName = compConfig.columnName || comp.columnName; + if (columnName) { + screenNumberingRules[columnName] = compConfig.autoGeneration.options.numberingRuleId; + console.log(`📋 화면 설정에서 채번 규칙 발견: ${columnName} → ${compConfig.autoGeneration.options.numberingRuleId}`); + } + } + // 중첩된 컴포넌트 확인 + if (comp.children && Array.isArray(comp.children)) { + findNumberingRules(comp.children); + } + } + }; + + if (layout?.components) { + findNumberingRules(layout.components); + } + console.log("📋 화면 설정에서 찾은 채번 규칙:", screenNumberingRules); + } catch (error) { + console.warn("⚠️ 화면 레이아웃 조회 실패:", error); + } + } + // 품목코드 필드를 찾아서 무조건 공백으로 초기화 let resetFieldName = ""; for (const field of itemCodeFields) { if (copiedData[field] !== undefined) { const originalValue = copiedData[field]; const ruleIdKey = `${field}_numberingRuleId`; - const hasNumberingRule = - rowData[ruleIdKey] !== undefined && rowData[ruleIdKey] !== null && rowData[ruleIdKey] !== ""; + + // 1순위: 원본 데이터에서 채번 규칙 ID 확인 + // 2순위: 화면 설정에서 채번 규칙 ID 확인 + const numberingRuleId = rowData[ruleIdKey] || screenNumberingRules[field]; + const hasNumberingRule = numberingRuleId !== undefined && numberingRuleId !== null && numberingRuleId !== ""; // 품목코드를 무조건 공백으로 초기화 copiedData[field] = ""; // 채번 규칙 ID가 있으면 복사 (저장 시 자동 생성) if (hasNumberingRule) { - copiedData[ruleIdKey] = rowData[ruleIdKey]; + copiedData[ruleIdKey] = numberingRuleId; console.log(`✅ 품목코드 초기화 (채번 규칙 있음): ${field} (기존값: ${originalValue})`); - console.log(`📋 채번 규칙 ID 복사: ${ruleIdKey} = ${rowData[ruleIdKey]}`); + console.log(`📋 채번 규칙 ID 설정: ${ruleIdKey} = ${numberingRuleId}`); } else { console.log(`✅ 품목코드 초기화 (수동 입력 필요): ${field} (기존값: ${originalValue})`); } @@ -3303,9 +3364,9 @@ export class ButtonActionExecutor { switch (editMode) { case "modal": - // 모달로 복사 폼 열기 (편집 모달 재사용) - console.log("📋 모달로 복사 폼 열기"); - await this.openEditModal(config, rowData, context); + // 모달로 복사 폼 열기 (편집 모달 재사용, INSERT 모드로) + console.log("📋 모달로 복사 폼 열기 (INSERT 모드)"); + await this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true break; case "navigate": @@ -3316,8 +3377,8 @@ export class ButtonActionExecutor { default: // 기본값: 모달 - console.log("📋 기본 모달로 복사 폼 열기"); - this.openEditModal(config, rowData, context); + console.log("📋 기본 모달로 복사 폼 열기 (INSERT 모드)"); + this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true } } catch (error: any) { console.error("❌ openCopyForm 실행 중 오류:", error);