From fb16e224f02e3831455391eb2671abc9055ae802 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Dec 2025 18:39:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=A4=91=EA=B0=84=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/common/ScreenModal.tsx | 10 + .../screen-embedding/ScreenSplitPanel.tsx | 2 + frontend/contexts/SplitPanelContext.tsx | 67 ++++++ .../button-primary/ButtonPrimaryComponent.tsx | 11 + .../card-display/CardDisplayComponent.tsx | 98 +++++---- .../ScreenSplitPanelConfigPanel.tsx | 205 +++++++++++++++++- .../table-list/TableListComponent.tsx | 16 ++ frontend/lib/utils/buttonActions.ts | 12 +- 8 files changed, 374 insertions(+), 47 deletions(-) diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 53fd0852..0713c1c3 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -17,6 +17,7 @@ import { toast } from "sonner"; import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; +import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; interface ScreenModalState { isOpen: boolean; @@ -32,6 +33,7 @@ interface ScreenModalProps { export const ScreenModal: React.FC = ({ className }) => { const { userId, userName, user } = useAuth(); + const splitPanelContext = useSplitPanelContext(); const [modalState, setModalState] = useState({ isOpen: false, @@ -152,6 +154,14 @@ export const ScreenModal: React.FC = ({ className }) => { setFormData(editData); setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) } else { + // ๐Ÿ†• ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฏธ๋ฆฌ ์„ค์ • + const parentData = splitPanelContext?.getMappedParentData() || {}; + if (Object.keys(parentData).length > 0) { + console.log("๐Ÿ”— [ScreenModal] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐ๊ฐ’ ์„ค์ •:", parentData); + setFormData(parentData); + } else { + setFormData({}); + } setOriginalData(null); // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ } diff --git a/frontend/components/screen-embedding/ScreenSplitPanel.tsx b/frontend/components/screen-embedding/ScreenSplitPanel.tsx index 2e43fcc6..4eba4f9b 100644 --- a/frontend/components/screen-embedding/ScreenSplitPanel.tsx +++ b/frontend/components/screen-embedding/ScreenSplitPanel.tsx @@ -33,6 +33,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp leftScreenId: config?.leftScreenId, rightScreenId: config?.rightScreenId, configSplitRatio, + parentDataMapping: config?.parentDataMapping, configKeys: config ? Object.keys(config) : [], }); @@ -125,6 +126,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp splitPanelId={splitPanelId} leftScreenId={config?.leftScreenId || null} rightScreenId={config?.rightScreenId || null} + parentDataMapping={config?.parentDataMapping || []} >
{/* ์ขŒ์ธก ํŒจ๋„ */} diff --git a/frontend/contexts/SplitPanelContext.tsx b/frontend/contexts/SplitPanelContext.tsx index bfb9610b..15f3e1f5 100644 --- a/frontend/contexts/SplitPanelContext.tsx +++ b/frontend/contexts/SplitPanelContext.tsx @@ -17,6 +17,15 @@ export interface SplitPanelDataReceiver { receiveData: (data: any[], mode: "append" | "replace" | "merge") => Promise; } +/** + * ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • + * ์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์šฐ์ธก ํ™”๋ฉด ์ €์žฅ ์‹œ ์ž๋™์œผ๋กœ ํฌํ•จ + */ +export interface ParentDataMapping { + sourceColumn: string; // ์ขŒ์ธก ํ™”๋ฉด์˜ ์ปฌ๋Ÿผ๋ช… (์˜ˆ: equipment_code) + targetColumn: string; // ์šฐ์ธก ํ™”๋ฉด ์ €์žฅ ์‹œ ์‚ฌ์šฉํ•  ์ปฌ๋Ÿผ๋ช… (์˜ˆ: equipment_code) +} + /** * ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ ๊ฐ’ */ @@ -54,6 +63,16 @@ interface SplitPanelContextValue { addItemIds: (ids: string[]) => void; removeItemIds: (ids: string[]) => void; clearItemIds: () => void; + + // ๐Ÿ†• ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ (์šฐ์ธก ํ™”๋ฉด ์ €์žฅ ์‹œ ๋ถ€๋ชจ ํ‚ค ์ „๋‹ฌ์šฉ) + selectedLeftData: Record | null; + setSelectedLeftData: (data: Record | null) => void; + + // ๐Ÿ†• ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • + parentDataMapping: ParentDataMapping[]; + + // ๐Ÿ†• ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์ธก ํ™”๋ฉด ์ €์žฅ ์‹œ ์‚ฌ์šฉ) + getMappedParentData: () => Record; } const SplitPanelContext = createContext(null); @@ -62,6 +81,7 @@ interface SplitPanelProviderProps { splitPanelId: string; leftScreenId: number | null; rightScreenId: number | null; + parentDataMapping?: ParentDataMapping[]; // ๐Ÿ†• ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • children: React.ReactNode; } @@ -72,6 +92,7 @@ export function SplitPanelProvider({ splitPanelId, leftScreenId, rightScreenId, + parentDataMapping = [], children, }: SplitPanelProviderProps) { // ์ขŒ์ธก/์šฐ์ธก ํ™”๋ฉด์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋งต @@ -83,6 +104,9 @@ export function SplitPanelProvider({ // ๐Ÿ†• ์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID ์ƒํƒœ const [addedItemIds, setAddedItemIds] = useState>(new Set()); + + // ๐Ÿ†• ์ขŒ์ธก์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ƒํƒœ + const [selectedLeftData, setSelectedLeftData] = useState | null>(null); /** * ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก @@ -232,6 +256,40 @@ export function SplitPanelProvider({ logger.debug(`[SplitPanelContext] ํ•ญ๋ชฉ ID ์ดˆ๊ธฐํ™”`); }, []); + /** + * ๐Ÿ†• ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ์„ค์ • + */ + const handleSetSelectedLeftData = useCallback((data: Record | null) => { + logger.info(`[SplitPanelContext] ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ์„ค์ •:`, { + hasData: !!data, + dataKeys: data ? Object.keys(data) : [], + }); + setSelectedLeftData(data); + }, []); + + /** + * ๐Ÿ†• ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + * ์šฐ์ธก ํ™”๋ฉด์—์„œ ์ €์žฅ ์‹œ ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ถ€๋ชจ ํ‚ค ๊ฐ’์„ ๊ฐ€์ ธ์˜ด + */ + const getMappedParentData = useCallback((): Record => { + if (!selectedLeftData || parentDataMapping.length === 0) { + return {}; + } + + const mappedData: Record = {}; + + for (const mapping of parentDataMapping) { + const value = selectedLeftData[mapping.sourceColumn]; + if (value !== undefined && value !== null) { + mappedData[mapping.targetColumn] = value; + logger.debug(`[SplitPanelContext] ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘: ${mapping.sourceColumn} โ†’ ${mapping.targetColumn} = ${value}`); + } + } + + logger.info(`[SplitPanelContext] ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ:`, mappedData); + return mappedData; + }, [selectedLeftData, parentDataMapping]); + // ๐Ÿ†• useMemo๋กœ value ๊ฐ์ฒด ๋ฉ”๋ชจ์ด์ œ์ด์…˜ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) const value = React.useMemo(() => ({ splitPanelId, @@ -247,6 +305,11 @@ export function SplitPanelProvider({ addItemIds, removeItemIds, clearItemIds, + // ๐Ÿ†• ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ๊ด€๋ จ + selectedLeftData, + setSelectedLeftData: handleSetSelectedLeftData, + parentDataMapping, + getMappedParentData, }), [ splitPanelId, leftScreenId, @@ -260,6 +323,10 @@ export function SplitPanelProvider({ addItemIds, removeItemIds, clearItemIds, + selectedLeftData, + handleSetSelectedLeftData, + parentDataMapping, + getMappedParentData, ]); return ( diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 180dacaa..564eed1d 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -692,6 +692,15 @@ export const ButtonPrimaryComponent: React.FC = ({ effectiveScreenId, }); + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์ธก ํ™”๋ฉด์—์„œ ์ €์žฅ ์‹œ ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ํฌํ•จ) + let splitPanelParentData: Record | undefined; + if (splitPanelContext && splitPanelPosition === "right") { + splitPanelParentData = splitPanelContext.getMappedParentData(); + if (Object.keys(splitPanelParentData).length > 0) { + console.log("๐Ÿ”— [ButtonPrimaryComponent] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ํฌํ•จ:", splitPanelParentData); + } + } + const context: ButtonActionContext = { formData: formData || {}, originalData: originalData, // ๐Ÿ”ง ๋นˆ ๊ฐ์ฒด ๋Œ€์‹  undefined ์œ ์ง€ (UPDATE ํŒ๋‹จ์— ์‚ฌ์šฉ) @@ -720,6 +729,8 @@ export const ButtonPrimaryComponent: React.FC = ({ flowSelectedStepId, // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ๋ณ„ ์„ค์ • (parentDataMapping ๋“ฑ) componentConfigs, + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ (์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ) + splitPanelParentData, } as ButtonActionContext; // ํ™•์ธ์ด ํ•„์š”ํ•œ ์•ก์…˜์ธ์ง€ ํ™•์ธ diff --git a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx index 0912afd7..094ddf70 100644 --- a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx +++ b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx @@ -43,6 +43,9 @@ export const CardDisplayComponent: React.FC = ({ const [loadedTableColumns, setLoadedTableColumns] = useState([]); const [loading, setLoading] = useState(false); + // ์„ ํƒ๋œ ์นด๋“œ ์ƒํƒœ + const [selectedCardId, setSelectedCardId] = useState(null); + // ์ƒ์„ธ๋ณด๊ธฐ ๋ชจ๋‹ฌ ์ƒํƒœ const [viewModalOpen, setViewModalOpen] = useState(false); const [selectedData, setSelectedData] = useState(null); @@ -261,26 +264,19 @@ export const CardDisplayComponent: React.FC = ({ borderRadius: "12px", // ์ปจํ…Œ์ด๋„ˆ ์ž์ฒด๋„ ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ }; - // ์นด๋“œ ์Šคํƒ€์ผ - ํ†ต์ผ๋œ ๋””์ž์ธ ์‹œ์Šคํ…œ ์ ์šฉ + // ์นด๋“œ ์Šคํƒ€์ผ - ์ปดํŒฉํŠธํ•œ ๋””์ž์ธ const cardStyle: React.CSSProperties = { backgroundColor: "white", - border: "2px solid #e5e7eb", // ๋” ๋ช…ํ™•ํ•œ ํ…Œ๋‘๋ฆฌ - borderRadius: "12px", // ํ†ต์ผ๋œ ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ - padding: "24px", // ๋” ์—ฌ์œ ๋กœ์šด ํŒจ๋”ฉ - boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", // ๋” ๊นŠ์€ ๊ทธ๋ฆผ์ž - transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // ๋ถ€๋“œ๋Ÿฌ์šด ํŠธ๋žœ์ง€์…˜ + border: "1px solid #e5e7eb", + borderRadius: "8px", + padding: "16px", + boxShadow: "0 1px 3px rgba(0, 0, 0, 0.08)", + transition: "all 0.2s ease", overflow: "hidden", display: "flex", flexDirection: "column", position: "relative", - minHeight: "240px", // ์ตœ์†Œ ๋†’์ด ๋” ์ฆ๊ฐ€ cursor: isDesignMode ? "pointer" : "default", - // ํ˜ธ๋ฒ„ ํšจ๊ณผ๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ์Šคํƒ€์ผ - "&:hover": { - transform: "translateY(-2px)", - boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", - borderColor: "#f59e0b", // ํ˜ธ๋ฒ„ ์‹œ ์˜ค๋ Œ์ง€ ํ…Œ๋‘๋ฆฌ - } }; // ํ…์ŠคํŠธ ์ž๋ฅด๊ธฐ ํ•จ์ˆ˜ @@ -328,6 +324,14 @@ export const CardDisplayComponent: React.FC = ({ }; const handleCardClick = (data: any) => { + const cardId = data.id || data.objid || data.ID; + // ์ด๋ฏธ ์„ ํƒ๋œ ์นด๋“œ๋ฅผ ๋‹ค์‹œ ํด๋ฆญํ•˜๋ฉด ์„ ํƒ ํ•ด์ œ + if (selectedCardId === cardId) { + setSelectedCardId(null); + } else { + setSelectedCardId(cardId); + } + if (componentConfig.onCardClick) { componentConfig.onCardClick(data); } @@ -421,67 +425,75 @@ export const CardDisplayComponent: React.FC = ({ ? getColumnValue(data, componentConfig.columnMapping.imageColumn) : data.avatar || data.image || ""; + const cardId = data.id || data.objid || data.ID || index; + const isCardSelected = selectedCardId === cardId; + return (
handleCardClick(data)} > - {/* ์นด๋“œ ์ด๋ฏธ์ง€ - ํ†ต์ผ๋œ ๋””์ž์ธ */} + {/* ์นด๋“œ ์ด๋ฏธ์ง€ */} {componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && ( -
-
- ๐Ÿ‘ค +
+
+ ๐Ÿ‘ค
)} - {/* ์นด๋“œ ํƒ€์ดํ‹€ - ํ†ต์ผ๋œ ๋””์ž์ธ */} - {componentConfig.cardStyle?.showTitle && ( -
-

{titleValue}

+ {/* ์นด๋“œ ํƒ€์ดํ‹€ + ์„œ๋ธŒํƒ€์ดํ‹€ (๊ฐ€๋กœ ๋ฐฐ์น˜) */} + {(componentConfig.cardStyle?.showTitle || componentConfig.cardStyle?.showSubtitle) && ( +
+ {componentConfig.cardStyle?.showTitle && ( +

{titleValue}

+ )} + {componentConfig.cardStyle?.showSubtitle && subtitleValue && ( + {subtitleValue} + )}
)} - {/* ์นด๋“œ ์„œ๋ธŒํƒ€์ดํ‹€ - ํ†ต์ผ๋œ ๋””์ž์ธ */} - {componentConfig.cardStyle?.showSubtitle && ( -
-

{subtitleValue}

-
- )} - - {/* ์นด๋“œ ์„ค๋ช… - ํ†ต์ผ๋œ ๋””์ž์ธ */} + {/* ์นด๋“œ ์„ค๋ช… */} {componentConfig.cardStyle?.showDescription && ( -
-

+

+

{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}

)} - {/* ์ถ”๊ฐ€ ํ‘œ์‹œ ์ปฌ๋Ÿผ๋“ค - ํ†ต์ผ๋œ ๋””์ž์ธ */} + {/* ์ถ”๊ฐ€ ํ‘œ์‹œ ์ปฌ๋Ÿผ๋“ค - ๊ฐ€๋กœ ๋ฐฐ์น˜ */} {componentConfig.columnMapping?.displayColumns && componentConfig.columnMapping.displayColumns.length > 0 && ( -
+
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => { const value = getColumnValue(data, columnName); if (!value) return null; return ( -
- {getColumnLabel(columnName)}: - {value} +
+ {getColumnLabel(columnName)}: + {value}
); })}
)} - {/* ์นด๋“œ ์•ก์…˜ (์„ ํƒ์‚ฌํ•ญ) */} -
+ {/* ์นด๋“œ ์•ก์…˜ */} +
+
+ ))} +
+ + {/* ๋งคํ•‘ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + + + {/* ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ */} +
+

+ ์‚ฌ์šฉ ์˜ˆ์‹œ: +
+ ์ขŒ์ธก: ์„ค๋น„ ๋ชฉ๋ก (equipment_mng) +
+ ์šฐ์ธก: ์ ๊ฒ€ํ•ญ๋ชฉ ์ถ”๊ฐ€ ํ™”๋ฉด +
+
+ ๋งคํ•‘ ์„ค์ •: +
+ - ์†Œ์Šค: equipment_code โ†’ ํƒ€๊ฒŸ: equipment_code +
+
+ ์ขŒ์ธก์—์„œ ์„ค๋น„๋ฅผ ์„ ํƒํ•˜๊ณ  ์šฐ์ธก์—์„œ ์ ๊ฒ€ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•˜๋ฉด, + ์„ ํƒํ•œ ์„ค๋น„์˜ equipment_code๊ฐ€ ์ž๋™์œผ๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. +

+
+ + )} + + + {/* ์„ค์ • ์š”์•ฝ */} @@ -343,6 +534,14 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl ํฌ๊ธฐ ์กฐ์ ˆ: {localConfig.resizable ? "๊ฐ€๋Šฅ" : "๋ถˆ๊ฐ€๋Šฅ"}
+
+ ๋ฐ์ดํ„ฐ ๋งคํ•‘: + + {(localConfig.parentDataMapping || []).length > 0 + ? `${localConfig.parentDataMapping.length}๊ฐœ ์„ค์ •` + : "๋ฏธ์„ค์ •"} + +
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 841e6f0a..a643e3a9 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -1466,6 +1466,22 @@ export const TableListComponent: React.FC = ({ handleRowSelection(rowKey, !isCurrentlySelected); + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ์— ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ (์ขŒ์ธก ํ™”๋ฉด์ธ ๊ฒฝ์šฐ) + if (splitPanelContext && splitPanelPosition === "left") { + if (!isCurrentlySelected) { + // ์„ ํƒ๋œ ๊ฒฝ์šฐ: ๋ฐ์ดํ„ฐ ์ €์žฅ + splitPanelContext.setSelectedLeftData(row); + console.log("๐Ÿ”— [TableList] ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์ €์žฅ:", { + row, + parentDataMapping: splitPanelContext.parentDataMapping, + }); + } else { + // ์„ ํƒ ํ•ด์ œ๋œ ๊ฒฝ์šฐ: ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + splitPanelContext.setSelectedLeftData(null); + console.log("๐Ÿ”— [TableList] ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”"); + } + } + console.log("ํ–‰ ํด๋ฆญ:", { row, index, isSelected: !isCurrentlySelected }); }; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index ad441754..2b8864d8 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -215,6 +215,9 @@ export interface ButtonActionContext { // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ๋ณ„ ์„ค์ • (parentDataMapping ๋“ฑ) componentConfigs?: Record; // ์ปดํฌ๋„ŒํŠธ ID โ†’ ์ปดํฌ๋„ŒํŠธ ์„ค์ • + + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ (์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ) + splitPanelParentData?: Record; } /** @@ -502,8 +505,15 @@ export class ButtonActionExecutor { // console.log("โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); // console.log("๐Ÿ“ฆ ์ตœ์ข… formData:", JSON.stringify(formData, null, 2)); + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ) + const splitPanelData = context.splitPanelParentData || {}; + if (Object.keys(splitPanelData).length > 0) { + console.log("๐Ÿ”— [handleSave] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ:", splitPanelData); + } + const dataWithUserInfo = { - ...formData, + ...splitPanelData, // ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋จผ์ € ์ ์šฉ + ...formData, // ํผ ๋ฐ์ดํ„ฐ๊ฐ€ ์šฐ์„  (๋ฎ์–ด์“ฐ๊ธฐ ๊ฐ€๋Šฅ) writer: formData.writer || writerValue, // โœ… ์ž…๋ ฅ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด userId created_by: writerValue, // created_by๋Š” ํ•ญ์ƒ ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ updated_by: writerValue, // updated_by๋Š” ํ•ญ์ƒ ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ From 3b875f20b15ee0f63652907ab0f20b4d8a98a609 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 2 Dec 2025 18:03:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=ED=99=94=EB=A9=B4=EA=B0=84=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=84=EB=8B=AC=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/entitySearchController.ts | 88 +++- frontend/components/common/ScreenModal.tsx | 9 +- .../screen-embedding/EmbeddedScreen.tsx | 15 + .../screen-embedding/ScreenSplitPanel.tsx | 1 + .../screen/InteractiveDataTable.tsx | 68 ++- .../screen/InteractiveScreenViewerDynamic.tsx | 28 +- .../table-category/CategoryValueAddDialog.tsx | 25 +- frontend/contexts/SplitPanelContext.tsx | 72 ++- .../lib/registry/DynamicComponentRenderer.tsx | 8 +- .../button-primary/ButtonPrimaryComponent.tsx | 18 +- .../card-display/CardDisplayComponent.tsx | 152 ++++-- .../ScreenSplitPanelConfigPanel.tsx | 464 +++++++++++++++--- .../table-list/TableListComponent.tsx | 67 ++- frontend/lib/utils/buttonActions.ts | 42 +- 14 files changed, 886 insertions(+), 171 deletions(-) diff --git a/backend-node/src/controllers/entitySearchController.ts b/backend-node/src/controllers/entitySearchController.ts index 880c54fc..4d911c57 100644 --- a/backend-node/src/controllers/entitySearchController.ts +++ b/backend-node/src/controllers/entitySearchController.ts @@ -32,10 +32,32 @@ export async function searchEntity(req: AuthenticatedRequest, res: Response) { const companyCode = req.user!.companyCode; // ๊ฒ€์ƒ‰ ํ•„๋“œ ํŒŒ์‹ฑ - const fields = searchFields + const requestedFields = searchFields ? (searchFields as string).split(",").map((f) => f.trim()) : []; + // ๐Ÿ†• ํ…Œ์ด๋ธ”์˜ ์‹ค์ œ ์ปฌ๋Ÿผ ๋ชฉ๋ก ์กฐํšŒ + const pool = getPool(); + const columnsResult = await pool.query( + `SELECT column_name FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1`, + [tableName] + ); + const existingColumns = new Set(columnsResult.rows.map((r: any) => r.column_name)); + + // ๐Ÿ†• ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ ํ•„ํ„ฐ๋ง + const fields = requestedFields.filter((field) => { + if (existingColumns.has(field)) { + return true; + } else { + logger.warn(`์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰: ํ…Œ์ด๋ธ” "${tableName}"์— ์ปฌ๋Ÿผ "${field}"์ด(๊ฐ€) ์กด์žฌํ•˜์ง€ ์•Š์•„ ์ œ์™ธ`); + return false; + } + }); + + const existingColumnsArray = Array.from(existingColumns); + logger.info(`์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ ํ•„๋“œ ํ™•์ธ - ํ…Œ์ด๋ธ”: ${tableName}, ์š”์ฒญํ•„๋“œ: [${requestedFields.join(", ")}], ์œ ํšจํ•„๋“œ: [${fields.join(", ")}], ํ…Œ์ด๋ธ”์ปฌ๋Ÿผ(์ƒ˜ํ”Œ): [${existingColumnsArray.slice(0, 10).join(", ")}]`); + // WHERE ์กฐ๊ฑด ์ƒ์„ฑ const whereConditions: string[] = []; const params: any[] = []; @@ -43,32 +65,57 @@ export async function searchEntity(req: AuthenticatedRequest, res: Response) { // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง if (companyCode !== "*") { - whereConditions.push(`company_code = $${paramIndex}`); - params.push(companyCode); - paramIndex++; + // ๐Ÿ†• company_code ์ปฌ๋Ÿผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ•„ํ„ฐ๋ง + if (existingColumns.has("company_code")) { + whereConditions.push(`company_code = $${paramIndex}`); + params.push(companyCode); + paramIndex++; + } } // ๊ฒ€์ƒ‰ ์กฐ๊ฑด - if (searchText && fields.length > 0) { - const searchConditions = fields.map((field) => { - const condition = `${field}::text ILIKE $${paramIndex}`; - paramIndex++; - return condition; - }); - whereConditions.push(`(${searchConditions.join(" OR ")})`); + if (searchText) { + // ์œ ํšจํ•œ ๊ฒ€์ƒ‰ ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ํ…์ŠคํŠธ ์ปฌ๋Ÿผ์—์„œ ๊ฒ€์ƒ‰ + let searchableFields = fields; + if (searchableFields.length === 0) { + // ๊ธฐ๋ณธ ๊ฒ€์ƒ‰ ์ปฌ๋Ÿผ: name, code, description ๋“ฑ ์ผ๋ฐ˜์ ์ธ ์ปฌ๋Ÿผ๋ช… + const defaultSearchColumns = [ + 'name', 'code', 'description', 'title', 'label', + 'item_name', 'item_code', 'item_number', + 'equipment_name', 'equipment_code', + 'inspection_item', 'consumable_name', // ์†Œ๋ชจํ’ˆ๋ช… ์ถ”๊ฐ€ + 'supplier_name', 'customer_name', 'product_name', + ]; + searchableFields = defaultSearchColumns.filter(col => existingColumns.has(col)); + + logger.info(`์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰: ๊ธฐ๋ณธ ๊ฒ€์ƒ‰ ํ•„๋“œ ์‚ฌ์šฉ - ํ…Œ์ด๋ธ”: ${tableName}, ๊ฒ€์ƒ‰ํ•„๋“œ: [${searchableFields.join(", ")}]`); + } + + if (searchableFields.length > 0) { + const searchConditions = searchableFields.map((field) => { + const condition = `${field}::text ILIKE $${paramIndex}`; + paramIndex++; + return condition; + }); + whereConditions.push(`(${searchConditions.join(" OR ")})`); - // ๊ฒ€์ƒ‰์–ด ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ - fields.forEach(() => { - params.push(`%${searchText}%`); - }); + // ๊ฒ€์ƒ‰์–ด ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + searchableFields.forEach(() => { + params.push(`%${searchText}%`); + }); + } } - // ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด + // ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด (์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ) const additionalFilter = JSON.parse(filterCondition as string); for (const [key, value] of Object.entries(additionalFilter)) { - whereConditions.push(`${key} = $${paramIndex}`); - params.push(value); - paramIndex++; + if (existingColumns.has(key)) { + whereConditions.push(`${key} = $${paramIndex}`); + params.push(value); + paramIndex++; + } else { + logger.warn("์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰: ํ•„ํ„ฐ ์กฐ๊ฑด์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ ์ œ์™ธ", { tableName, key }); + } } // ํŽ˜์ด์ง• @@ -78,8 +125,7 @@ export async function searchEntity(req: AuthenticatedRequest, res: Response) { ? `WHERE ${whereConditions.join(" AND ")}` : ""; - // ์ฟผ๋ฆฌ ์‹คํ–‰ - const pool = getPool(); + // ์ฟผ๋ฆฌ ์‹คํ–‰ (pool์€ ์œ„์—์„œ ์ด๋ฏธ ์„ ์–ธ๋จ) const countQuery = `SELECT COUNT(*) FROM ${tableName} ${whereClause}`; const dataQuery = ` SELECT * FROM ${tableName} ${whereClause} diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 0713c1c3..f5553572 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -131,7 +131,7 @@ export const ScreenModal: React.FC = ({ className }) => { // ์ „์—ญ ๋ชจ๋‹ฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { const handleOpenModal = (event: CustomEvent) => { - const { screenId, title, description, size, urlParams, editData } = event.detail; + const { screenId, title, description, size, urlParams, editData, splitPanelParentData } = event.detail; // ๐Ÿ†• ๋ชจ๋‹ฌ ์—ด๋ฆฐ ์‹œ๊ฐ„ ๊ธฐ๋ก modalOpenedAtRef.current = Date.now(); @@ -155,7 +155,12 @@ export const ScreenModal: React.FC = ({ className }) => { setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) } else { // ๐Ÿ†• ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฏธ๋ฆฌ ์„ค์ • - const parentData = splitPanelContext?.getMappedParentData() || {}; + // 1์ˆœ์œ„: ์ด๋ฒคํŠธ๋กœ ์ „๋‹ฌ๋œ splitPanelParentData (ํƒญ ์•ˆ์—์„œ ์—ด๋ฆฐ ๋ชจ๋‹ฌ) + // 2์ˆœ์œ„: splitPanelContext์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ (๋ถ„ํ•  ํŒจ๋„ ๋‚ด์—์„œ ์—ด๋ฆฐ ๋ชจ๋‹ฌ) + const parentData = splitPanelParentData && Object.keys(splitPanelParentData).length > 0 + ? splitPanelParentData + : (splitPanelContext?.getMappedParentData() || {}); + if (Object.keys(parentData).length > 0) { console.log("๐Ÿ”— [ScreenModal] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐ๊ฐ’ ์„ค์ •:", parentData); setFormData(parentData); diff --git a/frontend/components/screen-embedding/EmbeddedScreen.tsx b/frontend/components/screen-embedding/EmbeddedScreen.tsx index ce7030eb..3880fc54 100644 --- a/frontend/components/screen-embedding/EmbeddedScreen.tsx +++ b/frontend/components/screen-embedding/EmbeddedScreen.tsx @@ -91,6 +91,21 @@ export const EmbeddedScreen = forwardRef { + // ์šฐ์ธก ํ™”๋ฉด์ธ ๊ฒฝ์šฐ์—๋งŒ ์ ์šฉ + if (position !== "right" || !splitPanelContext) return; + + const mappedData = splitPanelContext.getMappedParentData(); + if (Object.keys(mappedData).length > 0) { + console.log("๐Ÿ”— [EmbeddedScreen] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ์ž๋™ ๋ฐ˜์˜:", mappedData); + setFormData((prev) => ({ + ...prev, + ...mappedData, + })); + } + }, [position, splitPanelContext, splitPanelContext?.selectedLeftData]); + // ์„ ํƒ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ „ํŒŒ useEffect(() => { onSelectionChanged?.(selectedRows); diff --git a/frontend/components/screen-embedding/ScreenSplitPanel.tsx b/frontend/components/screen-embedding/ScreenSplitPanel.tsx index 4eba4f9b..60b6bf24 100644 --- a/frontend/components/screen-embedding/ScreenSplitPanel.tsx +++ b/frontend/components/screen-embedding/ScreenSplitPanel.tsx @@ -127,6 +127,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp leftScreenId={config?.leftScreenId || null} rightScreenId={config?.rightScreenId || null} parentDataMapping={config?.parentDataMapping || []} + linkedFilters={config?.linkedFilters || []} >
{/* ์ขŒ์ธก ํŒจ๋„ */} diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 1119e698..88d11447 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -54,6 +54,7 @@ import { SaveModal } from "./SaveModal"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility } from "@/types/table-options"; +import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; // ํŒŒ์ผ ๋ฐ์ดํ„ฐ ํƒ€์ž… ์ •์˜ (AttachedFileInfo์™€ ํ˜ธํ™˜) interface FileInfo { @@ -105,6 +106,7 @@ export const InteractiveDataTable: React.FC = ({ const { isPreviewMode } = useScreenPreview(); // ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ํ™•์ธ const { user } = useAuth(); // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ const { registerTable, unregisterTable } = useTableOptions(); // Context ํ›… + const splitPanelContext = useSplitPanelContext(); // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); @@ -575,12 +577,72 @@ export const InteractiveDataTable: React.FC = ({ setLoading(true); try { - console.log("๐Ÿ” ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ์ž‘:", { tableName: component.tableName, page, pageSize }); + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์ผ ๋•Œ) + let linkedFilterValues: Record = {}; + let hasLinkedFiltersConfigured = false; // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ + let hasSelectedLeftData = false; // ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€ + + if (splitPanelContext) { + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • ์—ฌ๋ถ€ ํ™•์ธ (ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€) + const linkedFiltersConfig = splitPanelContext.linkedFilters || []; + hasLinkedFiltersConfigured = linkedFiltersConfig.some( + (filter) => filter.targetColumn?.startsWith(component.tableName + ".") || + filter.targetColumn === component.tableName + ); + + // ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์„ ํƒ ์—ฌ๋ถ€ ํ™•์ธ + hasSelectedLeftData = splitPanelContext.selectedLeftData && + Object.keys(splitPanelContext.selectedLeftData).length > 0; + + linkedFilterValues = splitPanelContext.getLinkedFilterValues(); + // ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๋งŒ ์ถ”์ถœ (ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… ํ˜•์‹์—์„œ) + const tableSpecificFilters: Record = {}; + for (const [key, value] of Object.entries(linkedFilterValues)) { + // key๊ฐ€ "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•์‹์ธ ๊ฒฝ์šฐ + if (key.includes(".")) { + const [tableName, columnName] = key.split("."); + if (tableName === component.tableName) { + tableSpecificFilters[columnName] = value; + hasLinkedFiltersConfigured = true; // ์ด ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ํ•„ํ„ฐ๊ฐ€ ์žˆ์Œ + } + } else { + // ํ…Œ์ด๋ธ”๋ช… ์—†์ด ์ปฌ๋Ÿผ๋ช…๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + tableSpecificFilters[key] = value; + } + } + linkedFilterValues = tableSpecificFilters; + } + + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€๋งŒ ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + // โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ (๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Œ) + if (hasLinkedFiltersConfigured && !hasSelectedLeftData) { + console.log("โš ๏ธ [InteractiveDataTable] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ •๋จ but ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋ฏธ์„ ํƒ โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ"); + setData([]); + setTotal(0); + setTotalPages(0); + setCurrentPage(1); + setLoading(false); + return; + } + + // ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๋ณ‘ํ•ฉ + const mergedSearchParams = { + ...searchParams, + ...linkedFilterValues, + }; + + console.log("๐Ÿ” ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ์ž‘:", { + tableName: component.tableName, + page, + pageSize, + linkedFilterValues, + mergedSearchParams, + }); const result = await tableTypeApi.getTableData(component.tableName, { page, size: pageSize, - search: searchParams, + search: mergedSearchParams, autoFilter: component.autoFilter, // ๐Ÿ†• ์ž๋™ ํ•„ํ„ฐ ์„ค์ • ์ „๋‹ฌ }); @@ -680,7 +742,7 @@ export const InteractiveDataTable: React.FC = ({ setLoading(false); } }, - [component.tableName, pageSize, component.autoFilter], // ๐Ÿ†• autoFilter ์ถ”๊ฐ€ + [component.tableName, pageSize, component.autoFilter, splitPanelContext?.selectedLeftData], // ๐Ÿ†• autoFilter, ์—ฐ๊ฒฐํ•„ํ„ฐ ์ถ”๊ฐ€ ); // ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋“œ diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index c9535285..3c9d16f5 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -19,6 +19,7 @@ import { FlowButtonGroup } from "./widgets/FlowButtonGroup"; import { FlowVisibilityConfig } from "@/types/control-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; +import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; // ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋Ÿฌ๋“ค์„ ๊ฐ•์ œ๋กœ ๋กœ๋“œํ•˜์—ฌ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋ก import "@/lib/registry/components/ButtonRenderer"; @@ -78,6 +79,7 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ํ™•์ธ const { userName: authUserName, user: authUser } = useAuth(); + const splitPanelContext = useSplitPanelContext(); // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ // ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (ScreenModal ๋“ฑ์—์„œ) const userName = externalUserName || authUserName; @@ -116,8 +118,30 @@ export const InteractiveScreenViewerDynamic: React.FC>({}); - // formData ๊ฒฐ์ • (์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ ๊ฒƒ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ) - const formData = externalFormData || localFormData; + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + const splitPanelMappedData = React.useMemo(() => { + if (splitPanelContext) { + return splitPanelContext.getMappedParentData(); + } + return {}; + }, [splitPanelContext, splitPanelContext?.selectedLeftData]); + + // formData ๊ฒฐ์ • (์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ ๊ฒƒ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ, ๋ถ„ํ•  ํŒจ๋„ ๋ฐ์ดํ„ฐ๋„ ๋ณ‘ํ•ฉ) + const formData = React.useMemo(() => { + const baseData = externalFormData || localFormData; + // ๋ถ„ํ•  ํŒจ๋„ ๋งคํ•‘ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณ‘ํ•ฉ (๊ธฐ์กด ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ) + if (Object.keys(splitPanelMappedData).length > 0) { + const merged = { ...baseData }; + for (const [key, value] of Object.entries(splitPanelMappedData)) { + // ๊ธฐ์กด ๊ฐ’์ด ์—†๊ฑฐ๋‚˜ ๋นˆ ๊ฐ’์ธ ๊ฒฝ์šฐ์—๋งŒ ๋งคํ•‘ ๋ฐ์ดํ„ฐ ์ ์šฉ + if (merged[key] === undefined || merged[key] === null || merged[key] === "") { + merged[key] = value; + } + } + return merged; + } + return baseData; + }, [externalFormData, localFormData, splitPanelMappedData]); // formData ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ const updateFormData = useCallback( diff --git a/frontend/components/table-category/CategoryValueAddDialog.tsx b/frontend/components/table-category/CategoryValueAddDialog.tsx index c486cc1d..9d962b22 100644 --- a/frontend/components/table-category/CategoryValueAddDialog.tsx +++ b/frontend/components/table-category/CategoryValueAddDialog.tsx @@ -52,23 +52,12 @@ export const CategoryValueAddDialog: React.FC< const [description, setDescription] = useState(""); const [color, setColor] = useState("none"); - // ๋ผ๋ฒจ์—์„œ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ - const generateCode = (label: string): string => { - // ํ•œ๊ธ€์„ ์˜๋ฌธ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๊ฑฐ๋‚˜, ์˜๋ฌธ/์ˆซ์ž๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋Œ€๋ฌธ์ž๋กœ - const cleaned = label - .replace(/[^a-zA-Z0-9๊ฐ€-ํžฃ\s]/g, "") // ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ - .trim() - .toUpperCase(); - - // ์˜๋ฌธ์ด ์žˆ์œผ๋ฉด ์˜๋ฌธ๋งŒ, ์—†์œผ๋ฉด ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ๋ฐ˜ - const englishOnly = cleaned.replace(/[^A-Z0-9\s]/g, "").replace(/\s+/g, "_"); - - if (englishOnly.length > 0) { - return englishOnly.substring(0, 20); // ์ตœ๋Œ€ 20์ž - } - - // ์˜๋ฌธ์ด ์—†์œผ๋ฉด CATEGORY_TIMESTAMP ํ˜•์‹ - return `CATEGORY_${Date.now().toString().slice(-6)}`; + // ๋ผ๋ฒจ์—์„œ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ (ํ•ญ์ƒ ๊ณ ์œ ํ•œ ์ฝ”๋“œ ์ƒ์„ฑ) + const generateCode = (): string => { + // ํ•ญ์ƒ CATEGORY_TIMESTAMP_RANDOM ํ˜•์‹์œผ๋กœ ๊ณ ์œ  ์ฝ”๋“œ ์ƒ์„ฑ + const timestamp = Date.now().toString().slice(-6); + const random = Math.random().toString(36).substring(2, 6).toUpperCase(); + return `CATEGORY_${timestamp}${random}`; }; const handleSubmit = () => { @@ -76,7 +65,7 @@ export const CategoryValueAddDialog: React.FC< return; } - const valueCode = generateCode(valueLabel); + const valueCode = generateCode(); onAdd({ tableName: "", // CategoryValueManager์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ๋จ diff --git a/frontend/contexts/SplitPanelContext.tsx b/frontend/contexts/SplitPanelContext.tsx index 15f3e1f5..99cccdd8 100644 --- a/frontend/contexts/SplitPanelContext.tsx +++ b/frontend/contexts/SplitPanelContext.tsx @@ -26,6 +26,15 @@ export interface ParentDataMapping { targetColumn: string; // ์šฐ์ธก ํ™”๋ฉด ์ €์žฅ ์‹œ ์‚ฌ์šฉํ•  ์ปฌ๋Ÿผ๋ช… (์˜ˆ: equipment_code) } +/** + * ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • + * ์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋กœ ์šฐ์ธก ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”์„ ์ž๋™ ํ•„ํ„ฐ๋ง + */ +export interface LinkedFilter { + sourceColumn: string; // ์ขŒ์ธก ํ™”๋ฉด์˜ ์ปฌ๋Ÿผ๋ช… (์˜ˆ: equipment_code) + targetColumn: string; // ์šฐ์ธก ํ™”๋ฉด ํ•„ํ„ฐ๋ง์— ์‚ฌ์šฉํ•  ์ปฌ๋Ÿผ๋ช… (์˜ˆ: equipment_code) +} + /** * ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ ๊ฐ’ */ @@ -73,6 +82,12 @@ interface SplitPanelContextValue { // ๐Ÿ†• ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์ธก ํ™”๋ฉด ์ €์žฅ ์‹œ ์‚ฌ์šฉ) getMappedParentData: () => Record; + + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • (์ขŒ์ธก ์„ ํƒ โ†’ ์šฐ์ธก ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง) + linkedFilters: LinkedFilter[]; + + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์ธก ํ…Œ์ด๋ธ” ์กฐํšŒ ์‹œ ์‚ฌ์šฉ) + getLinkedFilterValues: () => Record; } const SplitPanelContext = createContext(null); @@ -82,6 +97,7 @@ interface SplitPanelProviderProps { leftScreenId: number | null; rightScreenId: number | null; parentDataMapping?: ParentDataMapping[]; // ๐Ÿ†• ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • + linkedFilters?: LinkedFilter[]; // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • children: React.ReactNode; } @@ -93,6 +109,7 @@ export function SplitPanelProvider({ leftScreenId, rightScreenId, parentDataMapping = [], + linkedFilters = [], children, }: SplitPanelProviderProps) { // ์ขŒ์ธก/์šฐ์ธก ํ™”๋ฉด์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋งต @@ -270,26 +287,68 @@ export function SplitPanelProvider({ /** * ๐Ÿ†• ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ * ์šฐ์ธก ํ™”๋ฉด์—์„œ ์ €์žฅ ์‹œ ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ถ€๋ชจ ํ‚ค ๊ฐ’์„ ๊ฐ€์ ธ์˜ด + * + * ๋™์ž‘ ๋ฐฉ์‹: + * 1. ์ขŒ์ธก ๋ฐ์ดํ„ฐ์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์„ ์ž๋™์œผ๋กœ ์ „๋‹ฌ (๋™์ผํ•œ ์ปฌ๋Ÿผ๋ช…์ด๋ฉด ์ž๋™ ๋งคํ•‘) + * 2. ๋ช…์‹œ์  ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ์†Œ์Šคโ†’ํƒ€๊ฒŸ ๋ณ€ํ™˜ ์ ์šฉ (๋‹ค๋ฅธ ์ปฌ๋Ÿผ๋ช…์œผ๋กœ ๋งคํ•‘ ์‹œ) */ const getMappedParentData = useCallback((): Record => { - if (!selectedLeftData || parentDataMapping.length === 0) { + if (!selectedLeftData) { return {}; } const mappedData: Record = {}; + // 1๋‹จ๊ณ„: ์ขŒ์ธก ๋ฐ์ดํ„ฐ์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์„ ์ž๋™์œผ๋กœ ์ „๋‹ฌ (๋™์ผ ์ปฌ๋Ÿผ๋ช… ์ž๋™ ๋งคํ•‘) + for (const [key, value] of Object.entries(selectedLeftData)) { + if (value !== undefined && value !== null) { + mappedData[key] = value; + } + } + + // 2๋‹จ๊ณ„: ๋ช…์‹œ์  ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€ ์ ์šฉ (๋‹ค๋ฅธ ์ปฌ๋Ÿผ๋ช…์œผ๋กœ ๋ณ€ํ™˜) for (const mapping of parentDataMapping) { const value = selectedLeftData[mapping.sourceColumn]; if (value !== undefined && value !== null) { - mappedData[mapping.targetColumn] = value; - logger.debug(`[SplitPanelContext] ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘: ${mapping.sourceColumn} โ†’ ${mapping.targetColumn} = ${value}`); + // ์†Œ์Šค์™€ ํƒ€๊ฒŸ์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ ์ถ”๊ฐ€ ๋งคํ•‘ + if (mapping.sourceColumn !== mapping.targetColumn) { + mappedData[mapping.targetColumn] = value; + logger.debug(`[SplitPanelContext] ๋ช…์‹œ์  ๋งคํ•‘: ${mapping.sourceColumn} โ†’ ${mapping.targetColumn} = ${value}`); + } } } - logger.info(`[SplitPanelContext] ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ:`, mappedData); + logger.info(`[SplitPanelContext] ๋งคํ•‘๋œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ (์ž๋™+๋ช…์‹œ์ ):`, { + autoMappedKeys: Object.keys(selectedLeftData), + explicitMappings: parentDataMapping.length, + finalKeys: Object.keys(mappedData), + }); return mappedData; }, [selectedLeftData, parentDataMapping]); + /** + * ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + * ์šฐ์ธก ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” ์กฐํšŒ ์‹œ ์ด ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋ง + */ + const getLinkedFilterValues = useCallback((): Record => { + if (!selectedLeftData || linkedFilters.length === 0) { + return {}; + } + + const filterValues: Record = {}; + + for (const filter of linkedFilters) { + const value = selectedLeftData[filter.sourceColumn]; + if (value !== undefined && value !== null && value !== "") { + filterValues[filter.targetColumn] = value; + logger.debug(`[SplitPanelContext] ์—ฐ๊ฒฐ ํ•„ํ„ฐ: ${filter.sourceColumn} โ†’ ${filter.targetColumn} = ${value}`); + } + } + + logger.info(`[SplitPanelContext] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ฐ’:`, filterValues); + return filterValues; + }, [selectedLeftData, linkedFilters]); + // ๐Ÿ†• useMemo๋กœ value ๊ฐ์ฒด ๋ฉ”๋ชจ์ด์ œ์ด์…˜ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) const value = React.useMemo(() => ({ splitPanelId, @@ -310,6 +369,9 @@ export function SplitPanelProvider({ setSelectedLeftData: handleSetSelectedLeftData, parentDataMapping, getMappedParentData, + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ด€๋ จ + linkedFilters, + getLinkedFilterValues, }), [ splitPanelId, leftScreenId, @@ -327,6 +389,8 @@ export function SplitPanelProvider({ handleSetSelectedLeftData, parentDataMapping, getMappedParentData, + linkedFilters, + getLinkedFilterValues, ]); return ( diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 0ea687bf..c0e0c87e 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -374,6 +374,11 @@ export const DynamicComponentRenderer: React.FC = height: component.size?.height ? `${component.size.height}px` : component.style?.height, }; + // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ ์ปดํฌ๋„ŒํŠธ๋Š” componentConfig.tableName์„ ์‚ฌ์šฉํ•ด์•ผ ํ•จ (ํ™”๋ฉด ํ…Œ์ด๋ธ”์ด ์•„๋‹Œ ๊ฒ€์ƒ‰ ๋Œ€์ƒ ํ…Œ์ด๋ธ”) + const useConfigTableName = componentType === "entity-search-input" || + componentType === "autocomplete-search-input" || + componentType === "modal-repeater-table"; + const rendererProps = { component, isSelected, @@ -396,7 +401,8 @@ export const DynamicComponentRenderer: React.FC = formData, onFormDataChange, onChange: handleChange, // ๊ฐœ์„ ๋œ onChange ํ•ธ๋“ค๋Ÿฌ ์ „๋‹ฌ - tableName, + // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ ์ปดํฌ๋„ŒํŠธ๋Š” componentConfig.tableName ์œ ์ง€, ๊ทธ ์™ธ๋Š” ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช… ์‚ฌ์šฉ + tableName: useConfigTableName ? (component.componentConfig?.tableName || tableName) : tableName, menuId, // ๐Ÿ†• ๋ฉ”๋‰ด ID menuObjid, // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) selectedScreen, // ๐Ÿ†• ํ™”๋ฉด ์ •๋ณด diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 564eed1d..0bf8bea2 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -693,11 +693,21 @@ export const ButtonPrimaryComponent: React.FC = ({ }); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์ธก ํ™”๋ฉด์—์„œ ์ €์žฅ ์‹œ ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ํฌํ•จ) + // ์กฐ๊ฑด ์™„ํ™”: splitPanelContext๊ฐ€ ์žˆ๊ณ  selectedLeftData๊ฐ€ ์žˆ์œผ๋ฉด ๊ฐ€์ ธ์˜ด + // (ํƒญ ์•ˆ์—์„œ๋„ ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก) let splitPanelParentData: Record | undefined; - if (splitPanelContext && splitPanelPosition === "right") { - splitPanelParentData = splitPanelContext.getMappedParentData(); - if (Object.keys(splitPanelParentData).length > 0) { - console.log("๐Ÿ”— [ButtonPrimaryComponent] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ํฌํ•จ:", splitPanelParentData); + if (splitPanelContext) { + // ์šฐ์ธก ํ™”๋ฉด์ด๊ฑฐ๋‚˜, ํƒญ ์•ˆ์˜ ํ™”๋ฉด(splitPanelPosition์ด undefined)์ธ ๊ฒฝ์šฐ ๋ชจ๋‘ ์ฒ˜๋ฆฌ + // ์ขŒ์ธก ํ™”๋ฉด์ด ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ํฌํ•จ (์ขŒ์ธก์—์„œ ์ €์žฅ ์‹œ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ€๋ชจ๋กœ ํฌํ•จํ•˜๋ฉด ์•ˆ๋จ) + if (splitPanelPosition !== "left") { + splitPanelParentData = splitPanelContext.getMappedParentData(); + if (Object.keys(splitPanelParentData).length > 0) { + console.log("๐Ÿ”— [ButtonPrimaryComponent] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ํฌํ•จ:", { + splitPanelParentData, + splitPanelPosition, + isInTab: !splitPanelPosition, // splitPanelPosition์ด ์—†์œผ๋ฉด ํƒญ ์•ˆ + }); + } } } diff --git a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx index 094ddf70..8a78f674 100644 --- a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx +++ b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState, useMemo } from "react"; +import React, { useEffect, useState, useMemo, useCallback } from "react"; import { ComponentRendererProps } from "@/types/component"; import { CardDisplayConfig } from "./types"; import { tableTypeApi } from "@/lib/api/screen"; @@ -8,6 +8,9 @@ import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; +import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; +import { useModalDataStore } from "@/stores/modalDataStore"; export interface CardDisplayComponentProps extends ComponentRendererProps { config?: CardDisplayConfig; @@ -38,13 +41,18 @@ export const CardDisplayComponent: React.FC = ({ tableColumns = [], ...props }) => { + // ์ปจํ…์ŠคํŠธ (์„ ํƒ์  - ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋Š” ์—†์„ ์ˆ˜ ์žˆ์Œ) + const screenContext = useScreenContextOptional(); + const splitPanelContext = useSplitPanelContext(); + const splitPanelPosition = screenContext?.splitPanelPosition; + // ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ƒํƒœ ๊ด€๋ฆฌ const [loadedTableData, setLoadedTableData] = useState([]); const [loadedTableColumns, setLoadedTableColumns] = useState([]); const [loading, setLoading] = useState(false); - // ์„ ํƒ๋œ ์นด๋“œ ์ƒํƒœ - const [selectedCardId, setSelectedCardId] = useState(null); + // ์„ ํƒ๋œ ์นด๋“œ ์ƒํƒœ (Set์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ์™€ ๋™์ผํ•˜๊ฒŒ) + const [selectedRows, setSelectedRows] = useState>(new Set()); // ์ƒ์„ธ๋ณด๊ธฐ ๋ชจ๋‹ฌ ์ƒํƒœ const [viewModalOpen, setViewModalOpen] = useState(false); @@ -199,38 +207,132 @@ export const CardDisplayComponent: React.FC = ({ // ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ ๊ฒฐ์ • (๋กœ๋“œ๋œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์šฐ์„  ์‚ฌ์šฉ) const displayData = useMemo(() => { - // console.log("๐Ÿ“‹ CardDisplay: displayData ๊ฒฐ์ • ์ค‘", { - // dataSource: componentConfig.dataSource, - // loadedTableDataLength: loadedTableData.length, - // tableDataLength: tableData.length, - // staticDataLength: componentConfig.staticData?.length || 0, - // }); - // ๋กœ๋“œ๋œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ญ์ƒ ์šฐ์„  ์‚ฌ์šฉ (dataSource ์„ค์ • ๋ฌด์‹œ) if (loadedTableData.length > 0) { - // console.log("๐Ÿ“‹ CardDisplay: ๋กœ๋“œ๋œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ", loadedTableData.slice(0, 2)); return loadedTableData; } // props๋กœ ์ „๋‹ฌ๋ฐ›์€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ if (tableData.length > 0) { - // console.log("๐Ÿ“‹ CardDisplay: props ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ", tableData.slice(0, 2)); return tableData; } if (componentConfig.staticData && componentConfig.staticData.length > 0) { - // console.log("๐Ÿ“‹ CardDisplay: ์ •์  ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ", componentConfig.staticData.slice(0, 2)); return componentConfig.staticData; } // ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ - // console.log("๐Ÿ“‹ CardDisplay: ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Œ"); return []; }, [componentConfig.dataSource, loadedTableData, tableData, componentConfig.staticData]); // ์‹ค์ œ ์‚ฌ์šฉํ•  ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด (๋กœ๋“œ๋œ ์ปฌ๋Ÿผ ์šฐ์„  ์‚ฌ์šฉ) const actualTableColumns = loadedTableColumns.length > 0 ? loadedTableColumns : tableColumns; + // ์นด๋“œ ID ๊ฐ€์ ธ์˜ค๊ธฐ ํ•จ์ˆ˜ (ํ›…์€ ์กฐ๊ธฐ ๋ฆฌํ„ด ์ „์— ์„ ์–ธ) + const getCardKey = useCallback((data: any, index: number): string => { + return String(data.id || data.objid || data.ID || index); + }, []); + + // ์นด๋“œ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ (ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ์™€ ๋™์ผํ•œ ๋กœ์ง) + const handleCardSelection = useCallback((cardKey: string, data: any, checked: boolean) => { + const newSelectedRows = new Set(selectedRows); + if (checked) { + newSelectedRows.add(cardKey); + } else { + newSelectedRows.delete(cardKey); + } + setSelectedRows(newSelectedRows); + + // ์„ ํƒ๋œ ์นด๋“œ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ + const selectedRowsData = displayData.filter((item, index) => + newSelectedRows.has(getCardKey(item, index)) + ); + + // onFormDataChange ํ˜ธ์ถœ + if (onFormDataChange) { + onFormDataChange({ + selectedRows: Array.from(newSelectedRows), + selectedRowsData, + }); + } + + // modalDataStore์— ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ + const tableNameToUse = componentConfig.dataSource?.tableName || tableName; + if (tableNameToUse && selectedRowsData.length > 0) { + const modalItems = selectedRowsData.map((row, idx) => ({ + id: getCardKey(row, idx), + originalData: row, + additionalData: {}, + })); + useModalDataStore.getState().setData(tableNameToUse, modalItems); + console.log("โœ… [CardDisplay] modalDataStore์— ๋ฐ์ดํ„ฐ ์ €์žฅ:", { + dataSourceId: tableNameToUse, + count: modalItems.length, + }); + } else if (tableNameToUse && selectedRowsData.length === 0) { + useModalDataStore.getState().clearData(tableNameToUse); + console.log("๐Ÿ—‘๏ธ [CardDisplay] modalDataStore ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ:", tableNameToUse); + } + + // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ์— ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ (์ขŒ์ธก ํ™”๋ฉด์ธ ๊ฒฝ์šฐ) + if (splitPanelContext && splitPanelPosition === "left") { + if (checked) { + splitPanelContext.setSelectedLeftData(data); + console.log("๐Ÿ”— [CardDisplay] ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์ €์žฅ:", { + data, + parentDataMapping: splitPanelContext.parentDataMapping, + }); + } else if (newSelectedRows.size === 0) { + splitPanelContext.setSelectedLeftData(null); + console.log("๐Ÿ”— [CardDisplay] ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”"); + } + } + }, [selectedRows, displayData, getCardKey, onFormDataChange, componentConfig.dataSource?.tableName, tableName, splitPanelContext, splitPanelPosition]); + + const handleCardClick = useCallback((data: any, index: number) => { + const cardKey = getCardKey(data, index); + const isCurrentlySelected = selectedRows.has(cardKey); + + // ์„ ํƒ ํ† ๊ธ€ + handleCardSelection(cardKey, data, !isCurrentlySelected); + + if (componentConfig.onCardClick) { + componentConfig.onCardClick(data); + } + }, [getCardKey, selectedRows, handleCardSelection, componentConfig.onCardClick]); + + // DataProvidable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ (ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ์™€ ๋™์ผ) + const dataProvider = useMemo(() => ({ + componentId: component.id, + componentType: "card-display" as const, + + getSelectedData: () => { + const selectedData = displayData.filter((item, index) => + selectedRows.has(getCardKey(item, index)) + ); + return selectedData; + }, + + getAllData: () => { + return displayData; + }, + + clearSelection: () => { + setSelectedRows(new Set()); + }, + }), [component.id, displayData, selectedRows, getCardKey]); + + // ScreenContext์— ๋ฐ์ดํ„ฐ ์ œ๊ณต์ž๋กœ ๋“ฑ๋ก + useEffect(() => { + if (screenContext && component.id) { + screenContext.registerDataProvider(component.id, dataProvider); + + return () => { + screenContext.unregisterDataProvider(component.id); + }; + } + }, [screenContext, component.id, dataProvider]); + // ๋กœ๋”ฉ ์ค‘์ธ ๊ฒฝ์šฐ ๋กœ๋”ฉ ํ‘œ์‹œ if (loading) { return ( @@ -323,20 +425,6 @@ export const CardDisplayComponent: React.FC = ({ onClick?.(); }; - const handleCardClick = (data: any) => { - const cardId = data.id || data.objid || data.ID; - // ์ด๋ฏธ ์„ ํƒ๋œ ์นด๋“œ๋ฅผ ๋‹ค์‹œ ํด๋ฆญํ•˜๋ฉด ์„ ํƒ ํ•ด์ œ - if (selectedCardId === cardId) { - setSelectedCardId(null); - } else { - setSelectedCardId(cardId); - } - - if (componentConfig.onCardClick) { - componentConfig.onCardClick(data); - } - }; - // DOM ์•ˆ์ „ํ•œ props๋งŒ ํ•„ํ„ฐ๋ง (filterDOMProps ์œ ํ‹ธ๋ฆฌํ‹ฐ ์‚ฌ์šฉ) const safeDomProps = filterDOMProps(props); @@ -425,12 +513,12 @@ export const CardDisplayComponent: React.FC = ({ ? getColumnValue(data, componentConfig.columnMapping.imageColumn) : data.avatar || data.image || ""; - const cardId = data.id || data.objid || data.ID || index; - const isCardSelected = selectedCardId === cardId; + const cardKey = getCardKey(data, index); + const isCardSelected = selectedRows.has(cardKey); return (
= ({ : "0 1px 3px rgba(0, 0, 0, 0.08)", }} className="card-hover group cursor-pointer transition-all duration-150" - onClick={() => handleCardClick(data)} + onClick={() => handleCardClick(data, index)} > {/* ์นด๋“œ ์ด๋ฏธ์ง€ */} {componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && ( diff --git a/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelConfigPanel.tsx b/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelConfigPanel.tsx index 63f074d3..b8a1d3dc 100644 --- a/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelConfigPanel.tsx +++ b/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelConfigPanel.tsx @@ -15,7 +15,7 @@ import { getTableColumns } from "@/lib/api/tableManagement"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; -import type { ParentDataMapping } from "@/contexts/SplitPanelContext"; +import type { ParentDataMapping, LinkedFilter } from "@/contexts/SplitPanelContext"; interface ScreenSplitPanelConfigPanelProps { config: any; @@ -33,7 +33,15 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl // ์ขŒ์ธก ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก const [leftScreenColumns, setLeftScreenColumns] = useState>([]); - const [isLoadingColumns, setIsLoadingColumns] = useState(false); + const [isLoadingLeftColumns, setIsLoadingLeftColumns] = useState(false); + + // ์šฐ์ธก ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก (ํ…Œ์ด๋ธ”๋ณ„๋กœ ๊ทธ๋ฃนํ™”) + const [rightScreenTables, setRightScreenTables] = useState + }>>([]); + const [isLoadingRightColumns, setIsLoadingRightColumns] = useState(false); const [localConfig, setLocalConfig] = useState({ screenId: config.screenId || 0, @@ -44,6 +52,7 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl buttonLabel: config.buttonLabel || "๋ฐ์ดํ„ฐ ์ „๋‹ฌ", buttonPosition: config.buttonPosition || "center", parentDataMapping: config.parentDataMapping || [] as ParentDataMapping[], + linkedFilters: config.linkedFilters || [] as LinkedFilter[], ...config, }); @@ -59,6 +68,7 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl buttonLabel: config.buttonLabel || "๋ฐ์ดํ„ฐ ์ „๋‹ฌ", buttonPosition: config.buttonPosition || "center", parentDataMapping: config.parentDataMapping || [], + linkedFilters: config.linkedFilters || [], ...config, }); }, [config]); @@ -72,7 +82,7 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl } try { - setIsLoadingColumns(true); + setIsLoadingLeftColumns(true); // ์ขŒ์ธก ํ™”๋ฉด ์ •๋ณด ์กฐํšŒ const screenData = await screenApi.getScreen(localConfig.leftScreenId); @@ -96,13 +106,126 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl console.error("์ขŒ์ธก ํ™”๋ฉด ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); setLeftScreenColumns([]); } finally { - setIsLoadingColumns(false); + setIsLoadingLeftColumns(false); } }; loadLeftScreenColumns(); }, [localConfig.leftScreenId]); + // ์šฐ์ธก ํ™”๋ฉด์ด ๋ณ€๊ฒฝ๋˜๋ฉด ํ•ด๋‹น ํ™”๋ฉด ๋ฐ ์ž„๋ฒ ๋“œ๋œ ํ™”๋ฉด๋“ค์˜ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + const loadRightScreenColumns = async () => { + if (!localConfig.rightScreenId) { + setRightScreenTables([]); + return; + } + + try { + setIsLoadingRightColumns(true); + const tables: Array<{ tableName: string; screenName: string; columns: Array<{ columnName: string; columnLabel: string }> }> = []; + + // ์šฐ์ธก ํ™”๋ฉด ์ •๋ณด ์กฐํšŒ + const screenData = await screenApi.getScreen(localConfig.rightScreenId); + + // 1. ๋ฉ”์ธ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” (์žˆ๋Š” ๊ฒฝ์šฐ) + if (screenData?.tableName) { + const columnsResponse = await getTableColumns(screenData.tableName); + if (columnsResponse.success && columnsResponse.data?.columns) { + tables.push({ + tableName: screenData.tableName, + screenName: screenData.screenName || "๋ฉ”์ธ ํ™”๋ฉด", + columns: columnsResponse.data.columns.map((col: any) => ({ + columnName: col.column_name || col.columnName, + columnLabel: col.column_label || col.columnLabel || col.column_name || col.columnName, + })), + }); + } + } + + // 2. ๋ ˆ์ด์•„์›ƒ์—์„œ ์ž„๋ฒ ๋“œ๋œ ํ™”๋ฉด๋“ค์˜ ํ…Œ์ด๋ธ” ์ฐพ๊ธฐ (ํƒญ, ๋ถ„ํ•  ํŒจ๋„ ๋“ฑ) + const layoutData = await screenApi.getLayout(localConfig.rightScreenId); + const components = layoutData?.components || []; + + if (components.length > 0) { + const embeddedScreenIds = new Set(); + + // ์ปดํฌ๋„ŒํŠธ์—์„œ ์ž„๋ฒ ๋“œ๋œ ํ™”๋ฉด ID ์ˆ˜์ง‘ + const findEmbeddedScreens = (comps: any[]) => { + for (const comp of comps) { + const config = comp.componentConfig || {}; + + // TabsWidget์˜ ํƒญ๋“ค + if (comp.componentType === "tabs-widget" && config.tabs) { + for (const tab of config.tabs) { + if (tab.screenId) { + embeddedScreenIds.add(tab.screenId); + console.log("๐Ÿ” ํƒญ์—์„œ ํ™”๋ฉด ๋ฐœ๊ฒฌ:", tab.screenId, tab.screenName); + } + } + } + + // ScreenSplitPanel + if (comp.componentType === "screen-split-panel") { + if (config.leftScreenId) embeddedScreenIds.add(config.leftScreenId); + if (config.rightScreenId) embeddedScreenIds.add(config.rightScreenId); + } + + // EmbeddedScreen + if (comp.componentType === "embedded-screen" && config.screenId) { + embeddedScreenIds.add(config.screenId); + } + + // ์ค‘์ฒฉ๋œ ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ƒ‰ + if (comp.children) { + findEmbeddedScreens(comp.children); + } + } + }; + + findEmbeddedScreens(components); + console.log("๐Ÿ“‹ ๋ฐœ๊ฒฌ๋œ ์ž„๋ฒ ๋“œ ํ™”๋ฉด ID:", Array.from(embeddedScreenIds)); + + // ์ž„๋ฒ ๋“œ๋œ ํ™”๋ฉด๋“ค์˜ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + for (const embeddedScreenId of embeddedScreenIds) { + try { + const embeddedScreen = await screenApi.getScreen(embeddedScreenId); + if (embeddedScreen?.tableName) { + // ์ด๋ฏธ ์ถ”๊ฐ€๋œ ํ…Œ์ด๋ธ”์ธ์ง€ ํ™•์ธ + if (!tables.find(t => t.tableName === embeddedScreen.tableName)) { + const columnsResponse = await getTableColumns(embeddedScreen.tableName); + if (columnsResponse.success && columnsResponse.data?.columns) { + tables.push({ + tableName: embeddedScreen.tableName, + screenName: embeddedScreen.screenName || `ํ™”๋ฉด ${embeddedScreenId}`, + columns: columnsResponse.data.columns.map((col: any) => ({ + columnName: col.column_name || col.columnName, + columnLabel: col.column_label || col.columnLabel || col.column_name || col.columnName, + })), + }); + console.log("โœ… ํ…Œ์ด๋ธ” ์ถ”๊ฐ€:", embeddedScreen.tableName); + } + } + } + } catch (err) { + console.warn(`์ž„๋ฒ ๋“œ๋œ ํ™”๋ฉด ${embeddedScreenId} ๋กœ๋“œ ์‹คํŒจ:`, err); + } + } + } + + setRightScreenTables(tables); + console.log("๐Ÿ“‹ ์šฐ์ธก ํ™”๋ฉด ํ…Œ์ด๋ธ” ๋กœ๋“œ ์™„๋ฃŒ:", tables.map(t => t.tableName)); + } catch (error) { + console.error("์šฐ์ธก ํ™”๋ฉด ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); + setRightScreenTables([]); + } finally { + setIsLoadingRightColumns(false); + } + }; + + loadRightScreenColumns(); + }, [localConfig.rightScreenId]); + // ํ™”๋ฉด ๋ชฉ๋ก ๋กœ๋“œ useEffect(() => { const loadScreens = async () => { @@ -168,21 +291,51 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl updateConfig("parentDataMapping", newMappings); }; + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์ถ”๊ฐ€ + const addLinkedFilter = () => { + const newFilter: LinkedFilter = { + sourceColumn: "", + targetColumn: "", + }; + const newFilters = [...(localConfig.linkedFilters || []), newFilter]; + updateConfig("linkedFilters", newFilters); + }; + + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์ˆ˜์ • + const updateLinkedFilter = (index: number, field: keyof LinkedFilter, value: string) => { + const newFilters = [...(localConfig.linkedFilters || [])]; + newFilters[index] = { + ...newFilters[index], + [field]: value, + }; + updateConfig("linkedFilters", newFilters); + }; + + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์‚ญ์ œ + const removeLinkedFilter = (index: number) => { + const newFilters = (localConfig.linkedFilters || []).filter((_: any, i: number) => i !== index); + updateConfig("linkedFilters", newFilters); + }; + return (
- - - + + + ๋ ˆ์ด์•„์›ƒ - - - ํ™”๋ฉด ์„ค์ • + + + ํ™”๋ฉด - - - ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + + + ์—ฐ๊ฒฐํ•„ํ„ฐ + + + + ๋ฐ์ดํ„ฐ์ „๋‹ฌ @@ -385,6 +538,141 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl + {/* ์—ฐ๊ฒฐ ํ•„ํ„ฐ ํƒญ */} + + + + ์—ฐ๊ฒฐ ํ•„ํ„ฐ + + ์ขŒ์ธก ํ™”๋ฉด์—์„œ ํ–‰์„ ์„ ํƒํ•˜๋ฉด, ์šฐ์ธก ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”์ด ์ž๋™์œผ๋กœ ํ•„ํ„ฐ๋ง๋ฉ๋‹ˆ๋‹ค. + + + + {!localConfig.leftScreenId || !localConfig.rightScreenId ? ( +
+

+ ๋จผ์ € "ํ™”๋ฉด" ํƒญ์—์„œ ์ขŒ์ธก/์šฐ์ธก ํ™”๋ฉด์„ ๋ชจ๋‘ ์„ ํƒํ•˜์„ธ์š”. +

+
+ ) : isLoadingLeftColumns || isLoadingRightColumns ? ( +
+ + ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋”ฉ ์ค‘... +
+ ) : leftScreenColumns.length === 0 || rightScreenTables.length === 0 ? ( +
+

+ {leftScreenColumns.length === 0 && "์ขŒ์ธก ํ™”๋ฉด์— ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. "} + {rightScreenTables.length === 0 && "์šฐ์ธก ํ™”๋ฉด์— ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."} +

+
+ ) : ( + <> + {/* ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค๋ช… */} +
+

+ ์˜ˆ: ์ขŒ์ธก์—์„œ ์„ค๋น„๋ฅผ ์„ ํƒํ•˜๋ฉด โ†’ ์šฐ์ธก ์ ๊ฒ€ํ•ญ๋ชฉ์ด ํ•ด๋‹น ์„ค๋น„์˜ ํ•ญ๋ชฉ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +
+ ์ขŒ์ธก equipment_code โ†’ + ์šฐ์ธก equipment_code +

+
+ + {/* ํ•„ํ„ฐ ๋ชฉ๋ก */} +
+ {(localConfig.linkedFilters || []).map((filter: LinkedFilter, index: number) => ( +
+
+ ํ•„ํ„ฐ #{index + 1} + +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ ))} +
+ + {/* ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + + + {/* ํ˜„์žฌ ์„ค์ • ํ‘œ์‹œ */} + +
+ {(localConfig.linkedFilters || []).length > 0 + ? `${localConfig.linkedFilters.length}๊ฐœ ํ•„ํ„ฐ ์„ค์ •๋จ` + : "ํ•„ํ„ฐ ์—†์Œ - ์šฐ์ธก ํ™”๋ฉด์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค"} +
+ + )} +
+
+
+ {/* ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํƒญ */} @@ -395,69 +683,105 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl - {!localConfig.leftScreenId ? ( + {!localConfig.leftScreenId || !localConfig.rightScreenId ? (

- ๋จผ์ € "ํ™”๋ฉด ์„ค์ •" ํƒญ์—์„œ ์ขŒ์ธก ํ™”๋ฉด์„ ์„ ํƒํ•˜์„ธ์š”. + ๋จผ์ € "ํ™”๋ฉด ์„ค์ •" ํƒญ์—์„œ ์ขŒ์ธก/์šฐ์ธก ํ™”๋ฉด์„ ๋ชจ๋‘ ์„ ํƒํ•˜์„ธ์š”.

- ) : isLoadingColumns ? ( + ) : isLoadingLeftColumns || isLoadingRightColumns ? (
์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋”ฉ ์ค‘...
- ) : leftScreenColumns.length === 0 ? ( + ) : leftScreenColumns.length === 0 || rightScreenTables.length === 0 ? (

- ์ขŒ์ธก ํ™”๋ฉด์— ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + {leftScreenColumns.length === 0 && "์ขŒ์ธก ํ™”๋ฉด์— ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. "} + {rightScreenTables.length === 0 && "์šฐ์ธก ํ™”๋ฉด์— ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}

) : ( <> + {/* ์šฐ์ธก ํ™”๋ฉด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ํ‘œ์‹œ */} +
+

+ ์šฐ์ธก ํ™”๋ฉด์—์„œ ๊ฐ์ง€๋œ ํ…Œ์ด๋ธ” ({rightScreenTables.length}๊ฐœ): +

+
    + {rightScreenTables.map((table) => ( +
  • โ€ข {table.screenName}: {table.tableName}
  • + ))} +
+
+ {/* ๋งคํ•‘ ๋ชฉ๋ก */}
{(localConfig.parentDataMapping || []).map((mapping: ParentDataMapping, index: number) => ( -
-
-
-
- - -
- -
- - updateParentDataMapping(index, "targetColumn", e.target.value)} - placeholder="์ €์žฅํ•  ์ปฌ๋Ÿผ๋ช…" - className="h-8 text-xs" - /> -
+
+
+ ๋งคํ•‘ #{index + 1} + +
+
+
+ + +
+
+ +
+
+ +
-
))}
@@ -473,23 +797,23 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl ๋งคํ•‘ ์ถ”๊ฐ€ - {/* ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ */} + {/* ์ž๋™ ๋งคํ•‘ ์•ˆ๋‚ด */} +
+

+ ์ž๋™ ๋งคํ•‘: ์ขŒ์ธก์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์ด ์šฐ์ธก ํ™”๋ฉด์— ์ž๋™ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. +
+ ๋™์ผํ•œ ์ปฌ๋Ÿผ๋ช…(์˜ˆ: equipment_code)์ด ์žˆ์œผ๋ฉด ๋ณ„๋„ ์„ค์ • ์—†์ด ์ž๋™์œผ๋กœ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค. +

+
+ + {/* ์ˆ˜๋™ ๋งคํ•‘ ์•ˆ๋‚ด */}

- ์‚ฌ์šฉ ์˜ˆ์‹œ: + ์ˆ˜๋™ ๋งคํ•‘ (์„ ํƒ์‚ฌํ•ญ):
- ์ขŒ์ธก: ์„ค๋น„ ๋ชฉ๋ก (equipment_mng) + ์ปฌ๋Ÿผ๋ช…์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ ์œ„์—์„œ ๋งคํ•‘์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.
- ์šฐ์ธก: ์ ๊ฒ€ํ•ญ๋ชฉ ์ถ”๊ฐ€ ํ™”๋ฉด -
-
- ๋งคํ•‘ ์„ค์ •: -
- - ์†Œ์Šค: equipment_code โ†’ ํƒ€๊ฒŸ: equipment_code -
-
- ์ขŒ์ธก์—์„œ ์„ค๋น„๋ฅผ ์„ ํƒํ•˜๊ณ  ์šฐ์ธก์—์„œ ์ ๊ฒ€ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•˜๋ฉด, - ์„ ํƒํ•œ ์„ค๋น„์˜ equipment_code๊ฐ€ ์ž๋™์œผ๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. + ์˜ˆ: ์ขŒ์ธก user_id โ†’ ์šฐ์ธก created_by

diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index a643e3a9..5b2cff2b 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -1075,7 +1075,68 @@ export const TableListComponent: React.FC = ({ const sortBy = sortColumn || undefined; const sortOrder = sortDirection; const search = searchTerm || undefined; - const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; + + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์ผ ๋•Œ) + let linkedFilterValues: Record = {}; + let hasLinkedFiltersConfigured = false; // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ + let hasSelectedLeftData = false; // ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€ + + console.log("๐Ÿ” [TableList] ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ ํ™•์ธ:", { + hasSplitPanelContext: !!splitPanelContext, + tableName: tableConfig.selectedTable, + selectedLeftData: splitPanelContext?.selectedLeftData, + linkedFilters: splitPanelContext?.linkedFilters, + }); + + if (splitPanelContext) { + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • ์—ฌ๋ถ€ ํ™•์ธ (ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€) + const linkedFiltersConfig = splitPanelContext.linkedFilters || []; + hasLinkedFiltersConfigured = linkedFiltersConfig.some( + (filter) => filter.targetColumn?.startsWith(tableConfig.selectedTable + ".") || + filter.targetColumn === tableConfig.selectedTable + ); + + // ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์„ ํƒ ์—ฌ๋ถ€ ํ™•์ธ + hasSelectedLeftData = splitPanelContext.selectedLeftData && + Object.keys(splitPanelContext.selectedLeftData).length > 0; + + const allLinkedFilters = splitPanelContext.getLinkedFilterValues(); + console.log("๐Ÿ”— [TableList] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์›๋ณธ:", allLinkedFilters); + + // ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๋งŒ ์ถ”์ถœ (ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… ํ˜•์‹์—์„œ) + for (const [key, value] of Object.entries(allLinkedFilters)) { + if (key.includes(".")) { + const [tableName, columnName] = key.split("."); + if (tableName === tableConfig.selectedTable) { + linkedFilterValues[columnName] = value; + hasLinkedFiltersConfigured = true; // ์ด ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ํ•„ํ„ฐ๊ฐ€ ์žˆ์Œ + } + } else { + // ํ…Œ์ด๋ธ”๋ช… ์—†์ด ์ปฌ๋Ÿผ๋ช…๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + linkedFilterValues[key] = value; + } + } + if (Object.keys(linkedFilterValues).length > 0) { + console.log("๐Ÿ”— [TableList] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์ ์šฉ:", linkedFilterValues); + } + } + + // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€๋งŒ ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + // โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ (๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Œ) + if (hasLinkedFiltersConfigured && !hasSelectedLeftData) { + console.log("โš ๏ธ [TableList] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ •๋จ but ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋ฏธ์„ ํƒ โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ"); + setData([]); + setTotalItems(0); + setLoading(false); + return; + } + + // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ์™€ ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๋ณ‘ํ•ฉ + const filters = { + ...(Object.keys(searchValues).length > 0 ? searchValues : {}), + ...linkedFilterValues, + }; + const hasFilters = Object.keys(filters).length > 0; const entityJoinColumns = (tableConfig.columns || []) .filter((col) => col.additionalJoinInfo) @@ -1100,7 +1161,7 @@ export const TableListComponent: React.FC = ({ size: pageSize, sortBy, sortOrder, - search: filters, + search: hasFilters ? filters : undefined, enableEntityJoin: true, additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined, dataFilter: tableConfig.dataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ „๋‹ฌ @@ -1164,6 +1225,7 @@ export const TableListComponent: React.FC = ({ searchTerm, searchValues, isDesignMode, + splitPanelContext?.selectedLeftData, // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์žฌ์กฐํšŒ ]); const fetchTableDataDebounced = useCallback( @@ -2127,6 +2189,7 @@ export const TableListComponent: React.FC = ({ refreshKey, refreshTrigger, // ๊ฐ•์ œ ์ƒˆ๋กœ๊ณ ์นจ ํŠธ๋ฆฌ๊ฑฐ isDesignMode, + splitPanelContext?.selectedLeftData, // ๐Ÿ†• ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์„ ํƒ ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ // fetchTableDataDebounced ์ œ๊ฑฐ: useCallback ์žฌ์ƒ์„ฑ์œผ๋กœ ์ธํ•œ ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ ]); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 4e60536b..1da9d026 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -559,8 +559,7 @@ export class ButtonActionExecutor { // }); // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) - // console.log("๐Ÿ” ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘"); - // console.log("๐Ÿ“ฆ ํ˜„์žฌ formData:", JSON.stringify(formData, null, 2)); + console.log("๐Ÿ” ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘"); const fieldsWithNumbering: Record = {}; @@ -569,23 +568,39 @@ export class ButtonActionExecutor { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; - // console.log(`๐ŸŽฏ ๋ฐœ๊ฒฌ: ${fieldName} โ†’ ๊ทœ์น™ ${value}`); + console.log(`๐ŸŽฏ ๋ฐœ๊ฒฌ: ${fieldName} โ†’ ๊ทœ์น™ ${value}`); } } - // console.log("๐Ÿ“‹ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ:", fieldsWithNumbering); - // console.log("๐Ÿ“Š ํ•„๋“œ ๊ฐœ์ˆ˜:", Object.keys(fieldsWithNumbering).length); + console.log("๐Ÿ“‹ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ:", fieldsWithNumbering); + console.log("๐Ÿ“Š ํ•„๋“œ ๊ฐœ์ˆ˜:", Object.keys(fieldsWithNumbering).length); - // ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’ ์œ ์ง€ (์žฌํ• ๋‹นํ•˜์ง€ ์•Š์Œ) - // ์ฑ„๋ฒˆ ๊ทœ์น™์€ TextInputComponent ๋งˆ์šดํŠธ ์‹œ ์ด๋ฏธ ์ƒ์„ฑ๋˜์—ˆ์œผ๋ฏ€๋กœ - // ์ €์žฅ ์‹œ์ ์—๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ •ํ•œ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + // ๐Ÿ”ฅ ์ €์žฅ ์‹œ์ ์— allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€ if (Object.keys(fieldsWithNumbering).length > 0) { - console.log("โ„น๏ธ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ•„๋“œ ๊ฐ์ง€:", Object.keys(fieldsWithNumbering)); - console.log("โ„น๏ธ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’ ์œ ์ง€ (์žฌํ• ๋‹น ํ•˜์ง€ ์•Š์Œ)"); + console.log("๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)"); + const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); + + for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { + try { + console.log(`๐Ÿ”„ ${fieldName} ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ: ${ruleId}`); + const allocateResult = await allocateNumberingCode(ruleId); + + if (allocateResult.success && allocateResult.data?.generatedCode) { + const newCode = allocateResult.data.generatedCode; + console.log(`โœ… ${fieldName} ์ƒˆ ์ฝ”๋“œ ํ• ๋‹น: ${formData[fieldName]} โ†’ ${newCode}`); + formData[fieldName] = newCode; + } else { + console.warn(`โš ๏ธ ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ, ๊ธฐ์กด ๊ฐ’ ์œ ์ง€:`, allocateResult.error); + } + } catch (allocateError) { + console.error(`โŒ ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); + // ์˜ค๋ฅ˜ ์‹œ ๊ธฐ์กด ๊ฐ’ ์œ ์ง€ + } + } } - // console.log("โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); - // console.log("๐Ÿ“ฆ ์ตœ์ข… formData:", JSON.stringify(formData, null, 2)); + console.log("โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); + console.log("๐Ÿ“ฆ ์ตœ์ข… formData:", JSON.stringify(formData, null, 2)); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ) const splitPanelData = context.splitPanelParentData || {}; @@ -1210,6 +1225,7 @@ export class ButtonActionExecutor { // ๐Ÿ†• ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ const selectedData = context.selectedRowsData || []; console.log("๐Ÿ“ฆ [handleModal] ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ:", selectedData); + console.log("๐Ÿ“ฆ [handleModal] ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ:", context.splitPanelParentData); // ์ „์—ญ ๋ชจ๋‹ฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ const modalEvent = new CustomEvent("openScreenModal", { @@ -1221,6 +1237,8 @@ export class ButtonActionExecutor { // ๐Ÿ†• ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ selectedData: selectedData, selectedIds: selectedData.map((row: any) => row.id).filter(Boolean), + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (ํƒญ ์•ˆ ๋ชจ๋‹ฌ์—์„œ ์‚ฌ์šฉ) + splitPanelParentData: context.splitPanelParentData || {}, }, });