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/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/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 3e0f1a61..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,7 +183,7 @@ export const ScreenModal: React.FC = ({ className }) => { }); setScreenData(null); setFormData({}); - setSelectedData([]); // ๐Ÿ†• ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + setOriginalData(null); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); // localStorage์— ์ €์žฅ console.log("๐Ÿ”„ ์—ฐ์† ๋ชจ๋“œ ์ดˆ๊ธฐํ™”: false"); @@ -365,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("๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); @@ -619,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={() => { // ๋ถ€๋ชจ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์†ก @@ -637,8 +652,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/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 = ({ 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/api/dynamicForm.ts b/frontend/lib/api/dynamicForm.ts index 455ab5eb..a66d8f99 100644 --- a/frontend/lib/api/dynamicForm.ts +++ b/frontend/lib/api/dynamicForm.ts @@ -124,7 +124,7 @@ export class DynamicFormApi { * @returns ์—…๋ฐ์ดํŠธ ๊ฒฐ๊ณผ */ static async updateFormDataPartial( - id: number, + id: string | number, // ๐Ÿ”ง UUID ๋ฌธ์ž์—ด๋„ ์ง€์› originalData: Record, 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/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index a4dbd157..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) => ({ @@ -244,11 +249,54 @@ 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); + + // ๐Ÿ†• 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); @@ -268,7 +316,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,14 +368,69 @@ 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]); + + // ๐Ÿ†• 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..841e6f0a 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 ํ•„๋“œ ์‚ฌ์šฉ @@ -2334,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))", }} @@ -2706,7 +2725,7 @@ export const TableListComponent: React.FC = ({ }) ) : ( // ์ผ๋ฐ˜ ๋ Œ๋”๋ง (๊ทธ๋ฃน ์—†์Œ) - data.map((row, index) => ( + filteredData.map((row, index) => ( 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({ diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index cf53a490..ad441754 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -131,37 +131,37 @@ export interface ButtonActionConfig { // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๊ด€๋ จ (transferData ์•ก์…˜์šฉ) dataTransfer?: { // ์†Œ์Šค ์„ค์ • - sourceComponentId: string; // ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ปดํฌ๋„ŒํŠธ ID (ํ…Œ์ด๋ธ” ๋“ฑ) - sourceComponentType?: string; // ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… - + sourceComponentId: string; // ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ปดํฌ๋„ŒํŠธ ID (ํ…Œ์ด๋ธ” ๋“ฑ) + sourceComponentType?: string; // ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… + // ํƒ€๊ฒŸ ์„ค์ • - targetType: "component" | "screen"; // ํƒ€๊ฒŸ ํƒ€์ž… (๊ฐ™์€ ํ™”๋ฉด์˜ ์ปดํฌ๋„ŒํŠธ or ๋‹ค๋ฅธ ํ™”๋ฉด) - + targetType: "component" | "screen"; // ํƒ€๊ฒŸ ํƒ€์ž… (๊ฐ™์€ ํ™”๋ฉด์˜ ์ปดํฌ๋„ŒํŠธ or ๋‹ค๋ฅธ ํ™”๋ฉด) + // ํƒ€๊ฒŸ์ด ์ปดํฌ๋„ŒํŠธ์ธ ๊ฒฝ์šฐ - targetComponentId?: string; // ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ ID - + targetComponentId?: string; // ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ ID + // ํƒ€๊ฒŸ์ด ํ™”๋ฉด์ธ ๊ฒฝ์šฐ - targetScreenId?: number; // ํƒ€๊ฒŸ ํ™”๋ฉด ID - + targetScreenId?: number; // ํƒ€๊ฒŸ ํ™”๋ฉด ID + // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ๊ทœ์น™ mappingRules: Array<{ - sourceField: string; // ์†Œ์Šค ํ•„๋“œ๋ช… - targetField: string; // ํƒ€๊ฒŸ ํ•„๋“œ๋ช… + sourceField: string; // ์†Œ์Šค ํ•„๋“œ๋ช… + targetField: string; // ํƒ€๊ฒŸ ํ•„๋“œ๋ช… transform?: "sum" | "average" | "concat" | "first" | "last" | "count"; // ๋ณ€ํ™˜ ํ•จ์ˆ˜ - defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ + defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ }>; - + // ์ „๋‹ฌ ์˜ต์…˜ mode?: "append" | "replace" | "merge"; // ์ˆ˜์‹  ๋ชจ๋“œ (๊ธฐ๋ณธ: append) - clearAfterTransfer?: boolean; // ์ „๋‹ฌ ํ›„ ์†Œ์Šค ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” - confirmBeforeTransfer?: boolean; // ์ „๋‹ฌ ์ „ ํ™•์ธ ๋ฉ”์‹œ์ง€ - confirmMessage?: string; // ํ™•์ธ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ - + clearAfterTransfer?: boolean; // ์ „๋‹ฌ ํ›„ ์†Œ์Šค ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + confirmBeforeTransfer?: boolean; // ์ „๋‹ฌ ์ „ ํ™•์ธ ๋ฉ”์‹œ์ง€ + confirmMessage?: string; // ํ™•์ธ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ + // ๊ฒ€์ฆ validation?: { - requireSelection?: boolean; // ์„ ํƒ ํ•„์ˆ˜ (๊ธฐ๋ณธ: true) - minSelection?: number; // ์ตœ์†Œ ์„ ํƒ ๊ฐœ์ˆ˜ - maxSelection?: number; // ์ตœ๋Œ€ ์„ ํƒ ๊ฐœ์ˆ˜ + requireSelection?: boolean; // ์„ ํƒ ํ•„์ˆ˜ (๊ธฐ๋ณธ: true) + minSelection?: number; // ์ตœ์†Œ ์„ ํƒ ๊ฐœ์ˆ˜ + maxSelection?: number; // ์ตœ๋Œ€ ์„ ํƒ ๊ฐœ์ˆ˜ }; }; } @@ -190,7 +190,7 @@ export interface ButtonActionContext { // ํ”Œ๋กœ์šฐ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ •๋ณด (ํ”Œ๋กœ์šฐ ์œ„์ ฏ ์„ ํƒ ์•ก์…˜์šฉ) flowSelectedData?: any[]; flowSelectedStepId?: number | null; - + // ๐Ÿ†• ๊ฐ™์€ ํ™”๋ฉด์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ (TableList ์ž๋™ ๊ฐ์ง€์šฉ) allComponents?: any[]; @@ -268,6 +268,9 @@ export class ButtonActionExecutor { case "code_merge": return await this.handleCodeMerge(config, context); + case "transferData": + return await this.handleTransferData(config, context); + case "geolocation": return await this.handleGeolocation(config, context); @@ -292,7 +295,7 @@ export class ButtonActionExecutor { const { formData, originalData, tableName, screenId, onSave } = context; console.log("๐Ÿ’พ [handleSave] ์ €์žฅ ์‹œ์ž‘:", { formData, tableName, screenId, hasOnSave: !!onSave }); - + // ๐Ÿ†• EditModal ๋“ฑ์—์„œ ์ „๋‹ฌ๋œ onSave ์ฝœ๋ฐฑ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ if (onSave) { console.log("โœ… [handleSave] onSave ์ฝœ๋ฐฑ ๋ฐœ๊ฒฌ - ์ฝœ๋ฐฑ ์‹คํ–‰"); @@ -304,20 +307,22 @@ export class ButtonActionExecutor { throw error; } } - + console.log("โš ๏ธ [handleSave] onSave ์ฝœ๋ฐฑ ์—†์Œ - ๊ธฐ๋ณธ ์ €์žฅ ๋กœ์ง ์‹คํ–‰"); // ๐Ÿ†• ์ €์žฅ ์ „ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (SelectedItemsDetailInput ๋“ฑ์—์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘) // context.formData๋ฅผ ์ด๋ฒคํŠธ detail์— ํฌํ•จํ•˜์—ฌ ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ - window.dispatchEvent(new CustomEvent("beforeFormSave", { - detail: { - formData: context.formData - } - })); - + window.dispatchEvent( + new CustomEvent("beforeFormSave", { + detail: { + formData: context.formData, + }, + }), + ); + // ์•ฝ๊ฐ„์˜ ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ฃผ์–ด ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ formData๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ - await new Promise(resolve => setTimeout(resolve, 100)); - + await new Promise((resolve) => setTimeout(resolve, 100)); + console.log("๐Ÿ“ฆ [handleSave] beforeFormSave ์ด๋ฒคํŠธ ํ›„ formData:", context.formData); // ๐Ÿ†• SelectedItemsDetailInput ๋ฐฐ์น˜ ์ €์žฅ ์ฒ˜๋ฆฌ (fieldGroups ๊ตฌ์กฐ) @@ -328,33 +333,41 @@ export class ButtonActionExecutor { key, isArray: Array.isArray(value), length: Array.isArray(value) ? value.length : 0, - firstItem: Array.isArray(value) && value.length > 0 ? { - hasOriginalData: !!value[0]?.originalData, - hasFieldGroups: !!value[0]?.fieldGroups, - keys: Object.keys(value[0] || {}) - } : null - })) + firstItem: + Array.isArray(value) && value.length > 0 + ? { + hasOriginalData: !!value[0]?.originalData, + hasFieldGroups: !!value[0]?.fieldGroups, + keys: Object.keys(value[0] || {}), + } + : null, + })), }); // ๐Ÿ”ง formData ์ž์ฒด๊ฐ€ ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ (ScreenModal์˜ ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์ˆ˜์ •) if (Array.isArray(context.formData)) { - console.log("โš ๏ธ [handleSave] formData๊ฐ€ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค - SelectedItemsDetailInput์ด ์ด๋ฏธ ์ฒ˜๋ฆฌํ–ˆ์œผ๋ฏ€๋กœ ์ผ๋ฐ˜ ์ €์žฅ ๊ฑด๋„ˆ๋œ€"); + console.log( + "โš ๏ธ [handleSave] formData๊ฐ€ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค - SelectedItemsDetailInput์ด ์ด๋ฏธ ์ฒ˜๋ฆฌํ–ˆ์œผ๋ฏ€๋กœ ์ผ๋ฐ˜ ์ €์žฅ ๊ฑด๋„ˆ๋œ€", + ); console.log("โš ๏ธ [handleSave] formData ๋ฐฐ์—ด:", context.formData); // โœ… SelectedItemsDetailInput์ด ์ด๋ฏธ UPSERT๋ฅผ ์‹คํ–‰ํ–ˆ์œผ๋ฏ€๋กœ ์ผ๋ฐ˜ ์ €์žฅ์„ ๊ฑด๋„ˆ๋œ€ return true; // ์„ฑ๊ณต์œผ๋กœ ๋ฐ˜ํ™˜ } - const selectedItemsKeys = Object.keys(context.formData).filter(key => { + const selectedItemsKeys = Object.keys(context.formData).filter((key) => { const value = context.formData[key]; console.log(`๐Ÿ” [handleSave] ํ•„ํ„ฐ๋ง ์ฒดํฌ - ${key}:`, { isArray: Array.isArray(value), length: Array.isArray(value) ? value.length : 0, - firstItem: Array.isArray(value) && value.length > 0 ? { - keys: Object.keys(value[0] || {}), - hasOriginalData: !!value[0]?.originalData, - hasFieldGroups: !!value[0]?.fieldGroups, - actualValue: value[0], - } : null + firstItem: + Array.isArray(value) && value.length > 0 + ? { + keys: Object.keys(value[0] || {}), + hasOriginalData: !!value[0]?.originalData, + hasFieldGroups: !!value[0]?.fieldGroups, + actualValue: value[0], + } + : null, }); return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups; }); @@ -401,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; @@ -452,9 +476,9 @@ export class ButtonActionExecutor { // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) // console.log("๐Ÿ” ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘"); // console.log("๐Ÿ“ฆ ํ˜„์žฌ formData:", JSON.stringify(formData, null, 2)); - + const fieldsWithNumbering: Record = {}; - + // formData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ ์ฐพ๊ธฐ for (const [key, value] of Object.entries(formData)) { if (key.endsWith("_numberingRuleId") && value) { @@ -474,7 +498,7 @@ export class ButtonActionExecutor { console.log("โ„น๏ธ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ•„๋“œ ๊ฐ์ง€:", Object.keys(fieldsWithNumbering)); console.log("โ„น๏ธ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’ ์œ ์ง€ (์žฌํ• ๋‹น ํ•˜์ง€ ์•Š์Œ)"); } - + // console.log("โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); // console.log("๐Ÿ“ฆ ์ตœ์ข… formData:", JSON.stringify(formData, null, 2)); @@ -496,7 +520,7 @@ export class ButtonActionExecutor { // ๐Ÿ†• ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน์—์„œ ์‚ญ์ œ๋œ ํ•ญ๋ชฉ ์ฒ˜๋ฆฌ // formData์˜ ๊ฐ ํ•„๋“œ์—์„œ _deletedItemIds๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ console.log("๐Ÿ” [handleSave] ์‚ญ์ œ ํ•ญ๋ชฉ ๊ฒ€์ƒ‰ ์‹œ์ž‘ - dataWithUserInfo ํ‚ค:", Object.keys(dataWithUserInfo)); - + for (const [key, value] of Object.entries(dataWithUserInfo)) { console.log(`๐Ÿ” [handleSave] ํ•„๋“œ ๊ฒ€์‚ฌ: ${key}`, { type: typeof value, @@ -504,9 +528,9 @@ export class ButtonActionExecutor { isString: typeof value === "string", valuePreview: typeof value === "string" ? value.substring(0, 100) : value, }); - + let parsedValue = value; - + // JSON ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ ํŒŒ์‹ฑ ์‹œ๋„ if (typeof value === "string" && value.startsWith("[")) { try { @@ -516,25 +540,25 @@ export class ButtonActionExecutor { // ํŒŒ์‹ฑ ์‹คํŒจํ•˜๋ฉด ์›๋ณธ ๊ฐ’ ์œ ์ง€ } } - + if (Array.isArray(parsedValue) && parsedValue.length > 0) { const firstItem = parsedValue[0]; const deletedItemIds = firstItem?._deletedItemIds; const targetTable = firstItem?._targetTable; - + console.log(`๐Ÿ” [handleSave] ๋ฐฐ์—ด ํ•„๋“œ ๋ถ„์„: ${key}`, { firstItemKeys: firstItem ? Object.keys(firstItem) : [], deletedItemIds, targetTable, }); - + if (deletedItemIds && deletedItemIds.length > 0 && targetTable) { console.log("๐Ÿ—‘๏ธ [handleSave] ์‚ญ์ œํ•  ํ•ญ๋ชฉ ๋ฐœ๊ฒฌ:", { fieldKey: key, targetTable, deletedItemIds, }); - + // ์‚ญ์ œ API ํ˜ธ์ถœ for (const itemId of deletedItemIds) { try { @@ -552,7 +576,7 @@ export class ButtonActionExecutor { } } } - + saveResult = await DynamicFormApi.saveFormData({ screenId, tableName, @@ -665,12 +689,12 @@ export class ButtonActionExecutor { * ItemData[] โ†’ ๊ฐ ํ’ˆ๋ชฉ์˜ details ๋ฐฐ์—ด์„ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ๋กœ ์ €์žฅ */ private static async handleBatchSave( - config: ButtonActionConfig, + config: ButtonActionConfig, context: ButtonActionContext, - selectedItemsKeys: string[] + selectedItemsKeys: string[], ): Promise { const { formData, tableName, screenId, selectedRowsData, originalData } = context; - + console.log(`๐Ÿ” [handleBatchSave] context ํ™•์ธ:`, { hasSelectedRowsData: !!selectedRowsData, selectedRowsCount: selectedRowsData?.length || 0, @@ -691,39 +715,38 @@ export class ButtonActionExecutor { // ๐Ÿ†• ๋ถ€๋ชจ ํ™”๋ฉด ๋ฐ์ดํ„ฐ ์ค€๋น„ (parentDataMapping์šฉ) // selectedRowsData ๋˜๋Š” originalData๋ฅผ parentData๋กœ ์‚ฌ์šฉ const parentData = selectedRowsData?.[0] || originalData || {}; - + // ๐Ÿ†• modalDataStore์—์„œ ๋ˆ„์ ๋œ ๋ชจ๋“  ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ // (์—ฌ๋Ÿฌ ๋‹จ๊ณ„ ๋ชจ๋‹ฌ์—์„œ ์ „๋‹ฌ๋œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์šฉ) let modalDataStoreRegistry: Record = {}; - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { try { // Zustand store์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ - const { useModalDataStore } = await import('@/stores/modalDataStore'); + const { useModalDataStore } = await import("@/stores/modalDataStore"); modalDataStoreRegistry = useModalDataStore.getState().dataRegistry; } catch (error) { console.warn("โš ๏ธ modalDataStore ๋กœ๋“œ ์‹คํŒจ:", error); } } - + // ๊ฐ ํ…Œ์ด๋ธ”์˜ ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์„ modalDataStore๋กœ ๋ณ€ํ™˜ const modalDataStore: Record = {}; Object.entries(modalDataStoreRegistry).forEach(([key, items]) => { if (Array.isArray(items) && items.length > 0) { // ModalDataItem[] โ†’ originalData ์ถ”์ถœ - modalDataStore[key] = items.map(item => item.originalData || item); + modalDataStore[key] = items.map((item) => item.originalData || item); } }); - // ๊ฐ SelectedItemsDetailInput ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ for (const key of selectedItemsKeys) { // ๐Ÿ†• ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ: ItemData[] with fieldGroups - const items = formData[key] as Array<{ - id: string; - originalData: any; - fieldGroups: Record>; + const items = formData[key] as Array<{ + id: string; + originalData: any; + fieldGroups: Record>; }>; - + // ๐Ÿ†• ์ด ์ปดํฌ๋„ŒํŠธ์˜ parentDataMapping ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ const componentConfig = context.componentConfigs?.[key]; const parentDataMapping = componentConfig?.parentDataMapping || []; @@ -731,44 +754,42 @@ export class ButtonActionExecutor { // ๐Ÿ†• ๊ฐ ํ’ˆ๋ชฉ์˜ ๊ทธ๋ฃน ๊ฐ„ ์กฐํ•ฉ(์นดํ‹ฐ์…˜ ๊ณฑ) ์ƒ์„ฑ for (const item of items) { const groupKeys = Object.keys(item.fieldGroups); - + // ๊ฐ ๊ทธ๋ฃน์˜ ํ•ญ๋ชฉ ๋ฐฐ์—ด ๊ฐ€์ ธ์˜ค๊ธฐ - const groupArrays = groupKeys.map(groupKey => ({ + const groupArrays = groupKeys.map((groupKey) => ({ groupKey, - entries: item.fieldGroups[groupKey] || [] + entries: item.fieldGroups[groupKey] || [], })); - + // ์นดํ‹ฐ์…˜ ๊ณฑ ๊ณ„์‚ฐ ํ•จ์ˆ˜ const cartesianProduct = (arrays: any[][]): any[][] => { if (arrays.length === 0) return [[]]; - if (arrays.length === 1) return arrays[0].map(item => [item]); - + if (arrays.length === 1) return arrays[0].map((item) => [item]); + const [first, ...rest] = arrays; const restProduct = cartesianProduct(rest); - - return first.flatMap(item => - restProduct.map(combination => [item, ...combination]) - ); + + return first.flatMap((item) => restProduct.map((combination) => [item, ...combination])); }; - + // ๋ชจ๋“  ๊ทธ๋ฃน์˜ ์นดํ‹ฐ์…˜ ๊ณฑ ์ƒ์„ฑ - const entryArrays = groupArrays.map(g => g.entries); + const entryArrays = groupArrays.map((g) => g.entries); const combinations = cartesianProduct(entryArrays); - + // ๊ฐ ์กฐํ•ฉ์„ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ๋กœ ์ €์žฅ for (let i = 0; i < combinations.length; i++) { const combination = combinations[i]; try { // ๐Ÿ†• ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ ์šฉ const mappedData: any = {}; - + // 1. parentDataMapping ์„ค์ •์ด ์žˆ์œผ๋ฉด ์ ์šฉ if (parentDataMapping.length > 0) { for (const mapping of parentDataMapping) { let sourceData: any; const sourceTableName = mapping.sourceTable; const selectedItemTable = componentConfig?.sourceTable; - + if (sourceTableName === selectedItemTable) { sourceData = item.originalData; } else { @@ -779,9 +800,9 @@ export class ButtonActionExecutor { sourceData = parentData; } } - + const sourceValue = sourceData[mapping.sourceField]; - + if (sourceValue !== undefined && sourceValue !== null) { mappedData[mapping.targetField] = sourceValue; } else if (mapping.defaultValue !== undefined) { @@ -793,12 +814,12 @@ export class ButtonActionExecutor { if (item.originalData.id) { mappedData.item_id = item.originalData.id; } - + if (parentData.id || parentData.customer_id) { mappedData.customer_id = parentData.customer_id || parentData.id; } } - + // ๊ณตํ†ต ํ•„๋“œ ๋ณต์‚ฌ (company_code, currency_code ๋“ฑ) if (item.originalData.company_code && !mappedData.company_code) { mappedData.company_code = item.originalData.company_code; @@ -806,10 +827,10 @@ export class ButtonActionExecutor { if (item.originalData.currency_code && !mappedData.currency_code) { mappedData.currency_code = item.originalData.currency_code; } - + // ์›๋ณธ ๋ฐ์ดํ„ฐ๋กœ ์‹œ์ž‘ (๋งคํ•‘๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ) let mergedData = { ...mappedData }; - + // ๊ฐ ๊ทธ๋ฃน์˜ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ๋ณ‘ํ•ฉ for (let j = 0; j < combination.length; j++) { const entry = combination[j]; @@ -1137,13 +1158,13 @@ export class ButtonActionExecutor { // ๐Ÿ†• 1. ํ˜„์žฌ ํ™”๋ฉด์˜ TableList ๋˜๋Š” SplitPanelLayout ์ž๋™ ๊ฐ์ง€ let dataSourceId = config.dataSourceId; - + if (!dataSourceId && context.allComponents) { // TableList ์šฐ์„  ๊ฐ์ง€ const tableListComponent = context.allComponents.find( - (comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName + (comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName, ); - + if (tableListComponent) { dataSourceId = tableListComponent.componentConfig.tableName; console.log("โœจ TableList ์ž๋™ ๊ฐ์ง€:", { @@ -1153,9 +1174,9 @@ export class ButtonActionExecutor { } else { // TableList๊ฐ€ ์—†์œผ๋ฉด SplitPanelLayout์˜ ์ขŒ์ธก ํŒจ๋„ ๊ฐ์ง€ const splitPanelComponent = context.allComponents.find( - (comp: any) => comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName + (comp: any) => comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName, ); - + if (splitPanelComponent) { dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName; console.log("โœจ ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ํ…Œ์ด๋ธ” ์ž๋™ ๊ฐ์ง€:", { @@ -1165,7 +1186,7 @@ export class ButtonActionExecutor { } } } - + // ์—ฌ์ „ํžˆ ์—†์œผ๋ฉด context.tableName ๋˜๋Š” "default" ์‚ฌ์šฉ if (!dataSourceId) { dataSourceId = context.tableName || "default"; @@ -1175,7 +1196,7 @@ export class ButtonActionExecutor { try { const { useModalDataStore } = await import("@/stores/modalDataStore"); const dataRegistry = useModalDataStore.getState().dataRegistry; - + const modalData = dataRegistry[dataSourceId] || []; console.log("๐Ÿ“Š ํ˜„์žฌ ํ™”๋ฉด ๋ฐ์ดํ„ฐ ํ™•์ธ:", { @@ -1205,13 +1226,13 @@ export class ButtonActionExecutor { // 6. ๋™์  ๋ชจ๋‹ฌ ์ œ๋ชฉ ์ƒ์„ฑ const { useModalDataStore } = await import("@/stores/modalDataStore"); const dataRegistry = useModalDataStore.getState().dataRegistry; - + let finalTitle = "๋ฐ์ดํ„ฐ ์ž…๋ ฅ"; - + // ๐Ÿ†• ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ (์šฐ์„ ์ˆœ์œ„ 1) if (config.modalTitleBlocks && config.modalTitleBlocks.length > 0) { const titleParts: string[] = []; - + config.modalTitleBlocks.forEach((block) => { if (block.type === "text") { // ํ…์ŠคํŠธ ๋ธ”๋ก: ๊ทธ๋Œ€๋กœ ์ถ”๊ฐ€ @@ -1220,13 +1241,13 @@ export class ButtonActionExecutor { // ํ•„๋“œ ๋ธ”๋ก: ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ const tableName = block.tableName; const columnName = block.value; - + if (tableName && columnName) { const tableData = dataRegistry[tableName]; if (tableData && tableData.length > 0) { const firstItem = tableData[0].originalData || tableData[0]; const value = firstItem[columnName]; - + if (value !== undefined && value !== null) { titleParts.push(String(value)); console.log(`โœจ ๋™์  ํ•„๋“œ: ${tableName}.${columnName} โ†’ ${value}`); @@ -1241,28 +1262,28 @@ export class ButtonActionExecutor { } } }); - + finalTitle = titleParts.join(""); console.log("๐Ÿ“‹ ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ ์ƒ์„ฑ:", finalTitle); } // ๊ธฐ์กด ๋ฐฉ์‹: {tableName.columnName} ํŒจํ„ด (์šฐ์„ ์ˆœ์œ„ 2) else if (config.modalTitle) { finalTitle = config.modalTitle; - + if (finalTitle.includes("{")) { const matches = finalTitle.match(/\{([^}]+)\}/g); - + if (matches) { matches.forEach((match) => { const path = match.slice(1, -1); // {item_info.item_name} โ†’ item_info.item_name const [tableName, columnName] = path.split("."); - + if (tableName && columnName) { const tableData = dataRegistry[tableName]; if (tableData && tableData.length > 0) { const firstItem = tableData[0].originalData || tableData[0]; const value = firstItem[columnName]; - + if (value !== undefined && value !== null) { finalTitle = finalTitle.replace(match, String(value)); console.log(`โœจ ๋™์  ์ œ๋ชฉ: ${match} โ†’ ${value}`); @@ -1273,7 +1294,7 @@ export class ButtonActionExecutor { } } } - + // 7. ๋ชจ๋‹ฌ ์—ด๊ธฐ + URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ dataSourceId ์ „๋‹ฌ if (config.targetScreenId) { // config์— modalDescription์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ @@ -1301,10 +1322,10 @@ export class ButtonActionExecutor { }); window.dispatchEvent(modalEvent); - + // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ (๊ฐ„๋‹จํ•˜๊ฒŒ) toast.success(config.successMessage || "๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค."); - + return true; } else { console.error("๋ชจ๋‹ฌ๋กœ ์—ด ํ™”๋ฉด์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); @@ -1507,15 +1528,15 @@ export class ButtonActionExecutor { const layoutData = await screenApi.getLayout(config.targetScreenId); if (layoutData?.components) { hasSplitPanel = layoutData.components.some( - (comp: any) => - comp.type === "screen-split-panel" || + (comp: any) => + comp.type === "screen-split-panel" || comp.componentType === "screen-split-panel" || - comp.type === "split-panel-layout" || - comp.componentType === "split-panel-layout" + comp.type === "split-panel-layout" || + comp.componentType === "split-panel-layout", ); } - console.log("๐Ÿ” [openEditModal] ๋ถ„ํ•  ํŒจ๋„ ํ™•์ธ:", { - targetScreenId: config.targetScreenId, + console.log("๐Ÿ” [openEditModal] ๋ถ„ํ•  ํŒจ๋„ ํ™•์ธ:", { + targetScreenId: config.targetScreenId, hasSplitPanel, componentTypes: layoutData?.components?.map((c: any) => c.type || c.componentType) || [], }); @@ -1666,7 +1687,8 @@ export class ButtonActionExecutor { if (copiedData[field] !== undefined) { const originalValue = copiedData[field]; const ruleIdKey = `${field}_numberingRuleId`; - const hasNumberingRule = rowData[ruleIdKey] !== undefined && rowData[ruleIdKey] !== null && rowData[ruleIdKey] !== ""; + const hasNumberingRule = + rowData[ruleIdKey] !== undefined && rowData[ruleIdKey] !== null && rowData[ruleIdKey] !== ""; // ํ’ˆ๋ชฉ์ฝ”๋“œ๋ฅผ ๋ฌด์กฐ๊ฑด ๊ณต๋ฐฑ์œผ๋กœ ์ดˆ๊ธฐํ™” copiedData[field] = ""; @@ -1855,7 +1877,7 @@ export class ButtonActionExecutor { // flowConfig๊ฐ€ ์žˆ์œผ๋ฉด controlMode๊ฐ€ ๋ช…์‹œ๋˜์ง€ ์•Š์•„๋„ ํ”Œ๋กœ์šฐ ๋ชจ๋“œ๋กœ ๊ฐ„์ฃผ const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId; const isFlowMode = config.dataflowConfig?.controlMode === "flow" || hasFlowConfig; - + if (isFlowMode && config.dataflowConfig?.flowConfig) { console.log("๐ŸŽฏ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰:", config.dataflowConfig.flowConfig); @@ -2638,14 +2660,14 @@ export class ButtonActionExecutor { if (context.tableName) { const { tableDisplayStore } = await import("@/stores/tableDisplayStore"); const storedData = tableDisplayStore.getTableData(context.tableName); - + // ํ•„ํ„ฐ ์กฐ๊ฑด์€ ์ €์žฅ์†Œ ๋˜๋Š” context์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ const filterConditions = storedData?.filterConditions || context.filterConditions; const searchTerm = storedData?.searchTerm || context.searchTerm; try { const { entityJoinApi } = await import("@/lib/api/entityJoin"); - + const apiParams = { page: 1, size: 10000, // ์ตœ๋Œ€ 10,000๊ฐœ @@ -2655,7 +2677,7 @@ export class ButtonActionExecutor { enableEntityJoin: true, // โœ… Entity ์กฐ์ธ // autoFilter๋Š” entityJoinApi.getTableDataWithJoins ๋‚ด๋ถ€์—์„œ ์ž๋™์œผ๋กœ ์ ์šฉ๋จ }; - + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ค€์ˆ˜: autoFilter๋กœ company_code ์ž๋™ ์ ์šฉ const response = await entityJoinApi.getTableDataWithJoins(context.tableName, apiParams); @@ -2671,7 +2693,7 @@ export class ButtonActionExecutor { if (Array.isArray(response)) { // ๋ฐฐ์—ด๋กœ ์ง์ ‘ ๋ฐ˜ํ™˜๋œ ๊ฒฝ์šฐ dataToExport = response; - } else if (response && 'data' in response) { + } else if (response && "data" in response) { // EntityJoinResponse ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ dataToExport = response.data; } else { @@ -2712,7 +2734,7 @@ export class ButtonActionExecutor { // ํŒŒ์ผ๋ช… ์ƒ์„ฑ (๋ฉ”๋‰ด ์ด๋ฆ„ ์šฐ์„  ์‚ฌ์šฉ) let defaultFileName = context.tableName || "๋ฐ์ดํ„ฐ"; - + // localStorage์—์„œ ๋ฉ”๋‰ด ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ if (typeof window !== "undefined") { const menuName = localStorage.getItem("currentMenuName"); @@ -2720,107 +2742,104 @@ export class ButtonActionExecutor { defaultFileName = menuName; } } - + const fileName = config.excelFileName || `${defaultFileName}_${new Date().toISOString().split("T")[0]}.xlsx`; const sheetName = config.excelSheetName || "Sheet1"; const includeHeaders = config.excelIncludeHeaders !== false; - // ๐ŸŽจ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ์ปฌ๋Ÿผ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ - let visibleColumns: string[] | undefined = undefined; - let columnLabels: Record | undefined = undefined; - + // ๐ŸŽจ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ์ปฌ๋Ÿผ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ + let visibleColumns: string[] | undefined = undefined; + let columnLabels: Record | undefined = undefined; + + try { + // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ณ„๋„ API ์‚ฌ์šฉ) + const { apiClient } = await import("@/lib/api/client"); + const layoutResponse = await apiClient.get(`/screen-management/screens/${context.screenId}/layout`); + + if (layoutResponse.data?.success && layoutResponse.data?.data) { + let layoutData = layoutResponse.data.data; + + // components๊ฐ€ ๋ฌธ์ž์—ด์ด๋ฉด ํŒŒ์‹ฑ + if (typeof layoutData.components === "string") { + layoutData.components = JSON.parse(layoutData.components); + } + + // ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ + const findTableListComponent = (components: any[]): any => { + if (!Array.isArray(components)) return null; + + for (const comp of components) { + // componentType์ด 'table-list'์ธ์ง€ ํ™•์ธ + const isTableList = comp.componentType === "table-list"; + + // componentConfig ์•ˆ์—์„œ ํ…Œ์ด๋ธ”๋ช… ํ™•์ธ + const matchesTable = + comp.componentConfig?.selectedTable === context.tableName || + comp.componentConfig?.tableName === context.tableName; + + if (isTableList && matchesTable) { + return comp; + } + if (comp.children && comp.children.length > 0) { + const found = findTableListComponent(comp.children); + if (found) return found; + } + } + return null; + }; + + const tableListComponent = findTableListComponent(layoutData.components || []); + + if (tableListComponent && tableListComponent.componentConfig?.columns) { + const columns = tableListComponent.componentConfig.columns; + + // visible์ด true์ธ ์ปฌ๋Ÿผ๋งŒ ์ถ”์ถœ + visibleColumns = columns.filter((col: any) => col.visible !== false).map((col: any) => col.columnName); + + // ๐ŸŽฏ column_labels ํ…Œ์ด๋ธ”์—์„œ ์‹ค์ œ ๋ผ๋ฒจ ๊ฐ€์ ธ์˜ค๊ธฐ try { - // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ณ„๋„ API ์‚ฌ์šฉ) - const { apiClient } = await import("@/lib/api/client"); - const layoutResponse = await apiClient.get(`/screen-management/screens/${context.screenId}/layout`); - - if (layoutResponse.data?.success && layoutResponse.data?.data) { - let layoutData = layoutResponse.data.data; - - // components๊ฐ€ ๋ฌธ์ž์—ด์ด๋ฉด ํŒŒ์‹ฑ - if (typeof layoutData.components === 'string') { - layoutData.components = JSON.parse(layoutData.components); + const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, { + params: { page: 1, size: 9999 }, + }); + + if (columnsResponse.data?.success && columnsResponse.data?.data) { + let columnData = columnsResponse.data.data; + + // data๊ฐ€ ๊ฐ์ฒด์ด๊ณ  columns ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”์ถœ + if (columnData.columns && Array.isArray(columnData.columns)) { + columnData = columnData.columns; } - - // ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ - const findTableListComponent = (components: any[]): any => { - if (!Array.isArray(components)) return null; - - for (const comp of components) { - // componentType์ด 'table-list'์ธ์ง€ ํ™•์ธ - const isTableList = comp.componentType === 'table-list'; - - // componentConfig ์•ˆ์—์„œ ํ…Œ์ด๋ธ”๋ช… ํ™•์ธ - const matchesTable = - comp.componentConfig?.selectedTable === context.tableName || - comp.componentConfig?.tableName === context.tableName; - - if (isTableList && matchesTable) { - return comp; + + if (Array.isArray(columnData)) { + columnLabels = {}; + + // API์—์„œ ๊ฐ€์ ธ์˜จ ๋ผ๋ฒจ๋กœ ๋งคํ•‘ + columnData.forEach((colData: any) => { + const colName = colData.column_name || colData.columnName; + // ์šฐ์„ ์ˆœ์œ„: column_label > label > displayName > columnName + const labelValue = colData.column_label || colData.label || colData.displayName || colName; + if (colName && labelValue) { + columnLabels![colName] = labelValue; } - if (comp.children && comp.children.length > 0) { - const found = findTableListComponent(comp.children); - if (found) return found; - } - } - return null; - }; - - const tableListComponent = findTableListComponent(layoutData.components || []); - - if (tableListComponent && tableListComponent.componentConfig?.columns) { - const columns = tableListComponent.componentConfig.columns; - - // visible์ด true์ธ ์ปฌ๋Ÿผ๋งŒ ์ถ”์ถœ - visibleColumns = columns - .filter((col: any) => col.visible !== false) - .map((col: any) => col.columnName); - - // ๐ŸŽฏ column_labels ํ…Œ์ด๋ธ”์—์„œ ์‹ค์ œ ๋ผ๋ฒจ ๊ฐ€์ ธ์˜ค๊ธฐ - try { - const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, { - params: { page: 1, size: 9999 } - }); - - if (columnsResponse.data?.success && columnsResponse.data?.data) { - let columnData = columnsResponse.data.data; - - // data๊ฐ€ ๊ฐ์ฒด์ด๊ณ  columns ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”์ถœ - if (columnData.columns && Array.isArray(columnData.columns)) { - columnData = columnData.columns; - } - - if (Array.isArray(columnData)) { - columnLabels = {}; - - // API์—์„œ ๊ฐ€์ ธ์˜จ ๋ผ๋ฒจ๋กœ ๋งคํ•‘ - columnData.forEach((colData: any) => { - const colName = colData.column_name || colData.columnName; - // ์šฐ์„ ์ˆœ์œ„: column_label > label > displayName > columnName - const labelValue = colData.column_label || colData.label || colData.displayName || colName; - if (colName && labelValue) { - columnLabels![colName] = labelValue; - } - }); - } - } - } catch (error) { - // ์‹คํŒจ ์‹œ ์ปดํฌ๋„ŒํŠธ ์„ค์ •์˜ displayName ์‚ฌ์šฉ - columnLabels = {}; - columns.forEach((col: any) => { - if (col.columnName) { - columnLabels![col.columnName] = col.displayName || col.label || col.columnName; - } - }); - } - } else { - console.warn("โš ๏ธ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + }); } } } catch (error) { - console.error("โŒ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ ์‹คํŒจ:", error); + // ์‹คํŒจ ์‹œ ์ปดํฌ๋„ŒํŠธ ์„ค์ •์˜ displayName ์‚ฌ์šฉ + columnLabels = {}; + columns.forEach((col: any) => { + if (col.columnName) { + columnLabels![col.columnName] = col.displayName || col.label || col.columnName; + } + }); } - + } else { + console.warn("โš ๏ธ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } + } catch (error) { + console.error("โŒ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ ์‹คํŒจ:", error); + } // ๐ŸŽจ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋“ค ์กฐํšŒ (ํ•œ ๋ฒˆ๋งŒ) const categoryMap: Record> = {}; @@ -2830,20 +2849,20 @@ export class ButtonActionExecutor { if (context.tableName) { try { const { getCategoryColumns, getCategoryValues } = await import("@/lib/api/tableCategoryValue"); - + const categoryColumnsResponse = await getCategoryColumns(context.tableName); - + if (categoryColumnsResponse.success && categoryColumnsResponse.data) { // ๋ฐฑ์—”๋“œ์—์„œ ์ •์˜๋œ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ๋“ค - categoryColumns = categoryColumnsResponse.data.map((col: any) => - col.column_name || col.columnName || col.name - ).filter(Boolean); // undefined ์ œ๊ฑฐ - + categoryColumns = categoryColumnsResponse.data + .map((col: any) => col.column_name || col.columnName || col.name) + .filter(Boolean); // undefined ์ œ๊ฑฐ + // ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์˜ ๊ฐ’๋“ค ์กฐํšŒ for (const columnName of categoryColumns) { try { const valuesResponse = await getCategoryValues(context.tableName, columnName, false); - + if (valuesResponse.success && valuesResponse.data) { // valueCode โ†’ valueLabel ๋งคํ•‘ categoryMap[columnName] = {}; @@ -2854,7 +2873,6 @@ export class ButtonActionExecutor { categoryMap[columnName][code] = label; } }); - } } catch (error) { console.error(`โŒ ์นดํ…Œ๊ณ ๋ฆฌ "${columnName}" ์กฐํšŒ ์‹คํŒจ:`, error); @@ -2874,34 +2892,33 @@ export class ButtonActionExecutor { visibleColumns.forEach((columnName: string) => { // __checkbox__ ์ปฌ๋Ÿผ์€ ์ œ์™ธ if (columnName === "__checkbox__") return; - + if (columnName in row) { // ๋ผ๋ฒจ ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ const label = columnLabels?.[columnName] || columnName; - + // ๐ŸŽฏ Entity ์กฐ์ธ๋œ ๊ฐ’ ์šฐ์„  ์‚ฌ์šฉ let value = row[columnName]; - + // writer โ†’ writer_name ์‚ฌ์šฉ - if (columnName === 'writer' && row['writer_name']) { - value = row['writer_name']; + if (columnName === "writer" && row["writer_name"]) { + value = row["writer_name"]; } // ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ๋“ค๋„ _name ์šฐ์„  ์‚ฌ์šฉ else if (row[`${columnName}_name`]) { value = row[`${columnName}_name`]; } // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ํ•„๋“œ๋Š” ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (๋ฐฑ์—”๋“œ์—์„œ ์ •์˜๋œ ์ปฌ๋Ÿผ๋งŒ) - else if (categoryMap[columnName] && typeof value === 'string' && categoryMap[columnName][value]) { + else if (categoryMap[columnName] && typeof value === "string" && categoryMap[columnName][value]) { value = categoryMap[columnName][value]; } - + filteredRow[label] = value; } }); return filteredRow; }); - } // ์ตœ๋Œ€ ํ–‰ ์ˆ˜ ์ œํ•œ @@ -2928,8 +2945,8 @@ export class ButtonActionExecutor { */ private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { - console.log("๐Ÿ“ค ์—‘์…€ ์—…๋กœ๋“œ ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { - config, + console.log("๐Ÿ“ค ์—‘์…€ ์—…๋กœ๋“œ ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { + config, context, userId: context.userId, tableName: context.tableName, @@ -3027,7 +3044,7 @@ export class ButtonActionExecutor { userId: context.userId, onScanSuccess: (barcode: string) => { console.log("โœ… ๋ฐ”์ฝ”๋“œ ์Šค์บ” ์„ฑ๊ณต:", barcode); - + // ๋Œ€์ƒ ํ•„๋“œ์— ๊ฐ’ ์ž…๋ ฅ if (config.barcodeTargetField && context.onFormDataChange) { context.onFormDataChange({ @@ -3037,7 +3054,7 @@ export class ButtonActionExecutor { } toast.success(`๋ฐ”์ฝ”๋“œ ์Šค์บ” ์™„๋ฃŒ: ${barcode}`); - + // ์ž๋™ ์ œ์ถœ ์˜ต์…˜์ด ์ผœ์ ธ์žˆ์œผ๋ฉด ์ €์žฅ if (config.barcodeAutoSubmit) { this.handleSave(config, context); @@ -3164,7 +3181,7 @@ export class ButtonActionExecutor { // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ (์˜ต์…˜) if (config.mergeShowPreview !== false) { const { apiClient } = await import("@/lib/api/client"); - + const previewResponse = await apiClient.post("/code-merge/preview", { columnName, oldValue, @@ -3176,12 +3193,12 @@ export class ButtonActionExecutor { const confirmMerge = confirm( `โš ๏ธ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ํ™•์ธ\n\n` + - `${oldValue} โ†’ ${newValue}\n\n` + - `์˜ํ–ฅ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ:\n` + - `- ํ…Œ์ด๋ธ” ์ˆ˜: ${preview.preview.length}๊ฐœ\n` + - `- ์ด ํ–‰ ์ˆ˜: ${totalRows}๊ฐœ\n\n` + - `๋ฐ์ดํ„ฐ๋Š” ์‚ญ์ œ๋˜์ง€ ์•Š๊ณ , "${columnName}" ์ปฌ๋Ÿผ ๊ฐ’๋งŒ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.\n\n` + - `๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?` + `${oldValue} โ†’ ${newValue}\n\n` + + `์˜ํ–ฅ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ:\n` + + `- ํ…Œ์ด๋ธ” ์ˆ˜: ${preview.preview.length}๊ฐœ\n` + + `- ์ด ํ–‰ ์ˆ˜: ${totalRows}๊ฐœ\n\n` + + `๋ฐ์ดํ„ฐ๋Š” ์‚ญ์ œ๋˜์ง€ ์•Š๊ณ , "${columnName}" ์ปฌ๋Ÿผ ๊ฐ’๋งŒ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.\n\n` + + `๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`, ); if (!confirmMerge) { @@ -3194,7 +3211,7 @@ export class ButtonActionExecutor { toast.loading("์ฝ”๋“œ ๋ณ‘ํ•ฉ ์ค‘...", { duration: Infinity }); const { apiClient } = await import("@/lib/api/client"); - + const response = await apiClient.post("/code-merge/merge-all-tables", { columnName, oldValue, @@ -3206,8 +3223,7 @@ export class ButtonActionExecutor { if (response.data.success) { const data = response.data.data; toast.success( - `์ฝ”๋“œ ๋ณ‘ํ•ฉ ์™„๋ฃŒ!\n` + - `${data.affectedTables.length}๊ฐœ ํ…Œ์ด๋ธ”, ${data.totalRowsUpdated}๊ฐœ ํ–‰ ์—…๋ฐ์ดํŠธ` + `์ฝ”๋“œ ๋ณ‘ํ•ฉ ์™„๋ฃŒ!\n` + `${data.affectedTables.length}๊ฐœ ํ…Œ์ด๋ธ”, ${data.totalRowsUpdated}๊ฐœ ํ–‰ ์—…๋ฐ์ดํŠธ`, ); // ํ™”๋ฉด ์ƒˆ๋กœ๊ณ ์นจ @@ -3227,6 +3243,102 @@ export class ButtonActionExecutor { } } + /** + * ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์•ก์…˜ ์ฒ˜๋ฆฌ (๋ถ„ํ•  ํŒจ๋„์—์„œ ์ขŒ์ธก โ†’ ์šฐ์ธก ๋ฐ์ดํ„ฐ ์ „๋‹ฌ) + */ + private static async handleTransferData(config: ButtonActionConfig, context: ButtonActionContext): Promise { + 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; + } + } + /** * ์œ„์น˜์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์•ก์…˜ ์ฒ˜๋ฆฌ */ @@ -3296,7 +3408,7 @@ export class ButtonActionExecutor { if (config.geolocationUpdateField && config.geolocationExtraField && config.geolocationExtraValue !== undefined) { const extraTableName = config.geolocationExtraTableName; const currentTableName = config.geolocationTableName || context.tableName; - + // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— UPDATEํ•˜๋Š” ๊ฒฝ์šฐ if (extraTableName && extraTableName !== currentTableName) { console.log("๐Ÿ“ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ํ•„๋“œ ๋ณ€๊ฒฝ:", { @@ -3309,7 +3421,7 @@ export class ButtonActionExecutor { // ํ‚ค ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ const keyValue = context.formData?.[config.geolocationExtraKeySourceField || ""]; - + if (keyValue && config.geolocationExtraKeyField) { try { // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” UPDATE API ํ˜ธ์ถœ @@ -3321,7 +3433,7 @@ export class ButtonActionExecutor { updateField: config.geolocationExtraField, updateValue: config.geolocationExtraValue, }); - + if (response.data?.success) { extraTableUpdated = true; console.log("โœ… ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” UPDATE ์„ฑ๊ณต:", response.data); @@ -3357,14 +3469,18 @@ export class ButtonActionExecutor { } // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ - let successMsg = config.successMessage || + let successMsg = + config.successMessage || `์œ„์น˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.\n์œ„๋„: ${latitude.toFixed(6)}, ๊ฒฝ๋„: ${longitude.toFixed(6)}`; - + // ์ถ”๊ฐ€ ํ•„๋“œ ๋ณ€๊ฒฝ์ด ์žˆ์œผ๋ฉด ๋ฉ”์‹œ์ง€์— ํฌํ•จ if (config.geolocationUpdateField && config.geolocationExtraField) { if (extraTableUpdated) { successMsg += `\n[${config.geolocationExtraTableName}] ${config.geolocationExtraField}: ${config.geolocationExtraValue}`; - } else if (!config.geolocationExtraTableName || config.geolocationExtraTableName === (config.geolocationTableName || context.tableName)) { + } else if ( + !config.geolocationExtraTableName || + config.geolocationExtraTableName === (config.geolocationTableName || context.tableName) + ) { successMsg += `\n${config.geolocationExtraField}: ${config.geolocationExtraValue}`; } } @@ -3504,11 +3620,13 @@ export class ButtonActionExecutor { if (response.success) { toast.success(config.successMessage || "์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); - + // ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new CustomEvent("refreshTableData", { - detail: { tableName } - })); + window.dispatchEvent( + new CustomEvent("refreshTableData", { + detail: { tableName }, + }), + ); return true; } else { @@ -3636,6 +3754,11 @@ export const DEFAULT_BUTTON_ACTIONS: Record 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() { **์ถฉ๋Œ ์œ„ํ—˜๋„: ๋‚ฎ์Œ (๐ŸŸข)** ์ƒˆ๋กœ์šด ์‹œ์Šคํ…œ์€ ๊ธฐ์กด ์‹œ์Šคํ…œ๊ณผ **๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘**ํ•˜๋ฉฐ, ์ตœ์†Œํ•œ์˜ ์ˆ˜์ •๋งŒ์œผ๋กœ ํ†ตํ•ฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด ํŽ˜์ด์ง€์— ์กฐ๊ฑด ๋ถ„๊ธฐ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -