diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 1b54d3b9..1969f562 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -976,6 +976,19 @@ export const EditModal: React.FC = ({ className }) => { const groupedDataProp = groupData.length > 0 ? groupData : undefined; + // ๐Ÿ†• UniversalFormModal์ด ์žˆ๋Š”์ง€ ํ™•์ธ (์ž์ฒด ์ €์žฅ ๋กœ์ง ์‚ฌ์šฉ) + // ์ตœ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ ๋˜๋Š” ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ํ™”๋ฉด์— universal-form-modal์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const hasUniversalFormModal = screenData.components.some( + (c) => { + // ์ตœ์ƒ์œ„์— universal-form-modal์ด ์žˆ๋Š” ๊ฒฝ์šฐ + if (c.componentType === "universal-form-modal") return true; + // ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— universal-form-modal์ด ์žˆ๋Š” ๊ฒฝ์šฐ + // (์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ์œผ๋ฉด ๋‚ด๋ถ€ ํ™”๋ฉด์—์„œ universal-form-modal์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ€์ •) + if (c.componentType === "conditional-container") return true; + return false; + } + ); + // ๐Ÿ”‘ ์ฒจ๋ถ€ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ–‰(๋ ˆ์ฝ”๋“œ) ๋‹จ์œ„๋กœ ํŒŒ์ผ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก tableName ์ถ”๊ฐ€ const enrichedFormData = { ...(groupData.length > 0 ? groupData[0] : formData), @@ -1024,7 +1037,9 @@ export const EditModal: React.FC = ({ className }) => { id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} - onSave={handleSave} + // ๐Ÿ†• UniversalFormModal์ด ์žˆ์œผ๋ฉด onSave ์ „๋‹ฌ ์•ˆ ํ•จ (์ž์ฒด ์ €์žฅ ๋กœ์ง ์‚ฌ์šฉ) + // ModalRepeaterTable๋งŒ ์žˆ์œผ๋ฉด ๊ธฐ์กด๋Œ€๋กœ onSave ์ „๋‹ฌ (ํ˜ธํ™˜์„ฑ ์œ ์ง€) + onSave={hasUniversalFormModal ? undefined : handleSave} isInModal={true} // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ๋ฅผ ModalRepeaterTable์— ์ „๋‹ฌ groupedData={groupedDataProp} diff --git a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx index d5686f6c..59c82421 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx @@ -150,46 +150,54 @@ export function ConditionalSectionViewer({ /* ์‹คํ–‰ ๋ชจ๋“œ: ์‹ค์ œ ํ™”๋ฉด ๋ Œ๋”๋ง */
{/* ํ™”๋ฉด ํฌ๊ธฐ๋งŒํผ์˜ ์ ˆ๋Œ€ ์œ„์น˜ ์บ”๋ฒ„์Šค */} -
- {components.map((component) => { - const { position = { x: 0, y: 0, z: 1 }, size = { width: 200, height: 40 } } = component; - - return ( -
- -
- ); - })} -
+ {/* UniversalFormModal์ด ์žˆ์œผ๋ฉด onSave ์ „๋‹ฌํ•˜์ง€ ์•Š์Œ (์ž์ฒด ์ €์žฅ ๋กœ์ง ์‚ฌ์šฉ) */} + {(() => { + const hasUniversalFormModal = components.some( + (c) => c.componentType === "universal-form-modal" + ); + return ( +
+ {components.map((component) => { + const { position = { x: 0, y: 0, z: 1 }, size = { width: 200, height: 40 } } = component; + + return ( +
+ +
+ ); + })} +
+ ); + })()}
)} diff --git a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx index 456594c2..ad73c317 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { Dialog, DialogContent, @@ -12,9 +12,11 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Search, Loader2 } from "lucide-react"; import { useEntitySearch } from "../entity-search-input/useEntitySearch"; -import { ItemSelectionModalProps } from "./types"; +import { ItemSelectionModalProps, ModalFilterConfig } from "./types"; +import { apiClient } from "@/lib/api/client"; export function ItemSelectionModal({ open, @@ -29,27 +31,134 @@ export function ItemSelectionModal({ uniqueField, onSelect, columnLabels = {}, + modalFilters = [], }: ItemSelectionModalProps) { const [localSearchText, setLocalSearchText] = useState(""); const [selectedItems, setSelectedItems] = useState([]); + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ๊ฐ’ ์ƒํƒœ + const [modalFilterValues, setModalFilterValues] = useState>({}); + + // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ต์…˜ ์ƒํƒœ (categoryRef๋ณ„๋กœ ๋กœ๋“œ๋œ ์˜ต์…˜) + const [categoryOptions, setCategoryOptions] = useState>({}); + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ๊ฐ’๊ณผ ๊ธฐ๋ณธ filterCondition์„ ํ•ฉ์นœ ์ตœ์ข… ํ•„ํ„ฐ ์กฐ๊ฑด + const combinedFilterCondition = useMemo(() => { + const combined = { ...filterCondition }; + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ๊ฐ’ ์ถ”๊ฐ€ (๋นˆ ๊ฐ’์€ ์ œ์™ธ) + for (const [key, value] of Object.entries(modalFilterValues)) { + if (value !== undefined && value !== null && value !== "") { + combined[key] = value; + } + } + + return combined; + }, [filterCondition, modalFilterValues]); const { results, loading, error, search, clearSearch } = useEntitySearch({ tableName: sourceTable, searchFields: sourceSearchFields.length > 0 ? sourceSearchFields : sourceColumns, - filterCondition, + filterCondition: combinedFilterCondition, }); - // ๋ชจ๋‹ฌ ์—ด๋ฆด ๋•Œ ์ดˆ๊ธฐ ๊ฒ€์ƒ‰ + // ํ•„ํ„ฐ ์˜ต์…˜ ๋กœ๋“œ - ์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์˜ distinct ๊ฐ’ ์กฐํšŒ + const loadFilterOptions = async (filter: ModalFilterConfig) => { + // ๋“œ๋กญ๋‹ค์šด ํƒ€์ž…๋งŒ ์˜ต์…˜ ๋กœ๋“œ ํ•„์š” (select, category ์ง€์›) + const isDropdownType = filter.type === "select" || filter.type === "category"; + if (!isDropdownType) return; + + const cacheKey = `${sourceTable}.${filter.column}`; + + // ์ด๋ฏธ ๋กœ๋“œ๋œ ๊ฒฝ์šฐ ์Šคํ‚ต + if (categoryOptions[cacheKey]) return; + + try { + // ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ (POST ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ) + // ๋ฐฑ์—”๋“œ๋Š” 'size' ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•จ + const response = await apiClient.post(`/table-management/tables/${sourceTable}/data`, { + page: 1, + size: 10000, // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ๋ฅผ ์œ„ํ•ด ํฐ ๊ฐ’ ์„ค์ • + }); + + if (response.data?.success) { + // ์‘๋‹ต ๊ตฌ์กฐ์— ๋”ฐ๋ผ rows ์ถ”์ถœ + const rows = response.data.data?.rows || response.data.data?.data || response.data.data || []; + + if (Array.isArray(rows)) { + // ์ปฌ๋Ÿผ ๊ฐ’ ์ค‘๋ณต ์ œ๊ฑฐ + const uniqueValues = new Set(); + for (const row of rows) { + const val = row[filter.column]; + if (val !== null && val !== undefined && val !== "") { + uniqueValues.add(String(val)); + } + } + + // ์ •๋ ฌ ํ›„ ์˜ต์…˜์œผ๋กœ ๋ณ€ํ™˜ + const options = Array.from(uniqueValues) + .sort() + .map((val) => ({ + value: val, + label: val, + })); + + setCategoryOptions((prev) => ({ + ...prev, + [cacheKey]: options, + })); + } + } + } catch (error) { + console.error(`ํ•„ํ„ฐ ์˜ต์…˜ ๋กœ๋“œ ์‹คํŒจ (${cacheKey}):`, error); + setCategoryOptions((prev) => ({ + ...prev, + [cacheKey]: [], + })); + } + }; + + // ๋ชจ๋‹ฌ ์—ด๋ฆด ๋•Œ ์ดˆ๊ธฐ ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” useEffect(() => { if (open) { + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • & ์˜ต์…˜ ๋กœ๋“œ + const initialFilterValues: Record = {}; + for (const filter of modalFilters) { + if (filter.defaultValue !== undefined) { + initialFilterValues[filter.column] = filter.defaultValue; + } + // ๋“œ๋กญ๋‹ค์šด ํƒ€์ž…์ด๋ฉด ์˜ต์…˜ ๋กœ๋“œ (์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ distinct ๊ฐ’ ์กฐํšŒ) + const isDropdownType = filter.type === "select" || filter.type === "category"; + if (isDropdownType) { + loadFilterOptions(filter); + } + } + setModalFilterValues(initialFilterValues); + search("", 1); // ๋นˆ ๊ฒ€์ƒ‰์–ด๋กœ ์ „์ฒด ๋ชฉ๋ก ์กฐํšŒ setSelectedItems([]); } else { clearSearch(); setLocalSearchText(""); setSelectedItems([]); + setModalFilterValues({}); } }, [open]); + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ๊ฐ’ ๋ณ€๊ฒฝ ์‹œ ์žฌ๊ฒ€์ƒ‰ + useEffect(() => { + if (open) { + search(localSearchText, 1); + } + }, [modalFilterValues]); + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ๊ฐ’ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleModalFilterChange = (column: string, value: any) => { + setModalFilterValues((prev) => ({ + ...prev, + [column]: value, + })); + }; const handleSearch = () => { search(localSearchText, 1); @@ -202,6 +311,51 @@ export function ItemSelectionModal({ + {/* ๋ชจ๋‹ฌ ํ•„ํ„ฐ */} + {modalFilters.length > 0 && ( +
+ {modalFilters.map((filter) => { + // ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ํ•ด๋‹น ์ปฌ๋Ÿผ์—์„œ ๋กœ๋“œ๋œ ์˜ต์…˜ + const options = categoryOptions[`${sourceTable}.${filter.column}`] || []; + + // ๋“œ๋กญ๋‹ค์šด ํƒ€์ž…์ธ์ง€ ํ™•์ธ (select, category ๋ชจ๋‘ ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ์ฒ˜๋ฆฌ) + const isDropdownType = filter.type === "select" || filter.type === "category"; + + return ( +
+ {filter.label}: + {isDropdownType && ( + + )} + {filter.type === "text" && ( + handleModalFilterChange(filter.column, e.target.value)} + placeholder={filter.label} + className="h-7 text-xs w-[120px]" + /> + )} +
+ ); + })} +
+ )} + {/* ์„ ํƒ๋œ ํ•ญ๋ชฉ ์ˆ˜ */} {selectedItems.length > 0 && (
diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index c7d7c8b6..2caf1332 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -205,6 +205,9 @@ export function ModalRepeaterTableComponent({ const multiSelect = componentConfig?.multiSelect ?? propMultiSelect ?? true; const calculationRules = componentConfig?.calculationRules || propCalculationRules || []; + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์„ค์ • + const modalFilters = componentConfig?.modalFilters || []; + // โœ… value๋Š” formData[columnName] ์šฐ์„ , ์—†์œผ๋ฉด prop ์‚ฌ์šฉ const columnName = component?.columnName; const externalValue = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; @@ -889,6 +892,7 @@ export function ModalRepeaterTableComponent({ uniqueField={uniqueField} onSelect={handleAddItems} columnLabels={columnLabels} + modalFilters={modalFilters} />
); diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx index 7a11bdb1..2e1cf659 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx @@ -9,7 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Plus, X, Check, ChevronsUpDown } from "lucide-react"; -import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule, DynamicDataSourceConfig, DynamicDataSourceOption, MultiTableJoinStep } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule, DynamicDataSourceConfig, DynamicDataSourceOption, MultiTableJoinStep, ModalFilterConfig } from "./types"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { cn } from "@/lib/utils"; @@ -842,6 +842,97 @@ export function ModalRepeaterTableConfigPanel({ /> + + {/* ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์„ค์ • */} +
+
+ + +
+

+ ๋ชจ๋‹ฌ์—์„œ ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ํ•„ํ„ฐ๋งํ•  ์ปฌ๋Ÿผ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ํ•ด๋‹น ์ปฌ๋Ÿผ์—์„œ ๊ณ ์œ  ๊ฐ’๋“ค์ด ์ž๋™์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +

+ {(localConfig.modalFilters || []).length > 0 && ( +
+ {(localConfig.modalFilters || []).map((filter, index) => ( +
+ + { + const filters = [...(localConfig.modalFilters || [])]; + filters[index] = { ...filters[index], label: e.target.value }; + updateConfig({ modalFilters: filters }); + }} + placeholder="๋ผ๋ฒจ" + className="h-8 text-xs w-[100px]" + /> + + +
+ ))} +
+ )} +
{/* ๋ฐ˜๋ณต ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๊ด€๋ฆฌ */} diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 9604e7d2..88da4aef 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -31,8 +31,8 @@ import { CSS } from "@dnd-kit/utilities"; // SortableRow ์ปดํฌ๋„ŒํŠธ - ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ํ…Œ์ด๋ธ” ํ–‰ interface SortableRowProps { id: string; - children: (props: { - attributes: React.HTMLAttributes; + children: (props: { + attributes: React.HTMLAttributes; listeners: React.HTMLAttributes | undefined; isDragging: boolean; }) => React.ReactNode; @@ -40,14 +40,7 @@ interface SortableRowProps { } function SortableRow({ id, children, className }: SortableRowProps) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id }); + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), @@ -93,9 +86,9 @@ export function RepeaterTable({ }: RepeaterTableProps) { // ์ปจํ…Œ์ด๋„ˆ ref - ์‹ค์ œ ๋„ˆ๋น„ ์ธก์ •์šฉ const containerRef = useRef(null); - - // ๊ท ๋“ฑ ๋ถ„๋ฐฐ ๋ชจ๋“œ ์ƒํƒœ (true์ผ ๋•Œ ํ…Œ์ด๋ธ”์ด ์ปจํ…Œ์ด๋„ˆ์— ๋งž์ถค) - const [isEqualizedMode, setIsEqualizedMode] = useState(false); + + // ์ดˆ๊ธฐ ๊ท ๋“ฑ ๋ถ„๋ฐฐ ์‹คํ–‰ ์—ฌ๋ถ€ (๋งˆ์šดํŠธ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) + const initializedRef = useRef(false); // DnD ์„ผ์„œ ์„ค์ • const sensors = useSensors( @@ -106,7 +99,7 @@ export function RepeaterTable({ }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, - }) + }), ); // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ํ•ธ๋“ค๋Ÿฌ @@ -140,15 +133,15 @@ export function RepeaterTable({ } } }; - + const [editingCell, setEditingCell] = useState<{ rowIndex: number; field: string; } | null>(null); - + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค Popover ์—ด๋ฆผ ์ƒํƒœ const [openPopover, setOpenPopover] = useState(null); - + // ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ƒํƒœ ๊ด€๋ฆฌ const [columnWidths, setColumnWidths] = useState>(() => { const widths: Record = {}; @@ -157,7 +150,7 @@ export function RepeaterTable({ }); return widths; }); - + // ๊ธฐ๋ณธ ๋„ˆ๋น„ ์ €์žฅ (๋ฆฌ์…‹์šฉ) const defaultWidths = React.useMemo(() => { const widths: Record = {}; @@ -166,10 +159,10 @@ export function RepeaterTable({ }); return widths; }, [columns]); - + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ const [resizing, setResizing] = useState<{ field: string; startX: number; startWidth: number } | null>(null); - + // ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋Ÿฌ const handleMouseDown = (e: React.MouseEvent, field: string) => { e.preventDefault(); @@ -178,104 +171,171 @@ export function RepeaterTable({ startX: e.clientX, startWidth: columnWidths[field] || 120, }); - // ์ˆ˜๋™ ์กฐ์ • ์‹œ ๊ท ๋“ฑ ๋ถ„๋ฐฐ ๋ชจ๋“œ ํ•ด์ œ - setIsEqualizedMode(false); }; - - // ์ปฌ๋Ÿผ ํ™•์žฅ ์ƒํƒœ ์ถ”์  (ํ† ๊ธ€์šฉ) - const [expandedColumns, setExpandedColumns] = useState>(new Set()); - // ๋ฐ์ดํ„ฐ ๊ธฐ์ค€ ์ตœ์  ๋„ˆ๋น„ ๊ณ„์‚ฐ - const calculateAutoFitWidth = (field: string): number => { - const column = columns.find(col => col.field === field); - if (!column) return 120; + // ์ปจํ…Œ์ด๋„ˆ ๊ฐ€์šฉ ๋„ˆ๋น„ ๊ณ„์‚ฐ + const getAvailableWidth = (): number => { + if (!containerRef.current) return 800; + const containerWidth = containerRef.current.offsetWidth; + // ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค(32px) + ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ(40px) + border(2px) + return containerWidth - 74; + }; - // ํ—ค๋” ํ…์ŠคํŠธ ๊ธธ์ด (๋Œ€๋žต 8px per character + padding) - const headerWidth = (column.label?.length || field.length) * 8 + 40; + // ํ…์ŠคํŠธ ๋„ˆ๋น„ ๊ณ„์‚ฐ (ํ•œ๊ธ€/์˜๋ฌธ/์ˆซ์ž ํ˜ผํ•ฉ ๊ณ ๋ ค) + const measureTextWidth = (text: string): number => { + if (!text) return 0; + let width = 0; + for (const char of text) { + if (/[๊ฐ€-ํžฃ]/.test(char)) { + width += 15; // ํ•œ๊ธ€ (text-xs 12px ๊ธฐ์ค€) + } else if (/[a-zA-Z]/.test(char)) { + width += 9; // ์˜๋ฌธ + } else if (/[0-9]/.test(char)) { + width += 8; // ์ˆซ์ž + } else if (/[_\-.]/.test(char)) { + width += 6; // ํŠน์ˆ˜๋ฌธ์ž + } else if (/[\(\)]/.test(char)) { + width += 6; // ๊ด„ํ˜ธ + } else { + width += 8; // ๊ธฐํƒ€ + } + } + return width; + }; - // ๋ฐ์ดํ„ฐ ์ค‘ ๊ฐ€์žฅ ๊ธด ํ…์ŠคํŠธ ์ฐพ๊ธฐ + // ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๊ฐ€์žฅ ๊ธด ๊ธ€์ž ๋„ˆ๋น„ ๊ณ„์‚ฐ + // equalWidth: ๊ท ๋“ฑ ๋ถ„๋ฐฐ ์‹œ ๋„ˆ๋น„ (๊ฐ’์ด ์—†๋Š” ์ปฌ๋Ÿผ์˜ ์ตœ์†Œ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ) + const calculateColumnContentWidth = (field: string, equalWidth: number): number => { + const column = columns.find((col) => col.field === field); + if (!column) return equalWidth; + + // ๋‚ ์งœ ํ•„๋“œ๋Š” 110px (yyyy-MM-dd) + if (column.type === "date") { + return 110; + } + + // ํ•ด๋‹น ์ปฌ๋Ÿผ์— ๊ฐ’์ด ์žˆ๋Š”์ง€ ํ™•์ธ + let hasValue = false; let maxDataWidth = 0; - data.forEach(row => { + + data.forEach((row) => { const value = row[field]; - if (value !== undefined && value !== null) { + if (value !== undefined && value !== null && value !== "") { + hasValue = true; let displayText = String(value); - - // ์ˆซ์ž๋Š” ์ฒœ๋‹จ์œ„ ๊ตฌ๋ถ„์ž ํฌํ•จ - if (typeof value === 'number') { + + if (typeof value === "number") { displayText = value.toLocaleString(); } - // ๋‚ ์งœ๋Š” yyyy-mm-dd ํ˜•์‹ - if (column.type === 'date' && displayText.includes('T')) { - displayText = displayText.split('T')[0]; - } - - // ๋Œ€๋žต์ ์ธ ๋„ˆ๋น„ ๊ณ„์‚ฐ (8px per character + padding) - const textWidth = displayText.length * 8 + 32; + + const textWidth = measureTextWidth(displayText) + 20; // padding maxDataWidth = Math.max(maxDataWidth, textWidth); } }); - // ํ—ค๋”์™€ ๋ฐ์ดํ„ฐ ์ค‘ ํฐ ๊ฐ’ ์‚ฌ์šฉ, ์ตœ์†Œ 60px, ์ตœ๋Œ€ 400px - const optimalWidth = Math.max(headerWidth, maxDataWidth); - return Math.min(Math.max(optimalWidth, 60), 400); - }; + // ๊ฐ’์ด ์—†์œผ๋ฉด ๊ท ๋“ฑ ๋ถ„๋ฐฐ ๋„ˆ๋น„ ์‚ฌ์šฉ + if (!hasValue) { + return equalWidth; + } - // ๋”๋ธ”ํด๋ฆญ์œผ๋กœ auto-fit / ๊ธฐ๋ณธ ๋„ˆ๋น„ ํ† ๊ธ€ - const handleDoubleClick = (field: string) => { - // ๊ฐœ๋ณ„ ์ปฌ๋Ÿผ ์กฐ์ • ์‹œ ๊ท ๋“ฑ ๋ถ„๋ฐฐ ๋ชจ๋“œ ํ•ด์ œ - setIsEqualizedMode(false); - - setExpandedColumns(prev => { - const newSet = new Set(prev); - if (newSet.has(field)) { - // ํ™•์žฅ ์ƒํƒœ โ†’ ๊ธฐ๋ณธ ๋„ˆ๋น„๋กœ ๋ณต๊ตฌ - newSet.delete(field); - setColumnWidths(prevWidths => ({ - ...prevWidths, - [field]: defaultWidths[field] || 120, - })); - } else { - // ๊ธฐ๋ณธ ์ƒํƒœ โ†’ ๋ฐ์ดํ„ฐ ๊ธฐ์ค€ auto-fit - newSet.add(field); - const autoWidth = calculateAutoFitWidth(field); - setColumnWidths(prevWidths => ({ - ...prevWidths, - [field]: autoWidth, - })); + // ํ—ค๋” ํ…์ŠคํŠธ ๋„ˆ๋น„ (๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์žˆ์œผ๋ฉด headerLabel ์‚ฌ์šฉ) + let headerText = column.label || field; + if (column.dynamicDataSource?.enabled && column.dynamicDataSource.options.length > 0) { + const activeOptionId = activeDataSources[field] || column.dynamicDataSource.defaultOptionId; + const activeOption = column.dynamicDataSource.options.find((opt) => opt.id === activeOptionId) + || column.dynamicDataSource.options[0]; + if (activeOption?.headerLabel) { + headerText = activeOption.headerLabel; } - return newSet; - }); + } + const headerWidth = measureTextWidth(headerText) + 32; // padding + ๋“œ๋กญ๋‹ค์šด ์•„์ด์ฝ˜ + + // ํ—ค๋”์™€ ๋ฐ์ดํ„ฐ ์ค‘ ํฐ ๊ฐ’ ์‚ฌ์šฉ + return Math.max(headerWidth, maxDataWidth); }; - // ๊ท ๋“ฑ ๋ถ„๋ฐฐ ํŠธ๋ฆฌ๊ฑฐ ๊ฐ์ง€ - useEffect(() => { - if (equalizeWidthsTrigger === undefined || equalizeWidthsTrigger === 0) return; - if (!containerRef.current) return; - - // ์‹ค์ œ ์ปจํ…Œ์ด๋„ˆ ๋„ˆ๋น„ ์ธก์ • - const containerWidth = containerRef.current.offsetWidth; - - // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ ๋„ˆ๋น„(40px) + ํ…Œ์ด๋ธ” border(2px) ์ œ์™ธํ•œ ๊ฐ€์šฉ ๋„ˆ๋น„ ๊ณ„์‚ฐ - const checkboxColumnWidth = 40; - const borderWidth = 2; - const availableWidth = containerWidth - checkboxColumnWidth - borderWidth; - - // ์ปฌ๋Ÿผ ์ˆ˜๋กœ ๋‚˜๋ˆ ์„œ ๊ท ๋“ฑ ๋ถ„๋ฐฐ (์ตœ์†Œ 60px ๋ณด์žฅ) + // ํ—ค๋” ๋”๋ธ”ํด๋ฆญ: ํ•ด๋‹น ์ปฌ๋Ÿผ๋งŒ ๊ธ€์ž ๋„ˆ๋น„์— ๋งž์ถค + const handleDoubleClick = (field: string) => { + const availableWidth = getAvailableWidth(); const equalWidth = Math.max(60, Math.floor(availableWidth / columns.length)); - + const contentWidth = calculateColumnContentWidth(field, equalWidth); + setColumnWidths((prev) => ({ + ...prev, + [field]: contentWidth, + })); + }; + + // ๊ท ๋“ฑ ๋ถ„๋ฐฐ: ์ปฌ๋Ÿผ ์ˆ˜๋กœ ํ…Œ์ด๋ธ” ๋„ˆ๋น„๋ฅผ ๊ท ๋“ฑ ๋ถ„๋ฐฐ + const applyEqualizeWidths = () => { + const availableWidth = getAvailableWidth(); + const equalWidth = Math.max(60, Math.floor(availableWidth / columns.length)); + const newWidths: Record = {}; columns.forEach((col) => { newWidths[col.field] = equalWidth; }); - + setColumnWidths(newWidths); - setExpandedColumns(new Set()); // ํ™•์žฅ ์ƒํƒœ ์ดˆ๊ธฐํ™” - setIsEqualizedMode(true); // ๊ท ๋“ฑ ๋ถ„๋ฐฐ ๋ชจ๋“œ ํ™œ์„ฑํ™” - }, [equalizeWidthsTrigger, columns]); - + }; + + // ์ž๋™ ๋งž์ถค: ๊ฐ ์ปฌ๋Ÿผ์„ ๊ธ€์ž ๋„ˆ๋น„์— ๋งž์ถ”๊ณ , ์ปจํ…Œ์ด๋„ˆ๋ณด๋‹ค ์ž‘์œผ๋ฉด ๋‚จ๋Š” ๊ณต๊ฐ„ ๋ถ„๋ฐฐ + const applyAutoFitWidths = () => { + if (columns.length === 0) return; + + // ๊ท ๋“ฑ ๋ถ„๋ฐฐ ๋„ˆ๋น„ ๊ณ„์‚ฐ (๊ฐ’์ด ์—†๋Š” ์ปฌ๋Ÿผ์˜ ์ตœ์†Œ๊ฐ’) + const availableWidth = getAvailableWidth(); + const equalWidth = Math.max(60, Math.floor(availableWidth / columns.length)); + + // 1. ๊ฐ ์ปฌ๋Ÿผ์˜ ๊ธ€์ž ๋„ˆ๋น„ ๊ณ„์‚ฐ (๊ฐ’์ด ์—†์œผ๋ฉด ๊ท ๋“ฑ ๋ถ„๋ฐฐ ๋„ˆ๋น„ ์‚ฌ์šฉ) + const newWidths: Record = {}; + columns.forEach((col) => { + newWidths[col.field] = calculateColumnContentWidth(col.field, equalWidth); + }); + + // 2. ์ปจํ…Œ์ด๋„ˆ ๋„ˆ๋น„์™€ ๋น„๊ต + const totalContentWidth = Object.values(newWidths).reduce((sum, w) => sum + w, 0); + + // 3. ์ปจํ…Œ์ด๋„ˆ๋ณด๋‹ค ์ž‘์œผ๋ฉด ๋‚จ๋Š” ๊ณต๊ฐ„์„ ๊ท ๋“ฑ ๋ถ„๋ฐฐ (ํ…Œ์ด๋ธ” ๊ฝ‰ ์ฐธ ์œ ์ง€) + if (totalContentWidth < availableWidth) { + const extraSpace = availableWidth - totalContentWidth; + const extraPerColumn = Math.floor(extraSpace / columns.length); + columns.forEach((col) => { + newWidths[col.field] += extraPerColumn; + }); + } + // ์ปจํ…Œ์ด๋„ˆ๋ณด๋‹ค ํฌ๋ฉด ๊ทธ๋Œ€๋กœ (์Šคํฌ๋กค ์ƒ์„ฑ๋จ) + + setColumnWidths(newWidths); + }; + + // ์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์‹œ ๊ท ๋“ฑ ๋ถ„๋ฐฐ ์ ์šฉ + useEffect(() => { + if (initializedRef.current) return; + if (!containerRef.current || columns.length === 0) return; + + const timer = setTimeout(() => { + applyEqualizeWidths(); + initializedRef.current = true; + }, 100); + + return () => clearTimeout(timer); + }, [columns]); + + // ํŠธ๋ฆฌ๊ฑฐ ๊ฐ์ง€: 1=๊ท ๋“ฑ๋ถ„๋ฐฐ, 2=์ž๋™๋งž์ถค + useEffect(() => { + if (equalizeWidthsTrigger === undefined || equalizeWidthsTrigger === 0) return; + + // ํ™€์ˆ˜๋ฉด ์ž๋™๋งž์ถค, ์ง์ˆ˜๋ฉด ๊ท ๋“ฑ๋ถ„๋ฐฐ (ํ† ๊ธ€ ๋ฐฉ์‹) + if (equalizeWidthsTrigger % 2 === 1) { + applyAutoFitWidths(); + } else { + applyEqualizeWidths(); + } + }, [equalizeWidthsTrigger]); + useEffect(() => { if (!resizing) return; - + const handleMouseMove = (e: MouseEvent) => { if (!resizing) return; const diff = e.clientX - resizing.startX; @@ -285,14 +345,14 @@ export function RepeaterTable({ [resizing.field]: newWidth, })); }; - + const handleMouseUp = () => { setResizing(null); }; - + document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); - + return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); @@ -336,13 +396,8 @@ export function RepeaterTable({ const isAllSelected = data.length > 0 && selectedRows.size === data.length; const isIndeterminate = selectedRows.size > 0 && selectedRows.size < data.length; - const renderCell = ( - row: any, - column: RepeaterColumnConfig, - rowIndex: number - ) => { - const isEditing = - editingCell?.rowIndex === rowIndex && editingCell?.field === column.field; + const renderCell = (row: any, column: RepeaterColumnConfig, rowIndex: number) => { + const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.field === column.field; const value = row[column.field]; // ๊ณ„์‚ฐ ํ•„๋“œ๋Š” ํŽธ์ง‘ ๋ถˆ๊ฐ€ @@ -359,14 +414,8 @@ export function RepeaterTable({ return num.toLocaleString("ko-KR"); } }; - - return ( -
- {column.type === "number" - ? formatNumber(value) - : value || "-"} -
- ); + + return
{column.type === "number" ? formatNumber(value) : value || "-"}
; } // ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ @@ -377,22 +426,22 @@ export function RepeaterTable({ if (value === undefined || value === null || value === "") return ""; const num = typeof value === "number" ? value : parseFloat(value); if (isNaN(num)) return ""; - // ์ •์ˆ˜๋ฉด ์†Œ์ˆ˜์  ์—†์ด, ์†Œ์ˆ˜๋ฉด ์†Œ์ˆ˜์  ์œ ์ง€ - if (Number.isInteger(num)) { - return num.toString(); - } else { - return num.toString(); - } + return num.toString(); })(); - + return ( - handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0) - } - className="h-8 text-xs border-gray-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 rounded-none min-w-0 w-full" + onChange={(e) => { + const val = e.target.value; + // ์ˆซ์ž์™€ ์†Œ์ˆ˜์ ๋งŒ ํ—ˆ์šฉ + if (val === "" || /^-?\d*\.?\d*$/.test(val)) { + handleCellEdit(rowIndex, column.field, val === "" ? 0 : parseFloat(val) || 0); + } + }} + className="h-8 w-full min-w-0 rounded-none border-gray-200 text-right text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500" /> ); @@ -414,25 +463,21 @@ export function RepeaterTable({ } return String(val); }; - + return ( - handleCellEdit(rowIndex, column.field, e.target.value)} - className="h-8 text-xs border-gray-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 rounded-none min-w-0 w-full" + onClick={(e) => (e.target as HTMLInputElement).showPicker?.()} + className="h-8 w-full min-w-0 cursor-pointer rounded-none border border-gray-200 bg-white px-2 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-inner-spin-button]:hidden" /> ); case "select": return ( - handleCellEdit(rowIndex, column.field, newValue)}> + @@ -451,7 +496,7 @@ export function RepeaterTable({ type="text" value={value || ""} onChange={(e) => handleCellEdit(rowIndex, column.field, e.target.value)} - className="h-8 text-xs border-gray-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 rounded-none min-w-0 w-full" + className="h-8 w-full min-w-0 rounded-none border-gray-200 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500" /> ); } @@ -461,126 +506,124 @@ export function RepeaterTable({ const sortableItems = data.map((_, idx) => `row-${idx}`); return ( - +
-
- +
sum + w, 0) + 74}px)`, + }} > - + {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค ํ—ค๋” */} - {/* ์ฒดํฌ๋ฐ•์Šค ํ—ค๋” */} - - {columns.map((col) => { - const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0; - const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId; - const activeOption = hasDynamicSource - ? col.dynamicDataSource!.options.find(opt => opt.id === activeOptionId) || col.dynamicDataSource!.options[0] - : null; - - const isExpanded = expandedColumns.has(col.field); - - return ( - - ); - })} + + ); + })} @@ -589,7 +632,7 @@ export function RepeaterTable({ @@ -600,19 +643,19 @@ export function RepeaterTable({ key={`row-${rowIndex}`} id={`row-${rowIndex}`} className={cn( - "hover:bg-blue-50/50 transition-colors", - selectedRows.has(rowIndex) && "bg-blue-50" + "transition-colors hover:bg-blue-50/50", + selectedRows.has(rowIndex) && "bg-blue-50", )} > {({ attributes, listeners, isDragging }) => ( <> {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค */} - {/* ์ฒดํฌ๋ฐ•์Šค */} - {/* ๋ฐ์ดํ„ฐ ์ปฌ๋Ÿผ๋“ค */} {columns.map((col) => ( - @@ -651,4 +697,3 @@ export function RepeaterTable({ ); } - diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index 6097aaf3..092c27c6 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -17,6 +17,7 @@ export interface ModalRepeaterTableProps { modalTitle: string; // ๋ชจ๋‹ฌ ์ œ๋ชฉ (์˜ˆ: "ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰ ๋ฐ ์„ ํƒ") modalButtonText?: string; // ๋ชจ๋‹ฌ ์—ด๊ธฐ ๋ฒ„ํŠผ ํ…์ŠคํŠธ (๊ธฐ๋ณธ: "ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰") multiSelect?: boolean; // ๋‹ค์ค‘ ์„ ํƒ ํ—ˆ์šฉ (๊ธฐ๋ณธ: true) + modalFilters?: ModalFilterConfig[]; // ๋ชจ๋‹ฌ ๋‚ด ํ•„ํ„ฐ ์„ค์ • // Repeater ํ…Œ์ด๋ธ” ์„ค์ • columns: RepeaterColumnConfig[]; // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์„ค์ • @@ -75,6 +76,7 @@ export interface DynamicDataSourceConfig { export interface DynamicDataSourceOption { id: string; label: string; // ํ‘œ์‹œ ๋ผ๋ฒจ (์˜ˆ: "๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€") + headerLabel?: string; // ํ—ค๋”์— ํ‘œ์‹œ๋  ์ „์ฒด ๋ผ๋ฒจ (์˜ˆ: "๋‹จ๊ฐ€ - ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€") // ์กฐํšŒ ๋ฐฉ์‹ sourceType: "table" | "multiTable" | "api"; @@ -175,6 +177,14 @@ export interface CalculationRule { dependencies: string[]; // ์˜์กดํ•˜๋Š” ํ•„๋“œ๋“ค } +// ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์„ค์ • (๊ฐ„์†Œํ™”๋œ ๋ฒ„์ „) +export interface ModalFilterConfig { + column: string; // ํ•„ํ„ฐ ๋Œ€์ƒ ์ปฌ๋Ÿผ (์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช…) + label: string; // ํ•„ํ„ฐ ๋ผ๋ฒจ (UI์— ํ‘œ์‹œ๋  ์ด๋ฆ„) + type: "select" | "text"; // select: ๋“œ๋กญ๋‹ค์šด (distinct ๊ฐ’), text: ํ…์ŠคํŠธ ์ž…๋ ฅ + defaultValue?: string; // ๊ธฐ๋ณธ๊ฐ’ +} + export interface ItemSelectionModalProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -188,4 +198,7 @@ export interface ItemSelectionModalProps { uniqueField?: string; onSelect: (items: Record[]) => void; columnLabels?: Record; // ์ปฌ๋Ÿผ๋ช… -> ๋ผ๋ฒจ๋ช… ๋งคํ•‘ + + // ๋ชจ๋‹ฌ ๋‚ด๋ถ€ ํ•„ํ„ฐ (์‚ฌ์šฉ์ž ์„ ํƒ ๊ฐ€๋Šฅ) + modalFilters?: ModalFilterConfig[]; } diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx new file mode 100644 index 00000000..224459f0 --- /dev/null +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -0,0 +1,884 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Plus, Columns, AlignJustify } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; + +// ๊ธฐ์กด ModalRepeaterTable ์ปดํฌ๋„ŒํŠธ ์žฌ์‚ฌ์šฉ +import { RepeaterTable } from "../modal-repeater-table/RepeaterTable"; +import { ItemSelectionModal } from "../modal-repeater-table/ItemSelectionModal"; +import { RepeaterColumnConfig, CalculationRule, DynamicDataSourceOption } from "../modal-repeater-table/types"; + +// ํƒ€์ž… ์ •์˜ +import { + TableSectionConfig, + TableColumnConfig, + ValueMappingConfig, + TableJoinCondition, + FormDataState, +} from "./types"; + +interface TableSectionRendererProps { + sectionId: string; + tableConfig: TableSectionConfig; + formData: FormDataState; + onFormDataChange: (field: string, value: any) => void; + onTableDataChange: (data: any[]) => void; + className?: string; +} + +/** + * TableColumnConfig๋ฅผ RepeaterColumnConfig๋กœ ๋ณ€ํ™˜ + * columnModes ๋˜๋Š” lookup์ด ์žˆ์œผ๋ฉด dynamicDataSource๋กœ ๋ณ€ํ™˜ + */ +function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { + const baseColumn: RepeaterColumnConfig = { + field: col.field, + label: col.label, + type: col.type, + editable: col.editable ?? true, + calculated: col.calculated ?? false, + width: col.width || "150px", + required: col.required, + defaultValue: col.defaultValue, + selectOptions: col.selectOptions, + // valueMapping์€ ๋ณ„๋„๋กœ ์ฒ˜๋ฆฌ + }; + + // lookup ์„ค์ •์„ dynamicDataSource๋กœ ๋ณ€ํ™˜ (์ƒˆ๋กœ์šด ์กฐํšŒ ๊ธฐ๋Šฅ) + if (col.lookup?.enabled && col.lookup.options && col.lookup.options.length > 0) { + baseColumn.dynamicDataSource = { + enabled: true, + options: col.lookup.options.map((option) => ({ + id: option.id, + // "์ปฌ๋Ÿผ๋ช… - ์˜ต์…˜๋ผ๋ฒจ" ํ˜•์‹์œผ๋กœ ํ—ค๋”์— ํ‘œ์‹œ + label: option.displayLabel || option.label, + // ํ—ค๋”์— ํ‘œ์‹œ๋  ์ „์ฒด ๋ผ๋ฒจ (์ปฌ๋Ÿผ๋ช… - ์˜ต์…˜๋ผ๋ฒจ) + headerLabel: `${col.label} - ${option.displayLabel || option.label}`, + sourceType: "table" as const, + tableConfig: { + tableName: option.tableName, + valueColumn: option.valueColumn, + joinConditions: option.conditions.map((cond) => ({ + sourceField: cond.sourceField, + targetField: cond.targetColumn, + // sourceType์— ๋”ฐ๋ฅธ ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜ ์„ค์ • + sourceType: cond.sourceType, // "currentRow" | "sectionField" | "externalTable" + fromFormData: cond.sourceType === "sectionField", + sectionId: cond.sectionId, + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ • (sourceType์ด "externalTable"์ธ ๊ฒฝ์šฐ) + externalLookup: cond.externalLookup, + // ๊ฐ’ ๋ณ€ํ™˜ ์„ค์ • ์ „๋‹ฌ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) + transform: cond.transform?.enabled ? { + tableName: cond.transform.tableName, + matchColumn: cond.transform.matchColumn, + resultColumn: cond.transform.resultColumn, + } : undefined, + })), + }, + // ์กฐํšŒ ์œ ํ˜• ์ •๋ณด ์ถ”๊ฐ€ + lookupType: option.type, + })), + defaultOptionId: col.lookup.options.find((o) => o.isDefault)?.id || col.lookup.options[0]?.id, + }; + } + // columnModes๋ฅผ dynamicDataSource๋กœ ๋ณ€ํ™˜ (๊ธฐ์กด ๋กœ์ง ์œ ์ง€) + else if (col.columnModes && col.columnModes.length > 0) { + baseColumn.dynamicDataSource = { + enabled: true, + options: col.columnModes.map((mode) => ({ + id: mode.id, + label: mode.label, + sourceType: "table" as const, + // ์‹ค์ œ ์กฐํšŒ ๋กœ์ง์€ TableSectionRenderer์—์„œ ์ฒ˜๋ฆฌ + tableConfig: { + tableName: mode.valueMapping?.externalRef?.tableName || "", + valueColumn: mode.valueMapping?.externalRef?.valueColumn || "", + joinConditions: (mode.valueMapping?.externalRef?.joinConditions || []).map((jc) => ({ + sourceField: jc.sourceField, + targetField: jc.targetColumn, + })), + }, + })), + defaultOptionId: col.columnModes.find((m) => m.isDefault)?.id || col.columnModes[0]?.id, + }; + } + + return baseColumn; +} + +/** + * TableCalculationRule์„ CalculationRule๋กœ ๋ณ€ํ™˜ + */ +function convertToCalculationRule(calc: { resultField: string; formula: string; dependencies: string[] }): CalculationRule { + return { + result: calc.resultField, + formula: calc.formula, + dependencies: calc.dependencies, + }; +} + +/** + * ๊ฐ’ ๋ณ€ํ™˜ ํ•จ์ˆ˜: ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ํ†ตํ•ด ๊ฐ’์„ ๋ณ€ํ™˜ + * ์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜ ์ด๋ฆ„ "(๋ฌด)ํ…Œ์ŠคํŠธ์—…์ฒด" โ†’ ๊ฑฐ๋ž˜์ฒ˜ ์ฝ”๋“œ "CUST-0002" + */ +async function transformValue( + value: any, + transform: { tableName: string; matchColumn: string; resultColumn: string } +): Promise { + if (!value || !transform.tableName || !transform.matchColumn || !transform.resultColumn) { + return value; + } + + try { + // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๊ฒ€์ƒ‰ + const response = await apiClient.post( + `/table-management/tables/${transform.tableName}/data`, + { + search: { + [transform.matchColumn]: { + value: value, + operator: "equals" + } + }, + size: 1, + page: 1 + } + ); + + if (response.data.success && response.data.data?.data?.length > 0) { + const transformedValue = response.data.data.data[0][transform.resultColumn]; + return transformedValue; + } + + console.warn(`๋ณ€ํ™˜ ์‹คํŒจ: ${transform.tableName}.${transform.matchColumn} = "${value}" ์ธ ํ–‰์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); + return undefined; + } catch (error) { + console.error("๊ฐ’ ๋ณ€ํ™˜ ์˜ค๋ฅ˜:", error); + return undefined; + } +} + +/** + * ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ์กฐ๊ฑด ๊ฐ’์„ ์กฐํšŒํ•˜๋Š” ํ•จ์ˆ˜ + * LookupCondition.sourceType์ด "externalTable"์ธ ๊ฒฝ์šฐ ์‚ฌ์šฉ + */ +async function fetchExternalLookupValue( + externalLookup: { + tableName: string; + matchColumn: string; + matchSourceType: "currentRow" | "sourceTable" | "sectionField"; + matchSourceField: string; + matchSectionId?: string; + resultColumn: string; + }, + rowData: any, + sourceData: any, + formData: FormDataState +): Promise { + // 1. ๋น„๊ต ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + let matchValue: any; + if (externalLookup.matchSourceType === "currentRow") { + matchValue = rowData[externalLookup.matchSourceField]; + } else if (externalLookup.matchSourceType === "sourceTable") { + matchValue = sourceData?.[externalLookup.matchSourceField]; + } else { + matchValue = formData[externalLookup.matchSourceField]; + } + + if (matchValue === undefined || matchValue === null || matchValue === "") { + console.warn(`์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ: ๋น„๊ต ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค. (${externalLookup.matchSourceType}.${externalLookup.matchSourceField})`); + return undefined; + } + + // 2. ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’ ์กฐํšŒ (์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๊ฒ€์ƒ‰) + try { + const response = await apiClient.post( + `/table-management/tables/${externalLookup.tableName}/data`, + { + search: { + [externalLookup.matchColumn]: { + value: matchValue, + operator: "equals" + } + }, + size: 1, + page: 1 + } + ); + + if (response.data.success && response.data.data?.data?.length > 0) { + return response.data.data.data[0][externalLookup.resultColumn]; + } + + console.warn(`์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ: ${externalLookup.tableName}.${externalLookup.matchColumn} = "${matchValue}" ์ธ ํ–‰์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); + return undefined; + } catch (error) { + console.error("์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์˜ค๋ฅ˜:", error); + return undefined; + } +} + +/** + * ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’์„ ์กฐํšŒํ•˜๋Š” ํ•จ์ˆ˜ + * + * @param tableName - ์กฐํšŒํ•  ํ…Œ์ด๋ธ”๋ช… + * @param valueColumn - ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ๋ช… + * @param joinConditions - ์กฐ์ธ ์กฐ๊ฑด ๋ชฉ๋ก + * @param rowData - ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ (์„ค์ •๋œ ์ปฌ๋Ÿผ ํ•„๋“œ) + * @param sourceData - ์›๋ณธ ์†Œ์Šค ๋ฐ์ดํ„ฐ (_sourceData) + * @param formData - ํผ ๋ฐ์ดํ„ฐ (๋‹ค๋ฅธ ์„น์…˜ ํ•„๋“œ) + */ +async function fetchExternalValue( + tableName: string, + valueColumn: string, + joinConditions: TableJoinCondition[], + rowData: any, + sourceData: any, + formData: FormDataState +): Promise { + if (joinConditions.length === 0) { + return undefined; + } + + try { + const whereConditions: Record = {}; + + for (const condition of joinConditions) { + let value: any; + + // ๊ฐ’ ์ถœ์ฒ˜์— ๋”ฐ๋ผ ๊ฐ€์ ธ์˜ค๊ธฐ (4๊ฐ€์ง€ ์†Œ์Šค ํƒ€์ž… ์ง€์›) + if (condition.sourceType === "row") { + // ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ (์„ค์ •๋œ ์ปฌ๋Ÿผ ํ•„๋“œ) + value = rowData[condition.sourceField]; + } else if (condition.sourceType === "sourceData") { + // ์›๋ณธ ์†Œ์Šค ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ (_sourceData) + value = sourceData?.[condition.sourceField]; + } else if (condition.sourceType === "formData") { + // formData์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ (๋‹ค๋ฅธ ์„น์…˜) + value = formData[condition.sourceField]; + } else if (condition.sourceType === "externalTable" && condition.externalLookup) { + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒํ•˜์—ฌ ๊ฐ€์ ธ์˜ค๊ธฐ + value = await fetchExternalLookupValue(condition.externalLookup, rowData, sourceData, formData); + } + + if (value === undefined || value === null || value === "") { + return undefined; + } + + // ๊ฐ’ ๋ณ€ํ™˜์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ (์˜ˆ: ์ด๋ฆ„ โ†’ ์ฝ”๋“œ) - ๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜ + if (condition.transform) { + value = await transformValue(value, condition.transform); + if (value === undefined) { + return undefined; + } + } + + // ์ˆซ์žํ˜• ID ๋ณ€ํ™˜ + let convertedValue = value; + if (condition.targetColumn.endsWith("_id") || condition.targetColumn === "id") { + const numValue = Number(value); + if (!isNaN(numValue)) { + convertedValue = numValue; + } + } + + // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๊ฒ€์ƒ‰์„ ์œ„ํ•ด operator: "equals" ์‚ฌ์šฉ + whereConditions[condition.targetColumn] = { + value: convertedValue, + operator: "equals" + }; + } + + // API ํ˜ธ์ถœ + const response = await apiClient.post( + `/table-management/tables/${tableName}/data`, + { search: whereConditions, size: 1, page: 1 } + ); + + if (response.data.success && response.data.data?.data?.length > 0) { + return response.data.data.data[0][valueColumn]; + } + + return undefined; + } catch (error) { + console.error("์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์˜ค๋ฅ˜:", error); + return undefined; + } +} + +/** + * ํ…Œ์ด๋ธ” ์„น์…˜ ๋ Œ๋”๋Ÿฌ + * UniversalFormModal ๋‚ด์—์„œ ํ…Œ์ด๋ธ” ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•˜๊ณ  ํŽธ์ง‘ + */ +export function TableSectionRenderer({ + sectionId, + tableConfig, + formData, + onFormDataChange, + onTableDataChange, + className, +}: TableSectionRendererProps) { + // ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ƒํƒœ + const [tableData, setTableData] = useState([]); + + // ๋ชจ๋‹ฌ ์ƒํƒœ + const [modalOpen, setModalOpen] = useState(false); + + // ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ ์ƒํƒœ + const [selectedRows, setSelectedRows] = useState>(new Set()); + + // ๋„ˆ๋น„ ์กฐ์ • ํŠธ๋ฆฌ๊ฑฐ (ํ™€์ˆ˜: ์ž๋™๋งž์ถค, ์ง์ˆ˜: ๊ท ๋“ฑ๋ถ„๋ฐฐ) + const [widthTrigger, setWidthTrigger] = useState(0); + + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ™œ์„ฑํ™” ์ƒํƒœ + const [activeDataSources, setActiveDataSources] = useState>({}); + + // ๋‚ ์งœ ์ผ๊ด„ ์ ์šฉ ์™„๋ฃŒ ํ”Œ๋ž˜๊ทธ (์ปฌ๋Ÿผ๋ณ„๋กœ ํ•œ ๋ฒˆ๋งŒ ์ ์šฉ) + const [batchAppliedFields, setBatchAppliedFields] = useState>(new Set()); + + // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ ํ”Œ๋ž˜๊ทธ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) + const initialDataLoadedRef = React.useRef(false); + + // formData์—์„œ ์ดˆ๊ธฐ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ _groupedData ํ‘œ์‹œ) + useEffect(() => { + // ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋˜์—ˆ์œผ๋ฉด ์Šคํ‚ต + if (initialDataLoadedRef.current) return; + + const tableSectionKey = `_tableSection_${sectionId}`; + const initialData = formData[tableSectionKey]; + + if (Array.isArray(initialData) && initialData.length > 0) { + console.log("[TableSectionRenderer] ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ:", { + sectionId, + itemCount: initialData.length, + }); + setTableData(initialData); + initialDataLoadedRef.current = true; + } + }, [sectionId, formData]); + + // RepeaterColumnConfig๋กœ ๋ณ€ํ™˜ + const columns: RepeaterColumnConfig[] = (tableConfig.columns || []).map(convertToRepeaterColumn); + + // ๊ณ„์‚ฐ ๊ทœ์น™ ๋ณ€ํ™˜ + const calculationRules: CalculationRule[] = (tableConfig.calculations || []).map(convertToCalculationRule); + + // ๊ณ„์‚ฐ ๋กœ์ง + const calculateRow = useCallback( + (row: any): any => { + if (calculationRules.length === 0) return row; + + const updatedRow = { ...row }; + + for (const rule of calculationRules) { + try { + let formula = rule.formula; + const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []; + const dependencies = rule.dependencies.length > 0 ? rule.dependencies : fieldMatches; + + for (const dep of dependencies) { + if (dep === rule.result) continue; + const value = parseFloat(row[dep]) || 0; + formula = formula.replace(new RegExp(`\\b${dep}\\b`, "g"), value.toString()); + } + + const result = new Function(`return ${formula}`)(); + updatedRow[rule.result] = result; + } catch (error) { + console.error(`๊ณ„์‚ฐ ์˜ค๋ฅ˜ (${rule.formula}):`, error); + updatedRow[rule.result] = 0; + } + } + + return updatedRow; + }, + [calculationRules] + ); + + const calculateAll = useCallback( + (data: any[]): any[] => { + return data.map((row) => calculateRow(row)); + }, + [calculateRow] + ); + + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (๋‚ ์งœ ์ผ๊ด„ ์ ์šฉ ๋กœ์ง ํฌํ•จ) + const handleDataChange = useCallback( + (newData: any[]) => { + let processedData = newData; + + // ๋‚ ์งœ ์ผ๊ด„ ์ ์šฉ ๋กœ์ง: batchApply๊ฐ€ ํ™œ์„ฑํ™”๋œ ๋‚ ์งœ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ + const batchApplyColumns = tableConfig.columns.filter( + (col) => col.type === "date" && col.batchApply === true + ); + + for (const dateCol of batchApplyColumns) { + // ์ด๋ฏธ ์ผ๊ด„ ์ ์šฉ๋œ ์ปฌ๋Ÿผ์€ ๊ฑด๋„ˆ๋œ€ + if (batchAppliedFields.has(dateCol.field)) continue; + + // ํ•ด๋‹น ์ปฌ๋Ÿผ์— ๊ฐ’์ด ์žˆ๋Š” ํ–‰๊ณผ ์—†๋Š” ํ–‰ ๋ถ„๋ฅ˜ + const itemsWithDate = processedData.filter((item) => item[dateCol.field]); + const itemsWithoutDate = processedData.filter((item) => !item[dateCol.field]); + + // ์กฐ๊ฑด: ์ •ํ™•ํžˆ 1๊ฐœ๋งŒ ๋‚ ์งœ๊ฐ€ ์žˆ๊ณ , ๋‚˜๋จธ์ง€๋Š” ๋ชจ๋‘ ๋น„์–ด์žˆ์„ ๋•Œ + if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) { + const selectedDate = itemsWithDate[0][dateCol.field]; + + // ๋ชจ๋“  ํ–‰์— ๋™์ผํ•œ ๋‚ ์งœ ์ ์šฉ + processedData = processedData.map((item) => ({ + ...item, + [dateCol.field]: selectedDate, + })); + + // ํ”Œ๋ž˜๊ทธ ํ™œ์„ฑํ™” (์ดํ›„ ๊ฐœ๋ณ„ ์ˆ˜์ • ๊ฐ€๋Šฅ) + setBatchAppliedFields((prev) => new Set([...prev, dateCol.field])); + } + } + + setTableData(processedData); + onTableDataChange(processedData); + }, + [onTableDataChange, tableConfig.columns, batchAppliedFields] + ); + + // ํ–‰ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleRowChange = useCallback( + (index: number, newRow: any) => { + const calculatedRow = calculateRow(newRow); + const newData = [...tableData]; + newData[index] = calculatedRow; + handleDataChange(newData); + }, + [tableData, calculateRow, handleDataChange] + ); + + // ํ–‰ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ + const handleRowDelete = useCallback( + (index: number) => { + const newData = tableData.filter((_, i) => i !== index); + handleDataChange(newData); + }, + [tableData, handleDataChange] + ); + + // ์„ ํƒ๋œ ํ•ญ๋ชฉ ์ผ๊ด„ ์‚ญ์ œ + const handleBulkDelete = useCallback(() => { + if (selectedRows.size === 0) return; + const newData = tableData.filter((_, index) => !selectedRows.has(index)); + handleDataChange(newData); + setSelectedRows(new Set()); + + // ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ์‚ญ์ œ๋˜๋ฉด ์ผ๊ด„ ์ ์šฉ ํ”Œ๋ž˜๊ทธ๋„ ๋ฆฌ์…‹ + if (newData.length === 0) { + setBatchAppliedFields(new Set()); + } + }, [tableData, selectedRows, handleDataChange]); + + // ์•„์ดํ…œ ์ถ”๊ฐ€ ํ•ธ๋“ค๋Ÿฌ (๋ชจ๋‹ฌ์—์„œ ์„ ํƒ) + const handleAddItems = useCallback( + async (items: any[]) => { + // ๊ฐ ์•„์ดํ…œ์— ๋Œ€ํ•ด valueMapping ์ ์šฉ + const mappedItems = await Promise.all( + items.map(async (sourceItem) => { + const newItem: any = {}; + + for (const col of tableConfig.columns) { + const mapping = col.valueMapping; + + // 0. lookup ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ (๋™์  ์กฐํšŒ) + if (col.lookup?.enabled && col.lookup.options && col.lookup.options.length > 0) { + // ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ์˜ต์…˜ ๋˜๋Š” ๊ธฐ๋ณธ ์˜ต์…˜ ์‚ฌ์šฉ + const activeOptionId = activeDataSources[col.field]; + const defaultOption = col.lookup.options.find((o) => o.isDefault) || col.lookup.options[0]; + const selectedOption = activeOptionId + ? col.lookup.options.find((o) => o.id === activeOptionId) || defaultOption + : defaultOption; + + if (selectedOption) { + // sameTable ํƒ€์ž…: ์†Œ์Šค ๋ฐ์ดํ„ฐ์—์„œ ์ง์ ‘ ๊ฐ’ ๋ณต์‚ฌ + if (selectedOption.type === "sameTable") { + const value = sourceItem[selectedOption.valueColumn]; + if (value !== undefined) { + newItem[col.field] = value; + } + // _sourceData์— ์›๋ณธ ์ €์žฅ (๋‚˜์ค‘์— ๋‹ค๋ฅธ ์˜ต์…˜์œผ๋กœ ์ „ํ™˜ ์‹œ ์‚ฌ์šฉ) + newItem._sourceData = sourceItem; + continue; + } + + // relatedTable, combinedLookup: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ + // ์กฐ์ธ ์กฐ๊ฑด ๊ตฌ์„ฑ (4๊ฐ€์ง€ ์†Œ์Šค ํƒ€์ž… ์ง€์›) + const joinConditions: TableJoinCondition[] = selectedOption.conditions.map((cond) => { + // sourceType ๋งคํ•‘ + let sourceType: "row" | "sourceData" | "formData" | "externalTable"; + if (cond.sourceType === "currentRow") { + sourceType = "row"; + } else if (cond.sourceType === "sourceTable") { + sourceType = "sourceData"; + } else if (cond.sourceType === "externalTable") { + sourceType = "externalTable"; + } else { + sourceType = "formData"; + } + + return { + sourceType, + sourceField: cond.sourceField, + targetColumn: cond.targetColumn, + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ • + externalLookup: cond.externalLookup, + // ๊ฐ’ ๋ณ€ํ™˜ ์„ค์ • ์ „๋‹ฌ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) + transform: cond.transform?.enabled ? { + tableName: cond.transform.tableName, + matchColumn: cond.transform.matchColumn, + resultColumn: cond.transform.resultColumn, + } : undefined, + }; + }); + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’ ์กฐํšŒ (sourceItem์ด _sourceData ์—ญํ• ) + const value = await fetchExternalValue( + selectedOption.tableName, + selectedOption.valueColumn, + joinConditions, + { ...sourceItem, ...newItem }, // rowData (ํ˜„์žฌ ํ–‰) + sourceItem, // sourceData (์†Œ์Šค ํ…Œ์ด๋ธ” ์›๋ณธ) + formData + ); + + if (value !== undefined) { + newItem[col.field] = value; + } + + // _sourceData์— ์›๋ณธ ์ €์žฅ + newItem._sourceData = sourceItem; + } + continue; + } + + // 1. ๋จผ์ € col.sourceField ํ™•์ธ (๊ฐ„๋‹จ ๋งคํ•‘) + if (!mapping && col.sourceField) { + // sourceField๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ + if (sourceItem[col.sourceField] !== undefined) { + newItem[col.field] = sourceItem[col.sourceField]; + } + continue; + } + + if (!mapping) { + // ๋งคํ•‘ ์—†์œผ๋ฉด ์†Œ์Šค์—์„œ ๋™์ผ ํ•„๋“œ๋ช…์œผ๋กœ ๋ณต์‚ฌ + if (sourceItem[col.field] !== undefined) { + newItem[col.field] = sourceItem[col.field]; + } + continue; + } + + // 2. valueMapping์ด ์žˆ๋Š” ๊ฒฝ์šฐ (๊ณ ๊ธ‰ ๋งคํ•‘) + switch (mapping.type) { + case "source": + // ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๋ณต์‚ฌ + const srcField = mapping.sourceField || col.sourceField || col.field; + if (sourceItem[srcField] !== undefined) { + newItem[col.field] = sourceItem[srcField]; + } + break; + + case "manual": + // ์‚ฌ์šฉ์ž ์ž…๋ ฅ (๋นˆ ๊ฐ’ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’) + newItem[col.field] = col.defaultValue ?? undefined; + break; + + case "internal": + // formData์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + if (mapping.internalField) { + newItem[col.field] = formData[mapping.internalField]; + } + break; + + case "external": + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ + if (mapping.externalRef) { + const { tableName, valueColumn, joinConditions } = mapping.externalRef; + const value = await fetchExternalValue( + tableName, + valueColumn, + joinConditions, + { ...sourceItem, ...newItem }, // rowData + sourceItem, // sourceData + formData + ); + if (value !== undefined) { + newItem[col.field] = value; + } + } + break; + } + + // ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ + if (col.defaultValue !== undefined && newItem[col.field] === undefined) { + newItem[col.field] = col.defaultValue; + } + } + + return newItem; + }) + ); + + // ๊ณ„์‚ฐ ํ•„๋“œ ์—…๋ฐ์ดํŠธ + const calculatedItems = calculateAll(mappedItems); + + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ + const newData = [...tableData, ...calculatedItems]; + handleDataChange(newData); + }, + [tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources] + ); + + // ์ปฌ๋Ÿผ ๋ชจ๋“œ/์กฐํšŒ ์˜ต์…˜ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleDataSourceChange = useCallback( + async (columnField: string, optionId: string) => { + setActiveDataSources((prev) => ({ + ...prev, + [columnField]: optionId, + })); + + // ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๋ชจ๋“  ํ–‰ ๋ฐ์ดํ„ฐ ์žฌ์กฐํšŒ + const column = tableConfig.columns.find((col) => col.field === columnField); + + // lookup ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ (์ƒˆ๋กœ์šด ์กฐํšŒ ๊ธฐ๋Šฅ) + if (column?.lookup?.enabled && column.lookup.options) { + const selectedOption = column.lookup.options.find((opt) => opt.id === optionId); + if (!selectedOption) return; + + // sameTable ํƒ€์ž…: ํ˜„์žฌ ํ–‰์˜ ์†Œ์Šค ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ’ ๋ณต์‚ฌ (์™ธ๋ถ€ ์กฐํšŒ ํ•„์š” ์—†์Œ) + if (selectedOption.type === "sameTable") { + const updatedData = tableData.map((row) => { + // sourceField์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์™€ ํ•ด๋‹น ์ปฌ๋Ÿผ์— ๋ณต์‚ฌ + // row์— _sourceData๊ฐ€ ์žˆ์œผ๋ฉด ๊ฑฐ๊ธฐ์„œ, ์—†์œผ๋ฉด row ์ž์ฒด์—์„œ ๊ฐ€์ ธ์˜ด + const sourceData = row._sourceData || row; + const newValue = sourceData[selectedOption.valueColumn] ?? row[columnField]; + return { ...row, [columnField]: newValue }; + }); + + const calculatedData = calculateAll(updatedData); + handleDataChange(calculatedData); + return; + } + + // ๋ชจ๋“  ํ–‰์— ๋Œ€ํ•ด ์ƒˆ ๊ฐ’ ์กฐํšŒ + const updatedData = await Promise.all( + tableData.map(async (row) => { + let newValue: any = row[columnField]; + + // ์กฐ์ธ ์กฐ๊ฑด ๊ตฌ์„ฑ (4๊ฐ€์ง€ ์†Œ์Šค ํƒ€์ž… ์ง€์›) + const joinConditions: TableJoinCondition[] = selectedOption.conditions.map((cond) => { + // sourceType ๋งคํ•‘ + let sourceType: "row" | "sourceData" | "formData" | "externalTable"; + if (cond.sourceType === "currentRow") { + sourceType = "row"; + } else if (cond.sourceType === "sourceTable") { + sourceType = "sourceData"; + } else if (cond.sourceType === "externalTable") { + sourceType = "externalTable"; + } else { + sourceType = "formData"; + } + + return { + sourceType, + sourceField: cond.sourceField, + targetColumn: cond.targetColumn, + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ • + externalLookup: cond.externalLookup, + // ๊ฐ’ ๋ณ€ํ™˜ ์„ค์ • ์ „๋‹ฌ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) + transform: cond.transform?.enabled ? { + tableName: cond.transform.tableName, + matchColumn: cond.transform.matchColumn, + resultColumn: cond.transform.resultColumn, + } : undefined, + }; + }); + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’ ์กฐํšŒ (_sourceData ์ „๋‹ฌ) + const sourceData = row._sourceData || row; + const value = await fetchExternalValue( + selectedOption.tableName, + selectedOption.valueColumn, + joinConditions, + row, + sourceData, + formData + ); + + if (value !== undefined) { + newValue = value; + } + + return { ...row, [columnField]: newValue }; + }) + ); + + // ๊ณ„์‚ฐ ํ•„๋“œ ์—…๋ฐ์ดํŠธ + const calculatedData = calculateAll(updatedData); + handleDataChange(calculatedData); + return; + } + + // ๊ธฐ์กด columnModes ์ฒ˜๋ฆฌ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) + if (!column?.columnModes) return; + + const selectedMode = column.columnModes.find((mode) => mode.id === optionId); + if (!selectedMode) return; + + // ๋ชจ๋“  ํ–‰์— ๋Œ€ํ•ด ์ƒˆ ๊ฐ’ ์กฐํšŒ + const updatedData = await Promise.all( + tableData.map(async (row) => { + const mapping = selectedMode.valueMapping; + let newValue: any = row[columnField]; + const sourceData = row._sourceData || row; + + if (mapping.type === "external" && mapping.externalRef) { + const { tableName, valueColumn, joinConditions } = mapping.externalRef; + const value = await fetchExternalValue(tableName, valueColumn, joinConditions, row, sourceData, formData); + if (value !== undefined) { + newValue = value; + } + } else if (mapping.type === "source" && mapping.sourceField) { + newValue = row[mapping.sourceField]; + } else if (mapping.type === "internal" && mapping.internalField) { + newValue = formData[mapping.internalField]; + } + + return { ...row, [columnField]: newValue }; + }) + ); + + // ๊ณ„์‚ฐ ํ•„๋“œ ์—…๋ฐ์ดํŠธ + const calculatedData = calculateAll(updatedData); + handleDataChange(calculatedData); + }, + [tableConfig.columns, tableData, formData, calculateAll, handleDataChange] + ); + + // ์†Œ์Šค ํ…Œ์ด๋ธ” ์ •๋ณด + const { source, filters, uiConfig } = tableConfig; + const sourceTable = source.tableName; + const sourceColumns = source.displayColumns; + const sourceSearchFields = source.searchColumns; + const columnLabels = source.columnLabels || {}; + const modalTitle = uiConfig?.modalTitle || "ํ•ญ๋ชฉ ๊ฒ€์ƒ‰ ๋ฐ ์„ ํƒ"; + const addButtonText = uiConfig?.addButtonText || "ํ•ญ๋ชฉ ๊ฒ€์ƒ‰"; + const multiSelect = uiConfig?.multiSelect ?? true; + + // ๊ธฐ๋ณธ ํ•„ํ„ฐ ์กฐ๊ฑด ์ƒ์„ฑ (์‚ฌ์ „ ํ•„ํ„ฐ๋งŒ - ๋ชจ๋‹ฌ ํ•„ํ„ฐ๋Š” ItemSelectionModal์—์„œ ์ฒ˜๋ฆฌ) + const baseFilterCondition: Record = {}; + if (filters?.preFilters) { + for (const filter of filters.preFilters) { + // ๊ฐ„๋‹จํ•œ "=" ์—ฐ์‚ฐ์ž๋งŒ ์ฒ˜๋ฆฌ (ํ™•์žฅ ๊ฐ€๋Šฅ) + if (filter.operator === "=") { + baseFilterCondition[filter.column] = filter.value; + } + } + } + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์„ค์ •์„ ItemSelectionModal์— ์ „๋‹ฌํ•  ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const modalFiltersForModal = useMemo(() => { + if (!filters?.modalFilters) return []; + return filters.modalFilters.map((filter) => ({ + column: filter.column, + label: filter.label || filter.column, + // category ํƒ€์ž…์„ select๋กœ ๋ณ€ํ™˜ (ModalFilterConfig ํ˜ธํ™˜) + type: filter.type === "category" ? "select" as const : filter.type as "text" | "select", + options: filter.options, + categoryRef: filter.categoryRef, + defaultValue: filter.defaultValue, + })); + }, [filters?.modalFilters]); + + return ( +
+ {/* ์ถ”๊ฐ€ ๋ฒ„ํŠผ ์˜์—ญ */} +
+
+ + {tableData.length > 0 && `${tableData.length}๊ฐœ ํ•ญ๋ชฉ`} + {selectedRows.size > 0 && ` (${selectedRows.size}๊ฐœ ์„ ํƒ๋จ)`} + + {columns.length > 0 && ( + + )} +
+
+ {selectedRows.size > 0 && ( + + )} + +
+
+ + {/* Repeater ํ…Œ์ด๋ธ” */} + + + {/* ํ•ญ๋ชฉ ์„ ํƒ ๋ชจ๋‹ฌ */} + +
+ ); +} diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 03c2efb8..bc217299 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -38,6 +38,7 @@ import { OptionalFieldGroupConfig, } from "./types"; import { defaultConfig, generateUniqueId } from "./config"; +import { TableSectionRenderer } from "./TableSectionRenderer"; /** * ๐Ÿ”— ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด Select ํ•„๋“œ ์ปดํฌ๋„ŒํŠธ @@ -194,6 +195,10 @@ export function UniversalFormModalComponent({ // ๋กœ๋”ฉ ์ƒํƒœ const [saving, setSaving] = useState(false); + // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (INSERT/UPDATE/DELETE ์ถ”์ ์šฉ) + const [originalGroupedData, setOriginalGroupedData] = useState([]); + const groupedDataInitializedRef = useRef(false); + // ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ const [deleteDialog, setDeleteDialog] = useState<{ open: boolean; @@ -303,6 +308,12 @@ export function UniversalFormModalComponent({ console.log(`[UniversalFormModal] ๋ฐ˜๋ณต ์„น์…˜ ๋ณ‘ํ•ฉ: ${sectionKey}`, items); } } + + // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (UPDATE/DELETE ์ถ”์ ์šฉ) + if (originalGroupedData.length > 0) { + event.detail.formData._originalGroupedData = originalGroupedData; + console.log(`[UniversalFormModal] ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ: ${originalGroupedData.length}๊ฐœ`); + } }; window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener); @@ -310,7 +321,37 @@ export function UniversalFormModalComponent({ return () => { window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener); }; - }, [formData, repeatSections, config.sections]); + }, [formData, repeatSections, config.sections, originalGroupedData]); + + // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: _groupedData๊ฐ€ ์žˆ์œผ๋ฉด ํ…Œ์ด๋ธ” ์„น์…˜ ์ดˆ๊ธฐํ™” + useEffect(() => { + if (!_groupedData || _groupedData.length === 0) return; + if (groupedDataInitializedRef.current) return; // ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋จ + + // ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜ ์ฐพ๊ธฐ + const tableSection = config.sections.find((s) => s.type === "table"); + if (!tableSection) { + console.log("[UniversalFormModal] ํ…Œ์ด๋ธ” ์„น์…˜ ์—†์Œ - _groupedData ๋ฌด์‹œ"); + return; + } + + console.log("[UniversalFormModal] ์ˆ˜์ • ๋ชจ๋“œ - ํ…Œ์ด๋ธ” ์„น์…˜ ์ดˆ๊ธฐํ™”:", { + sectionId: tableSection.id, + itemCount: _groupedData.length, + }); + + // ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (์ˆ˜์ •/์‚ญ์ œ ์ถ”์ ์šฉ) + setOriginalGroupedData(JSON.parse(JSON.stringify(_groupedData))); + + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์„ค์ • + const tableSectionKey = `_tableSection_${tableSection.id}`; + setFormData((prev) => ({ + ...prev, + [tableSectionKey]: _groupedData, + })); + + groupedDataInitializedRef.current = true; + }, [_groupedData, config.sections]); // ํ•„๋“œ ๋ ˆ๋ฒจ linkedFieldGroup ๋ฐ์ดํ„ฐ ๋กœ๋“œ useEffect(() => { @@ -372,9 +413,12 @@ export function UniversalFormModalComponent({ items.push(createRepeatItem(section, i)); } newRepeatSections[section.id] = items; + } else if (section.type === "table") { + // ํ…Œ์ด๋ธ” ์„น์…˜์€ ํ•„๋“œ ์ดˆ๊ธฐํ™” ์Šคํ‚ต (TableSectionRenderer์—์„œ ์ฒ˜๋ฆฌ) + continue; } else { // ์ผ๋ฐ˜ ์„น์…˜ ํ•„๋“œ ์ดˆ๊ธฐํ™” - for (const field of section.fields || []) { + for (const field of (section.fields || [])) { // ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • let value = field.defaultValue ?? ""; @@ -448,7 +492,7 @@ export function UniversalFormModalComponent({ _index: index, }; - for (const field of section.fields || []) { + for (const field of (section.fields || [])) { item[field.columnName] = field.defaultValue ?? ""; } @@ -479,9 +523,9 @@ export function UniversalFormModalComponent({ let hasChanges = false; for (const section of config.sections) { - if (section.repeatable) continue; + if (section.repeatable || section.type === "table") continue; - for (const field of section.fields || []) { + for (const field of (section.fields || [])) { if ( field.numberingRule?.enabled && field.numberingRule?.generateOnOpen && @@ -781,9 +825,9 @@ export function UniversalFormModalComponent({ const missingFields: string[] = []; for (const section of config.sections) { - if (section.repeatable) continue; // ๋ฐ˜๋ณต ์„น์…˜์€ ๋ณ„๋„ ๊ฒ€์ฆ + if (section.repeatable || section.type === "table") continue; // ๋ฐ˜๋ณต ์„น์…˜ ๋ฐ ํ…Œ์ด๋ธ” ์„น์…˜์€ ๋ณ„๋„ ๊ฒ€์ฆ - for (const field of section.fields || []) { + for (const field of (section.fields || [])) { if (field.required && !field.hidden && !field.numberingRule?.hidden) { const value = formData[field.columnName]; if (value === undefined || value === null || value === "") { @@ -799,17 +843,28 @@ export function UniversalFormModalComponent({ // ๋‹จ์ผ ํ–‰ ์ €์žฅ const saveSingleRow = useCallback(async () => { const dataToSave = { ...formData }; + + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ถ”์ถœ (๋ณ„๋„ ์ €์žฅ์šฉ) + const tableSectionData: Record = {}; // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ•„๋“œ ์ œ๊ฑฐ (์ฑ„๋ฒˆ ๊ทœ์น™ ID๋Š” ์œ ์ง€ - buttonActions.ts์—์„œ ์‚ฌ์šฉ) Object.keys(dataToSave).forEach((key) => { - if (key.startsWith("_") && !key.includes("_numberingRuleId")) { + if (key.startsWith("_tableSection_")) { + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ๋Š” ๋ณ„๋„๋กœ ์ €์žฅ + const sectionId = key.replace("_tableSection_", ""); + tableSectionData[sectionId] = dataToSave[key] || []; + delete dataToSave[key]; + } else if (key.startsWith("_") && !key.includes("_numberingRuleId")) { delete dataToSave[key]; } }); // ์ €์žฅ ์‹œ์  ์ฑ„๋ฒˆ๊ทœ์น™ ์ฒ˜๋ฆฌ (generateOnSave๋งŒ ์ฒ˜๋ฆฌ) for (const section of config.sections) { - for (const field of section.fields || []) { + // ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ + if (section.type === "table") continue; + + for (const field of (section.fields || [])) { if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) { const response = await allocateNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { @@ -822,12 +877,140 @@ export function UniversalFormModalComponent({ } } + // ํ…Œ์ด๋ธ” ์„น์…˜์ด ์žˆ๊ณ  ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ํ’ˆ๋ชฉ๋ณ„๋กœ ์ €์žฅํ•˜๋Š” ๊ฒฝ์šฐ (๊ณตํ†ต + ๊ฐœ๋ณ„ ๋ณ‘ํ•ฉ ์ €์žฅ) + // targetTable์ด ์—†๊ฑฐ๋‚˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ + const tableSectionsForMainTable = config.sections.filter( + (s) => s.type === "table" && + (!s.tableConfig?.saveConfig?.targetTable || + s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName) + ); + + if (tableSectionsForMainTable.length > 0) { + // ๊ณตํ†ต ์ €์žฅ ํ•„๋“œ ์ˆ˜์ง‘ (sectionSaveModes ์„ค์ •์— ๋”ฐ๋ผ) + const commonFieldsData: Record = {}; + const { sectionSaveModes } = config.saveConfig; + + // ํ•„๋“œ ํƒ€์ž… ์„น์…˜์—์„œ ๊ณตํ†ต ์ €์žฅ ํ•„๋“œ ์ˆ˜์ง‘ + for (const section of config.sections) { + if (section.type === "table") continue; + + const sectionMode = sectionSaveModes?.find((s) => s.sectionId === section.id); + const defaultMode = "common"; // ํ•„๋“œ ํƒ€์ž… ์„น์…˜์˜ ๊ธฐ๋ณธ๊ฐ’์€ ๊ณตํ†ต ์ €์žฅ + const sectionSaveMode = sectionMode?.saveMode || defaultMode; + + if (section.fields) { + for (const field of section.fields) { + const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); + const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; + + if (fieldSaveMode === "common" && dataToSave[field.columnName] !== undefined) { + commonFieldsData[field.columnName] = dataToSave[field.columnName]; + } + } + } + } + + // ๊ฐ ํ…Œ์ด๋ธ” ์„น์…˜์˜ ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ์— ๊ณตํ†ต ํ•„๋“œ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ €์žฅ + for (const tableSection of tableSectionsForMainTable) { + const sectionData = tableSectionData[tableSection.id] || []; + + if (sectionData.length > 0) { + // ํ’ˆ๋ชฉ๋ณ„๋กœ ํ–‰ ์ €์žฅ + for (const item of sectionData) { + const rowToSave = { ...commonFieldsData, ...item }; + + // _sourceData ๋“ฑ ๋‚ด๋ถ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ๊ฑฐ + Object.keys(rowToSave).forEach((key) => { + if (key.startsWith("_")) { + delete rowToSave[key]; + } + }); + + const response = await apiClient.post( + `/table-management/tables/${config.saveConfig.tableName}/add`, + rowToSave + ); + + if (!response.data?.success) { + throw new Error(response.data?.message || "ํ’ˆ๋ชฉ ์ €์žฅ ์‹คํŒจ"); + } + } + + // ์ด๋ฏธ ์ €์žฅํ–ˆ์œผ๋ฏ€๋กœ ์•„๋ž˜ ๋กœ์ง์—์„œ ๋‹ค์‹œ ์ €์žฅํ•˜์ง€ ์•Š๋„๋ก ์ œ๊ฑฐ + delete tableSectionData[tableSection.id]; + } + } + + // ํ’ˆ๋ชฉ์ด ์—†์œผ๋ฉด ๊ณตํ†ต ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅํ•˜์ง€ ์•Š์Œ (ํ’ˆ๋ชฉ์ด ํ•„์š”ํ•œ ํ™”๋ฉด์ด๋ฏ€๋กœ) + // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์„น์…˜์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋ฉ”์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ + const hasOtherTableSections = Object.keys(tableSectionData).length > 0; + if (!hasOtherTableSections) { + return; // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•  ํ’ˆ๋ชฉ์ด ์—†์œผ๋ฉด ์ข…๋ฃŒ + } + } + + // ๋ฉ”์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ (ํ…Œ์ด๋ธ” ์„น์…˜์ด ์—†๊ฑฐ๋‚˜ ๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๊ฒฝ์šฐ) const response = await apiClient.post(`/table-management/tables/${config.saveConfig.tableName}/add`, dataToSave); if (!response.data?.success) { throw new Error(response.data?.message || "์ €์žฅ ์‹คํŒจ"); } - }, [config.sections, config.saveConfig.tableName, formData]); + + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ €์žฅ (๋ณ„๋„ ํ…Œ์ด๋ธ”์—) + for (const section of config.sections) { + if (section.type === "table" && section.tableConfig?.saveConfig?.targetTable) { + const sectionData = tableSectionData[section.id]; + if (sectionData && sectionData.length > 0) { + // ๋ฉ”์ธ ๋ ˆ์ฝ”๋“œ ID๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ (response.data์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ) + const mainRecordId = response.data?.data?.id; + + // ๊ณตํ†ต ์ €์žฅ ํ•„๋“œ ์ˆ˜์ง‘ (sectionSaveModes ์„ค์ •์— ๋”ฐ๋ผ) + const commonFieldsData: Record = {}; + const { sectionSaveModes } = config.saveConfig; + + if (sectionSaveModes && sectionSaveModes.length > 0) { + // ๋‹ค๋ฅธ ์„น์…˜์—์„œ ๊ณตํ†ต ์ €์žฅ์œผ๋กœ ์„ค์ •๋œ ํ•„๋“œ ๊ฐ’ ์ˆ˜์ง‘ + for (const otherSection of config.sections) { + if (otherSection.id === section.id) continue; // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์„น์…˜์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ + + const sectionMode = sectionSaveModes.find((s) => s.sectionId === otherSection.id); + const defaultMode = otherSection.type === "table" ? "individual" : "common"; + const sectionSaveMode = sectionMode?.saveMode || defaultMode; + + // ํ•„๋“œ ํƒ€์ž… ์„น์…˜์˜ ํ•„๋“œ๋“ค ์ฒ˜๋ฆฌ + if (otherSection.type !== "table" && otherSection.fields) { + for (const field of otherSection.fields) { + // ํ•„๋“œ๋ณ„ ์˜ค๋ฒ„๋ผ์ด๋“œ ํ™•์ธ + const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); + const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; + + // ๊ณตํ†ต ์ €์žฅ์ด๋ฉด formData์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์™€ ๋ชจ๋“  ํ’ˆ๋ชฉ์— ์ ์šฉ + if (fieldSaveMode === "common" && formData[field.columnName] !== undefined) { + commonFieldsData[field.columnName] = formData[field.columnName]; + } + } + } + } + } + + for (const item of sectionData) { + // ๊ณตํ†ต ํ•„๋“œ ๋ณ‘ํ•ฉ + ๊ฐœ๋ณ„ ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ + const itemToSave = { ...commonFieldsData, ...item }; + + // ๋ฉ”์ธ ๋ ˆ์ฝ”๋“œ์™€ ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ + if (mainRecordId && config.saveConfig.primaryKeyColumn) { + itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId; + } + + await apiClient.post( + `/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`, + itemToSave + ); + } + } + } + } + }, [config.sections, config.saveConfig.tableName, config.saveConfig.primaryKeyColumn, config.saveConfig.sectionSaveModes, formData]); // ๋‹ค์ค‘ ํ–‰ ์ €์žฅ (๊ฒธ์ง ๋“ฑ) const saveMultipleRows = useCallback(async () => { @@ -901,9 +1084,9 @@ export function UniversalFormModalComponent({ // ์ €์žฅ ์‹œ์  ์ฑ„๋ฒˆ๊ทœ์น™ ์ฒ˜๋ฆฌ (๋ฉ”์ธ ํ–‰๋งŒ) for (const section of config.sections) { - if (section.repeatable) continue; + if (section.repeatable || section.type === "table") continue; - for (const field of section.fields || []) { + for (const field of (section.fields || [])) { if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // generateOnSave ๋˜๋Š” generateOnOpen ๋ชจ๋‘ ์ €์žฅ ์‹œ ์‹ค์ œ ์ˆœ๋ฒˆ ํ• ๋‹น const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen; @@ -951,7 +1134,7 @@ export function UniversalFormModalComponent({ // 1. ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ const mainData: Record = {}; config.sections.forEach((section) => { - if (section.repeatable) return; // ๋ฐ˜๋ณต ์„น์…˜์€ ์ œ์™ธ + if (section.repeatable || section.type === "table") return; // ๋ฐ˜๋ณต ์„น์…˜ ๋ฐ ํ…Œ์ด๋ธ” ํƒ€์ž… ์ œ์™ธ (section.fields || []).forEach((field) => { const value = formData[field.columnName]; if (value !== undefined && value !== null && value !== "") { @@ -962,9 +1145,9 @@ export function UniversalFormModalComponent({ // 1-1. ์ฑ„๋ฒˆ๊ทœ์น™ ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ํ• ๋‹น) for (const section of config.sections) { - if (section.repeatable) continue; + if (section.repeatable || section.type === "table") continue; - for (const field of section.fields || []) { + for (const field of (section.fields || [])) { // ์ฑ„๋ฒˆ๊ทœ์น™์ด ํ™œ์„ฑํ™”๋œ ํ•„๋“œ ์ฒ˜๋ฆฌ if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // ์‹ ๊ทœ ์ƒ์„ฑ์ด๊ฑฐ๋‚˜ ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ฑ„๋ฒˆ @@ -1054,7 +1237,7 @@ export function UniversalFormModalComponent({ // ๋˜๋Š” ๋ฉ”์ธ ์„น์…˜์˜ ํ•„๋“œ ์ค‘ ๊ฐ™์€ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด ๋งคํ•‘ else { config.sections.forEach((section) => { - if (section.repeatable) return; + if (section.repeatable || section.type === "table") return; const matchingField = (section.fields || []).find((f) => f.columnName === mapping.targetColumn); if (matchingField && mainData[matchingField.columnName] !== undefined) { mainFieldMappings!.push({ @@ -1535,10 +1718,36 @@ export function UniversalFormModalComponent({ const isCollapsed = collapsedSections.has(section.id); const sectionColumns = section.columns || 2; + // ๋ฐ˜๋ณต ์„น์…˜ if (section.repeatable) { return renderRepeatableSection(section, isCollapsed); } + // ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜ + if (section.type === "table" && section.tableConfig) { + return ( + + + {section.title} + {section.description && {section.description}} + + + { + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ๋ฅผ formData์— ์ €์žฅ + handleFieldChange(`_tableSection_${section.id}`, data); + }} + /> + + + ); + } + + // ๊ธฐ๋ณธ ํ•„๋“œ ํƒ€์ž… ์„น์…˜ return ( {section.collapsible ? ( diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index 98cbc248..4ef28d6f 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -17,6 +17,7 @@ import { Settings, Database, Layout, + Table, } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -27,9 +28,11 @@ import { FormSectionConfig, FormFieldConfig, MODAL_SIZE_OPTIONS, + SECTION_TYPE_OPTIONS, } from "./types"; import { defaultSectionConfig, + defaultTableSectionConfig, generateSectionId, } from "./config"; @@ -37,6 +40,7 @@ import { import { FieldDetailSettingsModal } from "./modals/FieldDetailSettingsModal"; import { SaveSettingsModal } from "./modals/SaveSettingsModal"; import { SectionLayoutModal } from "./modals/SectionLayoutModal"; +import { TableSectionSettingsModal } from "./modals/TableSectionSettingsModal"; // ๋„์›€๋ง ํ…์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ const HelpText = ({ children }: { children: React.ReactNode }) => ( @@ -57,6 +61,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor const [saveSettingsModalOpen, setSaveSettingsModalOpen] = useState(false); const [sectionLayoutModalOpen, setSectionLayoutModalOpen] = useState(false); const [fieldDetailModalOpen, setFieldDetailModalOpen] = useState(false); + const [tableSectionSettingsModalOpen, setTableSectionSettingsModalOpen] = useState(false); const [selectedSection, setSelectedSection] = useState(null); const [selectedField, setSelectedField] = useState(null); @@ -95,23 +100,25 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); - const data = response.data?.data; + // API ์‘๋‹ต ๊ตฌ์กฐ: { success, data: { columns: [...], total, page, ... } } + const columns = response.data?.data?.columns; - if (response.data?.success && Array.isArray(data)) { + if (response.data?.success && Array.isArray(columns)) { setTableColumns((prev) => ({ ...prev, - [tableName]: data.map( + [tableName]: columns.map( (c: { columnName?: string; column_name?: string; dataType?: string; data_type?: string; + displayName?: string; columnComment?: string; column_comment?: string; }) => ({ name: c.columnName || c.column_name || "", type: c.dataType || c.data_type || "text", - label: c.columnComment || c.column_comment || c.columnName || c.column_name || "", + label: c.displayName || c.columnComment || c.column_comment || c.columnName || c.column_name || "", }), ), })); @@ -159,17 +166,55 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor ); // ์„น์…˜ ๊ด€๋ฆฌ - const addSection = useCallback(() => { + const addSection = useCallback((type: "fields" | "table" = "fields") => { const newSection: FormSectionConfig = { ...defaultSectionConfig, id: generateSectionId(), - title: `์„น์…˜ ${config.sections.length + 1}`, + title: type === "table" ? `ํ…Œ์ด๋ธ” ์„น์…˜ ${config.sections.length + 1}` : `์„น์…˜ ${config.sections.length + 1}`, + type, + fields: type === "fields" ? [] : undefined, + tableConfig: type === "table" ? { ...defaultTableSectionConfig } : undefined, }; onChange({ ...config, sections: [...config.sections, newSection], }); }, [config, onChange]); + + // ์„น์…˜ ํƒ€์ž… ๋ณ€๊ฒฝ + const changeSectionType = useCallback( + (sectionId: string, newType: "fields" | "table") => { + onChange({ + ...config, + sections: config.sections.map((s) => { + if (s.id !== sectionId) return s; + + if (newType === "table") { + return { + ...s, + type: "table", + fields: undefined, + tableConfig: { ...defaultTableSectionConfig }, + }; + } else { + return { + ...s, + type: "fields", + fields: [], + tableConfig: undefined, + }; + } + }), + }); + }, + [config, onChange] + ); + + // ํ…Œ์ด๋ธ” ์„น์…˜ ์„ค์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ + const handleOpenTableSectionSettings = (section: FormSectionConfig) => { + setSelectedSection(section); + setTableSectionSettingsModalOpen(true); + }; const updateSection = useCallback( (sectionId: string, updates: Partial) => { @@ -365,39 +410,56 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor - + {/* ์„น์…˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ๋“ค */} +
+ +
+ ์ˆœ์„œ + handleDoubleClick(col.field)} - title={isExpanded ? "๋”๋ธ”ํด๋ฆญํ•˜์—ฌ ๊ธฐ๋ณธ ๋„ˆ๋น„๋กœ ๋ณต๊ตฌ" : "๋”๋ธ”ํด๋ฆญํ•˜์—ฌ ๋‚ด์šฉ์— ๋งž๊ฒŒ ํ™•์žฅ"} - > -
-
- {hasDynamicSource ? ( - setOpenPopover(open ? col.field : null)} - > - - - - { + const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0; + const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId; + const activeOption = hasDynamicSource + ? col.dynamicDataSource!.options.find((opt) => opt.id === activeOptionId) || + col.dynamicDataSource!.options[0] + : null; + + return ( +
handleDoubleClick(col.field)} + title="๋”๋ธ”ํด๋ฆญํ•˜์—ฌ ๊ธ€์ž ๋„ˆ๋น„์— ๋งž์ถค" + > +
+
+ {hasDynamicSource ? ( + setOpenPopover(open ? col.field : null)} > -
- ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ -
- {col.dynamicDataSource!.options.map((option) => ( + - ))} - -
- ) : ( - <> - {col.label} - {col.required && *} - - )} + + +
+ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ +
+ {col.dynamicDataSource!.options.map((option) => ( + + ))} +
+ + ) : ( + <> + {col.label} + {col.required && *} + + )} +
+ {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค */} +
handleMouseDown(e, col.field)} + title="๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋„ˆ๋น„ ์กฐ์ •" + />
- {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค */} -
handleMouseDown(e, col.field)} - title="๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋„ˆ๋น„ ์กฐ์ •" - /> -
-
์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค + + handleRowSelect(rowIndex, !!checked)} @@ -630,10 +673,13 @@ export function RepeaterTable({ {renderCell(row, col, rowIndex)}
+ ํ…Œ์ด๋ธ” ์„น์…˜ + + - ํผ์„ ์—ฌ๋Ÿฌ ์„น์…˜์œผ๋กœ ๋‚˜๋ˆ„์–ด ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + ํ•„๋“œ ์„น์…˜: ์ผ๋ฐ˜ ์ž…๋ ฅ ํ•„๋“œ๋“ค์„ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค.
- ์˜ˆ: ๊ธฐ๋ณธ ์ •๋ณด, ๋ฐฐ์†ก ์ •๋ณด, ๊ฒฐ์ œ ์ •๋ณด + ํ…Œ์ด๋ธ” ์„น์…˜: ํ’ˆ๋ชฉ ๋ชฉ๋ก ๋“ฑ ๋ฐ˜๋ณต ํ…Œ์ด๋ธ” ํ˜•์‹ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
{config.sections.length === 0 ? (

์„น์…˜์ด ์—†์Šต๋‹ˆ๋‹ค

-

"์„น์…˜ ์ถ”๊ฐ€" ๋ฒ„ํŠผ์œผ๋กœ ํผ ์„น์…˜์„ ๋งŒ๋“œ์„ธ์š”

+

์œ„ ๋ฒ„ํŠผ์œผ๋กœ ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”

) : (
{config.sections.map((section, index) => (
- {/* ํ—ค๋”: ์ œ๋ชฉ + ์‚ญ์ œ */} + {/* ํ—ค๋”: ์ œ๋ชฉ + ํƒ€์ž… ๋ฐฐ์ง€ + ์‚ญ์ œ */}
{section.title} - {section.repeatable && ( + {section.type === "table" ? ( + + ํ…Œ์ด๋ธ” + + ) : section.repeatable ? ( ๋ฐ˜๋ณต - )} + ) : null}
- - {section.fields.length}๊ฐœ ํ•„๋“œ - + {section.type === "table" ? ( + + {section.tableConfig?.source?.tableName || "(์†Œ์Šค ๋ฏธ์„ค์ •)"} + + ) : ( + + {(section.fields || []).length}๊ฐœ ํ•„๋“œ + + )}
- {/* ํ•„๋“œ ๋ชฉ๋ก */} - {section.fields.length > 0 && ( + {/* ํ•„๋“œ ๋ชฉ๋ก (ํ•„๋“œ ํƒ€์ž…๋งŒ) */} + {section.type !== "table" && (section.fields || []).length > 0 && (
- {section.fields.slice(0, 4).map((field) => ( + {(section.fields || []).slice(0, 4).map((field) => ( ))} - {section.fields.length > 4 && ( + {(section.fields || []).length > 4 && ( - +{section.fields.length - 4} + +{(section.fields || []).length - 4} + + )} +
+ )} + + {/* ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก (ํ…Œ์ด๋ธ” ํƒ€์ž…๋งŒ) */} + {section.type === "table" && section.tableConfig?.columns && section.tableConfig.columns.length > 0 && ( +
+ {section.tableConfig.columns.slice(0, 4).map((col) => ( + + {col.label} + + ))} + {section.tableConfig.columns.length > 4 && ( + + +{section.tableConfig.columns.length - 4} )}
)} - {/* ๋ ˆ์ด์•„์›ƒ ์„ค์ • ๋ฒ„ํŠผ */} - + {/* ์„ค์ • ๋ฒ„ํŠผ (ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฆ„) */} + {section.type === "table" ? ( +
+ ํ…Œ์ด๋ธ” ์„ค์ • + + ) : ( + + )} ))} @@ -530,7 +624,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor const updatedSection = { ...selectedSection, // ๊ธฐ๋ณธ ํ•„๋“œ ๋ชฉ๋ก์—์„œ ์—…๋ฐ์ดํŠธ - fields: selectedSection.fields.map((f) => (f.id === updatedField.id ? updatedField : f)), + fields: (selectedSection.fields || []).map((f) => (f.id === updatedField.id ? updatedField : f)), // ์˜ต์…”๋„ ํ•„๋“œ ๊ทธ๋ฃน ๋‚ด ํ•„๋“œ๋„ ์—…๋ฐ์ดํŠธ optionalFieldGroups: selectedSection.optionalFieldGroups?.map((group) => ({ ...group, @@ -558,6 +652,46 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor onLoadTableColumns={loadTableColumns} /> )} + + {/* ํ…Œ์ด๋ธ” ์„น์…˜ ์„ค์ • ๋ชจ๋‹ฌ */} + {selectedSection && selectedSection.type === "table" && ( + { + const updatedSection = { + ...selectedSection, + ...updates, + }; + + // config ์—…๋ฐ์ดํŠธ + onChange({ + ...config, + sections: config.sections.map((s) => + s.id === selectedSection.id ? updatedSection : s + ), + }); + + setSelectedSection(updatedSection); + setTableSectionSettingsModalOpen(false); + }} + tables={tables.map(t => ({ table_name: t.name, comment: t.label }))} + tableColumns={Object.fromEntries( + Object.entries(tableColumns).map(([tableName, cols]) => [ + tableName, + cols.map(c => ({ + column_name: c.name, + data_type: c.type, + is_nullable: "YES", + comment: c.label, + })), + ]) + )} + onLoadTableColumns={loadTableColumns} + allSections={config.sections as FormSectionConfig[]} + /> + )} ); } diff --git a/frontend/lib/registry/components/universal-form-modal/config.ts b/frontend/lib/registry/components/universal-form-modal/config.ts index 85a3e3d9..e8b239f6 100644 --- a/frontend/lib/registry/components/universal-form-modal/config.ts +++ b/frontend/lib/registry/components/universal-form-modal/config.ts @@ -2,7 +2,16 @@ * ๋ฒ”์šฉ ํผ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ ์„ค์ • */ -import { UniversalFormModalConfig } from "./types"; +import { + UniversalFormModalConfig, + TableSectionConfig, + TableColumnConfig, + ValueMappingConfig, + ColumnModeConfig, + TablePreFilter, + TableModalFilter, + TableCalculationRule, +} from "./types"; // ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ export const defaultConfig: UniversalFormModalConfig = { @@ -77,6 +86,7 @@ export const defaultSectionConfig = { id: "", title: "์ƒˆ ์„น์…˜", description: "", + type: "fields" as const, collapsible: false, defaultCollapsed: false, columns: 2, @@ -95,6 +105,97 @@ export const defaultSectionConfig = { linkedFieldGroups: [], }; +// ============================================ +// ํ…Œ์ด๋ธ” ์„น์…˜ ๊ด€๋ จ ๊ธฐ๋ณธ๊ฐ’ +// ============================================ + +// ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์„น์…˜ ์„ค์ • +export const defaultTableSectionConfig: TableSectionConfig = { + source: { + tableName: "", + displayColumns: [], + searchColumns: [], + columnLabels: {}, + }, + filters: { + preFilters: [], + modalFilters: [], + }, + columns: [], + calculations: [], + saveConfig: { + targetTable: undefined, + uniqueField: undefined, + }, + uiConfig: { + addButtonText: "ํ•ญ๋ชฉ ๊ฒ€์ƒ‰", + modalTitle: "ํ•ญ๋ชฉ ๊ฒ€์ƒ‰ ๋ฐ ์„ ํƒ", + multiSelect: true, + maxHeight: "400px", + }, +}; + +// ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์„ค์ • +export const defaultTableColumnConfig: TableColumnConfig = { + field: "", + label: "", + type: "text", + editable: true, + calculated: false, + required: false, + width: "150px", + minWidth: "60px", + maxWidth: "400px", + defaultValue: undefined, + selectOptions: [], + valueMapping: undefined, + columnModes: [], +}; + +// ๊ธฐ๋ณธ ๊ฐ’ ๋งคํ•‘ ์„ค์ • +export const defaultValueMappingConfig: ValueMappingConfig = { + type: "source", + sourceField: "", + externalRef: undefined, + internalField: undefined, +}; + +// ๊ธฐ๋ณธ ์ปฌ๋Ÿผ ๋ชจ๋“œ ์„ค์ • +export const defaultColumnModeConfig: ColumnModeConfig = { + id: "", + label: "", + isDefault: false, + valueMapping: { + type: "source", + sourceField: "", + }, +}; + +// ๊ธฐ๋ณธ ์‚ฌ์ „ ํ•„ํ„ฐ ์„ค์ • +export const defaultPreFilterConfig: TablePreFilter = { + column: "", + operator: "=", + value: "", +}; + +// ๊ธฐ๋ณธ ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์„ค์ • +export const defaultModalFilterConfig: TableModalFilter = { + column: "", + label: "", + type: "category", + categoryRef: undefined, + options: [], + optionsFromTable: undefined, + defaultValue: undefined, +}; + +// ๊ธฐ๋ณธ ๊ณ„์‚ฐ ๊ทœ์น™ ์„ค์ • +export const defaultCalculationRuleConfig: TableCalculationRule = { + resultField: "", + formula: "", + dependencies: [], +}; + // ๊ธฐ๋ณธ ์˜ต์…”๋„ ํ•„๋“œ ๊ทธ๋ฃน ์„ค์ • export const defaultOptionalFieldGroupConfig = { id: "", @@ -184,3 +285,18 @@ export const generateFieldId = (): string => { export const generateLinkedFieldGroupId = (): string => { return generateUniqueId("linked"); }; + +// ์œ ํ‹ธ๋ฆฌํ‹ฐ: ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ID ์ƒ์„ฑ +export const generateTableColumnId = (): string => { + return generateUniqueId("tcol"); +}; + +// ์œ ํ‹ธ๋ฆฌํ‹ฐ: ์ปฌ๋Ÿผ ๋ชจ๋“œ ID ์ƒ์„ฑ +export const generateColumnModeId = (): string => { + return generateUniqueId("mode"); +}; + +// ์œ ํ‹ธ๋ฆฌํ‹ฐ: ํ•„ํ„ฐ ID ์ƒ์„ฑ +export const generateFilterId = (): string => { + return generateUniqueId("filter"); +}; diff --git a/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx index 94bdf3af..2607cf83 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx @@ -11,9 +11,10 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; -import { Plus, Trash2, Database, Layers } from "lucide-react"; +import { Plus, Trash2, Database, Layers, Info } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { SaveConfig, SubTableSaveConfig, SubTableFieldMapping, FormSectionConfig, FormFieldConfig } from "../types"; +import { SaveConfig, SubTableSaveConfig, SubTableFieldMapping, FormSectionConfig, FormFieldConfig, SectionSaveMode } from "../types"; // ๋„์›€๋ง ํ…์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ const HelpText = ({ children }: { children: React.ReactNode }) => ( @@ -219,19 +220,112 @@ export function SaveSettingsModal({ const getAllFields = (): { columnName: string; label: string; sectionTitle: string }[] => { const fields: { columnName: string; label: string; sectionTitle: string }[] = []; sections.forEach((section) => { - section.fields.forEach((field) => { - fields.push({ - columnName: field.columnName, - label: field.label, - sectionTitle: section.title, + // ํ•„๋“œ ํƒ€์ž… ์„น์…˜๋งŒ ์ฒ˜๋ฆฌ (ํ…Œ์ด๋ธ” ํƒ€์ž…์€ fields๊ฐ€ undefined) + if (section.fields && Array.isArray(section.fields)) { + section.fields.forEach((field) => { + fields.push({ + columnName: field.columnName, + label: field.label, + sectionTitle: section.title, + }); }); - }); + } }); return fields; }; const allFields = getAllFields(); + // ์„น์…˜๋ณ„ ์ €์žฅ ๋ฐฉ์‹ ์กฐํšŒ (์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜) + const getSectionSaveMode = (sectionId: string, sectionType: "fields" | "table"): "common" | "individual" => { + const sectionMode = localSaveConfig.sectionSaveModes?.find((s) => s.sectionId === sectionId); + if (sectionMode) { + return sectionMode.saveMode; + } + // ๊ธฐ๋ณธ๊ฐ’: fields ํƒ€์ž…์€ ๊ณตํ†ต ์ €์žฅ, table ํƒ€์ž…์€ ๊ฐœ๋ณ„ ์ €์žฅ + return sectionType === "fields" ? "common" : "individual"; + }; + + // ํ•„๋“œ๋ณ„ ์ €์žฅ ๋ฐฉ์‹ ์กฐํšŒ (์˜ค๋ฒ„๋ผ์ด๋“œ ํ™•์ธ) + const getFieldSaveMode = (sectionId: string, fieldName: string, sectionType: "fields" | "table"): "common" | "individual" => { + const sectionMode = localSaveConfig.sectionSaveModes?.find((s) => s.sectionId === sectionId); + if (sectionMode) { + // ํ•„๋“œ๋ณ„ ์˜ค๋ฒ„๋ผ์ด๋“œ ํ™•์ธ + const fieldOverride = sectionMode.fieldOverrides?.find((f) => f.fieldName === fieldName); + if (fieldOverride) { + return fieldOverride.saveMode; + } + return sectionMode.saveMode; + } + // ๊ธฐ๋ณธ๊ฐ’ + return sectionType === "fields" ? "common" : "individual"; + }; + + // ์„น์…˜๋ณ„ ์ €์žฅ ๋ฐฉ์‹ ์—…๋ฐ์ดํŠธ + const updateSectionSaveMode = (sectionId: string, mode: "common" | "individual") => { + const currentModes = localSaveConfig.sectionSaveModes || []; + const existingIndex = currentModes.findIndex((s) => s.sectionId === sectionId); + + let newModes: SectionSaveMode[]; + if (existingIndex >= 0) { + newModes = [...currentModes]; + newModes[existingIndex] = { ...newModes[existingIndex], saveMode: mode }; + } else { + newModes = [...currentModes, { sectionId, saveMode: mode }]; + } + + updateSaveConfig({ sectionSaveModes: newModes }); + }; + + // ํ•„๋“œ๋ณ„ ์˜ค๋ฒ„๋ผ์ด๋“œ ํ† ๊ธ€ + const toggleFieldOverride = (sectionId: string, fieldName: string, sectionType: "fields" | "table") => { + const currentModes = localSaveConfig.sectionSaveModes || []; + const sectionIndex = currentModes.findIndex((s) => s.sectionId === sectionId); + + // ์„น์…˜ ์„ค์ •์ด ์—†์œผ๋ฉด ๋จผ์ € ์ƒ์„ฑ + let newModes = [...currentModes]; + if (sectionIndex < 0) { + const defaultMode = sectionType === "fields" ? "common" : "individual"; + newModes.push({ sectionId, saveMode: defaultMode, fieldOverrides: [] }); + } + + const targetIndex = newModes.findIndex((s) => s.sectionId === sectionId); + const sectionMode = newModes[targetIndex]; + const currentFieldOverrides = sectionMode.fieldOverrides || []; + const fieldOverrideIndex = currentFieldOverrides.findIndex((f) => f.fieldName === fieldName); + + let newFieldOverrides; + if (fieldOverrideIndex >= 0) { + // ์ด๋ฏธ ์˜ค๋ฒ„๋ผ์ด๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ œ๊ฑฐ (์„น์…˜ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋Œ์•„๊ฐ) + newFieldOverrides = currentFieldOverrides.filter((f) => f.fieldName !== fieldName); + } else { + // ์˜ค๋ฒ„๋ผ์ด๋“œ ์ถ”๊ฐ€ (์„น์…˜ ๊ธฐ๋ณธ๊ฐ’์˜ ๋ฐ˜๋Œ€) + const oppositeMode = sectionMode.saveMode === "common" ? "individual" : "common"; + newFieldOverrides = [...currentFieldOverrides, { fieldName, saveMode: oppositeMode }]; + } + + newModes[targetIndex] = { ...sectionMode, fieldOverrides: newFieldOverrides }; + updateSaveConfig({ sectionSaveModes: newModes }); + }; + + // ์„น์…˜์˜ ํ•„๋“œ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ + const getSectionFields = (section: FormSectionConfig): { fieldName: string; label: string }[] => { + if (section.type === "table" && section.tableConfig) { + // ํ…Œ์ด๋ธ” ํƒ€์ž…: tableConfig.columns์—์„œ ํ•„๋“œ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ + return (section.tableConfig.columns || []).map((col) => ({ + fieldName: col.field, + label: col.label, + })); + } else if (section.fields) { + // ํ•„๋“œ ํƒ€์ž…: fields์—์„œ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ + return section.fields.map((field) => ({ + fieldName: field.columnName, + label: field.label, + })); + } + return []; + }; + return ( @@ -721,6 +815,150 @@ export function SaveSettingsModal({ )} + {/* ์„น์…˜๋ณ„ ์ €์žฅ ๋ฐฉ์‹ */} +
+
+ +

์„น์…˜๋ณ„ ์ €์žฅ ๋ฐฉ์‹

+
+ + {/* ์„ค๋ช… */} +
+
+ +
+

+ ๊ณตํ†ต ์ €์žฅ: ์ด ์„น์…˜์˜ ํ•„๋“œ ๊ฐ’์ด ๋ชจ๋“  ํ’ˆ๋ชฉ ํ–‰์— ๋™์ผํ•˜๊ฒŒ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค +
+ ์˜ˆ: ์ˆ˜์ฃผ๋ฒˆํ˜ธ, ๊ฑฐ๋ž˜์ฒ˜, ์ˆ˜์ฃผ์ผ - ํ’ˆ๋ชฉ์ด 3๊ฐœ๋ฉด 3๊ฐœ ํ–‰ ๋ชจ๋‘ ๊ฐ™์€ ๊ฐ’ +

+

+ ๊ฐœ๋ณ„ ์ €์žฅ: ์ด ์„น์…˜์˜ ํ•„๋“œ ๊ฐ’์ด ๊ฐ ํ’ˆ๋ชฉ๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค +
+ ์˜ˆ: ํ’ˆ๋ชฉ์ฝ”๋“œ, ์ˆ˜๋Ÿ‰, ๋‹จ๊ฐ€ - ํ’ˆ๋ชฉ๋งˆ๋‹ค ๋‹ค๋ฅธ ๊ฐ’ +

+
+
+
+ + {/* ์„น์…˜ ๋ชฉ๋ก */} + {sections.length === 0 ? ( +
+

์„น์…˜์ด ์—†์Šต๋‹ˆ๋‹ค

+
+ ) : ( + + {sections.map((section) => { + const sectionType = section.type || "fields"; + const currentMode = getSectionSaveMode(section.id, sectionType); + const sectionFields = getSectionFields(section); + + return ( + + +
+
+ {section.title} + + {sectionType === "table" ? "ํ…Œ์ด๋ธ”" : "ํ•„๋“œ"} + +
+ + {currentMode === "common" ? "๊ณตํ†ต ์ €์žฅ" : "๊ฐœ๋ณ„ ์ €์žฅ"} + +
+
+ + {/* ์ €์žฅ ๋ฐฉ์‹ ์„ ํƒ */} +
+ + updateSectionSaveMode(section.id, value as "common" | "individual")} + className="flex gap-4" + > +
+ + +
+
+ + +
+
+
+ + {/* ํ•„๋“œ ๋ชฉ๋ก */} + {sectionFields.length > 0 && ( + <> + +
+ + ํ•„๋“œ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์„น์…˜ ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +
+ {sectionFields.map((field) => { + const fieldMode = getFieldSaveMode(section.id, field.fieldName, sectionType); + const isOverridden = fieldMode !== currentMode; + + return ( + + ); + })} +
+
+ + )} +
+
+ ); + })} +
+ )} +
+ {/* ์ €์žฅ ํ›„ ๋™์ž‘ */}

์ €์žฅ ํ›„ ๋™์ž‘

diff --git a/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx index 057502c9..4a90a777 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx @@ -37,13 +37,19 @@ export function SectionLayoutModal({ onOpenFieldDetail, }: SectionLayoutModalProps) { - // ๋กœ์ปฌ ์ƒํƒœ๋กœ ์„น์…˜ ๊ด€๋ฆฌ - const [localSection, setLocalSection] = useState(section); + // ๋กœ์ปฌ ์ƒํƒœ๋กœ ์„น์…˜ ๊ด€๋ฆฌ (fields๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด๋กœ ์ดˆ๊ธฐํ™”) + const [localSection, setLocalSection] = useState(() => ({ + ...section, + fields: section.fields || [], + })); // open์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” useEffect(() => { if (open) { - setLocalSection(section); + setLocalSection({ + ...section, + fields: section.fields || [], + }); } }, [open, section]); @@ -59,42 +65,45 @@ export function SectionLayoutModal({ onOpenChange(false); }; + // fields ๋ฐฐ์—ด (์•ˆ์ „ํ•œ ์ ‘๊ทผ) + const fields = localSection.fields || []; + // ํ•„๋“œ ์ถ”๊ฐ€ const addField = () => { const newField: FormFieldConfig = { ...defaultFieldConfig, id: generateFieldId(), - label: `์ƒˆ ํ•„๋“œ ${localSection.fields.length + 1}`, - columnName: `field_${localSection.fields.length + 1}`, + label: `์ƒˆ ํ•„๋“œ ${fields.length + 1}`, + columnName: `field_${fields.length + 1}`, }; updateSection({ - fields: [...localSection.fields, newField], + fields: [...fields, newField], }); }; // ํ•„๋“œ ์‚ญ์ œ const removeField = (fieldId: string) => { updateSection({ - fields: localSection.fields.filter((f) => f.id !== fieldId), + fields: fields.filter((f) => f.id !== fieldId), }); }; // ํ•„๋“œ ์—…๋ฐ์ดํŠธ const updateField = (fieldId: string, updates: Partial) => { updateSection({ - fields: localSection.fields.map((f) => (f.id === fieldId ? { ...f, ...updates } : f)), + fields: fields.map((f) => (f.id === fieldId ? { ...f, ...updates } : f)), }); }; // ํ•„๋“œ ์ด๋™ const moveField = (fieldId: string, direction: "up" | "down") => { - const index = localSection.fields.findIndex((f) => f.id === fieldId); + const index = fields.findIndex((f) => f.id === fieldId); if (index === -1) return; if (direction === "up" && index === 0) return; - if (direction === "down" && index === localSection.fields.length - 1) return; + if (direction === "down" && index === fields.length - 1) return; - const newFields = [...localSection.fields]; + const newFields = [...fields]; const targetIndex = direction === "up" ? index - 1 : index + 1; [newFields[index], newFields[targetIndex]] = [newFields[targetIndex], newFields[index]]; @@ -317,7 +326,7 @@ export function SectionLayoutModal({

ํ•„๋“œ ๋ชฉ๋ก

- {localSection.fields.length}๊ฐœ + {fields.length}๊ฐœ
diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx new file mode 100644 index 00000000..797bce55 --- /dev/null +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx @@ -0,0 +1,1247 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, Trash2, Check, ChevronsUpDown, ArrowRight } from "lucide-react"; +import { cn } from "@/lib/utils"; + +// ํƒ€์ž… import +import { + TableColumnConfig, + ValueMappingConfig, + ColumnModeConfig, + TableJoinCondition, + LookupConfig, + LookupOption, + LookupCondition, + VALUE_MAPPING_TYPE_OPTIONS, + JOIN_SOURCE_TYPE_OPTIONS, + TABLE_COLUMN_TYPE_OPTIONS, + LOOKUP_TYPE_OPTIONS, + LOOKUP_CONDITION_SOURCE_OPTIONS, +} from "../types"; + +import { + defaultValueMappingConfig, + defaultColumnModeConfig, + generateColumnModeId, +} from "../config"; + +// ๋„์›€๋ง ํ…์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ +const HelpText = ({ children }: { children: React.ReactNode }) => ( +

{children}

+); + +interface TableColumnSettingsModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + column: TableColumnConfig; + sourceTableName: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… + sourceTableColumns: { column_name: string; data_type: string; comment?: string }[]; + formFields: { columnName: string; label: string; sectionId?: string; sectionTitle?: string }[]; // formData ํ•„๋“œ ๋ชฉ๋ก (์„น์…˜ ์ •๋ณด ํฌํ•จ) + sections: { id: string; title: string }[]; // ์„น์…˜ ๋ชฉ๋ก + onSave: (updatedColumn: TableColumnConfig) => void; + tables: { table_name: string; comment?: string }[]; + tableColumns: Record; + onLoadTableColumns: (tableName: string) => void; +} + +export function TableColumnSettingsModal({ + open, + onOpenChange, + column, + sourceTableName, + sourceTableColumns, + formFields, + sections, + onSave, + tables, + tableColumns, + onLoadTableColumns, +}: TableColumnSettingsModalProps) { + // ๋กœ์ปฌ ์ƒํƒœ + const [localColumn, setLocalColumn] = useState({ ...column }); + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ ์ƒํƒœ + const [externalTableOpen, setExternalTableOpen] = useState(false); + + // ์กฐํšŒ ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ ์ƒํƒœ (์˜ต์…˜๋ณ„) + const [lookupTableOpenMap, setLookupTableOpenMap] = useState>({}); + + // ํ™œ์„ฑ ํƒญ + const [activeTab, setActiveTab] = useState("basic"); + + // open์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” + useEffect(() => { + if (open) { + setLocalColumn({ ...column }); + } + }, [open, column]); + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + const externalTableName = localColumn.valueMapping?.externalRef?.tableName; + useEffect(() => { + if (externalTableName) { + onLoadTableColumns(externalTableName); + } + }, [externalTableName, onLoadTableColumns]); + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋ชฉ๋ก + const externalTableColumns = useMemo(() => { + if (!externalTableName) return []; + return tableColumns[externalTableName] || []; + }, [tableColumns, externalTableName]); + + // ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ + const updateColumn = (updates: Partial) => { + setLocalColumn((prev) => ({ ...prev, ...updates })); + }; + + // ๊ฐ’ ๋งคํ•‘ ์—…๋ฐ์ดํŠธ + const updateValueMapping = (updates: Partial) => { + const current = localColumn.valueMapping || { ...defaultValueMappingConfig }; + updateColumn({ + valueMapping: { ...current, ...updates }, + }); + }; + + // ์™ธ๋ถ€ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + const updateExternalRef = (updates: Partial>) => { + const current = localColumn.valueMapping?.externalRef || { + tableName: "", + valueColumn: "", + joinConditions: [], + }; + updateValueMapping({ + externalRef: { ...current, ...updates }, + }); + }; + + // ์กฐ์ธ ์กฐ๊ฑด ์ถ”๊ฐ€ + const addJoinCondition = () => { + const current = localColumn.valueMapping?.externalRef?.joinConditions || []; + const newCondition: TableJoinCondition = { + sourceType: "row", + sourceField: "", + targetColumn: "", + operator: "=", + }; + updateExternalRef({ + joinConditions: [...current, newCondition], + }); + }; + + // ์กฐ์ธ ์กฐ๊ฑด ์‚ญ์ œ + const removeJoinCondition = (index: number) => { + const current = localColumn.valueMapping?.externalRef?.joinConditions || []; + updateExternalRef({ + joinConditions: current.filter((_, i) => i !== index), + }); + }; + + // ์กฐ์ธ ์กฐ๊ฑด ์—…๋ฐ์ดํŠธ + const updateJoinCondition = (index: number, updates: Partial) => { + const current = localColumn.valueMapping?.externalRef?.joinConditions || []; + updateExternalRef({ + joinConditions: current.map((c, i) => (i === index ? { ...c, ...updates } : c)), + }); + }; + + // ์ปฌ๋Ÿผ ๋ชจ๋“œ ์ถ”๊ฐ€ + const addColumnMode = () => { + const newMode: ColumnModeConfig = { + ...defaultColumnModeConfig, + id: generateColumnModeId(), + label: `๋ชจ๋“œ ${(localColumn.columnModes || []).length + 1}`, + }; + updateColumn({ + columnModes: [...(localColumn.columnModes || []), newMode], + }); + }; + + // ์ปฌ๋Ÿผ ๋ชจ๋“œ ์‚ญ์ œ + const removeColumnMode = (index: number) => { + updateColumn({ + columnModes: (localColumn.columnModes || []).filter((_, i) => i !== index), + }); + }; + + // ์ปฌ๋Ÿผ ๋ชจ๋“œ ์—…๋ฐ์ดํŠธ + const updateColumnMode = (index: number, updates: Partial) => { + updateColumn({ + columnModes: (localColumn.columnModes || []).map((m, i) => + i === index ? { ...m, ...updates } : m + ), + }); + }; + + // ============================================ + // ์กฐํšŒ(Lookup) ๊ด€๋ จ ํ•จ์ˆ˜๋“ค + // ============================================ + + // ์กฐํšŒ ์„ค์ • ์—…๋ฐ์ดํŠธ + const updateLookup = (updates: Partial) => { + const current = localColumn.lookup || { enabled: false, options: [] }; + updateColumn({ + lookup: { ...current, ...updates }, + }); + }; + + // ์กฐํšŒ ์˜ต์…˜ ์ถ”๊ฐ€ + const addLookupOption = () => { + const newOption: LookupOption = { + id: `lookup_${Date.now()}`, + label: `์กฐํšŒ ์˜ต์…˜ ${(localColumn.lookup?.options || []).length + 1}`, + type: "sameTable", + tableName: sourceTableName, // ๊ธฐ๋ณธ๊ฐ’: ์†Œ์Šค ํ…Œ์ด๋ธ” + valueColumn: "", + conditions: [], + isDefault: (localColumn.lookup?.options || []).length === 0, // ์ฒซ ๋ฒˆ์งธ ์˜ต์…˜์€ ๊ธฐ๋ณธ๊ฐ’ + }; + updateLookup({ + options: [...(localColumn.lookup?.options || []), newOption], + }); + }; + + // ์กฐํšŒ ์˜ต์…˜ ์‚ญ์ œ + const removeLookupOption = (index: number) => { + const newOptions = (localColumn.lookup?.options || []).filter((_, i) => i !== index); + // ์‚ญ์ œ ํ›„ ๊ธฐ๋ณธ ์˜ต์…˜์ด ์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ + if (newOptions.length > 0 && !newOptions.some(opt => opt.isDefault)) { + newOptions[0].isDefault = true; + } + updateLookup({ options: newOptions }); + }; + + // ์กฐํšŒ ์˜ต์…˜ ์—…๋ฐ์ดํŠธ + const updateLookupOption = (index: number, updates: Partial) => { + updateLookup({ + options: (localColumn.lookup?.options || []).map((opt, i) => + i === index ? { ...opt, ...updates } : opt + ), + }); + }; + + // ์กฐํšŒ ์กฐ๊ฑด ์ถ”๊ฐ€ + const addLookupCondition = (optionIndex: number) => { + const option = localColumn.lookup?.options?.[optionIndex]; + if (!option) return; + + const newCondition: LookupCondition = { + sourceType: "currentRow", + sourceField: "", + targetColumn: "", + }; + updateLookupOption(optionIndex, { + conditions: [...(option.conditions || []), newCondition], + }); + }; + + // ์กฐํšŒ ์กฐ๊ฑด ์‚ญ์ œ + const removeLookupCondition = (optionIndex: number, conditionIndex: number) => { + const option = localColumn.lookup?.options?.[optionIndex]; + if (!option) return; + + updateLookupOption(optionIndex, { + conditions: option.conditions.filter((_, i) => i !== conditionIndex), + }); + }; + + // ์กฐํšŒ ์กฐ๊ฑด ์—…๋ฐ์ดํŠธ + const updateLookupCondition = (optionIndex: number, conditionIndex: number, updates: Partial) => { + const option = localColumn.lookup?.options?.[optionIndex]; + if (!option) return; + + updateLookupOption(optionIndex, { + conditions: option.conditions.map((c, i) => + i === conditionIndex ? { ...c, ...updates } : c + ), + }); + }; + + // ์กฐํšŒ ์˜ต์…˜์˜ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + if (localColumn.lookup?.enabled) { + localColumn.lookup.options?.forEach(option => { + if (option.tableName) { + onLoadTableColumns(option.tableName); + } + }); + } + }, [localColumn.lookup?.enabled, localColumn.lookup?.options, onLoadTableColumns]); + + // ์ €์žฅ ํ•จ์ˆ˜ + const handleSave = () => { + onSave(localColumn); + onOpenChange(false); + }; + + // ๊ฐ’ ๋งคํ•‘ ํƒ€์ž…์— ๋”ฐ๋ฅธ ์„ค์ • UI ๋ Œ๋”๋ง + const renderValueMappingConfig = () => { + const mappingType = localColumn.valueMapping?.type || "source"; + + switch (mappingType) { + case "source": + return ( +
+
+ + + ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๋ณต์‚ฌํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”. +
+
+ ); + + case "manual": + return ( +
+ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ž…๋ ฅํ•˜๋Š” ํ•„๋“œ์ž…๋‹ˆ๋‹ค. +
+ ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•˜๋ ค๋ฉด "๊ธฐ๋ณธ ์„ค์ •" ํƒญ์—์„œ ์„ค์ •ํ•˜์„ธ์š”. +
+ ); + + case "internal": + return ( +
+
+ + + ๊ฐ™์€ ๋ชจ๋‹ฌ์˜ ๋‹ค๋ฅธ ํ•„๋“œ ๊ฐ’์„ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค. +
+
+ ); + + case "external": + return ( +
+ {/* ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+ + + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + {tables.map((table) => ( + { + updateExternalRef({ tableName: table.table_name }); + setExternalTableOpen(false); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + +
+ + {/* ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ ์„ ํƒ */} + {externalTableName && ( +
+ + +
+ )} + + {/* ์กฐ์ธ ์กฐ๊ฑด */} + {externalTableName && ( +
+
+ + +
+ + {(localColumn.valueMapping?.externalRef?.joinConditions || []).map((condition, index) => ( +
+ {/* ์†Œ์Šค ํƒ€์ž… */} + + + {/* ์†Œ์Šค ํ•„๋“œ */} + + + + + {/* ํƒ€๊ฒŸ ์ปฌ๋Ÿผ */} + + + +
+ ))} + + {(localColumn.valueMapping?.externalRef?.joinConditions || []).length === 0 && ( +

+ ์กฐ์ธ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +

+ )} +
+ )} +
+ ); + + default: + return null; + } + }; + + return ( + + + + ์ปฌ๋Ÿผ ์ƒ์„ธ ์„ค์ • + + "{localColumn.label}" ์ปฌ๋Ÿผ์˜ ์ƒ์„ธ ์„ค์ •์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + + + +
+ +
+ + + ๊ธฐ๋ณธ ์„ค์ • + ์กฐํšŒ ์„ค์ • + ๊ฐ’ ๋งคํ•‘ + ์ปฌ๋Ÿผ ๋ชจ๋“œ + + + {/* ๊ธฐ๋ณธ ์„ค์ • ํƒญ */} + +
+
+ + updateColumn({ field: e.target.value })} + placeholder="field_name" + className="h-8 text-xs mt-1" + /> + ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋  ์ปฌ๋Ÿผ๋ช…์ž…๋‹ˆ๋‹ค. +
+
+ + updateColumn({ label: e.target.value })} + placeholder="ํ‘œ์‹œ ๋ผ๋ฒจ" + className="h-8 text-xs mt-1" + /> +
+
+ +
+
+ + +
+
+ + updateColumn({ width: e.target.value })} + placeholder="150px" + className="h-8 text-xs mt-1" + /> +
+
+ + updateColumn({ defaultValue: e.target.value })} + placeholder="๊ธฐ๋ณธ๊ฐ’" + className="h-8 text-xs mt-1" + /> +
+
+ + + +
+

์˜ต์…˜

+
+ + + +
+
+ + {/* Select ์˜ต์…˜ (ํƒ€์ž…์ด select์ผ ๋•Œ) */} + {localColumn.type === "select" && ( + <> + +
+

Select ์˜ต์…˜

+
+ {(localColumn.selectOptions || []).map((opt, index) => ( +
+ { + const newOptions = [...(localColumn.selectOptions || [])]; + newOptions[index] = { ...newOptions[index], value: e.target.value }; + updateColumn({ selectOptions: newOptions }); + }} + placeholder="๊ฐ’" + className="h-8 text-xs flex-1" + /> + { + const newOptions = [...(localColumn.selectOptions || [])]; + newOptions[index] = { ...newOptions[index], label: e.target.value }; + updateColumn({ selectOptions: newOptions }); + }} + placeholder="๋ผ๋ฒจ" + className="h-8 text-xs flex-1" + /> + +
+ ))} + +
+
+ + )} +
+ + {/* ์กฐํšŒ ์„ค์ • ํƒญ */} + + {/* ์กฐํšŒ ์—ฌ๋ถ€ ํ† ๊ธ€ */} +
+
+ +

+ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’์„ ์กฐํšŒํ•˜์—ฌ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. +

+
+ { + if (checked) { + updateLookup({ enabled: true, options: [] }); + } else { + updateColumn({ lookup: undefined }); + } + }} + /> +
+ + {/* ์กฐํšŒ ์„ค์ • (ํ™œ์„ฑํ™” ์‹œ) */} + {localColumn.lookup?.enabled && ( +
+ + +
+
+ +

+ ํ—ค๋”์—์„œ ์„ ํƒ ๊ฐ€๋Šฅํ•œ ์กฐํšŒ ๋ฐฉ์‹์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. +

+
+ +
+ + {(localColumn.lookup?.options || []).length === 0 ? ( +
+

์กฐํšŒ ์˜ต์…˜์ด ์—†์Šต๋‹ˆ๋‹ค

+

+ "์˜ต์…˜ ์ถ”๊ฐ€" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์กฐํšŒ ๋ฐฉ์‹์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +

+
+ ) : ( +
+ {(localColumn.lookup?.options || []).map((option, optIndex) => ( +
+ {/* ์˜ต์…˜ ํ—ค๋” */} +
+
+ {option.label || `์˜ต์…˜ ${optIndex + 1}`} + {option.isDefault && ( + ๊ธฐ๋ณธ + )} +
+ +
+ + {/* ๊ธฐ๋ณธ ์„ค์ • */} +
+
+ + updateLookupOption(optIndex, { label: e.target.value })} + placeholder="์˜ˆ: ๊ธฐ์ค€๋‹จ๊ฐ€" + className="h-8 text-xs mt-1" + /> +
+
+ + +
+
+ + {/* ์กฐํšŒ ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+
+ + {option.type === "sameTable" ? ( + + ) : ( + setLookupTableOpenMap(prev => ({ ...prev, [option.id]: open }))} + > + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + {tables.map((table) => ( + { + updateLookupOption(optIndex, { tableName: table.table_name }); + onLoadTableColumns(table.table_name); + setLookupTableOpenMap(prev => ({ ...prev, [option.id]: false })); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + + )} +
+
+ + +
+
+ + {/* ๊ธฐ๋ณธ ์˜ต์…˜ ์ฒดํฌ๋ฐ•์Šค */} +
+ { + if (checked) { + // ๊ธฐ๋ณธ ์˜ต์…˜์€ ํ•˜๋‚˜๋งŒ + updateLookup({ + options: (localColumn.lookup?.options || []).map((opt, i) => ({ + ...opt, + isDefault: i === optIndex, + })), + }); + } else { + updateLookupOption(optIndex, { isDefault: false }); + } + }} + className="scale-75" + /> + ๊ธฐ๋ณธ ์˜ต์…˜์œผ๋กœ ์„ค์ • +
+ + + + {/* ์กฐํšŒ ์กฐ๊ฑด */} +
+
+ + +
+ + {(option.conditions || []).length === 0 ? ( +

+ ์กฐํšŒ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +

+ ) : ( +
+ {option.conditions.map((condition, condIndex) => ( +
+ {/* ์†Œ์Šค ํƒ€์ž… */} + + + {/* ์„น์…˜ ์„ ํƒ (sectionField์ผ ๋•Œ) */} + {condition.sourceType === "sectionField" && ( + + )} + + {/* ์†Œ์Šค ํ•„๋“œ */} + + + = + + {/* ํƒ€๊ฒŸ ์ปฌ๋Ÿผ */} + + + +
+ ))} +
+ )} + + {/* ์กฐํšŒ ์œ ํ˜•๋ณ„ ์„ค๋ช… */} +
+ {option.type === "sameTable" && ( + <> + ๋™์ผ ํ…Œ์ด๋ธ” ์กฐํšŒ: ๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ์„ ํƒํ•œ ํ–‰์˜ ๋‹ค๋ฅธ ์ปฌ๋Ÿผ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. +
์˜ˆ: ํ’ˆ๋ชฉ ์„ ํƒ ์‹œ โ†’ ํ’ˆ๋ชฉ ํ…Œ์ด๋ธ”์˜ ๊ธฐ์ค€๋‹จ๊ฐ€ + + )} + {option.type === "relatedTable" && ( + <> + ์—ฐ๊ด€ ํ…Œ์ด๋ธ” ์กฐํšŒ: ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. +
์˜ˆ: ํ’ˆ๋ชฉ์ฝ”๋“œ๋กœ โ†’ ํ’ˆ๋ชฉ๋ณ„๋‹จ๊ฐ€ ํ…Œ์ด๋ธ”์—์„œ ๋‹จ๊ฐ€ ์กฐํšŒ + + )} + {option.type === "combinedLookup" && ( + <> + ๋ณตํ•ฉ ์กฐ๊ฑด ์กฐํšŒ: ๋‹ค๋ฅธ ์„น์…˜ ํ•„๋“œ์™€ ํ˜„์žฌ ํ–‰์„ ์กฐํ•ฉํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. +
์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜(์„น์…˜1) + ํ’ˆ๋ชฉ(ํ˜„์žฌํ–‰) โ†’ ๊ฑฐ๋ž˜์ฒ˜๋ณ„๋‹จ๊ฐ€ ํ…Œ์ด๋ธ” + + )} +
+
+
+ ))} +
+ )} +
+ )} +
+ + {/* ๊ฐ’ ๋งคํ•‘ ํƒญ */} + +
+ + + ์ด ์ปฌ๋Ÿผ์˜ ๊ฐ’์„ ์–ด๋””์„œ ๊ฐ€์ ธ์˜ฌ์ง€ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. +
+ + + + {renderValueMappingConfig()} +
+ + {/* ์ปฌ๋Ÿผ ๋ชจ๋“œ ํƒญ */} + +
+
+ +

+ ํ•˜๋‚˜์˜ ์ปฌ๋Ÿผ์—์„œ ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์ „ํ™˜ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +

+
+ +
+ + {(localColumn.columnModes || []).length === 0 ? ( +
+

์ปฌ๋Ÿผ ๋ชจ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+

+ ์˜ˆ: ๊ธฐ์ค€ ๋‹จ๊ฐ€ / ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€๋ฅผ ์ „ํ™˜ํ•˜์—ฌ ํ‘œ์‹œ +

+
+ ) : ( +
+ {(localColumn.columnModes || []).map((mode, index) => ( +
+
+
+ {mode.label || `๋ชจ๋“œ ${index + 1}`} + {mode.isDefault && ( + ๊ธฐ๋ณธ + )} +
+ +
+ +
+
+ + updateColumnMode(index, { label: e.target.value })} + placeholder="์˜ˆ: ๊ธฐ์ค€ ๋‹จ๊ฐ€" + className="h-8 text-xs mt-1" + /> +
+
+ +
+
+ +
+ + +
+
+ ))} +
+ )} +
+
+
+
+
+ + + + + +
+
+ ); +} + diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx new file mode 100644 index 00000000..5a845db0 --- /dev/null +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx @@ -0,0 +1,2154 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Check, ChevronsUpDown, Filter, Table as TableIcon, Search } from "lucide-react"; +import { cn } from "@/lib/utils"; + +// ํƒ€์ž… import +import { + FormSectionConfig, + TableSectionConfig, + TableColumnConfig, + TablePreFilter, + TableModalFilter, + TableCalculationRule, + LookupOption, + ExternalTableLookup, + TABLE_COLUMN_TYPE_OPTIONS, + FILTER_OPERATOR_OPTIONS, + MODAL_FILTER_TYPE_OPTIONS, + LOOKUP_TYPE_OPTIONS, + LOOKUP_CONDITION_SOURCE_OPTIONS, +} from "../types"; + +import { + defaultTableSectionConfig, + defaultTableColumnConfig, + defaultPreFilterConfig, + defaultModalFilterConfig, + defaultCalculationRuleConfig, + generateTableColumnId, + generateFilterId, +} from "../config"; + +// ๋„์›€๋ง ํ…์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ +const HelpText = ({ children }: { children: React.ReactNode }) => ( +

