diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx
index 2118bca3..3a440f07 100644
--- a/frontend/components/screen/ScreenDesigner.tsx
+++ b/frontend/components/screen/ScreenDesigner.tsx
@@ -2239,10 +2239,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
calculatedWidth: `${Math.round(widthPercent * 100) / 100}%`,
});
+ // ๐ ๋ผ๋ฒจ์ ๊ธฐ๋ฐ์ผ๋ก ๊ธฐ๋ณธ columnName ์์ฑ (ํ๊ธ โ ์ค๋ค์ดํฌ ์ผ์ด์ค)
+ // ์: "์ฐฝ๊ณ ์ฝ๋" โ "warehouse_code" ๋๋ ๊ทธ๋๋ก ์ ์ง
+ const generateDefaultColumnName = (label: string): string => {
+ // ํ๊ธ ๋ผ๋ฒจ์ ๊ฒฝ์ฐ ๊ทธ๋๋ก ์ฌ์ฉ (๋์ค์ ์ฌ์ฉ์๊ฐ ์์ ๊ฐ๋ฅ)
+ // ์๋ฌธ์ ๊ฒฝ์ฐ ์ค๋ค์ดํฌ ์ผ์ด์ค๋ก ๋ณํ
+ if (/[๊ฐ-ํฃ]/.test(label)) {
+ // ํ๊ธ์ด ํฌํจ๋ ๊ฒฝ์ฐ: ๊ณต๋ฐฑ์ ์ธ๋์ค์ฝ์ด๋ก, ์๋ฌธ์๋ก ๋ณํ
+ return label.replace(/\s+/g, "_").toLowerCase();
+ }
+ // ์๋ฌธ์ ๊ฒฝ์ฐ: ์นด๋ฉ์ผ์ด์ค/ํ์ค์นผ์ผ์ด์ค๋ฅผ ์ค๋ค์ดํฌ ์ผ์ด์ค๋ก ๋ณํ
+ return label
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
+ .replace(/\s+/g, "_")
+ .toLowerCase();
+ };
+
const newComponent: ComponentData = {
id: generateComponentId(),
type: "component", // โ
์ ์ปดํฌ๋ํธ ์์คํ
์ฌ์ฉ
label: component.name,
+ columnName: generateDefaultColumnName(component.name), // ๐ ๊ธฐ๋ณธ columnName ์๋ ์์ฑ
widgetType: component.webType,
componentType: component.id, // ์ ์ปดํฌ๋ํธ ์์คํ
์ ID (DynamicComponentRenderer์ฉ)
position: snappedPosition,
diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx
index 36f420fd..39f32a73 100644
--- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx
+++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx
@@ -91,6 +91,14 @@ export const ButtonConfigPanel: React.FC = ({
const [mappingSourceSearch, setMappingSourceSearch] = useState>({});
const [mappingTargetSearch, setMappingTargetSearch] = useState>({});
+ // ๐ openModalWithData ์ ์ฉ ํ๋ ๋งคํ ์ํ
+ const [modalSourceColumns, setModalSourceColumns] = useState>([]);
+ const [modalTargetColumns, setModalTargetColumns] = useState>([]);
+ const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState>({});
+ const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState>({});
+ const [modalSourceSearch, setModalSourceSearch] = useState>({});
+ const [modalTargetSearch, setModalTargetSearch] = useState>({});
+
// ๐ฏ ํ๋ก์ฐ ์์ ฏ์ด ํ๋ฉด์ ์๋์ง ํ์ธ
const hasFlowWidget = useMemo(() => {
const found = allComponents.some((comp: any) => {
@@ -318,6 +326,88 @@ export const ButtonConfigPanel: React.FC = ({
loadColumns();
}, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]);
+ // ๐ openModalWithData ์์ค/ํ๊ฒ ํ
์ด๋ธ ์ปฌ๋ผ ๋ก๋
+ useEffect(() => {
+ const actionType = config.action?.type;
+ if (actionType !== "openModalWithData") return;
+
+ const loadModalMappingColumns = async () => {
+ // ์์ค ํ
์ด๋ธ: ํ์ฌ ํ๋ฉด์ ๋ถํ ํจ๋ ๋๋ ํ
์ด๋ธ์์ ๊ฐ์ง
+ // allComponents์์ split-panel-layout ๋๋ table-list ์ฐพ๊ธฐ
+ let sourceTableName: string | null = null;
+
+ for (const comp of allComponents) {
+ const compType = comp.componentType || (comp as any).componentConfig?.type;
+ if (compType === "split-panel-layout" || compType === "screen-split-panel") {
+ // ๋ถํ ํจ๋์ ์ข์ธก ํ
์ด๋ธ๋ช
+ sourceTableName = (comp as any).componentConfig?.leftPanel?.tableName ||
+ (comp as any).componentConfig?.leftTableName;
+ break;
+ }
+ if (compType === "table-list") {
+ sourceTableName = (comp as any).componentConfig?.tableName;
+ break;
+ }
+ }
+
+ // ์์ค ํ
์ด๋ธ ์ปฌ๋ผ ๋ก๋
+ if (sourceTableName) {
+ try {
+ const response = await apiClient.get(`/table-management/tables/${sourceTableName}/columns`);
+ if (response.data.success) {
+ let columnData = response.data.data;
+ if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
+ if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
+
+ if (Array.isArray(columnData)) {
+ const columns = columnData.map((col: any) => ({
+ name: col.name || col.columnName,
+ label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
+ }));
+ setModalSourceColumns(columns);
+ console.log(`โ
[openModalWithData] ์์ค ํ
์ด๋ธ(${sourceTableName}) ์ปฌ๋ผ ๋ก๋:`, columns.length);
+ }
+ }
+ } catch (error) {
+ console.error("์์ค ํ
์ด๋ธ ์ปฌ๋ผ ๋ก๋ ์คํจ:", error);
+ }
+ }
+
+ // ํ๊ฒ ํ๋ฉด์ ํ
์ด๋ธ ์ปฌ๋ผ ๋ก๋
+ const targetScreenId = config.action?.targetScreenId;
+ if (targetScreenId) {
+ try {
+ // ํ๊ฒ ํ๋ฉด ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
+ const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`);
+ if (screenResponse.data.success && screenResponse.data.data) {
+ const targetTableName = screenResponse.data.data.tableName;
+ if (targetTableName) {
+ const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`);
+ if (columnResponse.data.success) {
+ let columnData = columnResponse.data.data;
+ if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
+ if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
+
+ if (Array.isArray(columnData)) {
+ const columns = columnData.map((col: any) => ({
+ name: col.name || col.columnName,
+ label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
+ }));
+ setModalTargetColumns(columns);
+ console.log(`โ
[openModalWithData] ํ๊ฒ ํ
์ด๋ธ(${targetTableName}) ์ปฌ๋ผ ๋ก๋:`, columns.length);
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error("ํ๊ฒ ํ๋ฉด ํ
์ด๋ธ ์ปฌ๋ผ ๋ก๋ ์คํจ:", error);
+ }
+ }
+ };
+
+ loadModalMappingColumns();
+ }, [config.action?.type, config.action?.targetScreenId, allComponents]);
+
// ํ๋ฉด ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ (ํ์ฌ ํธ์ง ์ค์ธ ํ๋ฉด์ ํ์ฌ ์ฝ๋ ๊ธฐ์ค)
useEffect(() => {
const fetchScreens = async () => {
@@ -1024,6 +1114,194 @@ export const ButtonConfigPanel: React.FC = ({
SelectedItemsDetailInput ์ปดํฌ๋ํธ๊ฐ ์๋ ํ๋ฉด์ ์ ํํ์ธ์
+
+ {/* ๐ ํ๋ ๋งคํ ์ค์ (์์ค ์ปฌ๋ผ โ ํ๊ฒ ์ปฌ๋ผ) */}
+
+
+
+
+
+
+ ์์ค ํ
์ด๋ธ์ ์ปฌ๋ผ๋ช
์ด ํ๊ฒ ํ๋ฉด์ ์
๋ ฅ ํ๋ ์ปฌ๋ผ๋ช
๊ณผ ๋ค๋ฅผ ๋ ๋งคํ์ ์ค์ ํ์ธ์.
+
+ ์: warehouse_code โ warehouse_id (๋ถํ ํจ๋์ ์ฐฝ๊ณ ์ฝ๋๋ฅผ ๋ชจ๋ฌ์ ์ฐฝ๊ณ ID์ ๋งคํ)
+
+
+ {/* ์ปฌ๋ผ ๋ก๋ ์ํ ํ์ */}
+ {modalSourceColumns.length > 0 || modalTargetColumns.length > 0 ? (
+
+ ์์ค ์ปฌ๋ผ: {modalSourceColumns.length}๊ฐ / ํ๊ฒ ์ปฌ๋ผ: {modalTargetColumns.length}๊ฐ
+
+ ) : (
+
+ ๋ถํ ํจ๋ ๋๋ ํ
์ด๋ธ ์ปดํฌ๋ํธ์ ๋์ ํ๋ฉด์ ์ค์ ํ๋ฉด ์ปฌ๋ผ ๋ชฉ๋ก์ด ๋ก๋๋ฉ๋๋ค.
+
+ )}
+
+ {(config.action?.fieldMappings || []).length === 0 ? (
+
+
+ ๋งคํ์ด ์์ผ๋ฉด ๊ฐ์ ์ด๋ฆ์ ์ปฌ๋ผ๋ผ๋ฆฌ ์๋์ผ๋ก ๋งคํ๋ฉ๋๋ค.
+
+
+ ) : (
+
+ {(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
+
+ {/* ์์ค ํ๋ ์ ํ (Combobox) */}
+
+
setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
+ >
+
+
+
+
+
+ setModalSourceSearch((prev) => ({ ...prev, [index]: value }))}
+ />
+
+ ์ปฌ๋ผ์ ์ฐพ์ ์ ์์ต๋๋ค
+
+ {modalSourceColumns.map((col) => (
+ {
+ const mappings = [...(config.action?.fieldMappings || [])];
+ mappings[index] = { ...mappings[index], sourceField: col.name };
+ onUpdateProperty("componentConfig.action.fieldMappings", mappings);
+ setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: false }));
+ }}
+ className="text-xs"
+ >
+
+ {col.label}
+ {col.label !== col.name && (
+ ({col.name})
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
โ
+
+ {/* ํ๊ฒ ํ๋ ์ ํ (Combobox) */}
+
+
setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))}
+ >
+
+
+
+
+
+ setModalTargetSearch((prev) => ({ ...prev, [index]: value }))}
+ />
+
+ ์ปฌ๋ผ์ ์ฐพ์ ์ ์์ต๋๋ค
+
+ {modalTargetColumns.map((col) => (
+ {
+ const mappings = [...(config.action?.fieldMappings || [])];
+ mappings[index] = { ...mappings[index], targetField: col.name };
+ onUpdateProperty("componentConfig.action.fieldMappings", mappings);
+ setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: false }));
+ }}
+ className="text-xs"
+ >
+
+ {col.label}
+ {col.label !== col.name && (
+ ({col.name})
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ {/* ์ญ์ ๋ฒํผ */}
+
+
+ ))}
+
+ )}
+
)}
diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx
index ff21ac3e..bb663c74 100644
--- a/frontend/components/screen/panels/PropertiesPanel.tsx
+++ b/frontend/components/screen/panels/PropertiesPanel.tsx
@@ -584,20 +584,23 @@ const PropertiesPanelComponent: React.FC = ({
- {selectedComponent.type === "widget" && (
+ {(selectedComponent.type === "widget" || selectedComponent.type === "component") && (
<>
diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts
index 7c1545d8..275efbb5 100644
--- a/frontend/lib/utils/buttonActions.ts
+++ b/frontend/lib/utils/buttonActions.ts
@@ -59,6 +59,7 @@ export interface ButtonActionConfig {
popupWidth?: number;
popupHeight?: number;
dataSourceId?: string; // ๐ modalDataStore์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ID (openModalWithData์ฉ)
+ fieldMappings?: Array<{ sourceField: string; targetField: string }>; // ๐ ํ๋ ๋งคํ (openModalWithData์ฉ)
// ํ์ธ ๋ฉ์์ง
confirmMessage?: string;
@@ -1548,10 +1549,27 @@ export class ButtonActionExecutor {
}
// ๐ ๋ถ๋ชจ ํ๋ฉด์ ์ ํ๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ (excludeFilter์์ ์ฌ์ฉ)
- const parentData = dataRegistry[dataSourceId]?.[0]?.originalData || dataRegistry[dataSourceId]?.[0] || {};
+ const rawParentData = dataRegistry[dataSourceId]?.[0]?.originalData || dataRegistry[dataSourceId]?.[0] || {};
+
+ // ๐ ํ๋ ๋งคํ ์ ์ฉ (์์ค ์ปฌ๋ผ โ ํ๊ฒ ์ปฌ๋ผ)
+ let parentData = { ...rawParentData };
+ if (config.fieldMappings && Array.isArray(config.fieldMappings) && config.fieldMappings.length > 0) {
+ console.log("๐ [openModalWithData] ํ๋ ๋งคํ ์ ์ฉ:", config.fieldMappings);
+
+ config.fieldMappings.forEach((mapping: { sourceField: string; targetField: string }) => {
+ if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) {
+ // ํ๊ฒ ํ๋์ ์์ค ํ๋ ๊ฐ ๋ณต์ฌ
+ parentData[mapping.targetField] = rawParentData[mapping.sourceField];
+ console.log(` โ
${mapping.sourceField} โ ${mapping.targetField}: ${rawParentData[mapping.sourceField]}`);
+ }
+ });
+ }
+
console.log("๐ฆ [openModalWithData] ๋ถ๋ชจ ๋ฐ์ดํฐ ์ ๋ฌ:", {
dataSourceId,
- parentData,
+ rawParentData,
+ mappedParentData: parentData,
+ fieldMappings: config.fieldMappings,
});
// ๐ ์ ์ญ ๋ชจ๋ฌ ์ํ ์
๋ฐ์ดํธ๋ฅผ ์ํ ์ด๋ฒคํธ ๋ฐ์ (URL ํ๋ผ๋ฏธํฐ ํฌํจ)