From 863ec614f41540eb795aabcb8cf13fac4aa151d9 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 25 Feb 2026 17:40:17 +0900 Subject: [PATCH 1/2] feat: Implement layer activation and data transfer enhancements - Added support for force-activated layer IDs in ScreenViewPage, allowing layers to be activated based on data events. - Introduced ScreenContextProvider in ScreenModal and EditModal to manage screen-specific data and context. - Enhanced V2Repeater to register as a DataReceiver, enabling automatic data handling and integration with ScreenContext. - Improved ButtonPrimaryComponent to support automatic target component discovery and layer activation for data transfers. - Updated various components to streamline data handling and improve user experience during data transfers and layer management. --- .../app/(main)/screens/[screenId]/page.tsx | 54 ++++++- frontend/components/common/ScreenModal.tsx | 6 + frontend/components/screen/EditModal.tsx | 8 +- frontend/components/screen/ScreenDesigner.tsx | 44 +++++- .../config-panels/ButtonConfigPanel.tsx | 134 +++++++++++++---- frontend/components/v2/V2Repeater.tsx | 82 ++++++++++- .../config-panels/V2RepeaterConfigPanel.tsx | 20 ++- frontend/contexts/ScreenContext.tsx | 71 ++++++++- .../ButtonPrimaryComponent.tsx | 138 +++++++++++++++--- .../v2-repeater/V2RepeaterRenderer.tsx | 1 + frontend/lib/utils/buttonActions.ts | 9 +- frontend/types/v2-repeater.ts | 1 + 12 files changed, 487 insertions(+), 81 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 95305aaf..160883ad 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -87,10 +87,12 @@ function ScreenViewPage() { // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ๋†’์ด ์ถ”์  (์ปดํฌ๋„ŒํŠธ ID โ†’ ๋†’์ด) const [conditionalContainerHeights, setConditionalContainerHeights] = useState>({}); - // ๐Ÿ†• ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ ์ง€์› + // ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ ์ง€์› const [conditionalLayers, setConditionalLayers] = useState([]); - // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ์˜์—ญ(Zone) ๋ชฉ๋ก + // ์กฐ๊ฑด๋ถ€ ์˜์—ญ(Zone) ๋ชฉ๋ก const [zones, setZones] = useState([]); + // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์— ์˜ํ•ด ๊ฐ•์ œ ํ™œ์„ฑํ™”๋œ ๋ ˆ์ด์–ด ID ๋ชฉ๋ก + const [forceActivatedLayerIds, setForceActivatedLayerIds] = useState([]); // ํŽธ์ง‘ ๋ชจ๋‹ฌ ์ƒํƒœ const [editModalOpen, setEditModalOpen] = useState(false); @@ -378,11 +380,51 @@ function ScreenViewPage() { } }); - return newActiveIds; - }, [formData, conditionalLayers, layout]); + // ๊ฐ•์ œ ํ™œ์„ฑํ™”๋œ ๋ ˆ์ด์–ด ID ๋ณ‘ํ•ฉ + for (const forcedId of forceActivatedLayerIds) { + if (!newActiveIds.includes(forcedId)) { + newActiveIds.push(forcedId); + } + } - // ๐Ÿ†• ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ž๋™ ๋กœ๋“œ (๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ํผ) - // ํ™”๋ฉด์˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—์„œ ์‚ฌ์šฉ์ž ํšŒ์‚ฌ ์ฝ”๋“œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜์—ฌ ํผ์— ์ž๋™ ์ฑ„์›€ + return newActiveIds; + }, [formData, conditionalLayers, layout, forceActivatedLayerIds]); + + // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์— ์˜ํ•œ ๋ ˆ์ด์–ด ๊ฐ•์ œ ํ™œ์„ฑํ™” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + useEffect(() => { + const handleActivateLayer = (e: Event) => { + const { componentId, targetLayerId } = (e as CustomEvent).detail || {}; + if (!componentId && !targetLayerId) return; + + // targetLayerId๊ฐ€ ์ง์ ‘ ์ง€์ •๋œ ๊ฒฝ์šฐ + if (targetLayerId) { + setForceActivatedLayerIds((prev) => + prev.includes(targetLayerId) ? prev : [...prev, targetLayerId], + ); + console.log(`๐Ÿ”“ [๋ ˆ์ด์–ด ๊ฐ•์ œ ํ™œ์„ฑํ™”] layerId: ${targetLayerId}`); + return; + } + + // componentId๋กœ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์†ํ•œ ๋ ˆ์ด์–ด๋ฅผ ์ฐพ์•„ ํ™œ์„ฑํ™” + for (const layer of conditionalLayers) { + const found = layer.components.some((comp) => comp.id === componentId); + if (found) { + setForceActivatedLayerIds((prev) => + prev.includes(layer.id) ? prev : [...prev, layer.id], + ); + console.log(`๐Ÿ”“ [๋ ˆ์ด์–ด ๊ฐ•์ œ ํ™œ์„ฑํ™”] componentId: ${componentId} โ†’ layerId: ${layer.id}`); + return; + } + } + }; + + window.addEventListener("activateLayerForComponent", handleActivateLayer); + return () => { + window.removeEventListener("activateLayerForComponent", handleActivateLayer); + }; + }, [conditionalLayers]); + + // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ž๋™ ๋กœ๋“œ (๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ํผ) useEffect(() => { const loadMainTableData = async () => { if (!screen || !layout || !layout.components || !companyCode) { diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 16dd5afc..a79f26e3 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -25,6 +25,7 @@ import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; import { ConditionalZone, LayerDefinition } from "@/types/screen-management"; +import { ScreenContextProvider } from "@/contexts/ScreenContext"; interface ScreenModalState { isOpen: boolean; @@ -1025,6 +1026,10 @@ export const ScreenModal: React.FC = ({ className }) => { ) : screenData ? ( +
= ({ className }) => {
+
) : (

ํ™”๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 8dad77db..5f13cb78 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -17,6 +17,7 @@ import { dynamicFormApi } from "@/lib/api/dynamicForm"; import { useAuth } from "@/hooks/useAuth"; import { ConditionalZone, LayerDefinition } from "@/types/screen-management"; import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; +import { ScreenContextProvider } from "@/contexts/ScreenContext"; interface EditModalState { isOpen: boolean; @@ -1385,12 +1386,16 @@ export const EditModal: React.FC = ({ className }) => {
) : screenData ? ( +
{ const baseHeight = (screenDimensions?.height || 600) + 30; if (activeConditionalComponents.length > 0) { @@ -1546,6 +1551,7 @@ export const EditModal: React.FC = ({ className }) => { ); })}
+
) : (

ํ™”๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index fcb5b100..7f94bca0 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -551,9 +551,12 @@ export default function ScreenDesigner({ originalRegion: null, }); - // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์˜ Zone ์ •๋ณด (์บ”๋ฒ„์Šค ํฌ๊ธฐ ๊ฒฐ์ •์šฉ) + // ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์˜ Zone ์ •๋ณด (์บ”๋ฒ„์Šค ํฌ๊ธฐ ๊ฒฐ์ •์šฉ) const [activeLayerZone, setActiveLayerZone] = useState(null); + // ๋‹ค๋ฅธ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๋ฉ”ํƒ€ ์ •๋ณด ์บ์‹œ (๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํƒ€๊ฒŸ ์„ ํƒ์šฉ) + const [otherLayerComponents, setOtherLayerComponents] = useState([]); + // ๐Ÿ†• activeLayerId ๋ณ€๊ฒฝ ์‹œ ํ•ด๋‹น ๋ ˆ์ด์–ด์˜ Zone ์ฐพ๊ธฐ useEffect(() => { if (activeLayerId <= 1 || !selectedScreen?.screenId) { @@ -578,6 +581,41 @@ export default function ScreenDesigner({ findZone(); }, [activeLayerId, selectedScreen?.screenId, zones]); + // ๋‹ค๋ฅธ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๋ฉ”ํƒ€ ์ •๋ณด ๋กœ๋“œ (๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํƒ€๊ฒŸ ์„ ํƒ์šฉ) + useEffect(() => { + if (!selectedScreen?.screenId) return; + const loadOtherLayerComponents = async () => { + try { + const allLayers = await screenApi.getScreenLayers(selectedScreen.screenId); + const currentLayerId = activeLayerIdRef.current || 1; + const otherLayers = allLayers.filter((l: any) => l.layer_id !== currentLayerId && l.layer_id > 0); + + const components: ComponentData[] = []; + for (const layerInfo of otherLayers) { + try { + const layerData = await screenApi.getLayerLayout(selectedScreen.screenId, layerInfo.layer_id); + const rawComps = layerData?.components; + if (rawComps && Array.isArray(rawComps)) { + for (const comp of rawComps) { + components.push({ + ...comp, + _layerName: layerInfo.layer_name || `๋ ˆ์ด์–ด ${layerInfo.layer_id}`, + _layerId: String(layerInfo.layer_id), + } as any); + } + } + } catch { + // ๊ฐœ๋ณ„ ๋ ˆ์ด์–ด ๋กœ๋“œ ์‹คํŒจ ๋ฌด์‹œ + } + } + setOtherLayerComponents(components); + } catch { + setOtherLayerComponents([]); + } + }; + loadOtherLayerComponents(); + }, [selectedScreen?.screenId, activeLayerId]); + // ์บ”๋ฒ„์Šค์— ๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ (DB ๊ธฐ๋ฐ˜ ๋ ˆ์ด์–ด: ๊ฐ ๋ ˆ์ด์–ด๋ณ„๋กœ ๋ณ„๋„ ๋กœ๋“œ๋˜๋ฏ€๋กœ ์ „์ฒด ํ‘œ์‹œ) const visibleComponents = useMemo(() => { return layout.components; @@ -6516,8 +6554,8 @@ export default function ScreenDesigner({ updateComponentProperty(selectedComponent.id, "style", style); } }} - allComponents={layout.components} // ๐Ÿ†• ํ”Œ๋กœ์šฐ ์œ„์ ฏ ๊ฐ์ง€์šฉ - menuObjid={menuObjid} // ๐Ÿ†• ๋ฉ”๋‰ด OBJID ์ „๋‹ฌ + allComponents={[...layout.components, ...otherLayerComponents]} + menuObjid={menuObjid} /> )} diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index ea2febb1..ba9acbf1 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -2966,11 +2966,17 @@ export const ButtonConfigPanel: React.FC = ({ - {/* ๋ฐ์ดํ„ฐ ์ œ๊ณต ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ํ•„ํ„ฐ๋ง */} + {/* ์ž๋™ ํƒ์ƒ‰ ์˜ต์…˜ (๋ ˆ์ด์–ด๋ณ„ ํ…Œ์ด๋ธ”์ด ๋‹ค๋ฅผ ๋•Œ ์œ ์šฉ) */} + +
+ ์ž๋™ ํƒ์ƒ‰ (ํ˜„์žฌ ํ™œ์„ฑ ํ…Œ์ด๋ธ”) + (auto) +
+
+ {/* ๋ฐ์ดํ„ฐ ์ œ๊ณต ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ํ•„ํ„ฐ๋ง (๋ชจ๋“  ๋ ˆ์ด์–ด ํฌํ•จ) */} {allComponents .filter((comp: any) => { const type = comp.componentType || comp.type || ""; - // ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋“ค return ["table-list", "repeater-field-group", "form-group", "data-table"].some((t) => type.includes(t), ); @@ -2978,11 +2984,17 @@ export const ButtonConfigPanel: React.FC = ({ .map((comp: any) => { const compType = comp.componentType || comp.type || "unknown"; const compLabel = comp.label || comp.componentConfig?.title || comp.id; + const layerName = comp._layerName; return (
{compLabel} ({compType}) + {layerName && ( + + {layerName} + + )}
); @@ -2999,7 +3011,9 @@ export const ButtonConfigPanel: React.FC = ({ )}
-

ํ…Œ์ด๋ธ”, ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน ๋“ฑ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ

+

+ ๋ ˆ์ด์–ด๋ณ„๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์ด ์žˆ์„ ๊ฒฝ์šฐ "์ž๋™ ํƒ์ƒ‰"์„ ์„ ํƒํ•˜๋ฉด ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๋ ˆ์ด์–ด์˜ ํ…Œ์ด๋ธ”์„ ์ž๋™์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค +

@@ -3037,33 +3051,47 @@ export const ButtonConfigPanel: React.FC = ({ { - const currentSources = config.action?.dataTransfer?.additionalSources || []; - const newSources = [...currentSources]; - if (newSources.length === 0) { - newSources.push({ componentId: "", fieldName: e.target.value }); - } else { - newSources[0] = { ...newSources[0], fieldName: e.target.value }; - } - onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources); - }} - className="h-8 text-xs" - /> -

ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์— ์ €์žฅ๋  ํ•„๋“œ๋ช…

+ + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + { + const currentSources = config.action?.dataTransfer?.additionalSources || []; + const newSources = [...currentSources]; + if (newSources.length === 0) { + newSources.push({ componentId: "", fieldName: "" }); + } else { + newSources[0] = { ...newSources[0], fieldName: "" }; + } + onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources); + }} + className="text-xs" + > + + ์„ ํƒ ์•ˆ ํ•จ (์ „์ฒด ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ) + + {(mappingTargetColumns.length > 0 ? mappingTargetColumns : currentTableColumns).map((col) => ( + { + const currentSources = config.action?.dataTransfer?.additionalSources || []; + const newSources = [...currentSources]; + if (newSources.length === 0) { + newSources.push({ componentId: "", fieldName: col.name }); + } else { + newSources[0] = { ...newSources[0], fieldName: col.name }; + } + onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources); + }} + className="text-xs" + > + + {col.label || col.name} + {col.label && col.label !== col.name && ( + ({col.name}) + )} + + ))} + + + + + +

์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋  ํƒ€๊ฒŸ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ

diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx index d2b288ff..bd50ffdb 100644 --- a/frontend/components/v2/V2Repeater.tsx +++ b/frontend/components/v2/V2Repeater.tsx @@ -23,6 +23,9 @@ import { import { apiClient } from "@/lib/api/client"; import { allocateNumberingCode } from "@/lib/api/numberingRule"; import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; +import { DataReceivable } from "@/types/data-transfer"; +import { toast } from "sonner"; // modal-repeater-table ์ปดํฌ๋„ŒํŠธ ์žฌ์‚ฌ์šฉ import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/RepeaterTable"; @@ -38,6 +41,7 @@ declare global { export const V2Repeater: React.FC = ({ config: propConfig, + componentId, parentId, data: initialData, onDataChange, @@ -48,6 +52,12 @@ export const V2Repeater: React.FC = ({ }) => { // ScreenModal์—์„œ ์ „๋‹ฌ๋œ groupedData (๋ชจ๋‹ฌ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์šฉ) const groupedData = (restProps as any).groupedData || (restProps as any)._groupedData; + + // componentId ๊ฒฐ์ •: ์ง์ ‘ ์ „๋‹ฌ ๋˜๋Š” component ๊ฐ์ฒด์—์„œ ์ถ”์ถœ + const effectiveComponentId = componentId || (restProps as any).component?.id; + + // ScreenContext ์—ฐ๋™ (DataReceiver ๋“ฑ๋ก, Provider ์—†์œผ๋ฉด null) + const screenContext = useScreenContextOptional(); // ์„ค์ • ๋ณ‘ํ•ฉ const config: V2RepeaterConfig = useMemo( () => ({ @@ -68,6 +78,57 @@ export const V2Repeater: React.FC = ({ // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • ํŠธ๋ฆฌ๊ฑฐ const [autoWidthTrigger, setAutoWidthTrigger] = useState(0); + // ScreenContext DataReceiver ๋“ฑ๋ก (๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์•ก์…˜ ์ˆ˜์‹ ) + const onDataChangeRef = useRef(onDataChange); + onDataChangeRef.current = onDataChange; + + const handleReceiveData = useCallback( + async (incomingData: any[], configOrMode?: any) => { + console.log("๐Ÿ“ฅ [V2Repeater] ๋ฐ์ดํ„ฐ ์ˆ˜์‹ :", { count: incomingData?.length, configOrMode }); + + if (!incomingData || incomingData.length === 0) { + toast.warning("์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); + return; + } + + // ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”: {0: {...}} ํ˜•ํƒœ ์ฒ˜๋ฆฌ + const normalizedData = incomingData.map((item: any) => { + if (item && typeof item === "object" && item[0] && typeof item[0] === "object") { + const { 0: originalData, ...additionalFields } = item; + return { ...originalData, ...additionalFields }; + } + return item; + }); + + const mode = configOrMode?.mode || configOrMode || "append"; + + setData((prev) => { + const next = mode === "replace" ? normalizedData : [...prev, ...normalizedData]; + onDataChangeRef.current?.(next); + return next; + }); + + toast.success(`${normalizedData.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + }, + [], + ); + + useEffect(() => { + if (screenContext && effectiveComponentId) { + const receiver: DataReceivable = { + componentId: effectiveComponentId, + componentType: "v2-repeater", + receiveData: handleReceiveData, + }; + console.log("๐Ÿ“‹ [V2Repeater] ScreenContext์— ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก:", effectiveComponentId); + screenContext.registerDataReceiver(effectiveComponentId, receiver); + + return () => { + screenContext.unregisterDataReceiver(effectiveComponentId); + }; + } + }, [screenContext, effectiveComponentId, handleReceiveData]); + // ์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ผ๋ฒจ ๋งคํ•‘ const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); @@ -512,17 +573,26 @@ export const V2Repeater: React.FC = ({ }); }, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]); - // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋กœ๋“œ (RepeaterTable ํ‘œ์‹œ์šฉ) + // ๋ฆฌํ”ผํ„ฐ ์ปฌ๋Ÿผ ์„ค์ •์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์ปฌ๋Ÿผ ์ž๋™ ๊ฐ์ง€ + const allCategoryColumns = useMemo(() => { + const fromConfig = config.columns + .filter((col) => col.inputType === "category") + .map((col) => col.key); + const merged = new Set([...sourceCategoryColumns, ...fromConfig]); + return Array.from(merged); + }, [sourceCategoryColumns, config.columns]); + + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋กœ๋“œ (RepeaterTable ํ‘œ์‹œ์šฉ) useEffect(() => { const loadCategoryLabels = async () => { - if (sourceCategoryColumns.length === 0 || data.length === 0) { + if (allCategoryColumns.length === 0 || data.length === 0) { return; } // ๋ฐ์ดํ„ฐ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์˜ ๋ชจ๋“  ๊ณ ์œ  ์ฝ”๋“œ ์ˆ˜์ง‘ const allCodes = new Set(); for (const row of data) { - for (const col of sourceCategoryColumns) { + for (const col of allCategoryColumns) { // _display_ ์ ‘๋‘์‚ฌ๊ฐ€ ์žˆ๋Š” ์ปฌ๋Ÿผ๊ณผ ์›๋ณธ ์ปฌ๋Ÿผ ๋ชจ๋‘ ํ™•์ธ const val = row[`_display_${col}`] || row[col]; if (val && typeof val === "string") { @@ -531,7 +601,7 @@ export const V2Repeater: React.FC = ({ .map((c: string) => c.trim()) .filter(Boolean); for (const code of codes) { - if (!categoryLabelMap[code] && code.startsWith("CATEGORY_")) { + if (!categoryLabelMap[code] && code.length > 0) { allCodes.add(code); } } @@ -560,7 +630,7 @@ export const V2Repeater: React.FC = ({ }; loadCategoryLabels(); - }, [data, sourceCategoryColumns]); + }, [data, allCategoryColumns]); // ๊ณ„์‚ฐ ๊ทœ์น™ ์ ์šฉ (์†Œ์Šค ํ…Œ์ด๋ธ”์˜ _display_* ํ•„๋“œ๋„ ์ฐธ์กฐ ๊ฐ€๋Šฅ) const applyCalculationRules = useCallback( @@ -1112,7 +1182,7 @@ export const V2Repeater: React.FC = ({ selectedRows={selectedRows} onSelectionChange={setSelectedRows} equalizeWidthsTrigger={autoWidthTrigger} - categoryColumns={sourceCategoryColumns} + categoryColumns={allCategoryColumns} categoryLabelMap={categoryLabelMap} /> diff --git a/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx b/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx index 5b5b5fc2..1f89ae12 100644 --- a/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx @@ -1214,13 +1214,21 @@ export const V2RepeaterConfigPanel: React.FC = ({ )} - {/* ํŽธ์ง‘ ๊ฐ€๋Šฅ ์ฒดํฌ๋ฐ•์Šค */} + {/* ํŽธ์ง‘ ๊ฐ€๋Šฅ ํ† ๊ธ€ */} {!col.isSourceDisplay && ( - updateColumnProp(col.key, "editable", !!checked)} - title="ํŽธ์ง‘ ๊ฐ€๋Šฅ" - /> + )} - - - - - - ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค - - {availableTables.map((table) => ( - { - onUpdateProperty("componentConfig.action.dataTransfer.sourceTable", table.name); - }} - className="text-xs" - > - - {table.label} - ({table.name}) - - ))} - - - - - - - -
- - - - - - - - - - ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค - - {availableTables.map((table) => ( - { - onUpdateProperty("componentConfig.action.dataTransfer.targetTable", table.name); - }} - className="text-xs" - > - - {table.label} - ({table.name}) - - ))} - - - - - -
+ {/* ํƒ€๊ฒŸ ํ…Œ์ด๋ธ” (๊ณตํ†ต) */} +
+ + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + + {availableTables.map((table) => ( + { + onUpdateProperty("componentConfig.action.dataTransfer.targetTable", table.name); + }} + className="text-xs" + > + + {table.label} + ({table.name}) + + ))} + + + + +
- {/* ํ•„๋“œ ๋งคํ•‘ ๊ทœ์น™ */} + {/* ์†Œ์Šค ํ…Œ์ด๋ธ” ๋งคํ•‘ ๊ทธ๋ฃน */}
- +