{children}

+); + +// ์ปฌ๋Ÿผ ์„ค์ • ์•„์ดํ…œ ์ปดํฌ๋„ŒํŠธ +interface ColumnSettingItemProps { + col: TableColumnConfig; + index: number; + totalCount: number; + saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; + displayColumns: string[]; // ๊ฒ€์ƒ‰ ์„ค์ •์—์„œ ์„ ํƒํ•œ ํ‘œ์‹œ ์ปฌ๋Ÿผ ๋ชฉ๋ก + sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // ์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ + sourceTableName: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… + tables: { table_name: string; comment?: string }[]; // ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก + tableColumns: Record; // ํ…Œ์ด๋ธ”๋ณ„ ์ปฌ๋Ÿผ + sections: { id: string; title: string }[]; // ์„น์…˜ ๋ชฉ๋ก + formFields: { columnName: string; label: string; sectionId?: string }[]; // ํผ ํ•„๋“œ ๋ชฉ๋ก + tableConfig: TableSectionConfig; // ํ˜„์žฌ ํ–‰ ํ•„๋“œ ๋ชฉ๋ก ํ‘œ์‹œ์šฉ + onLoadTableColumns: (tableName: string) => void; + onUpdate: (updates: Partial) => void; + onMoveUp: () => void; + onMoveDown: () => void; + onRemove: () => void; +} + +function ColumnSettingItem({ + col, + index, + totalCount, + saveTableColumns, + displayColumns, + sourceTableColumns, + sourceTableName, + tables, + tableColumns, + sections, + formFields, + tableConfig, + onLoadTableColumns, + onUpdate, + onMoveUp, + onMoveDown, + onRemove, +}: ColumnSettingItemProps) { + const [fieldSearchOpen, setFieldSearchOpen] = useState(false); + const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false); + const [lookupTableOpenMap, setLookupTableOpenMap] = useState>({}); + + // ์กฐํšŒ ์˜ต์…˜ ์ถ”๊ฐ€ + const addLookupOption = () => { + const newOption: LookupOption = { + id: `lookup_${Date.now()}`, + label: `์กฐํšŒ ์˜ต์…˜ ${(col.lookup?.options || []).length + 1}`, + type: "sameTable", + tableName: sourceTableName, + valueColumn: "", + conditions: [], + isDefault: (col.lookup?.options || []).length === 0, + }; + onUpdate({ + lookup: { + enabled: true, + options: [...(col.lookup?.options || []), newOption], + }, + }); + }; + + // ์กฐํšŒ ์˜ต์…˜ ์‚ญ์ œ + const removeLookupOption = (optIndex: number) => { + const newOptions = (col.lookup?.options || []).filter((_, i) => i !== optIndex); + if (newOptions.length > 0 && !newOptions.some((opt) => opt.isDefault)) { + newOptions[0].isDefault = true; + } + onUpdate({ + lookup: { + enabled: col.lookup?.enabled ?? false, + options: newOptions, + }, + }); + }; + + // ์กฐํšŒ ์˜ต์…˜ ์—…๋ฐ์ดํŠธ + const updateLookupOption = (optIndex: number, updates: Partial) => { + onUpdate({ + lookup: { + enabled: col.lookup?.enabled ?? false, + options: (col.lookup?.options || []).map((opt, i) => + i === optIndex ? { ...opt, ...updates } : opt + ), + }, + }); + }; + + // ์กฐํšŒ ์กฐ๊ฑด ์ถ”๊ฐ€ + const addLookupCondition = (optIndex: number) => { + const option = col.lookup?.options?.[optIndex]; + if (!option) return; + const newCondition: LookupCondition = { + sourceType: "currentRow", + sourceField: "", + targetColumn: "", + }; + updateLookupOption(optIndex, { + conditions: [...(option.conditions || []), newCondition], + }); + }; + + // ์กฐํšŒ ์กฐ๊ฑด ์‚ญ์ œ + const removeLookupCondition = (optIndex: number, condIndex: number) => { + const option = col.lookup?.options?.[optIndex]; + if (!option) return; + updateLookupOption(optIndex, { + conditions: option.conditions.filter((_, i) => i !== condIndex), + }); + }; + + // ์กฐํšŒ ์กฐ๊ฑด ์—…๋ฐ์ดํŠธ + const updateLookupCondition = (optIndex: number, condIndex: number, updates: Partial) => { + const option = col.lookup?.options?.[optIndex]; + if (!option) return; + updateLookupOption(optIndex, { + conditions: option.conditions.map((c, i) => + i === condIndex ? { ...c, ...updates } : c + ), + }); + }; + + return ( +
+
+
+ + {col.label || col.field || `์ปฌ๋Ÿผ ${index + 1}`} + + {TABLE_COLUMN_TYPE_OPTIONS.find((t) => t.value === col.type)?.label || col.type} + + {col.calculated && ๊ณ„์‚ฐ} +
+
+ + + +
+
+ +
+ {/* ํ•„๋“œ๋ช… - Combobox (์ €์žฅํ•  ์ปฌ๋Ÿผ) */} +
+ + + + + + + + + + + ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + {saveTableColumns.map((column) => ( + { + onUpdate({ + field: column.column_name, + // ๋ผ๋ฒจ์ด ๋น„์–ด์žˆ์œผ๋ฉด comment๋กœ ์ž๋™ ์„ค์ • + ...((!col.label || col.label.startsWith("์ปฌ๋Ÿผ ")) && column.comment ? { label: column.comment } : {}) + }); + setFieldSearchOpen(false); + }} + className="text-xs" + > + +
+ {column.column_name} + + {column.comment || column.data_type} + +
+
+ ))} +
+
+
+
+
+
+ + {/* ์†Œ์Šค ํ•„๋“œ - Combobox (๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ) */} +
+ + + + + + + + + + + ์†Œ์Šค ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + {/* ํ•„๋“œ๋ช…๊ณผ ๋™์ผ ์˜ต์…˜ */} + { + onUpdate({ sourceField: undefined }); + setSourceFieldSearchOpen(false); + }} + className="text-xs" + > + + (ํ•„๋“œ๋ช…๊ณผ ๋™์ผ) + + {/* ํ‘œ์‹œ ์ปฌ๋Ÿผ ๋ชฉ๋ก */} + {displayColumns.map((colName) => { + const colInfo = sourceTableColumns.find((c) => c.column_name === colName); + return ( + { + onUpdate({ sourceField: colName }); + setSourceFieldSearchOpen(false); + }} + className="text-xs" + > + +
+ {colName} + {colInfo?.comment && ( + + {colInfo.comment} + + )} +
+
+ ); + })} +
+
+
+
+
+
+ + {/* ๋ผ๋ฒจ */} +
+ + onUpdate({ label: e.target.value })} + placeholder="ํ‘œ์‹œ ๋ผ๋ฒจ" + className="h-8 text-xs mt-1" + /> +
+ + {/* ํƒ€์ž… */} +
+ + +
+ + {/* ๋„ˆ๋น„ */} +
+ + onUpdate({ width: e.target.value })} + placeholder="150px" + className="h-8 text-xs mt-1" + /> +
+
+ + {/* ์˜ต์…˜ ์Šค์œ„์น˜ */} +
+ + + + + {/* ๋‚ ์งœ ํƒ€์ž…์ผ ๋•Œ๋งŒ ์ผ๊ด„ ์ ์šฉ ์˜ต์…˜ ํ‘œ์‹œ */} + {col.type === "date" && ( + + )} +
+ + {/* ์กฐํšŒ ์„ค์ • (์กฐํšŒ ON์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {col.lookup?.enabled && ( +
+
+ + +
+ + {(col.lookup?.options || []).length === 0 ? ( +

+ "์˜ต์…˜ ์ถ”๊ฐ€" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์กฐํšŒ ๋ฐฉ์‹์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +

+ ) : ( +
+ {(col.lookup?.options || []).map((option, optIndex) => ( +
+ {/* ์˜ต์…˜ ํ—ค๋” */} +
+
+ {option.label || `์˜ต์…˜ ${optIndex + 1}`} + {option.isDefault && ( + ๊ธฐ๋ณธ + )} +
+ +
+ + {/* ๊ธฐ๋ณธ ์„ค์ • - ์ฒซ ๋ฒˆ์งธ ์ค„: ์˜ต์…˜๋ช…, ํ‘œ์‹œ ๋ผ๋ฒจ */} +
+
+ + updateLookupOption(optIndex, { label: e.target.value })} + placeholder="์˜ˆ: ๊ธฐ์ค€๋‹จ๊ฐ€" + className="h-7 text-xs mt-0.5" + /> +
+
+ + updateLookupOption(optIndex, { displayLabel: e.target.value })} + placeholder={`์˜ˆ: ๋‹จ๊ฐ€ (${option.label || "์˜ต์…˜๋ช…"})`} + className="h-7 text-xs mt-0.5" + /> +

