From 7c165a724ef51346dd44b8fa763ec901d82ddf12 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 7 Jan 2026 10:24:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B6=9C=EA=B3=A0=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=8B=AC=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=ED=99=94=20=EC=BB=AC=EB=9F=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?UI=20=EC=B6=94=EA=B0=80=20ButtonConfigPanel:=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=95=A1=EC=85=98=EC=97=90=20=EA=B7=B8=EB=A3=B9?= =?UTF-8?q?=ED=99=94=20=EC=BB=AC=EB=9F=BC=20=EC=84=A0=ED=83=9D=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=B6=94=EA=B0=80=20(=EC=98=81?= =?UTF-8?q?=EB=AC=B8/=ED=95=9C=EA=B8=80=20=EA=B2=80=EC=83=89=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90)=20ScreenSplitPanel/EmbeddedScreen:=20groupedData=20p?= =?UTF-8?q?rop=20=EC=A0=84=EB=8B=AC=20=EA=B2=BD=EB=A1=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20buttonActions:=20RepeaterFieldGroup=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C=20=EA=B3=B5=ED=86=B5=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=9A=B0=EC=84=A0=20=EC=A0=81=EC=9A=A9=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=91=ED=95=A9=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen-embedding/EmbeddedScreen.tsx | 5 +- .../screen-embedding/ScreenSplitPanel.tsx | 7 +- .../config-panels/ButtonConfigPanel.tsx | 136 ++++++++++++++++++ .../ScreenSplitPanelRenderer.tsx | 3 +- frontend/lib/utils/buttonActions.ts | 5 +- 5 files changed, 149 insertions(+), 7 deletions(-) diff --git a/frontend/components/screen-embedding/EmbeddedScreen.tsx b/frontend/components/screen-embedding/EmbeddedScreen.tsx index 3bfb7a77..0b32830e 100644 --- a/frontend/components/screen-embedding/EmbeddedScreen.tsx +++ b/frontend/components/screen-embedding/EmbeddedScreen.tsx @@ -27,13 +27,14 @@ interface EmbeddedScreenProps { onSelectionChanged?: (selectedRows: any[]) => void; position?: SplitPanelPosition; // 분할 패널 내 위치 (left/right) initialFormData?: Record; // 🆕 수정 모드에서 전달되는 초기 데이터 + groupedData?: Record[]; // 🆕 그룹 데이터 (수정 모드에서 원본 데이터 추적용) } /** * 임베드된 화면 컴포넌트 */ export const EmbeddedScreen = forwardRef( - ({ embedding, onSelectionChanged, position, initialFormData }, ref) => { + ({ embedding, onSelectionChanged, position, initialFormData, groupedData }, ref) => { const [layout, setLayout] = useState([]); const [selectedRows, setSelectedRows] = useState([]); const [loading, setLoading] = useState(true); @@ -430,6 +431,8 @@ export const EmbeddedScreen = forwardRef ); diff --git a/frontend/components/screen-embedding/ScreenSplitPanel.tsx b/frontend/components/screen-embedding/ScreenSplitPanel.tsx index f457e851..2f30a4ec 100644 --- a/frontend/components/screen-embedding/ScreenSplitPanel.tsx +++ b/frontend/components/screen-embedding/ScreenSplitPanel.tsx @@ -17,13 +17,14 @@ interface ScreenSplitPanelProps { screenId?: number; config?: any; // 설정 패널에서 오는 config (leftScreenId, rightScreenId, splitRatio, resizable) initialFormData?: Record; // 🆕 수정 모드에서 전달되는 초기 데이터 + groupedData?: Record[]; // 🆕 그룹 데이터 (수정 모드에서 원본 데이터 추적용) } /** * 분할 패널 컴포넌트 * 순수하게 화면 분할 기능만 제공합니다. */ -export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSplitPanelProps) { +export function ScreenSplitPanel({ screenId, config, initialFormData, groupedData }: ScreenSplitPanelProps) { // config에서 splitRatio 추출 (기본값 50) const configSplitRatio = config?.splitRatio ?? 50; @@ -117,7 +118,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp {/* 좌측 패널 */}
{hasLeftScreen ? ( - + ) : (

좌측 화면을 선택하세요

@@ -157,7 +158,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp {/* 우측 패널 */}
{hasRightScreen ? ( - + ) : (

우측 화면을 선택하세요

diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 417ea4ff..a97d78b3 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -60,6 +60,7 @@ export const ButtonConfigPanel: React.FC = ({ editModalTitle: String(config.action?.editModalTitle || ""), editModalDescription: String(config.action?.editModalDescription || ""), targetUrl: String(config.action?.targetUrl || ""), + groupByColumn: String(config.action?.groupByColumns?.[0] || ""), }); const [screens, setScreens] = useState([]); @@ -97,6 +98,11 @@ export const ButtonConfigPanel: React.FC = ({ const [modalTargetColumns, setModalTargetColumns] = useState>([]); const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState>({}); const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState>({}); + + // 🆕 그룹화 컬럼 선택용 상태 + const [currentTableColumns, setCurrentTableColumns] = useState>([]); + const [groupByColumnOpen, setGroupByColumnOpen] = useState(false); + const [groupByColumnSearch, setGroupByColumnSearch] = useState(""); const [modalSourceSearch, setModalSourceSearch] = useState>({}); const [modalTargetSearch, setModalTargetSearch] = useState>({}); @@ -130,6 +136,7 @@ export const ButtonConfigPanel: React.FC = ({ editModalTitle: String(latestAction.editModalTitle || ""), editModalDescription: String(latestAction.editModalDescription || ""), targetUrl: String(latestAction.targetUrl || ""), + groupByColumn: String(latestAction.groupByColumns?.[0] || ""), }); // 🆕 제목 블록 초기화 @@ -327,6 +334,35 @@ export const ButtonConfigPanel: React.FC = ({ loadColumns(); }, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]); + // 🆕 현재 테이블 컬럼 로드 (그룹화 컬럼 선택용) + useEffect(() => { + if (!currentTableName) return; + + const loadCurrentTableColumns = async () => { + try { + const response = await apiClient.get(`/table-management/tables/${currentTableName}/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, + })); + setCurrentTableColumns(columns); + console.log(`✅ 현재 테이블 ${currentTableName} 컬럼 로드 성공:`, columns.length, "개"); + } + } + } catch (error) { + console.error("현재 테이블 컬럼 로드 실패:", error); + } + }; + + loadCurrentTableColumns(); + }, [currentTableName]); + // 🆕 openModalWithData 소스/타겟 테이블 컬럼 로드 useEffect(() => { const actionType = config.action?.type; @@ -1529,6 +1565,106 @@ export const ButtonConfigPanel: React.FC = ({
)} + +
+ + + + + + +
+
+ + setGroupByColumnSearch(e.target.value)} + className="border-0 p-0 focus-visible:ring-0" + /> +
+
+ {currentTableColumns.length === 0 ? ( +
+ {currentTableName ? "컬럼을 불러오는 중..." : "테이블이 설정되지 않았습니다"} +
+ ) : ( + <> + {/* 선택 해제 옵션 */} +
{ + setLocalInputs((prev) => ({ ...prev, groupByColumn: "" })); + onUpdateProperty("componentConfig.action.groupByColumns", undefined); + setGroupByColumnOpen(false); + setGroupByColumnSearch(""); + }} + > + + 선택 안 함 +
+ {/* 컬럼 목록 */} + {currentTableColumns + .filter((col) => { + if (!groupByColumnSearch) return true; + const search = groupByColumnSearch.toLowerCase(); + return ( + col.name.toLowerCase().includes(search) || + col.label.toLowerCase().includes(search) + ); + }) + .map((col) => ( +
{ + setLocalInputs((prev) => ({ ...prev, groupByColumn: col.name })); + onUpdateProperty("componentConfig.action.groupByColumns", [col.name]); + setGroupByColumnOpen(false); + setGroupByColumnSearch(""); + }} + > + +
+ {col.name} + {col.label !== col.name && ( + {col.label} + )} +
+
+ ))} + + )} +
+
+
+
+

+ 여러 행을 하나의 그룹으로 묶어서 수정할 때 사용합니다 +

+
)} diff --git a/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx b/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx index 5dc1830c..adeb9e20 100644 --- a/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx +++ b/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx @@ -66,7 +66,7 @@ class ScreenSplitPanelRenderer extends AutoRegisteringComponentRenderer { }; render() { - const { component, style = {}, componentConfig, config, screenId, formData } = this.props as any; + const { component, style = {}, componentConfig, config, screenId, formData, groupedData } = this.props as any; // componentConfig 또는 config 또는 component.componentConfig 사용 const finalConfig = componentConfig || config || component?.componentConfig || {}; @@ -77,6 +77,7 @@ class ScreenSplitPanelRenderer extends AutoRegisteringComponentRenderer { screenId={screenId || finalConfig.screenId} config={finalConfig} initialFormData={formData} // 🆕 수정 데이터 전달 + groupedData={groupedData} // 🆕 그룹 데이터 전달 (수정 모드에서 원본 데이터 추적용) />
); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 6daf17e9..6c5d5d36 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1036,10 +1036,11 @@ export class ButtonActionExecutor { } // 🆕 공통 필드 병합 + 사용자 정보 추가 - // 공통 필드를 먼저 넣고, 개별 항목 데이터로 덮어씀 (개별 항목이 우선) + // 개별 항목 데이터를 먼저 넣고, 공통 필드로 덮어씀 (공통 필드가 우선) + // 이유: 사용자가 공통 필드(출고상태 등)를 변경하면 모든 항목에 적용되어야 함 const dataWithMeta: Record = { - ...commonFields, // 범용 폼 모달의 공통 필드 (order_no, manager_id 등) ...dataToSave, // RepeaterFieldGroup의 개별 항목 데이터 + ...commonFields, // 범용 폼 모달의 공통 필드 (outbound_status 등) - 공통 필드가 우선! created_by: context.userId, updated_by: context.userId, company_code: context.companyCode,