From 9614ce39735a920ba48867ef13180ed277fc7bc2 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Mon, 23 Feb 2026 09:16:44 +0900 Subject: [PATCH] feat: Enhance EditModal and V2Repeater functionality - Implemented zone offset adjustments for conditional components in EditModal to ensure correct rendering positions. - Added repeaterSave event dispatching in EditModal after saving data, improving integration with V2Repeater. - Updated V2Repeater to handle existing detail data loading based on foreign key relationships, enhancing data management. - Improved calculation rules handling in V2RepeaterConfigPanel, allowing for dynamic updates and better user experience. - Enhanced SplitPanelLayoutComponent to conditionally load data based on selected items and tab changes, improving performance and usability. --- frontend/components/screen/EditModal.tsx | 47 +++- frontend/components/screen/ScreenDesigner.tsx | 10 +- frontend/components/v2/V2Repeater.tsx | 215 ++++++++++++++---- frontend/components/v2/V2Select.tsx | 12 +- .../config-panels/V2RepeaterConfigPanel.tsx | 176 ++++++++++---- .../v2-repeater/V2RepeaterRenderer.tsx | 3 + .../SplitPanelLayoutComponent.tsx | 82 ++++--- frontend/types/v2-repeater.ts | 2 + 8 files changed, 410 insertions(+), 137 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index d8ce8e7a..0fd0cfec 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -565,12 +565,32 @@ export const EditModal: React.FC = ({ className }) => { return newActiveIds; }, [formData, groupData, conditionalLayers, screenData?.components]); - // ๐Ÿ†• ํ™œ์„ฑํ™”๋œ ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ + // ํ™œ์„ฑํ™”๋œ ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ (Zone ์˜คํ”„์…‹ ์ ์šฉ) const activeConditionalComponents = useMemo(() => { return conditionalLayers .filter((layer) => activeConditionalLayerIds.includes(layer.id)) - .flatMap((layer) => (layer as LayerDefinition & { components: ComponentData[] }).components || []); - }, [conditionalLayers, activeConditionalLayerIds]); + .flatMap((layer) => { + const layerWithComps = layer as LayerDefinition & { components: ComponentData[] }; + const comps = layerWithComps.components || []; + + // Zone ์˜คํ”„์…‹ ์ ์šฉ: ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ์ปดํฌ๋„ŒํŠธ๋Š” Zone ๋‚ด๋ถ€ ์ƒ๋Œ€ ์ขŒํ‘œ๋กœ ์ €์žฅ๋˜๋ฏ€๋กœ + // Zone์˜ ์ ˆ๋Œ€ ์ขŒํ‘œ๋ฅผ ๋”ํ•ด์ค˜์•ผ EditModal์—์„œ ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜์— ๋ Œ๋”๋ง๋จ + const associatedZone = zones.find((z) => z.zone_id === (layer as any).zoneId); + if (!associatedZone) return comps; + + const zoneOffsetX = associatedZone.x || 0; + const zoneOffsetY = associatedZone.y || 0; + + return comps.map((comp) => ({ + ...comp, + position: { + ...comp.position, + x: parseFloat(comp.position?.x?.toString() || "0") + zoneOffsetX, + y: parseFloat(comp.position?.y?.toString() || "0") + zoneOffsetY, + }, + })); + }); + }, [conditionalLayers, activeConditionalLayerIds, zones]); const handleClose = () => { setModalState({ @@ -881,14 +901,31 @@ export const EditModal: React.FC = ({ className }) => { } } + // V2Repeater ์ €์žฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋””ํ…Œ์ผ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ) + const hasRepeaterInstances = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0; + if (hasRepeaterInstances) { + const masterRecordId = groupData[0]?.id || formData.id; + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: masterRecordId, + masterRecordId, + mainFormData: formData, + tableName: screenData.screenInfo.tableName, + }, + }), + ); + console.log("๐Ÿ“‹ [EditModal] ๊ทธ๋ฃน ์ €์žฅ ํ›„ repeaterSave ์ด๋ฒคํŠธ ๋ฐœ์ƒ:", { masterRecordId }); + } + // ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ const messages: string[] = []; if (insertedCount > 0) messages.push(`${insertedCount}๊ฐœ ์ถ”๊ฐ€`); if (updatedCount > 0) messages.push(`${updatedCount}๊ฐœ ์ˆ˜์ •`); if (deletedCount > 0) messages.push(`${deletedCount}๊ฐœ ์‚ญ์ œ`); - if (messages.length > 0) { - toast.success(`ํ’ˆ๋ชฉ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค (${messages.join(", ")})`); + if (messages.length > 0 || hasRepeaterInstances) { + toast.success(messages.length > 0 ? `ํ’ˆ๋ชฉ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค (${messages.join(", ")})` : "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ onSave ์ฝœ๋ฐฑ ์‹คํ–‰ (ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ) if (modalState.onSave) { diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 76bd8973..af4fc96b 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -5555,8 +5555,12 @@ export default function ScreenDesigner({ return false; } - // 6. ์‚ญ์ œ (๋‹จ์ผ/๋‹ค์ค‘ ์„ ํƒ ์ง€์›) - if (e.key === "Delete" && (selectedComponent || groupState.selectedComponents.length > 0)) { + // 6. ์‚ญ์ œ (๋‹จ์ผ/๋‹ค์ค‘ ์„ ํƒ ์ง€์›) - Delete ๋˜๋Š” Backspace(Mac) + const isInputFocused = document.activeElement instanceof HTMLInputElement || + document.activeElement instanceof HTMLTextAreaElement || + document.activeElement instanceof HTMLSelectElement || + (document.activeElement as HTMLElement)?.isContentEditable; + if ((e.key === "Delete" || (e.key === "Backspace" && !isInputFocused)) && (selectedComponent || groupState.selectedComponents.length > 0)) { // console.log("๐Ÿ—‘๏ธ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ (๋‹จ์ถ•ํ‚ค)"); e.preventDefault(); e.stopPropagation(); @@ -7418,7 +7422,7 @@ export default function ScreenDesigner({

