From b74cb94191bb358795daee6e44ef462e53e27558 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 19 Nov 2025 10:03:38 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B3=B5=EC=82=AC?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/screenManagementService.ts | 189 ++++++-- .../CalculationBuilder.tsx | 429 ++++++++++++++++++ .../SelectedItemsDetailInputComponent.tsx | 40 +- .../SelectedItemsDetailInputConfigPanel.tsx | 338 +++++++++----- .../selected-items-detail-input/types.ts | 60 ++- frontend/lib/utils/buttonActions.ts | 11 + 6 files changed, 887 insertions(+), 180 deletions(-) create mode 100644 frontend/lib/registry/components/selected-items-detail-input/CalculationBuilder.tsx diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index c2036cbd..daf0ea26 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -2101,55 +2101,109 @@ export class ScreenManagementService { } /** - * 화면에 연결된 모달 화면들을 자동 감지 - * 버튼 컴포넌트의 popup 액션에서 targetScreenId를 추출 + * 화면에 연결된 모달/화면들을 재귀적으로 자동 감지 + * - 버튼 컴포넌트: popup/modal/edit/openModalWithData 액션의 targetScreenId + * - 조건부 컨테이너: sections[].screenId (조건별 화면 할당) + * - 중첩된 화면들도 모두 감지 (재귀) */ async detectLinkedModalScreens( screenId: number ): Promise<{ screenId: number; screenName: string; screenCode: string }[]> { - // 화면의 모든 레이아웃 조회 - const layouts = await query( - `SELECT layout_id, properties - FROM screen_layouts - WHERE screen_id = $1 - AND component_type = 'component' - AND properties IS NOT NULL`, - [screenId] - ); + console.log(`\n🔍 [재귀 감지 시작] 화면 ID: ${screenId}`); + + const allLinkedScreenIds = new Set(); + const visited = new Set(); // 무한 루프 방지 + const queue: number[] = [screenId]; // BFS 큐 - const linkedScreenIds = new Set(); + // BFS로 연결된 모든 화면 탐색 + while (queue.length > 0) { + const currentScreenId = queue.shift()!; + + // 이미 방문한 화면은 스킵 (순환 참조 방지) + if (visited.has(currentScreenId)) { + console.log(`⏭️ 이미 방문한 화면 스킵: ${currentScreenId}`); + continue; + } + + visited.add(currentScreenId); + console.log(`\n📋 현재 탐색 중인 화면: ${currentScreenId} (깊이: ${visited.size})`); - // 각 레이아웃에서 버튼의 popup/modal/edit 액션 확인 - for (const layout of layouts) { - try { - const properties = layout.properties; - - // 버튼 컴포넌트인지 확인 - if (properties?.componentType === "button" || properties?.componentType?.startsWith("button-")) { - const action = properties?.componentConfig?.action; + // 현재 화면의 모든 레이아웃 조회 + const layouts = await query( + `SELECT layout_id, properties + FROM screen_layouts + WHERE screen_id = $1 + AND component_type = 'component' + AND properties IS NOT NULL`, + [currentScreenId] + ); + + console.log(` 📦 레이아웃 개수: ${layouts.length}`); + + // 각 레이아웃에서 연결된 화면 ID 확인 + for (const layout of layouts) { + try { + const properties = layout.properties; - // popup, modal, edit 액션이고 targetScreenId가 있는 경우 - // edit 액션도 수정 폼 모달을 열기 때문에 포함 - if ((action?.type === "popup" || action?.type === "modal" || action?.type === "edit") && action?.targetScreenId) { - const targetScreenId = parseInt(action.targetScreenId); - if (!isNaN(targetScreenId)) { - linkedScreenIds.add(targetScreenId); - console.log(`🔗 연결된 모달 화면 발견: screenId=${targetScreenId}, actionType=${action.type} (레이아웃 ${layout.layout_id})`); + // 1. 버튼 컴포넌트의 액션 확인 + if (properties?.componentType === "button" || properties?.componentType?.startsWith("button-")) { + const action = properties?.componentConfig?.action; + + const modalActionTypes = ["popup", "modal", "edit", "openModalWithData"]; + if (modalActionTypes.includes(action?.type) && action?.targetScreenId) { + const targetScreenId = parseInt(action.targetScreenId); + if (!isNaN(targetScreenId) && targetScreenId !== currentScreenId) { + // 메인 화면이 아닌 경우에만 추가 + if (targetScreenId !== screenId) { + allLinkedScreenIds.add(targetScreenId); + } + // 아직 방문하지 않은 화면이면 큐에 추가 + if (!visited.has(targetScreenId)) { + queue.push(targetScreenId); + console.log(` 🔗 [버튼] 연결된 화면 발견: ${targetScreenId} (action: ${action.type}) → 큐에 추가`); + } + } } } + + // 2. conditional-container 컴포넌트의 sections 확인 + if (properties?.componentType === "conditional-container") { + const sections = properties?.componentConfig?.sections || []; + + for (const section of sections) { + if (section?.screenId) { + const sectionScreenId = parseInt(section.screenId); + if (!isNaN(sectionScreenId) && sectionScreenId !== currentScreenId) { + // 메인 화면이 아닌 경우에만 추가 + if (sectionScreenId !== screenId) { + allLinkedScreenIds.add(sectionScreenId); + } + // 아직 방문하지 않은 화면이면 큐에 추가 + if (!visited.has(sectionScreenId)) { + queue.push(sectionScreenId); + console.log(` 🔗 [조건부컨테이너] 연결된 화면 발견: ${sectionScreenId} (condition: ${section.condition}) → 큐에 추가`); + } + } + } + } + } + } catch (error) { + console.warn(` ⚠️ 레이아웃 ${layout.layout_id} 파싱 오류:`, error); } - } catch (error) { - // JSON 파싱 오류 등은 무시하고 계속 진행 - console.warn(`레이아웃 ${layout.layout_id} 파싱 오류:`, error); } } + console.log(`\n✅ [재귀 감지 완료] 총 방문한 화면: ${visited.size}개, 연결된 화면: ${allLinkedScreenIds.size}개`); + console.log(` 방문한 화면 ID: [${Array.from(visited).join(", ")}]`); + console.log(` 연결된 화면 ID: [${Array.from(allLinkedScreenIds).join(", ")}]`); + // 감지된 화면 ID들의 정보 조회 - if (linkedScreenIds.size === 0) { + if (allLinkedScreenIds.size === 0) { + console.log(`ℹ️ 연결된 화면이 없습니다.`); return []; } - const screenIds = Array.from(linkedScreenIds); + const screenIds = Array.from(allLinkedScreenIds); const placeholders = screenIds.map((_, i) => `$${i + 1}`).join(", "); const linkedScreens = await query( @@ -2161,6 +2215,11 @@ export class ScreenManagementService { screenIds ); + console.log(`\n📋 최종 감지된 화면 목록:`); + linkedScreens.forEach((s: any) => { + console.log(` - ${s.screen_name} (ID: ${s.screen_id}, 코드: ${s.screen_code})`); + }); + return linkedScreens.map((s) => ({ screenId: s.screen_id, screenName: s.screen_name, @@ -2430,23 +2489,23 @@ export class ScreenManagementService { for (const layout of layouts) { try { const properties = layout.properties; + let needsUpdate = false; - // 버튼 컴포넌트인지 확인 + // 1. 버튼 컴포넌트의 targetScreenId 업데이트 if ( properties?.componentType === "button" || properties?.componentType?.startsWith("button-") ) { const action = properties?.componentConfig?.action; - // targetScreenId가 있는 액션 (popup, modal, edit) + // targetScreenId가 있는 액션 (popup, modal, edit, openModalWithData) + const modalActionTypes = ["popup", "modal", "edit", "openModalWithData"]; if ( - (action?.type === "popup" || - action?.type === "modal" || - action?.type === "edit") && + modalActionTypes.includes(action?.type) && action?.targetScreenId ) { const oldScreenId = parseInt(action.targetScreenId); - console.log(`🔍 버튼 발견: layout ${layout.layout_id}, action=${action.type}, targetScreenId=${oldScreenId}`); + console.log(`🔍 [버튼] 발견: layout ${layout.layout_id}, action=${action.type}, targetScreenId=${oldScreenId}`); // 매핑에 있으면 업데이트 if (screenIdMapping.has(oldScreenId)) { @@ -2456,31 +2515,63 @@ export class ScreenManagementService { // properties 업데이트 properties.componentConfig.action.targetScreenId = newScreenId.toString(); + needsUpdate = true; - // 데이터베이스 업데이트 - await query( - `UPDATE screen_layouts - SET properties = $1 - WHERE layout_id = $2`, - [JSON.stringify(properties), layout.layout_id] - ); - - updateCount++; console.log( - `🔗 버튼 targetScreenId 업데이트: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id})` + `🔗 [버튼] targetScreenId 업데이트 준비: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id})` ); } else { console.log(`⚠️ 매핑 없음: ${oldScreenId} (업데이트 건너뜀)`); } } } + + // 2. conditional-container 컴포넌트의 sections[].screenId 업데이트 + if (properties?.componentType === "conditional-container") { + const sections = properties?.componentConfig?.sections || []; + + for (const section of sections) { + if (section?.screenId) { + const oldScreenId = parseInt(section.screenId); + console.log(`🔍 [조건부컨테이너] section 발견: layout ${layout.layout_id}, condition=${section.condition}, screenId=${oldScreenId}`); + + // 매핑에 있으면 업데이트 + if (screenIdMapping.has(oldScreenId)) { + const newScreenId = screenIdMapping.get(oldScreenId)!; + console.log(`✅ 매핑 발견: ${oldScreenId} → ${newScreenId}`); + + // section.screenId 업데이트 + section.screenId = newScreenId; + needsUpdate = true; + + console.log( + `🔗 [조건부컨테이너] screenId 업데이트 준비: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id}, condition=${section.condition})` + ); + } else { + console.log(`⚠️ 매핑 없음: ${oldScreenId} (업데이트 건너뜀)`); + } + } + } + } + + // 3. 업데이트가 필요한 경우 DB 저장 + if (needsUpdate) { + await query( + `UPDATE screen_layouts + SET properties = $1 + WHERE layout_id = $2`, + [JSON.stringify(properties), layout.layout_id] + ); + updateCount++; + console.log(`💾 레이아웃 ${layout.layout_id} 업데이트 완료`); + } } catch (error) { console.warn(`❌ 레이아웃 ${layout.layout_id} 업데이트 오류:`, error); // 개별 레이아웃 오류는 무시하고 계속 진행 } } - console.log(`✅ 총 ${updateCount}개 버튼의 targetScreenId 업데이트 완료`); + console.log(`✅ 총 ${updateCount}개 레이아웃의 연결된 화면 ID 업데이트 완료 (버튼 + 조건부컨테이너)`); return updateCount; } } diff --git a/frontend/lib/registry/components/selected-items-detail-input/CalculationBuilder.tsx b/frontend/lib/registry/components/selected-items-detail-input/CalculationBuilder.tsx new file mode 100644 index 00000000..92ac2e42 --- /dev/null +++ b/frontend/lib/registry/components/selected-items-detail-input/CalculationBuilder.tsx @@ -0,0 +1,429 @@ +"use client"; + +import React, { useState } from "react"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Plus, X, Calculator } from "lucide-react"; +import { CalculationNode, CalculationStep, AdditionalFieldDefinition } from "./types"; + +interface CalculationBuilderProps { + steps: CalculationStep[]; + availableFields: AdditionalFieldDefinition[]; + onChange: (steps: CalculationStep[]) => void; +} + +export const CalculationBuilder: React.FC = ({ + steps, + availableFields, + onChange, +}) => { + const [previewValues, setPreviewValues] = useState>({}); + + // 새 단계 추가 + const addStep = () => { + const newStep: CalculationStep = { + id: `step_${Date.now()}`, + label: `단계 ${steps.length + 1}`, + expression: { + type: "field", + fieldName: "", + }, + }; + onChange([...steps, newStep]); + }; + + // 단계 삭제 + const removeStep = (stepId: string) => { + onChange(steps.filter((s) => s.id !== stepId)); + }; + + // 단계 업데이트 + const updateStep = (stepId: string, updates: Partial) => { + onChange( + steps.map((s) => (s.id === stepId ? { ...s, ...updates } : s)) + ); + }; + + // 간단한 표현식 렌더링 + const renderSimpleExpression = (step: CalculationStep) => { + return ( +
+
+ {/* 왼쪽 항 */} + + + {step.expression.type === "constant" && ( + { + updateStep(step.id, { + expression: { + ...step.expression, + value: parseFloat(e.target.value) || 0, + }, + }); + }} + className="h-8 w-24 text-xs" + placeholder="값" + /> + )} +
+ + {/* 연산 추가 버튼 */} + {step.expression.type !== "operation" && ( + + )} + + {/* 연산식 */} + {step.expression.type === "operation" && ( +
+ {renderOperationExpression(step)} +
+ )} +
+ ); + }; + + // 연산식 렌더링 + const renderOperationExpression = (step: CalculationStep) => { + if (step.expression.type !== "operation") return null; + + return ( +
+ {/* 왼쪽 항 */} +
+ {renderNodeLabel(step.expression.left)} +
+ + {/* 연산자 */} + + + {/* 오른쪽 항 */} + + + {step.expression.right?.type === "constant" && ( + { + updateStep(step.id, { + expression: { + ...step.expression, + right: { + ...step.expression.right!, + value: parseFloat(e.target.value) || 0, + }, + }, + }); + }} + className="h-7 w-24 text-xs" + placeholder="값" + /> + )} +
+ ); + }; + + // 노드 라벨 표시 + const renderNodeLabel = (node?: CalculationNode): string => { + if (!node) return ""; + + switch (node.type) { + case "field": + const field = availableFields.find((f) => f.name === node.fieldName); + return field?.label || node.fieldName || "필드"; + case "constant": + return String(node.value || 0); + case "previous": + return "이전 결과"; + case "operation": + const left = renderNodeLabel(node.left); + const right = renderNodeLabel(node.right); + const op = node.operator === "*" ? "×" : node.operator === "/" ? "÷" : node.operator; + return `(${left} ${op} ${right})`; + default: + return ""; + } + }; + + // 함수 적용 UI + const renderFunctionStep = (step: CalculationStep) => { + if (step.expression.type !== "function") return null; + + return ( +
+ + + {(step.expression.functionName === "round" || + step.expression.functionName === "floor" || + step.expression.functionName === "ceil") && ( + <> + 단위: + + + )} +
+ ); + }; + + return ( +
+
+ + +
+ + {steps.length === 0 ? ( + + + +

+ 계산 단계를 추가하여 계산식을 만드세요 +

+
+
+ ) : ( +
+ {steps.map((step, idx) => ( + + +
+ + {step.label || `단계 ${idx + 1}`} + +
+ updateStep(step.id, { label: e.target.value })} + placeholder={`단계 ${idx + 1}`} + className="h-6 w-24 text-xs" + /> + +
+
+
+ + {step.expression.type === "function" + ? renderFunctionStep(step) + : renderSimpleExpression(step)} + + {/* 함수 적용 버튼 */} + {step.expression.type !== "function" && ( + + )} + +
+ ))} +
+ )} + + {/* 미리보기 */} + {steps.length > 0 && ( + + +
+ 계산식: +
+ {steps.map((step, idx) => ( +
+ {idx + 1}. {renderNodeLabel(step.expression)} +
+ ))} +
+
+
+
+ )} +
+ ); +}; + diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index 904fc4be..04403872 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -258,10 +258,32 @@ export const SelectedItemsDetailInputComponent: React.FC { const handleSaveRequest = () => { + // component.id를 문자열로 안전하게 변환 + const componentKey = String(component.id || "selected_items"); + + console.log("🔔 [SelectedItemsDetailInput] beforeFormSave 이벤트 수신!", { + itemsCount: items.length, + hasOnFormDataChange: !!onFormDataChange, + componentId: component.id, + componentIdType: typeof component.id, + componentKey, + }); + if (items.length > 0 && onFormDataChange) { - const dataToSave = { [component.id || "selected_items"]: items }; - console.log("📝 [SelectedItemsDetailInput] 저장 요청 시 데이터 전달:", dataToSave); + const dataToSave = { [componentKey]: items }; + console.log("📝 [SelectedItemsDetailInput] 저장 요청 시 데이터 전달:", { + key: componentKey, + itemsCount: items.length, + fullData: dataToSave, + firstItem: items[0], + }); onFormDataChange(dataToSave); + } else { + console.warn("⚠️ [SelectedItemsDetailInput] 저장 데이터 전달 실패:", { + hasItems: items.length > 0, + hasCallback: !!onFormDataChange, + itemsLength: items.length, + }); } }; @@ -342,6 +364,14 @@ export const SelectedItemsDetailInputComponent: React.FC { + console.log("📝 [handleFieldChange] 필드 값 변경:", { + itemId, + groupId, + entryId, + fieldName, + value, + }); + setItems((prevItems) => { return prevItems.map((item) => { if (item.id !== itemId) return item; @@ -357,6 +387,12 @@ export const SelectedItemsDetailInputComponent: React.FC
- { - if (checked) { - handleChange("autoCalculation", { - targetField: "calculated_price", - inputFields: { - basePrice: "current_unit_price", - discountType: "discount_type", - discountValue: "discount_value", - roundingType: "rounding_type", - roundingUnit: "rounding_unit_value", - }, - calculationType: "price", - }); - } else { - handleChange("autoCalculation", undefined); - } - }} - /> + { + if (checked) { + handleChange("autoCalculation", { + targetField: "", + mode: "template", + inputFields: { + basePrice: "", + discountType: "", + discountValue: "", + roundingType: "", + roundingUnit: "", + }, + calculationType: "price", + valueMapping: {}, + calculationSteps: [], + }); + } else { + handleChange("autoCalculation", undefined); + } + }} + />
{config.autoCalculation && (
+ {/* 계산 모드 선택 */}
- - handleChange("autoCalculation", { - ...config.autoCalculation, - targetField: e.target.value, - })} - placeholder="calculated_price" - className="h-7 text-xs" - /> + +
-
- - handleChange("autoCalculation", { - ...config.autoCalculation, - inputFields: { - ...config.autoCalculation.inputFields, - basePrice: e.target.value, - }, - })} - placeholder="current_unit_price" - className="h-7 text-xs" - /> -
+ {/* 템플릿 모드 */} + {config.autoCalculation.mode === "template" && ( + <> + {/* 계산 필드 선택 */} +
+ + +
+ {/* 계산 결과 필드 */} +
+ + +
-
-
- - handleChange("autoCalculation", { - ...config.autoCalculation, - inputFields: { - ...config.autoCalculation.inputFields, - discountType: e.target.value, - }, - })} - placeholder="discount_type" - className="h-7 text-xs" - /> -
+ {/* 기준 단가 필드 */} +
+ + +
-
- - handleChange("autoCalculation", { - ...config.autoCalculation, - inputFields: { - ...config.autoCalculation.inputFields, - discountValue: e.target.value, - }, - })} - placeholder="discount_value" - className="h-7 text-xs" - /> +
+ {/* 할인 방식 필드 */} +
+ + +
+ + {/* 할인값 필드 */} +
+ + +
+
+ +
+ {/* 반올림 방식 필드 */} +
+ + +
+ + {/* 반올림 단위 필드 */} +
+ + +
+
-
-
- - handleChange("autoCalculation", { - ...config.autoCalculation, - inputFields: { - ...config.autoCalculation.inputFields, - roundingType: e.target.value, - }, - })} - placeholder="rounding_type" - className="h-7 text-xs" - /> -
- -
- - handleChange("autoCalculation", { - ...config.autoCalculation, - inputFields: { - ...config.autoCalculation.inputFields, - roundingUnit: e.target.value, - }, - })} - placeholder="rounding_unit_value" - className="h-7 text-xs" - /> -
-
- -