- ์†Œ์Šค ํ•„๋“œ๋ฅผ ํƒ€๊ฒŸ ํ•„๋“œ์— ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. ๋น„์›Œ๋‘๋ฉด ๊ฐ™์€ ์ด๋ฆ„์˜ ํ•„๋“œ๋กœ ์ž๋™ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค. + ์—ฌ๋Ÿฌ ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ๋•Œ, ๊ฐ ํ…Œ์ด๋ธ”๋ณ„๋กœ ๋งคํ•‘ ๊ทœ์น™์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๋Ÿฐํƒ€์ž„์— ์†Œ์Šค ํ…Œ์ด๋ธ”์„ ์ž๋™ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.

- {!config.action?.dataTransfer?.sourceTable || !config.action?.dataTransfer?.targetTable ? ( + {!config.action?.dataTransfer?.targetTable ? (
-

๋จผ์ € ์†Œ์Šค ํ…Œ์ด๋ธ”๊ณผ ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”.

+

๋จผ์ € ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”.

- ) : (config.action?.dataTransfer?.mappingRules || []).length === 0 ? ( + ) : !(config.action?.dataTransfer?.multiTableMappings || []).length ? (

- ๋งคํ•‘ ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ ์ด๋ฆ„์˜ ํ•„๋“œ๋กœ ์ž๋™ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค. + ๋งคํ•‘ ๊ทธ๋ฃน์ด ์—†์Šต๋‹ˆ๋‹ค. ์†Œ์Šค ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

) : (
- {(config.action?.dataTransfer?.mappingRules || []).map((rule: any, index: number) => ( -
- {/* ์†Œ์Šค ํ•„๋“œ ์„ ํƒ (Combobox) */} -
- setMappingSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))} + {/* ์†Œ์Šค ํ…Œ์ด๋ธ” ํƒญ */} +
+ {(config.action?.dataTransfer?.multiTableMappings || []).map((group: any, gIdx: number) => ( +
+ - - - - - setMappingSourceSearch((prev) => ({ ...prev, [index]: value })) - } - /> - - - ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค - - - {mappingSourceColumns.map((col) => ( - { - const rules = [...(config.action?.dataTransfer?.mappingRules || [])]; - rules[index] = { ...rules[index], sourceField: col.name }; - onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", rules); - setMappingSourcePopoverOpen((prev) => ({ ...prev, [index]: false })); - }} - className="text-xs" - > - - {col.label} - {col.label !== col.name && ( - ({col.name}) - )} - - ))} - - - - - -
- - โ†’ - - {/* ํƒ€๊ฒŸ ํ•„๋“œ ์„ ํƒ (Combobox) */} -
- setMappingTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))} + {group.sourceTable + ? availableTables.find((t) => t.name === group.sourceTable)?.label || group.sourceTable + : `๊ทธ๋ฃน ${gIdx + 1}`} + {group.mappingRules?.length > 0 && ( + + {group.mappingRules.length} + + )} + + - - - - - setMappingTargetSearch((prev) => ({ ...prev, [index]: value })) - } - /> - - - ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค - - - {mappingTargetColumns.map((col) => ( - { - const rules = [...(config.action?.dataTransfer?.mappingRules || [])]; - rules[index] = { ...rules[index], targetField: col.name }; - onUpdateProperty("componentConfig.action.dataTransfer.mappingRules", rules); - setMappingTargetPopoverOpen((prev) => ({ ...prev, [index]: false })); - }} - className="text-xs" - > - - {col.label} - {col.label !== col.name && ( - ({col.name}) - )} - - ))} - - - - - + +
+ ))} +
- -
- ))} + {/* ํ™œ์„ฑ ๊ทธ๋ฃน ํŽธ์ง‘ ์˜์—ญ */} + {(() => { + const multiMappings = config.action?.dataTransfer?.multiTableMappings || []; + const activeGroup = multiMappings[activeMappingGroupIndex]; + if (!activeGroup) return null; + + const activeSourceTable = activeGroup.sourceTable || ""; + const activeSourceColumns = mappingSourceColumnsMap[activeSourceTable] || []; + const activeRules: any[] = activeGroup.mappingRules || []; + + const updateGroupField = (field: string, value: any) => { + const mappings = [...multiMappings]; + mappings[activeMappingGroupIndex] = { ...mappings[activeMappingGroupIndex], [field]: value }; + onUpdateProperty("componentConfig.action.dataTransfer.multiTableMappings", mappings); + }; + + return ( +
+ {/* ์†Œ์Šค ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+ + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + + {availableTables.map((table) => ( + { + updateGroupField("sourceTable", table.name); + if (!mappingSourceColumnsMap[table.name]) { + const cols = await loadMappingColumns(table.name); + setMappingSourceColumnsMap((prev) => ({ ...prev, [table.name]: cols })); + } + }} + className="text-xs" + > + + {table.label} + ({table.name}) + + ))} + + + + + +
+ + {/* ๋งคํ•‘ ๊ทœ์น™ ๋ชฉ๋ก */} +
+
+ + +
+ + {!activeSourceTable ? ( +

์†Œ์Šค ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์„ ํƒํ•˜์„ธ์š”.

+ ) : activeRules.length === 0 ? ( +

๋งคํ•‘ ์—†์Œ (๋™์ผ ํ•„๋“œ๋ช… ์ž๋™ ๋งคํ•‘)

+ ) : ( + activeRules.map((rule: any, rIdx: number) => { + const popoverKeyS = `${activeMappingGroupIndex}-${rIdx}-s`; + const popoverKeyT = `${activeMappingGroupIndex}-${rIdx}-t`; + return ( +
+
+ + setMappingSourcePopoverOpen((prev) => ({ ...prev, [popoverKeyS]: open })) + } + > + + + + + + + + ์ปฌ๋Ÿผ ์—†์Œ + + {activeSourceColumns.map((col) => ( + { + const newRules = [...activeRules]; + newRules[rIdx] = { ...newRules[rIdx], sourceField: col.name }; + updateGroupField("mappingRules", newRules); + setMappingSourcePopoverOpen((prev) => ({ ...prev, [popoverKeyS]: false })); + }} + className="text-xs" + > + + {col.label} + {col.label !== col.name && ({col.name})} + + ))} + + + + + +
+ + โ†’ + +
+ + setMappingTargetPopoverOpen((prev) => ({ ...prev, [popoverKeyT]: open })) + } + > + + + + + + + + ์ปฌ๋Ÿผ ์—†์Œ + + {mappingTargetColumns.map((col) => ( + { + const newRules = [...activeRules]; + newRules[rIdx] = { ...newRules[rIdx], targetField: col.name }; + updateGroupField("mappingRules", newRules); + setMappingTargetPopoverOpen((prev) => ({ ...prev, [popoverKeyT]: false })); + }} + className="text-xs" + > + + {col.label} + {col.label !== col.name && ({col.name})} + + ))} + + + + + +
+ + +
+ ); + }) + )} +
+
+ ); + })()}
)}
@@ -3647,9 +3712,9 @@ export const ButtonConfigPanel: React.FC = ({
1. ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค
- 2. ํ•„๋“œ ๋งคํ•‘ ๊ทœ์น™์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: ํ’ˆ๋ฒˆ โ†’ ํ’ˆ๋ชฉ์ฝ”๋“œ) + 2. ์†Œ์Šค ํ…Œ์ด๋ธ”๋ณ„๋กœ ํ•„๋“œ ๋งคํ•‘ ๊ทœ์น™์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค
- 3. ์ด ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋งคํ•‘๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ํƒ€๊ฒŸ์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค + 3. ์ด ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์†Œ์Šค ํ…Œ์ด๋ธ”์„ ์ž๋™ ๊ฐ์ง€ํ•˜์—ฌ ๋งคํ•‘๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ํƒ€๊ฒŸ์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค

diff --git a/frontend/components/table-category/CategoryColumnList.tsx b/frontend/components/table-category/CategoryColumnList.tsx index 3be70840..d6ed8c62 100644 --- a/frontend/components/table-category/CategoryColumnList.tsx +++ b/frontend/components/table-category/CategoryColumnList.tsx @@ -72,9 +72,10 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, allColumns = response.data; } - // category ํƒ€์ž… ์ปฌ๋Ÿผ๋งŒ ํ•„ํ„ฐ๋ง + // category ํƒ€์ž… ์ค‘ ์ž์ฒด ์นดํ…Œ๊ณ ๋ฆฌ๋งŒ ํ•„ํ„ฐ๋ง (์ฐธ์กฐ ์ปฌ๋Ÿผ ์ œ์™ธ) const categoryColumns = allColumns.filter( - (col: any) => col.inputType === "category" || col.input_type === "category" + (col: any) => (col.inputType === "category" || col.input_type === "category") + && !col.categoryRef && !col.category_ref ); console.log("โœ… ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ํ•„ํ„ฐ๋ง ์™„๋ฃŒ:", { diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx index bd50ffdb..1853ebe7 100644 --- a/frontend/components/v2/V2Repeater.tsx +++ b/frontend/components/v2/V2Repeater.tsx @@ -75,6 +75,15 @@ export const V2Repeater: React.FC = ({ const [selectedRows, setSelectedRows] = useState>(new Set()); const [modalOpen, setModalOpen] = useState(false); + // ์ €์žฅ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ ํ•ญ์ƒ ์ตœ์‹  data๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•œ ref + const dataRef = useRef(data); + useEffect(() => { + dataRef.current = data; + }, [data]); + + // ์ˆ˜์ • ๋ชจ๋“œ์—์„œ ๋กœ๋“œ๋œ ์›๋ณธ ID ๋ชฉ๋ก (์‚ญ์ œ ์ถ”์ ์šฉ) + const loadedIdsRef = useRef>(new Set()); + // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • ํŠธ๋ฆฌ๊ฑฐ const [autoWidthTrigger, setAutoWidthTrigger] = useState(0); @@ -91,17 +100,67 @@ export const V2Repeater: React.FC = ({ return; } - // ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”: {0: {...}} ํ˜•ํƒœ ์ฒ˜๋ฆฌ + // ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”: {0: {...}} ํ˜•ํƒœ ์ฒ˜๋ฆฌ + ์†Œ์Šค ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€ ํ•„๋“œ ์ œ๊ฑฐ + const metaFieldsToStrip = new Set([ + "id", + "created_date", + "updated_date", + "created_by", + "updated_by", + "company_code", + ]); const normalizedData = incomingData.map((item: any) => { + let raw = item; if (item && typeof item === "object" && item[0] && typeof item[0] === "object") { const { 0: originalData, ...additionalFields } = item; - return { ...originalData, ...additionalFields }; + raw = { ...originalData, ...additionalFields }; } - return item; + const cleaned: Record = {}; + for (const [key, value] of Object.entries(raw)) { + if (!metaFieldsToStrip.has(key)) { + cleaned[key] = value; + } + } + return cleaned; }); const mode = configOrMode?.mode || configOrMode || "append"; + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋ณ€ํ™˜ + // allCategoryColumns ๋˜๋Š” fromMainForm ์ปฌ๋Ÿผ์˜ ๊ฐ’์„ ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ + const codesToResolve = new Set(); + for (const item of normalizedData) { + for (const [key, val] of Object.entries(item)) { + if (key.startsWith("_")) continue; + if (typeof val === "string" && val && !categoryLabelMapRef.current[val]) { + codesToResolve.add(val as string); + } + } + } + + if (codesToResolve.size > 0) { + try { + const resp = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: Array.from(codesToResolve), + }); + if (resp.data?.success && resp.data.data) { + const labelData = resp.data.data as Record; + setCategoryLabelMap((prev) => ({ ...prev, ...labelData })); + for (const item of normalizedData) { + for (const key of Object.keys(item)) { + if (key.startsWith("_")) continue; + const val = item[key]; + if (typeof val === "string" && labelData[val]) { + item[key] = labelData[val]; + } + } + } + } + } catch { + // ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ์ฝ”๋“œ ์œ ์ง€ + } + } + setData((prev) => { const next = mode === "replace" ? normalizedData : [...prev, ...normalizedData]; onDataChangeRef.current?.(next); @@ -137,6 +196,10 @@ export const V2Repeater: React.FC = ({ // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋งคํ•‘ (RepeaterTable ํ‘œ์‹œ์šฉ) const [categoryLabelMap, setCategoryLabelMap] = useState>({}); + const categoryLabelMapRef = useRef>({}); + useEffect(() => { + categoryLabelMapRef.current = categoryLabelMap; + }, [categoryLabelMap]); // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด (inputType ๋งคํ•‘์šฉ) const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState>({}); @@ -170,35 +233,54 @@ export const V2Repeater: React.FC = ({ }; }, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]); - // ์ €์žฅ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + // ์ €์žฅ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ (dataRef/categoryLabelMapRef๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ญ์ƒ ์ตœ์‹  ์ƒํƒœ ์ฐธ์กฐ) useEffect(() => { const handleSaveEvent = async (event: CustomEvent) => { - // ๐Ÿ†• mainTableName์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด dataSource.tableName ์‚ฌ์šฉ - const tableName = - config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; - const eventParentId = event.detail?.parentId; - const mainFormData = event.detail?.mainFormData; + const currentData = dataRef.current; + const currentCategoryMap = categoryLabelMapRef.current; - // ๐Ÿ†• ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์—์„œ ์ƒ์„ฑ๋œ ID (FK ์—ฐ๊ฒฐ์šฉ) + const configTableName = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; + const tableName = configTableName || event.detail?.tableName; + const mainFormData = event.detail?.mainFormData; const masterRecordId = event.detail?.masterRecordId || mainFormData?.id; - if (!tableName || data.length === 0) { + console.log("๐Ÿ”ต [V2Repeater] repeaterSave ์ด๋ฒคํŠธ ์ˆ˜์‹ :", { + configTableName, + tableName, + masterRecordId, + dataLength: currentData.length, + foreignKeyColumn: config.foreignKeyColumn, + foreignKeySourceColumn: config.foreignKeySourceColumn, + dataSnapshot: currentData.map((r: any) => ({ id: r.id, item_name: r.item_name })), + }); + toast.info(`[๋””๋ฒ„๊ทธ] V2Repeater ์ด๋ฒคํŠธ ์ˆ˜์‹ : ${currentData.length}๊ฑด, table=${tableName}`); + + if (!tableName || currentData.length === 0) { + console.warn("๐Ÿ”ด [V2Repeater] ์ €์žฅ ์Šคํ‚ต:", { tableName, dataLength: currentData.length }); + toast.warning(`[๋””๋ฒ„๊ทธ] V2Repeater ์ €์žฅ ์Šคํ‚ต: data=${currentData.length}, table=${tableName}`); + window.dispatchEvent(new CustomEvent("repeaterSaveComplete")); return; } - // V2Repeater ์ €์žฅ ์‹œ์ž‘ - const saveInfo = { + if (config.foreignKeyColumn) { + const sourceCol = config.foreignKeySourceColumn; + const hasFkSource = sourceCol && mainFormData && mainFormData[sourceCol] !== undefined; + if (!hasFkSource && !masterRecordId) { + console.warn("๐Ÿ”ด [V2Repeater] FK ์†Œ์Šค ๊ฐ’/masterRecordId ๋ชจ๋‘ ์—†์–ด ์ €์žฅ ์Šคํ‚ต"); + window.dispatchEvent(new CustomEvent("repeaterSaveComplete")); + return; + } + } + + console.log("V2Repeater ์ €์žฅ ์‹œ์ž‘", { tableName, - useCustomTable: config.useCustomTable, - mainTableName: config.mainTableName, foreignKeyColumn: config.foreignKeyColumn, masterRecordId, - dataLength: data.length, - }; - console.log("V2Repeater ์ €์žฅ ์‹œ์ž‘", saveInfo); + dataLength: currentData.length, + }); try { - // ํ…Œ์ด๋ธ” ์œ ํšจ ์ปฌ๋Ÿผ ์กฐํšŒ let validColumns: Set = new Set(); try { const columnsResponse = await apiClient.get(`/table-management/tables/${tableName}/columns`); @@ -209,13 +291,10 @@ export const V2Repeater: React.FC = ({ console.warn("ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ ์‹คํŒจ"); } - for (let i = 0; i < data.length; i++) { - const row = data[i]; - - // ๋‚ด๋ถ€ ํ•„๋“œ ์ œ๊ฑฐ + for (let i = 0; i < currentData.length; i++) { + const row = currentData[i]; const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_"))); - // ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์‹œ์—๋Š” ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ ์•ˆํ•จ) let mergedData: Record; if (config.useCustomTable && config.mainTableName) { mergedData = { ...cleanRow }; @@ -242,59 +321,83 @@ export const V2Repeater: React.FC = ({ }; } - // ์œ ํšจํ•˜์ง€ ์•Š์€ ์ปฌ๋Ÿผ ์ œ๊ฑฐ const filteredData: Record = {}; for (const [key, value] of Object.entries(mergedData)) { if (validColumns.size === 0 || validColumns.has(key)) { - filteredData[key] = value; + if (typeof value === "string" && currentCategoryMap[value]) { + filteredData[key] = currentCategoryMap[value]; + } else { + filteredData[key] = value; + } } } - // ๊ธฐ์กด ํ–‰(id ์กด์žฌ)์€ UPDATE, ์ƒˆ ํ–‰์€ INSERT const rowId = row.id; + console.log(`๐Ÿ”ง [V2Repeater] ํ–‰ ${i} ์ €์žฅ:`, { + rowId, + isUpdate: rowId && typeof rowId === "string" && rowId.includes("-"), + filteredDataKeys: Object.keys(filteredData), + }); if (rowId && typeof rowId === "string" && rowId.includes("-")) { - // UUID ํ˜•ํƒœ์˜ id๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ์กด ๋ฐ์ดํ„ฐ โ†’ UPDATE const { id: _, created_date: _cd, updated_date: _ud, ...updateFields } = filteredData; await apiClient.put(`/table-management/tables/${tableName}/edit`, { originalData: { id: rowId }, updatedData: updateFields, }); } else { - // ์ƒˆ ํ–‰ โ†’ INSERT await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); } } + // ์‚ญ์ œ๋œ ํ–‰ ์ฒ˜๋ฆฌ: ์›๋ณธ์—๋Š” ์žˆ์—ˆ์ง€๋งŒ ํ˜„์žฌ data์— ์—†๋Š” ID๋ฅผ DELETE + const currentIds = new Set(currentData.map((r) => r.id).filter(Boolean)); + const deletedIds = Array.from(loadedIdsRef.current).filter((id) => !currentIds.has(id)); + if (deletedIds.length > 0) { + console.log("๐Ÿ—‘๏ธ [V2Repeater] ์‚ญ์ œํ•  ํ–‰:", deletedIds); + try { + await apiClient.delete(`/table-management/tables/${tableName}/delete`, { + data: deletedIds.map((id) => ({ id })), + }); + console.log(`โœ… [V2Repeater] ${deletedIds.length}๊ฑด ์‚ญ์ œ ์™„๋ฃŒ`); + } catch (deleteError) { + console.error("โŒ [V2Repeater] ์‚ญ์ œ ์‹คํŒจ:", deleteError); + } + } + + // ์ €์žฅ ์™„๋ฃŒ ํ›„ loadedIdsRef ๊ฐฑ์‹  + loadedIdsRef.current = new Set(currentData.map((r) => r.id).filter(Boolean)); + + toast.success(`V2Repeater ${currentData.length}๊ฑด ์ €์žฅ ์™„๋ฃŒ`); } catch (error) { console.error("โŒ V2Repeater ์ €์žฅ ์‹คํŒจ:", error); - throw error; + toast.error(`V2Repeater ์ €์žฅ ์‹คํŒจ: ${error}`); + } finally { + window.dispatchEvent(new CustomEvent("repeaterSaveComplete")); } }; - // V2 EventBus ๊ตฌ๋… const unsubscribe = v2EventBus.subscribe( V2_EVENTS.REPEATER_SAVE, async (payload) => { - const tableName = + const configTableName = config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; - if (payload.tableName === tableName) { + if (!configTableName || payload.tableName === configTableName) { await handleSaveEvent({ detail: payload } as CustomEvent); } }, - { componentId: `v2-repeater-${config.dataSource?.tableName}` }, + { componentId: `v2-repeater-${config.dataSource?.tableName || "same-table"}` }, ); - // ๋ ˆ๊ฑฐ์‹œ ์ด๋ฒคํŠธ๋„ ๊ณ„์† ์ง€์› (์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) window.addEventListener("repeaterSave" as any, handleSaveEvent); return () => { unsubscribe(); window.removeEventListener("repeaterSave" as any, handleSaveEvent); }; }, [ - data, config.dataSource?.tableName, config.useCustomTable, config.mainTableName, config.foreignKeyColumn, + config.foreignKeySourceColumn, parentId, ]); @@ -362,7 +465,6 @@ export const V2Repeater: React.FC = ({ }); // ๊ฐ ํ–‰์— ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ - // RepeaterTable์€ isSourceDisplay ์ปฌ๋Ÿผ์„ `_display_${col.key}` ํ•„๋“œ๋กœ ๋ Œ๋”๋งํ•จ rows.forEach((row: any) => { const sourceRecord = sourceMap.get(String(row[fkColumn])); if (sourceRecord) { @@ -380,12 +482,50 @@ export const V2Repeater: React.FC = ({ } } + // DB์—์„œ ๋กœ๋“œ๋œ ๋ฐ์ดํ„ฐ ์ค‘ CATEGORY_ ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ + const codesToResolve = new Set(); + for (const row of rows) { + for (const val of Object.values(row)) { + if (typeof val === "string" && val.startsWith("CATEGORY_")) { + codesToResolve.add(val); + } + } + } + + if (codesToResolve.size > 0) { + try { + const labelResp = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: Array.from(codesToResolve), + }); + if (labelResp.data?.success && labelResp.data.data) { + const labelData = labelResp.data.data as Record; + setCategoryLabelMap((prev) => ({ ...prev, ...labelData })); + for (const row of rows) { + for (const key of Object.keys(row)) { + if (key.startsWith("_")) continue; + const val = row[key]; + if (typeof val === "string" && labelData[val]) { + row[key] = labelData[val]; + } + } + } + } + } catch { + // ๋ผ๋ฒจ ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ์ฝ”๋“œ ์œ ์ง€ + } + } + + // ์›๋ณธ ID ๋ชฉ๋ก ๊ธฐ๋ก (์‚ญ์ œ ์ถ”์ ์šฉ) + const ids = rows.map((r: any) => r.id).filter(Boolean); + loadedIdsRef.current = new Set(ids); + console.log("๐Ÿ“‹ [V2Repeater] ์›๋ณธ ID ๊ธฐ๋ก:", ids); + setData(rows); dataLoadedRef.current = true; if (onDataChange) onDataChange(rows); } } catch (error) { - console.error("โŒ [V2Repeater] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error); + console.error("[V2Repeater] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error); } }; @@ -407,16 +547,28 @@ export const V2Repeater: React.FC = ({ if (!tableName) return; try { - const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); - const columns = response.data?.data?.columns || response.data?.columns || response.data || []; + const [colResponse, typeResponse] = await Promise.all([ + apiClient.get(`/table-management/tables/${tableName}/columns`), + apiClient.get(`/table-management/tables/${tableName}/web-types`), + ]); + const columns = colResponse.data?.data?.columns || colResponse.data?.columns || colResponse.data || []; + const inputTypes = typeResponse.data?.data || []; + + // inputType/categoryRef ๋งคํ•‘ ์ƒ์„ฑ + const typeMap: Record = {}; + inputTypes.forEach((t: any) => { + typeMap[t.columnName] = t; + }); const columnMap: Record = {}; columns.forEach((col: any) => { const name = col.columnName || col.column_name || col.name; + const typeInfo = typeMap[name]; columnMap[name] = { - inputType: col.inputType || col.input_type || col.webType || "text", + inputType: typeInfo?.inputType || col.inputType || col.input_type || col.webType || "text", displayName: col.displayName || col.display_name || col.label || name, detailSettings: col.detailSettings || col.detail_settings, + categoryRef: typeInfo?.categoryRef || null, }; }); setCurrentTableColumnInfo(columnMap); @@ -548,14 +700,18 @@ export const V2Repeater: React.FC = ({ else if (inputType === "code") type = "select"; else if (inputType === "category") type = "category"; // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… - // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ์ฐธ์กฐ ID ๊ฐ€์ ธ์˜ค๊ธฐ (tableName.columnName ํ˜•์‹) - // category ํƒ€์ž…์ธ ๊ฒฝ์šฐ ํ˜„์žฌ ํ…Œ์ด๋ธ”๋ช…๊ณผ ์ปฌ๋Ÿผ๋ช…์„ ์กฐํ•ฉ + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฐธ์กฐ ID ๊ฒฐ์ • + // DB์˜ category_ref ์„ค์ • ์šฐ์„ , ์—†์œผ๋ฉด ์ž๊ธฐ ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ let categoryRef: string | undefined; if (inputType === "category") { - // ๐Ÿ†• ์†Œ์Šค ํ‘œ์‹œ ์ปฌ๋Ÿผ์ด๋ฉด ์†Œ์Šค ํ…Œ์ด๋ธ” ์‚ฌ์šฉ, ์•„๋‹ˆ๋ฉด ํƒ€๊ฒŸ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ - const tableName = col.isSourceDisplay ? resolvedSourceTable : config.dataSource?.tableName; - if (tableName) { - categoryRef = `${tableName}.${col.key}`; + const dbCategoryRef = colInfo?.detailSettings?.categoryRef || colInfo?.categoryRef; + if (dbCategoryRef) { + categoryRef = dbCategoryRef; + } else { + const tableName = col.isSourceDisplay ? resolvedSourceTable : config.dataSource?.tableName; + if (tableName) { + categoryRef = `${tableName}.${col.key}`; + } } } @@ -574,63 +730,78 @@ export const V2Repeater: React.FC = ({ }, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]); // ๋ฆฌํ”ผํ„ฐ ์ปฌ๋Ÿผ ์„ค์ •์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์ปฌ๋Ÿผ ์ž๋™ ๊ฐ์ง€ + // repeaterColumns์˜ resolved type ์‚ฌ์šฉ (config + DB ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ชจ๋‘ ๋ฐ˜์˜) const allCategoryColumns = useMemo(() => { - const fromConfig = config.columns - .filter((col) => col.inputType === "category") - .map((col) => col.key); - const merged = new Set([...sourceCategoryColumns, ...fromConfig]); + const fromRepeater = repeaterColumns + .filter((col) => col.type === "category") + .map((col) => col.field.replace(/^_display_/, "")); + const merged = new Set([...sourceCategoryColumns, ...fromRepeater]); return Array.from(merged); - }, [sourceCategoryColumns, config.columns]); + }, [sourceCategoryColumns, repeaterColumns]); - // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋กœ๋“œ (RepeaterTable ํ‘œ์‹œ์šฉ) + // CATEGORY_ ์ฝ”๋“œ ๋ฐฐ์—ด์„ ๋ฐ›์•„ ๋ผ๋ฒจ์„ ์ผ๊ด„ ์กฐํšŒํ•˜๋Š” ํ•จ์ˆ˜ + const fetchCategoryLabels = useCallback(async (codes: string[]) => { + if (codes.length === 0) return; + try { + const response = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: codes, + }); + if (response.data?.success && response.data.data) { + setCategoryLabelMap((prev) => ({ ...prev, ...response.data.data })); + } + } catch (error) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ์กฐํšŒ ์‹คํŒจ:", error); + } + }, []); + + // parentFormData(๋งˆ์Šคํ„ฐ ํ–‰)์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋ฅผ ๋ฏธ๋ฆฌ ๋กœ๋“œ + // fromMainForm autoFill์—์„œ ์ฐธ์กฐํ•  ๋งˆ์Šคํ„ฐ ํ•„๋“œ์˜ ๋ผ๋ฒจ์„ ์‚ฌ์ „์— ํ™•๋ณด useEffect(() => { - const loadCategoryLabels = async () => { - if (allCategoryColumns.length === 0 || data.length === 0) { - return; - } + if (!parentFormData) return; + const codes: string[] = []; - // ๋ฐ์ดํ„ฐ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์˜ ๋ชจ๋“  ๊ณ ์œ  ์ฝ”๋“œ ์ˆ˜์ง‘ - const allCodes = new Set(); - for (const row of data) { - for (const col of allCategoryColumns) { - // _display_ ์ ‘๋‘์‚ฌ๊ฐ€ ์žˆ๋Š” ์ปฌ๋Ÿผ๊ณผ ์›๋ณธ ์ปฌ๋Ÿผ ๋ชจ๋‘ ํ™•์ธ - const val = row[`_display_${col}`] || row[col]; - if (val && typeof val === "string") { - const codes = val - .split(",") - .map((c: string) => c.trim()) - .filter(Boolean); - for (const code of codes) { - if (!categoryLabelMap[code] && code.length > 0) { - allCodes.add(code); - } - } - } + // fromMainForm autoFill์˜ sourceField ๊ฐ’ ์ค‘ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์— ํ•ด๋‹นํ•˜๋Š” ๊ฒƒ๋งŒ ์ˆ˜์ง‘ + for (const col of config.columns) { + if (col.autoFill?.type === "fromMainForm" && col.autoFill.sourceField) { + const val = parentFormData[col.autoFill.sourceField]; + if (typeof val === "string" && val && !categoryLabelMap[val]) { + codes.push(val); } } - - if (allCodes.size === 0) { - return; - } - - try { - const response = await apiClient.post("/table-categories/labels-by-codes", { - valueCodes: Array.from(allCodes), - }); - - if (response.data?.success && response.data.data) { - setCategoryLabelMap((prev) => ({ - ...prev, - ...response.data.data, - })); + // receiveFromParent ํŒจํ„ด + if ((col as any).receiveFromParent) { + const parentField = (col as any).parentFieldName || col.key; + const val = parentFormData[parentField]; + if (typeof val === "string" && val && !categoryLabelMap[val]) { + codes.push(val); } - } catch (error) { - console.error("์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ์กฐํšŒ ์‹คํŒจ:", error); } - }; + } - loadCategoryLabels(); - }, [data, allCategoryColumns]); + if (codes.length > 0) { + fetchCategoryLabels(codes); + } + }, [parentFormData, config.columns, fetchCategoryLabels]); + + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋กœ๋“œ + useEffect(() => { + if (data.length === 0) return; + + const allCodes = new Set(); + + for (const row of data) { + for (const col of allCategoryColumns) { + const val = row[`_display_${col}`] || row[col]; + if (val && typeof val === "string") { + val.split(",").map((c: string) => c.trim()).filter(Boolean).forEach((code: string) => { + if (!categoryLabelMap[code]) allCodes.add(code); + }); + } + } + } + + fetchCategoryLabels(Array.from(allCodes)); + }, [data, allCategoryColumns, fetchCategoryLabels]); // ๊ณ„์‚ฐ ๊ทœ์น™ ์ ์šฉ (์†Œ์Šค ํ…Œ์ด๋ธ”์˜ _display_* ํ•„๋“œ๋„ ์ฐธ์กฐ ๊ฐ€๋Šฅ) const applyCalculationRules = useCallback( @@ -747,7 +918,12 @@ export const V2Repeater: React.FC = ({ case "fromMainForm": if (col.autoFill.sourceField && mainFormData) { - return mainFormData[col.autoFill.sourceField]; + const rawValue = mainFormData[col.autoFill.sourceField]; + // categoryLabelMap์— ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (์ ‘๋‘์‚ฌ ๋ฌด๊ด€) + if (typeof rawValue === "string" && categoryLabelMap[rawValue]) { + return categoryLabelMap[rawValue]; + } + return rawValue; } return ""; @@ -767,7 +943,7 @@ export const V2Repeater: React.FC = ({ return undefined; } }, - [], + [categoryLabelMap], ); // ๐Ÿ†• ์ฑ„๋ฒˆ API ํ˜ธ์ถœ (๋น„๋™๊ธฐ) @@ -801,7 +977,12 @@ export const V2Repeater: React.FC = ({ const row: any = { _id: `grouped_${Date.now()}_${index}` }; for (const col of config.columns) { - const sourceValue = item[(col as any).sourceKey || col.key]; + let sourceValue = item[(col as any).sourceKey || col.key]; + + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋ณ€ํ™˜ (์ ‘๋‘์‚ฌ ๋ฌด๊ด€, categoryLabelMap ๊ธฐ๋ฐ˜) + if (typeof sourceValue === "string" && categoryLabelMap[sourceValue]) { + sourceValue = categoryLabelMap[sourceValue]; + } if (col.isSourceDisplay) { row[col.key] = sourceValue ?? ""; @@ -822,6 +1003,48 @@ export const V2Repeater: React.FC = ({ return row; }); + // ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์˜ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋ณ€ํ™˜ (์ ‘๋‘์‚ฌ ๋ฌด๊ด€) + const categoryColSet = new Set(allCategoryColumns); + const codesToResolve = new Set(); + for (const row of newRows) { + for (const col of config.columns) { + const val = row[col.key] || row[`_display_${col.key}`]; + if (typeof val === "string" && val && (categoryColSet.has(col.key) || col.autoFill?.type === "fromMainForm")) { + if (!categoryLabelMap[val]) { + codesToResolve.add(val); + } + } + } + } + + if (codesToResolve.size > 0) { + apiClient.post("/table-categories/labels-by-codes", { + valueCodes: Array.from(codesToResolve), + }).then((resp) => { + if (resp.data?.success && resp.data.data) { + const labelData = resp.data.data as Record; + setCategoryLabelMap((prev) => ({ ...prev, ...labelData })); + const convertedRows = newRows.map((row) => { + const updated = { ...row }; + for (const col of config.columns) { + const val = updated[col.key]; + if (typeof val === "string" && labelData[val]) { + updated[col.key] = labelData[val]; + } + const dispKey = `_display_${col.key}`; + const dispVal = updated[dispKey]; + if (typeof dispVal === "string" && labelData[dispVal]) { + updated[dispKey] = labelData[dispVal]; + } + } + return updated; + }); + setData(convertedRows); + onDataChange?.(convertedRows); + } + }).catch(() => {}); + } + setData(newRows); onDataChange?.(newRows); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -856,7 +1079,7 @@ export const V2Repeater: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [parentFormData, config.columns, generateAutoFillValueSync]); - // ํ–‰ ์ถ”๊ฐ€ (inline ๋ชจ๋“œ ๋˜๋Š” ๋ชจ๋‹ฌ ์—ด๊ธฐ) - ๋น„๋™๊ธฐ๋กœ ๋ณ€๊ฒฝ + // ํ–‰ ์ถ”๊ฐ€ (inline ๋ชจ๋“œ ๋˜๋Š” ๋ชจ๋‹ฌ ์—ด๊ธฐ) const handleAddRow = useCallback(async () => { if (isModalMode) { setModalOpen(true); @@ -864,11 +1087,10 @@ export const V2Repeater: React.FC = ({ const newRow: any = { _id: `new_${Date.now()}` }; const currentRowCount = data.length; - // ๋จผ์ € ๋™๊ธฐ์  ์ž๋™ ์ž…๋ ฅ ๊ฐ’ ์ ์šฉ + // ๋™๊ธฐ์  ์ž๋™ ์ž…๋ ฅ ๊ฐ’ ์ ์šฉ for (const col of config.columns) { const autoValue = generateAutoFillValueSync(col, currentRowCount, parentFormData); if (autoValue === null && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId) { - // ์ฑ„๋ฒˆ ๊ทœ์น™: ์ฆ‰์‹œ API ํ˜ธ์ถœ newRow[col.key] = await generateNumberingCode(col.autoFill.numberingRuleId); } else if (autoValue !== undefined) { newRow[col.key] = autoValue; @@ -877,10 +1099,51 @@ export const V2Repeater: React.FC = ({ } } + // fromMainForm ๋“ฑ์œผ๋กœ ๋„˜์–ด์˜จ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋ณ€ํ™˜ + // allCategoryColumns์— ํ•ด๋‹นํ•˜๋Š” ์ปฌ๋Ÿผ์ด๊ฑฐ๋‚˜ categoryLabelMap์— ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ๋ณ€ํ™˜ + const categoryColSet = new Set(allCategoryColumns); + const unresolvedCodes: string[] = []; + for (const col of config.columns) { + const val = newRow[col.key]; + if (typeof val !== "string" || !val) continue; + + // ์ด ์ปฌ๋Ÿผ์ด ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ด๊ฑฐ๋‚˜, fromMainForm์œผ๋กœ ๊ฐ€์ ธ์˜จ ๊ฐ’์ธ ๊ฒฝ์šฐ + const isCategoryCol = categoryColSet.has(col.key); + const isFromMainForm = col.autoFill?.type === "fromMainForm"; + + if (isCategoryCol || isFromMainForm) { + if (categoryLabelMap[val]) { + newRow[col.key] = categoryLabelMap[val]; + } else { + unresolvedCodes.push(val); + } + } + } + + if (unresolvedCodes.length > 0) { + try { + const resp = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: unresolvedCodes, + }); + if (resp.data?.success && resp.data.data) { + const labelData = resp.data.data as Record; + setCategoryLabelMap((prev) => ({ ...prev, ...labelData })); + for (const col of config.columns) { + const val = newRow[col.key]; + if (typeof val === "string" && labelData[val]) { + newRow[col.key] = labelData[val]; + } + } + } + } catch { + // ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ์ฝ”๋“œ ์œ ์ง€ + } + } + const newData = [...data, newRow]; handleDataChange(newData); } - }, [isModalMode, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode, parentFormData]); + }, [isModalMode, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode, parentFormData, categoryLabelMap, allCategoryColumns]); // ๋ชจ๋‹ฌ์—์„œ ํ•ญ๋ชฉ ์„ ํƒ - ๋น„๋™๊ธฐ๋กœ ๋ณ€๊ฒฝ const handleSelectItems = useCallback( @@ -905,8 +1168,12 @@ export const V2Repeater: React.FC = ({ // ๋ชจ๋“  ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ (์ˆœ์„œ๋Œ€๋กœ) for (const col of config.columns) { if (col.isSourceDisplay) { - // ์†Œ์Šค ํ‘œ์‹œ ์ปฌ๋Ÿผ: ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’ ๋ณต์‚ฌ (์ฝ๊ธฐ ์ „์šฉ) - row[`_display_${col.key}`] = item[col.key] || ""; + let displayVal = item[col.key] || ""; + // ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์ด๋ฉด ์ฝ”๋“œโ†’๋ผ๋ฒจ ๋ณ€ํ™˜ (์ ‘๋‘์‚ฌ ๋ฌด๊ด€) + if (typeof displayVal === "string" && categoryLabelMap[displayVal]) { + displayVal = categoryLabelMap[displayVal]; + } + row[`_display_${col.key}`] = displayVal; } else { // ์ž๋™ ์ž…๋ ฅ ๊ฐ’ ์ ์šฉ const autoValue = generateAutoFillValueSync(col, currentRowCount + index, parentFormData); @@ -926,6 +1193,43 @@ export const V2Repeater: React.FC = ({ }), ); + // ์นดํ…Œ๊ณ ๋ฆฌ/fromMainForm ์ปฌ๋Ÿผ์—์„œ ๋ฏธํ•ด๊ฒฐ ์ฝ”๋“œ ์ˆ˜์ง‘ ๋ฐ ๋ณ€ํ™˜ + const categoryColSet = new Set(allCategoryColumns); + const unresolvedCodes = new Set(); + for (const row of newRows) { + for (const col of config.columns) { + const val = row[col.key]; + if (typeof val !== "string" || !val) continue; + const isCategoryCol = categoryColSet.has(col.key); + const isFromMainForm = col.autoFill?.type === "fromMainForm"; + if ((isCategoryCol || isFromMainForm) && !categoryLabelMap[val]) { + unresolvedCodes.add(val); + } + } + } + + if (unresolvedCodes.size > 0) { + try { + const resp = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: Array.from(unresolvedCodes), + }); + if (resp.data?.success && resp.data.data) { + const labelData = resp.data.data as Record; + setCategoryLabelMap((prev) => ({ ...prev, ...labelData })); + for (const row of newRows) { + for (const col of config.columns) { + const val = row[col.key]; + if (typeof val === "string" && labelData[val]) { + row[col.key] = labelData[val]; + } + } + } + } + } catch { + // ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ์ฝ”๋“œ ์œ ์ง€ + } + } + const newData = [...data, ...newRows]; handleDataChange(newData); setModalOpen(false); @@ -939,6 +1243,8 @@ export const V2Repeater: React.FC = ({ generateAutoFillValueSync, generateNumberingCode, parentFormData, + categoryLabelMap, + allCategoryColumns, ], ); @@ -951,9 +1257,6 @@ export const V2Repeater: React.FC = ({ }, [config.columns]); // ๐Ÿ†• beforeFormSave ์ด๋ฒคํŠธ์—์„œ ์ฑ„๋ฒˆ placeholder๋ฅผ ์‹ค์ œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ - const dataRef = useRef(data); - dataRef.current = data; - useEffect(() => { const handleBeforeFormSave = async (event: Event) => { const customEvent = event as CustomEvent; diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 78969fd0..5ad6d0eb 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -480,15 +480,20 @@ export function RepeaterTable({ const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.field === column.field; const value = row[column.field]; - // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋ณ€ํ™˜ ํ•จ์ˆ˜ + // ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋ณ€ํ™˜ ํ•จ์ˆ˜ const getCategoryDisplayValue = (val: any): string => { if (!val || typeof val !== "string") return val || "-"; - // ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์ด ์•„๋‹ˆ๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ - const fieldName = column.field.replace(/^_display_/, ""); // _display_ ์ ‘๋‘์‚ฌ ์ œ๊ฑฐ - if (!categoryColumns.includes(fieldName)) return val; + const fieldName = column.field.replace(/^_display_/, ""); + const isCategoryColumn = categoryColumns.includes(fieldName); - // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ + // categoryLabelMap์— ์ง์ ‘ ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ๋ณ€ํ™˜ (์ ‘๋‘์‚ฌ ๋ฌด๊ด€) + if (categoryLabelMap[val]) return categoryLabelMap[val]; + + // ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์ด ์•„๋‹ˆ๋ฉด ์›๋ž˜ ๊ฐ’ ๋ฐ˜ํ™˜ + if (!isCategoryColumn) return val; + + // ์ฝค๋งˆ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ const codes = val .split(",") .map((c: string) => c.trim()) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 2e8ca106..aee70dd2 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -781,6 +781,7 @@ export const TableListComponent: React.FC = ({ const dataProvider: DataProvidable = { componentId: component.id, componentType: "table-list", + tableName: tableConfig.selectedTable, getSelectedData: () => { // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ์—์„œ ์„ ํƒ๋œ ํ–‰๋งŒ ๋ฐ˜ํ™˜ (์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ์ œ์™ธ) diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index 120022a5..06226c9e 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -554,6 +554,69 @@ export function TableSectionRenderer({ loadCategoryOptions(); }, [tableConfig.source.tableName, tableConfig.columns]); + // receiveFromParent / internal ๋งคํ•‘์œผ๋กœ ๋„˜์–ด์˜ค๋Š” formData ๊ฐ’์˜ ๋ผ๋ฒจ ์‚ฌ์ „ ๋กœ๋“œ + useEffect(() => { + if (!formData || Object.keys(formData).length === 0) return; + if (!tableConfig.columns) return; + + const codesToResolve: string[] = []; + for (const col of tableConfig.columns) { + // receiveFromParent ์ปฌ๋Ÿผ + if ((col as any).receiveFromParent) { + const parentField = (col as any).parentFieldName || col.field; + const val = formData[parentField]; + if (typeof val === "string" && val) { + codesToResolve.push(val); + } + } + // internal ๋งคํ•‘ ์ปฌ๋Ÿผ + const mapping = (col as any).valueMapping; + if (mapping?.type === "internal" && mapping.internalField) { + const val = formData[mapping.internalField]; + if (typeof val === "string" && val) { + codesToResolve.push(val); + } + } + } + + if (codesToResolve.length === 0) return; + + const loadParentLabels = async () => { + try { + const resp = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: codesToResolve, + }); + if (resp.data?.success && resp.data.data) { + const labelData = resp.data.data as Record; + // categoryOptionsMap์— ์ถ”๊ฐ€ (receiveFromParent ์ปฌ๋Ÿผ๋ณ„๋กœ) + const newOptionsMap: Record = {}; + for (const col of tableConfig.columns) { + let val: string | undefined; + if ((col as any).receiveFromParent) { + const parentField = (col as any).parentFieldName || col.field; + val = formData[parentField] as string; + } + const mapping = (col as any).valueMapping; + if (mapping?.type === "internal" && mapping.internalField) { + val = formData[mapping.internalField] as string; + } + if (val && typeof val === "string" && labelData[val]) { + newOptionsMap[col.field] = [{ value: val, label: labelData[val] }]; + } + } + if (Object.keys(newOptionsMap).length > 0) { + setCategoryOptionsMap((prev) => ({ ...prev, ...newOptionsMap })); + } + } + } catch { + // ๋ผ๋ฒจ ์กฐํšŒ ์‹คํŒจ ์‹œ ๋ฌด์‹œ + } + }; + + loadParentLabels(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formData, tableConfig.columns]); + // ์กฐ๊ฑด๋ถ€ ํ…Œ์ด๋ธ”: ๋™์  ์˜ต์…˜ ๋กœ๋“œ (optionSource ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ) useEffect(() => { if (!isConditionalMode) return; @@ -1005,6 +1068,23 @@ export function TableSectionRenderer({ }); }, [tableConfig.columns, dynamicSelectOptionsMap, categoryOptionsMap]); + // categoryOptionsMap์—์„œ RepeaterTable์šฉ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ํŒŒ์ƒ + const tableCategoryColumns = useMemo(() => { + return Object.keys(categoryOptionsMap); + }, [categoryOptionsMap]); + + const tableCategoryLabelMap = useMemo(() => { + const map: Record = {}; + for (const options of Object.values(categoryOptionsMap)) { + for (const opt of options) { + if (opt.value && opt.label) { + map[opt.value] = opt.label; + } + } + } + return map; + }, [categoryOptionsMap]); + // ์›๋ณธ ๊ณ„์‚ฐ ๊ทœ์น™ (์กฐ๊ฑด๋ถ€ ๊ณ„์‚ฐ ํฌํ•จ) const originalCalculationRules: TableCalculationRule[] = useMemo( () => tableConfig.calculations || [], @@ -1312,6 +1392,67 @@ export function TableSectionRenderer({ }), ); + // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์ปฌ๋Ÿผ์˜ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋ณ€ํ™˜ (categoryOptionsMap ํ™œ์šฉ) + const categoryFields = (tableConfig.columns || []) + .filter((col) => col.type === "category" || col.type === "select") + .reduce>>((acc, col) => { + const options = categoryOptionsMap[col.field]; + if (options && options.length > 0) { + acc[col.field] = {}; + for (const opt of options) { + acc[col.field][opt.value] = opt.label; + } + } + return acc; + }, {}); + + // receiveFromParent / internal ๋งคํ•‘์œผ๋กœ ๋„˜์–ด์˜จ ๊ฐ’๋„ ํฌํ•จํ•˜์—ฌ ๋ณ€ํ™˜ + if (Object.keys(categoryFields).length > 0) { + for (const item of mappedItems) { + for (const [field, codeToLabel] of Object.entries(categoryFields)) { + const val = item[field]; + if (typeof val === "string" && codeToLabel[val]) { + item[field] = codeToLabel[val]; + } + } + } + } + + // categoryOptionsMap์— ์—†๋Š” ๊ฒฝ์šฐ API fallback + const unresolvedCodes = new Set(); + const categoryColFields = new Set( + (tableConfig.columns || []).filter((col) => col.type === "category").map((col) => col.field), + ); + for (const item of mappedItems) { + for (const field of categoryColFields) { + const val = item[field]; + if (typeof val === "string" && val && !categoryFields[field]?.[val] && val !== item[field]) { + unresolvedCodes.add(val); + } + } + } + + if (unresolvedCodes.size > 0) { + try { + const labelResp = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: Array.from(unresolvedCodes), + }); + if (labelResp.data?.success && labelResp.data.data) { + const labelData = labelResp.data.data as Record; + for (const item of mappedItems) { + for (const field of categoryColFields) { + const val = item[field]; + if (typeof val === "string" && labelData[val]) { + item[field] = labelData[val]; + } + } + } + } + } catch { + // ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ์ฝ”๋“œ ์œ ์ง€ + } + } + // ๊ณ„์‚ฐ ํ•„๋“œ ์—…๋ฐ์ดํŠธ const calculatedItems = calculateAll(mappedItems); @@ -1319,7 +1460,7 @@ export function TableSectionRenderer({ const newData = [...tableData, ...calculatedItems]; handleDataChange(newData); }, - [tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources], + [tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources, categoryOptionsMap], ); // ์ปฌ๋Ÿผ ๋ชจ๋“œ/์กฐํšŒ ์˜ต์…˜ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ @@ -1667,6 +1808,31 @@ export function TableSectionRenderer({ }), ); + // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์ปฌ๋Ÿผ์˜ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋ณ€ํ™˜ (categoryOptionsMap ํ™œ์šฉ) + const categoryFields = (tableConfig.columns || []) + .filter((col) => col.type === "category" || col.type === "select") + .reduce>>((acc, col) => { + const options = categoryOptionsMap[col.field]; + if (options && options.length > 0) { + acc[col.field] = {}; + for (const opt of options) { + acc[col.field][opt.value] = opt.label; + } + } + return acc; + }, {}); + + if (Object.keys(categoryFields).length > 0) { + for (const item of mappedItems) { + for (const [field, codeToLabel] of Object.entries(categoryFields)) { + const val = item[field]; + if (typeof val === "string" && codeToLabel[val]) { + item[field] = codeToLabel[val]; + } + } + } + } + // ํ˜„์žฌ ์กฐ๊ฑด์˜ ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ const currentData = conditionalTableData[modalCondition] || []; const newData = [...currentData, ...mappedItems]; @@ -1964,6 +2130,8 @@ export function TableSectionRenderer({ [conditionValue]: newSelected, })); }} + categoryColumns={tableCategoryColumns} + categoryLabelMap={tableCategoryLabelMap} equalizeWidthsTrigger={widthTrigger} /> @@ -2055,6 +2223,8 @@ export function TableSectionRenderer({ })); }} equalizeWidthsTrigger={widthTrigger} + categoryColumns={tableCategoryColumns} + categoryLabelMap={tableCategoryLabelMap} /> ); @@ -2185,6 +2355,8 @@ export function TableSectionRenderer({ selectedRows={selectedRows} onSelectionChange={setSelectedRows} equalizeWidthsTrigger={widthTrigger} + categoryColumns={tableCategoryColumns} + categoryLabelMap={tableCategoryLabelMap} /> {/* ํ•ญ๋ชฉ ์„ ํƒ ๋ชจ๋‹ฌ */} diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index a937f5b2..c6673d8d 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -393,7 +393,7 @@ export interface TableModalFilter { export interface TableColumnConfig { field: string; // ํ•„๋“œ๋ช… (์ €์žฅํ•  ์ปฌ๋Ÿผ๋ช…) label: string; // ์ปฌ๋Ÿผ ํ—ค๋” ๋ผ๋ฒจ - type: "text" | "number" | "date" | "select"; // ์ž…๋ ฅ ํƒ€์ž… + type: "text" | "number" | "date" | "select" | "category"; // ์ž…๋ ฅ ํƒ€์ž… // ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ (๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ๋ช…) sourceField?: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช… (๋ฏธ์„ค์ • ์‹œ field์™€ ๋™์ผ) diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index 9505d3dd..371814b5 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -897,11 +897,30 @@ export const ButtonPrimaryComponent: React.FC = ({ } } - // 4. ๋งคํ•‘ ๊ทœ์น™ ์ ์šฉ + ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ - const mappedData = sourceData.map((row) => { - const mappedRow = applyMappingRules(row, dataTransferConfig.mappingRules || []); + // 4. ๋งคํ•‘ ๊ทœ์น™ ๊ฒฐ์ •: ๋ฉ€ํ‹ฐ ํ…Œ์ด๋ธ” ๋งคํ•‘ ๋˜๋Š” ๋ ˆ๊ฑฐ์‹œ ๋‹จ์ผ ๋งคํ•‘ + let effectiveMappingRules: any[] = dataTransferConfig.mappingRules || []; + + const sourceTableName = sourceProvider?.tableName; + const multiTableMappings: Array<{ sourceTable: string; mappingRules: any[] }> = + dataTransferConfig.multiTableMappings || []; + + if (multiTableMappings.length > 0 && sourceTableName) { + const matchedGroup = multiTableMappings.find((g) => g.sourceTable === sourceTableName); + if (matchedGroup) { + effectiveMappingRules = matchedGroup.mappingRules || []; + console.log(`โœ… [ButtonPrimary] ๋ฉ€ํ‹ฐ ํ…Œ์ด๋ธ” ๋งคํ•‘ ์ ์šฉ: ${sourceTableName}`, effectiveMappingRules); + } else { + console.log(`โš ๏ธ [ButtonPrimary] ์†Œ์Šค ํ…Œ์ด๋ธ” ${sourceTableName}์— ๋Œ€ํ•œ ๋งคํ•‘ ์—†์Œ, ๋™์ผ ํ•„๋“œ๋ช… ์ž๋™ ๋งคํ•‘`); + effectiveMappingRules = []; + } + } else if (multiTableMappings.length > 0 && !sourceTableName) { + console.log("โš ๏ธ [ButtonPrimary] ์†Œ์Šค ํ…Œ์ด๋ธ” ๋ฏธ๊ฐ์ง€, ์ฒซ ๋ฒˆ์งธ ๋งคํ•‘ ๊ทธ๋ฃน ์‚ฌ์šฉ"); + effectiveMappingRules = multiTableMappings[0]?.mappingRules || []; + } + + const mappedData = sourceData.map((row) => { + const mappedRow = applyMappingRules(row, effectiveMappingRules); - // ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋“  ํ–‰์— ํฌํ•จ return { ...mappedRow, ...additionalData, diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 22c2a6f4..1eaef469 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -654,7 +654,7 @@ export const TableListComponent: React.FC = ({ const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); const [displayColumns, setDisplayColumns] = useState([]); const [columnMeta, setColumnMeta] = useState< - Record + Record >({}); // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… โ†’ inputType) const [joinedColumnMeta, setJoinedColumnMeta] = useState< @@ -865,6 +865,7 @@ export const TableListComponent: React.FC = ({ const dataProvider: DataProvidable = { componentId: component.id, componentType: "table-list", + tableName: tableConfig.selectedTable, getSelectedData: () => { // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ์—์„œ ์„ ํƒ๋œ ํ–‰๋งŒ ๋ฐ˜ํ™˜ (์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ์ œ์™ธ) @@ -1233,13 +1234,16 @@ export const TableListComponent: React.FC = ({ const cached = tableColumnCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) { const labels: Record = {}; - const meta: Record = {}; + const meta: Record = {}; - // ์บ์‹œ๋œ inputTypes ๋งต ์ƒ์„ฑ const inputTypeMap: Record = {}; + const categoryRefMap: Record = {}; if (cached.inputTypes) { cached.inputTypes.forEach((col: any) => { inputTypeMap[col.columnName] = col.inputType; + if (col.categoryRef) { + categoryRefMap[col.columnName] = col.categoryRef; + } }); } @@ -1248,7 +1252,8 @@ export const TableListComponent: React.FC = ({ meta[col.columnName] = { webType: col.webType, codeCategory: col.codeCategory, - inputType: inputTypeMap[col.columnName], // ์บ์‹œ๋œ inputType ์‚ฌ์šฉ! + inputType: inputTypeMap[col.columnName], + categoryRef: categoryRefMap[col.columnName], }; }); @@ -1259,11 +1264,14 @@ export const TableListComponent: React.FC = ({ const columns = await tableTypeApi.getColumns(tableConfig.selectedTable); - // ์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ const inputTypes = await tableTypeApi.getColumnInputTypes(tableConfig.selectedTable); const inputTypeMap: Record = {}; + const categoryRefMap: Record = {}; inputTypes.forEach((col: any) => { inputTypeMap[col.columnName] = col.inputType; + if (col.categoryRef) { + categoryRefMap[col.columnName] = col.categoryRef; + } }); tableColumnCache.set(cacheKey, { @@ -1273,7 +1281,7 @@ export const TableListComponent: React.FC = ({ }); const labels: Record = {}; - const meta: Record = {}; + const meta: Record = {}; columns.forEach((col: any) => { labels[col.columnName] = col.displayName || col.comment || col.columnName; @@ -1281,6 +1289,7 @@ export const TableListComponent: React.FC = ({ webType: col.webType, codeCategory: col.codeCategory, inputType: inputTypeMap[col.columnName], + categoryRef: categoryRefMap[col.columnName], }; }); @@ -1355,14 +1364,22 @@ export const TableListComponent: React.FC = ({ for (const columnName of categoryColumns) { try { - // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ: "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ let targetTable = tableConfig.selectedTable; let targetColumn = columnName; - if (columnName.includes(".")) { + // category_ref๊ฐ€ ์žˆ์œผ๋ฉด ์ฐธ์กฐ ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ์กฐํšŒ + const meta = columnMeta[columnName]; + if (meta?.categoryRef) { + const refParts = meta.categoryRef.split("."); + if (refParts.length === 2) { + targetTable = refParts[0]; + targetColumn = refParts[1]; + } + } else if (columnName.includes(".")) { + // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ: "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ const parts = columnName.split("."); - targetTable = parts[0]; // ์กฐ์ธ๋œ ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: item_info) - targetColumn = parts[1]; // ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… (์˜ˆ: material) + targetTable = parts[0]; + targetColumn = parts[1]; } const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`); @@ -1563,7 +1580,8 @@ export const TableListComponent: React.FC = ({ categoryColumns.length, JSON.stringify(categoryColumns), JSON.stringify(tableConfig.columns), - ]); // ๋” ๋ช…ํ™•ํ•œ ์˜์กด์„ฑ + columnMeta, + ]); // ======================================== // ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 7f094517..7dc5e573 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -559,6 +559,7 @@ export class ButtonActionExecutor { } // ๐Ÿ†• EditModal ๋“ฑ์—์„œ ์ „๋‹ฌ๋œ onSave ์ฝœ๋ฐฑ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + // EditModal์ด ๋‚ด๋ถ€์—์„œ ์ง์ ‘ repeaterSave ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ  ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆผ if (onSave) { try { await onSave(); @@ -626,6 +627,7 @@ export class ButtonActionExecutor { // ๐Ÿ†• EditModal ๋“ฑ์—์„œ ์ „๋‹ฌ๋œ onSave ์ฝœ๋ฐฑ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ // ๋‹จ, _tableSection_ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๊ฑด๋„ˆ๋›ฐ๊ธฐ (handleUniversalFormModalTableSectionSave๊ฐ€ ์ฒ˜๋ฆฌ) + // EditModal์ด ๋‚ด๋ถ€์—์„œ ์ง์ ‘ repeaterSave ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ  ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆผ if (onSave && !hasTableSectionData) { try { await onSave(); @@ -1494,13 +1496,24 @@ export class ButtonActionExecutor { // @ts-ignore - window์— ๋™์  ์†์„ฑ ์‚ฌ์šฉ const v2RepeaterTables = Array.from(window.__v2RepeaterInstances || []); + // V2Repeater๊ฐ€ ๋™์ผ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜๋Š”์ง€ allComponents๋กœ ๊ฐ์ง€ + // (useCustomTable ๋ฏธ์„ค์ • = ํ™”๋ฉด ํ…Œ์ด๋ธ”์— ์ง์ ‘ ์ €์žฅํ•˜๋Š” ๋ฆฌํ”ผํ„ฐ) + const hasRepeaterOnSameTable = context.allComponents?.some((c: any) => { + const compType = c.componentType || c.overrides?.type; + if (compType !== "v2-repeater") return false; + const compConfig = c.componentConfig || c.overrides || {}; + return !compConfig.useCustomTable; + }) || false; + // ๋ฉ”์ธ ์ €์žฅ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ์กฐ๊ฑด: // 1. RepeatScreenModal ๋˜๋Š” RepeaterFieldGroup์—์„œ ๊ฐ™์€ ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ // 2. V2Repeater๊ฐ€ ๊ฐ™์€ ํ…Œ์ด๋ธ”์— ์กด์žฌ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์— ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ๋˜์–ด ์ €์žฅ๋จ) + // 3. allComponents์—์„œ useCustomTable ๋ฏธ์„ค์ • V2Repeater ๊ฐ์ง€ (๊ธ€๋กœ๋ฒŒ ๋“ฑ๋ก ์—†๋Š” ๊ฒฝ์šฐ) const shouldSkipMainSave = repeatScreenModalTables.includes(tableName) || repeaterFieldGroupTables.includes(tableName) || - v2RepeaterTables.includes(tableName); + v2RepeaterTables.includes(tableName) || + hasRepeaterOnSameTable; if (shouldSkipMainSave) { saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal/V2Repeater์—์„œ ์ฒ˜๋ฆฌ" }; @@ -1779,16 +1792,7 @@ export class ButtonActionExecutor { throw new Error("์ €์žฅ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ํ™”๋ฉดID ๋ˆ„๋ฝ)"); } - // ํ…Œ์ด๋ธ”๊ณผ ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ (๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์ „์— ์‹คํ–‰) - context.onRefresh?.(); - context.onFlowRefresh?.(); - - // ์ €์žฅ ์„ฑ๊ณต ํ›„ ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal ๋‹ซ๊ธฐ - window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal ์—ฐ์† ๋“ฑ๋ก ๋ชจ๋“œ ์ฒ˜๋ฆฌ - - // V2Repeater ์ €์žฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ + ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ ์ €์žฅ) - // ๐Ÿ”ง formData๋ฅผ ๋ฆฌํ”ผํ„ฐ์— ์ „๋‹ฌํ•˜์—ฌ ๊ฐ ํ–‰์— ๋ณ‘ํ•ฉ ์ €์žฅ + // V2Repeater ์ €์žฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์ „์— ์‹คํ–‰ํ•ด์•ผ V2Repeater๊ฐ€ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Œ) const savedId = saveResult?.data?.id || saveResult?.data?.data?.id || formData.id || context.formData?.id; // _deferSave ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์ˆœ์ฐจ ์ €์žฅ: ๋ ˆ๋ฒจ๋ณ„ ์ €์žฅ + tempโ†’real ID ๋งคํ•‘) @@ -1866,17 +1870,45 @@ export class ButtonActionExecutor { } } + console.log("๐ŸŸข [buttonActions] repeaterSave ์ด๋ฒคํŠธ ๋ฐœํ–‰:", { + parentId: savedId, + tableName: context.tableName, + masterRecordId: savedId, + mainFormDataKeys: Object.keys(mainFormData), + }); + + // V2Repeater ์ €์žฅ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์œ„ํ•œ Promise + const repeaterSavePromise = new Promise((resolve) => { + const fallbackTimeout = setTimeout(resolve, 5000); + const handler = () => { + clearTimeout(fallbackTimeout); + window.removeEventListener("repeaterSaveComplete", handler); + resolve(); + }; + window.addEventListener("repeaterSaveComplete", handler); + }); + window.dispatchEvent( new CustomEvent("repeaterSave", { detail: { parentId: savedId, tableName: context.tableName, - mainFormData, // ๐Ÿ†• ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ - masterRecordId: savedId, // ๐Ÿ†• ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID (FK ์ž๋™ ์—ฐ๊ฒฐ์šฉ) + mainFormData, + masterRecordId: savedId, }, }), ); + await repeaterSavePromise; + + // ํ…Œ์ด๋ธ”๊ณผ ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ (๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์ „์— ์‹คํ–‰) + context.onRefresh?.(); + context.onFlowRefresh?.(); + + // ์ €์žฅ ์„ฑ๊ณต ํ›„ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new CustomEvent("closeEditModal")); + window.dispatchEvent(new CustomEvent("saveSuccessInModal")); + return true; } catch (error) { console.error("์ €์žฅ ์˜ค๋ฅ˜:", error); @@ -1884,6 +1916,50 @@ export class ButtonActionExecutor { } } + /** + * V2Repeater ๋””ํ…Œ์ผ ๋ฐ์ดํ„ฐ ์ €์žฅ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (onSave ์ฝœ๋ฐฑ ๊ฒฝ๋กœ์—์„œ๋„ ์‚ฌ์šฉ) + */ + private static async dispatchRepeaterSave(context: ButtonActionContext): Promise { + const formData = context.formData || {}; + const savedId = formData.id; + + if (!savedId) { + console.log("โš ๏ธ [dispatchRepeaterSave] savedId(formData.id) ์—†์Œ - ์Šคํ‚ต"); + return; + } + + console.log("๐ŸŸข [dispatchRepeaterSave] repeaterSave ์ด๋ฒคํŠธ ๋ฐœํ–‰:", { + parentId: savedId, + tableName: context.tableName, + masterRecordId: savedId, + formDataKeys: Object.keys(formData), + }); + + const repeaterSavePromise = new Promise((resolve) => { + const fallbackTimeout = setTimeout(resolve, 5000); + const handler = () => { + clearTimeout(fallbackTimeout); + window.removeEventListener("repeaterSaveComplete", handler); + resolve(); + }; + window.addEventListener("repeaterSaveComplete", handler); + }); + + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: savedId, + tableName: context.tableName, + mainFormData: formData, + masterRecordId: savedId, + }, + }), + ); + + await repeaterSavePromise; + console.log("โœ… [dispatchRepeaterSave] repeaterSave ์™„๋ฃŒ"); + } + /** * DB์—์„œ ์กฐํšŒํ•œ ์‹ค์ œ ๊ธฐ๋ณธํ‚ค๋กœ formData์—์„œ ๊ฐ’ ์ถ”์ถœ * @param formData ํผ ๋ฐ์ดํ„ฐ diff --git a/frontend/types/data-transfer.ts b/frontend/types/data-transfer.ts index cdb5f55f..61aad8db 100644 --- a/frontend/types/data-transfer.ts +++ b/frontend/types/data-transfer.ts @@ -57,6 +57,15 @@ export interface MappingRule { required?: boolean; // ํ•„์ˆ˜ ์—ฌ๋ถ€ } +/** + * ๋ฉ€ํ‹ฐ ํ…Œ์ด๋ธ” ๋งคํ•‘ ๊ทธ๋ฃน + * ์†Œ์Šค ํ…Œ์ด๋ธ”๋ณ„๋กœ ๋ณ„๋„์˜ ๋งคํ•‘ ๊ทœ์น™์„ ์ •์˜ + */ +export interface MultiTableMappingGroup { + sourceTable: string; + mappingRules: MappingRule[]; +} + /** * ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ์„ค์ • * ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ์˜ ์„ค์ • @@ -155,6 +164,7 @@ export interface DataReceivable { export interface DataProvidable { componentId: string; componentType: string; + tableName?: string; /** * ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