diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 8a943b96..9e06804b 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -1355,9 +1355,20 @@ export class DynamicFormService { console.log(`๐Ÿ“‹ ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ ์กฐํšŒ ๊ฒฐ๊ณผ:`, screenLayouts.length); // ์ €์žฅ ๋ฒ„ํŠผ ์ค‘์—์„œ ์ œ์–ด๊ด€๋ฆฌ๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒƒ ์ฐพ๊ธฐ + let controlConfigFound = false; for (const layout of screenLayouts) { const properties = layout.properties as any; + // ๋””๋ฒ„๊น…: ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ์ถœ๋ ฅ + console.log(`๐Ÿ” ์ปดํฌ๋„ŒํŠธ ๊ฒ€์‚ฌ:`, { + componentId: layout.component_id, + componentType: properties?.componentType, + actionType: properties?.componentConfig?.action?.type, + enableDataflowControl: properties?.webTypeConfig?.enableDataflowControl, + hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig, + hasDiagramId: !!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId, + }); + // ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์ด๊ณ  ์ €์žฅ ์•ก์…˜์ด๋ฉฐ ์ œ์–ด๊ด€๋ฆฌ๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ if ( properties?.componentType === "button-primary" && @@ -1365,6 +1376,7 @@ export class DynamicFormService { properties?.webTypeConfig?.enableDataflowControl === true && properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId ) { + controlConfigFound = true; const diagramId = properties.webTypeConfig.dataflowConfig.selectedDiagramId; const relationshipId = @@ -1377,9 +1389,39 @@ export class DynamicFormService { triggerType, }); - // ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ - const controlResult = - await this.dataflowControlService.executeDataflowControl( + // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ (relationshipId๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ๋กœ ๊ฐ„์ฃผ) + let controlResult: any; + + if (!relationshipId) { + // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ + console.log(`๐Ÿš€ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ (flowId: ${diagramId})`); + const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService"); + + const executionResult = await NodeFlowExecutionService.executeFlow(diagramId, { + sourceData: [savedData], + dataSourceType: "formData", + buttonId: "save-button", + screenId: screenId, + userId: userId, + formData: savedData, + }); + + controlResult = { + success: executionResult.success, + message: executionResult.message, + executedActions: executionResult.nodes?.map((node) => ({ + nodeId: node.nodeId, + status: node.status, + duration: node.duration, + })), + errors: executionResult.nodes + ?.filter((node) => node.status === "failed") + .map((node) => node.error || "์‹คํ–‰ ์‹คํŒจ"), + }; + } else { + // ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ + console.log(`๐ŸŽฏ ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ (relationshipId: ${relationshipId})`); + controlResult = await this.dataflowControlService.executeDataflowControl( diagramId, relationshipId, triggerType, @@ -1387,6 +1429,7 @@ export class DynamicFormService { tableName, userId ); + } console.log(`๐ŸŽฏ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ ๊ฒฐ๊ณผ:`, controlResult); @@ -1417,6 +1460,10 @@ export class DynamicFormService { break; } } + + if (!controlConfigFound) { + console.log(`โ„น๏ธ ์ œ์–ด๊ด€๋ฆฌ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค. (ํ™”๋ฉด ID: ${screenId})`); + } } catch (error) { console.error("โŒ ์ œ์–ด๊ด€๋ฆฌ ์„ค์ • ํ™•์ธ ๋ฐ ์‹คํ–‰ ์˜ค๋ฅ˜:", error); // ์—๋Ÿฌ๋ฅผ ๋‹ค์‹œ ๋˜์ง€์ง€ ์•Š์Œ - ๋ฉ”์ธ ์ €์žฅ ํ”„๋กœ์„ธ์Šค์— ์˜ํ–ฅ ์ฃผ์ง€ ์•Š๊ธฐ ์œ„ํ•ด diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 198c850b..f3c3d133 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -1278,6 +1278,11 @@ export class ScreenManagementService { }, }; + // ๐Ÿ” ๋””๋ฒ„๊น…: webTypeConfig.dataflowConfig ํ™•์ธ + if ((component as any).webTypeConfig?.dataflowConfig) { + console.log(`๐Ÿ” ์ปดํฌ๋„ŒํŠธ ${component.id}์˜ dataflowConfig:`, JSON.stringify((component as any).webTypeConfig.dataflowConfig, null, 2)); + } + await query( `INSERT INTO screen_layouts ( screen_id, component_type, component_id, parent_id, diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 353e487c..0e30d32d 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Search, Database, RefreshCw, Settings, Plus, Activity, Trash2 } from "lucide-react"; +import { Search, Database, RefreshCw, Settings, Plus, Activity, Trash2, Copy } from "lucide-react"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { toast } from "sonner"; import { useMultiLang } from "@/hooks/useMultiLang"; @@ -84,6 +84,10 @@ export default function TableManagementPage() { const [createTableModalOpen, setCreateTableModalOpen] = useState(false); const [addColumnModalOpen, setAddColumnModalOpen] = useState(false); const [ddlLogViewerOpen, setDdlLogViewerOpen] = useState(false); + + // ํ…Œ์ด๋ธ” ๋ณต์ œ ๊ด€๋ จ ์ƒํƒœ + const [duplicateModalMode, setDuplicateModalMode] = useState<"create" | "duplicate">("create"); + const [duplicateSourceTable, setDuplicateSourceTable] = useState(null); // ๋กœ๊ทธ ๋ทฐ์–ด ์ƒํƒœ const [logViewerOpen, setLogViewerOpen] = useState(false); @@ -97,8 +101,8 @@ export default function TableManagementPage() { // ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก (์ฒดํฌ๋ฐ•์Šค) const [selectedTableIds, setSelectedTableIds] = useState>(new Set()); - // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์—ฌ๋ถ€ ํ™•์ธ (ํšŒ์‚ฌ์ฝ”๋“œ๊ฐ€ "*"์ธ ๊ฒฝ์šฐ) - const isSuperAdmin = user?.companyCode === "*"; + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์—ฌ๋ถ€ ํ™•์ธ (ํšŒ์‚ฌ์ฝ”๋“œ๊ฐ€ "*" AND userType์ด "SUPER_ADMIN") + const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN"; // ๋‹ค๊ตญ์–ด ํ…์ŠคํŠธ ๋กœ๋“œ useEffect(() => { @@ -706,13 +710,36 @@ export default function TableManagementPage() { {isSuperAdmin && ( <> + + {selectedTable && ( diff --git a/frontend/components/screen/ScreenDesigner_new.tsx b/frontend/components/screen/ScreenDesigner_new.tsx index 16a50f3f..46c3ffdf 100644 --- a/frontend/components/screen/ScreenDesigner_new.tsx +++ b/frontend/components/screen/ScreenDesigner_new.tsx @@ -263,6 +263,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD try { setIsSaving(true); + + // ๐Ÿ” ๋””๋ฒ„๊น…: ์ €์žฅํ•  ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ํ™•์ธ + console.log("๐Ÿ” ๋ ˆ์ด์•„์›ƒ ์ €์žฅ ์š”์ฒญ:", { + screenId: selectedScreen.screenId, + componentsCount: layout.components.length, + components: layout.components.map(c => ({ + id: c.id, + type: c.type, + webTypeConfig: (c as any).webTypeConfig, + })), + }); + const response = await screenApi.saveScreenLayout(selectedScreen.screenId, layout); if (response.success) { toast.success("ํ™”๋ฉด์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); diff --git a/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx b/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx index 35e1befe..18a51bba 100644 --- a/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx @@ -63,11 +63,17 @@ export const ImprovedButtonControlConfigPanel: React.FC { const selectedFlow = flows.find((f) => f.flowId.toString() === flowId); if (selectedFlow) { - onUpdateProperty("webTypeConfig.dataflowConfig.flowConfig", { - flowId: selectedFlow.flowId, - flowName: selectedFlow.flowName, - executionTiming: "before", // ๊ธฐ๋ณธ๊ฐ’ - contextData: {}, + // ์ „์ฒด dataflowConfig ์—…๋ฐ์ดํŠธ (selectedDiagramId ํฌํ•จ) + onUpdateProperty("webTypeConfig.dataflowConfig", { + ...dataflowConfig, + selectedDiagramId: selectedFlow.flowId, // ๋ฐฑ์—”๋“œ์—์„œ ์‚ฌ์šฉ + selectedRelationshipId: null, // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ๋Š” ๊ด€๊ณ„ ID ๋ถˆํ•„์š” + flowConfig: { + flowId: selectedFlow.flowId, + flowName: selectedFlow.flowName, + executionTiming: "before", // ๊ธฐ๋ณธ๊ฐ’ + contextData: {}, + }, }); } }; diff --git a/frontend/lib/api/screen.ts b/frontend/lib/api/screen.ts index b8b77f85..695e5a51 100644 --- a/frontend/lib/api/screen.ts +++ b/frontend/lib/api/screen.ts @@ -151,6 +151,12 @@ export const screenApi = { await apiClient.post(`/screen-management/screens/${screenId}/layout`, layoutData); }, + // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (ScreenDesigner_new.tsx์šฉ) + saveScreenLayout: async (screenId: number, layoutData: LayoutData): Promise> => { + const response = await apiClient.post(`/screen-management/screens/${screenId}/layout`, layoutData); + return response.data; + }, + // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ getLayout: async (screenId: number): Promise => { const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`); diff --git a/frontend/lib/api/tableManagement.ts b/frontend/lib/api/tableManagement.ts index f0178a85..d03d83bf 100644 --- a/frontend/lib/api/tableManagement.ts +++ b/frontend/lib/api/tableManagement.ts @@ -54,9 +54,18 @@ export interface ColumnSettings { isVisible?: boolean; } +// ์ปฌ๋Ÿผ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์‘๋‹ต +export interface ColumnListData { + columns: ColumnTypeInfo[]; + total: number; + page: number; + size: number; + totalPages: number; +} + // API ์‘๋‹ต ํƒ€์ž…๋“ค export interface TableListResponse extends ApiResponse {} -export interface ColumnListResponse extends ApiResponse {} +export interface ColumnListResponse extends ApiResponse {} export interface ColumnSettingsResponse extends ApiResponse {} /** diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 497b42b7..9ee27c36 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -874,7 +874,9 @@ export const TableListComponent: React.FC = ({
{tableConfig.showHeader && (
-

{tableConfig.title || tableLabel}

+

+ {tableConfig.title || tableLabel || finalSelectedTable} +

)} @@ -936,7 +938,9 @@ export const TableListComponent: React.FC = ({ {/* ํ—ค๋” */} {tableConfig.showHeader && (
-

{tableConfig.title || tableLabel}

+

+ {tableConfig.title || tableLabel || finalSelectedTable} +

)} diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 6b977155..f268e625 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -727,6 +727,30 @@ export const TableListConfigPanel: React.FC = ({
ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์„ค์ •
+ {/* ํ…Œ์ด๋ธ” ์ œ๋ชฉ ์„ค์ • */} +
+
+

ํ…Œ์ด๋ธ” ์ œ๋ชฉ

+
+
+
+ + handleChange("title", e.target.value)} + placeholder="ํ…Œ์ด๋ธ” ์ œ๋ชฉ ์ž…๋ ฅ..." + className="h-8 text-xs" + /> +

+ ์šฐ์„ ์ˆœ์œ„: ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ œ๋ชฉ โ†’ ํ…Œ์ด๋ธ” ๋ผ๋ฒจ๋ช… โ†’ ํ…Œ์ด๋ธ”๋ช… +

+
+
+ {/* ๊ฐ€๋กœ ์Šคํฌ๋กค ๋ฐ ์ปฌ๋Ÿผ ๊ณ ์ • */}
diff --git a/frontend/types/ddl.ts b/frontend/types/ddl.ts index 38dce318..89cd447a 100644 --- a/frontend/types/ddl.ts +++ b/frontend/types/ddl.ts @@ -246,6 +246,9 @@ export interface CreateTableModalProps { isOpen: boolean; onClose: () => void; onSuccess: (result: DDLExecutionResult) => void; + // ๐Ÿ†• ๋ณต์ œ ๋ชจ๋“œ ๊ด€๋ จ + mode?: "create" | "duplicate"; + sourceTableName?: string; // ๋ณต์ œ ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋ช… } // ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์†์„ฑ diff --git a/ํ…Œ์ด๋ธ”_๋ณต์ œ_๊ธฐ๋Šฅ_๊ตฌํ˜„_๊ณ„ํš์„œ.md b/ํ…Œ์ด๋ธ”_๋ณต์ œ_๊ธฐ๋Šฅ_๊ตฌํ˜„_๊ณ„ํš์„œ.md new file mode 100644 index 00000000..49058dbe --- /dev/null +++ b/ํ…Œ์ด๋ธ”_๋ณต์ œ_๊ธฐ๋Šฅ_๊ตฌํ˜„_๊ณ„ํš์„œ.md @@ -0,0 +1,879 @@ +# ํ…Œ์ด๋ธ” ๋ณต์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ณ„ํš์„œ + +## ๐Ÿ“‹ ๊ฐœ์š” + +ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์— ๊ธฐ์กด ํ…Œ์ด๋ธ”์„ ๋ณต์ œํ•˜์—ฌ ์ƒˆ๋กœ์šด ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ณต์ œ ์‹œ ์›๋ณธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๊ตฌ์กฐ์™€ ์„ค์ •์„ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ • ํ›„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## ๐ŸŽฏ ์ฃผ์š” ์š”๊ตฌ์‚ฌํ•ญ + +### 1. ๊ถŒํ•œ ์ œํ•œ + +- **์ตœ๊ณ  ๊ด€๋ฆฌ์ž(company_code = "\*" && user_type = "SUPER_ADMIN")๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ** +- ์ผ๋ฐ˜ ํšŒ์‚ฌ ์‚ฌ์šฉ์ž๋Š” ํ…Œ์ด๋ธ” ๋ณต์ œ ๋ฒ„ํŠผ์ด ๋ณด์ด์ง€ ์•Š์Œ + +### 2. ๋ณต์ œ ํ”„๋กœ์„ธ์Šค + +1. ํ…Œ์ด๋ธ” ๋ชฉ๋ก์—์„œ ๋ณต์ œํ•  ํ…Œ์ด๋ธ” ์„ ํƒ (์ฒดํฌ๋ฐ•์Šค) +2. "๋ณต์ œ" ๋ฒ„ํŠผ ํด๋ฆญ +3. ๊ธฐ์กด "ํ…Œ์ด๋ธ” ์ƒ์„ฑ" ๋ชจ๋‹ฌ์ด "ํ…Œ์ด๋ธ” ๋ณต์ œ" ๋ชจ๋“œ๋กœ ์—ด๋ฆผ +4. ์›๋ณธ ํ…Œ์ด๋ธ”์˜ ์„ค์ •์ด ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง: + - ํ…Œ์ด๋ธ”๋ช…: ๋นˆ ์ƒํƒœ (์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ๋กœ ์ž…๋ ฅ) + - ํ…Œ์ด๋ธ” ์„ค๋ช…: ์›๋ณธ ์„ค๋ช… ๋ณต์‚ฌ (์ˆ˜์ • ๊ฐ€๋Šฅ) + - ์ปฌ๋Ÿผ ์ •์˜: ์›๋ณธ ์ปฌ๋Ÿผ๋“ค์ด ๋ชจ๋‘ ๋กœ๋“œ๋จ (์ˆ˜์ •/์‚ญ์ œ ๊ฐ€๋Šฅ) +5. ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ • ํ›„ ์ €์žฅ +6. ํ…Œ์ด๋ธ”๋ช… ์ค‘๋ณต ๊ฒ€์ฆ (์‹ค์‹œ๊ฐ„) +7. DDL ์‹คํ–‰ ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ + +### 3. ์ œ์•ฝ์‚ฌํ•ญ + +- **ํ…Œ์ด๋ธ”๋ช… ์ค‘๋ณต ๋ถˆ๊ฐ€** (์‹ค์‹œ๊ฐ„ ๊ฒ€์ฆ) +- ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ”์€ ๋ณต์ œ ๋ถˆ๊ฐ€ (์„ ํƒ ๋ถˆ๊ฐ€ ์ฒ˜๋ฆฌ) +- ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ์ปฌ๋Ÿผ ํ•„์š” + +--- + +## ๐Ÿ—๏ธ ๊ตฌํ˜„ ์•„ํ‚คํ…์ฒ˜ + +### ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ + +``` +๐Ÿ“ฆ ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ ํŽ˜์ด์ง€ (tableMng/page.tsx) +โ”œโ”€โ”€ ๐Ÿ“„ ํ…Œ์ด๋ธ” ๋ชฉ๋ก (๊ธฐ์กด) +โ”‚ โ”œโ”€โ”€ ์ฒดํฌ๋ฐ•์Šค (๋‹จ์ผ ์„ ํƒ ๋˜๋Š” ๋‹ค์ค‘ ์„ ํƒ) +โ”‚ โ””โ”€โ”€ ๋ณต์ œ ๋ฒ„ํŠผ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ํ‘œ์‹œ) +โ”‚ +โ””โ”€โ”€ ๐Ÿ”ง CreateTableModal ํ™•์žฅ (CreateTableModal.tsx) + โ”œโ”€โ”€ ๋ชจ๋“œ: "create" | "duplicate" + โ”œโ”€โ”€ Props ์ถ”๊ฐ€: duplicateTableName?, isDuplicateMode? + โ”œโ”€โ”€ ๋ณต์ œ ๋ชจ๋“œ ์‹œ ์ž๋™ ๋กœ๋“œ: + โ”‚ โ”œโ”€โ”€ ์›๋ณธ ํ…Œ์ด๋ธ” ์„ค๋ช… + โ”‚ โ””โ”€โ”€ ์›๋ณธ ์ปฌ๋Ÿผ ๋ชฉ๋ก (์ „์ฒด ์„ค์ • ํฌํ•จ) + โ””โ”€โ”€ ์‹ค์‹œ๊ฐ„ ํ…Œ์ด๋ธ”๋ช… ์ค‘๋ณต ๊ฒ€์ฆ +``` + +--- + +## ๐Ÿ“ ์ƒ์„ธ ๊ตฌํ˜„ ๊ณ„ํš + +### Phase 1: UI ์ถ”๊ฐ€ (ํ…Œ์ด๋ธ” ๋ชฉ๋ก ํŽ˜์ด์ง€) + +#### 1.1. ์ฒดํฌ๋ฐ•์Šค ์ถ”๊ฐ€ + +**ํŒŒ์ผ**: `frontend/app/(main)/admin/tableMng/page.tsx` + +**๋ณ€๊ฒฝ์‚ฌํ•ญ**: + +- ๊ฐ ํ…Œ์ด๋ธ” ํ–‰์— ์ฒดํฌ๋ฐ•์Šค ์ถ”๊ฐ€ +- ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ์ƒํƒœ ๊ด€๋ฆฌ: `selectedTableIds` (Set) +- ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ”์€ ์ฒดํฌ๋ฐ•์Šค ๋น„ํ™œ์„ฑํ™” + +```typescript +// ์ƒํƒœ ์ถ”๊ฐ€ +const [selectedTableIds, setSelectedTableIds] = useState>( + new Set() +); + +// ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์—ฌ๋ถ€ ํ™•์ธ ์ˆ˜์ • (๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ํ•„์š”) +// โŒ ๊ธฐ์กด: const isSuperAdmin = user?.companyCode === "*"; +// โœ… ์ˆ˜์ •: const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN"; + +// ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก (๋ณต์ œ ๋ถˆ๊ฐ€) +const SYSTEM_TABLES = [ + "user_info", + "company_info", + "menu_info", + "auth_group", + "menu_auth_group", + "user_auth", + "dept_info", + "code_info", + "code_category", + // ... ๊ธฐํƒ€ ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” +]; + +// ์ฒดํฌ๋ฐ•์Šค ํ•ธ๋“ค๋Ÿฌ +const handleTableSelect = (tableName: string) => { + setSelectedTableIds((prev) => { + const newSet = new Set(prev); + if (newSet.has(tableName)) { + newSet.delete(tableName); + } else { + newSet.add(tableName); + } + return newSet; + }); +}; +``` + +#### 1.2. ๋ณต์ œ ๋ฒ„ํŠผ ์ถ”๊ฐ€ + +**์œ„์น˜**: ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ƒ๋‹จ (์ƒˆ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋ฒ„ํŠผ ์˜†) + +```typescript +{ + isSuperAdmin && ( + + ); +} +``` + +**์กฐ๊ฑด**: + +- ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ํ‘œ์‹œ +- ์ •ํ™•ํžˆ 1๊ฐœ์˜ ํ…Œ์ด๋ธ”์ด ์„ ํƒ๋œ ๊ฒฝ์šฐ๋งŒ ํ™œ์„ฑํ™” +- ์„ ํƒ๋œ ํ…Œ์ด๋ธ”์ด ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ ๋น„ํ™œ์„ฑํ™” + +--- + +### Phase 2: CreateTableModal ํ™•์žฅ + +#### 2.1. Props ํ™•์žฅ + +**ํŒŒ์ผ**: `frontend/components/admin/CreateTableModal.tsx` + +```typescript +export interface CreateTableModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: (result: any) => void; + + // ๐Ÿ†• ๋ณต์ œ ๋ชจ๋“œ ๊ด€๋ จ + mode?: "create" | "duplicate"; + sourceTableName?: string; // ๋ณต์ œ ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋ช… +} +``` + +#### 2.2. ๋ณต์ œ ๋ชจ๋“œ ๊ฐ์ง€ ๋ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + +```typescript +export function CreateTableModal({ + isOpen, + onClose, + onSuccess, + mode = "create", + sourceTableName, +}: CreateTableModalProps) { + const isDuplicateMode = mode === "duplicate" && sourceTableName; + + // ๋ณต์ œ ๋ชจ๋“œ์ผ ๋•Œ ์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ + useEffect(() => { + if (isOpen && isDuplicateMode && sourceTableName) { + loadSourceTableData(sourceTableName); + } + }, [isOpen, isDuplicateMode, sourceTableName]); + + const loadSourceTableData = async (tableName: string) => { + setLoading(true); + try { + // 1. ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ + const tableResponse = await apiClient.get( + `/table-management/tables/${tableName}` + ); + + if (tableResponse.data.success) { + const tableInfo = tableResponse.data.data; + setDescription(tableInfo.description || ""); + } + + // 2. ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ + const columnsResponse = await tableManagementApi.getColumnList(tableName); + + if (columnsResponse.success && columnsResponse.data) { + const loadedColumns: CreateColumnDefinition[] = + columnsResponse.data.map((col, idx) => ({ + name: col.columnName, + label: col.displayName || col.columnName, + inputType: col.webType as any, + nullable: col.isNullable === "YES", + order: idx + 1, + description: col.description, + codeCategory: col.codeCategory || undefined, + referenceTable: col.referenceTable || undefined, + referenceColumn: col.referenceColumn || undefined, + displayColumn: col.displayColumn || undefined, + })); + + setColumns(loadedColumns); + toast.success(`${tableName} ํ…Œ์ด๋ธ” ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค.`); + } + } catch (error: any) { + console.error("์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:", error); + toast.error("์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + onClose(); + } finally { + setLoading(false); + } + }; +} +``` + +#### 2.3. ๋ชจ๋‹ฌ ์ œ๋ชฉ ๋ฐ ๋ฒ„ํŠผ ํ…์ŠคํŠธ ๋ณ€๊ฒฝ + +```typescript +return ( + + + + + {isDuplicateMode ? "ํ…Œ์ด๋ธ” ๋ณต์ œ" : "์ƒˆ ํ…Œ์ด๋ธ” ์ƒ์„ฑ"} + + + {isDuplicateMode + ? `${sourceTableName} ํ…Œ์ด๋ธ”์„ ๋ณต์ œํ•˜์—ฌ ์ƒˆ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.` + : "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒˆ๋กœ์šด ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค."} + + + + {/* ... ํผ ๋‚ด์šฉ ... */} + + + + + + + +); +``` + +#### 2.4. ํ…Œ์ด๋ธ”๋ช… ์‹ค์‹œ๊ฐ„ ์ค‘๋ณต ๊ฒ€์ฆ ๊ฐ•ํ™” + +```typescript +// ํ…Œ์ด๋ธ”๋ช… ๋ณ€๊ฒฝ ์‹œ ์‹ค์‹œ๊ฐ„ ๊ฒ€์ฆ +const handleTableNameChange = async (name: string) => { + setTableName(name); + + // ๊ธฐ๋ณธ ๊ฒ€์ฆ + const error = validateTableName(name); + if (error) { + setTableNameError(error); + return; + } + + // ์ค‘๋ณต ๊ฒ€์ฆ + setValidating(true); + try { + const result = await tableManagementApi.checkTableExists(name); + if (result.success && result.data?.exists) { + setTableNameError("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ํ…Œ์ด๋ธ”๋ช…์ž…๋‹ˆ๋‹ค."); + } else { + setTableNameError(""); + } + } catch (error) { + console.error("ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ ์˜ค๋ฅ˜:", error); + } finally { + setValidating(false); + } +}; +``` + +--- + +### Phase 3: ๋ฐฑ์—”๋“œ API ๊ตฌํ˜„ (ํ•„์š” ์‹œ) + +#### 3.1. ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ API + +**์—”๋“œํฌ์ธํŠธ**: `GET /api/table-management/tables/:tableName` + +**์‘๋‹ต**: + +```json +{ + "success": true, + "data": { + "tableName": "contracts", + "displayName": "๊ณ„์•ฝ ๊ด€๋ฆฌ", + "description": "๊ณ„์•ฝ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํ…Œ์ด๋ธ”", + "columnCount": 15 + } +} +``` + +**๊ตฌํ˜„ ์œ„์น˜**: `backend-node/src/controllers/tableManagementController.ts` + +```typescript +/** + * ํŠน์ • ํ…Œ์ด๋ธ”์˜ ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ + */ +async getTableInfo(req: Request, res: Response) { + const { tableName } = req.params; + + try { + const query = ` + SELECT + t.table_name as "tableName", + dt.display_name as "displayName", + dt.description as "description", + COUNT(c.column_name) as "columnCount" + FROM information_schema.tables t + LEFT JOIN db_table_types dt ON dt.table_name = t.table_name + LEFT JOIN information_schema.columns c ON c.table_name = t.table_name + WHERE t.table_schema = 'public' + AND t.table_name = $1 + GROUP BY t.table_name, dt.display_name, dt.description + `; + + const result = await pool.query(query, [tableName]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: `ํ…Œ์ด๋ธ” '${tableName}'์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.` + }); + } + + return res.json({ + success: true, + data: result.rows[0] + }); + + } catch (error: any) { + logger.error("ํ…Œ์ด๋ธ” ์ •๋ณด ์กฐํšŒ ์‹คํŒจ", { tableName, error: error.message }); + return res.status(500).json({ + success: false, + message: "ํ…Œ์ด๋ธ” ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค." + }); + } +} +``` + +#### 3.2. ๋ผ์šฐํŠธ ์ถ”๊ฐ€ + +**ํŒŒ์ผ**: `backend-node/src/routes/tableManagement.ts` + +```typescript +// ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ (์ƒˆ๋กœ ์ถ”๊ฐ€) +router.get("/tables/:tableName", controller.getTableInfo); +``` + +--- + +### Phase 4: ํ†ตํ•ฉ ๋ฐ ํ…Œ์ŠคํŠธ + +#### 4.1. ํ…Œ์ด๋ธ” ๋ชฉ๋ก ํŽ˜์ด์ง€์—์„œ ๋ณต์ œ ํ๋ฆ„ ๊ตฌํ˜„ + +```typescript +// tableMng/page.tsx + +const handleDuplicateTable = async () => { + // ์„ ํƒ๋œ ํ…Œ์ด๋ธ” 1๊ฐœ ํ™•์ธ + if (selectedTableIds.size !== 1) { + toast.error("๋ณต์ œํ•  ํ…Œ์ด๋ธ”์„ 1๊ฐœ๋งŒ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + + const sourceTable = Array.from(selectedTableIds)[0]; + + // ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ์ฒดํฌ + if (SYSTEM_TABLES.includes(sourceTable)) { + toast.error("์‹œ์Šคํ…œ ํ…Œ์ด๋ธ”์€ ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + // ๋ณต์ œ ๋ชจ๋‹ฌ ์—ด๊ธฐ + setDuplicateSourceTable(sourceTable); + setCreateTableModalMode("duplicate"); + setCreateTableModalOpen(true); +}; + +// ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ + { + setCreateTableModalOpen(false); + setDuplicateSourceTable(null); + setCreateTableModalMode("create"); + }} + onSuccess={(result) => { + loadTables(); // ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ + setSelectedTableIds(new Set()); // ์„ ํƒ ์ดˆ๊ธฐํ™” + }} + mode={createTableModalMode} + sourceTableName={duplicateSourceTable} +/>; +``` + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 1: ์ •์ƒ ๋ณต์ œ ํ๋ฆ„ + +1. ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธ +2. ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ ํŽ˜์ด์ง€ ์ ‘์† +3. ๋ณต์ œํ•  ํ…Œ์ด๋ธ” 1๊ฐœ ์„ ํƒ (์˜ˆ: `contracts`) +4. "ํ…Œ์ด๋ธ” ๋ณต์ œ" ๋ฒ„ํŠผ ํด๋ฆญ +5. ๋ชจ๋‹ฌ์ด ์—ด๋ฆฌ๊ณ  ์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด๊ฐ€ ์ž๋™์œผ๋กœ ๋กœ๋“œ๋จ +6. ์ƒˆ ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ (์˜ˆ: `contracts_backup`) +7. ์ปฌ๋Ÿผ ์ •๋ณด ํ™•์ธ ๋ฐ ํ•„์š” ์‹œ ์ˆ˜์ • +8. "๋ณต์ œ ์ƒ์„ฑ" ๋ฒ„ํŠผ ํด๋ฆญ +9. ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜๊ณ  ๋ชฉ๋ก์— ํ‘œ์‹œ๋จ +10. ์„ฑ๊ณต ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ํ™•์ธ + +### ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 2: ํ…Œ์ด๋ธ”๋ช… ์ค‘๋ณต ๊ฒ€์ฆ + +1. ํ…Œ์ด๋ธ” ๋ณต์ œ ํ๋ฆ„ ์‹œ์ž‘ +2. ๊ธฐ์กด์— ์กด์žฌํ•˜๋Š” ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ (์˜ˆ: `user_info`) +3. ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ: "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ํ…Œ์ด๋ธ”๋ช…์ž…๋‹ˆ๋‹ค." +4. "๋ณต์ œ ์ƒ์„ฑ" ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” ํ™•์ธ + +### ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 3: ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ๋ณต์ œ ์ œํ•œ + +1. ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ๋„ (์˜ˆ: `user_info`) +2. ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Œ +3. ๋˜๋Š” ์„ ํƒ ์‹œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€: "์‹œ์Šคํ…œ ํ…Œ์ด๋ธ”์€ ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + +### ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 4: ๊ถŒํ•œ ์ œํ•œ + +1. ์ผ๋ฐ˜ ํšŒ์‚ฌ ์‚ฌ์šฉ์ž๋กœ ๋กœ๊ทธ์ธ +2. ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ ํŽ˜์ด์ง€ ์ ‘์† +3. "ํ…Œ์ด๋ธ” ๋ณต์ œ" ๋ฒ„ํŠผ์ด ๋ณด์ด์ง€ ์•Š์Œ +4. ์ฒดํฌ๋ฐ•์Šค๋„ ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ (์„ ํƒ ์‚ฌํ•ญ) + +### ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 5: ์ปฌ๋Ÿผ ์ˆ˜์ • ํ›„ ๋ณต์ œ + +1. ํ…Œ์ด๋ธ” ๋ณต์ œ ๋ชจ๋‹ฌ ์—ด๊ธฐ +2. ์›๋ณธ ์ปฌ๋Ÿผ ์ค‘ ์ผ๋ถ€ ์‚ญ์ œ +3. ์ƒˆ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ +4. ์ปฌ๋Ÿผ ๋ผ๋ฒจ ๋ณ€๊ฒฝ +5. ์ €์žฅ ์‹œ ์ˆ˜์ •๋œ ๊ตฌ์กฐ๋กœ ํ…Œ์ด๋ธ” ์ƒ์„ฑ๋จ + +--- + +## ๐Ÿ“Š ๋ฐ์ดํ„ฐ ํ๋ฆ„๋„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. ํ…Œ์ด๋ธ” ์„ ํƒ (์ฒดํฌ๋ฐ•์Šค) โ”‚ +โ”‚ - ์‚ฌ์šฉ์ž๊ฐ€ ๋ณต์ œํ•  ํ…Œ์ด๋ธ” 1๊ฐœ ์„ ํƒ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2. "ํ…Œ์ด๋ธ” ๋ณต์ œ" ๋ฒ„ํŠผ ํด๋ฆญ โ”‚ +โ”‚ - handleDuplicateTable() ์‹คํ–‰ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 3. CreateTableModal ์—ด๋ฆผ (๋ณต์ œ ๋ชจ๋“œ) โ”‚ +โ”‚ - mode: "duplicate" โ”‚ +โ”‚ - sourceTableName: ์„ ํƒ๋œ ํ…Œ์ด๋ธ”๋ช… โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 4. ์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด ์ž๋™ ๋กœ๋“œ โ”‚ +โ”‚ - GET /api/table-management/tables/:tableName โ”‚ +โ”‚ โ†’ ํ…Œ์ด๋ธ” ์„ค๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ โ”‚ +โ”‚ - GET /api/table-management/tables/:tableName/columns โ”‚ +โ”‚ โ†’ ๋ชจ๋“  ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 5. ์‚ฌ์šฉ์ž ํŽธ์ง‘ โ”‚ +โ”‚ - ์ƒˆ ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ (์‹ค์‹œ๊ฐ„ ์ค‘๋ณต ๊ฒ€์ฆ) โ”‚ +โ”‚ - ํ…Œ์ด๋ธ” ์„ค๋ช… ์ˆ˜์ • โ”‚ +โ”‚ - ์ปฌ๋Ÿผ ์ถ”๊ฐ€/์‚ญ์ œ/์ˆ˜์ • โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 6. "๋ณต์ œ ์ƒ์„ฑ" ๋ฒ„ํŠผ ํด๋ฆญ โ”‚ +โ”‚ - POST /api/ddl/create-table โ”‚ +โ”‚ (๊ธฐ์กด ํ…Œ์ด๋ธ” ์ƒ์„ฑ API ์žฌ์‚ฌ์šฉ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 7. DDL ์‹คํ–‰ ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ โ”‚ +โ”‚ - CREATE TABLE ... (์ƒˆ ํ…Œ์ด๋ธ”๋ช…) โ”‚ +โ”‚ - db_column_types ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ โ”‚ +โ”‚ - db_table_types ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 8. ์„ฑ๊ณต ์ฒ˜๋ฆฌ โ”‚ +โ”‚ - ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ โ”‚ +โ”‚ - ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ โ”‚ +โ”‚ - ์„ฑ๊ณต ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐Ÿš€ ๊ตฌํ˜„ ์ˆœ์„œ (์šฐ์„ ์ˆœ์œ„) + +### Step 0: ๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ฒดํฌ ๋กœ์ง) + +1. โœ… `frontend/app/(main)/admin/tableMng/page.tsx` - line 101 + - ๊ธฐ์กด: `const isSuperAdmin = user?.companyCode === "*";` + - ์ˆ˜์ •: `const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN";` + +**์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 5๋ถ„ + +### Step 1: UI ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ (ํ”„๋ก ํŠธ์—”๋“œ) + +1. โœ… ํ…Œ์ด๋ธ” ๋ชฉ๋ก์— ์ฒดํฌ๋ฐ•์Šค ์ถ”๊ฐ€ +2. โœ… ๋ณต์ œ ๋ฒ„ํŠผ ์ถ”๊ฐ€ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ํ‘œ์‹œ) +3. โœ… ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ์ƒํƒœ ๊ด€๋ฆฌ +4. โœ… ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ๋ณต์ œ ์ œํ•œ + +**์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1-2์‹œ๊ฐ„ + +### Step 2: CreateTableModal ํ™•์žฅ + +1. โœ… Props ํ™•์žฅ (mode, sourceTableName) +2. โœ… ๋ณต์ œ ๋ชจ๋“œ ๊ฐ์ง€ ๋กœ์ง +3. โœ… ์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ ํ•จ์ˆ˜ +4. โœ… ๋ชจ๋‹ฌ ์ œ๋ชฉ ๋ฐ ๋ฒ„ํŠผ ํ…์ŠคํŠธ ๋™์  ๋ณ€๊ฒฝ +5. โœ… ํ…Œ์ด๋ธ”๋ช… ์‹ค์‹œ๊ฐ„ ์ค‘๋ณต ๊ฒ€์ฆ + +**์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 2-3์‹œ๊ฐ„ + +### Step 3: ๋ฐฑ์—”๋“œ API ์ถ”๊ฐ€ (ํ•„์š” ์‹œ) + +1. โœ… ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ API ์ถ”๊ฐ€ +2. โœ… ๋ผ์šฐํŠธ ์ถ”๊ฐ€ +3. โœ… ๊ถŒํ•œ ๊ฒ€์ฆ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) + +**์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1์‹œ๊ฐ„ + +### Step 4: ํ†ตํ•ฉ ๋ฐ ํ…Œ์ŠคํŠธ + +1. โœ… ์ „์ฒด ํ๋ฆ„ ํ†ตํ•ฉ +2. โœ… ๊ฐ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์‹คํ–‰ +3. โœ… ๋ฒ„๊ทธ ์ˆ˜์ • ๋ฐ ์ตœ์ ํ™” + +**์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 2-3์‹œ๊ฐ„ + +**์ „์ฒด ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: **6-9์‹œ๊ฐ„** (๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ํฌํ•จ) + +--- + +## ๐Ÿ”’ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ + +### 1. ๊ถŒํ•œ ์ฒดํฌ + +- ํ”„๋ก ํŠธ์—”๋“œ: `isSuperAdmin` ํ”Œ๋ž˜๊ทธ๋กœ UI ์ œ์–ด + - `user?.companyCode === "*" && user?.userType === "SUPER_ADMIN"` +- ๋ฐฑ์—”๋“œ: ๋ชจ๋“  API ํ˜ธ์ถœ ์‹œ ๋‘ ๊ฐ€์ง€ ์กฐ๊ฑด ๋ชจ๋‘ ๊ฒ€์ฆ + +```typescript +// ๋ฐฑ์—”๋“œ ๋ฏธ๋“ค์›จ์–ด (๊ธฐ์กด requireSuperAdmin ์žฌ์‚ฌ์šฉ) +import { requireSuperAdmin } from "@/middleware/superAdminMiddleware"; + +// ๋˜๋Š” ์ธ๋ผ์ธ ์ฒดํฌ +const requireSuperAdmin = (req: Request, res: Response, next: NextFunction) => { + if (req.user?.companyCode !== "*" || req.user?.userType !== "SUPER_ADMIN") { + return res.status(403).json({ + success: false, + message: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + next(); +}; + +// ๋ผ์šฐํŠธ ์ ์šฉ +router.post( + "/tables/:tableName/duplicate", + requireSuperAdmin, + controller.duplicateTable +); +``` + +### 2. ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ๋ณดํ˜ธ + +- SYSTEM_TABLES ์ƒ์ˆ˜๋กœ ๊ด€๋ฆฌ +- ๋ณต์ œ ์‹œ๋„ ์‹œ ์„œ๋ฒ„ ์ธก์—์„œ๋„ ๊ฒ€์ฆ + +### 3. ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ + +- SQL Injection ๋ฐฉ์ง€: ์ •๊ทœ์‹ ๊ฒ€์ฆ (`^[a-z][a-z0-9_]*$`) +- ์˜ˆ์•ฝ์–ด ์ฒดํฌ +- ๊ธธ์ด ์ œํ•œ (3-63์ž) + +--- + +## ๐Ÿ“Œ ์ฃผ์š” ์ฒดํฌํฌ์ธํŠธ + +### ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ (UX) + +- โœ… ๋ณต์ œ ๋ฒ„ํŠผ์€ 1๊ฐœ ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ์—๋งŒ ํ™œ์„ฑํ™” +- โœ… ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ (๋ชจ๋‹ฌ ์—ด๋ฆด ๋•Œ) +- โœ… ์‹ค์‹œ๊ฐ„ ํ…Œ์ด๋ธ”๋ช… ์ค‘๋ณต ๊ฒ€์ฆ +- โœ… ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ +- โœ… ์„ฑ๊ณต/์‹คํŒจ ํ† ์ŠคํŠธ ์•Œ๋ฆผ + +### ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ + +- โœ… ์›๋ณธ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ ์ •๋ณด ์ •ํ™•ํžˆ ๋ณต์‚ฌ +- โœ… webType, codeCategory, referenceTable ๋“ฑ ๊ด€๊ณ„ ์ •๋ณด ํฌํ•จ +- โœ… ์ปฌ๋Ÿผ ์ˆœ์„œ ์œ ์ง€ + +### ์„ฑ๋Šฅ + +- โœ… ์ปฌ๋Ÿผ์ด ๋งŽ์€ ํ…Œ์ด๋ธ”๋„ ๋น ๋ฅด๊ฒŒ ๋กœ๋“œ +- โœ… ๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ ์ตœ์†Œํ™” +- โœ… ๋กœ๋”ฉ ์ค‘ ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ + +--- + +## ๐ŸŽจ UI ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ + +### ๋ณต์ œ ๋ฒ„ํŠผ + +```tsx + +``` + +### ์ฒดํฌ๋ฐ•์Šค (ํ…Œ์ด๋ธ” ํ–‰) + +```tsx + handleTableSelect(table.tableName)} + disabled={SYSTEM_TABLES.includes(table.tableName)} + className="h-4 w-4" +/> +``` + +### ๋ชจ๋‹ฌ ์ œ๋ชฉ (๋ณต์ œ ๋ชจ๋“œ) + +```tsx + + ํ…Œ์ด๋ธ” ๋ณต์ œ + + + {sourceTableName} ํ…Œ์ด๋ธ”์„ ๋ณต์ œํ•˜์—ฌ ์ƒˆ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + +``` + +--- + +## ๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ + +### ๊ธฐ์กด ๊ตฌํ˜„ ํŒŒ์ผ + +- `frontend/app/(main)/admin/tableMng/page.tsx` - ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ ํŽ˜์ด์ง€ +- `frontend/components/admin/CreateTableModal.tsx` - ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋ชจ๋‹ฌ +- `frontend/components/admin/ColumnDefinitionTable.tsx` - ์ปฌ๋Ÿผ ์ •์˜ ํ…Œ์ด๋ธ” +- `frontend/lib/api/tableManagement.ts` - ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ API +- `backend-node/src/controllers/tableManagementController.ts` - ๋ฐฑ์—”๋“œ ์ปจํŠธ๋กค๋Ÿฌ + +### ์œ ์‚ฌ ๊ธฐ๋Šฅ ์ฐธ๊ณ  + +- Yard ๋ ˆ์ด์•„์›ƒ ๋ณต์ œ ๊ธฐ๋Šฅ: `YardLayoutList.tsx` (onDuplicate ํ•ธ๋“ค๋Ÿฌ) +- ๋ฉ”๋‰ด ๋ณต์ œ ๊ธฐ๋Šฅ (์žˆ๋‹ค๋ฉด) + +--- + +## โœ… ์™„๋ฃŒ ๊ธฐ์ค€ + +### Phase 1 ์™„๋ฃŒ ์กฐ๊ฑด + +- [ ] ํ…Œ์ด๋ธ” ๋ชฉ๋ก์— ์ฒดํฌ๋ฐ•์Šค ์ถ”๊ฐ€๋จ +- [ ] ๋ณต์ œ ๋ฒ„ํŠผ ์ถ”๊ฐ€ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ๋ณด์ž„) +- [ ] 1๊ฐœ ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ์—๋งŒ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” +- [ ] ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ”์€ ์„ ํƒ ๋ถˆ๊ฐ€ + +### Phase 2 ์™„๋ฃŒ ์กฐ๊ฑด + +- [ ] CreateTableModal์ด ๋ณต์ œ ๋ชจ๋“œ๋ฅผ ์ง€์› +- [ ] ์›๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด๊ฐ€ ์ž๋™์œผ๋กœ ๋กœ๋“œ๋จ +- [ ] ๋ชจ๋‹ฌ ์ œ๋ชฉ๊ณผ ๋ฒ„ํŠผ ํ…์ŠคํŠธ๊ฐ€ ๋ชจ๋“œ์— ๋”ฐ๋ผ ๋ณ€๊ฒฝ๋จ +- [ ] ํ…Œ์ด๋ธ”๋ช… ์‹ค์‹œ๊ฐ„ ์ค‘๋ณต ๊ฒ€์ฆ ์ž‘๋™ + +### Phase 3 ์™„๋ฃŒ ์กฐ๊ฑด + +- [ ] ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ API ์ถ”๊ฐ€ +- [ ] ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ๊ฒ€์ฆ +- [ ] ๋ผ์šฐํŠธ ๋“ฑ๋ก + +### Phase 4 ์™„๋ฃŒ ์กฐ๊ฑด + +- [ ] ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ํ†ต๊ณผ +- [ ] ๋ฒ„๊ทธ ์ˆ˜์ • ์™„๋ฃŒ +- [ ] ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ ๋ฌธ์„œ ์ž‘์„ฑ + +--- + +## ๐Ÿ› ์˜ˆ์ƒ ์ด์Šˆ ๋ฐ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ + +### ์ด์Šˆ 1: ์ปฌ๋Ÿผ์ด ๋„ˆ๋ฌด ๋งŽ์€ ํ…Œ์ด๋ธ” ๋ณต์ œ ์‹œ ๋А๋ฆผ + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + +- ํŽ˜์ด์ง€๋„ค์ด์…˜ ์—†์ด ์ „์ฒด ์ปฌ๋Ÿผ ํ•œ ๋ฒˆ์— ๋กœ๋“œ (`size=9999`) +- ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ํ‘œ์‹œ +- ํ•„์š”์‹œ API์—์„œ ์บ์‹ฑ ์ ์šฉ + +### ์ด์Šˆ 2: ๋ณต์ œ ํ›„ ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ ์žŠ์Œ + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + +- ํ…Œ์ด๋ธ”๋ช… ํ•„๋“œ๋ฅผ ๋น„์›Œ๋‘๊ณ  ํฌ์ปค์Šค ์ž๋™ ์ด๋™ +- placeholder์— ์˜ˆ์‹œ ์ œ๊ณต (์˜ˆ: `contracts_copy`) + +### ์ด์Šˆ 3: ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ์‹ค์ˆ˜๋กœ ๋ณต์ œ + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + +- ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ ์–‘์ชฝ์—์„œ ๊ฒ€์ฆ +- SYSTEM_TABLES ๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ + +### ์ด์Šˆ 4: ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ (Foreign Key) ๋ณต์‚ฌ ์—ฌ๋ถ€ + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + +- ํ˜„์žฌ๋Š” ์ปฌ๋Ÿผ ๊ตฌ์กฐ๋งŒ ๋ณต์‚ฌ +- Foreign Key๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ์„ค์ • +- ํ–ฅํ›„ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์œผ๋กœ ์ œ์•ฝ์กฐ๊ฑด ๋ณต์‚ฌ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ + +--- + +## ๐Ÿ”ฎ ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ (Optional) + +### 1. ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ๋ณต์ œ + +- ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ํ•œ ๋ฒˆ์— ๋ณต์ œ +- ์ ‘๋‘์‚ฌ ๋˜๋Š” ์ ‘๋ฏธ์‚ฌ ์ž๋™ ์ถ”๊ฐ€ (์˜ˆ: `_copy`) + +### 2. ๋ฐ์ดํ„ฐ ํฌํ•จ ๋ณต์ œ + +- ํ…Œ์ด๋ธ” ๊ตฌ์กฐ + ๋ฐ์ดํ„ฐ๋„ ํ•จ๊ป˜ ๋ณต์ œ +- `INSERT INTO ... SELECT` ๋ฐฉ์‹ + +### 3. ์ œ์•ฝ์กฐ๊ฑด ๋ณต์‚ฌ + +- Primary Key, Foreign Key, Unique, Check ๋“ฑ +- ์ธ๋ฑ์Šค๋„ ํ•จ๊ป˜ ๋ณต์‚ฌ + +### 4. ๋ณต์ œ ํ…œํ”Œ๋ฆฟ + +- ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์ €์žฅ +- ๋น ๋ฅธ ๋ณต์ œ ๊ธฐ๋Šฅ + +### 5. ๋ณต์ œ ์ด๋ ฅ ๊ด€๋ฆฌ + +- ์–ด๋–ค ํ…Œ์ด๋ธ”์ด ์–ด๋””์„œ ๋ณต์ œ๋˜์—ˆ๋Š”์ง€ ์ถ”์  +- DDL ๋กœ๊ทธ์— ๋ณต์ œ ์ •๋ณด ๊ธฐ๋ก + +--- + +## ๐Ÿ“ž ๋ฌธ์˜ ๋ฐ ์ง€์› + +- ๊ธฐ์ˆ  ๋ฌธ์˜: ๊ฐœ๋ฐœํŒ€ +- ์‚ฌ์šฉ ๋ฐฉ๋ฒ•: ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ ์ฐธ์กฐ +- ๋ฒ„๊ทธ ์ œ๋ณด: ์ด์Šˆ ํŠธ๋ž˜์ปค + +--- + +**์ž‘์„ฑ์ผ**: 2025-10-31 +**์ž‘์„ฑ์ž**: AI Assistant +**๋ฒ„์ „**: 1.0 +**์ƒํƒœ**: โœ… ๊ตฌํ˜„ ์™„๋ฃŒ + +--- + +## ๐Ÿ› ๋ฒ„๊ทธ ์ˆ˜์ • ๋‚ด์—ญ + +### API ์‘๋‹ต ๊ตฌ์กฐ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ + +**๋ฌธ์ œ**: ๋ชจ๋‹ฌ์ด ๋œจ์ž๋งˆ์ž ๋ฐ”๋กœ ๋‹ซํžˆ๋ฉด์„œ "ํ…Œ์ด๋ธ” ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" ํ† ์ŠคํŠธ ํ‘œ์‹œ + +**์›์ธ**: + +- ๋ฐฑ์—”๋“œ API๋Š” `{ columns: [], total, page, size }` ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ +- ํ”„๋ก ํŠธ์—”๋“œ๋Š” `data`๋ฅผ ์ง์ ‘ ๋ฐฐ์—ด๋กœ ์ทจ๊ธ‰ +- ํƒ€์ž… ์ •์˜: `ColumnListResponse extends ApiResponse` (์ž˜๋ชป๋จ) + +**ํ•ด๊ฒฐ**: + +1. **API ํƒ€์ž… ์ˆ˜์ •** (`frontend/lib/api/tableManagement.ts`) + + ```typescript + // ์ถ”๊ฐ€๋œ ํƒ€์ž… + export interface ColumnListData { + columns: ColumnTypeInfo[]; + total: number; + page: number; + size: number; + totalPages: number; + } + + // ์ˆ˜์ •๋œ ํƒ€์ž… + export interface ColumnListResponse extends ApiResponse {} + ``` + +2. **CreateTableModal ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์ˆ˜์ •** + + ```typescript + // Before (์ž˜๋ชป๋จ) + if ( + columnsResponse.success && + columnsResponse.data && + columnsResponse.data.length > 0 + ) { + const firstColumn = columnsResponse.data[0]; + } + + // After (์˜ฌ๋ฐ”๋ฆ„) + if (columnsResponse.success && columnsResponse.data) { + const columnsList = columnsResponse.data.columns; + if (columnsList && columnsList.length > 0) { + const firstColumn = columnsList[0]; + } + } + ``` + +3. **๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ถ”๊ฐ€** + - API ์‘๋‹ต ์ „์ฒด ๋กœ๊ทธ + - ์ปฌ๋Ÿผ ๋ฆฌ์ŠคํŠธ ์ถ”์ถœ ํ›„ ๋กœ๊ทธ + - ์—๋Ÿฌ ์ƒํ™ฉ๋ณ„ ์ƒ์„ธ ๋กœ๊ทธ + +**๊ฒฐ๊ณผ**: + +- โœ… ๋ณต์ œ ๋ชจ๋“œ์—์„œ ํ…Œ์ด๋ธ” ์ •๋ณด ์ •์ƒ ๋กœ๋“œ +- โœ… ํƒ€์ž… ์•ˆ์ „์„ฑ ํ–ฅ์ƒ +- โœ… ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐœ์„