+ ๋น„์›Œ๋‘๋ฉด ์˜ต์…˜๋ช…๋งŒ ํ‘œ์‹œ +

+
+
+ + {/* ๊ธฐ๋ณธ ์„ค์ • - ๋‘ ๋ฒˆ์งธ ์ค„: ์กฐํšŒ ์œ ํ˜•, ํ…Œ์ด๋ธ”, ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ */} +
+
+ + +
+
+ + {option.type === "sameTable" ? ( + + ) : ( + setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: open }))} + > + + + + + + + + ์—†์Œ + + {tables.map((table) => ( + { + updateLookupOption(optIndex, { tableName: table.table_name }); + onLoadTableColumns(table.table_name); + setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: false })); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + + )} +
+
+ + +
+
+ + {/* ๊ธฐ๋ณธ ์˜ต์…˜ & ์กฐํšŒ ์กฐ๊ฑด */} +
+ + +
+ + {/* ์กฐํšŒ ์กฐ๊ฑด ๋ชฉ๋ก */} + {(option.conditions || []).length > 0 && ( +
+ {option.conditions.map((cond, condIndex) => ( +
+ {/* ๊ธฐ๋ณธ ์กฐ๊ฑด ํ–‰ */} +
+ + + {/* ๋‹ค๋ฅธ ์„น์…˜ ์„ ํƒ ์‹œ - ์„น์…˜ ๋“œ๋กญ๋‹ค์šด */} + {cond.sourceType === "sectionField" && ( + + )} + + {/* ํ˜„์žฌ ํ–‰ / ์†Œ์Šค ํ…Œ์ด๋ธ” / ๋‹ค๋ฅธ ์„น์…˜ - ํ•„๋“œ ์„ ํƒ */} + {cond.sourceType !== "externalTable" && ( +
+ + {cond.sourceField && ( +

+ {cond.sourceType === "currentRow" + ? `rowData.${cond.sourceField}` + : cond.sourceType === "sourceTable" + ? `${sourceTableName}.${cond.sourceField}` + : `formData.${cond.sourceField}` + } +

+ )} +
+ )} + + {/* ํ˜„์žฌ ํ–‰ / ์†Œ์Šค ํ…Œ์ด๋ธ” / ๋‹ค๋ฅธ ์„น์…˜์ผ ๋•Œ = ๊ธฐํ˜ธ์™€ ์กฐํšŒ ์ปฌ๋Ÿผ */} + {cond.sourceType !== "externalTable" && ( + <> + = + +
+ + {cond.targetColumn && option.tableName && ( +

+ {option.tableName}.{cond.targetColumn} +

+ )} +
+ + )} + + +
+ + {/* ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ • */} + {cond.sourceType === "externalTable" && cond.externalLookup && ( +
+

์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ์กฐ๊ฑด ๊ฐ’ ์กฐํšŒ

+ + {/* 1ํ–‰: ์กฐํšŒ ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+
+

์กฐํšŒ ํ…Œ์ด๋ธ”

+ setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: open }))} + > + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {tables.map((table) => ( + { + onLoadTableColumns(table.table_name); + updateLookupCondition(optIndex, condIndex, { + externalLookup: { ...cond.externalLookup!, tableName: table.table_name, matchColumn: "", resultColumn: "" } + }); + setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: false })); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + +
+ +
+

์ฐพ์„ ์ปฌ๋Ÿผ

+ +
+ +
+

๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ

+ +
+
+ + {/* 2ํ–‰: ๋น„๊ต ๊ฐ’ ์ถœ์ฒ˜ */} +
+

๋น„๊ต ๊ฐ’ ์ถœ์ฒ˜ (์ฐพ์„ ๋•Œ ์‚ฌ์šฉํ•  ๊ฐ’)

+
+ + + {cond.externalLookup.matchSourceType === "sectionField" && ( + + )} + + +
+
+ + {/* 3ํ–‰: ์ตœ์ข… ์กฐํšŒ ์ปฌ๋Ÿผ */} +
+ ์กฐํšŒ๋œ ๊ฐ’ (๋น„๊ตํ•  ์ปฌ๋Ÿผ) + = + +
+ + {/* ์„ค๋ช… ํ…์ŠคํŠธ */} + {cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && cond.targetColumn && ( +

+ {cond.externalLookup.tableName}์—์„œ {cond.externalLookup.matchColumn} = ์ž…๋ ฅ๊ฐ’(๋น„๊ต ๊ฐ’ ์ถœ์ฒ˜)์ธ ํ–‰์˜{" "} + {cond.externalLookup.resultColumn} ๊ฐ’์„ ๊ฐ€์ ธ์™€ {option.tableName}.{cond.targetColumn}์™€ ๋น„๊ต +

+ )} +
+ )} + + {/* ๊ฐ’ ๋ณ€ํ™˜ ์„ค์ • (๋‹ค๋ฅธ ์„น์…˜์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {cond.sourceType === "sectionField" && ( +
+ + + {cond.transform?.enabled && ( +
+
+
+ + + + + + + + + + ์—†์Œ + + {tables.map((table) => ( + { + updateLookupCondition(optIndex, condIndex, { + transform: { ...cond.transform!, tableName: table.table_name, matchColumn: "", resultColumn: "" } + }); + onLoadTableColumns(table.table_name); + }} + className="text-xs" + > + + {table.table_name} + + ))} + + + + + +
+
+ + +
+
+ + +
+
+ {cond.transform.tableName && cond.transform.matchColumn && cond.transform.resultColumn && ( +

+ {cond.transform.tableName}์—์„œ {cond.transform.matchColumn} = ์ž…๋ ฅ๊ฐ’ ์ธ ํ–‰์˜ {cond.transform.resultColumn} ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ +

+ )} +
+ )} +
+ )} +
+ ))} +
+ )} + + {/* ์กฐํšŒ ์œ ํ˜• ์„ค๋ช… */} +

+ {option.type === "sameTable" && "๋™์ผ ํ…Œ์ด๋ธ”: ๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ์„ ํƒํ•œ ํ–‰์˜ ๋‹ค๋ฅธ ์ปฌ๋Ÿผ ๊ฐ’"} + {option.type === "relatedTable" && "์—ฐ๊ด€ ํ…Œ์ด๋ธ”: ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์กฐํšŒ"} + {option.type === "combinedLookup" && "๋ณตํ•ฉ ์กฐ๊ฑด: ๋‹ค๋ฅธ ์„น์…˜ ํ•„๋“œ + ํ˜„์žฌ ํ–‰ ์กฐํ•ฉ ์กฐํšŒ"} +

+
+ ))} +
+ )} +
+ )} +
+ ); +} + +interface TableSectionSettingsModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + section: FormSectionConfig; + onSave: (updates: Partial) => void; + tables: { table_name: string; comment?: string }[]; + tableColumns: Record; + onLoadTableColumns: (tableName: string) => void; + // ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก (table_column_category_values์—์„œ ๊ฐ€์ ธ์˜ด) + categoryList?: { tableName: string; columnName: string; displayName?: string }[]; + onLoadCategoryList?: () => void; + // ์ „์ฒด ์„น์…˜ ๋ชฉ๋ก (๋‹ค๋ฅธ ์„น์…˜ ํ•„๋“œ ์ฐธ์กฐ์šฉ) + allSections?: FormSectionConfig[]; +} + +export function TableSectionSettingsModal({ + open, + onOpenChange, + section, + onSave, + tables, + tableColumns, + onLoadTableColumns, + categoryList = [], + onLoadCategoryList, + allSections = [], +}: TableSectionSettingsModalProps) { + // ๋กœ์ปฌ ์ƒํƒœ + const [title, setTitle] = useState(section.title); + const [description, setDescription] = useState(section.description || ""); + const [tableConfig, setTableConfig] = useState( + section.tableConfig || { ...defaultTableSectionConfig } + ); + + // ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ Combobox ์ƒํƒœ + const [tableSearchOpen, setTableSearchOpen] = useState(false); + const [saveTableSearchOpen, setSaveTableSearchOpen] = useState(false); + + // ํ™œ์„ฑ ํƒญ + const [activeTab, setActiveTab] = useState("source"); + + // open์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” + useEffect(() => { + if (open) { + setTitle(section.title); + setDescription(section.description || ""); + setTableConfig(section.tableConfig || { ...defaultTableSectionConfig }); + } + }, [open, section]); + + // ์†Œ์Šค ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์‹œ ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + if (tableConfig.source.tableName) { + onLoadTableColumns(tableConfig.source.tableName); + } + }, [tableConfig.source.tableName, onLoadTableColumns]); + + // ์ €์žฅ ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์‹œ ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + if (tableConfig.saveConfig?.targetTable) { + onLoadTableColumns(tableConfig.saveConfig.targetTable); + } + }, [tableConfig.saveConfig?.targetTable, onLoadTableColumns]); + + // ์กฐํšŒ ์„ค์ •์— ์žˆ๋Š” ํ…Œ์ด๋ธ”๋“ค์˜ ์ปฌ๋Ÿผ ๋กœ๋“œ (๋ชจ๋‹ฌ ์—ด๋ฆด ๋•Œ) + useEffect(() => { + if (open && tableConfig.columns) { + const tablesToLoad = new Set(); + + // ๊ฐ ์ปฌ๋Ÿผ์˜ lookup ์„ค์ •์—์„œ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ + tableConfig.columns.forEach((col) => { + if (col.lookup?.enabled && col.lookup.options) { + col.lookup.options.forEach((option) => { + // ์กฐํšŒ ํ…Œ์ด๋ธ” + if (option.tableName) { + tablesToLoad.add(option.tableName); + } + // ๋ณ€ํ™˜ ํ…Œ์ด๋ธ” + option.conditions?.forEach((cond) => { + if (cond.transform?.enabled && cond.transform.tableName) { + tablesToLoad.add(cond.transform.tableName); + } + }); + }); + } + }); + + // ์ˆ˜์ง‘๋œ ํ…Œ์ด๋ธ”๋“ค์˜ ์ปฌ๋Ÿผ ๋กœ๋“œ + tablesToLoad.forEach((tableName) => { + if (!tableColumns[tableName]) { + onLoadTableColumns(tableName); + } + }); + } + }, [open, tableConfig.columns, tableColumns, onLoadTableColumns]); + + // ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋ชฉ๋ก + const sourceTableColumns = useMemo(() => { + return tableColumns[tableConfig.source.tableName] || []; + }, [tableColumns, tableConfig.source.tableName]); + + // ์ €์žฅ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋ชฉ๋ก + const saveTableColumns = useMemo(() => { + // ์ €์žฅ ํ…Œ์ด๋ธ”์ด ์ง€์ •๋˜์–ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ, ์•„๋‹ˆ๋ฉด ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์‚ฌ์šฉ + const targetTable = tableConfig.saveConfig?.targetTable; + if (targetTable) { + return tableColumns[targetTable] || []; + } + return sourceTableColumns; + }, [tableColumns, tableConfig.saveConfig?.targetTable, sourceTableColumns]); + + // ๋‹ค๋ฅธ ์„น์…˜ ๋ชฉ๋ก (ํ˜„์žฌ ์„น์…˜ ์ œ์™ธ, ํ…Œ์ด๋ธ” ํƒ€์ž…์ด ์•„๋‹Œ ์„น์…˜๋งŒ) + const otherSections = useMemo(() => { + return allSections + .filter((s) => s.id !== section.id && s.type !== "table") + .map((s) => ({ id: s.id, title: s.title })); + }, [allSections, section.id]); + + // ๋‹ค๋ฅธ ์„น์…˜์˜ ํ•„๋“œ ๋ชฉ๋ก + const otherSectionFields = useMemo(() => { + const fields: { columnName: string; label: string; sectionId: string }[] = []; + allSections + .filter((s) => s.id !== section.id && s.type !== "table") + .forEach((s) => { + (s.fields || []).forEach((f) => { + fields.push({ + columnName: f.columnName, + label: f.label, + sectionId: s.id, + }); + }); + }); + return fields; + }, [allSections, section.id]); + + // ์„ค์ • ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ + const updateTableConfig = (updates: Partial) => { + setTableConfig((prev) => ({ ...prev, ...updates })); + }; + + const updateSource = (updates: Partial) => { + updateTableConfig({ + source: { ...tableConfig.source, ...updates }, + }); + }; + + const updateFilters = (updates: Partial) => { + updateTableConfig({ + filters: { ...tableConfig.filters, ...updates }, + }); + }; + + const updateUiConfig = (updates: Partial>) => { + updateTableConfig({ + uiConfig: { ...tableConfig.uiConfig, ...updates }, + }); + }; + + const updateSaveConfig = (updates: Partial>) => { + updateTableConfig({ + saveConfig: { ...tableConfig.saveConfig, ...updates }, + }); + }; + + // ์ €์žฅ ํ•จ์ˆ˜ + const handleSave = () => { + onSave({ + title, + description, + tableConfig, + }); + onOpenChange(false); + }; + + // ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + const addColumn = () => { + const newColumn: TableColumnConfig = { + ...defaultTableColumnConfig, + field: `column_${(tableConfig.columns || []).length + 1}`, + label: `์ปฌ๋Ÿผ ${(tableConfig.columns || []).length + 1}`, + }; + updateTableConfig({ + columns: [...(tableConfig.columns || []), newColumn], + }); + }; + + // ์ปฌ๋Ÿผ ์‚ญ์ œ + const removeColumn = (index: number) => { + updateTableConfig({ + columns: (tableConfig.columns || []).filter((_, i) => i !== index), + }); + }; + + // ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ + const updateColumn = (index: number, updates: Partial) => { + updateTableConfig({ + columns: (tableConfig.columns || []).map((col, i) => + i === index ? { ...col, ...updates } : col + ), + }); + }; + + // ์ปฌ๋Ÿผ ์ด๋™ + const moveColumn = (index: number, direction: "up" | "down") => { + const columns = [...(tableConfig.columns || [])]; + if (direction === "up" && index > 0) { + [columns[index - 1], columns[index]] = [columns[index], columns[index - 1]]; + } else if (direction === "down" && index < columns.length - 1) { + [columns[index], columns[index + 1]] = [columns[index + 1], columns[index]]; + } + updateTableConfig({ columns }); + }; + + // ์‚ฌ์ „ ํ•„ํ„ฐ ์ถ”๊ฐ€ + const addPreFilter = () => { + const newFilter: TablePreFilter = { ...defaultPreFilterConfig }; + updateFilters({ + preFilters: [...(tableConfig.filters?.preFilters || []), newFilter], + }); + }; + + // ์‚ฌ์ „ ํ•„ํ„ฐ ์‚ญ์ œ + const removePreFilter = (index: number) => { + updateFilters({ + preFilters: (tableConfig.filters?.preFilters || []).filter((_, i) => i !== index), + }); + }; + + // ์‚ฌ์ „ ํ•„ํ„ฐ ์—…๋ฐ์ดํŠธ + const updatePreFilter = (index: number, updates: Partial) => { + updateFilters({ + preFilters: (tableConfig.filters?.preFilters || []).map((f, i) => + i === index ? { ...f, ...updates } : f + ), + }); + }; + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์ถ”๊ฐ€ + const addModalFilter = () => { + const newFilter: TableModalFilter = { ...defaultModalFilterConfig }; + updateFilters({ + modalFilters: [...(tableConfig.filters?.modalFilters || []), newFilter], + }); + }; + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์‚ญ์ œ + const removeModalFilter = (index: number) => { + updateFilters({ + modalFilters: (tableConfig.filters?.modalFilters || []).filter((_, i) => i !== index), + }); + }; + + // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์—…๋ฐ์ดํŠธ + const updateModalFilter = (index: number, updates: Partial) => { + updateFilters({ + modalFilters: (tableConfig.filters?.modalFilters || []).map((f, i) => + i === index ? { ...f, ...updates } : f + ), + }); + }; + + // ๊ณ„์‚ฐ ๊ทœ์น™ ์ถ”๊ฐ€ + const addCalculation = () => { + const newCalc: TableCalculationRule = { ...defaultCalculationRuleConfig }; + updateTableConfig({ + calculations: [...(tableConfig.calculations || []), newCalc], + }); + }; + + // ๊ณ„์‚ฐ ๊ทœ์น™ ์‚ญ์ œ + const removeCalculation = (index: number) => { + updateTableConfig({ + calculations: (tableConfig.calculations || []).filter((_, i) => i !== index), + }); + }; + + // ๊ณ„์‚ฐ ๊ทœ์น™ ์—…๋ฐ์ดํŠธ + const updateCalculation = (index: number, updates: Partial) => { + updateTableConfig({ + calculations: (tableConfig.calculations || []).map((c, i) => + i === index ? { ...c, ...updates } : c + ), + }); + }; + + // ํ‘œ์‹œ ์ปฌ๋Ÿผ ํ† ๊ธ€ + const toggleDisplayColumn = (columnName: string) => { + const current = tableConfig.source.displayColumns || []; + if (current.includes(columnName)) { + updateSource({ + displayColumns: current.filter((c) => c !== columnName), + }); + } else { + updateSource({ + displayColumns: [...current, columnName], + }); + } + }; + + // ๊ฒ€์ƒ‰ ์ปฌ๋Ÿผ ํ† ๊ธ€ + const toggleSearchColumn = (columnName: string) => { + const current = tableConfig.source.searchColumns || []; + if (current.includes(columnName)) { + updateSource({ + searchColumns: current.filter((c) => c !== columnName), + }); + } else { + updateSource({ + searchColumns: [...current, columnName], + }); + } + }; + + // ํ‘œ์‹œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ + const moveDisplayColumn = (index: number, direction: "up" | "down") => { + const columns = [...(tableConfig.source.displayColumns || [])]; + if (direction === "up" && index > 0) { + [columns[index - 1], columns[index]] = [columns[index], columns[index - 1]]; + } else if (direction === "down" && index < columns.length - 1) { + [columns[index], columns[index + 1]] = [columns[index + 1], columns[index]]; + } + updateSource({ displayColumns: columns }); + }; + + // ํ‘œ์‹œ ์ปฌ๋Ÿผ ์‚ญ์ œ (์ˆœ์„œ ํŽธ์ง‘ ์˜์—ญ์—์„œ) + const removeDisplayColumn = (columnName: string) => { + updateSource({ + displayColumns: (tableConfig.source.displayColumns || []).filter((c) => c !== columnName), + }); + }; + + return ( + + + + ํ…Œ์ด๋ธ” ์„น์…˜ ์„ค์ • + + ํ…Œ์ด๋ธ” ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•˜๊ณ  ํŽธ์ง‘ํ•˜๋Š” ์„น์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + + + +
+ +
+ {/* ๊ธฐ๋ณธ ์ •๋ณด */} +
+

