"use client"; import React, { useEffect, useRef, useCallback, useMemo, useState } from "react"; import { Layers } from "lucide-react"; import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; import { ComponentDefinition, ComponentCategory, ComponentRendererProps } from "@/types/component"; import { RepeaterInput } from "@/components/webtypes/RepeaterInput"; import { RepeaterConfigPanel } from "@/components/webtypes/config/RepeaterConfigPanel"; import { useScreenContextOptional, DataReceivable } from "@/contexts/ScreenContext"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; import { applyMappingRules } from "@/lib/utils/dataMapping"; import { toast } from "sonner"; import { apiClient } from "@/lib/api/client"; /** * Repeater Field Group ์ปดํฌ๋„ŒํŠธ */ const RepeaterFieldGroupComponent: React.FC = (props) => { const { component, value, onChange, readonly, disabled, formData, onFormDataChange, menuObjid } = props; const screenContext = useScreenContextOptional(); const splitPanelContext = useSplitPanelContext(); const receiverRef = useRef(null); // ๐Ÿ†• ๊ทธ๋ฃนํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์ƒํƒœ const [groupedData, setGroupedData] = useState(null); const [isLoadingGroupData, setIsLoadingGroupData] = useState(false); const groupDataLoadedRef = useRef(false); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ID ๋ชฉ๋ก (์‚ญ์ œ ์ถ”์ ์šฉ) const [originalItemIds, setOriginalItemIds] = useState([]); // ๐Ÿ†• DB์—์„œ ๋กœ๋“œํ•œ ์ปฌ๋Ÿผ ์ •๋ณด (webType ๋“ฑ) const [columnInfo, setColumnInfo] = useState>({}); // ์ปดํฌ๋„ŒํŠธ์˜ ํ•„๋“œ๋ช… (formData ํ‚ค) const fieldName = (component as any).columnName || component.id; // repeaterConfig ๋˜๋Š” componentConfig์—์„œ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ const rawConfig = (component as any).repeaterConfig || component.componentConfig || { fields: [] }; // ๐Ÿ†• ๊ทธ๋ฃนํ™” ์„ค์ • (์˜ˆ: groupByColumn: "inbound_number") const groupByColumn = rawConfig.groupByColumn; const targetTable = rawConfig.targetTable; // ๐Ÿ†• DB ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ์ ์šฉํ•œ config ์ƒ์„ฑ (webType โ†’ type ๋งคํ•‘) const config = useMemo(() => { const rawFields = rawConfig.fields || []; console.log("๐Ÿ“‹ [RepeaterFieldGroup] config ์ƒ์„ฑ:", { rawFieldsCount: rawFields.length, rawFieldNames: rawFields.map((f: any) => f.name), columnInfoKeys: Object.keys(columnInfo), hasColumnInfo: Object.keys(columnInfo).length > 0, }); const fields = rawFields.map((field: any) => { const colInfo = columnInfo[field.name]; // DB์˜ webType ๋˜๋Š” web_type์„ field.type์œผ๋กœ ์ ์šฉ const dbWebType = colInfo?.webType || colInfo?.web_type; // ํƒ€์ž… ์˜ค๋ฒ„๋ผ์ด๋“œ ์กฐ๊ฑด: // 1. field.type์ด ์—†๊ฑฐ๋‚˜ // 2. field.type์ด 'direct'(๊ธฐ๋ณธ๊ฐ’)์ด๊ณ  DB์— ๋” ๊ตฌ์ฒด์ ์ธ ํƒ€์ž…์ด ์žˆ๋Š” ๊ฒฝ์šฐ const shouldOverride = !field.type || (field.type === "direct" && dbWebType && dbWebType !== "text"); if (colInfo && dbWebType && shouldOverride) { console.log(`โœ… [RepeaterFieldGroup] ํ•„๋“œ ํƒ€์ž… ๋งคํ•‘: ${field.name} โ†’ ${dbWebType}`); return { ...field, type: dbWebType }; } return field; }); return { ...rawConfig, fields }; }, [rawConfig, columnInfo]); // formData์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (value prop๋ณด๋‹ค ์šฐ์„ ) const rawValue = formData?.[fieldName] ?? value; // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ ๊ฐ์ง€: formData์— id๊ฐ€ ์žˆ๊ณ , fieldName์œผ๋กœ ๊ฐ’์„ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ // formData ์ž์ฒด๋ฅผ ๋ฐฐ์—ด์˜ ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์œผ๋กœ ์‚ฌ์šฉ (๋‹จ์ผ ํ–‰ ์ˆ˜์ • ์‹œ) const isEditMode = formData?.id && !rawValue && !value; // ๐Ÿ†• ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน์˜ ํ•„๋“œ๋“ค์ด formData์— ์žˆ๋Š”์ง€ ํ™•์ธ const configFields = config.fields || []; const hasRepeaterFieldsInFormData = configFields.length > 0 && configFields.some((field: any) => formData?.[field.name] !== undefined); // ๐Ÿ†• formData์™€ config.fields์˜ ํ•„๋“œ ์ด๋ฆ„ ๋งค์นญ ํ™•์ธ const matchingFields = configFields.filter((field: any) => formData?.[field.name] !== undefined); // ๐Ÿ†• ๊ทธ๋ฃน ํ‚ค ๊ฐ’ (์˜ˆ: formData.inbound_number) const groupKeyValue = groupByColumn ? formData?.[groupByColumn] : null; // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ์œ„์น˜ ๋ฐ ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ํ™•์ธ const splitPanelPosition = screenContext?.splitPanelPosition; const isRightPanel = splitPanelPosition === "right"; const selectedLeftData = splitPanelContext?.selectedLeftData; // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ •์—์„œ FK ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ // screen-split-panel์—์„œ ์„ค์ •ํ•œ linkedFilters ์‚ฌ์šฉ const linkedFilters = splitPanelContext?.linkedFilters || []; const getLinkedFilterValues = splitPanelContext?.getLinkedFilterValues; // ๐Ÿ†• FK ์ปฌ๋Ÿผ ์„ค์ • ์šฐ์„ ์ˆœ์œ„: // 1. linkedFilters์—์„œ targetTable์— ํ•ด๋‹นํ•˜๋Š” ์„ค์ • ์ฐพ๊ธฐ // 2. config.fkColumn (์ปดํฌ๋„ŒํŠธ ์„ค์ •) // 3. config.groupByColumn (๊ทธ๋ฃนํ™” ์ปฌ๋Ÿผ) let fkSourceColumn: string | null = null; let fkTargetColumn: string | null = null; let linkedFilterTargetTable: string | null = null; // linkedFilters์—์„œ FK ์ปฌ๋Ÿผ ์ฐพ๊ธฐ if (linkedFilters.length > 0 && selectedLeftData) { // ์ฒซ ๋ฒˆ์งธ linkedFilter ์‚ฌ์šฉ (์ผ๋ฐ˜์ ์œผ๋กœ ํ•˜๋‚˜๋งŒ ์„ค์ •๋จ) const linkedFilter = linkedFilters[0]; fkSourceColumn = linkedFilter.sourceColumn; // targetColumn์ด "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•์‹์ผ ์ˆ˜ ์žˆ์Œ โ†’ ๋ถ„๋ฆฌ // ์˜ˆ: "dtg_maintenance_history.serial_no" โ†’ table: "dtg_maintenance_history", column: "serial_no" const targetColumnParts = linkedFilter.targetColumn.split("."); if (targetColumnParts.length === 2) { linkedFilterTargetTable = targetColumnParts[0]; fkTargetColumn = targetColumnParts[1]; } else { fkTargetColumn = linkedFilter.targetColumn; } } // ๐Ÿ†• targetTable ์šฐ์„ ์ˆœ์œ„: config.targetTable > linkedFilters์—์„œ ์ถ”์ถœํ•œ ํ…Œ์ด๋ธ” const effectiveTargetTable = targetTable || linkedFilterTargetTable; // ๐Ÿ†• DB์—์„œ ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ (webType ๋“ฑ) useEffect(() => { const loadColumnInfo = async () => { if (!effectiveTargetTable) return; try { const response = await apiClient.get(`/table-management/tables/${effectiveTargetTable}/columns`); console.log("๐Ÿ“‹ [RepeaterFieldGroup] ์ปฌ๋Ÿผ ์ •๋ณด ์‘๋‹ต:", response.data); // ์‘๋‹ต ๊ตฌ์กฐ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ์ถ”์ถœ // ์‹ค์ œ ์‘๋‹ต: { success: true, data: { columns: [...], page, size, total, totalPages } } let columns: any[] = []; if (response.data?.success && response.data?.data) { // data.columns๊ฐ€ ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ (์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ) if (Array.isArray(response.data.data.columns)) { columns = response.data.data.columns; } // data๊ฐ€ ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ else if (Array.isArray(response.data.data)) { columns = response.data.data; } // data ์ž์ฒด๊ฐ€ ๊ฐ์ฒด์ด๊ณ  ๋ฐฐ์—ด์ด ์•„๋‹Œ ๊ฒฝ์šฐ (ํ‚ค-๊ฐ’ ํ˜•ํƒœ) else if (typeof response.data.data === "object") { columns = Object.values(response.data.data); } } // success ์—†์ด ๋ฐ”๋กœ ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ else if (Array.isArray(response.data)) { columns = response.data; } console.log("๐Ÿ“‹ [RepeaterFieldGroup] ํŒŒ์‹ฑ๋œ ์ปฌ๋Ÿผ ๋ฐฐ์—ด:", columns.length, "๊ฐœ"); if (columns.length > 0) { const colMap: Record = {}; columns.forEach((col: any) => { // columnName ๋˜๋Š” column_name ๋˜๋Š” name ํ‚ค ์‚ฌ์šฉ const colName = col.columnName || col.column_name || col.name; if (colName) { colMap[colName] = col; } }); setColumnInfo(colMap); console.log("๐Ÿ“‹ [RepeaterFieldGroup] ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ ์™„๋ฃŒ:", { table: effectiveTargetTable, columns: Object.keys(colMap), webTypes: Object.entries(colMap).map( ([name, info]: [string, any]) => `${name}: ${info.webType || info.web_type || "unknown"}`, ), }); } } catch (error) { console.error("โŒ [RepeaterFieldGroup] ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:", error); } }; loadColumnInfo(); }, [effectiveTargetTable]); // linkedFilters๊ฐ€ ์—†์œผ๋ฉด config์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ const fkColumn = fkTargetColumn || config.fkColumn || config.groupByColumn; const fkValue = fkSourceColumn && selectedLeftData ? selectedLeftData[fkSourceColumn] : fkColumn && selectedLeftData ? selectedLeftData[fkColumn] : null; console.log("๐Ÿ”„ [RepeaterFieldGroup] ๋ Œ๋”๋ง:", { fieldName, hasFormData: !!formData, formDataId: formData?.id, formDataValue: formData?.[fieldName], propsValue: value, rawValue, isEditMode, hasRepeaterFieldsInFormData, configFieldNames: configFields.map((f: any) => f.name), formDataKeys: formData ? Object.keys(formData) : [], matchingFieldNames: matchingFields.map((f: any) => f.name), groupByColumn, groupKeyValue, targetTable, linkedFilterTargetTable, effectiveTargetTable, hasGroupedData: groupedData !== null, groupedDataLength: groupedData?.length, // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๊ด€๋ จ ์ •๋ณด linkedFiltersCount: linkedFilters.length, linkedFilters: linkedFilters.map((f) => `${f.sourceColumn} โ†’ ${f.targetColumn}`), fkSourceColumn, fkTargetColumn, splitPanelPosition, isRightPanel, hasSelectedLeftData: !!selectedLeftData, // ๐Ÿ†• selectedLeftData ์ƒ์„ธ ์ •๋ณด (๋””๋ฒ„๊น…์šฉ) selectedLeftDataId: selectedLeftData?.id, selectedLeftDataFkValue: fkSourceColumn ? selectedLeftData?.[fkSourceColumn] : "N/A", selectedLeftData: selectedLeftData ? JSON.stringify(selectedLeftData).slice(0, 200) : null, fkColumn, fkValue, }); // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ์—์„œ ๊ทธ๋ฃนํ™”๋œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ useEffect(() => { const loadGroupedData = async () => { // ์ด๋ฏธ ๋กœ๋“œํ–ˆ๊ฑฐ๋‚˜ ์กฐ๊ฑด์ด ๋งž์ง€ ์•Š์œผ๋ฉด ์Šคํ‚ต if (groupDataLoadedRef.current) return; if (!isEditMode || !groupByColumn || !groupKeyValue || !targetTable) return; console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ์ž‘:", { groupByColumn, groupKeyValue, targetTable, }); setIsLoadingGroupData(true); groupDataLoadedRef.current = true; try { // API ํ˜ธ์ถœ: ๊ฐ™์€ ๊ทธ๋ฃน ํ‚ค๋ฅผ ๊ฐ€์ง„ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ // search ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ (filters๊ฐ€ ์•„๋‹Œ search) const response = await apiClient.post(`/table-management/tables/${targetTable}/data`, { page: 1, size: 100, // ์ถฉ๋ถ„ํžˆ ํฐ ๊ฐ’ search: { [groupByColumn]: groupKeyValue }, }); console.log("๐Ÿ” [RepeaterFieldGroup] API ์‘๋‹ต ๊ตฌ์กฐ:", { success: response.data?.success, hasData: !!response.data?.data, dataType: typeof response.data?.data, dataKeys: response.data?.data ? Object.keys(response.data.data) : [], }); // ์‘๋‹ต ๊ตฌ์กฐ: { success, data: { data: [...], total, page, totalPages } } if (response.data?.success && response.data?.data?.data) { const items = response.data.data.data; // ์‹ค์ œ ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด console.log("โœ… [RepeaterFieldGroup] ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ:", { count: items.length, groupByColumn, groupKeyValue, firstItem: items[0], }); setGroupedData(items); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ID ๋ชฉ๋ก ์ €์žฅ (์‚ญ์ œ ์ถ”์ ์šฉ) 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) { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: targetTable, _originalItemIds: itemIds, // ๐Ÿ†• ์›๋ณธ ID ๋ชฉ๋ก๋„ ํ•จ๊ป˜ ์ „๋‹ฌ _existingRecord: !!item.id, // ๐Ÿ†• ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ ํ”Œ๋ž˜๊ทธ (id๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ) _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก })); onChange(dataWithMeta); } } else { console.warn("โš ๏ธ [RepeaterFieldGroup] ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", response.data); setGroupedData([]); } } catch (error) { console.error("โŒ [RepeaterFieldGroup] ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error); setGroupedData([]); } finally { setIsLoadingGroupData(false); } }; loadGroupedData(); }, [isEditMode, groupByColumn, groupKeyValue, targetTable, onChange]); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์„ ํƒ ์‹œ FK ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ // ์ขŒ์ธก ํ…Œ์ด๋ธ”์˜ serial_no ๋“ฑ์„ ๊ธฐ์ค€์œผ๋กœ ์šฐ์ธก repeater ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง const prevFkValueRef = useRef(null); useEffect(() => { const loadDataByFK = async () => { // ์šฐ์ธก ํŒจ๋„์ด ์•„๋‹ˆ๋ฉด ์Šคํ‚ต if (!isRightPanel) { return; } // ๐Ÿ†• fkValue๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋นˆ ๊ฐ’์ด๋ฉด ๋นˆ ์ƒํƒœ๋กœ ์ดˆ๊ธฐํ™” if (!fkValue || fkValue === "" || fkValue === null || fkValue === undefined) { console.log("๐Ÿ”„ [RepeaterFieldGroup] FK ๊ฐ’ ์—†์Œ - ๋นˆ ์ƒํƒœ๋กœ ์ดˆ๊ธฐํ™”:", { fkColumn, fkValue, prevFkValue: prevFkValueRef.current, }); // ์ด์ „์— ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์—ˆ๋‹ค๋ฉด ์ดˆ๊ธฐํ™” if (prevFkValueRef.current !== null) { setGroupedData([]); setOriginalItemIds([]); onChange?.([]); prevFkValueRef.current = null; } return; } // FK ์ปฌ๋Ÿผ์ด๋‚˜ ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์ด ์—†์œผ๋ฉด ์Šคํ‚ต if (!fkColumn || !effectiveTargetTable) { console.log("โญ๏ธ [RepeaterFieldGroup] FK ๊ธฐ๋ฐ˜ ๋กœ๋“œ ์Šคํ‚ต (์„ค์ • ๋ถ€์กฑ):", { fkColumn, effectiveTargetTable, }); return; } // ๊ฐ™์€ FK ๊ฐ’์œผ๋กœ ์ด๋ฏธ ๋กœ๋“œํ–ˆ์œผ๋ฉด ์Šคํ‚ต const currentFkValueStr = String(fkValue); if (prevFkValueRef.current === currentFkValueStr) { console.log("โญ๏ธ [RepeaterFieldGroup] ๊ฐ™์€ FK ๊ฐ’ - ์Šคํ‚ต:", currentFkValueStr); return; } prevFkValueRef.current = currentFkValueStr; console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ๋ถ„ํ•  ํŒจ๋„ FK ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ:", { fkColumn, fkValue, effectiveTargetTable, }); setIsLoadingGroupData(true); try { // API ํ˜ธ์ถœ: FK ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ const response = await apiClient.post(`/table-management/tables/${effectiveTargetTable}/data`, { page: 1, size: 100, search: { [fkColumn]: fkValue }, }); if (response.data?.success) { const items = response.data?.data?.data || []; console.log("โœ… [RepeaterFieldGroup] FK ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ:", { count: items.length, fkColumn, fkValue, effectiveTargetTable, }); // ๐Ÿ†• ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋“  ์—†๋“  ํ•ญ์ƒ ์ƒํƒœ ์—…๋ฐ์ดํŠธ (๋นˆ ๋ฐฐ์—ด๋„ ๋ช…ํ™•ํžˆ ์„ค์ •) setGroupedData(items); // ์›๋ณธ ๋ฐ์ดํ„ฐ ID ๋ชฉ๋ก ์ €์žฅ const itemIds = items.map((item: any) => String(item.id)).filter(Boolean); setOriginalItemIds(itemIds); // onChange ํ˜ธ์ถœ (effectiveTargetTable ์‚ฌ์šฉ) if (onChange) { if (items.length > 0) { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: effectiveTargetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก })); onChange(dataWithMeta); } else { // ๐Ÿ†• ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด ์ „๋‹ฌ (์ด์ „ ๋ฐ์ดํ„ฐ ํด๋ฆฌ์–ด) console.log("โ„น๏ธ [RepeaterFieldGroup] FK ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ์—†์Œ - ๋นˆ ์ƒํƒœ๋กœ ์ดˆ๊ธฐํ™”"); onChange([]); } } } else { // API ์‹คํŒจ ์‹œ ๋นˆ ๋ฐฐ์—ด๋กœ ์„ค์ • console.log("โš ๏ธ [RepeaterFieldGroup] FK ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ - ๋นˆ ์ƒํƒœ๋กœ ์ดˆ๊ธฐํ™”"); setGroupedData([]); setOriginalItemIds([]); onChange?.([]); } } catch (error) { console.error("โŒ [RepeaterFieldGroup] FK ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error); setGroupedData([]); } finally { setIsLoadingGroupData(false); } }; loadDataByFK(); }, [isRightPanel, fkColumn, fkValue, effectiveTargetTable, onChange]); // ๊ฐ’์ด JSON ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ ํŒŒ์‹ฑ let parsedValue: any[] = []; // ๐Ÿ†• ๊ทธ๋ฃนํ™”๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (๋นˆ ๋ฐฐ์—ด ํฌํ•จ!) // groupedData๊ฐ€ null์ด ์•„๋‹ˆ๋ฉด (๋นˆ ๋ฐฐ์—ด์ด๋ผ๋„) ํ•ด๋‹น ๊ฐ’์„ ์‚ฌ์šฉ if (groupedData !== null) { parsedValue = groupedData; } else if (isEditMode && hasRepeaterFieldsInFormData && !groupByColumn) { // ๊ทธ๋ฃนํ™” ์„ค์ •์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋‹จ์ผ ํ–‰ ์‚ฌ์šฉ console.log("๐Ÿ“ [RepeaterFieldGroup] ์ˆ˜์ • ๋ชจ๋“œ - formData๋ฅผ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋กœ ์‚ฌ์šฉ", { formDataId: formData?.id, matchingFieldsCount: matchingFields.length, }); parsedValue = [{ ...formData }]; } else if (typeof rawValue === "string" && rawValue.trim() !== "") { // ๋นˆ ๋ฌธ์ž์—ด์ด ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ JSON ํŒŒ์‹ฑ ์‹œ๋„ try { parsedValue = JSON.parse(rawValue); } catch { parsedValue = []; } } else if (Array.isArray(rawValue)) { parsedValue = rawValue; } // parsedValue๋ฅผ ref๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์ตœ์‹  ๊ฐ’ ์œ ์ง€ const parsedValueRef = useRef(parsedValue); parsedValueRef.current = parsedValue; // onChange๋ฅผ ref๋กœ ๊ด€๋ฆฌ const onChangeRef = useRef(onChange); onChangeRef.current = onChange; // onFormDataChange๋ฅผ ref๋กœ ๊ด€๋ฆฌ const onFormDataChangeRef = useRef(onFormDataChange); onFormDataChangeRef.current = onFormDataChange; // fieldName์„ ref๋กœ ๊ด€๋ฆฌ const fieldNameRef = useRef(fieldName); fieldNameRef.current = fieldName; // config๋ฅผ ref๋กœ ๊ด€๋ฆฌ const configRef = useRef(config); configRef.current = config; // ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ํ•ธ๋“ค๋Ÿฌ const handleReceiveData = useCallback((data: any[], mappingRulesOrMode?: any[] | string) => { console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ๋ฐ์ดํ„ฐ ์ˆ˜์‹ :", { data, mappingRulesOrMode }); if (!data || data.length === 0) { toast.warning("์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); return; } // ๋งคํ•‘ ๊ทœ์น™์ด ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ์—๋งŒ ์ ์šฉ let processedData = data; if (Array.isArray(mappingRulesOrMode) && mappingRulesOrMode.length > 0) { processedData = applyMappingRules(data, mappingRulesOrMode); } // ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”: ๊ฐ ํ•ญ๋ชฉ์—์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ // ๋ฐ์ดํ„ฐ๊ฐ€ {0: {...}, inbound_type: "..."} ํ˜•ํƒœ์ธ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ const normalizedData = processedData.map((item: any) => { // item์ด {0: {...์‹ค์ œ๋ฐ์ดํ„ฐ...}, ์ถ”๊ฐ€ํ•„๋“œ: ๊ฐ’} ํ˜•ํƒœ์ธ ๊ฒฝ์šฐ if (item && typeof item === "object" && item[0] && typeof item[0] === "object") { // 0๋ฒˆ ์ธ๋ฑ์Šค์˜ ๋ฐ์ดํ„ฐ์™€ ๋‚˜๋จธ์ง€ ํ•„๋“œ๋ฅผ ๋ณ‘ํ•ฉ const { 0: originalData, ...additionalFields } = item; return { ...originalData, ...additionalFields }; } return item; }); // ๐Ÿ†• ์ •์˜๋œ ํ•„๋“œ๋งŒ ํ•„ํ„ฐ๋ง (๋ถˆํ•„์š”ํ•œ ํ•„๋“œ ์ œ๊ฑฐ) // ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน์— ์ •์˜๋œ ํ•„๋“œ + ์‹œ์Šคํ…œ ํ•„๋“œ๋งŒ ์œ ์ง€ const definedFields = configRef.current.fields || []; const definedFieldNames = new Set(definedFields.map((f: any) => f.name)); // ์‹œ์Šคํ…œ ํ•„๋“œ ๋ฐ ํ•„์ˆ˜ ํ•„๋“œ ์ถ”๊ฐ€ (id๋Š” ์ œ์™ธ - ์ƒˆ ๋ ˆ์ฝ”๋“œ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด) const systemFields = new Set([ "_targetTable", "_isNewItem", "created_date", "updated_date", "writer", "company_code", ]); const filteredData = normalizedData.map((item: any) => { const filteredItem: Record = {}; Object.keys(item).forEach((key) => { // ๐Ÿ†• id ํ•„๋“œ๋Š” ์ œ์™ธ (์ƒˆ ๋ ˆ์ฝ”๋“œ๋กœ ์ €์žฅ๋˜๋„๋ก) if (key === "id") { return; // id ํ•„๋“œ ์ œ์™ธ } // ์ •์˜๋œ ํ•„๋“œ์ด๊ฑฐ๋‚˜ ์‹œ์Šคํ…œ ํ•„๋“œ์ธ ๊ฒฝ์šฐ๋งŒ ํฌํ•จ if (definedFieldNames.has(key) || systemFields.has(key)) { filteredItem[key] = item[key]; } }); // ๐Ÿ†• ์ƒˆ ํ•ญ๋ชฉ์ž„์„ ํ‘œ์‹œํ•˜๋Š” ํ”Œ๋ž˜๊ทธ ์ถ”๊ฐ€ filteredItem._isNewItem = true; return filteredItem; }); console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ์ •๊ทœํ™”๋œ ๋ฐ์ดํ„ฐ:", normalizedData); console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ:", filteredData); // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ƒˆ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (๊ธฐ๋ณธ ๋ชจ๋“œ: append) const currentValue = parsedValueRef.current; // mode๊ฐ€ "replace"์ธ ๊ฒฝ์šฐ ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋Œ€์ฒด, ๊ทธ ์™ธ์—๋Š” ์ถ”๊ฐ€ const mode = typeof mappingRulesOrMode === "string" ? mappingRulesOrMode : "append"; let newItems: any[]; let addedCount = 0; let duplicateCount = 0; if (mode === "replace") { newItems = filteredData; addedCount = filteredData.length; } else { // ๐Ÿ†• ์ค‘๋ณต ์ฒดํฌ: item_code๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ํ•ญ๋ชฉ ์ œ์™ธ (id๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ) const existingItemCodes = new Set(currentValue.map((item: any) => item.item_code).filter(Boolean)); const uniqueNewItems = filteredData.filter((item: any) => { const itemCode = item.item_code; if (itemCode && existingItemCodes.has(itemCode)) { duplicateCount++; return false; // ์ค‘๋ณต ํ•ญ๋ชฉ ์ œ์™ธ } return true; }); newItems = [...currentValue, ...uniqueNewItems]; addedCount = uniqueNewItems.length; } console.log("๐Ÿ“ฅ [RepeaterFieldGroup] ์ตœ์ข… ๋ฐ์ดํ„ฐ:", { currentValue, newItems, mode, addedCount, duplicateCount, }); // ๐Ÿ†• groupedData ์ƒํƒœ๋„ ์ง์ ‘ ์—…๋ฐ์ดํŠธ (UI ์ฆ‰์‹œ ๋ฐ˜์˜) setGroupedData(newItems); // ๐Ÿ†• SplitPanelContext์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID ๋“ฑ๋ก (์ขŒ์ธก ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง์šฉ) // item_code๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋“ฑ๋ก (id๋Š” ์ƒˆ ๋ ˆ์ฝ”๋“œ๋ผ ์—†์„ ์ˆ˜ ์žˆ์Œ) if (splitPanelContext?.addItemIds && addedCount > 0) { const newItemCodes = newItems.map((item: any) => String(item.item_code)).filter(Boolean); splitPanelContext.addItemIds(newItemCodes); } // JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ const jsonValue = JSON.stringify(newItems); console.log("๐Ÿ“ฅ [RepeaterFieldGroup] onChange/onFormDataChange ํ˜ธ์ถœ:", { jsonValue, hasOnChange: !!onChangeRef.current, hasOnFormDataChange: !!onFormDataChangeRef.current, fieldName: fieldNameRef.current, }); // onFormDataChange๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (EmbeddedScreen์˜ formData ์ƒํƒœ ์—…๋ฐ์ดํŠธ) if (onFormDataChangeRef.current) { onFormDataChangeRef.current(fieldNameRef.current, jsonValue); } // ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด onChange ์‚ฌ์šฉ else if (onChangeRef.current) { onChangeRef.current(jsonValue); } // ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ if (addedCount > 0) { if (duplicateCount > 0) { toast.success(`${addedCount}๊ฐœ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค (${duplicateCount}๊ฐœ ์ค‘๋ณต ์ œ์™ธ)`); } else { toast.success(`${addedCount}๊ฐœ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); } } else if (duplicateCount > 0) { toast.warning(`${duplicateCount}๊ฐœ ํ•ญ๋ชฉ์ด ์ด๋ฏธ ์ถ”๊ฐ€๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค`); } }, []); // DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ const dataReceiver = useMemo( () => ({ componentId: component.id, componentType: "repeater-field-group", receiveData: handleReceiveData, }), [component.id, handleReceiveData], ); // ScreenContext์— ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก useEffect(() => { if (screenContext && component.id) { console.log("๐Ÿ“‹ [RepeaterFieldGroup] ScreenContext์— ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก:", component.id); screenContext.registerDataReceiver(component.id, dataReceiver); return () => { screenContext.unregisterDataReceiver(component.id); }; } }, [screenContext, component.id, dataReceiver]); // SplitPanelContext์— ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก (๋ถ„ํ•  ํŒจ๋„ ๋‚ด์—์„œ๋งŒ) useEffect(() => { const splitPanelPosition = screenContext?.splitPanelPosition; if (splitPanelContext?.isInSplitPanel && splitPanelPosition && component.id) { console.log("๐Ÿ”— [RepeaterFieldGroup] SplitPanelContext์— ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก:", { componentId: component.id, position: splitPanelPosition, }); splitPanelContext.registerReceiver(splitPanelPosition, component.id, dataReceiver); receiverRef.current = dataReceiver; return () => { console.log("๐Ÿ”— [RepeaterFieldGroup] SplitPanelContext์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ํ•ด์ œ:", component.id); splitPanelContext.unregisterReceiver(splitPanelPosition, component.id); receiverRef.current = null; }; } }, [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[]) => { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); // ๐Ÿ†• ๋ชจ๋“  ํ•ญ๋ชฉ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ let valueWithMeta = newValue.map((item: any) => ({ ...item, _targetTable: effectiveTargetTable || targetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก })); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์šฐ์ธก์ธ ๊ฒฝ์šฐ, FK ๊ฐ’ ์ถ”๊ฐ€ if (isRightPanel && fkColumn && fkValue) { valueWithMeta = valueWithMeta.map((item: any) => { if (item._isNewItem) { console.log("๐Ÿ”— [RepeaterFieldGroup] ์ƒˆ ํ•ญ๋ชฉ์— FK ๊ฐ’ ์ถ”๊ฐ€:", { fkColumn, fkValue }); return { ...item, [fkColumn]: fkValue }; } return item; }); } // ๋ฐฐ์—ด์„ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ const jsonValue = JSON.stringify(valueWithMeta); console.log("๐Ÿ“ค [RepeaterFieldGroup] ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ:", { fieldName, itemCount: valueWithMeta.length, isRightPanel, hasScreenContextUpdateFormData: !!screenContext?.updateFormData, }); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ์šฐ์ธก์—์„œ๋Š” ScreenContext.updateFormData๋งŒ ์‚ฌ์šฉ // (์ค‘๋ณต ์ €์žฅ ๋ฐฉ์ง€: onChange/onFormDataChange๋Š” ๋ถ€๋ชจ์—๊ฒŒ ์ „๋‹ฌ๋˜์–ด ๋‹ค์‹œ formData๋กœ ๋Œ์•„์˜ด) if (isRightPanel && screenContext?.updateFormData) { screenContext.updateFormData(fieldName, jsonValue); console.log("๐Ÿ“ค [RepeaterFieldGroup] screenContext.updateFormData ํ˜ธ์ถœ (์šฐ์ธก ํŒจ๋„):", { fieldName }); } else { // ๋ถ„ํ•  ํŒจ๋„์ด ์•„๋‹ˆ๊ฑฐ๋‚˜ ์ขŒ์ธก ํŒจ๋„์ธ ๊ฒฝ์šฐ ๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ onChange?.(jsonValue); if (onFormDataChange) { onFormDataChange(fieldName, jsonValue); console.log("๐Ÿ“ค [RepeaterFieldGroup] onFormDataChange(props) ํ˜ธ์ถœ:", { fieldName }); } } // ๐Ÿ†• groupedData ์ƒํƒœ๋„ ์—…๋ฐ์ดํŠธ setGroupedData(valueWithMeta); // ๐Ÿ†• 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, onFormDataChange, splitPanelContext, screenContext?.splitPanelPosition, screenContext?.updateFormData, isRightPanel, effectiveTargetTable, targetTable, fkColumn, fkValue, fieldName, ], ); // ๐Ÿ†• config์— effectiveTargetTable ๋ณ‘ํ•ฉ (linkedFilters์—์„œ ์ถ”์ถœ๋œ ํ…Œ์ด๋ธ”๋„ ํฌํ•จ) const effectiveConfig = { ...config, targetTable: effectiveTargetTable || config.targetTable, }; return ( ); }; /** * Repeater Field Group ๋ Œ๋”๋Ÿฌ * ์—ฌ๋Ÿฌ ํ•„๋“œ๋ฅผ ๊ฐ€์ง„ ํ•ญ๋ชฉ๋“ค์„ ๋™์ ์œผ๋กœ ์ถ”๊ฐ€/์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ */ export class RepeaterFieldGroupRenderer extends AutoRegisteringComponentRenderer { /** * ์ปดํฌ๋„ŒํŠธ ์ •์˜ */ static componentDefinition: ComponentDefinition = { id: "repeater-field-group", name: "๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน", nameEng: "Repeater Field Group", description: "์—ฌ๋Ÿฌ ํ•„๋“œ๋ฅผ ๊ฐ€์ง„ ํ•ญ๋ชฉ๋“ค์„ ๋™์ ์œผ๋กœ ์ถ”๊ฐ€/์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ˜๋ณต ๊ฐ€๋Šฅํ•œ ํ•„๋“œ ๊ทธ๋ฃน", category: ComponentCategory.INPUT, webType: "array", // ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃธ icon: Layers, component: RepeaterFieldGroupRenderer, configPanel: RepeaterConfigPanel, defaultSize: { width: 600, height: 200, // ๊ธฐ๋ณธ ๋†’์ด ์กฐ์ • }, defaultConfig: { fields: [], // ๋นˆ ๋ฐฐ์—ด๋กœ ์‹œ์ž‘ - ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ํ•„๋“œ ์ถ”๊ฐ€ minItems: 1, // ๊ธฐ๋ณธ 1๊ฐœ ํ•ญ๋ชฉ maxItems: 20, addButtonText: "ํ•ญ๋ชฉ ์ถ”๊ฐ€", allowReorder: true, showIndex: true, collapsible: false, layout: "grid", showDivider: true, emptyMessage: "ํ•„๋“œ๋ฅผ ๋จผ์ € ์ •์˜ํ•˜์„ธ์š”.", }, tags: ["repeater", "fieldgroup", "dynamic", "multi", "form", "array", "fields"], author: "System", version: "1.0.0", }; /** * ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง */ render(): React.ReactElement { return ; } } // ์ปดํฌ๋„ŒํŠธ ์ž๋™ ๋“ฑ๋ก RepeaterFieldGroupRenderer.registerSelf(); // Hot Reload ์ง€์› (๊ฐœ๋ฐœ ๋ชจ๋“œ) if (process.env.NODE_ENV === "development") { RepeaterFieldGroupRenderer.enableHotReload(); }