+
+
수정 중
- {groupFields.map((field) => (
-
-
- {field.label}
- {field.required && * }
-
- {renderField(field, item.id, group.id, entry.id, entry)}
-
- ))}
+ {/* 🆕 가로 Grid 배치 (2~3열) */}
+
+ {groupFields.map((field) => (
+
+
+ {field.label}
+ {field.required && * }
+
+ {renderField(field, item.id, group.id, entry.id, entry)}
+
+ ))}
+
);
diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx
index a2b2df51..0f8bca42 100644
--- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx
+++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx
@@ -1,8 +1,9 @@
"use client";
-import React, { useState, useMemo } from "react";
+import React, { useState, useMemo, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@@ -14,6 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
+import { getSecondLevelMenus, getCategoryColumns, getCategoryValues } from "@/lib/api/tableCategoryValue";
export interface SelectedItemsDetailInputConfigPanelProps {
config: SelectedItemsDetailInputConfig;
@@ -47,6 +49,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC
(config.fieldGroups || []);
+
// 🆕 그룹별 펼침/접힘 상태
const [expandedGroups, setExpandedGroups] = useState>({});
@@ -61,6 +64,77 @@ export const SelectedItemsDetailInputConfigPanel: React.FC>([]);
+ const [categoryColumns, setCategoryColumns] = useState>>({});
+ const [categoryValues, setCategoryValues] = useState>>({});
+
+ // 2레벨 메뉴 목록 로드
+ useEffect(() => {
+ const loadMenus = async () => {
+ const response = await getSecondLevelMenus();
+ if (response.success && response.data) {
+ setSecondLevelMenus(response.data);
+ }
+ };
+ loadMenus();
+ }, []);
+
+ // 메뉴 선택 시 카테고리 목록 로드
+ const handleMenuSelect = async (menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
+ if (!config.targetTable) {
+ console.warn("⚠️ targetTable이 설정되지 않았습니다");
+ return;
+ }
+
+ console.log("🔍 카테고리 목록 로드 시작", { targetTable: config.targetTable, menuObjid, fieldType });
+
+ const response = await getCategoryColumns(config.targetTable);
+
+ console.log("📥 getCategoryColumns 응답:", response);
+
+ if (response.success && response.data) {
+ console.log("✅ 카테고리 컬럼 데이터:", response.data);
+ setCategoryColumns(prev => ({ ...prev, [fieldType]: response.data }));
+ } else {
+ console.error("❌ 카테고리 컬럼 로드 실패:", response);
+ }
+
+ // valueMapping 업데이트
+ handleChange("autoCalculation", {
+ ...config.autoCalculation,
+ valueMapping: {
+ ...config.autoCalculation.valueMapping,
+ _selectedMenus: {
+ ...(config.autoCalculation.valueMapping as any)?._selectedMenus,
+ [fieldType]: menuObjid,
+ },
+ },
+ });
+ };
+
+ // 카테고리 선택 시 카테고리 값 목록 로드
+ const handleCategorySelect = async (columnName: string, menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
+ if (!config.targetTable) return;
+
+ const response = await getCategoryValues(config.targetTable, columnName, false, menuObjid);
+ if (response.success && response.data) {
+ setCategoryValues(prev => ({ ...prev, [fieldType]: response.data }));
+ }
+
+ // valueMapping 업데이트
+ handleChange("autoCalculation", {
+ ...config.autoCalculation,
+ valueMapping: {
+ ...config.autoCalculation.valueMapping,
+ _selectedCategories: {
+ ...(config.autoCalculation.valueMapping as any)?._selectedCategories,
+ [fieldType]: columnName,
+ },
+ },
+ });
+ };
+
// 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정
React.useEffect(() => {
if (screenTableName && !config.targetTable) {
@@ -568,6 +642,85 @@ export const SelectedItemsDetailInputConfigPanel: React.FC
+ {/* 🆕 원본 데이터 자동 채우기 */}
+
@@ -970,6 +1123,478 @@ export const SelectedItemsDetailInputConfigPanel: 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);
+ }
+ }}
+ />
+
+
+ {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"
+ />
+
+
+
+
+ 할인 방식
+ 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"
+ />
+
+
+
+
+ 💡 위 필드명들이 추가 입력 필드에 있어야 자동 계산이 작동합니다
+
+
+ {/* 카테고리 값 매핑 */}
+
+
카테고리 값 매핑
+
+ {/* 할인 방식 매핑 */}
+
+
+
+ 할인 방식 연산 매핑
+
+
+
+
+ {/* 1단계: 메뉴 선택 */}
+
+ 1단계: 메뉴 선택
+ handleMenuSelect(Number(value), "discountType")}
+ >
+
+
+
+
+ {secondLevelMenus.map((menu) => (
+
+ {menu.parentMenuName} > {menu.menuName}
+
+ ))}
+
+
+
+
+ {/* 2단계: 카테고리 선택 */}
+ {(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType && (
+
+ 2단계: 카테고리 선택
+ handleCategorySelect(
+ value,
+ (config.autoCalculation.valueMapping as any)._selectedMenus.discountType,
+ "discountType"
+ )}
+ >
+
+
+
+
+ {(categoryColumns.discountType || []).map((col: any) => (
+
+ {col.columnLabel || col.columnName}
+
+ ))}
+
+
+
+ )}
+
+ {/* 3단계: 값 매핑 */}
+ {(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && (
+
+
3단계: 카테고리 값 → 연산 매핑
+ {["할인없음", "할인율(%)", "할인금액"].map((label, idx) => {
+ const operations = ["none", "rate", "amount"];
+ return (
+
+ {label}
+ →
+ op === operations[idx])?.[0] || ""
+ }
+ onValueChange={(value) => {
+ const newMapping = { ...config.autoCalculation.valueMapping?.discountType };
+ Object.keys(newMapping).forEach(key => {
+ if (newMapping[key] === operations[idx]) delete newMapping[key];
+ });
+ if (value) {
+ newMapping[value] = operations[idx];
+ }
+ handleChange("autoCalculation", {
+ ...config.autoCalculation,
+ valueMapping: {
+ ...config.autoCalculation.valueMapping,
+ discountType: newMapping,
+ },
+ });
+ }}
+ >
+
+
+
+
+ {(categoryValues.discountType || []).map((val: any) => (
+
+ {val.valueLabel}
+
+ ))}
+
+
+ {operations[idx]}
+
+ );
+ })}
+
+ )}
+
+
+
+ {/* 반올림 방식 매핑 */}
+
+
+
+ 반올림 방식 연산 매핑
+
+
+
+
+ {/* 1단계: 메뉴 선택 */}
+
+ 1단계: 메뉴 선택
+ handleMenuSelect(Number(value), "roundingType")}
+ >
+
+
+
+
+ {secondLevelMenus.map((menu) => (
+
+ {menu.parentMenuName} > {menu.menuName}
+
+ ))}
+
+
+
+
+ {/* 2단계: 카테고리 선택 */}
+ {(config.autoCalculation.valueMapping as any)?._selectedMenus?.roundingType && (
+
+ 2단계: 카테고리 선택
+ handleCategorySelect(
+ value,
+ (config.autoCalculation.valueMapping as any)._selectedMenus.roundingType,
+ "roundingType"
+ )}
+ >
+
+
+
+
+ {(categoryColumns.roundingType || []).map((col: any) => (
+
+ {col.columnLabel || col.columnName}
+
+ ))}
+
+
+
+ )}
+
+ {/* 3단계: 값 매핑 */}
+ {(config.autoCalculation.valueMapping as any)?._selectedCategories?.roundingType && (
+
+
3단계: 카테고리 값 → 연산 매핑
+ {["반올림없음", "반올림", "절삭", "올림"].map((label, idx) => {
+ const operations = ["none", "round", "floor", "ceil"];
+ return (
+
+ {label}
+ →
+ op === operations[idx])?.[0] || ""
+ }
+ onValueChange={(value) => {
+ const newMapping = { ...config.autoCalculation.valueMapping?.roundingType };
+ Object.keys(newMapping).forEach(key => {
+ if (newMapping[key] === operations[idx]) delete newMapping[key];
+ });
+ if (value) {
+ newMapping[value] = operations[idx];
+ }
+ handleChange("autoCalculation", {
+ ...config.autoCalculation,
+ valueMapping: {
+ ...config.autoCalculation.valueMapping,
+ roundingType: newMapping,
+ },
+ });
+ }}
+ >
+
+
+
+
+ {(categoryValues.roundingType || []).map((val: any) => (
+
+ {val.valueLabel}
+
+ ))}
+
+
+ {operations[idx]}
+
+ );
+ })}
+
+ )}
+
+
+
+ {/* 반올림 단위 매핑 */}
+
+
+
+ 반올림 단위 값 매핑
+
+
+
+
+ {/* 1단계: 메뉴 선택 */}
+
+ 1단계: 메뉴 선택
+ handleMenuSelect(Number(value), "roundingUnit")}
+ >
+
+
+
+
+ {secondLevelMenus.map((menu) => (
+
+ {menu.parentMenuName} > {menu.menuName}
+
+ ))}
+
+
+
+
+ {/* 2단계: 카테고리 선택 */}
+ {(config.autoCalculation.valueMapping as any)?._selectedMenus?.roundingUnit && (
+
+ 2단계: 카테고리 선택
+ handleCategorySelect(
+ value,
+ (config.autoCalculation.valueMapping as any)._selectedMenus.roundingUnit,
+ "roundingUnit"
+ )}
+ >
+
+
+
+
+ {(categoryColumns.roundingUnit || []).map((col: any) => (
+
+ {col.columnLabel || col.columnName}
+
+ ))}
+
+
+
+ )}
+
+ {/* 3단계: 값 매핑 */}
+ {(config.autoCalculation.valueMapping as any)?._selectedCategories?.roundingUnit && (
+
+
3단계: 카테고리 값 → 단위 값 매핑
+ {["1원", "10원", "100원", "1,000원"].map((label) => {
+ const unitValue = label === "1,000원" ? 1000 : parseInt(label);
+ return (
+
+ {label}
+ →
+ val === unitValue)?.[0] || ""
+ }
+ onValueChange={(value) => {
+ const newMapping = { ...config.autoCalculation.valueMapping?.roundingUnit };
+ Object.keys(newMapping).forEach(key => {
+ if (newMapping[key] === unitValue) delete newMapping[key];
+ });
+ if (value) {
+ newMapping[value] = unitValue;
+ }
+ handleChange("autoCalculation", {
+ ...config.autoCalculation,
+ valueMapping: {
+ ...config.autoCalculation.valueMapping,
+ roundingUnit: newMapping,
+ },
+ });
+ }}
+ >
+
+
+
+
+ {(categoryValues.roundingUnit || []).map((val: any) => (
+
+ {val.valueLabel}
+
+ ))}
+
+
+ {unitValue}
+
+ );
+ })}
+
+ )}
+
+
+
+
+ 💡 1단계: 메뉴 선택 → 2단계: 카테고리 선택 → 3단계: 값 매핑
+
+
+
+ )}
+
+
{/* 옵션 */}
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 05c11e4a..0e6120c6 100644
--- a/frontend/lib/registry/components/selected-items-detail-input/types.ts
+++ b/frontend/lib/registry/components/selected-items-detail-input/types.ts
@@ -22,6 +22,10 @@ export interface AdditionalFieldDefinition {
placeholder?: string;
/** 기본값 */
defaultValue?: any;
+ /** 🆕 원본 데이터에서 자동으로 값을 가져올 필드명 */
+ autoFillFrom?: string;
+ /** 🆕 자동 채우기할 데이터의 테이블명 (비워두면 주 데이터 소스 사용) */
+ autoFillFromTable?: string;
/** 선택 옵션 (type이 select일 때) */
options?: Array<{ label: string; value: string }>;
/** 필드 너비 (px 또는 %) */
@@ -54,6 +58,39 @@ export interface FieldGroup {
displayItems?: DisplayItem[];
}
+/**
+ * 🆕 자동 계산 설정
+ */
+export interface AutoCalculationConfig {
+ /** 계산 대상 필드명 (예: calculated_price) */
+ targetField: string;
+ /** 계산에 사용할 입력 필드들 */
+ inputFields: {
+ basePrice: string; // 기본 단가 필드명
+ discountType: string; // 할인 방식 필드명
+ discountValue: string; // 할인값 필드명
+ roundingType: string; // 반올림 방식 필드명
+ roundingUnit: string; // 반올림 단위 필드명
+ };
+ /** 계산 함수 타입 */
+ calculationType: "price" | "custom";
+ /** 🆕 카테고리 값 → 연산 매핑 */
+ valueMapping?: {
+ /** 할인 방식 매핑 */
+ discountType?: {
+ [valueCode: string]: "none" | "rate" | "amount"; // 예: { "CATEGORY_544740": "rate" }
+ };
+ /** 반올림 방식 매핑 */
+ roundingType?: {
+ [valueCode: string]: "none" | "round" | "floor" | "ceil";
+ };
+ /** 반올림 단위 매핑 (숫자로 변환) */
+ roundingUnit?: {
+ [valueCode: string]: number; // 예: { "10": 10, "100": 100 }
+ };
+ };
+}
+
/**
* SelectedItemsDetailInput 컴포넌트 설정 타입
*/
@@ -93,6 +130,12 @@ export interface SelectedItemsDetailInputConfig extends ComponentConfig {
*/
targetTable?: string;
+ /**
+ * 🆕 자동 계산 설정
+ * 특정 필드가 변경되면 다른 필드를 자동으로 계산
+ */
+ autoCalculation?: AutoCalculationConfig;
+
/**
* 레이아웃 모드
* - grid: 테이블 형식 (기본)
diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx
index b7364a4b..5d2d621f 100644
--- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx
+++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx
@@ -418,8 +418,17 @@ export const SplitPanelLayoutComponent: React.FC
setSelectedLeftItem(item);
setExpandedRightItems(new Set()); // 좌측 항목 변경 시 우측 확장 초기화
loadRightData(item);
+
+ // 🆕 modalDataStore에 선택된 좌측 항목 저장 (단일 선택)
+ const leftTableName = componentConfig.leftPanel?.tableName;
+ if (leftTableName && !isDesignMode) {
+ import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
+ useModalDataStore.getState().setData(leftTableName, [item]);
+ console.log(`✅ 분할 패널 좌측 선택: ${leftTableName}`, item);
+ });
+ }
},
- [loadRightData],
+ [loadRightData, componentConfig.leftPanel?.tableName, isDesignMode],
);
// 우측 항목 확장/축소 토글
diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts
index 1d39ce91..615aedf9 100644
--- a/frontend/lib/utils/buttonActions.ts
+++ b/frontend/lib/utils/buttonActions.ts
@@ -41,6 +41,13 @@ export interface ButtonActionConfig {
// 모달/팝업 관련
modalTitle?: string;
+ modalTitleBlocks?: Array<{ // 🆕 블록 기반 제목 (우선순위 높음)
+ id: string;
+ type: "text" | "field";
+ value: string; // type=text: 텍스트 내용, type=field: 컬럼명
+ tableName?: string; // type=field일 때 테이블명
+ label?: string; // type=field일 때 표시용 라벨
+ }>;
modalDescription?: string;
modalSize?: "sm" | "md" | "lg" | "xl";
popupWidth?: number;
@@ -207,6 +214,20 @@ export class ButtonActionExecutor {
await new Promise(resolve => setTimeout(resolve, 100));
// 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조)
+ console.log("🔍 [handleSave] formData 구조 확인:", {
+ keys: Object.keys(context.formData),
+ values: Object.entries(context.formData).map(([key, value]) => ({
+ key,
+ isArray: Array.isArray(value),
+ length: Array.isArray(value) ? value.length : 0,
+ firstItem: Array.isArray(value) && value.length > 0 ? {
+ hasOriginalData: !!value[0]?.originalData,
+ hasFieldGroups: !!value[0]?.fieldGroups,
+ keys: Object.keys(value[0] || {})
+ } : null
+ }))
+ });
+
const selectedItemsKeys = Object.keys(context.formData).filter(key => {
const value = context.formData[key];
return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups;
@@ -215,6 +236,8 @@ export class ButtonActionExecutor {
if (selectedItemsKeys.length > 0) {
console.log("🔄 [handleSave] SelectedItemsDetailInput 배치 저장 감지:", selectedItemsKeys);
return await this.handleBatchSave(config, context, selectedItemsKeys);
+ } else {
+ console.log("⚠️ [handleSave] SelectedItemsDetailInput 데이터 감지 실패 - 일반 저장 진행");
}
// 폼 유효성 검사
@@ -830,11 +853,11 @@ export class ButtonActionExecutor {
dataSourceId: config.dataSourceId,
});
- // 🆕 1. dataSourceId 자동 결정
+ // 🆕 1. 현재 화면의 TableList 또는 SplitPanelLayout 자동 감지
let dataSourceId = config.dataSourceId;
- // dataSourceId가 없으면 같은 화면의 TableList 자동 감지
if (!dataSourceId && context.allComponents) {
+ // TableList 우선 감지
const tableListComponent = context.allComponents.find(
(comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName
);
@@ -845,6 +868,19 @@ export class ButtonActionExecutor {
componentId: tableListComponent.id,
tableName: dataSourceId,
});
+ } else {
+ // TableList가 없으면 SplitPanelLayout의 좌측 패널 감지
+ const splitPanelComponent = context.allComponents.find(
+ (comp: any) => comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName
+ );
+
+ if (splitPanelComponent) {
+ dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName;
+ console.log("✨ 분할 패널 좌측 테이블 자동 감지:", {
+ componentId: splitPanelComponent.id,
+ tableName: dataSourceId,
+ });
+ }
}
}
@@ -853,21 +889,30 @@ export class ButtonActionExecutor {
dataSourceId = context.tableName || "default";
}
- // 2. modalDataStore에서 데이터 확인
+ // 🆕 2. modalDataStore에서 현재 선택된 데이터 확인
try {
const { useModalDataStore } = await import("@/stores/modalDataStore");
- const modalData = useModalDataStore.getState().dataRegistry[dataSourceId] || [];
+ const dataRegistry = useModalDataStore.getState().dataRegistry;
+
+ const modalData = dataRegistry[dataSourceId] || [];
+
+ console.log("📊 현재 화면 데이터 확인:", {
+ dataSourceId,
+ count: modalData.length,
+ allKeys: Object.keys(dataRegistry), // 🆕 전체 데이터 키 확인
+ });
if (modalData.length === 0) {
- console.warn("⚠️ 전달할 데이터가 없습니다:", dataSourceId);
+ console.warn("⚠️ 선택된 데이터가 없습니다:", dataSourceId);
toast.warning("선택된 데이터가 없습니다. 먼저 항목을 선택해주세요.");
return false;
}
- console.log("✅ 전달할 데이터:", {
- dataSourceId,
- count: modalData.length,
- data: modalData,
+ console.log("✅ 모달 데이터 준비 완료:", {
+ currentData: { id: dataSourceId, count: modalData.length },
+ previousData: Object.entries(dataRegistry)
+ .filter(([key]) => key !== dataSourceId)
+ .map(([key, data]: [string, any]) => ({ id: key, count: data.length })),
});
} catch (error) {
console.error("❌ 데이터 확인 실패:", error);
@@ -875,7 +920,79 @@ export class ButtonActionExecutor {
return false;
}
- // 3. 모달 열기 + URL 파라미터로 dataSourceId 전달
+ // 6. 동적 모달 제목 생성
+ const { useModalDataStore } = await import("@/stores/modalDataStore");
+ const dataRegistry = useModalDataStore.getState().dataRegistry;
+
+ let finalTitle = "데이터 입력";
+
+ // 🆕 블록 기반 제목 (우선순위 1)
+ if (config.modalTitleBlocks && config.modalTitleBlocks.length > 0) {
+ const titleParts: string[] = [];
+
+ config.modalTitleBlocks.forEach((block) => {
+ if (block.type === "text") {
+ // 텍스트 블록: 그대로 추가
+ titleParts.push(block.value);
+ } else if (block.type === "field") {
+ // 필드 블록: 데이터에서 값 가져오기
+ const tableName = block.tableName;
+ const columnName = block.value;
+
+ if (tableName && columnName) {
+ const tableData = dataRegistry[tableName];
+ if (tableData && tableData.length > 0) {
+ const firstItem = tableData[0].originalData || tableData[0];
+ const value = firstItem[columnName];
+
+ if (value !== undefined && value !== null) {
+ titleParts.push(String(value));
+ console.log(`✨ 동적 필드: ${tableName}.${columnName} → ${value}`);
+ } else {
+ // 데이터 없으면 라벨 표시
+ titleParts.push(block.label || columnName);
+ }
+ } else {
+ // 테이블 데이터 없으면 라벨 표시
+ titleParts.push(block.label || columnName);
+ }
+ }
+ }
+ });
+
+ finalTitle = titleParts.join("");
+ console.log("📋 블록 기반 제목 생성:", finalTitle);
+ }
+ // 기존 방식: {tableName.columnName} 패턴 (우선순위 2)
+ else if (config.modalTitle) {
+ finalTitle = config.modalTitle;
+
+ if (finalTitle.includes("{")) {
+ const matches = finalTitle.match(/\{([^}]+)\}/g);
+
+ if (matches) {
+ matches.forEach((match) => {
+ const path = match.slice(1, -1); // {item_info.item_name} → item_info.item_name
+ const [tableName, columnName] = path.split(".");
+
+ if (tableName && columnName) {
+ const tableData = dataRegistry[tableName];
+ if (tableData && tableData.length > 0) {
+ const firstItem = tableData[0].originalData || tableData[0];
+ const value = firstItem[columnName];
+
+ if (value !== undefined && value !== null) {
+ finalTitle = finalTitle.replace(match, String(value));
+ console.log(`✨ 동적 제목: ${match} → ${value}`);
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ // 7. 모달 열기 + URL 파라미터로 dataSourceId 전달
if (config.targetScreenId) {
// config에 modalDescription이 있으면 우선 사용
let description = config.modalDescription || "";
@@ -894,10 +1011,10 @@ export class ButtonActionExecutor {
const modalEvent = new CustomEvent("openScreenModal", {
detail: {
screenId: config.targetScreenId,
- title: config.modalTitle || "데이터 입력",
+ title: finalTitle, // 🆕 동적 제목 사용
description: description,
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
- urlParams: { dataSourceId }, // 🆕 URL 파라미터로 dataSourceId 전달
+ urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음)
},
});
diff --git a/frontend/types/unified-core.ts b/frontend/types/unified-core.ts
index cba1c3f7..4da2280a 100644
--- a/frontend/types/unified-core.ts
+++ b/frontend/types/unified-core.ts
@@ -85,8 +85,7 @@ export type ComponentType =
| "area"
| "layout"
| "flow"
- | "component"
- | "category-manager";
+ | "component";
/**
* 기본 위치 정보