๊ธฐ๋ณธ ์ •๋ณด

+
+
+ + setTitle(e.target.value)} + placeholder="์˜ˆ: ํ’ˆ๋ชฉ ๋ชฉ๋ก" + className="h-9 text-sm" + /> +
+
+ + setDescription(e.target.value)} + placeholder="์„น์…˜์— ๋Œ€ํ•œ ์„ค๋ช…" + className="h-9 text-sm" + /> +
+
+
+ + {/* ํƒญ ๊ตฌ์„ฑ */} + + + ํ…Œ์ด๋ธ” ์„ค์ • + ์ปฌ๋Ÿผ ์„ค์ • + ๊ฒ€์ƒ‰ ์„ค์ • + ๊ณ ๊ธ‰ ์„ค์ • + + + {/* ํ…Œ์ด๋ธ” ์„ค์ • ํƒญ */} + + {/* ์†Œ์Šค ํ…Œ์ด๋ธ” ์„ค์ • */} +
+

๊ฒ€์ƒ‰์šฉ ์†Œ์Šค ํ…Œ์ด๋ธ”

+

๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค.

+ +
+ + + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + {tables.map((table) => ( + { + updateSource({ tableName: table.table_name }); + setTableSearchOpen(false); + }} + className="text-sm" + > + +
+ {table.table_name} + {table.comment && ( + {table.comment} + )} +
+
+ ))} +
+
+
+
+
+
+
+ + {/* ์ €์žฅ ํ…Œ์ด๋ธ” ์„ค์ • */} +
+