- 💡 위 필드명들이 추가 입력 필드에 있어야 자동 계산이 작동합니다 -

- {/* 카테고리 값 매핑 */}
@@ -1591,6 +1683,24 @@ export const SelectedItemsDetailInputConfigPanel: React.FC
+ + )} + + {/* 커스텀 모드 (계산식 빌더) */} + {config.autoCalculation.mode === "custom" && ( +
+ { + handleChange("autoCalculation", { + ...config.autoCalculation, + calculationSteps: steps, + }); + }} + /> +
+ )}
)}
diff --git a/frontend/lib/registry/components/selected-items-detail-input/types.ts b/frontend/lib/registry/components/selected-items-detail-input/types.ts index 0e6120c6..4fd0ba10 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/types.ts +++ b/frontend/lib/registry/components/selected-items-detail-input/types.ts @@ -58,37 +58,67 @@ export interface FieldGroup { displayItems?: DisplayItem[]; } +/** + * 🆕 계산식 노드 타입 + */ +export type CalculationNodeType = "field" | "constant" | "operation" | "function" | "previous"; + +export interface CalculationNode { + type: CalculationNodeType; + // field: 필드명 + fieldName?: string; + // constant: 상수값 + value?: number; + // operation: 연산 + operator?: "+" | "-" | "*" | "/" | "%" | "^"; + left?: CalculationNode; + right?: CalculationNode; + // function: 함수 + functionName?: "round" | "floor" | "ceil" | "abs" | "max" | "min"; + params?: CalculationNode[]; +} + +/** + * 🆕 계산 단계 + */ +export interface CalculationStep { + id: string; + label: string; + expression: CalculationNode; +} + /** * 🆕 자동 계산 설정 */ export interface AutoCalculationConfig { /** 계산 대상 필드명 (예: calculated_price) */ targetField: string; - /** 계산에 사용할 입력 필드들 */ - inputFields: { - basePrice: string; // 기본 단가 필드명 - discountType: string; // 할인 방식 필드명 - discountValue: string; // 할인값 필드명 - roundingType: string; // 반올림 방식 필드명 - roundingUnit: string; // 반올림 단위 필드명 + /** 🆕 계산 방식 */ + mode: "template" | "custom"; + + /** 템플릿 모드 (기존 방식) */ + inputFields?: { + basePrice: string; + discountType: string; + discountValue: string; + roundingType: string; + roundingUnit: string; }; - /** 계산 함수 타입 */ - calculationType: "price" | "custom"; - /** 🆕 카테고리 값 → 연산 매핑 */ + calculationType?: "price" | "custom"; valueMapping?: { - /** 할인 방식 매핑 */ discountType?: { - [valueCode: string]: "none" | "rate" | "amount"; // 예: { "CATEGORY_544740": "rate" } + [valueCode: string]: "none" | "rate" | "amount"; }; - /** 반올림 방식 매핑 */ roundingType?: { [valueCode: string]: "none" | "round" | "floor" | "ceil"; }; - /** 반올림 단위 매핑 (숫자로 변환) */ roundingUnit?: { - [valueCode: string]: number; // 예: { "10": 10, "100": 100 } + [valueCode: string]: number; }; }; + + /** 커스텀 모드 (계산식 빌더) */ + calculationSteps?: CalculationStep[]; } /** diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 615aedf9..84cc3626 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -230,6 +230,16 @@ export class ButtonActionExecutor { const selectedItemsKeys = Object.keys(context.formData).filter(key => { const value = context.formData[key]; + console.log(`🔍 [handleSave] 필터링 체크 - ${key}:`, { + isArray: Array.isArray(value), + length: Array.isArray(value) ? value.length : 0, + firstItem: Array.isArray(value) && value.length > 0 ? { + keys: Object.keys(value[0] || {}), + hasOriginalData: !!value[0]?.originalData, + hasFieldGroups: !!value[0]?.fieldGroups, + actualValue: value[0], + } : null + }); return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups; }); @@ -238,6 +248,7 @@ export class ButtonActionExecutor { return await this.handleBatchSave(config, context, selectedItemsKeys); } else { console.log("⚠️ [handleSave] SelectedItemsDetailInput 데이터 감지 실패 - 일반 저장 진행"); + console.log("⚠️ [handleSave] formData 전체 내용:", context.formData); } // 폼 유효성 검사