From ffb59b4e1b867b3c666af099954824a182958e82 Mon Sep 17 00:00:00 2001
From: SeongHyun Kim
Date: Mon, 15 Dec 2025 18:09:55 +0900
Subject: [PATCH 1/3] =?UTF-8?q?=EC=B6=9C=ED=95=98=EA=B3=84=ED=9A=8D=20?=
=?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/lib/utils/buttonActions.ts | 25 +++++++++++--------------
1 file changed, 11 insertions(+), 14 deletions(-)
diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts
index cb21ac5a..587513e4 100644
--- a/frontend/lib/utils/buttonActions.ts
+++ b/frontend/lib/utils/buttonActions.ts
@@ -1780,16 +1780,15 @@ export class ButtonActionExecutor {
/**
* 모달 액션 처리
- * 🔧 modal 액션은 항상 신규 등록(INSERT) 모드로 동작
- * edit 액션만 수정(UPDATE) 모드로 동작해야 함
+ * 선택된 데이터가 있으면 함께 전달 (출하계획 등에서 사용)
*/
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise {
// 모달 열기 로직
- console.log("모달 열기 (신규 등록 모드):", {
+ console.log("모달 열기:", {
title: config.modalTitle,
size: config.modalSize,
targetScreenId: config.targetScreenId,
- // 🔧 selectedRowsData는 modal 액션에서 사용하지 않음 (신규 등록이므로)
+ selectedRowsData: context.selectedRowsData,
});
if (config.targetScreenId) {
@@ -1806,11 +1805,10 @@ export class ButtonActionExecutor {
}
}
- // 🔧 modal 액션은 신규 등록이므로 selectedData를 전달하지 않음
- // selectedData가 있으면 ScreenModal에서 originalData로 인식하여 UPDATE 모드로 동작하게 됨
- // edit 액션만 selectedData/editData를 사용하여 UPDATE 모드로 동작
- console.log("📦 [handleModal] 신규 등록 모드 - selectedData 전달하지 않음");
- console.log("📦 [handleModal] 분할 패널 부모 데이터 (초기값으로 사용):", context.splitPanelParentData);
+ // 선택된 행 데이터 수집
+ const selectedData = context.selectedRowsData || [];
+ console.log("📦 [handleModal] 선택된 데이터:", selectedData);
+ console.log("📦 [handleModal] 분할 패널 부모 데이터:", context.splitPanelParentData);
// 전역 모달 상태 업데이트를 위한 이벤트 발생
const modalEvent = new CustomEvent("openScreenModal", {
@@ -1819,11 +1817,10 @@ export class ButtonActionExecutor {
title: config.modalTitle || "화면",
description: description,
size: config.modalSize || "md",
- // 🔧 신규 등록이므로 selectedData/selectedIds를 전달하지 않음
- // edit 액션에서만 이 데이터를 사용
- selectedData: [],
- selectedIds: [],
- // 🆕 분할 패널 부모 데이터 전달 (탭 안 모달에서 초기값으로 사용)
+ // 선택된 행 데이터 전달
+ selectedData: selectedData,
+ selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
+ // 분할 패널 부모 데이터 전달 (탭 안 모달에서 사용)
splitPanelParentData: context.splitPanelParentData || {},
},
});
From f6051e8bbdc0f01024d960a8241ec3c743de50c8 Mon Sep 17 00:00:00 2001
From: SeongHyun Kim
Date: Mon, 15 Dec 2025 18:39:59 +0900
Subject: [PATCH 2/3] =?UTF-8?q?fix(button-actions):=20openModalWithData=20?=
=?UTF-8?q?=EC=95=A1=EC=85=98=EC=97=90=EC=84=9C=20=EC=84=A0=ED=83=9D?=
=?UTF-8?q?=EB=90=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=84=EB=8B=AC=20?=
=?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- handleOpenModalWithData에서 modalDataStore 데이터를 selectedData/selectedIds로 이벤트에 포함
- RepeatScreenModal에서 groupedData로 사용할 수 있도록 데이터 전달 경로 완성
- ButtonConfigPanel 필드 매핑 UI를 세로 배치로 변경하여 가독성 개선
- split-panel-layout2 컴포넌트 타입 소스 테이블 감지 지원 추가
- currentTableName 폴백 로직 추가로 테이블명 감지 안정성 향상
---
.../config-panels/ButtonConfigPanel.tsx | 168 +++++++++++++-----
frontend/lib/utils/buttonActions.ts | 10 ++
2 files changed, 129 insertions(+), 49 deletions(-)
diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx
index 39f32a73..54315683 100644
--- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx
+++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx
@@ -333,22 +333,72 @@ export const ButtonConfigPanel: React.FC = ({
const loadModalMappingColumns = async () => {
// 소스 테이블: 현재 화면의 분할 패널 또는 테이블에서 감지
- // allComponents에서 split-panel-layout 또는 table-list 찾기
let sourceTableName: string | null = null;
+ console.log("[openModalWithData] 컬럼 로드 시작:", {
+ allComponentsCount: allComponents.length,
+ currentTableName,
+ targetScreenId: config.action?.targetScreenId,
+ });
+
+ // 모든 컴포넌트 타입 로그
+ allComponents.forEach((comp, idx) => {
+ const compType = comp.componentType || (comp as any).componentConfig?.type;
+ console.log(` [${idx}] componentType: ${compType}, tableName: ${(comp as any).componentConfig?.tableName || (comp as any).componentConfig?.leftPanel?.tableName || 'N/A'}`);
+ });
+
for (const comp of allComponents) {
const compType = comp.componentType || (comp as any).componentConfig?.type;
+ const compConfig = (comp as any).componentConfig || {};
+
+ // 분할 패널 타입들 (다양한 경로에서 테이블명 추출)
if (compType === "split-panel-layout" || compType === "screen-split-panel") {
- // 분할 패널의 좌측 테이블명
- sourceTableName = (comp as any).componentConfig?.leftPanel?.tableName ||
- (comp as any).componentConfig?.leftTableName;
- break;
+ sourceTableName = compConfig?.leftPanel?.tableName ||
+ compConfig?.leftTableName ||
+ compConfig?.tableName;
+ if (sourceTableName) {
+ console.log(`✅ [openModalWithData] split-panel-layout에서 소스 테이블 감지: ${sourceTableName}`);
+ break;
+ }
}
+
+ // split-panel-layout2 타입 (새로운 분할 패널)
+ if (compType === "split-panel-layout2") {
+ sourceTableName = compConfig?.leftPanel?.tableName ||
+ compConfig?.tableName ||
+ compConfig?.leftTableName;
+ if (sourceTableName) {
+ console.log(`✅ [openModalWithData] split-panel-layout2에서 소스 테이블 감지: ${sourceTableName}`);
+ break;
+ }
+ }
+
+ // 테이블 리스트 타입
if (compType === "table-list") {
- sourceTableName = (comp as any).componentConfig?.tableName;
+ sourceTableName = compConfig?.tableName;
+ if (sourceTableName) {
+ console.log(`✅ [openModalWithData] table-list에서 소스 테이블 감지: ${sourceTableName}`);
+ break;
+ }
+ }
+
+ // 🆕 모든 컴포넌트에서 tableName 찾기 (폴백)
+ if (!sourceTableName && compConfig?.tableName) {
+ sourceTableName = compConfig.tableName;
+ console.log(`✅ [openModalWithData] ${compType}에서 소스 테이블 감지 (폴백): ${sourceTableName}`);
break;
}
}
+
+ // 여전히 없으면 currentTableName 사용 (화면 레벨 테이블명)
+ if (!sourceTableName && currentTableName) {
+ sourceTableName = currentTableName;
+ console.log(`✅ [openModalWithData] currentTableName에서 소스 테이블 사용: ${sourceTableName}`);
+ }
+
+ if (!sourceTableName) {
+ console.warn("[openModalWithData] 소스 테이블을 찾을 수 없습니다.");
+ }
// 소스 테이블 컬럼 로드
if (sourceTableName) {
@@ -361,11 +411,11 @@ export const ButtonConfigPanel: React.FC = ({
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,
+ name: col.name || col.columnName || col.column_name,
+ label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name,
}));
setModalSourceColumns(columns);
- console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드:`, columns.length);
+ console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드 완료:`, columns.length);
}
}
} catch (error) {
@@ -379,8 +429,12 @@ export const ButtonConfigPanel: React.FC = ({
try {
// 타겟 화면 정보 가져오기
const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`);
+ console.log("[openModalWithData] 타겟 화면 응답:", screenResponse.data);
+
if (screenResponse.data.success && screenResponse.data.data) {
const targetTableName = screenResponse.data.data.tableName;
+ console.log("[openModalWithData] 타겟 화면 테이블명:", targetTableName);
+
if (targetTableName) {
const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`);
if (columnResponse.data.success) {
@@ -390,23 +444,27 @@ export const ButtonConfigPanel: React.FC = ({
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,
+ name: col.name || col.columnName || col.column_name,
+ label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name,
}));
setModalTargetColumns(columns);
- console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드:`, columns.length);
+ console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드 완료:`, columns.length);
}
}
+ } else {
+ console.warn("[openModalWithData] 타겟 화면에 테이블명이 없습니다.");
}
}
} catch (error) {
console.error("타겟 화면 테이블 컬럼 로드 실패:", error);
}
+ } else {
+ console.warn("[openModalWithData] 타겟 화면 ID가 없습니다.");
}
};
loadModalMappingColumns();
- }, [config.action?.type, config.action?.targetScreenId, allComponents]);
+ }, [config.action?.type, config.action?.targetScreenId, allComponents, currentTableName]);
// 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준)
useEffect(() => {
@@ -1158,11 +1216,12 @@ export const ButtonConfigPanel: React.FC = ({
) : (
-
+
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
-
- {/* 소스 필드 선택 (Combobox) */}
-
+
+ {/* 소스 필드 선택 (Combobox) - 세로 배치 */}
+
+
setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
@@ -1171,15 +1230,17 @@ export const ButtonConfigPanel: React.FC = ({
-
+
= ({
value={modalSourceSearch[index] || ""}
onValueChange={(value) => setModalSourceSearch((prev) => ({ ...prev, [index]: value }))}
/>
-
+
컬럼을 찾을 수 없습니다
{modalSourceColumns.map((col) => (
@@ -1208,9 +1269,9 @@ export const ButtonConfigPanel: React.FC = ({
mapping.sourceField === col.name ? "opacity-100" : "opacity-0"
)}
/>
- {col.label}
+ {col.label}
{col.label !== col.name && (
- ({col.name})
+ ({col.name})
)}
))}
@@ -1221,10 +1282,14 @@ export const ButtonConfigPanel: React.FC = ({
-
→
+ {/* 화살표 표시 */}
+
+ ↓
+
- {/* 타겟 필드 선택 (Combobox) */}
-
+ {/* 타겟 필드 선택 (Combobox) - 세로 배치 */}
+
+
setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))}
@@ -1233,15 +1298,17 @@ export const ButtonConfigPanel: React.FC = ({
-
+
= ({
value={modalTargetSearch[index] || ""}
onValueChange={(value) => setModalTargetSearch((prev) => ({ ...prev, [index]: value }))}
/>
-
+
컬럼을 찾을 수 없습니다
{modalTargetColumns.map((col) => (
@@ -1270,9 +1337,9 @@ export const ButtonConfigPanel: React.FC = ({
mapping.targetField === col.name ? "opacity-100" : "opacity-0"
)}
/>
- {col.label}
+ {col.label}
{col.label !== col.name && (
- ({col.name})
+ ({col.name})
)}
))}
@@ -1284,19 +1351,22 @@ export const ButtonConfigPanel: React.FC = ({
{/* 삭제 버튼 */}
-
+
+
+
))}
diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts
index 587513e4..394e15c2 100644
--- a/frontend/lib/utils/buttonActions.ts
+++ b/frontend/lib/utils/buttonActions.ts
@@ -2020,11 +2020,18 @@ export class ButtonActionExecutor {
});
}
+ // 🆕 modalDataStore에서 선택된 전체 데이터 가져오기 (RepeatScreenModal에서 사용)
+ const modalData = dataRegistry[dataSourceId] || [];
+ const selectedData = modalData.map((item: any) => item.originalData || item);
+ const selectedIds = selectedData.map((row: any) => row.id).filter(Boolean);
+
console.log("📦 [openModalWithData] 부모 데이터 전달:", {
dataSourceId,
rawParentData,
mappedParentData: parentData,
fieldMappings: config.fieldMappings,
+ selectedDataCount: selectedData.length,
+ selectedIds,
});
// 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
@@ -2036,6 +2043,9 @@ export class ButtonActionExecutor {
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음)
splitPanelParentData: parentData, // 🆕 부모 데이터 전달 (excludeFilter에서 사용)
+ // 🆕 선택된 데이터 전달 (RepeatScreenModal에서 groupedData로 사용)
+ selectedData: selectedData,
+ selectedIds: selectedIds,
},
});
From 4cff9e4cecd2720b2caed841deeba42f0df4e2d8 Mon Sep 17 00:00:00 2001
From: SeongHyun Kim
Date: Tue, 16 Dec 2025 09:13:42 +0900
Subject: [PATCH 3/3] =?UTF-8?q?fix(button-actions):=20=EC=B6=9C=ED=95=98?=
=?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EB=AA=A8=EB=8B=AC=20=EB=8D=B0=EC=9D=B4?=
=?UTF-8?q?=ED=84=B0=20=EC=A0=84=EB=8B=AC=20=EC=98=A4=EB=A5=98=20=EC=88=98?=
=?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- handleModal: context.selectedRowsData를 selectedData로 복원하여 출하계획 등 모달에서 사용 가능
- handleOpenModalWithData: modalDataStore 데이터를 selectedData/selectedIds로 이벤트에 포함
- ButtonConfigPanel: split-panel-layout2 타입 소스 테이블 감지 지원 추가
- ButtonConfigPanel: column_name/display_name 컬럼 형식 폴백 추가
- ButtonConfigPanel: currentTableName 폴백으로 테이블명 감지 안정성 향상
- ButtonConfigPanel: 필드 매핑 UI를 세로 배치로 변경하여 가독성 개선
---
.../button-primary/ButtonPrimaryComponent.tsx | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
index 26bbd0c9..160591c6 100644
--- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
+++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
@@ -880,6 +880,44 @@ export const ButtonPrimaryComponent: React.FC = ({
return;
}
+ // 모달 액션인데 선택된 데이터가 있으면 경고 메시지 표시하고 중단
+ // (신규 등록 모달에서 선택된 데이터가 초기값으로 전달되는 것을 방지)
+ if (processedConfig.action.type === "modal" && effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) {
+ toast.warning("신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요.");
+ return;
+ }
+
+ // 수정(edit) 액션 검증
+ if (processedConfig.action.type === "edit") {
+ // 선택된 데이터가 없으면 경고
+ if (!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) {
+ toast.warning("수정할 항목을 선택해주세요.");
+ return;
+ }
+
+ // groupByColumns 설정이 있으면 해당 컬럼 값이 유일한지 확인
+ const groupByColumns = processedConfig.action.groupByColumns;
+ if (groupByColumns && groupByColumns.length > 0 && effectiveSelectedRowsData.length > 1) {
+ // 첫 번째 그룹핑 컬럼 기준으로 중복 체크 (예: order_no)
+ const groupByColumn = groupByColumns[0];
+ const uniqueValues = new Set(
+ effectiveSelectedRowsData.map((row: any) => row[groupByColumn]).filter(Boolean)
+ );
+
+ if (uniqueValues.size > 1) {
+ // 컬럼명을 한글로 변환 (order_no -> 수주번호)
+ const columnLabels: Record = {
+ order_no: "수주번호",
+ shipment_no: "출하번호",
+ purchase_no: "구매번호",
+ };
+ const columnLabel = columnLabels[groupByColumn] || groupByColumn;
+ toast.warning(`${columnLabel} 하나만 선택해주세요. (현재 ${uniqueValues.size}개 선택됨)`);
+ return;
+ }
+ }
+ }
+
// 🆕 모든 컴포넌트의 설정 수집 (parentDataMapping 등)
const componentConfigs: Record = {};
if (allComponents && Array.isArray(allComponents)) {