ํŽธ์ง‘: Ctrl+C(๋ณต์‚ฌ), Ctrl+V(๋ถ™์—ฌ๋„ฃ๊ธฐ), Ctrl+S(์ €์žฅ), - Ctrl+Z(์‹คํ–‰์ทจ์†Œ), Delete(์‚ญ์ œ) + Ctrl+Z(์‹คํ–‰์ทจ์†Œ), Delete/Backspace(์‚ญ์ œ)

โš ๏ธ diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx index 0f16cd31..734032f3 100644 --- a/frontend/components/v2/V2Repeater.tsx +++ b/frontend/components/v2/V2Repeater.tsx @@ -43,6 +43,7 @@ export const V2Repeater: React.FC = ({ onDataChange, onRowClick, className, + formData: parentFormData, }) => { // ์„ค์ • ๋ณ‘ํ•ฉ const config: V2RepeaterConfig = useMemo( @@ -153,21 +154,15 @@ export const V2Repeater: React.FC = ({ // ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์‹œ์—๋Š” ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ ์•ˆํ•จ) let mergedData: Record; if (config.useCustomTable && config.mainTableName) { - // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”: ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅ mergedData = { ...cleanRow }; - // ๐Ÿ†• FK ์ž๋™ ์—ฐ๊ฒฐ - foreignKeySourceColumn์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ ์‚ฌ์šฉ if (config.foreignKeyColumn) { - // foreignKeySourceColumn์ด ์žˆ์œผ๋ฉด mainFormData์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ ์‚ฌ์šฉ - // ์—†์œผ๋ฉด ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID ์‚ฌ์šฉ (๊ธฐ์กด ๋™์ž‘) const sourceColumn = config.foreignKeySourceColumn; let fkValue: any; if (sourceColumn && mainFormData && mainFormData[sourceColumn] !== undefined) { - // mainFormData์—์„œ ์ฐธ์กฐ ์ปฌ๋Ÿผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ fkValue = mainFormData[sourceColumn]; } else { - // ๊ธฐ๋ณธ: ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID ์‚ฌ์šฉ fkValue = masterRecordId; } @@ -176,7 +171,6 @@ export const V2Repeater: React.FC = ({ } } } else { - // ๊ธฐ์กด ๋ฐฉ์‹: ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; mergedData = { ...mainFormDataWithoutId, @@ -192,7 +186,19 @@ export const V2Repeater: React.FC = ({ } } - await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); + // ๊ธฐ์กด ํ–‰(id ์กด์žฌ)์€ UPDATE, ์ƒˆ ํ–‰์€ INSERT + const rowId = row.id; + 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); + } } } catch (error) { console.error("โŒ V2Repeater ์ €์žฅ ์‹คํŒจ:", error); @@ -228,6 +234,108 @@ export const V2Repeater: React.FC = ({ parentId, ]); + // ์ˆ˜์ • ๋ชจ๋“œ: useCustomTable + FK ๊ธฐ๋ฐ˜์œผ๋กœ ๊ธฐ์กด ๋””ํ…Œ์ผ ๋ฐ์ดํ„ฐ ์ž๋™ ๋กœ๋“œ + const dataLoadedRef = useRef(false); + useEffect(() => { + if (dataLoadedRef.current) return; + if (!config.useCustomTable || !config.mainTableName || !config.foreignKeyColumn) return; + if (!parentFormData) return; + + const fkSourceColumn = config.foreignKeySourceColumn || config.foreignKeyColumn; + const fkValue = parentFormData[fkSourceColumn]; + if (!fkValue) return; + + // ์ด๋ฏธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + if (data.length > 0) return; + + const loadExistingData = async () => { + try { + console.log("๐Ÿ“ฅ [V2Repeater] ์ˆ˜์ • ๋ชจ๋“œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ:", { + tableName: config.mainTableName, + fkColumn: config.foreignKeyColumn, + fkValue, + }); + + const response = await apiClient.post( + `/table-management/tables/${config.mainTableName}/data`, + { + page: 1, + size: 1000, + search: { [config.foreignKeyColumn]: fkValue }, + autoFilter: true, + } + ); + + const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || []; + if (Array.isArray(rows) && rows.length > 0) { + console.log(`โœ… [V2Repeater] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ${rows.length}๊ฑด ๋กœ๋“œ ์™„๋ฃŒ`); + + // isSourceDisplay ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ๋ณด๊ฐ• + const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay); + const sourceTable = config.dataSource?.sourceTable; + const fkColumn = config.dataSource?.foreignKey; + const refKey = config.dataSource?.referenceKey || "id"; + + if (sourceDisplayColumns.length > 0 && sourceTable && fkColumn) { + try { + const fkValues = rows.map((row) => row[fkColumn]).filter(Boolean); + const uniqueValues = [...new Set(fkValues)]; + + if (uniqueValues.length > 0) { + // FK ๊ฐ’ ๊ธฐ๋ฐ˜์œผ๋กœ ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ๋งŒ ์กฐํšŒ + const sourcePromises = uniqueValues.map((val) => + apiClient.post(`/table-management/tables/${sourceTable}/data`, { + page: 1, size: 1, + search: { [refKey]: val }, + autoFilter: true, + }).then((r) => r.data?.data?.data || r.data?.data?.rows || []) + .catch(() => []) + ); + const sourceResults = await Promise.all(sourcePromises); + const sourceMap = new Map(); + sourceResults.flat().forEach((sr: any) => { + if (sr[refKey]) sourceMap.set(String(sr[refKey]), sr); + }); + + // ๊ฐ ํ–‰์— ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ + // RepeaterTable์€ isSourceDisplay ์ปฌ๋Ÿผ์„ `_display_${col.key}` ํ•„๋“œ๋กœ ๋ Œ๋”๋งํ•จ + rows.forEach((row: any) => { + const sourceRecord = sourceMap.get(String(row[fkColumn])); + if (sourceRecord) { + sourceDisplayColumns.forEach((col) => { + const displayValue = sourceRecord[col.key] ?? null; + row[col.key] = displayValue; + row[`_display_${col.key}`] = displayValue; + }); + } + }); + console.log("โœ… [V2Repeater] ์†Œ์Šค ํ…Œ์ด๋ธ” ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ๋ณด๊ฐ• ์™„๋ฃŒ"); + } + } catch (sourceError) { + console.warn("โš ๏ธ [V2Repeater] ์†Œ์Šค ํ…Œ์ด๋ธ” ์กฐํšŒ ์‹คํŒจ (ํ‘œ์‹œ๋งŒ ์˜ํ–ฅ):", sourceError); + } + } + + setData(rows); + dataLoadedRef.current = true; + if (onDataChange) onDataChange(rows); + } + } catch (error) { + console.error("โŒ [V2Repeater] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + + loadExistingData(); + }, [ + config.useCustomTable, + config.mainTableName, + config.foreignKeyColumn, + config.foreignKeySourceColumn, + parentFormData, + data.length, + onDataChange, + ]); + // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ useEffect(() => { const loadCurrentTableColumnInfo = async () => { @@ -451,58 +559,71 @@ export const V2Repeater: React.FC = ({ loadCategoryLabels(); }, [data, sourceCategoryColumns]); + // ๊ณ„์‚ฐ ๊ทœ์น™ ์ ์šฉ (์†Œ์Šค ํ…Œ์ด๋ธ”์˜ _display_* ํ•„๋“œ๋„ ์ฐธ์กฐ ๊ฐ€๋Šฅ) + const applyCalculationRules = useCallback( + (row: any): any => { + const rules = config.calculationRules; + if (!rules || rules.length === 0) return row; + + const updatedRow = { ...row }; + for (const rule of rules) { + if (!rule.targetColumn || !rule.formula) continue; + try { + let formula = rule.formula; + const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []; + for (const field of fieldMatches) { + if (field === rule.targetColumn) continue; + // ์ง์ ‘ ํ•„๋“œ โ†’ _display_* ํ•„๋“œ ์ˆœ์œผ๋กœ ๊ฐ’ ํƒ์ƒ‰ + const raw = updatedRow[field] ?? updatedRow[`_display_${field}`]; + const value = parseFloat(raw) || 0; + formula = formula.replace(new RegExp(`\\b${field}\\b`, "g"), value.toString()); + } + updatedRow[rule.targetColumn] = new Function(`return ${formula}`)(); + } catch { + updatedRow[rule.targetColumn] = 0; + } + } + return updatedRow; + }, + [config.calculationRules], + ); + + // _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ onDataChange ํ˜ธ์ถœ + const notifyDataChange = useCallback( + (newData: any[]) => { + if (!onDataChange) return; + const targetTable = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; + if (targetTable) { + onDataChange(newData.map((row) => ({ ...row, _targetTable: targetTable }))); + } else { + onDataChange(newData); + } + }, + [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], + ); + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleDataChange = useCallback( (newData: any[]) => { - setData(newData); - - // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ ์ „๋‹ฌ (๋ฐฑ์—”๋“œ์—์„œ ํ…Œ์ด๋ธ” ๋ถ„๋ฆฌ์šฉ) - if (onDataChange) { - const targetTable = - config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; - - if (targetTable) { - // ๊ฐ ํ–‰์— _targetTable ์ถ”๊ฐ€ - const dataWithTarget = newData.map((row) => ({ - ...row, - _targetTable: targetTable, - })); - onDataChange(dataWithTarget); - } else { - onDataChange(newData); - } - } - - // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • + const calculated = newData.map(applyCalculationRules); + setData(calculated); + notifyDataChange(calculated); setAutoWidthTrigger((prev) => prev + 1); }, - [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], + [applyCalculationRules, notifyDataChange], ); // ํ–‰ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleRowChange = useCallback( (index: number, newRow: any) => { + const calculated = applyCalculationRules(newRow); const newData = [...data]; - newData[index] = newRow; + newData[index] = calculated; setData(newData); - - // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ - if (onDataChange) { - const targetTable = - config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; - - if (targetTable) { - const dataWithTarget = newData.map((row) => ({ - ...row, - _targetTable: targetTable, - })); - onDataChange(dataWithTarget); - } else { - onDataChange(newData); - } - } + notifyDataChange(newData); }, - [data, onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], + [data, applyCalculationRules, notifyDataChange], ); // ํ–‰ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index c7ea8c94..b13d450e 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -153,13 +153,11 @@ const DropdownSelect = forwardRef { - // value๋Š” CommandItem์˜ value (๋ผ๋ฒจ) - // search๋Š” ๊ฒ€์ƒ‰์–ด + filter={(itemValue, search) => { if (!search) return 1; - const normalizedValue = value.toLowerCase(); - const normalizedSearch = search.toLowerCase(); - if (normalizedValue.includes(normalizedSearch)) return 1; + const option = options.find((o) => o.value === itemValue); + const label = (option?.label || option?.value || "").toLowerCase(); + if (label.includes(search.toLowerCase())) return 1; return 0; }} > @@ -172,7 +170,7 @@ const DropdownSelect = forwardRef handleSelect(option.value)} > = ({ const [currentTableColumns, setCurrentTableColumns] = useState([]); // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ const [entityColumns, setEntityColumns] = useState([]); // ์—”ํ‹ฐํ‹ฐ ํƒ€์ž… ์ปฌ๋Ÿผ const [sourceTableColumns, setSourceTableColumns] = useState([]); // ์†Œ์Šค(์—”ํ‹ฐํ‹ฐ) ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ - const [calculationRules, setCalculationRules] = useState([]); + const [calculationRules, setCalculationRules] = useState( + config.calculationRules || [] + ); const [loadingColumns, setLoadingColumns] = useState(false); const [loadingSourceColumns, setLoadingSourceColumns] = useState(false); @@ -553,26 +555,56 @@ export const V2RepeaterConfigPanel: React.FC = ({ updateConfig({ columns: newColumns }); }; + // ๊ณ„์‚ฐ ๊ทœ์น™์„ config์— ๋ฐ˜์˜ํ•˜๋Š” ํ—ฌํผ + const syncCalculationRules = (rules: CalculationRule[]) => { + setCalculationRules(rules); + updateConfig({ calculationRules: rules }); + }; + // ๊ณ„์‚ฐ ๊ทœ์น™ ์ถ”๊ฐ€ const addCalculationRule = () => { - setCalculationRules(prev => [ - ...prev, + const newRules = [ + ...calculationRules, { id: `calc_${Date.now()}`, targetColumn: "", formula: "" } - ]); + ]; + syncCalculationRules(newRules); }; // ๊ณ„์‚ฐ ๊ทœ์น™ ์‚ญ์ œ const removeCalculationRule = (id: string) => { - setCalculationRules(prev => prev.filter(r => r.id !== id)); + syncCalculationRules(calculationRules.filter(r => r.id !== id)); }; // ๊ณ„์‚ฐ ๊ทœ์น™ ์—…๋ฐ์ดํŠธ const updateCalculationRule = (id: string, field: keyof CalculationRule, value: string) => { - setCalculationRules(prev => - prev.map(r => r.id === id ? { ...r, [field]: value } : r) + syncCalculationRules( + calculationRules.map(r => r.id === id ? { ...r, [field]: value } : r) ); }; + // ์ˆ˜์‹ ์ž…๋ ฅ ํ•„๋“œ์— ์ปฌ๋Ÿผ๋ช… ์‚ฝ์ž… + const insertColumnToFormula = (ruleId: string, columnKey: string) => { + const rule = calculationRules.find(r => r.id === ruleId); + if (!rule) return; + const newFormula = rule.formula ? `${rule.formula} ${columnKey}` : columnKey; + updateCalculationRule(ruleId, "formula", newFormula); + }; + + // ์ˆ˜์‹์˜ ์˜์–ด ์ปฌ๋Ÿผ๋ช…์„ ํ•œ๊ธ€ ์ œ๋ชฉ์œผ๋กœ ๋ณ€ํ™˜ + const formulaToKorean = (formula: string): string => { + if (!formula) return ""; + let result = formula; + const allCols = config.columns || []; + // ๊ธด ์ปฌ๋Ÿผ๋ช…๋ถ€ํ„ฐ ์น˜ํ™˜ (๋ถ€๋ถ„ ๋งค์นญ ๋ฐฉ์ง€) + const sorted = [...allCols].sort((a, b) => b.key.length - a.key.length); + for (const col of sorted) { + if (col.title && col.key) { + result = result.replace(new RegExp(`\\b${col.key}\\b`, "g"), col.title); + } + } + return result; + }; + // ์—”ํ‹ฐํ‹ฐ ์ปฌ๋Ÿผ ์„ ํƒ ์‹œ ์†Œ์Šค ํ…Œ์ด๋ธ” ์ž๋™ ์„ค์ • const handleEntityColumnSelect = (columnName: string) => { const selectedEntity = entityColumns.find(c => c.columnName === columnName); @@ -1374,7 +1406,7 @@ export const V2RepeaterConfigPanel: React.FC = ({ {(isModalMode || isInlineMode) && config.columns.length > 0 && ( <> -

+
-

- ์˜ˆ: ๊ธˆ์•ก = ์ˆ˜๋Ÿ‰ * ๋‹จ๊ฐ€ -

-
+
{calculationRules.map((rule) => ( -
- - - = - - updateCalculationRule(rule.id, "formula", e.target.value)} - placeholder="quantity * unit_price" - className="h-7 flex-1 text-xs" - /> - - +
+
+ + = + updateCalculationRule(rule.id, "formula", e.target.value)} + placeholder="์ปฌ๋Ÿผ ํด๋ฆญ ๋˜๋Š” ์ง์ ‘ ์ž…๋ ฅ" + className="h-6 flex-1 font-mono text-[10px]" + /> + +
+ + {/* ํ•œ๊ธ€ ์ˆ˜์‹ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {rule.formula && ( +

+ {config.columns.find(c => c.key === rule.targetColumn)?.title || rule.targetColumn || "๊ฒฐ๊ณผ"} = {formulaToKorean(rule.formula)} +

+ )} + + {/* ์ปฌ๋Ÿผ ์นฉ: ๋””ํ…Œ์ผ ์ปฌ๋Ÿผ + ์†Œ์Šค(ํ’ˆ๋ชฉ) ์ปฌ๋Ÿผ + ์—ฐ์‚ฐ์ž */} +
+ {config.columns + .filter(col => col.key !== rule.targetColumn && !col.isSourceDisplay) + .map((col) => ( + + ))} + {config.columns + .filter(col => col.isSourceDisplay) + .map((col) => ( + + ))} + {["+", "-", "*", "/", "(", ")"].map((op) => ( + + ))} +
))} {calculationRules.length === 0 && ( -

+

๊ณ„์‚ฐ ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค

)} diff --git a/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx b/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx index 908bc4f1..e531b655 100644 --- a/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx +++ b/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx @@ -20,6 +20,7 @@ interface V2RepeaterRendererProps { onRowClick?: (row: any) => void; onButtonClick?: (action: string, row?: any, buttonConfig?: any) => void; parentId?: string | number; + formData?: Record; } const V2RepeaterRenderer: React.FC = ({ @@ -31,6 +32,7 @@ const V2RepeaterRenderer: React.FC = ({ onRowClick, onButtonClick, parentId, + formData, }) => { // component.componentConfig ๋˜๋Š” component.config์—์„œ V2RepeaterConfig ์ถ”์ถœ const config: V2RepeaterConfig = React.useMemo(() => { @@ -101,6 +103,7 @@ const V2RepeaterRenderer: React.FC = ({ onRowClick={onRowClick} onButtonClick={onButtonClick} className={component?.className} + formData={formData} /> ); }; diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index ebecedb3..f56b0fb3 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1526,22 +1526,30 @@ export const SplitPanelLayoutComponent: React.FC [componentConfig.rightPanel?.additionalTabs, isDesignMode, toast], ); - // ํƒญ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ขŒ์ธก ๋ฏธ์„ ํƒ ์‹œ์—๋„ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ) + // ํƒญ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleTabChange = useCallback( (newTabIndex: number) => { setActiveTabIndex(newTabIndex); + // ๋ฉ”์ธ ํŒจ๋„์ด "detail"(์„ ํƒ ์‹œ ํ‘œ์‹œ)์ด๋ฉด ์ขŒ์ธก ๋ฏธ์„ ํƒ ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail"; + const requireSelection = mainRelationType === "detail"; + if (newTabIndex === 0) { if (!rightData || (Array.isArray(rightData) && rightData.length === 0)) { - loadRightData(selectedLeftItem); + if (!requireSelection || selectedLeftItem) { + loadRightData(selectedLeftItem); + } } } else { if (!tabsData[newTabIndex]) { - loadTabData(newTabIndex, selectedLeftItem); + if (!requireSelection || selectedLeftItem) { + loadTabData(newTabIndex, selectedLeftItem); + } } } }, - [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData], + [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData, componentConfig.rightPanel?.relation?.type], ); // ์ขŒ์ธก ํ•ญ๋ชฉ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ (๋™์ผ ํ•ญ๋ชฉ ์žฌํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ โ†’ ์ „์ฒด ๋ฐ์ดํ„ฐ ํ‘œ์‹œ) @@ -1554,24 +1562,31 @@ export const SplitPanelLayoutComponent: React.FC selectedLeftItem[leftPk] === item[leftPk]; if (isSameItem) { - // ์„ ํƒ ํ•ด์ œ โ†’ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ + // ์„ ํƒ ํ•ด์ œ setSelectedLeftItem(null); - setCustomLeftSelectedData({}); // ์ปค์Šคํ…€ ๋ชจ๋“œ ์šฐ์ธก ํผ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + setCustomLeftSelectedData({}); setExpandedRightItems(new Set()); setTabsData({}); - if (activeTabIndex === 0) { - loadRightData(null); + + const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail"; + if (mainRelationType === "detail") { + // "์„ ํƒ ์‹œ ํ‘œ์‹œ" ๋ชจ๋“œ: ์„ ํƒ ํ•ด์ œ ์‹œ ๋ฐ์ดํ„ฐ ๋น„์›€ + setRightData(null); } else { - loadTabData(activeTabIndex, null); - } - // ์ถ”๊ฐ€ ํƒญ๋“ค๋„ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ - const tabs = componentConfig.rightPanel?.additionalTabs; - if (tabs && tabs.length > 0) { - tabs.forEach((_: any, idx: number) => { - if (idx + 1 !== activeTabIndex) { - loadTabData(idx + 1, null); - } - }); + // "์—ฐ๊ด€ ๋ชฉ๋ก" ๋ชจ๋“œ: ์„ ํƒ ํ•ด์ œ ์‹œ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ + if (activeTabIndex === 0) { + loadRightData(null); + } else { + loadTabData(activeTabIndex, null); + } + const tabs = componentConfig.rightPanel?.additionalTabs; + if (tabs && tabs.length > 0) { + tabs.forEach((_: any, idx: number) => { + if (idx + 1 !== activeTabIndex) { + loadTabData(idx + 1, null); + } + }); + } } return; } @@ -2778,15 +2793,17 @@ export const SplitPanelLayoutComponent: React.FC if (relationshipType === "join") { loadRightData(null); } - // ์ถ”๊ฐ€ ํƒญ: ๊ฐ ํƒญ์˜ relation.type์— ๋”ฐ๋ผ ์ดˆ๊ธฐ ๋กœ๋“œ ๊ฒฐ์ • - const tabs = componentConfig.rightPanel?.additionalTabs; - if (tabs && tabs.length > 0) { - tabs.forEach((tab: any, idx: number) => { - const tabRelType = tab.relation?.type || "join"; - if (tabRelType === "join") { - loadTabData(idx + 1, null); - } - }); + // ์ถ”๊ฐ€ ํƒญ: ๋ฉ”์ธ ํŒจ๋„์ด "detail"(์„ ํƒ ์‹œ ํ‘œ์‹œ)์ด๋ฉด ์ถ”๊ฐ€ ํƒญ๋„ ์ดˆ๊ธฐ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + if (relationshipType !== "detail") { + const tabs = componentConfig.rightPanel?.additionalTabs; + if (tabs && tabs.length > 0) { + tabs.forEach((tab: any, idx: number) => { + const tabRelType = tab.relation?.type || "join"; + if (tabRelType === "join") { + loadTabData(idx + 1, null); + } + }); + } } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -3734,6 +3751,17 @@ export const SplitPanelLayoutComponent: React.FC const currentTabData = tabsData[activeTabIndex] || []; const isTabLoading = tabsLoading[activeTabIndex]; + // ๋ฉ”์ธ ํŒจ๋„์ด "detail"(์„ ํƒ ์‹œ ํ‘œ์‹œ)์ด๋ฉด ์ขŒ์ธก ๋ฏธ์„ ํƒ ์‹œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ + const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail"; + if (mainRelationType === "detail" && !selectedLeftItem && !isDesignMode) { + return ( +
+

์ขŒ์ธก์—์„œ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”

+

์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค

+
+ ); + } + if (isTabLoading) { return (
diff --git a/frontend/types/v2-repeater.ts b/frontend/types/v2-repeater.ts index d09ac9e9..fab7a523 100644 --- a/frontend/types/v2-repeater.ts +++ b/frontend/types/v2-repeater.ts @@ -180,7 +180,9 @@ export interface V2RepeaterProps { data?: any[]; // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ (์—†์œผ๋ฉด API๋กœ ๋กœ๋“œ) onDataChange?: (data: any[]) => void; onRowClick?: (row: any) => void; + onButtonClick?: (action: string, row?: any, buttonConfig?: any) => void; className?: string; + formData?: Record; // ์ˆ˜์ • ๋ชจ๋“œ์—์„œ FK ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ์šฉ } // ๊ธฐ๋ณธ ์„ค์ •๊ฐ’