From 1503dd87bbc3091e0cad52495067469c6bd7ec22 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Dec 2025 10:09:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B6=84=ED=95=A0?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=88=98=EC=A0=95=EB=AA=A8=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/common/ScreenModal.tsx | 3 - .../RepeaterFieldGroupRenderer.tsx | 77 ++++++++++++- frontend/lib/utils/buttonActions.ts | 104 ++++++++++++++++++ 3 files changed, 177 insertions(+), 7 deletions(-) diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 3e0f1a61..70567c99 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -177,7 +177,6 @@ export const ScreenModal: React.FC = ({ className }) => { }); setScreenData(null); setFormData({}); - setSelectedData([]); // ๐Ÿ†• ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); // localStorage์— ์ €์žฅ console.log("๐Ÿ”„ ์—ฐ์† ๋ชจ๋“œ ์ดˆ๊ธฐํ™”: false"); @@ -637,8 +636,6 @@ export const ScreenModal: React.FC = ({ className }) => { userId={userId} userName={userName} companyCode={user?.companyCode} - // ๐Ÿ†• ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (RepeatScreenModal ๋“ฑ์—์„œ ์‚ฌ์šฉ) - groupedData={selectedData.length > 0 ? selectedData : undefined} /> ); })} diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index a4dbd157..3b7fc339 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -244,11 +244,46 @@ const RepeaterFieldGroupComponent: React.FC = (props) => const currentValue = parsedValueRef.current; // mode๊ฐ€ "replace"์ธ ๊ฒฝ์šฐ ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋Œ€์ฒด, ๊ทธ ์™ธ์—๋Š” ์ถ”๊ฐ€ - // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ const mode = typeof mappingRulesOrMode === "string" ? mappingRulesOrMode : "append"; - const newItems = mode === "replace" ? filteredData : [...currentValue, ...filteredData]; + + let newItems: any[]; + let addedCount = 0; + let duplicateCount = 0; + + if (mode === "replace") { + newItems = filteredData; + addedCount = filteredData.length; + } else { + // ๐Ÿ†• ์ค‘๋ณต ์ฒดํฌ: id ๋˜๋Š” ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ํ•ญ๋ชฉ ์ œ์™ธ + const existingIds = new Set( + currentValue + .map((item: any) => item.id || item.po_item_id || item.item_id) + .filter(Boolean) + ); + + const uniqueNewItems = filteredData.filter((item: any) => { + const itemId = item.id || item.po_item_id || item.item_id; + if (itemId && existingIds.has(itemId)) { + duplicateCount++; + return false; // ์ค‘๋ณต ํ•ญ๋ชฉ ์ œ์™ธ + } + return true; + }); + + newItems = [...currentValue, ...uniqueNewItems]; + addedCount = uniqueNewItems.length; + } - console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ์ตœ์ข… ๋ฐ์ดํ„ฐ:", { currentValue, newItems, mode }); + console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ์ตœ์ข… ๋ฐ์ดํ„ฐ:", { + currentValue, + newItems, + mode, + addedCount, + duplicateCount, + }); + + // ๐Ÿ†• groupedData ์ƒํƒœ๋„ ์ง์ ‘ ์—…๋ฐ์ดํŠธ (UI ์ฆ‰์‹œ ๋ฐ˜์˜) + setGroupedData(newItems); // JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ const jsonValue = JSON.stringify(newItems); @@ -268,7 +303,16 @@ const RepeaterFieldGroupComponent: React.FC = (props) => onChangeRef.current(jsonValue); } - toast.success(`${filteredData.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); + // ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + if (addedCount > 0) { + if (duplicateCount > 0) { + toast.success(`${addedCount}๊ฐœ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค (${duplicateCount}๊ฐœ ์ค‘๋ณต ์ œ์™ธ)`); + } else { + toast.success(`${addedCount}๊ฐœ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); + } + } else if (duplicateCount > 0) { + toast.warning(`${duplicateCount}๊ฐœ ํ•ญ๋ชฉ์ด ์ด๋ฏธ ์ถ”๊ฐ€๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค`); + } }, []); // DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ @@ -311,6 +355,31 @@ const RepeaterFieldGroupComponent: React.FC = (props) => } }, [splitPanelContext, screenContext?.splitPanelPosition, component.id, dataReceiver]); + // ๐Ÿ†• ์ „์—ญ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ (splitPanelDataTransfer) + useEffect(() => { + const handleSplitPanelDataTransfer = (event: CustomEvent) => { + const { data, mode, mappingRules } = event.detail; + + console.log("๐Ÿ“ฅ [RepeaterFieldGroup] splitPanelDataTransfer ์ด๋ฒคํŠธ ์ˆ˜์‹ :", { + dataCount: data?.length, + mode, + componentId: component.id, + }); + + // ์šฐ์ธก ํŒจ๋„์˜ ๋ฆฌํ”ผํ„ฐ ํ•„๋“œ ๊ทธ๋ฃน๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹  + const splitPanelPosition = screenContext?.splitPanelPosition; + if (splitPanelPosition === "right" && data && data.length > 0) { + handleReceiveData(data, mappingRules || mode || "append"); + } + }; + + window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener); + + return () => { + window.removeEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener); + }; + }, [screenContext?.splitPanelPosition, handleReceiveData, component.id]); + return ( { + try { + console.log("๐Ÿ“ค [handleTransferData] ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ์ž‘:", { config, context }); + + // ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ํ™•์ธ + const selectedRows = context.selectedRowsData || context.flowSelectedData || []; + + if (!selectedRows || selectedRows.length === 0) { + toast.error("์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return false; + } + + console.log("๐Ÿ“ค [handleTransferData] ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ:", selectedRows); + + // dataTransfer ์„ค์ • ํ™•์ธ + const dataTransfer = config.dataTransfer; + + if (!dataTransfer) { + // dataTransfer ์„ค์ •์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋™์ž‘: ์ „์—ญ ์ด๋ฒคํŠธ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + console.log("๐Ÿ“ค [handleTransferData] dataTransfer ์„ค์ • ์—†์Œ - ์ „์—ญ ์ด๋ฒคํŠธ ๋ฐœ์ƒ"); + + const transferEvent = new CustomEvent("splitPanelDataTransfer", { + detail: { + data: selectedRows, + mode: "append", + sourcePosition: "left", + }, + }); + window.dispatchEvent(transferEvent); + + toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + return true; + } + + // dataTransfer ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ + const { targetType, targetComponentId, targetScreenId, mappingRules, receiveMode } = dataTransfer; + + if (targetType === "component" && targetComponentId) { + // ๊ฐ™์€ ํ™”๋ฉด ๋‚ด ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ + console.log("๐Ÿ“ค [handleTransferData] ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ:", targetComponentId); + + const transferEvent = new CustomEvent("componentDataTransfer", { + detail: { + targetComponentId, + data: selectedRows, + mappingRules, + mode: receiveMode || "append", + }, + }); + window.dispatchEvent(transferEvent); + + toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + return true; + } else if (targetType === "screen" && targetScreenId) { + // ๋‹ค๋ฅธ ํ™”๋ฉด์œผ๋กœ ์ „๋‹ฌ (๋ถ„ํ•  ํŒจ๋„ ๋“ฑ) + console.log("๐Ÿ“ค [handleTransferData] ํ™”๋ฉด์œผ๋กœ ์ „๋‹ฌ:", targetScreenId); + + const transferEvent = new CustomEvent("screenDataTransfer", { + detail: { + targetScreenId, + data: selectedRows, + mappingRules, + mode: receiveMode || "append", + }, + }); + window.dispatchEvent(transferEvent); + + toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + return true; + } else { + // ๊ธฐ๋ณธ: ๋ถ„ํ•  ํŒจ๋„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ด๋ฒคํŠธ + console.log("๐Ÿ“ค [handleTransferData] ๊ธฐ๋ณธ ๋ถ„ํ•  ํŒจ๋„ ์ „๋‹ฌ"); + + const transferEvent = new CustomEvent("splitPanelDataTransfer", { + detail: { + data: selectedRows, + mappingRules, + mode: receiveMode || "append", + sourcePosition: "left", + }, + }); + window.dispatchEvent(transferEvent); + + toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + return true; + } + } catch (error: any) { + console.error("โŒ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹คํŒจ:", error); + toast.error(error.message || "๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + return false; + } + } + /** * ํผ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ */ @@ -3293,4 +3392,9 @@ export const DEFAULT_BUTTON_ACTIONS: Record Date: Mon, 1 Dec 2025 10:19:20 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/contexts/SplitPanelContext.tsx | 49 +++++++++++++++++ .../RepeaterFieldGroupRenderer.tsx | 55 +++++++++++++++++-- .../table-list/TableListComponent.tsx | 46 +++++++++++----- 3 files changed, 131 insertions(+), 19 deletions(-) diff --git a/frontend/contexts/SplitPanelContext.tsx b/frontend/contexts/SplitPanelContext.tsx index e5052295..bfb9610b 100644 --- a/frontend/contexts/SplitPanelContext.tsx +++ b/frontend/contexts/SplitPanelContext.tsx @@ -48,6 +48,12 @@ interface SplitPanelContextValue { // screenId๋กœ ์œ„์น˜ ์ฐพ๊ธฐ getPositionByScreenId: (screenId: number) => SplitPanelPosition | null; + + // ๐Ÿ†• ์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID ๊ด€๋ฆฌ (์ขŒ์ธก ํ…Œ์ด๋ธ”์—์„œ ํ•„ํ„ฐ๋ง์šฉ) + addedItemIds: Set; + addItemIds: (ids: string[]) => void; + removeItemIds: (ids: string[]) => void; + clearItemIds: () => void; } const SplitPanelContext = createContext(null); @@ -74,6 +80,9 @@ export function SplitPanelProvider({ // ๊ฐ•์ œ ๋ฆฌ๋ Œ๋”๋ง์šฉ ์ƒํƒœ const [, forceUpdate] = useState(0); + + // ๐Ÿ†• ์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID ์ƒํƒœ + const [addedItemIds, setAddedItemIds] = useState>(new Set()); /** * ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก @@ -191,6 +200,38 @@ export function SplitPanelProvider({ [leftScreenId, rightScreenId] ); + /** + * ๐Ÿ†• ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID ๋“ฑ๋ก + */ + const addItemIds = useCallback((ids: string[]) => { + setAddedItemIds((prev) => { + const newSet = new Set(prev); + ids.forEach((id) => newSet.add(id)); + logger.debug(`[SplitPanelContext] ํ•ญ๋ชฉ ID ์ถ”๊ฐ€: ${ids.length}๊ฐœ`, { ids }); + return newSet; + }); + }, []); + + /** + * ๐Ÿ†• ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID ์ œ๊ฑฐ + */ + const removeItemIds = useCallback((ids: string[]) => { + setAddedItemIds((prev) => { + const newSet = new Set(prev); + ids.forEach((id) => newSet.delete(id)); + logger.debug(`[SplitPanelContext] ํ•ญ๋ชฉ ID ์ œ๊ฑฐ: ${ids.length}๊ฐœ`, { ids }); + return newSet; + }); + }, []); + + /** + * ๐Ÿ†• ๋ชจ๋“  ํ•ญ๋ชฉ ID ์ดˆ๊ธฐํ™” + */ + const clearItemIds = useCallback(() => { + setAddedItemIds(new Set()); + logger.debug(`[SplitPanelContext] ํ•ญ๋ชฉ ID ์ดˆ๊ธฐํ™”`); + }, []); + // ๐Ÿ†• useMemo๋กœ value ๊ฐ์ฒด ๋ฉ”๋ชจ์ด์ œ์ด์…˜ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) const value = React.useMemo(() => ({ splitPanelId, @@ -202,6 +243,10 @@ export function SplitPanelProvider({ getOtherSideReceivers, isInSplitPanel: true, getPositionByScreenId, + addedItemIds, + addItemIds, + removeItemIds, + clearItemIds, }), [ splitPanelId, leftScreenId, @@ -211,6 +256,10 @@ export function SplitPanelProvider({ transferToOtherSide, getOtherSideReceivers, getPositionByScreenId, + addedItemIds, + addItemIds, + removeItemIds, + clearItemIds, ]); return ( diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index 3b7fc339..c47ff3c9 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -120,10 +120,15 @@ const RepeaterFieldGroupComponent: React.FC = (props) => setGroupedData(items); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ID ๋ชฉ๋ก ์ €์žฅ (์‚ญ์ œ ์ถ”์ ์šฉ) - const itemIds = items.map((item: any) => item.id).filter(Boolean); + const itemIds = items.map((item: any) => String(item.id || item.po_item_id || item.item_id)).filter(Boolean); setOriginalItemIds(itemIds); console.log("๐Ÿ“‹ [RepeaterFieldGroup] ์›๋ณธ ๋ฐ์ดํ„ฐ ID ๋ชฉ๋ก ์ €์žฅ:", itemIds); + // ๐Ÿ†• SplitPanelContext์— ๊ธฐ์กด ํ•ญ๋ชฉ ID ๋“ฑ๋ก (์ขŒ์ธก ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง์šฉ) + if (splitPanelContext?.addItemIds && itemIds.length > 0) { + splitPanelContext.addItemIds(itemIds); + } + // onChange ํ˜ธ์ถœํ•˜์—ฌ ๋ถ€๋ชจ์—๊ฒŒ ์•Œ๋ฆผ if (onChange && items.length > 0) { const dataWithMeta = items.map((item: any) => ({ @@ -285,6 +290,14 @@ const RepeaterFieldGroupComponent: React.FC = (props) => // ๐Ÿ†• groupedData ์ƒํƒœ๋„ ์ง์ ‘ ์—…๋ฐ์ดํŠธ (UI ์ฆ‰์‹œ ๋ฐ˜์˜) setGroupedData(newItems); + // ๐Ÿ†• SplitPanelContext์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID ๋“ฑ๋ก (์ขŒ์ธก ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง์šฉ) + if (splitPanelContext?.addItemIds && addedCount > 0) { + const newItemIds = newItems + .map((item: any) => String(item.id || item.po_item_id || item.item_id)) + .filter(Boolean); + splitPanelContext.addItemIds(newItemIds); + } + // JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ const jsonValue = JSON.stringify(newItems); console.log("๐Ÿ“ฅ [RepeaterFieldGroup] onChange/onFormDataChange ํ˜ธ์ถœ:", { @@ -380,14 +393,44 @@ const RepeaterFieldGroupComponent: React.FC = (props) => }; }, [screenContext?.splitPanelPosition, handleReceiveData, component.id]); + // ๐Ÿ†• RepeaterInput์—์„œ ํ•ญ๋ชฉ ๋ณ€๊ฒฝ ์‹œ SplitPanelContext์˜ addedItemIds ๋™๊ธฐํ™” + const handleRepeaterChange = useCallback((newValue: any[]) => { + // ๋ฐฐ์—ด์„ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ + const jsonValue = JSON.stringify(newValue); + onChange?.(jsonValue); + + // ๐Ÿ†• groupedData ์ƒํƒœ๋„ ์—…๋ฐ์ดํŠธ + setGroupedData(newValue); + + // ๐Ÿ†• SplitPanelContext์˜ addedItemIds ๋™๊ธฐํ™” + if (splitPanelContext?.isInSplitPanel && screenContext?.splitPanelPosition === "right") { + // ํ˜„์žฌ ํ•ญ๋ชฉ๋“ค์˜ ID ๋ชฉ๋ก + const currentIds = newValue + .map((item: any) => String(item.id || item.po_item_id || item.item_id)) + .filter(Boolean); + + // ๊ธฐ์กด addedItemIds์™€ ๋น„๊ตํ•˜์—ฌ ์‚ญ์ œ๋œ ID ์ฐพ๊ธฐ + const addedIds = splitPanelContext.addedItemIds; + const removedIds = Array.from(addedIds).filter(id => !currentIds.includes(id)); + + if (removedIds.length > 0) { + console.log("๐Ÿ—‘๏ธ [RepeaterFieldGroup] ์‚ญ์ œ๋œ ํ•ญ๋ชฉ ID ์ œ๊ฑฐ:", removedIds); + splitPanelContext.removeItemIds(removedIds); + } + + // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ID๊ฐ€ ์žˆ์œผ๋ฉด ๋“ฑ๋ก + const newIds = currentIds.filter((id: string) => !addedIds.has(id)); + if (newIds.length > 0) { + console.log("โž• [RepeaterFieldGroup] ์ƒˆ ํ•ญ๋ชฉ ID ์ถ”๊ฐ€:", newIds); + splitPanelContext.addItemIds(newIds); + } + } + }, [onChange, splitPanelContext, screenContext?.splitPanelPosition]); + return ( { - // ๋ฐฐ์—ด์„ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ - const jsonValue = JSON.stringify(newValue); - onChange?.(jsonValue); - }} + onChange={handleRepeaterChange} config={config} disabled={disabled} readonly={readonly} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index a0f01727..f0690943 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -330,6 +330,25 @@ export const TableListComponent: React.FC = ({ const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์šฐ์ธก์— ์ด๋ฏธ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ํ•„ํ„ฐ๋ง (์ขŒ์ธก ํ…Œ์ด๋ธ”์—๋งŒ ์ ์šฉ) + const filteredData = useMemo(() => { + // ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก์— ์žˆ๊ณ , ์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ•„ํ„ฐ๋ง + if (splitPanelPosition === "left" && splitPanelContext?.addedItemIds && splitPanelContext.addedItemIds.size > 0) { + const addedIds = splitPanelContext.addedItemIds; + const filtered = data.filter((row) => { + const rowId = String(row.id || row.po_item_id || row.item_id || ""); + return !addedIds.has(rowId); + }); + console.log("๐Ÿ” [TableList] ์šฐ์ธก ์ถ”๊ฐ€ ํ•ญ๋ชฉ ํ•„ํ„ฐ๋ง:", { + originalCount: data.length, + filteredCount: filtered.length, + addedIdsCount: addedIds.size, + }); + return filtered; + } + return data; + }, [data, splitPanelPosition, splitPanelContext?.addedItemIds]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); @@ -438,8 +457,8 @@ export const TableListComponent: React.FC = ({ componentType: "table-list", getSelectedData: () => { - // ์„ ํƒ๋œ ํ–‰์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ - const selectedData = data.filter((row) => { + // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ์—์„œ ์„ ํƒ๋œ ํ–‰๋งŒ ๋ฐ˜ํ™˜ (์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ์ œ์™ธ) + const selectedData = filteredData.filter((row) => { const rowId = String(row.id || row[tableConfig.selectedTable + "_id"] || ""); return selectedRows.has(rowId); }); @@ -447,7 +466,8 @@ export const TableListComponent: React.FC = ({ }, getAllData: () => { - return data; + // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + return filteredData; }, clearSelection: () => { @@ -1375,31 +1395,31 @@ export const TableListComponent: React.FC = ({ }); } - const allRowsSelected = data.every((row, index) => newSelectedRows.has(getRowKey(row, index))); - setIsAllSelected(allRowsSelected && data.length > 0); + const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index))); + setIsAllSelected(allRowsSelected && filteredData.length > 0); }; const handleSelectAll = (checked: boolean) => { if (checked) { - const allKeys = data.map((row, index) => getRowKey(row, index)); + const allKeys = filteredData.map((row, index) => getRowKey(row, index)); const newSelectedRows = new Set(allKeys); setSelectedRows(newSelectedRows); setIsAllSelected(true); if (onSelectedRowsChange) { - onSelectedRowsChange(Array.from(newSelectedRows), data, sortColumn || undefined, sortDirection); + onSelectedRowsChange(Array.from(newSelectedRows), filteredData, sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: Array.from(newSelectedRows), - selectedRowsData: data, + selectedRowsData: filteredData, }); } // ๐Ÿ†• modalDataStore์— ์ „์ฒด ๋ฐ์ดํ„ฐ ์ €์žฅ - if (tableConfig.selectedTable && data.length > 0) { + if (tableConfig.selectedTable && filteredData.length > 0) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { - const modalItems = data.map((row, idx) => ({ + const modalItems = filteredData.map((row, idx) => ({ id: getRowKey(row, idx), originalData: row, additionalData: {}, @@ -2003,11 +2023,11 @@ export const TableListComponent: React.FC = ({ // ๋ฐ์ดํ„ฐ ๊ทธ๋ฃนํ™” const groupedData = useMemo((): GroupedData[] => { - if (groupByColumns.length === 0 || data.length === 0) return []; + if (groupByColumns.length === 0 || filteredData.length === 0) return []; const grouped = new Map(); - data.forEach((item) => { + filteredData.forEach((item) => { // ๊ทธ๋ฃน ํ‚ค ์ƒ์„ฑ: "ํ†ตํ™”:KRW > ๋‹จ์œ„:EA" const keyParts = groupByColumns.map((col) => { // ์นดํ…Œ๊ณ ๋ฆฌ/์—”ํ‹ฐํ‹ฐ ํƒ€์ž…์ธ ๊ฒฝ์šฐ _name ํ•„๋“œ ์‚ฌ์šฉ @@ -2706,7 +2726,7 @@ export const TableListComponent: React.FC = ({ }) ) : ( // ์ผ๋ฐ˜ ๋ Œ๋”๋ง (๊ทธ๋ฃน ์—†์Œ) - data.map((row, index) => ( + filteredData.map((row, index) => ( Date: Mon, 1 Dec 2025 10:36:57 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=EA=B2=80=EC=83=89=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=8B=A4=EC=A4=91=EC=84=A0=ED=83=9D=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/tableManagementService.ts | 16 ++- .../table-search-widget/TableSearchWidget.tsx | 125 ++++++++++++------ .../TableSearchWidgetConfigPanel.tsx | 1 + 3 files changed, 104 insertions(+), 38 deletions(-) diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 173de022..28da136e 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1165,12 +1165,26 @@ export class TableManagementService { paramCount: number; } | null> { try { - // ๐Ÿ”ง ๋‚ ์งœ ๋ฒ”์œ„ ๋ฌธ์ž์—ด "YYYY-MM-DD|YYYY-MM-DD" ์ฒดํฌ (์ตœ์šฐ์„ !) + // ๐Ÿ”ง ํŒŒ์ดํ”„๋กœ ๊ตฌ๋ถ„๋œ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ (๋‹ค์ค‘์„ ํƒ ๋˜๋Š” ๋‚ ์งœ ๋ฒ”์œ„) if (typeof value === "string" && value.includes("|")) { const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); + + // ๋‚ ์งœ ํƒ€์ž…์ด๋ฉด ๋‚ ์งœ ๋ฒ”์œ„๋กœ ์ฒ˜๋ฆฌ if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) { return this.buildDateRangeCondition(columnName, value, paramIndex); } + + // ๊ทธ ์™ธ ํƒ€์ž…์ด๋ฉด ๋‹ค์ค‘์„ ํƒ(IN ์กฐ๊ฑด)์œผ๋กœ ์ฒ˜๋ฆฌ + const multiValues = value.split("|").filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const placeholders = multiValues.map((_: string, idx: number) => `$${paramIndex + idx}`).join(", "); + logger.info(`๐Ÿ” ๋‹ค์ค‘์„ ํƒ ํ•„ํ„ฐ ์ ์šฉ: ${columnName} IN (${multiValues.join(", ")})`); + return { + whereClause: `${columnName}::text IN (${placeholders})`, + values: multiValues, + paramCount: multiValues.length, + }; + } } // ๐Ÿ”ง ๋‚ ์งœ ๋ฒ”์œ„ ๊ฐ์ฒด {from, to} ์ฒดํฌ diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index e13e3d94..6d513976 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Settings, Filter, Layers, X } from "lucide-react"; +import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel"; @@ -13,6 +13,9 @@ import { TableFilter } from "@/types/table-options"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Checkbox } from "@/components/ui/checkbox"; +import { cn } from "@/lib/utils"; interface PresetFilter { id: string; @@ -20,6 +23,7 @@ interface PresetFilter { columnLabel: string; filterType: "text" | "number" | "date" | "select"; width?: number; + multiSelect?: boolean; // ๋‹ค์ค‘์„ ํƒ ์—ฌ๋ถ€ (select ํƒ€์ž…์—์„œ๋งŒ ์‚ฌ์šฉ) } interface TableSearchWidgetProps { @@ -280,6 +284,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table } } + // ๋‹ค์ค‘์„ ํƒ ๋ฐฐ์—ด์„ ์ฒ˜๋ฆฌ (ํŒŒ์ดํ”„๋กœ ์—ฐ๊ฒฐ๋œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜) + if (filter.filterType === "select" && Array.isArray(filterValue)) { + filterValue = filterValue.join("|"); + } + return { ...filter, value: filterValue || "", @@ -289,6 +298,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table // ๋นˆ ๊ฐ’ ์ฒดํฌ if (!f.value) return false; if (typeof f.value === "string" && f.value === "") return false; + if (Array.isArray(f.value) && f.value.length === 0) return false; return true; }); @@ -343,12 +353,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table case "select": { let options = selectOptions[filter.columnName] || []; - // ํ˜„์žฌ ์„ ํƒ๋œ ๊ฐ’์ด ์˜ต์…˜ ๋ชฉ๋ก์— ์—†์œผ๋ฉด ์ถ”๊ฐ€ (๋ฐ์ดํ„ฐ ์—†์„ ๋•Œ๋„ ์„ ํƒ๊ฐ’ ์œ ์ง€) - if (value && !options.find((opt) => opt.value === value)) { - const savedLabel = selectedLabels[filter.columnName] || value; - options = [{ value, label: savedLabel }, ...options]; - } - // ์ค‘๋ณต ์ œ๊ฑฐ (value ๊ธฐ์ค€) const uniqueOptions = options.reduce( (acc, option) => { @@ -360,39 +364,86 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table [] as Array<{ value: string; label: string }>, ); + // ํ•ญ์ƒ ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ + const selectedValues: string[] = Array.isArray(value) ? value : (value ? [value] : []); + + // ์„ ํƒ๋œ ๊ฐ’๋“ค์˜ ๋ผ๋ฒจ ํ‘œ์‹œ + const getDisplayText = () => { + if (selectedValues.length === 0) return column?.columnLabel || "์„ ํƒ"; + if (selectedValues.length === 1) { + const opt = uniqueOptions.find(o => o.value === selectedValues[0]); + return opt?.label || selectedValues[0]; + } + return `${selectedValues.length}๊ฐœ ์„ ํƒ๋จ`; + }; + + const handleMultiSelectChange = (optionValue: string, checked: boolean) => { + let newValues: string[]; + if (checked) { + newValues = [...selectedValues, optionValue]; + } else { + newValues = selectedValues.filter(v => v !== optionValue); + } + handleFilterChange(filter.columnName, newValues.length > 0 ? newValues : ""); + }; + return ( - + + ); } diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx index 8c4ab6a1..3424abb9 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx @@ -29,6 +29,7 @@ interface PresetFilter { columnLabel: string; filterType: "text" | "number" | "date" | "select"; width?: number; + multiSelect?: boolean; // ๋‹ค์ค‘์„ ํƒ ์—ฌ๋ถ€ (select ํƒ€์ž…์—์„œ๋งŒ ์‚ฌ์šฉ) } export function TableSearchWidgetConfigPanel({ -- 2.43.0 From 93b92960e78389b4b6a56ceef08cf78831726190 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Dec 2025 11:20:06 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=83=81?= =?UTF-8?q?=EB=8B=A8=20=EC=97=AC=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../registry/components/table-list/TableListComponent.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index f0690943..841e6f0a 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2354,7 +2354,7 @@ export const TableListComponent: React.FC = ({ )} -
+
= ({
= ({ className="sticky z-50" style={{ position: "sticky", - top: "-2px", + top: 0, zIndex: 50, backgroundColor: "hsl(var(--background))", }} -- 2.43.0 From da6ac92391b4b25187c52a5501408b6d65947afb Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Dec 2025 15:21:03 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=9D=B4=20=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/dynamicFormController.ts | 2 +- .../src/services/dynamicFormService.ts | 2 +- frontend/components/common/ScreenModal.tsx | 26 +- .../screen/InteractiveScreenViewerDynamic.tsx | 4 + .../screen/panels/UnifiedPropertiesPanel.tsx | 1 + frontend/lib/api/dynamicForm.ts | 2 +- .../lib/registry/DynamicComponentRenderer.tsx | 5 + .../AutocompleteSearchInputComponent.tsx | 36 +- .../AutocompleteSearchInputConfigPanel.tsx | 35 +- .../button-primary/ButtonPrimaryComponent.tsx | 2 +- frontend/lib/utils/buttonActions.ts | 17 +- ..._์ž„๋ฒ ๋”ฉ_๋ฐ_๋ฐ์ดํ„ฐ_์ „๋‹ฌ_์‹œ์Šคํ…œ_๊ตฌํ˜„_๊ณ„ํš์„œ.md | 702 ++++++++++-------- ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_Phase1-4_๊ตฌํ˜„_์™„๋ฃŒ.md | 70 +- ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_์ถฉ๋Œ_๋ถ„์„_๋ณด๊ณ ์„œ.md | 126 +++- 14 files changed, 621 insertions(+), 409 deletions(-) diff --git a/backend-node/src/controllers/dynamicFormController.ts b/backend-node/src/controllers/dynamicFormController.ts index 738d1964..6adf8cd6 100644 --- a/backend-node/src/controllers/dynamicFormController.ts +++ b/backend-node/src/controllers/dynamicFormController.ts @@ -203,7 +203,7 @@ export const updateFormDataPartial = async ( }; const result = await dynamicFormService.updateFormDataPartial( - parseInt(id), + id, // ๐Ÿ”ง parseInt ์ œ๊ฑฐ - UUID ๋ฌธ์ž์—ด๋„ ์ง€์› tableName, originalData, newDataWithMeta diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 11648577..ffaf8586 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -746,7 +746,7 @@ export class DynamicFormService { * ํผ ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ (๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์—…๋ฐ์ดํŠธ) */ async updateFormDataPartial( - id: number, + id: string | number, // ๐Ÿ”ง UUID ๋ฌธ์ž์—ด๋„ ์ง€์› tableName: string, originalData: Record, newData: Record diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 70567c99..53fd0852 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -57,6 +57,9 @@ export const ScreenModal: React.FC = ({ className }) => { // ํผ ๋ฐ์ดํ„ฐ ์ƒํƒœ ์ถ”๊ฐ€ const [formData, setFormData] = useState>({}); + // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ƒํƒœ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ UPDATE ํŒ๋‹จ์šฉ) + const [originalData, setOriginalData] = useState | null>(null); + // ์—ฐ์† ๋“ฑ๋ก ๋ชจ๋“œ ์ƒํƒœ (state๋กœ ๋ณ€๊ฒฝ - ์ฒดํฌ๋ฐ•์Šค UI ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•ด) const [continuousMode, setContinuousMode] = useState(false); @@ -143,10 +146,13 @@ export const ScreenModal: React.FC = ({ className }) => { console.log("โœ… URL ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€:", urlParams); } - // ๐Ÿ†• editData๊ฐ€ ์žˆ์œผ๋ฉด formData๋กœ ์„ค์ • (์ˆ˜์ • ๋ชจ๋“œ) + // ๐Ÿ†• editData๊ฐ€ ์žˆ์œผ๋ฉด formData์™€ originalData๋กœ ์„ค์ • (์ˆ˜์ • ๋ชจ๋“œ) if (editData) { console.log("๐Ÿ“ [ScreenModal] ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์„ค์ •:", editData); setFormData(editData); + setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) + } else { + setOriginalData(null); // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ } setModalState({ @@ -177,6 +183,7 @@ export const ScreenModal: React.FC = ({ className }) => { }); setScreenData(null); setFormData({}); + setOriginalData(null); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); // localStorage์— ์ €์žฅ console.log("๐Ÿ”„ ์—ฐ์† ๋ชจ๋“œ ์ดˆ๊ธฐํ™”: false"); @@ -364,12 +371,15 @@ export const ScreenModal: React.FC = ({ className }) => { "โš ๏ธ [ScreenModal] ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ(๋ฐฐ์—ด)๋Š” formData๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์Œ. SelectedItemsDetailInput๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.", ); setFormData(normalizedData); // SelectedItemsDetailInput์ด ์ง์ ‘ ์‚ฌ์šฉ + setOriginalData(normalizedData[0] || null); // ๐Ÿ†• ์ฒซ ๋ฒˆ์งธ ๋ ˆ์ฝ”๋“œ๋ฅผ ์›๋ณธ์œผ๋กœ ์ €์žฅ } else { setFormData(normalizedData); + setOriginalData(normalizedData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) } // setFormData ์งํ›„ ํ™•์ธ console.log("๐Ÿ”„ setFormData ํ˜ธ์ถœ ์™„๋ฃŒ (๋‚ ์งœ ์ •๊ทœํ™”๋จ)"); + console.log("๐Ÿ”„ setOriginalData ํ˜ธ์ถœ ์™„๋ฃŒ (UPDATE ํŒ๋‹จ์šฉ)"); } else { console.error("โŒ ์ˆ˜์ • ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", response.error); toast.error("๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); @@ -618,11 +628,17 @@ export const ScreenModal: React.FC = ({ className }) => { component={adjustedComponent} allComponents={screenData.components} formData={formData} + originalData={originalData} // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (UPDATE ํŒ๋‹จ์šฉ) onFormDataChange={(fieldName, value) => { - setFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); + console.log("๐Ÿ”ง [ScreenModal] onFormDataChange ํ˜ธ์ถœ:", { fieldName, value }); + setFormData((prev) => { + const newFormData = { + ...prev, + [fieldName]: value, + }; + console.log("๐Ÿ”ง [ScreenModal] formData ์—…๋ฐ์ดํŠธ:", { prev, newFormData }); + return newFormData; + }); }} onRefresh={() => { // ๋ถ€๋ชจ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์†ก diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index e351b68c..c9535285 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -53,6 +53,8 @@ interface InteractiveScreenViewerProps { disabledFields?: string[]; // ๐Ÿ†• EditModal ๋‚ด๋ถ€์ธ์ง€ ์—ฌ๋ถ€ (button-primary๊ฐ€ EditModal์˜ handleSave ์‚ฌ์šฉํ•˜๋„๋ก) isInModal?: boolean; + // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ UPDATE ํŒ๋‹จ์šฉ) + originalData?: Record | null; } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -72,6 +74,7 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ํ™•์ธ const { userName: authUserName, user: authUser } = useAuth(); @@ -331,6 +334,7 @@ export const InteractiveScreenViewerDynamic: React.FC = ({ , newData: Record, tableName: string, diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index fe93f4af..0ea687bf 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -337,6 +337,11 @@ export const DynamicComponentRenderer: React.FC = // onChange ํ•ธ๋“ค๋Ÿฌ - ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ const handleChange = (value: any) => { + // autocomplete-search-input, entity-search-input์€ ์ž์ฒด์ ์œผ๋กœ onFormDataChange๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ ์ค‘๋ณต ์ €์žฅ ๋ฐฉ์ง€ + if (componentType === "autocomplete-search-input" || componentType === "entity-search-input") { + return; + } + // React ์ด๋ฒคํŠธ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ ๊ฐ’ ์ถ”์ถœ let actualValue = value; if (value && typeof value === "object" && value.nativeEvent && value.target) { diff --git a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx index e3572e33..1c5920f0 100644 --- a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx +++ b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx @@ -57,20 +57,42 @@ export function AutocompleteSearchInputComponent({ filterCondition, }); + // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ref๋กœ๋„ ์œ ์ง€ (๋ฆฌ๋ Œ๋”๋ง ์‹œ ์ดˆ๊ธฐํ™” ๋ฐฉ์ง€) + const selectedDataRef = useRef(null); + const inputValueRef = useRef(""); + // formData์—์„œ ํ˜„์žฌ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (isInteractive ๋ชจ๋“œ) const currentValue = isInteractive && formData && component?.columnName ? formData[component.columnName] : value; - // value๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ํ‘œ์‹œ๊ฐ’ ์—…๋ฐ์ดํŠธ + // selectedData ๋ณ€๊ฒฝ ์‹œ ref๋„ ์—…๋ฐ์ดํŠธ useEffect(() => { - if (currentValue && selectedData) { - setInputValue(selectedData[displayField] || ""); - } else if (!currentValue) { - setInputValue(""); - setSelectedData(null); + if (selectedData) { + selectedDataRef.current = selectedData; + inputValueRef.current = inputValue; } - }, [currentValue, displayField, selectedData]); + }, [selectedData, inputValue]); + + // ๋ฆฌ๋ Œ๋”๋ง ์‹œ ref์—์„œ ๊ฐ’ ๋ณต์› + useEffect(() => { + if (!selectedData && selectedDataRef.current) { + setSelectedData(selectedDataRef.current); + setInputValue(inputValueRef.current); + } + }, []); + + // value๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ํ‘œ์‹œ๊ฐ’ ์—…๋ฐ์ดํŠธ - ๋‹จ, selectedData๊ฐ€ ์žˆ์œผ๋ฉด ์œ ์ง€ + useEffect(() => { + // selectedData๊ฐ€ ์žˆ์œผ๋ฉด ํ‘œ์‹œ๊ฐ’ ์œ ์ง€ (์‚ฌ์šฉ์ž๊ฐ€ ๋ฐฉ๊ธˆ ์„ ํƒํ•œ ๊ฒฝ์šฐ) + if (selectedData || selectedDataRef.current) { + return; + } + + if (!currentValue) { + setInputValue(""); + } + }, [currentValue, selectedData]); // ์™ธ๋ถ€ ํด๋ฆญ ๊ฐ์ง€ useEffect(() => { diff --git a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx index e6942704..d2290c2f 100644 --- a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx +++ b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -21,7 +21,9 @@ export function AutocompleteSearchInputConfigPanel({ config, onConfigChange, }: AutocompleteSearchInputConfigPanelProps) { - const [localConfig, setLocalConfig] = useState(config); + // ์ดˆ๊ธฐํ™” ์—ฌ๋ถ€ ์ถ”์  (์ฒซ ๋งˆ์šดํŠธ ์‹œ์—๋งŒ config๋กœ ์ดˆ๊ธฐํ™”) + const isInitialized = useRef(false); + const [localConfig, setLocalConfig] = useState(config); const [allTables, setAllTables] = useState([]); const [sourceTableColumns, setSourceTableColumns] = useState([]); const [targetTableColumns, setTargetTableColumns] = useState([]); @@ -32,12 +34,21 @@ export function AutocompleteSearchInputConfigPanel({ const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false); const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false); + // ์ฒซ ๋งˆ์šดํŠธ ์‹œ์—๋งŒ config๋กœ ์ดˆ๊ธฐํ™” (์ดํ›„์—๋Š” localConfig ์œ ์ง€) useEffect(() => { - setLocalConfig(config); + if (!isInitialized.current && config) { + setLocalConfig(config); + isInitialized.current = true; + } }, [config]); const updateConfig = (updates: Partial) => { const newConfig = { ...localConfig, ...updates }; + console.log("๐Ÿ”ง [AutocompleteConfigPanel] updateConfig:", { + updates, + localConfig, + newConfig, + }); setLocalConfig(newConfig); onConfigChange(newConfig); }; @@ -325,10 +336,11 @@ export function AutocompleteSearchInputConfigPanel({
- updateFieldMapping(index, { targetField: value }) - } + value={mapping.targetField || undefined} + onValueChange={(value) => { + console.log("๐Ÿ”ง [Select] targetField ๋ณ€๊ฒฝ:", value); + updateFieldMapping(index, { targetField: value }); + }} disabled={!localConfig.targetTable || isLoadingTargetColumns} > diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 1e00442f..180dacaa 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -694,7 +694,7 @@ export const ButtonPrimaryComponent: React.FC = ({ const context: ButtonActionContext = { formData: formData || {}, - originalData: originalData || {}, // ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ์šฉ ์›๋ณธ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + originalData: originalData, // ๐Ÿ”ง ๋นˆ ๊ฐ์ฒด ๋Œ€์‹  undefined ์œ ์ง€ (UPDATE ํŒ๋‹จ์— ์‚ฌ์šฉ) screenId: effectiveScreenId, // ๐Ÿ†• ScreenContext์—์„œ ๊ฐ€์ ธ์˜จ ๊ฐ’ ์‚ฌ์šฉ tableName: effectiveTableName, // ๐Ÿ†• ScreenContext์—์„œ ๊ฐ€์ ธ์˜จ ๊ฐ’ ์‚ฌ์šฉ userId, // ๐Ÿ†• ์‚ฌ์šฉ์ž ID diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index f7f1fc20..ad441754 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -414,9 +414,20 @@ export class ButtonActionExecutor { const primaryKeys = primaryKeyResult.data || []; const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys); - // ๋‹จ์ˆœํžˆ ๊ธฐ๋ณธํ‚ค ๊ฐ’ ์กด์žฌ ์—ฌ๋ถ€๋กœ ํŒ๋‹จ (์ž„์‹œ) - // TODO: ์‹ค์ œ ํ…Œ์ด๋ธ”์—์„œ ๊ธฐ๋ณธํ‚ค๋กœ ๋ ˆ์ฝ”๋“œ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธํ•˜๋Š” API ํ•„์š” - const isUpdate = false; // ํ˜„์žฌ๋Š” ํ•ญ์ƒ INSERT๋กœ ์ฒ˜๋ฆฌ + // ๐Ÿ”ง ์ˆ˜์ •: originalData๊ฐ€ ์žˆ๊ณ  ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด UPDATE ๋ชจ๋“œ๋กœ ์ฒ˜๋ฆฌ + // originalData๋Š” ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ editData๋กœ ์ „๋‹ฌ๋˜์–ด context.originalData๋กœ ์„ค์ •๋จ + // ๋นˆ ๊ฐ์ฒด {}๋„ truthy์ด๋ฏ€๋กœ Object.keys๋กœ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์œ ๋ฌด ํ™•์ธ + const hasRealOriginalData = originalData && Object.keys(originalData).length > 0; + const isUpdate = hasRealOriginalData && !!primaryKeyValue; + + console.log("๐Ÿ” [handleSave] INSERT/UPDATE ํŒ๋‹จ:", { + hasOriginalData: !!originalData, + hasRealOriginalData, + originalDataKeys: originalData ? Object.keys(originalData) : [], + primaryKeyValue, + isUpdate, + primaryKeys, + }); let saveResult; diff --git a/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_๋ฐ_๋ฐ์ดํ„ฐ_์ „๋‹ฌ_์‹œ์Šคํ…œ_๊ตฌํ˜„_๊ณ„ํš์„œ.md b/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_๋ฐ_๋ฐ์ดํ„ฐ_์ „๋‹ฌ_์‹œ์Šคํ…œ_๊ตฌํ˜„_๊ณ„ํš์„œ.md index 7aed8903..716d7f98 100644 --- a/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_๋ฐ_๋ฐ์ดํ„ฐ_์ „๋‹ฌ_์‹œ์Šคํ…œ_๊ตฌํ˜„_๊ณ„ํš์„œ.md +++ b/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_๋ฐ_๋ฐ์ดํ„ฐ_์ „๋‹ฌ_์‹œ์Šคํ…œ_๊ตฌํ˜„_๊ณ„ํš์„œ.md @@ -1,6 +1,7 @@ # ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ๋ฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ์Šคํ…œ ๊ตฌํ˜„ ๊ณ„ํš์„œ ## ๐Ÿ“‹ ๋ชฉ์ฐจ + 1. [๊ฐœ์š”](#๊ฐœ์š”) 2. [ํ˜„์žฌ ๋ฌธ์ œ์ ](#ํ˜„์žฌ-๋ฌธ์ œ์ ) 3. [๋ชฉํ‘œ](#๋ชฉํ‘œ) @@ -17,9 +18,11 @@ ## ๊ฐœ์š” ### ๋ฐฐ๊ฒฝ + ํ˜„์žฌ ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ์€ ๋‹จ์ผ ํ™”๋ฉด ๋‹จ์œ„๋กœ๋งŒ ๋™์ž‘ํ•˜๋ฉฐ, ํ™”๋ฉด ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์ด๋‚˜ ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์‹ค๋ฌด์—์„œ๋Š” "์ž…๊ณ  ๋“ฑ๋ก"๊ณผ ๊ฐ™์ด **์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•˜๊ณ  ์šฐ์ธก์œผ๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๋Š”** ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ### ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ + - **ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ**: ๊ธฐ์กด ํ™”๋ฉด์„ ๋‹ค๋ฅธ ํ™”๋ฉด ์•ˆ์— ์žฌ์‚ฌ์šฉ - **๋ฐ์ดํ„ฐ ์ „๋‹ฌ**: ํ•œ ํ™”๋ฉด์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฅธ ํ™”๋ฉด์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ - **์œ ์—ฐํ•œ ๋งคํ•‘**: ํ…Œ์ด๋ธ”๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ž…๋ ฅ ํ•„๋“œ, ์…€๋ ‰ํŠธ ๋ฐ•์Šค, ๋ฆฌํ”ผํ„ฐ ๋“ฑ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ ์ฃผ์ž… ๊ฐ€๋Šฅ @@ -30,18 +33,22 @@ ## ํ˜„์žฌ ๋ฌธ์ œ์  ### 1. ํ™”๋ฉด ์žฌ์‚ฌ์šฉ ๋ถˆ๊ฐ€ + - ๊ฐ ํ™”๋ฉด์€ ๋…๋ฆฝ์ ์œผ๋กœ๋งŒ ๋™์ž‘ - ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์—ฌ๋Ÿฌ ํ™”๋ฉด์—์„œ ์ค‘๋ณต ๊ตฌํ˜„ ### 2. ํ™”๋ฉด ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ถˆ๊ฐ€ + - ํ•œ ํ™”๋ฉด์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฅธ ํ™”๋ฉด์œผ๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์—†์Œ - ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ๋ณต์‚ฌ/๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•ด์•ผ ํ•จ ### 3. ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌํ˜„ ๋ถˆ๊ฐ€ + - "๋ฐœ์ฃผ ๋ชฉ๋ก ์กฐํšŒ โ†’ ํ’ˆ๋ชฉ ์„ ํƒ โ†’ ์ž…๊ณ  ๋“ฑ๋ก"๊ณผ ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋‹จ์ผ ํ™”๋ฉด์—์„œ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€ - ์—ฌ๋Ÿฌ ํ™”๋ฉด์„ ์˜ค๊ฐ€๋ฉฐ ์ž‘์—…ํ•ด์•ผ ํ•˜๋Š” ๋ถˆํŽธํ•จ ### 4. ์ปดํฌ๋„ŒํŠธ๋ณ„ ๋ฐ์ดํ„ฐ ์ฃผ์ž… ๋ถˆ๊ฐ€ + - ํ…Œ์ด๋ธ”์—๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Œ - ์ž…๋ ฅ ํ•„๋“œ, ์…€๋ ‰ํŠธ ๋ฐ•์Šค ๋“ฑ์— ์ž๋™์œผ๋กœ ๊ฐ’์„ ์„ค์ •ํ•  ์ˆ˜ ์—†์Œ @@ -50,12 +57,14 @@ ## ๋ชฉํ‘œ ### ์ฃผ์š” ๋ชฉํ‘œ + 1. **ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ์‹œ์Šคํ…œ ๊ตฌ์ถ•**: ๊ธฐ์กด ํ™”๋ฉด์„ ์ปจํ…Œ์ด๋„ˆ๋กœ ์‚ฌ์šฉ 2. **๋ฒ”์šฉ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ์Šคํ…œ**: ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ง€์› 3. **์‹œ๊ฐ์  ๋งคํ•‘ ์„ค์ • UI**: ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ๋งคํ•‘ ๊ทœ์น™ ์„ค์ • 4. **์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ**: ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๊ฒฐ๊ณผ๋ฅผ ์ฆ‰์‹œ ํ™•์ธ ### ๋ถ€๊ฐ€ ๋ชฉํ‘œ + - ์กฐ๊ฑด๋ถ€ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (ํ•„ํ„ฐ๋ง) - ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ํ•จ์ˆ˜ (ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜ ๋“ฑ) - ์–‘๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” @@ -128,19 +137,19 @@ ```sql CREATE TABLE screen_embedding ( id SERIAL PRIMARY KEY, - + -- ๋ถ€๋ชจ ํ™”๋ฉด (์ปจํ…Œ์ด๋„ˆ) parent_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), - + -- ์ž์‹ ํ™”๋ฉด (์ž„๋ฒ ๋“œ๋  ํ™”๋ฉด) child_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), - + -- ์ž„๋ฒ ๋”ฉ ์œ„์น˜ position VARCHAR(20) NOT NULL, -- 'left', 'right', 'top', 'bottom', 'center' - + -- ์ž„๋ฒ ๋”ฉ ๋ชจ๋“œ mode VARCHAR(20) NOT NULL, -- 'view', 'select', 'form', 'edit' - + -- ์ถ”๊ฐ€ ์„ค์ • config JSONB, -- { @@ -150,18 +159,18 @@ CREATE TABLE screen_embedding ( -- "multiSelect": true, -- "showToolbar": true -- } - + -- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ company_code VARCHAR(20) NOT NULL, - + -- ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by VARCHAR(50), - - CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id) + + CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE, - CONSTRAINT fk_child_screen FOREIGN KEY (child_screen_id) + CONSTRAINT fk_child_screen FOREIGN KEY (child_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE ); @@ -175,17 +184,17 @@ CREATE INDEX idx_screen_embedding_child ON screen_embedding(child_screen_id, com ```sql CREATE TABLE screen_data_transfer ( id SERIAL PRIMARY KEY, - + -- ์†Œ์Šค ํ™”๋ฉด (๋ฐ์ดํ„ฐ ์ œ๊ณต) source_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), - + -- ํƒ€๊ฒŸ ํ™”๋ฉด (๋ฐ์ดํ„ฐ ์ˆ˜์‹ ) target_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), - + -- ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ (์„ ํƒ ์˜์—ญ) source_component_id VARCHAR(100), source_component_type VARCHAR(50), -- 'table', 'list', 'grid' - + -- ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ์„ค์ • (JSONB ๋ฐฐ์—ด) data_receivers JSONB NOT NULL, -- [ @@ -207,7 +216,7 @@ CREATE TABLE screen_data_transfer ( -- } -- } -- ] - + -- ์ „๋‹ฌ ๋ฒ„ํŠผ ์„ค์ • button_config JSONB, -- { @@ -221,18 +230,18 @@ CREATE TABLE screen_data_transfer ( -- "customValidation": "function(rows) { return rows.length > 0; }" -- } -- } - + -- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ company_code VARCHAR(20) NOT NULL, - + -- ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by VARCHAR(50), - - CONSTRAINT fk_source_screen FOREIGN KEY (source_screen_id) + + CONSTRAINT fk_source_screen FOREIGN KEY (source_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE, - CONSTRAINT fk_target_screen FOREIGN KEY (target_screen_id) + CONSTRAINT fk_target_screen FOREIGN KEY (target_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE ); @@ -246,19 +255,19 @@ CREATE INDEX idx_screen_data_transfer_target ON screen_data_transfer(target_scre ```sql CREATE TABLE screen_split_panel ( id SERIAL PRIMARY KEY, - + -- ๋ถ€๋ชจ ํ™”๋ฉด (๋ถ„ํ•  ํŒจ๋„ ์ปจํ…Œ์ด๋„ˆ) screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), - + -- ์ขŒ์ธก ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ left_embedding_id INTEGER REFERENCES screen_embedding(id), - + -- ์šฐ์ธก ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ right_embedding_id INTEGER REFERENCES screen_embedding(id), - + -- ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์„ค์ • data_transfer_id INTEGER REFERENCES screen_data_transfer(id), - + -- ๋ ˆ์ด์•„์›ƒ ์„ค์ • layout_config JSONB, -- { @@ -268,21 +277,21 @@ CREATE TABLE screen_split_panel ( -- "minRightWidth": 400, -- "orientation": "horizontal" // 'horizontal' | 'vertical' -- } - + -- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ company_code VARCHAR(20) NOT NULL, - + -- ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), - - CONSTRAINT fk_screen FOREIGN KEY (screen_id) + + CONSTRAINT fk_screen FOREIGN KEY (screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE, - CONSTRAINT fk_left_embedding FOREIGN KEY (left_embedding_id) + CONSTRAINT fk_left_embedding FOREIGN KEY (left_embedding_id) REFERENCES screen_embedding(id) ON DELETE SET NULL, - CONSTRAINT fk_right_embedding FOREIGN KEY (right_embedding_id) + CONSTRAINT fk_right_embedding FOREIGN KEY (right_embedding_id) REFERENCES screen_embedding(id) ON DELETE SET NULL, - CONSTRAINT fk_data_transfer FOREIGN KEY (data_transfer_id) + CONSTRAINT fk_data_transfer FOREIGN KEY (data_transfer_id) REFERENCES screen_data_transfer(id) ON DELETE SET NULL ); @@ -298,19 +307,14 @@ CREATE INDEX idx_screen_split_panel_screen ON screen_split_panel(screen_id, comp ```typescript // ์ž„๋ฒ ๋”ฉ ๋ชจ๋“œ -type EmbeddingMode = - | "view" // ์ฝ๊ธฐ ์ „์šฉ - | "select" // ์„ ํƒ ๋ชจ๋“œ (์ฒดํฌ๋ฐ•์Šค) - | "form" // ํผ ์ž…๋ ฅ ๋ชจ๋“œ - | "edit"; // ํŽธ์ง‘ ๋ชจ๋“œ +type EmbeddingMode = + | "view" // ์ฝ๊ธฐ ์ „์šฉ + | "select" // ์„ ํƒ ๋ชจ๋“œ (์ฒดํฌ๋ฐ•์Šค) + | "form" // ํผ ์ž…๋ ฅ ๋ชจ๋“œ + | "edit"; // ํŽธ์ง‘ ๋ชจ๋“œ // ์ž„๋ฒ ๋”ฉ ์œ„์น˜ -type EmbeddingPosition = - | "left" - | "right" - | "top" - | "bottom" - | "center"; +type EmbeddingPosition = "left" | "right" | "top" | "bottom" | "center"; // ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ์„ค์ • interface ScreenEmbedding { @@ -320,8 +324,8 @@ interface ScreenEmbedding { position: EmbeddingPosition; mode: EmbeddingMode; config: { - width?: string; // "50%", "400px" - height?: string; // "100%", "600px" + width?: string; // "50%", "400px" + height?: string; // "100%", "600px" resizable?: boolean; multiSelect?: boolean; showToolbar?: boolean; @@ -336,40 +340,40 @@ interface ScreenEmbedding { ```typescript // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… -type ComponentType = - | "table" // ํ…Œ์ด๋ธ” - | "input" // ์ž…๋ ฅ ํ•„๋“œ - | "select" // ์…€๋ ‰ํŠธ ๋ฐ•์Šค - | "textarea" // ํ…์ŠคํŠธ ์˜์—ญ - | "checkbox" // ์ฒดํฌ๋ฐ•์Šค - | "radio" // ๋ผ๋””์˜ค ๋ฒ„ํŠผ - | "date" // ๋‚ ์งœ ์„ ํƒ - | "repeater" // ๋ฆฌํ”ผํ„ฐ (๋ฐ˜๋ณต ๊ทธ๋ฃน) - | "form-group" // ํผ ๊ทธ๋ฃน - | "hidden"; // ํžˆ๋“  ํ•„๋“œ +type ComponentType = + | "table" // ํ…Œ์ด๋ธ” + | "input" // ์ž…๋ ฅ ํ•„๋“œ + | "select" // ์…€๋ ‰ํŠธ ๋ฐ•์Šค + | "textarea" // ํ…์ŠคํŠธ ์˜์—ญ + | "checkbox" // ์ฒดํฌ๋ฐ•์Šค + | "radio" // ๋ผ๋””์˜ค ๋ฒ„ํŠผ + | "date" // ๋‚ ์งœ ์„ ํƒ + | "repeater" // ๋ฆฌํ”ผํ„ฐ (๋ฐ˜๋ณต ๊ทธ๋ฃน) + | "form-group" // ํผ ๊ทธ๋ฃน + | "hidden"; // ํžˆ๋“  ํ•„๋“œ // ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ๋ชจ๋“œ -type DataReceiveMode = - | "append" // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ - | "replace" // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ฎ์–ด์“ฐ๊ธฐ - | "merge"; // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉ (ํ‚ค ๊ธฐ์ค€) +type DataReceiveMode = + | "append" // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ + | "replace" // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ฎ์–ด์“ฐ๊ธฐ + | "merge"; // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉ (ํ‚ค ๊ธฐ์ค€) // ๋ณ€ํ™˜ ํ•จ์ˆ˜ -type TransformFunction = - | "none" // ๋ณ€ํ™˜ ์—†์Œ - | "sum" // ํ•ฉ๊ณ„ - | "average" // ํ‰๊ท  - | "count" // ๊ฐœ์ˆ˜ - | "min" // ์ตœ์†Œ๊ฐ’ - | "max" // ์ตœ๋Œ€๊ฐ’ - | "first" // ์ฒซ ๋ฒˆ์งธ ๊ฐ’ - | "last" // ๋งˆ์ง€๋ง‰ ๊ฐ’ - | "concat" // ๋ฌธ์ž์—ด ๊ฒฐํ•ฉ - | "join" // ๋ฐฐ์—ด ๊ฒฐํ•ฉ - | "custom"; // ์ปค์Šคํ…€ ํ•จ์ˆ˜ +type TransformFunction = + | "none" // ๋ณ€ํ™˜ ์—†์Œ + | "sum" // ํ•ฉ๊ณ„ + | "average" // ํ‰๊ท  + | "count" // ๊ฐœ์ˆ˜ + | "min" // ์ตœ์†Œ๊ฐ’ + | "max" // ์ตœ๋Œ€๊ฐ’ + | "first" // ์ฒซ ๋ฒˆ์งธ ๊ฐ’ + | "last" // ๋งˆ์ง€๋ง‰ ๊ฐ’ + | "concat" // ๋ฌธ์ž์—ด ๊ฒฐํ•ฉ + | "join" // ๋ฐฐ์—ด ๊ฒฐํ•ฉ + | "custom"; // ์ปค์Šคํ…€ ํ•จ์ˆ˜ // ์กฐ๊ฑด ์—ฐ์‚ฐ์ž -type ConditionOperator = +type ConditionOperator = | "equals" | "notEquals" | "contains" @@ -383,12 +387,12 @@ type ConditionOperator = // ๋งคํ•‘ ๊ทœ์น™ interface MappingRule { - sourceField: string; // ์†Œ์Šค ํ•„๋“œ๋ช… - targetField: string; // ํƒ€๊ฒŸ ํ•„๋“œ๋ช… + sourceField: string; // ์†Œ์Šค ํ•„๋“œ๋ช… + targetField: string; // ํƒ€๊ฒŸ ํ•„๋“œ๋ช… transform?: TransformFunction; // ๋ณ€ํ™˜ ํ•จ์ˆ˜ - transformConfig?: any; // ๋ณ€ํ™˜ ํ•จ์ˆ˜ ์„ค์ • - defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ - required?: boolean; // ํ•„์ˆ˜ ์—ฌ๋ถ€ + transformConfig?: any; // ๋ณ€ํ™˜ ํ•จ์ˆ˜ ์„ค์ • + defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ + required?: boolean; // ํ•„์ˆ˜ ์—ฌ๋ถ€ } // ์กฐ๊ฑด @@ -400,16 +404,16 @@ interface Condition { // ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž interface DataReceiver { - targetComponentId: string; // ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ ID + targetComponentId: string; // ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ ID targetComponentType: ComponentType; mode: DataReceiveMode; mappingRules: MappingRule[]; - condition?: Condition; // ์กฐ๊ฑด๋ถ€ ์ „๋‹ฌ + condition?: Condition; // ์กฐ๊ฑด๋ถ€ ์ „๋‹ฌ validation?: { required?: boolean; minRows?: number; maxRows?: number; - customValidation?: string; // JavaScript ํ•จ์ˆ˜ ๋ฌธ์ž์—ด + customValidation?: string; // JavaScript ํ•จ์ˆ˜ ๋ฌธ์ž์—ด }; } @@ -447,10 +451,10 @@ interface ScreenDataTransfer { ```typescript // ๋ ˆ์ด์•„์›ƒ ์„ค์ • interface LayoutConfig { - splitRatio: number; // 0-100 (์ขŒ์ธก ๋น„์œจ) + splitRatio: number; // 0-100 (์ขŒ์ธก ๋น„์œจ) resizable: boolean; - minLeftWidth?: number; // ์ตœ์†Œ ์ขŒ์ธก ๋„ˆ๋น„ (px) - minRightWidth?: number; // ์ตœ์†Œ ์šฐ์ธก ๋„ˆ๋น„ (px) + minLeftWidth?: number; // ์ตœ์†Œ ์ขŒ์ธก ๋„ˆ๋น„ (px) + minRightWidth?: number; // ์ตœ์†Œ ์šฐ์ธก ๋„ˆ๋น„ (px) orientation: "horizontal" | "vertical"; } @@ -473,22 +477,22 @@ interface ScreenSplitPanel { interface DataReceivable { // ์ปดํฌ๋„ŒํŠธ ID componentId: string; - + // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… componentType: ComponentType; - + // ๋ฐ์ดํ„ฐ ์ˆ˜์‹  receiveData(data: any[], mode: DataReceiveMode): Promise; - + // ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ getData(): any; - + // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” clearData(): void; - + // ๊ฒ€์ฆ validate(): boolean; - + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ onDataReceived?: (data: any[]) => void; onDataCleared?: () => void; @@ -498,13 +502,13 @@ interface DataReceivable { interface Selectable { // ์„ ํƒ๋œ ํ–‰/ํ•ญ๋ชฉ ๊ฐ€์ ธ์˜ค๊ธฐ getSelectedRows(): any[]; - + // ์„ ํƒ ์ดˆ๊ธฐํ™” clearSelection(): void; - + // ์ „์ฒด ์„ ํƒ selectAll(): void; - + // ์„ ํƒ ์ด๋ฒคํŠธ onSelectionChanged?: (selectedRows: any[]) => void; } @@ -522,51 +526,62 @@ interface ScreenSplitPanelProps { onDataTransferred?: (data: any[]) => void; } -export function ScreenSplitPanel({ config, onDataTransferred }: ScreenSplitPanelProps) { +export function ScreenSplitPanel({ + config, + onDataTransferred, +}: ScreenSplitPanelProps) { const leftScreenRef = useRef(null); const rightScreenRef = useRef(null); const [splitRatio, setSplitRatio] = useState(config.layoutConfig.splitRatio); - + // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํ•ธ๋“ค๋Ÿฌ const handleTransferData = async () => { // 1. ์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ const selectedRows = leftScreenRef.current?.getSelectedRows() || []; - + if (selectedRows.length === 0) { toast.error("์„ ํƒ๋œ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค."); return; } - + // 2. ๊ฒ€์ฆ if (config.dataTransfer.buttonConfig.validation) { const validation = config.dataTransfer.buttonConfig.validation; - - if (validation.minSelection && selectedRows.length < validation.minSelection) { + + if ( + validation.minSelection && + selectedRows.length < validation.minSelection + ) { toast.error(`์ตœ์†Œ ${validation.minSelection}๊ฐœ ์ด์ƒ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`); return; } - - if (validation.maxSelection && selectedRows.length > validation.maxSelection) { - toast.error(`์ตœ๋Œ€ ${validation.maxSelection}๊ฐœ๊นŒ์ง€๋งŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.`); + + if ( + validation.maxSelection && + selectedRows.length > validation.maxSelection + ) { + toast.error( + `์ตœ๋Œ€ ${validation.maxSelection}๊ฐœ๊นŒ์ง€๋งŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.` + ); return; } - + if (validation.confirmMessage) { const confirmed = await confirm(validation.confirmMessage); if (!confirmed) return; } } - + // 3. ๋ฐ์ดํ„ฐ ์ „๋‹ฌ try { await rightScreenRef.current?.receiveData( selectedRows, config.dataTransfer.dataReceivers ); - + toast.success("๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); onDataTransferred?.(selectedRows); - + // 4. ์ขŒ์ธก ์„ ํƒ ์ดˆ๊ธฐํ™” (์˜ต์…˜) if (config.dataTransfer.buttonConfig.clearAfterTransfer) { leftScreenRef.current?.clearSelection(); @@ -576,24 +591,19 @@ export function ScreenSplitPanel({ config, onDataTransferred }: ScreenSplitPanel console.error(error); } }; - + return (
{/* ์ขŒ์ธก ํŒจ๋„ */}
- +
- + {/* ๋ฆฌ์‚ฌ์ด์ € */} {config.layoutConfig.resizable && ( - setSplitRatio(newRatio)} - /> + setSplitRatio(newRatio)} /> )} - + {/* ์ „๋‹ฌ ๋ฒ„ํŠผ */}
- + {/* ์šฐ์ธก ํŒจ๋„ */}
( - ({ embedding }, ref) => { - const [screenData, setScreenData] = useState(null); - const [selectedRows, setSelectedRows] = useState([]); - const componentRefs = useRef>(new Map()); - - // ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋“œ - useEffect(() => { - loadScreenData(embedding.childScreenId); - }, [embedding.childScreenId]); - - // ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•œ ๋ฉ”์„œ๋“œ - useImperativeHandle(ref, () => ({ - getSelectedRows: () => selectedRows, - - clearSelection: () => { - setSelectedRows([]); - }, - - receiveData: async (data: any[], receivers: DataReceiver[]) => { - // ๊ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ - for (const receiver of receivers) { - const component = componentRefs.current.get(receiver.targetComponentId); - - if (!component) { - console.warn(`์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${receiver.targetComponentId}`); - continue; - } - - // ์กฐ๊ฑด ํ™•์ธ - let filteredData = data; - if (receiver.condition) { - filteredData = filterData(data, receiver.condition); - } - - // ๋งคํ•‘ ์ ์šฉ - const mappedData = applyMappingRules(filteredData, receiver.mappingRules); - - // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ - await component.receiveData(mappedData, receiver.mode); +export const EmbeddedScreen = forwardRef< + EmbeddedScreenHandle, + EmbeddedScreenProps +>(({ embedding }, ref) => { + const [screenData, setScreenData] = useState(null); + const [selectedRows, setSelectedRows] = useState([]); + const componentRefs = useRef>(new Map()); + + // ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋“œ + useEffect(() => { + loadScreenData(embedding.childScreenId); + }, [embedding.childScreenId]); + + // ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•œ ๋ฉ”์„œ๋“œ + useImperativeHandle(ref, () => ({ + getSelectedRows: () => selectedRows, + + clearSelection: () => { + setSelectedRows([]); + }, + + receiveData: async (data: any[], receivers: DataReceiver[]) => { + // ๊ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + for (const receiver of receivers) { + const component = componentRefs.current.get(receiver.targetComponentId); + + if (!component) { + console.warn( + `์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${receiver.targetComponentId}` + ); + continue; } - }, - - getData: () => { - const allData: Record = {}; - componentRefs.current.forEach((component, id) => { - allData[id] = component.getData(); - }); - return allData; + + // ์กฐ๊ฑด ํ™•์ธ + let filteredData = data; + if (receiver.condition) { + filteredData = filterData(data, receiver.condition); + } + + // ๋งคํ•‘ ์ ์šฉ + const mappedData = applyMappingRules( + filteredData, + receiver.mappingRules + ); + + // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + await component.receiveData(mappedData, receiver.mode); } - })); - - // ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก - const registerComponent = (id: string, component: DataReceivable) => { - componentRefs.current.set(id, component); - }; - - return ( -
- {screenData && ( - - )} -
- ); - } -); + }, + + getData: () => { + const allData: Record = {}; + componentRefs.current.forEach((component, id) => { + allData[id] = component.getData(); + }); + return allData; + }, + })); + + // ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก + const registerComponent = (id: string, component: DataReceivable) => { + componentRefs.current.set(id, component); + }; + + return ( +
+ {screenData && ( + + )} +
+ ); +}); ``` ### 3. DataReceivable ๊ตฌํ˜„ ์˜ˆ์‹œ @@ -716,7 +735,7 @@ class TableComponent implements DataReceivable { componentId: string; componentType: ComponentType = "table"; private rows: any[] = []; - + async receiveData(data: any[], mode: DataReceiveMode): Promise { switch (mode) { case "append": @@ -727,30 +746,30 @@ class TableComponent implements DataReceivable { break; case "merge": // ํ‚ค ๊ธฐ๋ฐ˜ ๋ณ‘ํ•ฉ (์˜ˆ: id ํ•„๋“œ) - const existingIds = new Set(this.rows.map(r => r.id)); - const newRows = data.filter(r => !existingIds.has(r.id)); + const existingIds = new Set(this.rows.map((r) => r.id)); + const newRows = data.filter((r) => !existingIds.has(r.id)); this.rows = [...this.rows, ...newRows]; break; } - + this.render(); this.onDataReceived?.(data); } - + getData(): any { return this.rows; } - + clearData(): void { this.rows = []; this.render(); this.onDataCleared?.(); } - + validate(): boolean { return this.rows.length > 0; } - + private render() { // ํ…Œ์ด๋ธ” ๋ฆฌ๋ Œ๋”๋ง } @@ -764,7 +783,7 @@ class InputComponent implements DataReceivable { componentId: string; componentType: ComponentType = "input"; private value: any = ""; - + async receiveData(data: any[], mode: DataReceiveMode): Promise { // ์ž…๋ ฅ ํ•„๋“œ๋Š” ๋‹จ์ผ ๊ฐ’์ด๋ฏ€๋กœ ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ๋งŒ ์‚ฌ์šฉ if (data.length > 0) { @@ -773,21 +792,21 @@ class InputComponent implements DataReceivable { this.onDataReceived?.(data); } } - + getData(): any { return this.value; } - + clearData(): void { this.value = ""; this.render(); this.onDataCleared?.(); } - + validate(): boolean { return this.value !== null && this.value !== undefined && this.value !== ""; } - + private render() { // ์ž…๋ ฅ ํ•„๋“œ ๋ฆฌ๋ Œ๋”๋ง } @@ -812,7 +831,7 @@ export async function getScreenEmbeddings( AND company_code = $2 ORDER BY position `; - + const result = await pool.query(query, [parentScreenId, companyCode]); return { success: true, data: result.rows }; } @@ -828,16 +847,16 @@ export async function createScreenEmbedding( ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; - + const result = await pool.query(query, [ embedding.parentScreenId, embedding.childScreenId, embedding.position, embedding.mode, JSON.stringify(embedding.config), - companyCode + companyCode, ]); - + return { success: true, data: result.rows[0] }; } @@ -850,26 +869,26 @@ export async function updateScreenEmbedding( const updates: string[] = []; const values: any[] = []; let paramIndex = 1; - + if (embedding.position) { updates.push(`position = $${paramIndex++}`); values.push(embedding.position); } - + if (embedding.mode) { updates.push(`mode = $${paramIndex++}`); values.push(embedding.mode); } - + if (embedding.config) { updates.push(`config = $${paramIndex++}`); values.push(JSON.stringify(embedding.config)); } - + updates.push(`updated_at = NOW()`); - + values.push(id, companyCode); - + const query = ` UPDATE screen_embedding SET ${updates.join(", ")} @@ -877,13 +896,13 @@ export async function updateScreenEmbedding( AND company_code = $${paramIndex++} RETURNING * `; - + const result = await pool.query(query, values); - + if (result.rowCount === 0) { return { success: false, message: "์ž„๋ฒ ๋”ฉ ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }; } - + return { success: true, data: result.rows[0] }; } @@ -896,13 +915,13 @@ export async function deleteScreenEmbedding( DELETE FROM screen_embedding WHERE id = $1 AND company_code = $2 `; - + const result = await pool.query(query, [id, companyCode]); - + if (result.rowCount === 0) { return { success: false, message: "์ž„๋ฒ ๋”ฉ ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }; } - + return { success: true }; } ``` @@ -922,13 +941,17 @@ export async function getScreenDataTransfer( AND target_screen_id = $2 AND company_code = $3 `; - - const result = await pool.query(query, [sourceScreenId, targetScreenId, companyCode]); - + + const result = await pool.query(query, [ + sourceScreenId, + targetScreenId, + companyCode, + ]); + if (result.rowCount === 0) { return { success: false, message: "๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }; } - + return { success: true, data: result.rows[0] }; } @@ -944,7 +967,7 @@ export async function createScreenDataTransfer( ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; - + const result = await pool.query(query, [ transfer.sourceScreenId, transfer.targetScreenId, @@ -952,9 +975,9 @@ export async function createScreenDataTransfer( transfer.sourceComponentType, JSON.stringify(transfer.dataReceivers), JSON.stringify(transfer.buttonConfig), - companyCode + companyCode, ]); - + return { success: true, data: result.rows[0] }; } @@ -967,21 +990,21 @@ export async function updateScreenDataTransfer( const updates: string[] = []; const values: any[] = []; let paramIndex = 1; - + if (transfer.dataReceivers) { updates.push(`data_receivers = $${paramIndex++}`); values.push(JSON.stringify(transfer.dataReceivers)); } - + if (transfer.buttonConfig) { updates.push(`button_config = $${paramIndex++}`); values.push(JSON.stringify(transfer.buttonConfig)); } - + updates.push(`updated_at = NOW()`); - + values.push(id, companyCode); - + const query = ` UPDATE screen_data_transfer SET ${updates.join(", ")} @@ -989,13 +1012,13 @@ export async function updateScreenDataTransfer( AND company_code = $${paramIndex++} RETURNING * `; - + const result = await pool.query(query, values); - + if (result.rowCount === 0) { return { success: false, message: "๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }; } - + return { success: true, data: result.rows[0] }; } ``` @@ -1021,13 +1044,13 @@ export async function getScreenSplitPanel( WHERE ssp.screen_id = $1 AND ssp.company_code = $2 `; - + const result = await pool.query(query, [screenId, companyCode]); - + if (result.rowCount === 0) { return { success: false, message: "๋ถ„ํ•  ํŒจ๋„ ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }; } - + return { success: true, data: result.rows[0] }; } @@ -1037,19 +1060,28 @@ export async function createScreenSplitPanel( companyCode: string ): Promise> { const client = await pool.connect(); - + try { await client.query("BEGIN"); - + // 1. ์ขŒ์ธก ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ - const leftEmbedding = await createScreenEmbedding(panel.leftEmbedding, companyCode); - + const leftEmbedding = await createScreenEmbedding( + panel.leftEmbedding, + companyCode + ); + // 2. ์šฐ์ธก ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ - const rightEmbedding = await createScreenEmbedding(panel.rightEmbedding, companyCode); - + const rightEmbedding = await createScreenEmbedding( + panel.rightEmbedding, + companyCode + ); + // 3. ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์„ค์ • ์ƒ์„ฑ - const dataTransfer = await createScreenDataTransfer(panel.dataTransfer, companyCode); - + const dataTransfer = await createScreenDataTransfer( + panel.dataTransfer, + companyCode + ); + // 4. ๋ถ„ํ•  ํŒจ๋„ ์ƒ์„ฑ const query = ` INSERT INTO screen_split_panel ( @@ -1058,18 +1090,18 @@ export async function createScreenSplitPanel( ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; - + const result = await client.query(query, [ panel.screenId, leftEmbedding.data!.id, rightEmbedding.data!.id, dataTransfer.data!.id, JSON.stringify(panel.layoutConfig), - companyCode + companyCode, ]); - + await client.query("COMMIT"); - + return { success: true, data: result.rows[0] }; } catch (error) { await client.query("ROLLBACK"); @@ -1087,6 +1119,7 @@ export async function createScreenSplitPanel( ### Phase 1: ๊ธฐ๋ณธ ์ธํ”„๋ผ ๊ตฌ์ถ• (1-2์ฃผ) #### 1.1 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + - [ ] `screen_embedding` ํ…Œ์ด๋ธ” ์ƒ์„ฑ - [ ] `screen_data_transfer` ํ…Œ์ด๋ธ” ์ƒ์„ฑ - [ ] `screen_split_panel` ํ…Œ์ด๋ธ” ์ƒ์„ฑ @@ -1094,12 +1127,14 @@ export async function createScreenSplitPanel( - [ ] ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… #### 1.2 ํƒ€์ž… ์ •์˜ + - [ ] TypeScript ์ธํ„ฐํŽ˜์ด์Šค ์ž‘์„ฑ - [ ] `types/screen-embedding.ts` - [ ] `types/data-transfer.ts` - [ ] `types/split-panel.ts` #### 1.3 ๋ฐฑ์—”๋“œ API + - [ ] ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ CRUD API - [ ] ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์„ค์ • CRUD API - [ ] ๋ถ„ํ•  ํŒจ๋„ CRUD API @@ -1108,12 +1143,14 @@ export async function createScreenSplitPanel( ### Phase 2: ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ๊ธฐ๋Šฅ (2-3์ฃผ) #### 2.1 EmbeddedScreen ์ปดํฌ๋„ŒํŠธ + - [ ] ๊ธฐ๋ณธ ์ž„๋ฒ ๋”ฉ ๊ธฐ๋Šฅ - [ ] ๋ชจ๋“œ๋ณ„ ๋ Œ๋”๋ง (view, select, form, edit) - [ ] ์„ ํƒ ๋ชจ๋“œ ๊ตฌํ˜„ (์ฒดํฌ๋ฐ•์Šค) - [ ] ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง #### 2.2 DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ + - [ ] TableComponent - [ ] InputComponent - [ ] SelectComponent @@ -1123,6 +1160,7 @@ export async function createScreenSplitPanel( - [ ] HiddenComponent #### 2.3 ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก ์‹œ์Šคํ…œ + - [ ] ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ž๋™ ๋“ฑ๋ก - [ ] ์ปดํฌ๋„ŒํŠธ ID ๊ด€๋ฆฌ - [ ] ์ปดํฌ๋„ŒํŠธ ์ฐธ์กฐ ๊ด€๋ฆฌ @@ -1130,6 +1168,7 @@ export async function createScreenSplitPanel( ### Phase 3: ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ์Šคํ…œ (2-3์ฃผ) #### 3.1 ๋งคํ•‘ ์—”์ง„ + - [ ] ๋งคํ•‘ ๊ทœ์น™ ํŒŒ์‹ฑ - [ ] ํ•„๋“œ ๋งคํ•‘ ์ ์šฉ - [ ] ๋ณ€ํ™˜ ํ•จ์ˆ˜ ๊ตฌํ˜„ @@ -1139,11 +1178,13 @@ export async function createScreenSplitPanel( - [ ] concat, join #### 3.2 ์กฐ๊ฑด๋ถ€ ์ „๋‹ฌ + - [ ] ์กฐ๊ฑด ํŒŒ์‹ฑ - [ ] ํ•„ํ„ฐ๋ง ๋กœ์ง - [ ] ๋ณตํ•ฉ ์กฐ๊ฑด ์ง€์› #### 3.3 ๊ฒ€์ฆ ์‹œ์Šคํ…œ + - [ ] ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ - [ ] ์ตœ์†Œ/์ตœ๋Œ€ ํ–‰ ์ˆ˜ ๊ฒ€์ฆ - [ ] ์ปค์Šคํ…€ ๊ฒ€์ฆ ํ•จ์ˆ˜ ์‹คํ–‰ @@ -1151,18 +1192,21 @@ export async function createScreenSplitPanel( ### Phase 4: ๋ถ„ํ•  ํŒจ๋„ UI (2-3์ฃผ) #### 4.1 ScreenSplitPanel ์ปดํฌ๋„ŒํŠธ + - [ ] ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ - [ ] ๋ฆฌ์‚ฌ์ด์ € ๊ตฌํ˜„ - [ ] ์ „๋‹ฌ ๋ฒ„ํŠผ - [ ] ๋ฐ˜์‘ํ˜• ๋””์ž์ธ #### 4.2 ์„ค์ • UI + - [ ] ํ™”๋ฉด ์„ ํƒ ๋“œ๋กญ๋‹ค์šด - [ ] ๋งคํ•‘ ๊ทœ์น™ ์„ค์ • UI - [ ] ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๋งคํ•‘ - [ ] ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ #### 4.3 ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ + - [ ] ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ - [ ] ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ - [ ] ์„ฑ๊ณต/์‹คํŒจ ํ† ์ŠคํŠธ @@ -1170,14 +1214,17 @@ export async function createScreenSplitPanel( ### Phase 5: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ (2-3์ฃผ) #### 5.1 ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” + - [ ] ์šฐ์ธก โ†’ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋ฐ˜์˜ - [ ] ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ #### 5.2 ํŠธ๋žœ์žญ์…˜ ์ง€์› + - [ ] ์ „์ฒด ์„ฑ๊ณต ๋˜๋Š” ์ „์ฒด ์‹คํŒจ - [ ] ๋กค๋ฐฑ ๊ธฐ๋Šฅ #### 5.3 ์„ฑ๋Šฅ ์ตœ์ ํ™” + - [ ] ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ - [ ] ๊ฐ€์ƒ ์Šคํฌ๋กค๋ง - [ ] ๋ฉ”๋ชจ์ด์ œ์ด์…˜ @@ -1185,15 +1232,18 @@ export async function createScreenSplitPanel( ### Phase 6: ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œํ™” (1-2์ฃผ) #### 6.1 ๋‹จ์œ„ ํ…Œ์ŠคํŠธ + - [ ] ๋งคํ•‘ ์—”์ง„ ํ…Œ์ŠคํŠธ - [ ] ๋ณ€ํ™˜ ํ•จ์ˆ˜ ํ…Œ์ŠคํŠธ - [ ] ๊ฒ€์ฆ ๋กœ์ง ํ…Œ์ŠคํŠธ #### 6.2 ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + - [ ] ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ - [ ] ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ #### 6.3 ๋ฌธ์„œํ™” + - [ ] ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ - [ ] ๊ฐœ๋ฐœ์ž ๋ฌธ์„œ - [ ] API ๋ฌธ์„œ @@ -1205,6 +1255,7 @@ export async function createScreenSplitPanel( ### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ž…๊ณ  ๋“ฑ๋ก #### ์š”๊ตฌ์‚ฌํ•ญ + - ๋ฐœ์ฃผ ๋ชฉ๋ก์—์„œ ํ’ˆ๋ชฉ์„ ์„ ํƒํ•˜์—ฌ ์ž…๊ณ  ๋“ฑ๋ก - ์„ ํƒ๋œ ํ’ˆ๋ชฉ์˜ ์ •๋ณด๋ฅผ ์ž…๊ณ  ์ฒ˜๋ฆฌ ํ’ˆ๋ชฉ ํ…Œ์ด๋ธ”์— ์ถ”๊ฐ€ - ๊ณต๊ธ‰์ž ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ์ž…๋ ฅ ํ•„๋“œ์— ์„ค์ • @@ -1216,23 +1267,23 @@ export async function createScreenSplitPanel( const ์ž…๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { screenId: 100, leftEmbedding: { - childScreenId: 10, // ๋ฐœ์ฃผ ๋ชฉ๋ก ์กฐํšŒ ํ™”๋ฉด + childScreenId: 10, // ๋ฐœ์ฃผ ๋ชฉ๋ก ์กฐํšŒ ํ™”๋ฉด position: "left", mode: "select", config: { width: "50%", multiSelect: true, showSearch: true, - showPagination: true - } + showPagination: true, + }, }, rightEmbedding: { - childScreenId: 20, // ์ž…๊ณ  ๋“ฑ๋ก ํผ ํ™”๋ฉด + childScreenId: 20, // ์ž…๊ณ  ๋“ฑ๋ก ํผ ํ™”๋ฉด position: "right", mode: "form", config: { - width: "50%" - } + width: "50%", + }, }, dataTransfer: { sourceScreenId: 10, @@ -1248,33 +1299,33 @@ const ์ž…๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { { sourceField: "ํ’ˆ๋ชฉ์ฝ”๋“œ", targetField: "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, { sourceField: "ํ’ˆ๋ชฉ๋ช…", targetField: "ํ’ˆ๋ชฉ๋ช…" }, { sourceField: "๋ฐœ์ฃผ์ˆ˜๋Ÿ‰", targetField: "๋ฐœ์ฃผ์ˆ˜๋Ÿ‰" }, - { sourceField: "๋ฏธ์ž…๊ณ ์ˆ˜๋Ÿ‰", targetField: "์ž…๊ณ ์ˆ˜๋Ÿ‰" } - ] + { sourceField: "๋ฏธ์ž…๊ณ ์ˆ˜๋Ÿ‰", targetField: "์ž…๊ณ ์ˆ˜๋Ÿ‰" }, + ], }, { targetComponentId: "input-๊ณต๊ธ‰์ž", targetComponentType: "input", mode: "replace", mappingRules: [ - { - sourceField: "๊ณต๊ธ‰์ž", + { + sourceField: "๊ณต๊ธ‰์ž", targetField: "value", - transform: "first" - } - ] + transform: "first", + }, + ], }, { targetComponentId: "input-ํ’ˆ๋ชฉ์ˆ˜", targetComponentType: "input", mode: "replace", mappingRules: [ - { - sourceField: "ํ’ˆ๋ชฉ์ฝ”๋“œ", + { + sourceField: "ํ’ˆ๋ชฉ์ฝ”๋“œ", targetField: "value", - transform: "count" - } - ] - } + transform: "count", + }, + ], + }, ], buttonConfig: { label: "์„ ํƒ ํ’ˆ๋ชฉ ์ถ”๊ฐ€", @@ -1282,23 +1333,24 @@ const ์ž…๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { icon: "ArrowRight", validation: { requireSelection: true, - minSelection: 1 - } - } + minSelection: 1, + }, + }, }, layoutConfig: { splitRatio: 50, resizable: true, minLeftWidth: 400, minRightWidth: 600, - orientation: "horizontal" - } + orientation: "horizontal", + }, }; ``` ### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ˆ˜์ฃผ ๋“ฑ๋ก #### ์š”๊ตฌ์‚ฌํ•ญ + - ๊ฒฌ์ ์„œ ๋ชฉ๋ก์—์„œ ํ’ˆ๋ชฉ์„ ์„ ํƒํ•˜์—ฌ ์ˆ˜์ฃผ ๋“ฑ๋ก - ๊ณ ๊ฐ ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ํผ์— ์„ค์ • - ํ’ˆ๋ชฉ๋ณ„ ์ˆ˜๋Ÿ‰ ๋ฐ ๊ธˆ์•ก ์ž๋™ ๊ณ„์‚ฐ @@ -1310,21 +1362,21 @@ const ์ž…๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { const ์ˆ˜์ฃผ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { screenId: 101, leftEmbedding: { - childScreenId: 30, // ๊ฒฌ์ ์„œ ๋ชฉ๋ก ์กฐํšŒ ํ™”๋ฉด + childScreenId: 30, // ๊ฒฌ์ ์„œ ๋ชฉ๋ก ์กฐํšŒ ํ™”๋ฉด position: "left", mode: "select", config: { width: "40%", - multiSelect: true - } + multiSelect: true, + }, }, rightEmbedding: { - childScreenId: 40, // ์ˆ˜์ฃผ ๋“ฑ๋ก ํผ ํ™”๋ฉด + childScreenId: 40, // ์ˆ˜์ฃผ ๋“ฑ๋ก ํผ ํ™”๋ฉด position: "right", mode: "form", config: { - width: "60%" - } + width: "60%", + }, }, dataTransfer: { sourceScreenId: 30, @@ -1339,54 +1391,55 @@ const ์ˆ˜์ฃผ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { { sourceField: "ํ’ˆ๋ชฉ๋ช…", targetField: "ํ’ˆ๋ชฉ๋ช…" }, { sourceField: "์ˆ˜๋Ÿ‰", targetField: "์ˆ˜๋Ÿ‰" }, { sourceField: "๋‹จ๊ฐ€", targetField: "๋‹จ๊ฐ€" }, - { - sourceField: "์ˆ˜๋Ÿ‰", + { + sourceField: "์ˆ˜๋Ÿ‰", targetField: "๊ธˆ์•ก", transform: "custom", transformConfig: { - formula: "์ˆ˜๋Ÿ‰ * ๋‹จ๊ฐ€" - } - } - ] + formula: "์ˆ˜๋Ÿ‰ * ๋‹จ๊ฐ€", + }, + }, + ], }, { targetComponentId: "input-๊ณ ๊ฐ๋ช…", targetComponentType: "input", mode: "replace", mappingRules: [ - { sourceField: "๊ณ ๊ฐ๋ช…", targetField: "value", transform: "first" } - ] + { sourceField: "๊ณ ๊ฐ๋ช…", targetField: "value", transform: "first" }, + ], }, { targetComponentId: "input-์ด๊ธˆ์•ก", targetComponentType: "input", mode: "replace", mappingRules: [ - { - sourceField: "๊ธˆ์•ก", + { + sourceField: "๊ธˆ์•ก", targetField: "value", - transform: "sum" - } - ] - } + transform: "sum", + }, + ], + }, ], buttonConfig: { label: "๊ฒฌ์ ์„œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", position: "center", - icon: "Download" - } + icon: "Download", + }, }, layoutConfig: { splitRatio: 40, resizable: true, - orientation: "horizontal" - } + orientation: "horizontal", + }, }; ``` ### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ์ถœ๊ณ  ๋“ฑ๋ก #### ์š”๊ตฌ์‚ฌํ•ญ + - ์žฌ๊ณ  ๋ชฉ๋ก์—์„œ ํ’ˆ๋ชฉ์„ ์„ ํƒํ•˜์—ฌ ์ถœ๊ณ  ๋“ฑ๋ก - ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ํ™•์ธ ๋ฐ ๊ฒฝ๊ณ  - ์ถœ๊ณ  ๊ฐ€๋Šฅ ์ˆ˜๋Ÿ‰๋งŒ ํ•„ํ„ฐ๋ง @@ -1398,21 +1451,21 @@ const ์ˆ˜์ฃผ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { screenId: 102, leftEmbedding: { - childScreenId: 50, // ์žฌ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ํ™”๋ฉด + childScreenId: 50, // ์žฌ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ํ™”๋ฉด position: "left", mode: "select", config: { width: "45%", - multiSelect: true - } + multiSelect: true, + }, }, rightEmbedding: { - childScreenId: 60, // ์ถœ๊ณ  ๋“ฑ๋ก ํผ ํ™”๋ฉด + childScreenId: 60, // ์ถœ๊ณ  ๋“ฑ๋ก ํผ ํ™”๋ฉด position: "right", mode: "form", config: { - width: "55%" - } + width: "55%", + }, }, dataTransfer: { sourceScreenId: 50, @@ -1426,26 +1479,26 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { { sourceField: "ํ’ˆ๋ชฉ์ฝ”๋“œ", targetField: "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, { sourceField: "ํ’ˆ๋ชฉ๋ช…", targetField: "ํ’ˆ๋ชฉ๋ช…" }, { sourceField: "์žฌ๊ณ ์ˆ˜๋Ÿ‰", targetField: "๊ฐ€์šฉ์ˆ˜๋Ÿ‰" }, - { sourceField: "์ฐฝ๊ณ ", targetField: "์ถœ๊ณ ์ฐฝ๊ณ " } + { sourceField: "์ฐฝ๊ณ ", targetField: "์ถœ๊ณ ์ฐฝ๊ณ " }, ], condition: { field: "์žฌ๊ณ ์ˆ˜๋Ÿ‰", operator: "greaterThan", - value: 0 - } + value: 0, + }, }, { targetComponentId: "input-์ด์ถœ๊ณ ์ˆ˜๋Ÿ‰", targetComponentType: "input", mode: "replace", mappingRules: [ - { - sourceField: "์žฌ๊ณ ์ˆ˜๋Ÿ‰", + { + sourceField: "์žฌ๊ณ ์ˆ˜๋Ÿ‰", targetField: "value", - transform: "sum" - } - ] - } + transform: "sum", + }, + ], + }, ], buttonConfig: { label: "์ถœ๊ณ  ํ’ˆ๋ชฉ ์ถ”๊ฐ€", @@ -1453,15 +1506,15 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { icon: "ArrowRight", validation: { requireSelection: true, - confirmMessage: "์„ ํƒํ•œ ํ’ˆ๋ชฉ์„ ์ถœ๊ณ  ์ฒ˜๋ฆฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" - } - } + confirmMessage: "์„ ํƒํ•œ ํ’ˆ๋ชฉ์„ ์ถœ๊ณ  ์ฒ˜๋ฆฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", + }, + }, }, layoutConfig: { splitRatio: 45, resizable: true, - orientation: "horizontal" - } + orientation: "horizontal", + }, }; ``` @@ -1472,11 +1525,13 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { ### 1. ์„ฑ๋Šฅ ์ตœ์ ํ™” #### ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + - ๊ฐ€์ƒ ์Šคํฌ๋กค๋ง ์ ์šฉ - ์ฒญํฌ ๋‹จ์œ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ #### ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ + - ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ์ฐธ์กฐ ํ•ด์ œ - ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ - ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํ™œ์šฉ @@ -1484,11 +1539,13 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { ### 2. ๋ณด์•ˆ #### ๊ถŒํ•œ ๊ฒ€์ฆ + - ํ™”๋ฉด ์ ‘๊ทผ ๊ถŒํ•œ ํ™•์ธ - ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๊ถŒํ•œ ํ™•์ธ - ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ฒฉ๋ฆฌ #### ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ + - ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ - SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ - XSS ๋ฐฉ์ง€ @@ -1496,22 +1553,26 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { ### 3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ #### ์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฉ”์‹œ์ง€ + - ๋ช…ํ™•ํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ - ๋ณต๊ตฌ ๋ฐฉ๋ฒ• ์•ˆ๋‚ด - ๋กœ๊ทธ ๊ธฐ๋ก #### ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ + - ๋ถ€๋ถ„ ์‹คํŒจ ์‹œ ์ „์ฒด ๋กค๋ฐฑ - ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€ ### 4. ํ™•์žฅ์„ฑ #### ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹œ์Šคํ…œ + - ์ปค์Šคํ…€ ๋ณ€ํ™˜ ํ•จ์ˆ˜ ๋“ฑ๋ก - ์ปค์Šคํ…€ ๊ฒ€์ฆ ํ•จ์ˆ˜ ๋“ฑ๋ก - ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ถ”๊ฐ€ #### ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ + - ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ „/ํ›„ ์ด๋ฒคํŠธ - ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ @@ -1520,31 +1581,37 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { ## ๋งˆ์ผ์Šคํ†ค ### M1: ๊ธฐ๋ณธ ์ธํ”„๋ผ (2์ฃผ) + - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์™„์„ฑ - ๋ฐฑ์—”๋“œ API ์™„์„ฑ - ํƒ€์ž… ์ •์˜ ์™„์„ฑ ### M2: ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ (3์ฃผ) + - EmbeddedScreen ์ปดํฌ๋„ŒํŠธ ์™„์„ฑ - DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ ์™„๋ฃŒ - ์„ ํƒ ๋ชจ๋“œ ๋™์ž‘ ํ™•์ธ ### M3: ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (3์ฃผ) + - ๋งคํ•‘ ์—”์ง„ ์™„์„ฑ - ๋ณ€ํ™˜ ํ•จ์ˆ˜ ๊ตฌํ˜„ ์™„๋ฃŒ - ์กฐ๊ฑด๋ถ€ ์ „๋‹ฌ ๋™์ž‘ ํ™•์ธ ### M4: ๋ถ„ํ•  ํŒจ๋„ UI (3์ฃผ) + - ScreenSplitPanel ์ปดํฌ๋„ŒํŠธ ์™„์„ฑ - ์„ค์ • UI ์™„์„ฑ - ์ž…๊ณ  ๋“ฑ๋ก ์‹œ๋‚˜๋ฆฌ์˜ค ์™„์„ฑ ### M5: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๋ฐ ์ตœ์ ํ™” (3์ฃผ) + - ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์™„์„ฑ - ์„ฑ๋Šฅ ์ตœ์ ํ™” ์™„๋ฃŒ - ์ „์ฒด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ### M6: ๋ฌธ์„œํ™” ๋ฐ ๋ฐฐํฌ (1์ฃผ) + - ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ ์ž‘์„ฑ - ๊ฐœ๋ฐœ์ž ๋ฌธ์„œ ์ž‘์„ฑ - ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ @@ -1567,6 +1634,7 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { ## ์„ฑ๊ณต ์ง€ํ‘œ ### ๊ธฐ๋Šฅ์  ์ง€ํ‘œ + - [ ] ์ž…๊ณ  ๋“ฑ๋ก ์‹œ๋‚˜๋ฆฌ์˜ค ์™„๋ฒฝ ๋™์ž‘ - [ ] ์ˆ˜์ฃผ ๋“ฑ๋ก ์‹œ๋‚˜๋ฆฌ์˜ค ์™„๋ฒฝ ๋™์ž‘ - [ ] ์ถœ๊ณ  ๋“ฑ๋ก ์‹œ๋‚˜๋ฆฌ์˜ค ์™„๋ฒฝ ๋™์ž‘ @@ -1574,11 +1642,13 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { - [ ] ๋ชจ๋“  ๋ณ€ํ™˜ ํ•จ์ˆ˜ ์ •์ƒ ๋™์ž‘ ### ์„ฑ๋Šฅ ์ง€ํ‘œ + - [ ] 1000๊ฐœ ํ–‰ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ < 1์ดˆ - [ ] ํ™”๋ฉด ๋กœ๋”ฉ ์‹œ๊ฐ„ < 2์ดˆ - [ ] ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ < 100MB ### ์‚ฌ์šฉ์„ฑ ์ง€ํ‘œ + - [ ] ์„ค์ • UI ์ง๊ด€์  - [ ] ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ช…ํ™• - [ ] ๋ฌธ์„œ ์™„์„ฑ๋„ 90% ์ด์ƒ @@ -1588,15 +1658,18 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { ## ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ ### ๊ธฐ์ˆ ์  ๋ฆฌ์Šคํฌ + - **๋ณต์žก๋„ ์ฆ๊ฐ€**: ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„์œผ๋กœ ๊ด€๋ฆฌ - **์„ฑ๋Šฅ ๋ฌธ์ œ**: ์ดˆ๊ธฐ๋ถ€ํ„ฐ ์ตœ์ ํ™” ๊ณ ๋ ค - **ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ**: ๊ธฐ์กด ์‹œ์Šคํ…œ๊ณผ ์ถฉ๋Œ ๋ฐฉ์ง€ ### ์ผ์ • ๋ฆฌ์Šคํฌ + - **์˜ˆ์ƒ ๊ธฐ๊ฐ„ ์ดˆ๊ณผ**: ๋ฒ„ํผ 2์ฃผ ํ™•๋ณด - **์šฐ์„ ์ˆœ์œ„ ๋ณ€๊ฒฝ**: ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๋จผ์ € ๊ตฌํ˜„ ### ์ธ๋ ฅ ๋ฆฌ์Šคํฌ + - **๋‹ด๋‹น์ž ๋ถ€์žฌ**: ๋ฌธ์„œํ™” ์ฒ ์ €ํžˆ - **์ง€์‹ ๊ณต์œ **: ์ฃผ๊ฐ„ ๋ฆฌ๋ทฐ ๋ฏธํŒ… @@ -1605,4 +1678,3 @@ const ์ถœ๊ณ ๋“ฑ๋ก_์„ค์ •: ScreenSplitPanel = { ## ๊ฒฐ๋ก  ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ๋ฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ์Šคํ…œ์€ ๋ณต์žกํ•œ ์—…๋ฌด ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋‹จ๊ณ„๋ณ„๋กœ ์ฒด๊ณ„์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ์•ฝ 3.5๊ฐœ์›” ๋‚ด์— ์™„์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - diff --git a/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_Phase1-4_๊ตฌํ˜„_์™„๋ฃŒ.md b/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_Phase1-4_๊ตฌํ˜„_์™„๋ฃŒ.md index cf4879c0..c1880ef7 100644 --- a/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_Phase1-4_๊ตฌํ˜„_์™„๋ฃŒ.md +++ b/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_Phase1-4_๊ตฌํ˜„_์™„๋ฃŒ.md @@ -21,12 +21,14 @@ **์ƒ์„ฑ๋œ ํ…Œ์ด๋ธ”**: 1. **screen_embedding** (ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ์„ค์ •) + - ํ•œ ํ™”๋ฉด์„ ๋‹ค๋ฅธ ํ™”๋ฉด ์•ˆ์— ์ž„๋ฒ ๋“œ - ์œ„์น˜ (left, right, top, bottom, center) - ๋ชจ๋“œ (view, select, form, edit) - ์„ค์ • (width, height, multiSelect ๋“ฑ) 2. **screen_data_transfer** (๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์„ค์ •) + - ์†Œ์Šค ํ™”๋ฉด โ†’ ํƒ€๊ฒŸ ํ™”๋ฉด ๋ฐ์ดํ„ฐ ์ „๋‹ฌ - ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋ฐฐ์—ด (JSONB) - ๋งคํ•‘ ๊ทœ์น™, ์กฐ๊ฑด, ๊ฒ€์ฆ @@ -38,6 +40,7 @@ - ๋ ˆ์ด์•„์›ƒ ์„ค์ • (splitRatio, resizable ๋“ฑ) **์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ**: + - ์ž…๊ณ  ๋“ฑ๋ก ์‹œ๋‚˜๋ฆฌ์˜ค ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ํฌํ•จ - ๋ฐœ์ฃผ ๋ชฉ๋ก โ†’ ์ž…๊ณ  ์ฒ˜๋ฆฌ ํ’ˆ๋ชฉ ๋งคํ•‘ ์˜ˆ์‹œ @@ -46,6 +49,7 @@ **ํŒŒ์ผ**: `frontend/types/screen-embedding.ts` **์ฃผ์š” ํƒ€์ž…**: + ```typescript // ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ - EmbeddingMode: "view" | "select" | "form" | "edit" @@ -67,13 +71,15 @@ #### 1.3 ๋ฐฑ์—”๋“œ API -**ํŒŒ์ผ**: +**ํŒŒ์ผ**: + - `backend-node/src/controllers/screenEmbeddingController.ts` - `backend-node/src/routes/screenEmbeddingRoutes.ts` **API ์—”๋“œํฌ์ธํŠธ**: **ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ**: + - `GET /api/screen-embedding?parentScreenId=1` - ๋ชฉ๋ก ์กฐํšŒ - `GET /api/screen-embedding/:id` - ์ƒ์„ธ ์กฐํšŒ - `POST /api/screen-embedding` - ์ƒ์„ฑ @@ -81,18 +87,21 @@ - `DELETE /api/screen-embedding/:id` - ์‚ญ์ œ **๋ฐ์ดํ„ฐ ์ „๋‹ฌ**: + - `GET /api/screen-data-transfer?sourceScreenId=1&targetScreenId=2` - ์กฐํšŒ - `POST /api/screen-data-transfer` - ์ƒ์„ฑ - `PUT /api/screen-data-transfer/:id` - ์ˆ˜์ • - `DELETE /api/screen-data-transfer/:id` - ์‚ญ์ œ **๋ถ„ํ•  ํŒจ๋„**: + - `GET /api/screen-split-panel/:screenId` - ์กฐํšŒ - `POST /api/screen-split-panel` - ์ƒ์„ฑ (ํŠธ๋žœ์žญ์…˜) - `PUT /api/screen-split-panel/:id` - ์ˆ˜์ • - `DELETE /api/screen-split-panel/:id` - ์‚ญ์ œ (CASCADE) **ํŠน์ง•**: + - โœ… ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ง€์› (company_code ํ•„ํ„ฐ๋ง) - โœ… ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ (๋ถ„ํ•  ํŒจ๋„ ์ƒ์„ฑ/์‚ญ์ œ) - โœ… ์™ธ๋ž˜ํ‚ค CASCADE ์ฒ˜๋ฆฌ @@ -103,25 +112,24 @@ **ํŒŒ์ผ**: `frontend/lib/api/screenEmbedding.ts` **ํ•จ์ˆ˜**: + ```typescript // ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ -- getScreenEmbeddings(parentScreenId) -- getScreenEmbeddingById(id) -- createScreenEmbedding(data) -- updateScreenEmbedding(id, data) -- deleteScreenEmbedding(id) - -// ๋ฐ์ดํ„ฐ ์ „๋‹ฌ -- getScreenDataTransfer(sourceScreenId, targetScreenId) -- createScreenDataTransfer(data) -- updateScreenDataTransfer(id, data) -- deleteScreenDataTransfer(id) - -// ๋ถ„ํ•  ํŒจ๋„ -- getScreenSplitPanel(screenId) -- createScreenSplitPanel(data) -- updateScreenSplitPanel(id, layoutConfig) -- deleteScreenSplitPanel(id) +-getScreenEmbeddings(parentScreenId) - + getScreenEmbeddingById(id) - + createScreenEmbedding(data) - + updateScreenEmbedding(id, data) - + deleteScreenEmbedding(id) - + // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + getScreenDataTransfer(sourceScreenId, targetScreenId) - + createScreenDataTransfer(data) - + updateScreenDataTransfer(id, data) - + deleteScreenDataTransfer(id) - + // ๋ถ„ํ•  ํŒจ๋„ + getScreenSplitPanel(screenId) - + createScreenSplitPanel(data) - + updateScreenSplitPanel(id, layoutConfig) - + deleteScreenSplitPanel(id); ``` --- @@ -133,6 +141,7 @@ **ํŒŒ์ผ**: `frontend/components/screen-embedding/EmbeddedScreen.tsx` **์ฃผ์š” ๊ธฐ๋Šฅ**: + - โœ… ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋“œ - โœ… ๋ชจ๋“œ๋ณ„ ๋ Œ๋”๋ง (view, select, form, edit) - โœ… ์„ ํƒ ๋ชจ๋“œ ์ง€์› (์ฒดํฌ๋ฐ•์Šค) @@ -141,6 +150,7 @@ - โœ… ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ UI **์™ธ๋ถ€ ์ธํ„ฐํŽ˜์ด์Šค** (useImperativeHandle): + ```typescript - getSelectedRows(): any[] - clearSelection(): void @@ -149,6 +159,7 @@ ``` **๋ฐ์ดํ„ฐ ์ˆ˜์‹  ํ”„๋กœ์„ธ์Šค**: + 1. ์กฐ๊ฑด ํ•„ํ„ฐ๋ง (condition) 2. ๋งคํ•‘ ๊ทœ์น™ ์ ์šฉ (mappingRules) 3. ๊ฒ€์ฆ (validation) @@ -165,10 +176,12 @@ **์ฃผ์š” ํ•จ์ˆ˜**: 1. **applyMappingRules(data, rules)** + - ์ผ๋ฐ˜ ๋งคํ•‘: ๊ฐ ํ–‰์— ๋Œ€ํ•ด ํ•„๋“œ ๋งคํ•‘ - ๋ณ€ํ™˜ ๋งคํ•‘: ์ง‘๊ณ„ ํ•จ์ˆ˜ ์ ์šฉ 2. **๋ณ€ํ™˜ ํ•จ์ˆ˜ ์ง€์›**: + - `sum`: ํ•ฉ๊ณ„ - `average`: ํ‰๊ท  - `count`: ๊ฐœ์ˆ˜ @@ -177,15 +190,18 @@ - `concat`, `join`: ๋ฌธ์ž์—ด ๊ฒฐํ•ฉ 3. **filterDataByCondition(data, condition)** + - ์กฐ๊ฑด ์—ฐ์‚ฐ์ž: equals, notEquals, contains, greaterThan, lessThan, in, notIn 4. **validateMappingResult(data, rules)** + - ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ 5. **previewMapping(sampleData, rules)** - ๋งคํ•‘ ๊ฒฐ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ **ํŠน์ง•**: + - โœ… ์ค‘์ฒฉ ๊ฐ์ฒด ์ง€์› (`user.address.city`) - โœ… ํƒ€์ž… ์•ˆ์ „์„ฑ - โœ… ์—๋Ÿฌ ์ฒ˜๋ฆฌ @@ -195,6 +211,7 @@ **ํŒŒ์ผ**: `frontend/lib/utils/logger.ts` **๊ธฐ๋Šฅ**: + - debug, info, warn, error ๋ ˆ๋ฒจ - ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ debug ์ถœ๋ ฅ - ํƒ€์ž„์Šคํƒฌํ”„ ํฌํ•จ @@ -208,6 +225,7 @@ **ํŒŒ์ผ**: `frontend/components/screen-embedding/ScreenSplitPanel.tsx` **์ฃผ์š” ๊ธฐ๋Šฅ**: + - โœ… ์ขŒ์šฐ ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ - โœ… ๋ฆฌ์‚ฌ์ด์ € (๋“œ๋ž˜๊ทธ๋กœ ๋น„์œจ ์กฐ์ •) - โœ… ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ฒ„ํŠผ @@ -218,6 +236,7 @@ - โœ… ์ „๋‹ฌ ํ›„ ์„ ํƒ ์ดˆ๊ธฐํ™” (์˜ต์…˜) **UI ๊ตฌ์กฐ**: + ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ [์ขŒ์ธก ํŒจ๋„ 50%] โ”‚ [๋ฒ„ํŠผ] โ”‚ [์šฐ์ธก ํŒจ๋„ 50%] โ”‚ @@ -230,6 +249,7 @@ ``` **์ด๋ฒคํŠธ ํ๋ฆ„**: + 1. ์ขŒ์ธก์—์„œ ํ–‰ ์„ ํƒ โ†’ ์„ ํƒ ์นด์šดํŠธ ์—…๋ฐ์ดํŠธ 2. ์ „๋‹ฌ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ๊ฒ€์ฆ 3. ์šฐ์ธก ํ™”๋ฉด์˜ ์ปดํฌ๋„ŒํŠธ๋“ค์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ @@ -281,7 +301,7 @@ ERP-node/ const inboundConfig: ScreenSplitPanel = { screenId: 100, leftEmbedding: { - childScreenId: 10, // ๋ฐœ์ฃผ ๋ชฉ๋ก ์กฐํšŒ + childScreenId: 10, // ๋ฐœ์ฃผ ๋ชฉ๋ก ์กฐํšŒ position: "left", mode: "select", config: { @@ -290,7 +310,7 @@ const inboundConfig: ScreenSplitPanel = { }, }, rightEmbedding: { - childScreenId: 20, // ์ž…๊ณ  ๋“ฑ๋ก ํผ + childScreenId: 20, // ์ž…๊ณ  ๋“ฑ๋ก ํผ position: "right", mode: "form", config: { @@ -352,7 +372,7 @@ const inboundConfig: ScreenSplitPanel = { onDataTransferred={(data) => { console.log("์ „๋‹ฌ๋œ ๋ฐ์ดํ„ฐ:", data); }} -/> +/>; ``` --- @@ -395,6 +415,7 @@ const inboundConfig: ScreenSplitPanel = { ### Phase 5: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ (์˜ˆ์ •) 1. **DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„** + - TableComponent - InputComponent - SelectComponent @@ -402,6 +423,7 @@ const inboundConfig: ScreenSplitPanel = { - ๊ธฐํƒ€ ์ปดํฌ๋„ŒํŠธ๋“ค 2. **์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™”** + - ์šฐ์ธก โ†’ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋ฐ˜์˜ - ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ @@ -412,6 +434,7 @@ const inboundConfig: ScreenSplitPanel = { ### Phase 6: ์„ค์ • UI (์˜ˆ์ •) 1. **์‹œ๊ฐ์  ๋งคํ•‘ ์„ค์ • UI** + - ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ํ•„๋“œ ๋งคํ•‘ - ๋ณ€ํ™˜ ํ•จ์ˆ˜ ์„ ํƒ - ์กฐ๊ฑด ์„ค์ • @@ -463,7 +486,7 @@ import { getScreenSplitPanel } from "@/lib/api/screenEmbedding"; const { data: config } = await getScreenSplitPanel(screenId); // ๋ Œ๋”๋ง - +; ``` --- @@ -471,6 +494,7 @@ const { data: config } = await getScreenSplitPanel(screenId); ## โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### ๊ตฌํ˜„ ์™„๋ฃŒ + - [x] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ (3๊ฐœ ํ…Œ์ด๋ธ”) - [x] TypeScript ํƒ€์ž… ์ •์˜ - [x] ๋ฐฑ์—”๋“œ API (15๊ฐœ ์—”๋“œํฌ์ธํŠธ) @@ -481,6 +505,7 @@ const { data: config } = await getScreenSplitPanel(screenId); - [x] ๋กœ๊ฑฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ### ๋‹ค์Œ ๋‹จ๊ณ„ + - [ ] DataReceivable ๊ตฌํ˜„ (๊ฐ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋ณ„) - [ ] ์„ค์ • UI (๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๋งคํ•‘) - [ ] ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ @@ -500,4 +525,3 @@ const { data: config } = await getScreenSplitPanel(screenId); - โœ… ๋งคํ•‘ ์—”์ง„ ์™„์„ฑ ์ด์ œ ์ž…๊ณ  ๋“ฑ๋ก๊ณผ ๊ฐ™์€ ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„๋Š” ๊ฐ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋ณ„ DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„๊ณผ ์„ค์ • UI ๊ฐœ๋ฐœ์ž…๋‹ˆ๋‹ค. - diff --git a/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_์ถฉ๋Œ_๋ถ„์„_๋ณด๊ณ ์„œ.md b/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_์ถฉ๋Œ_๋ถ„์„_๋ณด๊ณ ์„œ.md index 00e16b8e..6cebf31e 100644 --- a/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_์ถฉ๋Œ_๋ถ„์„_๋ณด๊ณ ์„œ.md +++ b/ํ™”๋ฉด_์ž„๋ฒ ๋”ฉ_์‹œ์Šคํ…œ_์ถฉ๋Œ_๋ถ„์„_๋ณด๊ณ ์„œ.md @@ -11,6 +11,7 @@ ### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ #### ์ƒˆ๋กœ์šด ํ…Œ์ด๋ธ” (๋…๋ฆฝ์ ) + ```sql - screen_embedding (์‹ ๊ทœ) - screen_data_transfer (์‹ ๊ทœ) @@ -18,11 +19,13 @@ ``` **์ถฉ๋Œ ์—†๋Š” ์ด์œ **: + - โœ… ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ํ…Œ์ด๋ธ”๋ช… - โœ… ๊ธฐ์กด ํ…Œ์ด๋ธ”๊ณผ ์ด๋ฆ„ ์ค‘๋ณต ์—†์Œ - โœ… ์™ธ๋ž˜ํ‚ค๋Š” ๊ธฐ์กด `screen_definitions`๋งŒ ์ฐธ์กฐ (์ฝ๊ธฐ ์ „์šฉ) #### ๊ธฐ์กด ํ…Œ์ด๋ธ” (์˜ํ–ฅ ์—†์Œ) + ```sql - screen_definitions (๋ณ€๊ฒฝ ์—†์Œ) - screen_layouts (๋ณ€๊ฒฝ ์—†์Œ) @@ -32,6 +35,7 @@ ``` **ํ™•์ธ ์‚ฌํ•ญ**: + - โœ… ๊ธฐ์กด ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ - โœ… ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถˆํ•„์š” - โœ… ๊ธฐ์กด ์ฟผ๋ฆฌ ์˜ํ–ฅ ์—†์Œ @@ -41,6 +45,7 @@ ### 2. API ์—”๋“œํฌ์ธํŠธ #### ์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ (๋…๋ฆฝ์ ) + ``` POST /api/screen-embedding GET /api/screen-embedding @@ -59,11 +64,13 @@ DELETE /api/screen-split-panel/:id ``` **์ถฉ๋Œ ์—†๋Š” ์ด์œ **: + - โœ… ๊ธฐ์กด `/api/screen-management/*` ์™€ ๋‹ค๋ฅธ ๊ฒฝ๋กœ - โœ… ์ƒˆ๋กœ์šด ๋ผ์šฐํŠธ ์ถ”๊ฐ€๋งŒ (๊ธฐ์กด ๋ผ์šฐํŠธ ์ˆ˜์ • ์—†์Œ) - โœ… ๋…๋ฆฝ์ ์ธ ์ปจํŠธ๋กค๋Ÿฌ ํŒŒ์ผ #### ๊ธฐ์กด ์—”๋“œํฌ์ธํŠธ (์˜ํ–ฅ ์—†์Œ) + ``` /api/screen-management/* (๋ณ€๊ฒฝ ์—†์Œ) /api/screen/* (๋ณ€๊ฒฝ ์—†์Œ) @@ -75,16 +82,19 @@ DELETE /api/screen-split-panel/:id ### 3. TypeScript ํƒ€์ž… #### ์ƒˆ๋กœ์šด ํƒ€์ž… ํŒŒ์ผ (๋…๋ฆฝ์ ) + ```typescript -frontend/types/screen-embedding.ts (์‹ ๊ทœ) +frontend / types / screen - embedding.ts(์‹ ๊ทœ); ``` **์ถฉ๋Œ ์—†๋Š” ์ด์œ **: + - โœ… ๊ธฐ์กด `screen.ts`, `screen-management.ts` ์™€ ๋ณ„๋„ ํŒŒ์ผ - โœ… ํƒ€์ž…๋ช… ์ค‘๋ณต ์—†์Œ - โœ… ๋…๋ฆฝ์ ์ธ ๋„ค์ž„์ŠคํŽ˜์ด์Šค #### ๊ธฐ์กด ํƒ€์ž… (์˜ํ–ฅ ์—†์Œ) + ```typescript frontend/types/screen.ts (๋ณ€๊ฒฝ ์—†์Œ) frontend/types/screen-management.ts (๋ณ€๊ฒฝ ์—†์Œ) @@ -96,6 +106,7 @@ backend-node/src/types/screen.ts (๋ณ€๊ฒฝ ์—†์Œ) ### 4. ํ”„๋ก ํŠธ์—”๋“œ ์ปดํฌ๋„ŒํŠธ #### ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ (๋…๋ฆฝ์ ) + ``` frontend/components/screen-embedding/ โ”œโ”€โ”€ EmbeddedScreen.tsx (์‹ ๊ทœ) @@ -104,11 +115,13 @@ frontend/components/screen-embedding/ ``` **์ถฉ๋Œ ์—†๋Š” ์ด์œ **: + - โœ… ๋ณ„๋„ ๋””๋ ‰ํ† ๋ฆฌ (`screen-embedding/`) - โœ… ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ • ์—†์Œ - โœ… ๋…๋ฆฝ์ ์œผ๋กœ import ๊ฐ€๋Šฅ #### ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ (์˜ํ–ฅ ์—†์Œ) + ``` frontend/components/screen/ (๋ณ€๊ฒฝ ์—†์Œ) frontend/app/(main)/screens/[screenId]/page.tsx (๋ณ€๊ฒฝ ์—†์Œ) @@ -121,17 +134,20 @@ frontend/app/(main)/screens/[screenId]/page.tsx (๋ณ€๊ฒฝ ์—†์Œ) ### 1. screen_definitions ํ…Œ์ด๋ธ” ์ฐธ์กฐ **ํ˜„์žฌ ๊ตฌ์กฐ**: + ```sql -- ์ƒˆ ํ…Œ์ด๋ธ”๋“ค์ด screen_definitions๋ฅผ ์ฐธ์กฐ -CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id) +CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id) REFERENCES screen_definitions(screen_id) ON DELETE CASCADE ``` **์ž ์žฌ์  ๋ฌธ์ œ**: + - โš ๏ธ ๊ธฐ์กด ํ™”๋ฉด ์‚ญ์ œ ์‹œ ์ž„๋ฒ ๋”ฉ ์„ค์ •๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋จ (CASCADE) - โš ๏ธ ํ™”๋ฉด ID ๋ณ€๊ฒฝ ์‹œ ์ž„๋ฒ ๋”ฉ ์„ค์ •์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Œ **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + ```sql -- ์ด๋ฏธ ๊ตฌํ˜„๋จ: ON DELETE CASCADE -- ํ™”๋ฉด ์‚ญ์ œ ์‹œ ์ž๋™์œผ๋กœ ๊ด€๋ จ ์ž„๋ฒ ๋”ฉ๋„ ์‚ญ์ œ @@ -139,6 +155,7 @@ CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id) ``` **๊ถŒ์žฅ ์‚ฌํ•ญ**: + - โœ… ํ™”๋ฉด ์‚ญ์ œ ์ „ ์ž„๋ฒ ๋”ฉ ์‚ฌ์šฉ ์—ฌ๋ถ€ ํ™•์ธ UI ์ถ”๊ฐ€ (Phase 6) - โœ… ์‚ญ์ œ ์‹œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ @@ -147,21 +164,23 @@ CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id) ### 2. ํ™”๋ฉด ๋ Œ๋”๋ง ๋กœ์ง **ํ˜„์žฌ ํ™”๋ฉด ๋ Œ๋”๋ง**: + ```typescript // frontend/app/(main)/screens/[screenId]/page.tsx function ScreenViewPage() { // ๊ธฐ์กด: ๋‹จ์ผ ํ™”๋ฉด ๋ Œ๋”๋ง const screenId = parseInt(params.screenId as string); - + // ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ const layout = await screenApi.getScreenLayout(screenId); - + // ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง - + ; } ``` **์ƒˆ๋กœ์šด ๋ Œ๋”๋ง (๋ถ„ํ•  ํŒจ๋„)**: + ```typescript // ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด์ธ ๊ฒฝ์šฐ if (isSplitPanelScreen) { @@ -174,10 +193,12 @@ return ; ``` **์ž ์žฌ์  ๋ฌธ์ œ**: + - โš ๏ธ ํ™”๋ฉด ํƒ€์ž… ๊ตฌ๋ถ„ ๋กœ์ง ํ•„์š” - โš ๏ธ ๊ธฐ์กด ํ™”๋ฉด ๋ Œ๋”๋ง ๋กœ์ง ์ˆ˜์ • ํ•„์š” **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + ```typescript // 1. screen_definitions์— screen_type ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (์„ ํƒ์‚ฌํ•ญ) ALTER TABLE screen_definitions ADD COLUMN screen_type VARCHAR(20) DEFAULT 'normal'; @@ -191,40 +212,45 @@ if (splitPanelConfig.success && splitPanelConfig.data) { ``` **๊ถŒ์žฅ ๊ตฌํ˜„**: + ```typescript // frontend/app/(main)/screens/[screenId]/page.tsx ์ˆ˜์ • useEffect(() => { const loadScreen = async () => { // 1. ๋ถ„ํ•  ํŒจ๋„ ํ™•์ธ const splitPanelResult = await getScreenSplitPanel(screenId); - + if (splitPanelResult.success && splitPanelResult.data) { // ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด - setScreenType('split_panel'); + setScreenType("split_panel"); setSplitPanelConfig(splitPanelResult.data); return; } - + // 2. ์ผ๋ฐ˜ ํ™”๋ฉด const screenResult = await screenApi.getScreen(screenId); const layoutResult = await screenApi.getScreenLayout(screenId); - - setScreenType('normal'); + + setScreenType("normal"); setScreen(screenResult.data); setLayout(layoutResult.data); }; - + loadScreen(); }, [screenId]); // ๋ Œ๋”๋ง -{screenType === 'split_panel' && splitPanelConfig && ( - -)} +{ + screenType === "split_panel" && splitPanelConfig && ( + + ); +} -{screenType === 'normal' && layout && ( - -)} +{ + screenType === "normal" && layout && ( + + ); +} ``` --- @@ -232,6 +258,7 @@ useEffect(() => { ### 3. ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก ์‹œ์Šคํ…œ **ํ˜„์žฌ ์‹œ์Šคํ…œ**: + ```typescript // frontend/lib/registry/components.ts const componentRegistry = new Map(); @@ -242,6 +269,7 @@ export function registerComponent(id: string, component: any) { ``` **์ƒˆ๋กœ์šด ์š”๊ตฌ์‚ฌํ•ญ**: + ```typescript // DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ ํ•„์š” interface DataReceivable { @@ -254,29 +282,31 @@ interface DataReceivable { ``` **์ž ์žฌ์  ๋ฌธ์ œ**: + - โš ๏ธ ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋“ค์ด DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๋ฏธ๊ตฌํ˜„ - โš ๏ธ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ๊ธฐ๋Šฅ ์—†์Œ **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + ```typescript // Phase 5์—์„œ ๊ตฌํ˜„ ์˜ˆ์ • // ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ž˜ํ•‘ํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด ์‚ฌ์šฉ class TableComponentAdapter implements DataReceivable { constructor(private tableComponent: any) {} - + async receiveData(data: any[], mode: DataReceiveMode) { - if (mode === 'append') { + if (mode === "append") { this.tableComponent.addRows(data); - } else if (mode === 'replace') { + } else if (mode === "replace") { this.tableComponent.setRows(data); } } - + getData() { return this.tableComponent.getRows(); } - + clearData() { this.tableComponent.clearRows(); } @@ -284,6 +314,7 @@ class TableComponentAdapter implements DataReceivable { ``` **๊ถŒ์žฅ ์‚ฌํ•ญ**: + - โœ… ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ • ์—†์ด ์–ด๋Œ‘ํ„ฐ๋กœ ๋ž˜ํ•‘ - โœ… ์ ์ง„์ ์œผ๋กœ DataReceivable ๊ตฌํ˜„ - โœ… ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ @@ -297,38 +328,41 @@ class TableComponentAdapter implements DataReceivable { **ํŒŒ์ผ**: `frontend/app/(main)/screens/[screenId]/page.tsx` **์ˆ˜์ • ๋‚ด์šฉ**: + ```typescript import { getScreenSplitPanel } from "@/lib/api/screenEmbedding"; import { ScreenSplitPanel } from "@/components/screen-embedding"; function ScreenViewPage() { - const [screenType, setScreenType] = useState<'normal' | 'split_panel'>('normal'); + const [screenType, setScreenType] = useState<"normal" | "split_panel">( + "normal" + ); const [splitPanelConfig, setSplitPanelConfig] = useState(null); - + useEffect(() => { const loadScreen = async () => { // ๋ถ„ํ•  ํŒจ๋„ ํ™•์ธ const splitResult = await getScreenSplitPanel(screenId); - + if (splitResult.success && splitResult.data) { - setScreenType('split_panel'); + setScreenType("split_panel"); setSplitPanelConfig(splitResult.data); setLoading(false); return; } - + // ์ผ๋ฐ˜ ํ™”๋ฉด ๋กœ๋“œ (๊ธฐ์กด ๋กœ์ง) // ... }; - + loadScreen(); }, [screenId]); - + // ๋ Œ๋”๋ง - if (screenType === 'split_panel' && splitPanelConfig) { + if (screenType === "split_panel" && splitPanelConfig) { return ; } - + // ๊ธฐ์กด ๋ Œ๋”๋ง ๋กœ์ง // ... } @@ -343,6 +377,7 @@ function ScreenViewPage() { **ํŒŒ์ผ**: ํ™”๋ฉด ๊ด€๋ฆฌ ํŽ˜์ด์ง€ **์ถ”๊ฐ€ ๊ธฐ๋Šฅ**: + - ํ™”๋ฉด ์ƒ์„ฑ ์‹œ "๋ถ„ํ•  ํŒจ๋„" ํƒ€์ž… ์„ ํƒ - ๋ถ„ํ•  ํŒจ๋„ ์„ค์ • UI - ์ž„๋ฒ ๋”ฉ ์„ค์ • UI @@ -354,15 +389,15 @@ function ScreenViewPage() { ## ๐Ÿ“Š ์ถฉ๋Œ ์œ„ํ—˜๋„ ํ‰๊ฐ€ -| ํ•ญ๋ชฉ | ์œ„ํ—˜๋„ | ์„ค๋ช… | ์กฐ์น˜ ํ•„์š” | -|------|--------|------|-----------| -| ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ | ๐ŸŸข ๋‚ฎ์Œ | ๋…๋ฆฝ์ ์ธ ์ƒˆ ํ…Œ์ด๋ธ” | โŒ ๋ถˆํ•„์š” | -| API ์—”๋“œํฌ์ธํŠธ | ๐ŸŸข ๋‚ฎ์Œ | ์ƒˆ๋กœ์šด ๊ฒฝ๋กœ ์ถ”๊ฐ€ | โŒ ๋ถˆํ•„์š” | -| TypeScript ํƒ€์ž… | ๐ŸŸข ๋‚ฎ์Œ | ๋ณ„๋„ ํŒŒ์ผ | โŒ ๋ถˆํ•„์š” | -| ํ”„๋ก ํŠธ์—”๋“œ ์ปดํฌ๋„ŒํŠธ | ๐ŸŸข ๋‚ฎ์Œ | ๋ณ„๋„ ๋””๋ ‰ํ† ๋ฆฌ | โŒ ๋ถˆํ•„์š” | -| ํ™”๋ฉด ๋ Œ๋”๋ง ๋กœ์ง | ๐ŸŸก ์ค‘๊ฐ„ | ์กฐ๊ฑด ๋ถ„๊ธฐ ์ถ”๊ฐ€ ํ•„์š” | โœ… ํ•„์š” | -| ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก ์‹œ์Šคํ…œ | ๐ŸŸก ์ค‘๊ฐ„ | ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด ํ•„์š” | โœ… ํ•„์š” (Phase 5) | -| ์™ธ๋ž˜ํ‚ค CASCADE | ๐ŸŸก ์ค‘๊ฐ„ | ํ™”๋ฉด ์‚ญ์ œ ์‹œ ์ฃผ์˜ | โš ๏ธ ์ฃผ์˜ | +| ํ•ญ๋ชฉ | ์œ„ํ—˜๋„ | ์„ค๋ช… | ์กฐ์น˜ ํ•„์š” | +| -------------------- | ------- | ------------------- | ----------------- | +| ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ | ๐ŸŸข ๋‚ฎ์Œ | ๋…๋ฆฝ์ ์ธ ์ƒˆ ํ…Œ์ด๋ธ” | โŒ ๋ถˆํ•„์š” | +| API ์—”๋“œํฌ์ธํŠธ | ๐ŸŸข ๋‚ฎ์Œ | ์ƒˆ๋กœ์šด ๊ฒฝ๋กœ ์ถ”๊ฐ€ | โŒ ๋ถˆํ•„์š” | +| TypeScript ํƒ€์ž… | ๐ŸŸข ๋‚ฎ์Œ | ๋ณ„๋„ ํŒŒ์ผ | โŒ ๋ถˆํ•„์š” | +| ํ”„๋ก ํŠธ์—”๋“œ ์ปดํฌ๋„ŒํŠธ | ๐ŸŸข ๋‚ฎ์Œ | ๋ณ„๋„ ๋””๋ ‰ํ† ๋ฆฌ | โŒ ๋ถˆํ•„์š” | +| ํ™”๋ฉด ๋ Œ๋”๋ง ๋กœ์ง | ๐ŸŸก ์ค‘๊ฐ„ | ์กฐ๊ฑด ๋ถ„๊ธฐ ์ถ”๊ฐ€ ํ•„์š” | โœ… ํ•„์š” | +| ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก ์‹œ์Šคํ…œ | ๐ŸŸก ์ค‘๊ฐ„ | ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด ํ•„์š” | โœ… ํ•„์š” (Phase 5) | +| ์™ธ๋ž˜ํ‚ค CASCADE | ๐ŸŸก ์ค‘๊ฐ„ | ํ™”๋ฉด ์‚ญ์ œ ์‹œ ์ฃผ์˜ | โš ๏ธ ์ฃผ์˜ | **์ „์ฒด ์œ„ํ—˜๋„**: ๐ŸŸข **๋‚ฎ์Œ** (๋Œ€๋ถ€๋ถ„ ๋…๋ฆฝ์ ) @@ -371,24 +406,28 @@ function ScreenViewPage() { ## โœ… ์•ˆ์ „์„ฑ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค + - [x] ์ƒˆ ํ…Œ์ด๋ธ”๋ช…์ด ๊ธฐ์กด๊ณผ ์ค‘๋ณต๋˜์ง€ ์•Š์Œ - [x] ๊ธฐ์กด ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ - [x] ์™ธ๋ž˜ํ‚ค CASCADE ์„ค์ • ์™„๋ฃŒ - [x] ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ (company_code) ์ง€์› ### ๋ฐฑ์—”๋“œ + - [x] ์ƒˆ ๋ผ์šฐํŠธ๊ฐ€ ๊ธฐ์กด๊ณผ ์ถฉ๋Œํ•˜์ง€ ์•Š์Œ - [x] ๋…๋ฆฝ์ ์ธ ์ปจํŠธ๋กค๋Ÿฌ ํŒŒ์ผ - [x] ๊ธฐ์กด API ์ˆ˜์ • ์—†์Œ - [x] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์™„๋ฃŒ ### ํ”„๋ก ํŠธ์—”๋“œ + - [x] ์ƒˆ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณ„๋„ ๋””๋ ‰ํ† ๋ฆฌ - [x] ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ • ์—†์Œ - [x] ๋…๋ฆฝ์ ์ธ ํƒ€์ž… ์ •์˜ - [ ] ํ™”๋ฉด ํŽ˜์ด์ง€ ์ˆ˜์ • ํ•„์š” (์กฐ๊ฑด ๋ถ„๊ธฐ) ### ํ˜ธํ™˜์„ฑ + - [x] ๊ธฐ์กด ํ™”๋ฉด ๋™์ž‘ ์˜ํ–ฅ ์—†์Œ - [x] ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ - [ ] ์ปดํฌ๋„ŒํŠธ ์–ด๋Œ‘ํ„ฐ ๊ตฌํ˜„ (Phase 5) @@ -400,6 +439,7 @@ function ScreenViewPage() { ### ์ฆ‰์‹œ ์กฐ์น˜ (ํ•„์ˆ˜) 1. **ํ™”๋ฉด ํŽ˜์ด์ง€ ์ˆ˜์ •** + ```typescript // frontend/app/(main)/screens/[screenId]/page.tsx // ๋ถ„ํ•  ํŒจ๋„ ํ™•์ธ ๋กœ์ง ์ถ”๊ฐ€ @@ -421,11 +461,13 @@ function ScreenViewPage() { ### ๋‹จ๊ณ„์  ์กฐ์น˜ (Phase 5-6) 1. **์ปดํฌ๋„ŒํŠธ ์–ด๋Œ‘ํ„ฐ ๊ตฌํ˜„** + - TableComponent โ†’ DataReceivable - InputComponent โ†’ DataReceivable - ๊ธฐํƒ€ ์ปดํฌ๋„ŒํŠธ๋“ค 2. **์„ค์ • UI ๊ฐœ๋ฐœ** + - ๋ถ„ํ•  ํŒจ๋„ ์ƒ์„ฑ UI - ๋งคํ•‘ ๊ทœ์น™ ์„ค์ • UI - ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ @@ -442,6 +484,7 @@ function ScreenViewPage() { ### โœ… ์•ˆ์ „์„ฑ ํ‰๊ฐ€: ๋†’์Œ **์ด์œ **: + 1. โœ… ๋Œ€๋ถ€๋ถ„์˜ ์ฝ”๋“œ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์ถ”๊ฐ€๋จ 2. โœ… ๊ธฐ์กด ์‹œ์Šคํ…œ ์ˆ˜์ • ์ตœ์†Œํ™” 3. โœ… ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ @@ -450,10 +493,12 @@ function ScreenViewPage() { ### โš ๏ธ ์ฃผ์˜ ์‚ฌํ•ญ 1. **ํ™”๋ฉด ํŽ˜์ด์ง€ ์ˆ˜์ • ํ•„์š”** + - ๋ถ„ํ•  ํŒจ๋„ ํ™•์ธ ๋กœ์ง ์ถ”๊ฐ€ - ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ๊ตฌํ˜„ 2. **์ ์ง„์  ๊ตฌํ˜„ ๊ถŒ์žฅ** + - Phase 5: ์ปดํฌ๋„ŒํŠธ ์–ด๋Œ‘ํ„ฐ - Phase 6: ์„ค์ • UI - ๋‹จ๊ณ„๋ณ„ ํ…Œ์ŠคํŠธ @@ -467,4 +512,3 @@ function ScreenViewPage() { **์ถฉ๋Œ ์œ„ํ—˜๋„: ๋‚ฎ์Œ (๐ŸŸข)** ์ƒˆ๋กœ์šด ์‹œ์Šคํ…œ์€ ๊ธฐ์กด ์‹œ์Šคํ…œ๊ณผ **๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘**ํ•˜๋ฉฐ, ์ตœ์†Œํ•œ์˜ ์ˆ˜์ •๋งŒ์œผ๋กœ ํ†ตํ•ฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด ํŽ˜์ด์ง€์— ์กฐ๊ฑด ๋ถ„๊ธฐ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - -- 2.43.0