diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 2d3fb513..76363e4f 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -102,13 +102,6 @@ export const EditModal: React.FC = ({ className }) => { useEffect(() => { const handleOpenEditModal = (event: CustomEvent) => { const { screenId, title, description, modalSize, editData, onSave } = event.detail; - console.log("๐Ÿš€ EditModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ์ˆ˜์‹ :", { - screenId, - title, - description, - modalSize, - editData, - }); setModalState({ isOpen: true, @@ -126,7 +119,16 @@ export const EditModal: React.FC = ({ className }) => { }; const handleCloseEditModal = () => { - console.log("๐Ÿšช EditModal ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ์ˆ˜์‹ "); + // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ onSave ์ฝœ๋ฐฑ ์‹คํ–‰ (ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ) + if (modalState.onSave) { + try { + modalState.onSave(); + } catch (callbackError) { + console.error("โš ๏ธ onSave ์ฝœ๋ฐฑ ์—๋Ÿฌ:", callbackError); + } + } + + // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ handleClose(); }; @@ -137,7 +139,7 @@ export const EditModal: React.FC = ({ className }) => { window.removeEventListener("openEditModal", handleOpenEditModal as EventListener); window.removeEventListener("closeEditModal", handleCloseEditModal); }; - }, []); + }, [modalState.onSave]); // modalState.onSave๋ฅผ ์˜์กด์„ฑ์— ์ถ”๊ฐ€ํ•˜์—ฌ ์ตœ์‹  ์ฝœ๋ฐฑ ์ฐธ์กฐ // ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ useEffect(() => { @@ -211,12 +213,6 @@ export const EditModal: React.FC = ({ className }) => { } try { - console.log("๐Ÿ’พ ์ˆ˜์ • ์ €์žฅ ์‹œ์ž‘:", { - tableName: screenData.screenInfo.tableName, - formData, - originalData, - }); - // ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์ถ”์ถœ const changedData: Record = {}; Object.keys(formData).forEach((key) => { @@ -225,26 +221,33 @@ export const EditModal: React.FC = ({ className }) => { } }); - console.log("๐Ÿ“ ๋ณ€๊ฒฝ๋œ ํ•„๋“œ:", changedData); - if (Object.keys(changedData).length === 0) { toast.info("๋ณ€๊ฒฝ๋œ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."); handleClose(); return; } + // ๊ธฐ๋ณธํ‚ค ํ™•์ธ (id ๋˜๋Š” ์ฒซ ๋ฒˆ์งธ ํ‚ค) + const recordId = originalData.id || Object.values(originalData)[0]; + // UPDATE ์•ก์…˜ ์‹คํ–‰ - const response = await dynamicFormApi.updateData(screenData.screenInfo.tableName, { - ...originalData, // ์›๋ณธ ๋ฐ์ดํ„ฐ (WHERE ์กฐ๊ฑด์šฉ) - ...changedData, // ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ๋งŒ - }); + const response = await dynamicFormApi.updateFormDataPartial( + recordId, + originalData, + changedData, + screenData.screenInfo.tableName, + ); if (response.success) { toast.success("๋ฐ์ดํ„ฐ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); - // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ onSave ์ฝœ๋ฐฑ ์‹คํ–‰ + // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ onSave ์ฝœ๋ฐฑ ์‹คํ–‰ (ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ) if (modalState.onSave) { - modalState.onSave(); + try { + modalState.onSave(); + } catch (callbackError) { + console.error("โš ๏ธ onSave ์ฝœ๋ฐฑ ์—๋Ÿฌ:", callbackError); + } } handleClose(); @@ -335,16 +338,10 @@ export const EditModal: React.FC = ({ className }) => { allComponents={screenData.components} formData={formData} onFormDataChange={(fieldName, value) => { - console.log(`๐ŸŽฏ EditModal onFormDataChange ํ˜ธ์ถœ: ${fieldName} = "${value}"`); - console.log("๐Ÿ“‹ ํ˜„์žฌ formData:", formData); - setFormData((prev) => { - const newFormData = { - ...prev, - [fieldName]: value, - }; - console.log("๐Ÿ“ EditModal ์—…๋ฐ์ดํŠธ๋œ formData:", newFormData); - return newFormData; - }); + setFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); }} screenInfo={{ id: modalState.screenId!, diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index b54df6ad..17662cac 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -769,7 +769,7 @@ export const InteractiveDataTable: React.FC = ({ setShowSaveModal(true); }, [getDisplayColumns, generateAutoValue, component.addModalConfig]); - // ๋ฐ์ดํ„ฐ ์ˆ˜์ • ํ•ธ๋“ค๋Ÿฌ (SaveModal ์‚ฌ์šฉ) + // ๋ฐ์ดํ„ฐ ์ˆ˜์ • ํ•ธ๋“ค๋Ÿฌ (EditModal ์‚ฌ์šฉ) const handleEditData = useCallback(() => { if (selectedRows.size !== 1) return; @@ -793,17 +793,25 @@ export const InteractiveDataTable: React.FC = ({ initialData[col.columnName] = selectedRowData[col.columnName] || ""; }); - setEditFormData(initialData); - setEditingRowData(selectedRowData); - // ์ˆ˜์ • ๋ชจ๋‹ฌ ์„ค์ •์—์„œ ์ œ๋ชฉ๊ณผ ์„ค๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ - const editModalTitle = component.editModalConfig?.title || ""; + const editModalTitle = component.editModalConfig?.title || "๋ฐ์ดํ„ฐ ์ˆ˜์ •"; const editModalDescription = component.editModalConfig?.description || ""; - console.log("๐Ÿ“ ์ˆ˜์ • ๋ชจ๋‹ฌ ์„ค์ •:", { editModalTitle, editModalDescription }); - - setShowEditModal(true); - }, [selectedRows, data, getDisplayColumns, component.editModalConfig]); + // ์ „์—ญ EditModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + const event = new CustomEvent("openEditModal", { + detail: { + screenId, + title: editModalTitle, + description: editModalDescription, + modalSize: "lg", + editData: initialData, + onSave: () => { + loadData(); // ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ + }, + }, + }); + window.dispatchEvent(event); + }, [selectedRows, data, getDisplayColumns, component.addModalConfig, component.editModalConfig, loadData]); // ์ˆ˜์ • ํผ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleEditFormChange = useCallback((columnName: string, value: any) => { diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 7ed39353..e85aab58 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -38,6 +38,7 @@ interface InteractiveScreenViewerProps { id: number; tableName?: string; }; + onSave?: () => Promise; } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -47,6 +48,7 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ํ™•์ธ const { userName, user } = useAuth(); @@ -204,8 +206,7 @@ export const InteractiveScreenViewerDynamic: React.FC { - // ํ™”๋ฉด ๋‹ซ๊ธฐ ๋กœ์ง (ํ•„์š”์‹œ ๊ตฌํ˜„) - console.log("๐Ÿšช ํ™”๋ฉด ๋‹ซ๊ธฐ ์š”์ฒญ"); + // buttonActions.ts๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌํ•จ }} /> ); @@ -299,6 +300,18 @@ export const InteractiveScreenViewerDynamic: React.FC { + // EditModal์—์„œ ์ „๋‹ฌ๋œ onSave๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (์ˆ˜์ • ๋ชจ๋‹ฌ) + if (onSave) { + try { + await onSave(); + } catch (error) { + console.error("์ €์žฅ ์˜ค๋ฅ˜:", error); + toast.error("์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + return; + } + + // ์ผ๋ฐ˜ ์ €์žฅ ์•ก์…˜ (์‹ ๊ทœ ์ƒ์„ฑ) if (!screenInfo?.tableName) { toast.error("ํ…Œ์ด๋ธ”๋ช…์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 5a11f325..51364429 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -381,19 +381,37 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // ์‹คํ–‰์ทจ์†Œ const undo = useCallback(() => { - if (historyIndex > 0) { - setHistoryIndex((prev) => prev - 1); - setLayout(history[historyIndex - 1]); - } - }, [history, historyIndex]); + setHistoryIndex((prevIndex) => { + if (prevIndex > 0) { + const newIndex = prevIndex - 1; + setHistory((prevHistory) => { + if (prevHistory[newIndex]) { + setLayout(prevHistory[newIndex]); + } + return prevHistory; + }); + return newIndex; + } + return prevIndex; + }); + }, []); // ๋‹ค์‹œ์‹คํ–‰ const redo = useCallback(() => { - if (historyIndex < history.length - 1) { - setHistoryIndex((prev) => prev + 1); - setLayout(history[historyIndex + 1]); - } - }, [history, historyIndex]); + setHistoryIndex((prevIndex) => { + let newIndex = prevIndex; + setHistory((prevHistory) => { + if (prevIndex < prevHistory.length - 1) { + newIndex = prevIndex + 1; + if (prevHistory[newIndex]) { + setLayout(prevHistory[newIndex]); + } + } + return prevHistory; + }); + return newIndex; + }); + }, []); // ์ปดํฌ๋„ŒํŠธ ์†์„ฑ ์—…๋ฐ์ดํŠธ const updateComponentProperty = useCallback( diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 4c376b09..8b805d93 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -234,9 +234,13 @@ export class ButtonActionExecutor { throw new Error("์ €์žฅ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ํ™”๋ฉดID ๋ˆ„๋ฝ)"); } - // ํ…Œ์ด๋ธ”๊ณผ ํ”Œ๋กœ์šฐ ๋ชจ๋‘ ์ƒˆ๋กœ๊ณ ์นจ + // ํ…Œ์ด๋ธ”๊ณผ ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ (๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์ „์— ์‹คํ–‰) context.onRefresh?.(); context.onFlowRefresh?.(); + + // ์ €์žฅ ์„ฑ๊ณต ํ›„ EditModal ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new CustomEvent("closeEditModal")); + return true; } catch (error) { console.error("์ €์žฅ ์˜ค๋ฅ˜:", error); diff --git a/ํ™”๋ฉด๊ด€๋ฆฌ_๋ฐ_ํ…Œ์ด๋ธ”๊ด€๋ฆฌ_๊ฐœ์„ ์‚ฌํ•ญ_๋ชฉ๋ก.md b/ํ™”๋ฉด๊ด€๋ฆฌ_๋ฐ_ํ…Œ์ด๋ธ”๊ด€๋ฆฌ_๊ฐœ์„ ์‚ฌํ•ญ_๋ชฉ๋ก.md new file mode 100644 index 00000000..666f41f1 --- /dev/null +++ b/ํ™”๋ฉด๊ด€๋ฆฌ_๋ฐ_ํ…Œ์ด๋ธ”๊ด€๋ฆฌ_๊ฐœ์„ ์‚ฌํ•ญ_๋ชฉ๋ก.md @@ -0,0 +1,386 @@ +# ํ™”๋ฉด๊ด€๋ฆฌ ๋ฐ ํ…Œ์ด๋ธ”๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ฐœ์„ ์‚ฌํ•ญ ๋ชฉ๋ก + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-11-03 +- **๋ชฉ์ **: ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๊ธฐ๋ฐ˜ ๊ฐœ์„ ์‚ฌํ•ญ ์ •๋ฆฌ +- **์šฐ์„ ์ˆœ์œ„**: ๋†’์Œ + +--- + +## 1. ํ™”๋ฉด๊ด€๋ฆฌ (Screen Management) ๊ฐœ์„ ์‚ฌํ•ญ + +### 1.1 ๋ฆฌ์ŠคํŠธ ์ปฌ๋Ÿผ Width ์กฐ์ ˆ ๊ธฐ๋Šฅ +**ํ˜„์žฌ ๋ฌธ์ œ**: ๋ฆฌ์ŠคํŠธ ์ปฌ๋Ÿผ์˜ ๋„ˆ๋น„๊ฐ€ ๊ณ ์ •๋˜์–ด ์žˆ์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์กฐ์ ˆํ•  ์ˆ˜ ์—†์Œ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ ์ปฌ๋Ÿผ์˜ ๋„ˆ๋น„๋ฅผ ๋“œ๋ž˜๊ทธ๋กœ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ +- ์กฐ์ ˆ๋œ ๋„ˆ๋น„๋Š” ์ €์žฅ๋˜์–ด ๋‹ค์Œ ์ ‘์† ์‹œ์—๋„ ์œ ์ง€๋˜์–ด์•ผ ํ•จ +- ์ตœ์†Œ/์ตœ๋Œ€ ๋„ˆ๋น„ ์ œํ•œ ํ•„์š” + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +- ์ปฌ๋Ÿผ ํ—ค๋”์— ๋ฆฌ์‚ฌ์ด์ € ํ•ธ๋“ค ์ถ”๊ฐ€ +- `ComponentData` ์ธํ„ฐํŽ˜์ด์Šค์— `columnWidths` ์†์„ฑ ์ถ”๊ฐ€ +- PropertiesPanel์—์„œ ๊ฐœ๋ณ„ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์„ค์ • UI ์ œ๊ณต + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/screen/ScreenDesigner.tsx` +- `frontend/components/screen/RealtimePreview.tsx` +- `frontend/types/screen.ts` + +--- + +### 1.2 ๋˜๋Œ๋ฆฌ๊ธฐ(Undo) ๋‹จ์ถ•ํ‚ค ์—๋Ÿฌ ์ˆ˜์ • +**ํ˜„์žฌ ๋ฌธ์ œ**: ๋˜๋Œ๋ฆฌ๊ธฐ ๋‹จ์ถ•ํ‚ค(Ctrl+Z/Cmd+Z) ์‹คํ–‰ ์‹œ ์—๋Ÿฌ ๋ฐœ์ƒ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ๋˜๋Œ๋ฆฌ๊ธฐ ๊ธฐ๋Šฅ์ด ์•ˆ์ •์ ์œผ๋กœ ์ž‘๋™ํ•ด์•ผ ํ•จ +- ๋‹ค์‹œ ์‹คํ–‰(Redo) ๊ธฐ๋Šฅ๋„ ํ•จ๊ป˜ ์ œ๊ณต (Ctrl+Y/Cmd+Shift+Z) + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +- ํžˆ์Šคํ† ๋ฆฌ ์Šคํƒ ๊ตฌํ˜„ (์ตœ๋Œ€ 50๊ฐœ ์ƒํƒœ ์ €์žฅ) +- `useUndo` ์ปค์Šคํ…€ ํ›… ์ƒ์„ฑ +- ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€ + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/hooks/useUndo.ts` (์‹ ๊ทœ ์ƒ์„ฑ) +- `frontend/components/screen/ScreenDesigner.tsx` + +--- + +### 1.3 ๋ฆฌ์ŠคํŠธ ํ—ค๋” ์Šคํƒ€์ผ ๊ฐœ์„  +**ํ˜„์žฌ ๋ฌธ์ œ**: ๋ฆฌ์ŠคํŠธ ํ—ค๋”๊ฐ€ ๋ˆˆ์— ์ž˜ ๋„์ง€ ์•Š์Œ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ํ—ค๋”๊ฐ€ ์‹œ๊ฐ์ ์œผ๋กœ ๊ตฌ๋ถ„๋˜์–ด์•ผ ํ•จ +- ๋ฐฐ๊ฒฝ์ƒ‰, ํฐํŠธ ๊ตต๊ธฐ, ํ…Œ๋‘๋ฆฌ ๋“ฑ์œผ๋กœ ๊ฐ•์กฐ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +- ํ—ค๋” ๊ธฐ๋ณธ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ: + - ๋ฐฐ๊ฒฝ์ƒ‰: `bg-muted` โ†’ `bg-primary/10` + - ํฐํŠธ: `font-medium` โ†’ `font-semibold` + - ํ•˜๋‹จ ํ…Œ๋‘๋ฆฌ: `border-b-2 border-primary` + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/screen/RealtimePreview.tsx` +- `frontend/components/screen-viewer/InteractiveScreenViewer.tsx` + +--- + +### 1.4 ํ…์ŠคํŠธ ์ค„๋ฐ”๊ฟˆ ๋ฌธ์ œ ๋ฐฉ์ง€ +**ํ˜„์žฌ ๋ฌธ์ œ**: ํ™”๋ฉด์„ ์ค„์˜€์„ ๋•Œ ํ…์ŠคํŠธ๊ฐ€ 2์ค„๋กœ ๋‚˜๋‰˜๊ฑฐ๋‚˜ ๊นจ์ง€๋Š” ํ˜„์ƒ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ํ…์ŠคํŠธ๊ฐ€ ํ•ญ์ƒ 1์ค„๋กœ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•จ +- ๊ธด ํ…์ŠคํŠธ๋Š” ๋ง์ค„์ž„ํ‘œ(...) ์ฒ˜๋ฆฌ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +- ๋ชจ๋“  ํ…์ŠคํŠธ ์š”์†Œ์— ๋‹ค์Œ ํด๋ž˜์Šค ์ ์šฉ: + ```tsx + className="truncate whitespace-nowrap overflow-hidden" + ``` +- ํˆดํŒ์œผ๋กœ ์ „์ฒด ํ…์ŠคํŠธ ํ‘œ์‹œ + +**๊ด€๋ จ ํŒŒ์ผ**: +- ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ๋ถ€๋ถ„ + +--- + +### 1.5 ์ˆ˜์ • ๋ชจ๋‹ฌ ์ž๋™ ๋‹ซ๊ธฐ +**ํ˜„์žฌ ๋ฌธ์ œ**: ์ˆ˜์ • ์™„๋ฃŒ ํ›„ ๋ชจ๋‹ฌ์ด ์ž๋™์œผ๋กœ ๋‹ซํžˆ์ง€ ์•Š์Œ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ์ˆ˜์ • ์™„๋ฃŒ ์‹œ ๋ชจ๋‹ฌ์ด ์ฆ‰์‹œ ๋‹ซํ˜€์•ผ ํ•จ +- ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ํ›„ ๋‹ซ๊ธฐ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +```typescript +const handleUpdate = async () => { + const result = await updateData(formData); + if (result.success) { + toast.success("์ˆ˜์ •์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + setIsModalOpen(false); // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + refreshList(); // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ + } +}; +``` + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/screen-viewer/InteractiveScreenViewer.tsx` + +--- + +### 1.6 ํ…Œ์ด๋ธ” Align ์กฐ์ ˆ ๊ธฐ๋Šฅ +**ํ˜„์žฌ ๋ฌธ์ œ**: ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์˜ ์ •๋ ฌ(align)์„ ์‚ฌ์šฉ์ž๊ฐ€ ์กฐ์ ˆํ•  ์ˆ˜ ์—†์Œ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ๊ฐ ์ปฌ๋Ÿผ์˜ ์ •๋ ฌ์„ left/center/right๋กœ ์„ค์ • ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ +- ์ˆซ์ž ํƒ€์ž…์€ ๊ธฐ๋ณธ์ ์œผ๋กœ right ์ •๋ ฌ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +- `TableColumnConfig` ์ธํ„ฐํŽ˜์ด์Šค์— `align` ์†์„ฑ ์ถ”๊ฐ€ +- PropertiesPanel์—์„œ ์ •๋ ฌ ์„ ํƒ UI ์ œ๊ณต +- ์ปฌ๋Ÿผ ํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์ •๋ ฌ ์„ค์ • + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/types/screen.ts` +- `frontend/components/screen/PropertiesPanel.tsx` + +--- + +### 1.7 ์ˆซ์ž ์ฒœ ๋‹จ์œ„ ์ฝค๋งˆ ํ‘œ์‹œ +**ํ˜„์žฌ ๋ฌธ์ œ**: ์ˆซ์ž๊ฐ€ ์ฝค๋งˆ ์—†์ด ํ‘œ์‹œ๋จ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ๋ชจ๋“  ์ˆซ์ž๋Š” ์ฒœ ๋‹จ์œ„๋งˆ๋‹ค ์ฝค๋งˆ(,)๋ฅผ ์ฐ์–ด์•ผ ํ•จ +- ์˜ˆ: 1000000 โ†’ 1,000,000 + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +```typescript +// ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ์ƒ์„ฑ +export const formatNumber = (value: number | string): string => { + const num = typeof value === "string" ? parseFloat(value) : value; + if (isNaN(num)) return "0"; + return new Intl.NumberFormat("ko-KR").format(num); +}; +``` + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/lib/utils/numberFormat.ts` (์‹ ๊ทœ ์ƒ์„ฑ) +- ๋ชจ๋“  ์ˆซ์ž ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ + +--- + +### 1.8 Drilldown UI ๊ฐœ์„  +**ํ˜„์žฌ ๋ฌธ์ œ**: ํ™”๋ฉด์ด ํšก์œผ๋กœ ๋„ˆ๋ฌด ๊ธธ๊ฒŒ ๋‚˜์—ด๋จ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ๊ณ„์ธต์  ๊ตฌ์กฐ๋กœ ์ •๋ณด ํ‘œ์‹œ +- ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ๊ธฐ๋Šฅ์œผ๋กœ ๊ณต๊ฐ„ ์ ˆ์•ฝ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +- Accordion ์ปดํฌ๋„ŒํŠธ ํ™œ์šฉ +- ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ์ ์šฉ +- ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๋ ˆ์ด์•„์›ƒ ํŒจํ„ด + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/screen/ScreenDesigner.tsx` +- `frontend/components/ui/accordion.tsx` + +--- + +## 2. ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ (Table Management) ๊ฐœ์„ ์‚ฌํ•ญ + +### 2.1 ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด ์„ ํƒ ๊ธฐ๋Šฅ +**ํ˜„์žฌ ๋ฌธ์ œ**: ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์—†์Œ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ํ…Œ์ด๋ธ” ์ƒ์„ฑ/์ˆ˜์ • ์‹œ ๋‹ค์Œ ์ •๋ณด๋ฅผ ์„ ํƒ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ: + - ํ…Œ์ด๋ธ” ํƒ€์ž… (๋งˆ์Šคํ„ฐ/ํŠธ๋žœ์žญ์…˜/์ฝ”๋“œ) + - ์นดํ…Œ๊ณ ๋ฆฌ + - ๋กœ๊ทธ ์‚ฌ์šฉ ์—ฌ๋ถ€ + - ๋ฒ„์ „ ๊ด€๋ฆฌ ์—ฌ๋ถ€ + - ์†Œํ”„ํŠธ ์‚ญ์ œ ์—ฌ๋ถ€ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +- `TableManagement.tsx`์— ์„ ํƒ UI ์ถ”๊ฐ€ +- `CREATE TABLE` DDL ์ž๋™ ์ƒ์„ฑ ์‹œ ์˜ต์…˜ ๋ฐ˜์˜ + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/table/TableManagement.tsx` +- `backend-node/src/controllers/tableController.ts` + +--- + +### 2.2 ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ +**ํ˜„์žฌ ๋ฌธ์ œ**: ๊ธฐ์กด ํ…Œ์ด๋ธ”์— ์ƒˆ ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ ๋ถ€์กฑ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ํ…Œ์ด๋ธ” ์ˆ˜์ • ์‹œ ์ปฌ๋Ÿผ์„ ๋™์ ์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ +- `ALTER TABLE ADD COLUMN` DDL ์ž๋™ ์ƒ์„ฑ +- ์ปฌ๋Ÿผ ์ˆœ์„œ ์กฐ์ • ๊ธฐ๋Šฅ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +```typescript +// ์ปฌ๋Ÿผ ์ถ”๊ฐ€ API +POST /api/table-management/tables/:tableName/columns +{ + "columnName": "new_column", + "dataType": "VARCHAR(100)", + "nullable": true, + "defaultValue": null +} +``` + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/table/TableManagement.tsx` +- `backend-node/src/controllers/tableController.ts` +- `backend-node/src/services/ddlExecutionService.ts` + +--- + +### 2.3 ํ…Œ์ด๋ธ” ๋ณต์ œ ๊ธฐ๋Šฅ +**ํ˜„์žฌ ๋ฌธ์ œ**: ๊ธฐ์กด ํ…Œ์ด๋ธ”์˜ ๊ตฌ์กฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์›€ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ๊ธฐ์กด ํ…Œ์ด๋ธ”์„ ๋ณต์ œํ•˜์—ฌ ์ƒˆ ํ…Œ์ด๋ธ” ์ƒ์„ฑ +- ๋‹ค์Œ ์ •๋ณด๋ฅผ ๋ณต์‚ฌ: + - ์ปฌ๋Ÿผ ๊ตฌ์กฐ (์ด๋ฆ„, ํƒ€์ž…, ์ œ์•ฝ์กฐ๊ฑด) + - ์ธ๋ฑ์Šค ์ •์˜ + - ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„ (์„ ํƒ์ ) +- ๋ฐ์ดํ„ฐ๋Š” ๋ณต์‚ฌํ•˜์ง€ ์•Š์Œ (๊ตฌ์กฐ๋งŒ) + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +```typescript +// ํ…Œ์ด๋ธ” ๋ณต์ œ API +POST /api/table-management/tables/:sourceTableName/clone +{ + "newTableName": "cloned_table", + "includeIndexes": true, + "includeForeignKeys": false, + "copyData": false +} +``` + +**๊ตฌํ˜„ ๋‹จ๊ณ„**: +1. ์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด ์กฐํšŒ (INFORMATION_SCHEMA) +2. DDL ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ +3. ์ƒˆ ํ…Œ์ด๋ธ” ์ƒ์„ฑ +4. ์ธ๋ฑ์Šค ๋ฐ ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +5. ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/table/TableManagement.tsx` +- `backend-node/src/controllers/tableController.ts` +- `backend-node/src/services/ddlExecutionService.ts` + +**์ฐธ๊ณ  ๋ฌธ์„œ**: +- `/Users/kimjuseok/ERP-node/ํ…Œ์ด๋ธ”_๋ณต์ œ_๊ธฐ๋Šฅ_๊ตฌํ˜„_๊ณ„ํš์„œ.md` + +--- + +### 2.4 ์ฑ„๋ฒˆ Rule ๊ด€๋ฆฌ ๊ธฐ๋Šฅ +**ํ˜„์žฌ ๋ฌธ์ œ**: ์ž๋™ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์‚ฌ์šฉ์ž๊ฐ€ ๊ด€๋ฆฌํ•  ์ˆ˜ ์—†์Œ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ UI +- ๊ทœ์น™ ํ˜•์‹: + - ์ ‘๋‘์‚ฌ (์˜ˆ: "PROD-") + - ๋‚ ์งœ ํฌ๋งท (์˜ˆ: "YYYYMMDD") + - ์ผ๋ จ๋ฒˆํ˜ธ ์ž๋ฆฟ์ˆ˜ (์˜ˆ: 5์ž๋ฆฌ โ†’ 00001) + - ๊ตฌ๋ถ„์ž (์˜ˆ: "-") +- ์˜ˆ์‹œ: `PROD-20251103-00001` + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +```typescript +interface NumberingRule { + id: string; + ruleName: string; + prefix: string; + dateFormat?: "YYYY" | "YYYYMM" | "YYYYMMDD" | "YYYYMMDD-HH"; + sequenceDigits: number; + separator: string; + resetPeriod: "none" | "daily" | "monthly" | "yearly"; + currentSequence: number; + tableName: string; + columnName: string; +} +``` + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/table/NumberingRuleManagement.tsx` (์‹ ๊ทœ ์ƒ์„ฑ) +- `backend-node/src/controllers/numberingRuleController.ts` (์‹ ๊ทœ ์ƒ์„ฑ) +- `backend-node/src/services/numberingRuleService.ts` (์‹ ๊ทœ ์ƒ์„ฑ) + +--- + +## 3. ์ œ์–ด ๊ด€๋ฆฌ (Flow Management) ๊ฐœ์„ ์‚ฌํ•ญ + +### 3.1 ์ œ๋ชฉ ํด๋ฆญ ์‹œ ๋…ธ๋“œ ์„ ํƒ ํ•ด์ œ +**ํ˜„์žฌ ๋ฌธ์ œ**: ์ œ๋ชฉ์„ ์ž…๋ ฅํ•  ๋•Œ ๋ฐฑ์ŠคํŽ˜์ด์Šค๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋…ธ๋“œ๊ฐ€ ์‚ญ์ œ๋จ + +**์š”๊ตฌ์‚ฌํ•ญ**: +- ์ œ๋ชฉ(ํ”Œ๋กœ์šฐ๋ช…) ์ž…๋ ฅ๋ž€ ํด๋ฆญ ์‹œ ๋…ธ๋“œ ์„ ํƒ์ด ํ•ด์ œ๋˜์–ด์•ผ ํ•จ +- ๋ฐฑ์ŠคํŽ˜์ด์Šค ํ‚ค๊ฐ€ ํ…์ŠคํŠธ ์ž…๋ ฅ์œผ๋กœ๋งŒ ์ž‘๋™ํ•ด์•ผ ํ•จ + +**๊ตฌํ˜„ ๋ฐฉ์•ˆ**: +```typescript +const handleTitleClick = (e: React.MouseEvent) => { + e.stopPropagation(); // ์ด๋ฒคํŠธ ์ „ํŒŒ ์ค‘์ง€ + setSelectedNodes([]); // ๋…ธ๋“œ ์„ ํƒ ํ•ด์ œ +}; + +const handleTitleKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation(); // ๋ฐฑ์ŠคํŽ˜์ด์Šค ํ‚ค๊ฐ€ ๋…ธ๋“œ ์‚ญ์ œ๋กœ ์ „ํŒŒ๋˜์ง€ ์•Š๋„๋ก +}; + + setFlowName(e.target.value)} +/> +``` + +**๊ด€๋ จ ํŒŒ์ผ**: +- `frontend/components/flow/FlowDesigner.tsx` +- `frontend/components/flow/FlowCanvas.tsx` + +--- + +## 4. ์šฐ์„ ์ˆœ์œ„ ๋ฐ ๊ตฌํ˜„ ์ผ์ • + +### ๋†’์Œ (์ฆ‰์‹œ ์ˆ˜์ • ํ•„์š”) +1. **๋˜๋Œ๋ฆฌ๊ธฐ ๋‹จ์ถ•ํ‚ค ์—๋Ÿฌ ์ˆ˜์ •** - ๊ธฐ๋Šฅ ์˜ค๋ฅ˜ +2. **์ˆ˜์ • ๋ชจ๋‹ฌ ์ž๋™ ๋‹ซ๊ธฐ** - ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ €ํ•ด +3. **์ œ์–ด๊ด€๋ฆฌ ์ œ๋ชฉ ์ž…๋ ฅ ๋ฌธ์ œ** - ๋ฐ์ดํ„ฐ ์†์‹ค ์œ„ํ—˜ +4. **์ˆซ์ž ์ฒœ ๋‹จ์œ„ ์ฝค๋งˆ ํ‘œ์‹œ** - ๊ฐ€๋…์„ฑ ๋ฌธ์ œ + +### ์ค‘๊ฐ„ (2์ฃผ ๋‚ด ์™„๋ฃŒ) +5. **๋ฆฌ์ŠคํŠธ ์ปฌ๋Ÿผ Width ์กฐ์ ˆ** +6. **๋ฆฌ์ŠคํŠธ ํ—ค๋” ์Šคํƒ€์ผ ๊ฐœ์„ ** +7. **ํ…์ŠคํŠธ ์ค„๋ฐ”๊ฟˆ ๋ฌธ์ œ ๋ฐฉ์ง€** +8. **ํ…Œ์ด๋ธ” Align ์กฐ์ ˆ** +9. **์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ** + +### ๋‚ฎ์Œ (๊ธฐ๋Šฅ ์ถ”๊ฐ€) +10. **ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด ์„ ํƒ** +11. **ํ…Œ์ด๋ธ” ๋ณต์ œ ๊ธฐ๋Šฅ** +12. **Drilldown UI ๊ฐœ์„ ** +13. **์ฑ„๋ฒˆ Rule ๊ด€๋ฆฌ** + +--- + +## 5. ํ…Œ์ŠคํŠธ ๊ณ„ํš + +๊ฐ ๊ฐœ์„ ์‚ฌํ•ญ ์™„๋ฃŒ ์‹œ ๋‹ค์Œ์„ ํ™•์ธ: + +### ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ +- [ ] ์ƒˆ ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•จ +- [ ] ๊ธฐ์กด ๊ธฐ๋Šฅ์— ์˜ํ–ฅ ์—†์Œ +- [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•จ + +### ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ…Œ์ŠคํŠธ +- [ ] UI๊ฐ€ ์ง๊ด€์ ์ž„ +- [ ] ๋ฐ˜์‘ ์†๋„๊ฐ€ ๋น ๋ฆ„ +- [ ] ๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ ๋Œ€์‘ + +### ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ +- [ ] ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ์„ฑ๋Šฅ ์ €ํ•˜ ์—†์Œ +- [ ] ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์—†์Œ + +--- + +## 6. ์ฐธ๊ณ  ๋ฌธ์„œ + +- [ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ ํ˜„ํ™ฉ](ํ™”๋ฉด๊ด€๋ฆฌ_๋ฐ_ํ…Œ์ด๋ธ”๊ด€๋ฆฌ_๊ฐœ์„ ์‚ฌํ•ญ_๋ชฉ๋ก.md) +- [ํ…Œ์ด๋ธ” ๋ณต์ œ ๊ธฐ๋Šฅ ๊ณ„ํš์„œ](ํ…Œ์ด๋ธ”_๋ณต์ œ_๊ธฐ๋Šฅ_๊ตฌํ˜„_๊ณ„ํš์„œ.md) +- [Shadcn/ui ๋ ˆ์ด์•„์›ƒ ํŒจํ„ด](docs/shadcn-ui-๋ ˆ์ด์•„์›ƒ-ํŒจํ„ด-๋ถ„์„-๋ณด๊ณ ์„œ.md) + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ์ž‘์„ฑ์ž | ๋‚ด์šฉ | +|------|--------|------| +| 2025-11-03 | ๊ฐœ๋ฐœํŒ€ | ์ดˆ์•ˆ ์ž‘์„ฑ | +