์ €์žฅ์šฉ ํ…Œ์ด๋ธ”

+

ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค. ๋ฏธ์„ค์ • ์‹œ ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

+ +
+
+ + + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + { + updateSaveConfig({ targetTable: undefined }); + setSaveTableSearchOpen(false); + }} + className="text-sm" + > + + (๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ๋™์ผ) + + {tables.map((table) => ( + { + updateSaveConfig({ targetTable: table.table_name }); + // ์„ ํƒ ์ฆ‰์‹œ ์ปฌ๋Ÿผ ๋กœ๋“œ ์š”์ฒญ + onLoadTableColumns(table.table_name); + setSaveTableSearchOpen(false); + }} + className="text-sm" + > + +
+ {table.table_name} + {table.comment && ( + {table.comment} + )} +
+
+ ))} +
+
+
+
+
+
+
+ + updateSaveConfig({ uniqueField: e.target.value || undefined })} + placeholder="์˜ˆ: item_id" + className="h-9 text-sm mt-1" + /> + ๋™์ผ ๊ฐ’์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +
+
+
+
+ + {/* ์ปฌ๋Ÿผ ์„ค์ • ํƒญ */} + + {/* ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ */} + {saveTableColumns.length === 0 && !tableConfig.saveConfig?.targetTable && !tableConfig.source.tableName && ( +
+

+ "ํ…Œ์ด๋ธ” ์„ค์ •" ํƒญ์—์„œ ์ €์žฅ ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”. ์„ ํƒํ•œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์—ฌ๊ธฐ์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +

+
+ )} + + {/* ํ…Œ์ด๋ธ”์€ ์„ ํƒํ–ˆ์ง€๋งŒ ์ปฌ๋Ÿผ์ด ์•„์ง ๋กœ๋“œ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ */} + {saveTableColumns.length === 0 && (tableConfig.saveConfig?.targetTable || tableConfig.source.tableName) && ( +
+

+ ํ…Œ์ด๋ธ” "{tableConfig.saveConfig?.targetTable || tableConfig.source.tableName}" ์˜ ์ปฌ๋Ÿผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค... +

+
+ )} + +
+
+ + {saveTableColumns.length > 0 && ( +

+ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ: {saveTableColumns.length}๊ฐœ ({tableConfig.saveConfig?.targetTable || tableConfig.source.tableName || "ํ…Œ์ด๋ธ” ๋ฏธ์„ ํƒ"}) +

+ )} +
+ +
+ + {(tableConfig.columns || []).length === 0 ? ( +
+ +

์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค

+

"์ปฌ๋Ÿผ ์ถ”๊ฐ€" ๋ฒ„ํŠผ์œผ๋กœ ์ถ”๊ฐ€ํ•˜์„ธ์š”

+
+ ) : ( +
+ {(tableConfig.columns || []).map((col, index) => ( + updateColumn(index, updates)} + onMoveUp={() => moveColumn(index, "up")} + onMoveDown={() => moveColumn(index, "down")} + onRemove={() => removeColumn(index)} + /> + ))} +
+ )} +
+ + {/* ๊ฒ€์ƒ‰ ์„ค์ • ํƒญ */} + + {/* ํ‘œ์‹œ ์ปฌ๋Ÿผ / ๊ฒ€์ƒ‰ ์ปฌ๋Ÿผ ์„ค์ • */} +
+

๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ์„ค์ •

+

๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ๋ณด์—ฌ์ค„ ์ปฌ๋Ÿผ๊ณผ ๊ฒ€์ƒ‰ ๋Œ€์ƒ ์ปฌ๋Ÿผ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

+ + {/* ์†Œ์Šค ํ…Œ์ด๋ธ” ๋ฏธ์„ ํƒ ์‹œ ์•ˆ๋‚ด */} + {!tableConfig.source.tableName && ( +
+

+ "ํ…Œ์ด๋ธ” ์„ค์ •" ํƒญ์—์„œ ๊ฒ€์ƒ‰์šฉ ์†Œ์Šค ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”. +

+
+ )} + + {/* ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ ํƒ */} + {sourceTableColumns.length > 0 && ( +
+ +
+
+ {sourceTableColumns.map((col) => ( + + ))} +
+
+ ์„ ํƒ๋œ ์ปฌ๋Ÿผ: {(tableConfig.source.displayColumns || []).length}๊ฐœ + + {/* ์„ ํƒ๋œ ์ปฌ๋Ÿผ ์ˆœ์„œ ํŽธ์ง‘ */} + {(tableConfig.source.displayColumns || []).length > 0 && ( +
+ +
+ {(tableConfig.source.displayColumns || []).map((colName, index) => { + const colInfo = sourceTableColumns.find((c) => c.column_name === colName); + return ( +
+ + {colName} + {colInfo?.comment && ( + + {colInfo.comment} + + )} +
+ + + +
+
+ ); + })} +
+
+ )} +
+ )} + + {/* ๊ฒ€์ƒ‰ ์ปฌ๋Ÿผ ์„ ํƒ */} + {sourceTableColumns.length > 0 && ( +
+ +
+
+ {sourceTableColumns.map((col) => ( + + ))} +
+
+ ๊ฒ€์ƒ‰ ์ปฌ๋Ÿผ: {(tableConfig.source.searchColumns || []).length}๊ฐœ +
+ )} +
+ + + + {/* ์‚ฌ์ „ ํ•„ํ„ฐ */} +
+
+
+ +

ํ•ญ์ƒ ์ ์šฉ๋˜๋Š” ํ•„ํ„ฐ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค.

+
+ +
+ + {(tableConfig.filters?.preFilters || []).map((filter, index) => ( +
+ + + + + updatePreFilter(index, { value: e.target.value })} + placeholder="๊ฐ’" + className="h-8 text-xs flex-1" + /> + + +
+ ))} +
+ + + + {/* ๋ชจ๋‹ฌ ํ•„ํ„ฐ */} +
+
+
+ +

์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ํ•„ํ„ฐ์ž…๋‹ˆ๋‹ค.

+
+ +
+ + {(tableConfig.filters?.modalFilters || []).map((filter, index) => ( +
+
+ {/* ์ปฌ๋Ÿผ ์„ ํƒ */} + + + {/* ๋ผ๋ฒจ */} + updateModalFilter(index, { label: e.target.value })} + placeholder="๋ผ๋ฒจ" + className="h-8 text-xs w-[100px]" + /> + + {/* ํƒ€์ž… */} + + + {/* ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ (ํƒ€์ž…์ด category์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {filter.type === "category" && ( + + )} + + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
+
+ ))} +
+
+ + {/* ๊ณ ๊ธ‰ ์„ค์ • ํƒญ */} + + {/* UI ์„ค์ • */} +
+

UI ์„ค์ •

+
+
+ + updateUiConfig({ addButtonText: e.target.value })} + placeholder="ํ•ญ๋ชฉ ๊ฒ€์ƒ‰" + className="h-8 text-xs mt-1" + /> +
+
+ + updateUiConfig({ modalTitle: e.target.value })} + placeholder="ํ•ญ๋ชฉ ๊ฒ€์ƒ‰ ๋ฐ ์„ ํƒ" + className="h-8 text-xs mt-1" + /> +
+
+ + updateUiConfig({ maxHeight: e.target.value })} + placeholder="400px" + className="h-8 text-xs mt-1" + /> +
+
+ +
+
+
+ + {/* ๊ณ„์‚ฐ ๊ทœ์น™ */} +
+
+
+

๊ณ„์‚ฐ ๊ทœ์น™

+

๋‹ค๋ฅธ ์ปฌ๋Ÿผ ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๋™ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

+
+ +
+ + {(tableConfig.calculations || []).map((calc, index) => ( +
+ + = + updateCalculation(index, { formula: e.target.value })} + placeholder="์ˆ˜์‹ (์˜ˆ: quantity * unit_price)" + className="h-8 text-xs flex-1" + /> + +
+ ))} +
+
+
+
+
+
+ + + + + +
+
+ ); +} diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index 3b5801c2..4e25f7d7 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -184,7 +184,12 @@ export interface FormSectionConfig { description?: string; collapsible?: boolean; // ์ ‘์„ ์ˆ˜ ์žˆ๋Š”์ง€ (๊ธฐ๋ณธ: false) defaultCollapsed?: boolean; // ๊ธฐ๋ณธ ์ ‘ํž˜ ์ƒํƒœ (๊ธฐ๋ณธ: false) - fields: FormFieldConfig[]; + + // ์„น์…˜ ํƒ€์ž…: fields (๊ธฐ๋ณธ) ๋˜๋Š” table (ํ…Œ์ด๋ธ” ํ˜•์‹) + type?: "fields" | "table"; + + // type: "fields" ์ผ ๋•Œ ์‚ฌ์šฉ + fields?: FormFieldConfig[]; // ๋ฐ˜๋ณต ์„น์…˜ (๊ฒธ์ง ๋“ฑ) repeatable?: boolean; @@ -199,6 +204,294 @@ export interface FormSectionConfig { // ์„น์…˜ ๋ ˆ์ด์•„์›ƒ columns?: number; // ํ•„๋“œ ๋ฐฐ์น˜ ์ปฌ๋Ÿผ ์ˆ˜ (๊ธฐ๋ณธ: 2) gap?: string; // ํ•„๋“œ ๊ฐ„ ๊ฐ„๊ฒฉ + + // type: "table" ์ผ ๋•Œ ์‚ฌ์šฉ + tableConfig?: TableSectionConfig; +} + +// ============================================ +// ํ…Œ์ด๋ธ” ์„น์…˜ ๊ด€๋ จ ํƒ€์ž… ์ •์˜ +// ============================================ + +/** + * ํ…Œ์ด๋ธ” ์„น์…˜ ์„ค์ • + * ๋ชจ๋‹ฌ ๋‚ด์—์„œ ํ…Œ์ด๋ธ” ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•˜๊ณ  ํŽธ์ง‘ํ•˜๋Š” ์„น์…˜ + */ +export interface TableSectionConfig { + // 1. ์†Œ์Šค ์„ค์ • (๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ํ…Œ์ด๋ธ”) + source: { + tableName: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: item_info) + displayColumns: string[]; // ๋ชจ๋‹ฌ์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ + searchColumns: string[]; // ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ + columnLabels?: Record; // ์ปฌ๋Ÿผ ๋ผ๋ฒจ (์ปฌ๋Ÿผ๋ช… -> ํ‘œ์‹œ ๋ผ๋ฒจ) + }; + + // 2. ํ•„ํ„ฐ ์„ค์ • + filters?: { + // ์‚ฌ์ „ ํ•„ํ„ฐ (ํ•ญ์ƒ ์ ์šฉ, ์‚ฌ์šฉ์ž์—๊ฒŒ ๋…ธ์ถœ๋˜์ง€ ์•Š์Œ) + preFilters?: TablePreFilter[]; + + // ๋ชจ๋‹ฌ ๋‚ด ํ•„ํ„ฐ UI (์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒ ๊ฐ€๋Šฅ) + modalFilters?: TableModalFilter[]; + }; + + // 3. ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์„ค์ • + columns: TableColumnConfig[]; + + // 4. ๊ณ„์‚ฐ ๊ทœ์น™ + calculations?: TableCalculationRule[]; + + // 5. ์ €์žฅ ์„ค์ • + saveConfig?: { + targetTable?: string; // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ €์žฅ ์‹œ (๋ฏธ์ง€์ • ์‹œ ๋ฉ”์ธ ํ…Œ์ด๋ธ”) + uniqueField?: string; // ์ค‘๋ณต ์ฒดํฌ ํ•„๋“œ + }; + + // 6. UI ์„ค์ • + uiConfig?: { + addButtonText?: string; // ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํ…์ŠคํŠธ (๊ธฐ๋ณธ: "ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰") + modalTitle?: string; // ๋ชจ๋‹ฌ ์ œ๋ชฉ (๊ธฐ๋ณธ: "ํ•ญ๋ชฉ ๊ฒ€์ƒ‰ ๋ฐ ์„ ํƒ") + multiSelect?: boolean; // ๋‹ค์ค‘ ์„ ํƒ ํ—ˆ์šฉ (๊ธฐ๋ณธ: true) + maxHeight?: string; // ํ…Œ์ด๋ธ” ์ตœ๋Œ€ ๋†’์ด (๊ธฐ๋ณธ: "400px") + }; +} + +/** + * ์‚ฌ์ „ ํ•„ํ„ฐ ์กฐ๊ฑด + * ๊ฒ€์ƒ‰ ์‹œ ํ•ญ์ƒ ์ ์šฉ๋˜๋Š” ํ•„ํ„ฐ ์กฐ๊ฑด + */ +export interface TablePreFilter { + column: string; // ํ•„ํ„ฐํ•  ์ปฌ๋Ÿผ + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "notIn" | "like"; + value: any; // ํ•„ํ„ฐ ๊ฐ’ +} + +/** + * ๋ชจ๋‹ฌ ๋‚ด ํ•„ํ„ฐ ์„ค์ • + * ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ํ•„ํ„ฐ UI + */ +export interface TableModalFilter { + column: string; // ํ•„ํ„ฐํ•  ์ปฌ๋Ÿผ + label: string; // ํ•„ํ„ฐ ๋ผ๋ฒจ + type: "category" | "text"; // ํ•„ํ„ฐ ํƒ€์ž… (category: ๋“œ๋กญ๋‹ค์šด, text: ํ…์ŠคํŠธ ์ž…๋ ฅ) + + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฐธ์กฐ (type: "category"์ผ ๋•Œ) - ํ…Œ์ด๋ธ”์—์„œ ์ปฌ๋Ÿผ์˜ distinct ๊ฐ’ ์กฐํšŒ + categoryRef?: { + tableName: string; // ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: "item_info") + columnName: string; // ์ปฌ๋Ÿผ๋ช… (์˜ˆ: "division") + }; + + // ์ •์  ์˜ต์…˜ (์ง์ ‘ ์ž…๋ ฅํ•œ ๊ฒฝ์šฐ) + options?: { value: string; label: string }[]; + + // ํ…Œ์ด๋ธ”์—์„œ ๋™์  ๋กœ๋“œ (ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์กฐํšŒ) + optionsFromTable?: { + tableName: string; + valueColumn: string; + labelColumn: string; + distinct?: boolean; // ์ค‘๋ณต ์ œ๊ฑฐ (๊ธฐ๋ณธ: true) + }; + + // ๊ธฐ๋ณธ๊ฐ’ + defaultValue?: any; +} + +/** + * ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์„ค์ • + */ +export interface TableColumnConfig { + field: string; // ํ•„๋“œ๋ช… (์ €์žฅํ•  ์ปฌ๋Ÿผ๋ช…) + label: string; // ์ปฌ๋Ÿผ ํ—ค๋” ๋ผ๋ฒจ + type: "text" | "number" | "date" | "select"; // ์ž…๋ ฅ ํƒ€์ž… + + // ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ (๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์—์„œ ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ๋ช…) + sourceField?: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช… (๋ฏธ์„ค์ • ์‹œ field์™€ ๋™์ผ) + + // ํŽธ์ง‘ ์„ค์ • + editable?: boolean; // ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) + calculated?: boolean; // ๊ณ„์‚ฐ ํ•„๋“œ ์—ฌ๋ถ€ (์ž๋™ ์ฝ๊ธฐ์ „์šฉ) + required?: boolean; // ํ•„์ˆ˜ ์ž…๋ ฅ ์—ฌ๋ถ€ + + // ๋„ˆ๋น„ ์„ค์ • + width?: string; // ๊ธฐ๋ณธ ๋„ˆ๋น„ (์˜ˆ: "150px") + minWidth?: string; // ์ตœ์†Œ ๋„ˆ๋น„ + maxWidth?: string; // ์ตœ๋Œ€ ๋„ˆ๋น„ + + // ๊ธฐ๋ณธ๊ฐ’ + defaultValue?: any; + + // Select ์˜ต์…˜ (type์ด "select"์ผ ๋•Œ) + selectOptions?: { value: string; label: string }[]; + + // ๊ฐ’ ๋งคํ•‘ (ํ•ต์‹ฌ ๊ธฐ๋Šฅ) - ๊ณ ๊ธ‰ ์„ค์ •์šฉ + valueMapping?: ValueMappingConfig; + + // ์ปฌ๋Ÿผ ๋ชจ๋“œ ์ „ํ™˜ (๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค) + columnModes?: ColumnModeConfig[]; + + // ์กฐํšŒ ์„ค์ • (๋™์  ๊ฐ’ ์กฐํšŒ) + lookup?: LookupConfig; + + // ๋‚ ์งœ ์ผ๊ด„ ์ ์šฉ (type์ด "date"์ผ ๋•Œ๋งŒ ์‚ฌ์šฉ) + // ํ™œ์„ฑํ™” ์‹œ ์ฒซ ๋ฒˆ์งธ ๋‚ ์งœ ์ž…๋ ฅ ์‹œ ๋ชจ๋“  ํ–‰์— ๋™์ผํ•œ ๋‚ ์งœ๊ฐ€ ์ž๋™ ์ ์šฉ๋จ + batchApply?: boolean; +} + +// ============================================ +// ์กฐํšŒ(Lookup) ์„ค์ • ๊ด€๋ จ ํƒ€์ž… ์ •์˜ +// ============================================ + +/** + * ์กฐํšŒ ์œ ํ˜• + * - sameTable: ๋™์ผ ํ…Œ์ด๋ธ” ์กฐํšŒ (์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๋‹ค๋ฅธ ์ปฌ๋Ÿผ ๊ฐ’) + * - relatedTable: ์—ฐ๊ด€ ํ…Œ์ด๋ธ” ์กฐํšŒ (ํ˜„์žฌ ํ–‰ ๊ธฐ์ค€์œผ๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ) + * - combinedLookup: ๋ณตํ•ฉ ์กฐ๊ฑด ์กฐํšŒ (๋‹ค๋ฅธ ์„น์…˜ ํ•„๋“œ + ํ˜„์žฌ ํ–‰ ์กฐํ•ฉ) + */ +export type LookupType = "sameTable" | "relatedTable" | "combinedLookup"; + +/** + * ๊ฐ’ ๋ณ€ํ™˜ ์„ค์ • + * ์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜ ์ด๋ฆ„ โ†’ ๊ฑฐ๋ž˜์ฒ˜ ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜ + */ +export interface LookupTransform { + enabled: boolean; // ๋ณ€ํ™˜ ์‚ฌ์šฉ ์—ฌ๋ถ€ + tableName: string; // ๋ณ€ํ™˜ ํ…Œ์ด๋ธ” (์˜ˆ: customer_mng) + matchColumn: string; // ์ฐพ์„ ์ปฌ๋Ÿผ (์˜ˆ: customer_name) + resultColumn: string; // ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ (์˜ˆ: customer_code) +} + +/** + * ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ • + * ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ ์กฐ๊ฑด ๊ฐ’์„ ์กฐํšŒํ•˜์—ฌ ์‚ฌ์šฉ (์ด๋ฆ„โ†’์ฝ”๋“œ ๋ณ€ํ™˜ ๋“ฑ) + */ +export interface ExternalTableLookup { + tableName: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ” + matchColumn: string; // ์กฐํšŒ ์กฐ๊ฑด ์ปฌ๋Ÿผ (WHERE ์ ˆ์—์„œ ๋น„๊ตํ•  ์ปฌ๋Ÿผ) + matchSourceType: "currentRow" | "sourceTable" | "sectionField"; // ๋น„๊ต๊ฐ’ ์ถœ์ฒ˜ + matchSourceField: string; // ๋น„๊ต๊ฐ’ ํ•„๋“œ๋ช… + matchSectionId?: string; // sectionField์ธ ๊ฒฝ์šฐ ์„น์…˜ ID + resultColumn: string; // ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ (SELECT ์ ˆ) +} + +/** + * ์กฐํšŒ ์กฐ๊ฑด ์„ค์ • + * + * sourceType ์„ค๋ช…: + * - "currentRow": ํ…Œ์ด๋ธ”์— ์„ค์ •๋œ ์ปฌ๋Ÿผ ํ•„๋“œ๊ฐ’ (rowData์—์„œ ๊ฐ€์ ธ์˜ด, ์˜ˆ: part_code, quantity) + * - "sourceTable": ์›๋ณธ ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๊ฐ’ (_sourceData์—์„œ ๊ฐ€์ ธ์˜ด, ์˜ˆ: item_number, company_code) + * - "sectionField": ํผ์˜ ๋‹ค๋ฅธ ์„น์…˜ ํ•„๋“œ๊ฐ’ (formData์—์„œ ๊ฐ€์ ธ์˜ด, ์˜ˆ: partner_id) + * - "externalTable": ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒํ•œ ๊ฐ’ (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ’์„ ์กฐํšŒํ•ด์„œ ์กฐ๊ฑด์œผ๋กœ ์‚ฌ์šฉ) + */ +export interface LookupCondition { + sourceType: "currentRow" | "sourceTable" | "sectionField" | "externalTable"; // ๊ฐ’ ์ถœ์ฒ˜ + sourceField: string; // ์ถœ์ฒ˜์˜ ํ•„๋“œ๋ช… (์ฐธ์กฐํ•  ํ•„๋“œ) + sectionId?: string; // sectionField์ธ ๊ฒฝ์šฐ ์„น์…˜ ID + targetColumn: string; // ์กฐํšŒ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ • (sourceType์ด "externalTable"์ธ ๊ฒฝ์šฐ) + externalLookup?: ExternalTableLookup; + + // ๊ฐ’ ๋ณ€ํ™˜ ์„ค์ • (์„ ํƒ) - ์ด๋ฆ„โ†’์ฝ”๋“œ ๋“ฑ ๋ณ€ํ™˜์ด ํ•„์š”ํ•  ๋•Œ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) + transform?: LookupTransform; +} + +/** + * ์กฐํšŒ ์˜ต์…˜ ์„ค์ • + * ํ•˜๋‚˜์˜ ์ปฌ๋Ÿผ์— ์—ฌ๋Ÿฌ ์กฐํšŒ ๋ฐฉ์‹์„ ์ •์˜ํ•˜๊ณ  ํ—ค๋”์—์„œ ์„ ํƒ ๊ฐ€๋Šฅ + */ +export interface LookupOption { + id: string; // ์˜ต์…˜ ๊ณ ์œ  ID + label: string; // ์˜ต์…˜ ๋ผ๋ฒจ (์˜ˆ: "๊ธฐ์ค€๋‹จ๊ฐ€", "๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€") + displayLabel?: string; // ํ—ค๋” ๋“œ๋กญ๋‹ค์šด์— ํ‘œ์‹œ๋  ํ…์ŠคํŠธ (์˜ˆ: "๊ธฐ์ค€๋‹จ๊ฐ€" โ†’ "๋‹จ๊ฐ€ (๊ธฐ์ค€๋‹จ๊ฐ€)") + type: LookupType; // ์กฐํšŒ ์œ ํ˜• + + // ์กฐํšŒ ํ…Œ์ด๋ธ” ์„ค์ • + tableName: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ” + valueColumn: string; // ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ + + // ์กฐํšŒ ์กฐ๊ฑด (์—ฌ๋Ÿฌ ์กฐ๊ฑด AND๋กœ ๊ฒฐํ•ฉ) + conditions: LookupCondition[]; + + // ๊ธฐ๋ณธ ์˜ต์…˜ ์—ฌ๋ถ€ + isDefault?: boolean; +} + +/** + * ์ปฌ๋Ÿผ ์กฐํšŒ ์„ค์ • + */ +export interface LookupConfig { + enabled: boolean; // ์กฐํšŒ ์‚ฌ์šฉ ์—ฌ๋ถ€ + options: LookupOption[]; // ์กฐํšŒ ์˜ต์…˜ ๋ชฉ๋ก + defaultOptionId?: string; // ๊ธฐ๋ณธ ์„ ํƒ ์˜ต์…˜ ID +} + +/** + * ๊ฐ’ ๋งคํ•‘ ์„ค์ • + * ์ปฌ๋Ÿผ ๊ฐ’์„ ์–ด๋””์„œ ๊ฐ€์ ธ์˜ฌ์ง€ ์ •์˜ + */ +export interface ValueMappingConfig { + type: "source" | "manual" | "external" | "internal"; + + // type: "source" - ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๋ณต์‚ฌ + sourceField?: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช… + + // type: "external" - ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ + externalRef?: { + tableName: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ” + valueColumn: string; // ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ + joinConditions: TableJoinCondition[]; + }; + + // type: "internal" - formData์˜ ๋‹ค๋ฅธ ํ•„๋“œ ๊ฐ’ ์ง์ ‘ ์‚ฌ์šฉ + internalField?: string; // formData์˜ ํ•„๋“œ๋ช… +} + +/** + * ํ…Œ์ด๋ธ” ์กฐ์ธ ์กฐ๊ฑด + * ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์‹œ ์‚ฌ์šฉํ•˜๋Š” ์กฐ์ธ ์กฐ๊ฑด + * + * sourceType ์„ค๋ช…: + * - "row": ํ˜„์žฌ ํ–‰์˜ ์„ค์ •๋œ ์ปฌ๋Ÿผ (rowData) + * - "sourceData": ์›๋ณธ ์†Œ์Šค ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ (_sourceData) + * - "formData": ํผ์˜ ๋‹ค๋ฅธ ์„น์…˜ ํ•„๋“œ (formData) + * - "externalTable": ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒํ•œ ๊ฐ’ + */ +export interface TableJoinCondition { + sourceType: "row" | "sourceData" | "formData" | "externalTable"; // ๊ฐ’ ์ถœ์ฒ˜ + sourceField: string; // ์ถœ์ฒ˜์˜ ํ•„๋“œ๋ช… + targetColumn: string; // ์กฐํšŒ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ + operator?: "=" | "!=" | ">" | "<" | ">=" | "<="; // ์—ฐ์‚ฐ์ž (๊ธฐ๋ณธ: "=") + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ • (sourceType์ด "externalTable"์ธ ๊ฒฝ์šฐ) + externalLookup?: ExternalTableLookup; + + // ๊ฐ’ ๋ณ€ํ™˜ ์„ค์ • (์„ ํƒ) - ์ด๋ฆ„โ†’์ฝ”๋“œ ๋“ฑ ์ค‘๊ฐ„ ๋ณ€ํ™˜์ด ํ•„์š”ํ•  ๋•Œ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) + transform?: { + tableName: string; // ๋ณ€ํ™˜ ํ…Œ์ด๋ธ” (์˜ˆ: customer_mng) + matchColumn: string; // ์ฐพ์„ ์ปฌ๋Ÿผ (์˜ˆ: customer_name) + resultColumn: string; // ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ (์˜ˆ: customer_code) + }; +} + +/** + * ์ปฌ๋Ÿผ ๋ชจ๋“œ ์„ค์ • + * ํ•˜๋‚˜์˜ ์ปฌ๋Ÿผ์—์„œ ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์ „ํ™˜ํ•˜์—ฌ ์‚ฌ์šฉ + */ +export interface ColumnModeConfig { + id: string; // ๋ชจ๋“œ ๊ณ ์œ  ID + label: string; // ๋ชจ๋“œ ๋ผ๋ฒจ (์˜ˆ: "๊ธฐ์ค€ ๋‹จ๊ฐ€", "๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€") + isDefault?: boolean; // ๊ธฐ๋ณธ ๋ชจ๋“œ ์—ฌ๋ถ€ + valueMapping: ValueMappingConfig; // ์ด ๋ชจ๋“œ์˜ ๊ฐ’ ๋งคํ•‘ +} + +/** + * ํ…Œ์ด๋ธ” ๊ณ„์‚ฐ ๊ทœ์น™ + * ๋‹ค๋ฅธ ์ปฌ๋Ÿผ ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๋™ ๊ณ„์‚ฐ + */ +export interface TableCalculationRule { + resultField: string; // ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ํ•„๋“œ + formula: string; // ๊ณ„์‚ฐ ๊ณต์‹ (์˜ˆ: "quantity * unit_price") + dependencies: string[]; // ์˜์กดํ•˜๋Š” ํ•„๋“œ๋“ค } // ๋‹ค์ค‘ ํ–‰ ์ €์žฅ ์„ค์ • @@ -214,6 +507,21 @@ export interface MultiRowSaveConfig { mainSectionFields?: string[]; // ๋ฉ”์ธ ํ–‰์—๋งŒ ์ €์žฅํ•  ํ•„๋“œ } +/** + * ์„น์…˜๋ณ„ ์ €์žฅ ๋ฐฉ์‹ ์„ค์ • + * ๊ณตํ†ต ์ €์žฅ: ํ•ด๋‹น ์„น์…˜์˜ ํ•„๋“œ ๊ฐ’์ด ๋ชจ๋“  ํ’ˆ๋ชฉ ํ–‰์— ๋™์ผํ•˜๊ฒŒ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค (์˜ˆ: ์ˆ˜์ฃผ๋ฒˆํ˜ธ, ๊ฑฐ๋ž˜์ฒ˜) + * ๊ฐœ๋ณ„ ์ €์žฅ: ํ•ด๋‹น ์„น์…˜์˜ ํ•„๋“œ ๊ฐ’์ด ๊ฐ ํ’ˆ๋ชฉ๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค (์˜ˆ: ํ’ˆ๋ชฉ์ฝ”๋“œ, ์ˆ˜๋Ÿ‰, ๋‹จ๊ฐ€) + */ +export interface SectionSaveMode { + sectionId: string; + saveMode: "common" | "individual"; // ๊ณตํ†ต ์ €์žฅ / ๊ฐœ๋ณ„ ์ €์žฅ + // ํ•„๋“œ๋ณ„ ์„ธ๋ถ€ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ - ์„น์…˜ ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•  ํ•„๋“œ) + fieldOverrides?: { + fieldName: string; + saveMode: "common" | "individual"; + }[]; +} + // ์ €์žฅ ์„ค์ • export interface SaveConfig { tableName: string; @@ -225,6 +533,9 @@ export interface SaveConfig { // ์ปค์Šคํ…€ API ์ €์žฅ ์„ค์ • (ํ…Œ์ด๋ธ” ์ง์ ‘ ์ €์žฅ ๋Œ€์‹  ์ „์šฉ API ์‚ฌ์šฉ) customApiSave?: CustomApiSaveConfig; + // ์„น์…˜๋ณ„ ์ €์žฅ ๋ฐฉ์‹ ์„ค์ • + sectionSaveModes?: SectionSaveMode[]; + // ์ €์žฅ ํ›„ ๋™์ž‘ (๊ฐ„ํŽธ ์„ค์ •) showToast?: boolean; // ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ (๊ธฐ๋ณธ: true) refreshParent?: boolean; // ๋ถ€๋ชจ ์ƒˆ๋กœ๊ณ ์นจ (๊ธฐ๋ณธ: true) @@ -432,3 +743,69 @@ export const LINKED_FIELD_DISPLAY_FORMAT_OPTIONS = [ { value: "code_name", label: "์ฝ”๋“œ - ์ด๋ฆ„ (์˜ˆ: SALES - ์˜์—…๋ถ€)" }, { value: "name_code", label: "์ด๋ฆ„ (์ฝ”๋“œ) (์˜ˆ: ์˜์—…๋ถ€ (SALES))" }, ] as const; + +// ============================================ +// ํ…Œ์ด๋ธ” ์„น์…˜ ๊ด€๋ จ ์ƒ์ˆ˜ +// ============================================ + +// ์„น์…˜ ํƒ€์ž… ์˜ต์…˜ +export const SECTION_TYPE_OPTIONS = [ + { value: "fields", label: "ํ•„๋“œ ํƒ€์ž…" }, + { value: "table", label: "ํ…Œ์ด๋ธ” ํƒ€์ž…" }, +] as const; + +// ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ํƒ€์ž… ์˜ต์…˜ +export const TABLE_COLUMN_TYPE_OPTIONS = [ + { value: "text", label: "ํ…์ŠคํŠธ" }, + { value: "number", label: "์ˆซ์ž" }, + { value: "date", label: "๋‚ ์งœ" }, + { value: "select", label: "์„ ํƒ(๋“œ๋กญ๋‹ค์šด)" }, +] as const; + +// ๊ฐ’ ๋งคํ•‘ ํƒ€์ž… ์˜ต์…˜ +export const VALUE_MAPPING_TYPE_OPTIONS = [ + { value: "source", label: "์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๋ณต์‚ฌ" }, + { value: "manual", label: "์‚ฌ์šฉ์ž ์ง์ ‘ ์ž…๋ ฅ" }, + { value: "external", label: "์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์กฐํšŒ" }, + { value: "internal", label: "ํผ ๋ฐ์ดํ„ฐ ์ฐธ์กฐ" }, +] as const; + +// ์กฐ์ธ ์กฐ๊ฑด ์†Œ์Šค ํƒ€์ž… ์˜ต์…˜ +export const JOIN_SOURCE_TYPE_OPTIONS = [ + { value: "row", label: "ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ" }, + { value: "formData", label: "ํผ ํ•„๋“œ ๊ฐ’" }, +] as const; + +// ํ•„ํ„ฐ ์—ฐ์‚ฐ์ž ์˜ต์…˜ +export const FILTER_OPERATOR_OPTIONS = [ + { value: "=", label: "๊ฐ™์Œ (=)" }, + { value: "!=", label: "๋‹ค๋ฆ„ (!=)" }, + { value: ">", label: "ํผ (>)" }, + { value: "<", label: "์ž‘์Œ (<)" }, + { value: ">=", label: "ํฌ๊ฑฐ๋‚˜ ๊ฐ™์Œ (>=)" }, + { value: "<=", label: "์ž‘๊ฑฐ๋‚˜ ๊ฐ™์Œ (<=)" }, + { value: "in", label: "ํฌํ•จ (IN)" }, + { value: "notIn", label: "๋ฏธํฌํ•จ (NOT IN)" }, + { value: "like", label: "์œ ์‚ฌ (LIKE)" }, +] as const; + +// ๋ชจ๋‹ฌ ํ•„ํ„ฐ ํƒ€์ž… ์˜ต์…˜ +export const MODAL_FILTER_TYPE_OPTIONS = [ + { value: "category", label: "ํ…Œ์ด๋ธ” ์กฐํšŒ" }, + { value: "text", label: "ํ…์ŠคํŠธ ์ž…๋ ฅ" }, +] as const; + +// ์กฐํšŒ ์œ ํ˜• ์˜ต์…˜ +export const LOOKUP_TYPE_OPTIONS = [ + { value: "sameTable", label: "๋™์ผ ํ…Œ์ด๋ธ” ์กฐํšŒ" }, + { value: "relatedTable", label: "์—ฐ๊ด€ ํ…Œ์ด๋ธ” ์กฐํšŒ" }, + { value: "combinedLookup", label: "๋ณตํ•ฉ ์กฐ๊ฑด ์กฐํšŒ" }, +] as const; + +// ์กฐํšŒ ์กฐ๊ฑด ์†Œ์Šค ํƒ€์ž… ์˜ต์…˜ +export const LOOKUP_CONDITION_SOURCE_OPTIONS = [ + { value: "currentRow", label: "ํ˜„์žฌ ํ–‰" }, + { value: "sourceTable", label: "์†Œ์Šค ํ…Œ์ด๋ธ”" }, + { value: "sectionField", label: "๋‹ค๋ฅธ ์„น์…˜" }, + { value: "externalTable", label: "์™ธ๋ถ€ ํ…Œ์ด๋ธ”" }, +] as const; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 19a41a52..de98028a 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -675,6 +675,14 @@ export class ButtonActionExecutor { console.log("โš ๏ธ [handleSave] formData ์ „์ฒด ๋‚ด์šฉ:", context.formData); } + // ๐Ÿ†• Universal Form Modal ํ…Œ์ด๋ธ” ์„น์…˜ ๋ณ‘ํ•ฉ ์ €์žฅ ์ฒ˜๋ฆฌ + // ๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ ๋‚ด๋ถ€์— _tableSection_ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ณตํ†ต ํ•„๋“œ + ๊ฐœ๋ณ„ ํ’ˆ๋ชฉ ๋ณ‘ํ•ฉ ์ €์žฅ + const universalFormModalResult = await this.handleUniversalFormModalTableSectionSave(config, context, formData); + if (universalFormModalResult.handled) { + console.log("โœ… [handleSave] Universal Form Modal ํ…Œ์ด๋ธ” ์„น์…˜ ์ €์žฅ ์™„๋ฃŒ"); + return universalFormModalResult.success; + } + // ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ if (config.validateForm) { const validation = this.validateFormData(formData); @@ -1479,6 +1487,244 @@ export class ButtonActionExecutor { } } + /** + * ๐Ÿ†• Universal Form Modal ํ…Œ์ด๋ธ” ์„น์…˜ ๋ณ‘ํ•ฉ ์ €์žฅ ์ฒ˜๋ฆฌ + * ๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ ๋‚ด๋ถ€์˜ ๊ณตํ†ต ํ•„๋“œ + _tableSection_ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ ํ’ˆ๋ชฉ๋ณ„๋กœ ์ €์žฅ + * ์ˆ˜์ • ๋ชจ๋“œ: INSERT/UPDATE/DELETE ์ง€์› + */ + private static async handleUniversalFormModalTableSectionSave( + config: ButtonActionConfig, + context: ButtonActionContext, + formData: Record, + ): Promise<{ handled: boolean; success: boolean }> { + const { tableName, screenId } = context; + + // ๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ ํ‚ค ์ฐพ๊ธฐ (์ปฌ๋Ÿผ๋ช…์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ) + const universalFormModalKey = Object.keys(formData).find((key) => { + const value = formData[key]; + if (!value || typeof value !== "object" || Array.isArray(value)) return false; + // _tableSection_ ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ + return Object.keys(value).some((k) => k.startsWith("_tableSection_")); + }); + + if (!universalFormModalKey) { + return { handled: false, success: false }; + } + + console.log("๐ŸŽฏ [handleUniversalFormModalTableSectionSave] Universal Form Modal ๊ฐ์ง€:", universalFormModalKey); + + const modalData = formData[universalFormModalKey]; + + // _tableSection_ ๋ฐ์ดํ„ฐ ์ถ”์ถœ + const tableSectionData: Record = {}; + const commonFieldsData: Record = {}; + + // ๐Ÿ†• ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ถ”์ถœ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ UPDATE/DELETE ์ถ”์ ์šฉ) + // modalData ๋‚ด๋ถ€ ๋˜๋Š” ์ตœ์ƒ์œ„ formData์—์„œ ์ฐพ์Œ + const originalGroupedData: any[] = modalData._originalGroupedData || formData._originalGroupedData || []; + + for (const [key, value] of Object.entries(modalData)) { + if (key.startsWith("_tableSection_")) { + const sectionId = key.replace("_tableSection_", ""); + tableSectionData[sectionId] = value as any[]; + } else if (!key.startsWith("_")) { + // _๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋Š” ํ•„๋“œ๋Š” ๊ณตํ†ต ํ•„๋“œ๋กœ ์ฒ˜๋ฆฌ + commonFieldsData[key] = value; + } + } + + console.log("๐ŸŽฏ [handleUniversalFormModalTableSectionSave] ๋ฐ์ดํ„ฐ ๋ถ„๋ฆฌ:", { + commonFields: Object.keys(commonFieldsData), + tableSections: Object.keys(tableSectionData), + tableSectionCounts: Object.entries(tableSectionData).map(([k, v]) => ({ [k]: v.length })), + originalGroupedDataCount: originalGroupedData.length, + isEditMode: originalGroupedData.length > 0, + }); + + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ณ  ์›๋ณธ ๋ฐ์ดํ„ฐ๋„ ์—†์œผ๋ฉด ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ + const hasTableSectionData = Object.values(tableSectionData).some((arr) => arr.length > 0); + if (!hasTableSectionData && originalGroupedData.length === 0) { + console.log("โš ๏ธ [handleUniversalFormModalTableSectionSave] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์—†์Œ - ์ผ๋ฐ˜ ์ €์žฅ์œผ๋กœ ์ „ํ™˜"); + return { handled: false, success: false }; + } + + try { + // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ + if (!context.userId) { + throw new Error("์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”."); + } + + const userInfo = { + writer: context.userId, + created_by: context.userId, + updated_by: context.userId, + company_code: context.companyCode || "", + }; + + let insertedCount = 0; + let updatedCount = 0; + let deletedCount = 0; + + // ๊ฐ ํ…Œ์ด๋ธ” ์„น์…˜ ์ฒ˜๋ฆฌ + for (const [sectionId, currentItems] of Object.entries(tableSectionData)) { + console.log(`๐Ÿ”„ [handleUniversalFormModalTableSectionSave] ์„น์…˜ ${sectionId} ์ฒ˜๋ฆฌ ์‹œ์ž‘: ${currentItems.length}๊ฐœ ํ’ˆ๋ชฉ`); + + // 1๏ธโƒฃ ์‹ ๊ทœ ํ’ˆ๋ชฉ INSERT (id๊ฐ€ ์—†๋Š” ํ•ญ๋ชฉ) + const newItems = currentItems.filter((item) => !item.id); + for (const item of newItems) { + const rowToSave = { ...commonFieldsData, ...item, ...userInfo }; + + // ๋‚ด๋ถ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ๊ฑฐ + Object.keys(rowToSave).forEach((key) => { + if (key.startsWith("_")) { + delete rowToSave[key]; + } + }); + + console.log("โž• [INSERT] ์‹ ๊ทœ ํ’ˆ๋ชฉ:", rowToSave); + + const saveResult = await DynamicFormApi.saveFormData({ + screenId: screenId!, + tableName: tableName!, + data: rowToSave, + }); + + if (!saveResult.success) { + throw new Error(saveResult.message || "์‹ ๊ทœ ํ’ˆ๋ชฉ ์ €์žฅ ์‹คํŒจ"); + } + + insertedCount++; + } + + // 2๏ธโƒฃ ๊ธฐ์กด ํ’ˆ๋ชฉ UPDATE (id๊ฐ€ ์žˆ๋Š” ํ•ญ๋ชฉ, ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ๋งŒ) + const existingItems = currentItems.filter((item) => item.id); + for (const item of existingItems) { + const originalItem = originalGroupedData.find((orig) => orig.id === item.id); + + if (!originalItem) { + console.warn(`โš ๏ธ [UPDATE] ์›๋ณธ ๋ฐ์ดํ„ฐ ์—†์Œ - INSERT๋กœ ์ฒ˜๋ฆฌ: id=${item.id}`); + // ์›๋ณธ์ด ์—†์œผ๋ฉด ์‹ ๊ทœ๋กœ ์ฒ˜๋ฆฌ + const rowToSave = { ...commonFieldsData, ...item, ...userInfo }; + Object.keys(rowToSave).forEach((key) => { + if (key.startsWith("_")) { + delete rowToSave[key]; + } + }); + delete rowToSave.id; // id ์ œ๊ฑฐํ•˜์—ฌ INSERT + + const saveResult = await DynamicFormApi.saveFormData({ + screenId: screenId!, + tableName: tableName!, + data: rowToSave, + }); + + if (!saveResult.success) { + throw new Error(saveResult.message || "ํ’ˆ๋ชฉ ์ €์žฅ ์‹คํŒจ"); + } + + insertedCount++; + continue; + } + + // ๋ณ€๊ฒฝ ์‚ฌํ•ญ ํ™•์ธ (๊ณตํ†ต ํ•„๋“œ ํฌํ•จ) + const currentDataWithCommon = { ...commonFieldsData, ...item }; + const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon); + + if (hasChanges) { + console.log(`๐Ÿ”„ [UPDATE] ํ’ˆ๋ชฉ ์ˆ˜์ •: id=${item.id}`); + + // ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ + const updateResult = await DynamicFormApi.updateFormDataPartial( + item.id, + originalItem, + currentDataWithCommon, + tableName!, + ); + + if (!updateResult.success) { + throw new Error(updateResult.message || "ํ’ˆ๋ชฉ ์ˆ˜์ • ์‹คํŒจ"); + } + + updatedCount++; + } else { + console.log(`โญ๏ธ [SKIP] ๋ณ€๊ฒฝ ์—†์Œ: id=${item.id}`); + } + } + + // 3๏ธโƒฃ ์‚ญ์ œ๋œ ํ’ˆ๋ชฉ DELETE (์›๋ณธ์—๋Š” ์žˆ์ง€๋งŒ ํ˜„์žฌ์—๋Š” ์—†๋Š” ํ•ญ๋ชฉ) + const currentIds = new Set(currentItems.map((item) => item.id).filter(Boolean)); + const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(orig.id)); + + for (const deletedItem of deletedItems) { + console.log(`๐Ÿ—‘๏ธ [DELETE] ํ’ˆ๋ชฉ ์‚ญ์ œ: id=${deletedItem.id}`); + + const deleteResult = await DynamicFormApi.deleteFormDataFromTable(tableName!, deletedItem.id); + + if (!deleteResult.success) { + throw new Error(deleteResult.message || "ํ’ˆ๋ชฉ ์‚ญ์ œ ์‹คํŒจ"); + } + + deletedCount++; + } + } + + // ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + const resultParts: string[] = []; + if (insertedCount > 0) resultParts.push(`${insertedCount}๊ฐœ ์ถ”๊ฐ€`); + if (updatedCount > 0) resultParts.push(`${updatedCount}๊ฐœ ์ˆ˜์ •`); + if (deletedCount > 0) resultParts.push(`${deletedCount}๊ฐœ ์‚ญ์ œ`); + + const resultMessage = resultParts.length > 0 ? resultParts.join(", ") : "๋ณ€๊ฒฝ ์‚ฌํ•ญ ์—†์Œ"; + + console.log(`โœ… [handleUniversalFormModalTableSectionSave] ์™„๋ฃŒ: ${resultMessage}`); + toast.success(`์ €์žฅ ์™„๋ฃŒ: ${resultMessage}`); + + // ์ €์žฅ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new CustomEvent("saveSuccess")); + window.dispatchEvent(new CustomEvent("refreshTable")); + // EditModal ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new CustomEvent("closeEditModal")); + + return { handled: true, success: true }; + } catch (error: any) { + console.error("โŒ [handleUniversalFormModalTableSectionSave] ์ €์žฅ ์˜ค๋ฅ˜:", error); + toast.error(error.message || "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + return { handled: true, success: false }; + } + } + + /** + * ๋‘ ๊ฐ์ฒด ๊ฐ„ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ํ™•์ธ + */ + private static checkForChanges(original: Record, current: Record): boolean { + // ๋น„๊ตํ•  ํ•„๋“œ ๋ชฉ๋ก (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ์™ธ) + const fieldsToCompare = new Set([ + ...Object.keys(original).filter((k) => !k.startsWith("_")), + ...Object.keys(current).filter((k) => !k.startsWith("_")), + ]); + + for (const field of fieldsToCompare) { + // ์‹œ์Šคํ…œ ํ•„๋“œ๋Š” ๋น„๊ต์—์„œ ์ œ์™ธ + if (["created_date", "updated_date", "created_by", "updated_by", "writer"].includes(field)) { + continue; + } + + const originalValue = original[field]; + const currentValue = current[field]; + + // null/undefined ํ†ต์ผ ์ฒ˜๋ฆฌ + const normalizedOriginal = originalValue === null || originalValue === undefined ? "" : String(originalValue); + const normalizedCurrent = currentValue === null || currentValue === undefined ? "" : String(currentValue); + + if (normalizedOriginal !== normalizedCurrent) { + console.log(` ๐Ÿ“ ๋ณ€๊ฒฝ ๊ฐ์ง€: ${field} = "${normalizedOriginal}" โ†’ "${normalizedCurrent}"`); + return true; + } + } + + return false; + } + /** * ๐Ÿ†• ๋ฐฐ์น˜ ์ €์žฅ ์•ก์…˜ ์ฒ˜๋ฆฌ (SelectedItemsDetailInput์šฉ - ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ) * ItemData[] โ†’ ๊ฐ ํ’ˆ๋ชฉ์˜ details ๋ฐฐ์—ด์„ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ๋กœ ์ €์žฅ