diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 6e0432d1..92eb4bb7 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react"; import { ItemSelectionModal } from "./ItemSelectionModal"; import { RepeaterTable } from "./RepeaterTable"; -import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition, DynamicDataSourceOption } from "./types"; import { useCalculation } from "./useCalculation"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -293,6 +293,9 @@ export function ModalRepeaterTableComponent({ // ๐Ÿ†• ์ˆ˜์ฃผ์ผ ์ผ๊ด„ ์ ์šฉ ํ”Œ๋ž˜๊ทธ (๋”ฑ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) const [isOrderDateApplied, setIsOrderDateApplied] = useState(false); + + // ๐Ÿ†• ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ™œ์„ฑํ™” ์ƒํƒœ (์ปฌ๋Ÿผ๋ณ„๋กœ ํ˜„์žฌ ์„ ํƒ๋œ ์˜ต์…˜ ID) + const [activeDataSources, setActiveDataSources] = useState>({}); // columns๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด sourceColumns๋กœ๋ถ€ํ„ฐ ์ž๋™ ์ƒ์„ฑ const columns = React.useMemo((): RepeaterColumnConfig[] => { @@ -409,6 +412,193 @@ export function ModalRepeaterTableComponent({ }, [localValue, columnName, component?.id, onFormDataChange, targetTable]); const { calculateRow, calculateAll } = useCalculation(calculationRules); + + /** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœ + * ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๋ชจ๋“  ํ–‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กœ์šด ์†Œ์Šค์—์„œ ๋‹ค์‹œ ์กฐํšŒ + */ + const handleDataSourceChange = async (columnField: string, optionId: string) => { + console.log(`๐Ÿ”„ ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ณ€๊ฒฝ: ${columnField} โ†’ ${optionId}`); + + // ํ™œ์„ฑํ™” ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setActiveDataSources((prev) => ({ + ...prev, + [columnField]: optionId, + })); + + // ํ•ด๋‹น ์ปฌ๋Ÿผ ์ฐพ๊ธฐ + const column = columns.find((col) => col.field === columnField); + if (!column?.dynamicDataSource?.enabled) { + console.warn(`โš ๏ธ ์ปฌ๋Ÿผ "${columnField}"์— ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ`); + return; + } + + // ์„ ํƒ๋œ ์˜ต์…˜ ์ฐพ๊ธฐ + const option = column.dynamicDataSource.options.find((opt) => opt.id === optionId); + if (!option) { + console.warn(`โš ๏ธ ์˜ต์…˜ "${optionId}"์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ`); + return; + } + + // ๋ชจ๋“  ํ–‰์— ๋Œ€ํ•ด ์ƒˆ ๊ฐ’ ์กฐํšŒ + const updatedData = await Promise.all( + localValue.map(async (row, index) => { + try { + const newValue = await fetchDynamicValue(option, row); + console.log(` โœ… ํ–‰ ${index}: ${columnField} = ${newValue}`); + return { + ...row, + [columnField]: newValue, + }; + } catch (error) { + console.error(` โŒ ํ–‰ ${index} ์กฐํšŒ ์‹คํŒจ:`, error); + return row; + } + }) + ); + + // ๊ณ„์‚ฐ ํ•„๋“œ ์—…๋ฐ์ดํŠธ ํ›„ ๋ฐ์ดํ„ฐ ๋ฐ˜์˜ + const calculatedData = calculateAll(updatedData); + handleChange(calculatedData); + }; + + /** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์˜ต์…˜์— ๋”ฐ๋ผ ๊ฐ’ ์กฐํšŒ + */ + async function fetchDynamicValue( + option: DynamicDataSourceOption, + rowData: any + ): Promise { + if (option.sourceType === "table" && option.tableConfig) { + // ํ…Œ์ด๋ธ” ์ง์ ‘ ์กฐํšŒ (๋‹จ์ˆœ ์กฐ์ธ) + const { tableName, valueColumn, joinConditions } = option.tableConfig; + + const whereConditions: Record = {}; + for (const cond of joinConditions) { + const value = rowData[cond.sourceField]; + if (value === undefined || value === null) { + console.warn(`โš ๏ธ ์กฐ์ธ ์กฐ๊ฑด์˜ ์†Œ์Šค ํ•„๋“œ "${cond.sourceField}" ๊ฐ’์ด ์—†์Œ`); + return undefined; + } + whereConditions[cond.targetField] = value; + } + + console.log(`๐Ÿ” ํ…Œ์ด๋ธ” ์กฐํšŒ: ${tableName}`, whereConditions); + + 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; + + } else if (option.sourceType === "multiTable" && option.multiTableConfig) { + // ํ…Œ์ด๋ธ” ๋ณตํ•ฉ ์กฐ์ธ (2๊ฐœ ์ด์ƒ ํ…Œ์ด๋ธ” ์ˆœ์ฐจ ์กฐ์ธ) + const { joinChain, valueColumn } = option.multiTableConfig; + + if (!joinChain || joinChain.length === 0) { + console.warn("โš ๏ธ ์กฐ์ธ ์ฒด์ธ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."); + return undefined; + } + + console.log(`๐Ÿ”— ๋ณตํ•ฉ ์กฐ์ธ ์‹œ์ž‘: ${joinChain.length}๋‹จ๊ณ„`); + + // ํ˜„์žฌ ๊ฐ’์„ ์ถ”์  (์ฒซ ๋‹จ๊ณ„๋Š” ํ˜„์žฌ ํ–‰์—์„œ ์‹œ์ž‘) + let currentValue: any = null; + let currentRow: any = null; + + for (let i = 0; i < joinChain.length; i++) { + const step = joinChain[i]; + const { tableName, joinCondition, outputField } = step; + + // ์กฐ์ธ ์กฐ๊ฑด ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + let fromValue: any; + if (i === 0) { + // ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„: ํ˜„์žฌ ํ–‰์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + fromValue = rowData[joinCondition.fromField]; + console.log(` ๐Ÿ“ ๋‹จ๊ณ„ ${i + 1}: ํ˜„์žฌํ–‰.${joinCondition.fromField} = ${fromValue}`); + } else { + // ์ดํ›„ ๋‹จ๊ณ„: ์ด์ „ ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + fromValue = currentRow?.[joinCondition.fromField] || currentValue; + console.log(` ๐Ÿ“ ๋‹จ๊ณ„ ${i + 1}: ์ด์ „๊ฒฐ๊ณผ.${joinCondition.fromField} = ${fromValue}`); + } + + if (fromValue === undefined || fromValue === null) { + console.warn(`โš ๏ธ ๋‹จ๊ณ„ ${i + 1}: ์กฐ์ธ ์กฐ๊ฑด ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค. (${joinCondition.fromField})`); + return undefined; + } + + // ํ…Œ์ด๋ธ” ์กฐํšŒ + const whereConditions: Record = { + [joinCondition.toField]: fromValue + }; + + console.log(` ๐Ÿ” ๋‹จ๊ณ„ ${i + 1}: ${tableName} ์กฐํšŒ`, whereConditions); + + try { + 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) { + currentRow = response.data.data.data[0]; + currentValue = outputField ? currentRow[outputField] : currentRow; + console.log(` โœ… ๋‹จ๊ณ„ ${i + 1} ์„ฑ๊ณต:`, { outputField, value: currentValue }); + } else { + console.warn(` โš ๏ธ ๋‹จ๊ณ„ ${i + 1}: ์กฐํšŒ ๊ฒฐ๊ณผ ์—†์Œ`); + return undefined; + } + } catch (error) { + console.error(` โŒ ๋‹จ๊ณ„ ${i + 1} ์กฐํšŒ ์‹คํŒจ:`, error); + return undefined; + } + } + + // ์ตœ์ข… ๊ฐ’ ๋ฐ˜ํ™˜ (๋งˆ์ง€๋ง‰ ํ…Œ์ด๋ธ”์—์„œ valueColumn ๊ฐ€์ ธ์˜ค๊ธฐ) + const finalValue = currentRow?.[valueColumn]; + console.log(`๐ŸŽฏ ๋ณตํ•ฉ ์กฐ์ธ ์™„๋ฃŒ: ${valueColumn} = ${finalValue}`); + return finalValue; + + } else if (option.sourceType === "api" && option.apiConfig) { + // ์ „์šฉ API ํ˜ธ์ถœ (๋ณต์žกํ•œ ๋‹ค์ค‘ ์กฐ์ธ) + const { endpoint, method = "GET", parameterMappings, responseValueField } = option.apiConfig; + + // ํŒŒ๋ผ๋ฏธํ„ฐ ๋นŒ๋“œ + const params: Record = {}; + for (const mapping of parameterMappings) { + const value = rowData[mapping.sourceField]; + if (value !== undefined && value !== null) { + params[mapping.paramName] = value; + } + } + + console.log(`๐Ÿ” API ํ˜ธ์ถœ: ${method} ${endpoint}`, params); + + let response; + if (method === "POST") { + response = await apiClient.post(endpoint, params); + } else { + response = await apiClient.get(endpoint, { params }); + } + + if (response.data.success && response.data.data) { + // responseValueField๋กœ ๊ฐ’ ์ถ”์ถœ (์ค‘์ฒฉ ๊ฒฝ๋กœ ์ง€์›: "data.price") + const keys = responseValueField.split("."); + let value = response.data.data; + for (const key of keys) { + value = value?.[key]; + } + return value; + } + return undefined; + } + + return undefined; + } // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ์— ๊ณ„์‚ฐ ํ•„๋“œ ์ ์šฉ useEffect(() => { @@ -579,6 +769,8 @@ export function ModalRepeaterTableComponent({ onDataChange={handleChange} onRowChange={handleRowChange} onRowDelete={handleRowDelete} + activeDataSources={activeDataSources} + onDataSourceChange={handleDataSourceChange} /> {/* ํ•ญ๋ชฉ ์„ ํƒ ๋ชจ๋‹ฌ */} diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx index 507ab54d..914e34f1 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx @@ -9,7 +9,8 @@ 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 } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule, DynamicDataSourceConfig, DynamicDataSourceOption, MultiTableJoinStep } from "./types"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { cn } from "@/lib/utils"; @@ -169,6 +170,10 @@ export function ModalRepeaterTableConfigPanel({ const [openTableCombo, setOpenTableCombo] = useState(false); const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false); const [openUniqueFieldCombo, setOpenUniqueFieldCombo] = useState(false); + + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • ๋ชจ๋‹ฌ + const [dynamicSourceModalOpen, setDynamicSourceModalOpen] = useState(false); + const [editingDynamicSourceColumnIndex, setEditingDynamicSourceColumnIndex] = useState(null); // config ๋ณ€๊ฒฝ ์‹œ localConfig ๋™๊ธฐํ™” (cleanupInitialConfig ์ ์šฉ) useEffect(() => { @@ -397,6 +402,101 @@ export function ModalRepeaterTableConfigPanel({ updateConfig({ calculationRules: rules }); }; + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • ํ•จ์ˆ˜๋“ค + const openDynamicSourceModal = (columnIndex: number) => { + setEditingDynamicSourceColumnIndex(columnIndex); + setDynamicSourceModalOpen(true); + }; + + const toggleDynamicDataSource = (columnIndex: number, enabled: boolean) => { + const columns = [...(localConfig.columns || [])]; + if (enabled) { + columns[columnIndex] = { + ...columns[columnIndex], + dynamicDataSource: { + enabled: true, + options: [], + }, + }; + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { dynamicDataSource, ...rest } = columns[columnIndex]; + columns[columnIndex] = rest; + } + updateConfig({ columns }); + }; + + const addDynamicSourceOption = (columnIndex: number) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + const newOption: DynamicDataSourceOption = { + id: `option_${Date.now()}`, + label: "์ƒˆ ์˜ต์…˜", + sourceType: "table", + tableConfig: { + tableName: "", + valueColumn: "", + joinConditions: [], + }, + }; + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + enabled: true, + options: [...(col.dynamicDataSource?.options || []), newOption], + }, + }; + updateConfig({ columns }); + }; + + const updateDynamicSourceOption = (columnIndex: number, optionIndex: number, updates: Partial) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + const options = [...(col.dynamicDataSource?.options || [])]; + options[optionIndex] = { ...options[optionIndex], ...updates }; + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + options, + }, + }; + updateConfig({ columns }); + }; + + const removeDynamicSourceOption = (columnIndex: number, optionIndex: number) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + const options = [...(col.dynamicDataSource?.options || [])]; + options.splice(optionIndex, 1); + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + options, + }, + }; + updateConfig({ columns }); + }; + + const setDefaultDynamicSourceOption = (columnIndex: number, optionId: string) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + defaultOptionId: optionId, + }, + }; + updateConfig({ columns }); + }; + return (
{/* ์†Œ์Šค/์ €์žฅ ํ…Œ์ด๋ธ” ์„ค์ • */} @@ -1327,6 +1427,60 @@ export function ModalRepeaterTableConfigPanel({ )}
+ + {/* 6. ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • */} +
+
+ + toggleDynamicDataSource(index, checked)} + /> +
+

+ ์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ์œผ๋กœ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ „ํ™˜ (์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€, ํ’ˆ๋ชฉ๋ณ„ ๋‹จ๊ฐ€) +

+ + {col.dynamicDataSource?.enabled && ( +
+
+ + {col.dynamicDataSource.options.length}๊ฐœ ์˜ต์…˜ ์„ค์ •๋จ + + +
+ + {/* ์˜ต์…˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {col.dynamicDataSource.options.length > 0 && ( +
+ {col.dynamicDataSource.options.map((opt) => ( + + {opt.label} + {col.dynamicDataSource?.defaultOptionId === opt.id && " (๊ธฐ๋ณธ)"} + + ))} +
+ )} +
+ )} +
))} @@ -1493,6 +1647,650 @@ export function ModalRepeaterTableConfigPanel({ + + {/* ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • ๋ชจ๋‹ฌ */} + + + + + ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + {editingDynamicSourceColumnIndex !== null && localConfig.columns?.[editingDynamicSourceColumnIndex] && ( + + ({localConfig.columns[editingDynamicSourceColumnIndex].label}) + + )} + + + ์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ ์‹œ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค ์˜ต์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค + + + + {editingDynamicSourceColumnIndex !== null && localConfig.columns?.[editingDynamicSourceColumnIndex] && ( +
+ {/* ์˜ต์…˜ ๋ชฉ๋ก */} +
+ {(localConfig.columns[editingDynamicSourceColumnIndex].dynamicDataSource?.options || []).map((option, optIndex) => ( +
+
+
+ ์˜ต์…˜ {optIndex + 1} + {localConfig.columns![editingDynamicSourceColumnIndex].dynamicDataSource?.defaultOptionId === option.id && ( + ๊ธฐ๋ณธ + )} +
+
+ {localConfig.columns![editingDynamicSourceColumnIndex].dynamicDataSource?.defaultOptionId !== option.id && ( + + )} + +
+
+ + {/* ์˜ต์…˜ ๋ผ๋ฒจ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { label: e.target.value })} + placeholder="์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€" + className="h-8 text-xs" + /> +
+ + {/* ์†Œ์Šค ํƒ€์ž… */} +
+ + +
+ + {/* ํ…Œ์ด๋ธ” ์ง์ ‘ ์กฐํšŒ ์„ค์ • */} + {option.sourceType === "table" && ( +
+

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

+ + {/* ์ฐธ์กฐ ํ…Œ์ด๋ธ” */} +
+ + +
+ + {/* ๊ฐ’ ์ปฌ๋Ÿผ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + tableConfig: { ...option.tableConfig!, valueColumn: value }, + })} + /> +
+ + {/* ์กฐ์ธ ์กฐ๊ฑด */} +
+
+ + +
+ + {(option.tableConfig?.joinConditions || []).map((cond, condIndex) => ( +
+ + = + { + const newConditions = [...(option.tableConfig?.joinConditions || [])]; + newConditions[condIndex] = { ...newConditions[condIndex], targetField: value }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + tableConfig: { ...option.tableConfig!, joinConditions: newConditions }, + }); + }} + /> + +
+ ))} +
+
+ )} + + {/* ํ…Œ์ด๋ธ” ๋ณตํ•ฉ ์กฐ์ธ ์„ค์ • (2๊ฐœ ์ด์ƒ ํ…Œ์ด๋ธ”) */} + {option.sourceType === "multiTable" && ( +
+
+

๋ณตํ•ฉ ์กฐ์ธ ์„ค์ •

+

+ ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์กฐ์ธํ•ฉ๋‹ˆ๋‹ค +

+
+ + {/* ์กฐ์ธ ์ฒด์ธ */} +
+
+ + +
+ + {/* ์‹œ์ž‘์  ์•ˆ๋‚ด */} +
+

์‹œ์ž‘: ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ

+

+ ์ฒซ ๋ฒˆ์งธ ์กฐ์ธ์€ ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค +

+
+ + {/* ์กฐ์ธ ๋‹จ๊ณ„๋“ค */} + {(option.multiTableConfig?.joinChain || []).map((step, stepIndex) => ( +
+
+
+
+ {stepIndex + 1} +
+ ์กฐ์ธ ๋‹จ๊ณ„ {stepIndex + 1} +
+ +
+ + {/* ์กฐ์ธํ•  ํ…Œ์ด๋ธ” */} +
+ + +
+ + {/* ์กฐ์ธ ์กฐ๊ฑด */} +
+
+ + {stepIndex === 0 ? ( + + ) : ( + { + const newChain = [...(option.multiTableConfig?.joinChain || [])]; + newChain[stepIndex] = { + ...newChain[stepIndex], + joinCondition: { ...newChain[stepIndex].joinCondition, fromField: e.target.value } + }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, + }); + }} + placeholder={option.multiTableConfig?.joinChain[stepIndex - 1]?.outputField || "์ด์ „ ์ถœ๋ ฅ ํ•„๋“œ"} + className="h-8 text-xs" + /> + )} +
+ +
+ = +
+ +
+ + { + const newChain = [...(option.multiTableConfig?.joinChain || [])]; + newChain[stepIndex] = { + ...newChain[stepIndex], + joinCondition: { ...newChain[stepIndex].joinCondition, toField: value } + }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, + }); + }} + /> +
+
+ + {/* ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ „๋‹ฌํ•  ํ•„๋“œ */} +
+ + { + const newChain = [...(option.multiTableConfig?.joinChain || [])]; + newChain[stepIndex] = { ...newChain[stepIndex], outputField: value }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, + }); + }} + /> +

+ {stepIndex < (option.multiTableConfig?.joinChain.length || 0) - 1 + ? "๋‹ค์Œ ์กฐ์ธ ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉํ•  ํ•„๋“œ" + : "๋งˆ์ง€๋ง‰ ๋‹จ๊ณ„๋ฉด ๋น„์›Œ๋‘์„ธ์š”"} +

+
+ + {/* ์กฐ์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {step.tableName && step.joinCondition.fromField && step.joinCondition.toField && ( +
+ + {stepIndex === 0 ? "ํ˜„์žฌํ–‰" : option.multiTableConfig?.joinChain[stepIndex - 1]?.tableName} + + .{step.joinCondition.fromField} + = + {step.tableName} + .{step.joinCondition.toField} + {step.outputField && ( + + โ†’ {step.outputField} + + )} +
+ )} +
+ ))} + + {/* ์กฐ์ธ ์ฒด์ธ์ด ์—†์„ ๋•Œ ์•ˆ๋‚ด */} + {(!option.multiTableConfig?.joinChain || option.multiTableConfig.joinChain.length === 0) && ( +
+

+ ์กฐ์ธ ์ฒด์ธ์ด ์—†์Šต๋‹ˆ๋‹ค +

+

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

+
+ )} +
+ + {/* ์ตœ์ข… ๊ฐ’ ์ปฌ๋Ÿผ */} + {option.multiTableConfig?.joinChain && option.multiTableConfig.joinChain.length > 0 && ( +
+ + { + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, valueColumn: value }, + }); + }} + /> +

+ ๋งˆ์ง€๋ง‰ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ€์ ธ์˜ฌ ๊ฐ’ +

+
+ )} + + {/* ์ „์ฒด ์กฐ์ธ ๊ฒฝ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {option.multiTableConfig?.joinChain && option.multiTableConfig.joinChain.length > 0 && ( +
+

์กฐ์ธ ๊ฒฝ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

+
+ {option.multiTableConfig.joinChain.map((step, idx) => ( +
+ {idx === 0 && ( + <> + ํ˜„์žฌํ–‰ + .{step.joinCondition.fromField} + โ†’ + + )} + {step.tableName} + .{step.joinCondition.toField} + {step.outputField && idx < option.multiTableConfig!.joinChain.length - 1 && ( + <> + โ†’ + {step.outputField} + + )} +
+ ))} + {option.multiTableConfig.valueColumn && ( +
+ ์ตœ์ข… ๊ฐ’: + {option.multiTableConfig.joinChain[option.multiTableConfig.joinChain.length - 1]?.tableName}.{option.multiTableConfig.valueColumn} +
+ )} +
+
+ )} +
+ )} + + {/* API ํ˜ธ์ถœ ์„ค์ • */} + {option.sourceType === "api" && ( +
+

API ํ˜ธ์ถœ ์„ค์ •

+

+ ๋ณต์žกํ•œ ๋‹ค์ค‘ ์กฐ์ธ์€ ๋ฐฑ์—”๋“œ API๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค +

+ + {/* API ์—”๋“œํฌ์ธํŠธ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + apiConfig: { ...option.apiConfig!, endpoint: e.target.value }, + })} + placeholder="/api/price/customer" + className="h-8 text-xs font-mono" + /> +
+ + {/* HTTP ๋ฉ”์„œ๋“œ */} +
+ + +
+ + {/* ํŒŒ๋ผ๋ฏธํ„ฐ ๋งคํ•‘ */} +
+
+ + +
+ + {(option.apiConfig?.parameterMappings || []).map((mapping, mapIndex) => ( +
+ { + const newMappings = [...(option.apiConfig?.parameterMappings || [])]; + newMappings[mapIndex] = { ...newMappings[mapIndex], paramName: e.target.value }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + apiConfig: { ...option.apiConfig!, parameterMappings: newMappings }, + }); + }} + placeholder="ํŒŒ๋ผ๋ฏธํ„ฐ๋ช…" + className="h-7 text-[10px] flex-1" + /> + = + + +
+ ))} +
+ + {/* ์‘๋‹ต ๊ฐ’ ํ•„๋“œ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + apiConfig: { ...option.apiConfig!, responseValueField: e.target.value }, + })} + placeholder="price (๋˜๋Š” data.price)" + className="h-8 text-xs font-mono" + /> +

+ API ์‘๋‹ต์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ (์ค‘์ฒฉ ๊ฒฝ๋กœ ์ง€์›: data.price) +

+
+
+ )} +
+ ))} + + {/* ์˜ต์…˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + +
+ + {/* ์•ˆ๋‚ด */} +
+

์‚ฌ์šฉ ์˜ˆ์‹œ

+
    +
  • - ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€: customer_item_price ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ
  • +
  • - ํ’ˆ๋ชฉ๋ณ„ ๋‹จ๊ฐ€: item_info ํ…Œ์ด๋ธ”์—์„œ ๊ธฐ์ค€ ๋‹จ๊ฐ€ ์กฐํšŒ
  • +
  • - ๊ณ„์•ฝ ๋‹จ๊ฐ€: ์ „์šฉ API๋กœ ๋ณต์žกํ•œ ์กฐ์ธ ์ฒ˜๋ฆฌ
  • +
+
+
+ )} + + + + +
+
); } diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 703256b2..869884a7 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -4,7 +4,8 @@ import React, { useState, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; -import { Trash2 } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Trash2, ChevronDown, Check } from "lucide-react"; import { RepeaterColumnConfig } from "./types"; import { cn } from "@/lib/utils"; @@ -14,6 +15,9 @@ interface RepeaterTableProps { onDataChange: (newData: any[]) => void; onRowChange: (index: number, newRow: any) => void; onRowDelete: (index: number) => void; + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ๊ด€๋ จ + activeDataSources?: Record; // ์ปฌ๋Ÿผ๋ณ„ ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๋ฐ์ดํ„ฐ ์†Œ์Šค ID + onDataSourceChange?: (columnField: string, optionId: string) => void; // ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ } export function RepeaterTable({ @@ -22,11 +26,16 @@ export function RepeaterTable({ onDataChange, onRowChange, onRowDelete, + activeDataSources = {}, + onDataSourceChange, }: RepeaterTableProps) { const [editingCell, setEditingCell] = useState<{ rowIndex: number; field: string; } | null>(null); + + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค Popover ์—ด๋ฆผ ์ƒํƒœ + const [openPopover, setOpenPopover] = useState(null); // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€ (ํ•„์š”์‹œ ํ™œ์„ฑํ™”) // useEffect(() => { @@ -144,16 +153,79 @@ export function RepeaterTable({ # - {columns.map((col) => ( - - {col.label} - {col.required && *} - - ))} + {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; + + return ( + + {hasDynamicSource ? ( + setOpenPopover(open ? col.field : null)} + > + + + + +
+ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ +
+ {col.dynamicDataSource!.options.map((option) => ( + + ))} +
+
+ ) : ( + <> + {col.label} + {col.required && *} + + )} + + ); + })} ์‚ญ์ œ diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index c0cac4a9..028a892b 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -10,7 +10,7 @@ export interface ModalRepeaterTableProps { sourceColumnLabels?: Record; // ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ๋ผ๋ฒจ (์ปฌ๋Ÿผ๋ช… -> ํ‘œ์‹œ ๋ผ๋ฒจ) sourceSearchFields?: string[]; // ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋“ค - // ๐Ÿ†• ์ €์žฅ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ค์ • + // ์ €์žฅ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ค์ • targetTable?: string; // ์ €์žฅํ•  ํ…Œ์ด๋ธ” (์˜ˆ: "sales_order_mng") // ๋ชจ๋‹ฌ ์„ค์ • @@ -25,14 +25,14 @@ export interface ModalRepeaterTableProps { calculationRules?: CalculationRule[]; // ์ž๋™ ๊ณ„์‚ฐ ๊ทœ์น™ // ๋ฐ์ดํ„ฐ - value: any[]; // ํ˜„์žฌ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ๋“ค - onChange: (newData: any[]) => void; // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ + value: Record[]; // ํ˜„์žฌ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ๋“ค + onChange: (newData: Record[]) => void; // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ // ์ค‘๋ณต ์ฒดํฌ uniqueField?: string; // ์ค‘๋ณต ์ฒดํฌํ•  ํ•„๋“œ (์˜ˆ: "item_code") // ํ•„ํ„ฐ๋ง - filterCondition?: Record; + filterCondition?: Record; companyCode?: string; // ์Šคํƒ€์ผ @@ -47,11 +47,92 @@ export interface RepeaterColumnConfig { calculated?: boolean; // ๊ณ„์‚ฐ ํ•„๋“œ ์—ฌ๋ถ€ width?: string; // ์ปฌ๋Ÿผ ๋„ˆ๋น„ required?: boolean; // ํ•„์ˆ˜ ์ž…๋ ฅ ์—ฌ๋ถ€ - defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ + defaultValue?: string | number | boolean; // ๊ธฐ๋ณธ๊ฐ’ selectOptions?: { value: string; label: string }[]; // select์ผ ๋•Œ ์˜ต์…˜ - - // ๐Ÿ†• ์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ • + + // ์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ • mapping?: ColumnMapping; // ์ด ์ปฌ๋Ÿผ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋””์„œ ๊ฐ€์ ธ์˜ฌ์ง€ ์„ค์ • + + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค (์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ์œผ๋กœ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ „ํ™˜) + dynamicDataSource?: DynamicDataSourceConfig; +} + +/** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + * ์ปฌ๋Ÿผ ํ—ค๋”๋ฅผ ํด๋ฆญํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ + * ์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€, ํ’ˆ๋ชฉ๋ณ„ ๋‹จ๊ฐ€, ๊ธฐ์ค€ ๋‹จ๊ฐ€ ๋“ฑ์„ ์„ ํƒ + */ +export interface DynamicDataSourceConfig { + enabled: boolean; + options: DynamicDataSourceOption[]; + defaultOptionId?: string; // ๊ธฐ๋ณธ ์„ ํƒ ์˜ต์…˜ ID +} + +/** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์˜ต์…˜ + * ๊ฐ ์˜ต์…˜์€ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”/API์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ•์„ ์ •์˜ + */ +export interface DynamicDataSourceOption { + id: string; + label: string; // ํ‘œ์‹œ ๋ผ๋ฒจ (์˜ˆ: "๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€") + + // ์กฐํšŒ ๋ฐฉ์‹ + sourceType: "table" | "multiTable" | "api"; + + // ํ…Œ์ด๋ธ” ์ง์ ‘ ์กฐํšŒ (๋‹จ์ˆœ ์กฐ์ธ - 1๊ฐœ ํ…Œ์ด๋ธ”) + tableConfig?: { + tableName: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”๋ช… + valueColumn: string; // ๊ฐ€์ ธ์˜ฌ ๊ฐ’ ์ปฌ๋Ÿผ + joinConditions: { + sourceField: string; // ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ + targetField: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ•„๋“œ + }[]; + }; + + // ํ…Œ์ด๋ธ” ๋ณตํ•ฉ ์กฐ์ธ (2๊ฐœ ์ด์ƒ ํ…Œ์ด๋ธ” ์กฐ์ธ) + multiTableConfig?: { + // ์กฐ์ธ ์ฒด์ธ ์ •์˜ (์ˆœ์„œ๋Œ€๋กœ ์กฐ์ธ) + joinChain: MultiTableJoinStep[]; + // ์ตœ์ข…์ ์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ๊ฐ’ ์ปฌ๋Ÿผ (๋งˆ์ง€๋ง‰ ํ…Œ์ด๋ธ”์—์„œ) + valueColumn: string; + }; + + // ์ „์šฉ API ํ˜ธ์ถœ (๋ณต์žกํ•œ ๋‹ค์ค‘ ์กฐ์ธ) + apiConfig?: { + endpoint: string; // API ์—”๋“œํฌ์ธํŠธ (์˜ˆ: "/api/price/customer") + method?: "GET" | "POST"; // HTTP ๋ฉ”์„œ๋“œ (๊ธฐ๋ณธ: GET) + parameterMappings: { + paramName: string; // API ํŒŒ๋ผ๋ฏธํ„ฐ๋ช… + sourceField: string; // ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ + }[]; + responseValueField: string; // ์‘๋‹ต์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ + }; +} + +/** + * ๋ณตํ•ฉ ์กฐ์ธ ๋‹จ๊ณ„ ์ •์˜ + * ์˜ˆ: item_info.item_number โ†’ customer_item.item_code โ†’ customer_item.id โ†’ customer_item_price.customer_item_id + */ +export interface MultiTableJoinStep { + // ์กฐ์ธํ•  ํ…Œ์ด๋ธ” + tableName: string; + // ์กฐ์ธ ์กฐ๊ฑด + joinCondition: { + // ์ด์ „ ๋‹จ๊ณ„์˜ ํ•„๋“œ (์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ) + fromField: string; + // ์ด ํ…Œ์ด๋ธ”์˜ ํ•„๋“œ + toField: string; + }; + // ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ „๋‹ฌํ•  ํ•„๋“œ (๋‹ค์Œ ์กฐ์ธ์— ์‚ฌ์šฉ) + outputField?: string; + // ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด (์„ ํƒ์‚ฌํ•ญ) + additionalFilters?: { + field: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<="; + value: string | number | boolean; + // ๊ฐ’์ด ํ˜„์žฌ ํ–‰์—์„œ ์˜ค๋Š” ๊ฒฝ์šฐ + valueFromField?: string; + }[]; } /** @@ -61,16 +142,16 @@ export interface RepeaterColumnConfig { export interface ColumnMapping { /** ๋งคํ•‘ ํƒ€์ž… */ type: "source" | "reference" | "manual"; - + /** ๋งคํ•‘ ํƒ€์ž…๋ณ„ ์„ค์ • */ // type: "source" - ์†Œ์Šค ํ…Œ์ด๋ธ” (๋ชจ๋‹ฌ์—์„œ ์„ ํƒํ•œ ํ•ญ๋ชฉ)์˜ ์ปฌ๋Ÿผ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ sourceField?: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช… (์˜ˆ: "item_name") - + // type: "reference" - ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์ฐธ์กฐ (์กฐ์ธ) referenceTable?: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: "customer_item_mapping") referenceField?: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ (์˜ˆ: "basic_price") joinCondition?: JoinCondition[]; // ์กฐ์ธ ์กฐ๊ฑด - + // type: "manual" - ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ž…๋ ฅ } @@ -101,11 +182,10 @@ export interface ItemSelectionModalProps { sourceColumns: string[]; sourceSearchFields?: string[]; multiSelect?: boolean; - filterCondition?: Record; + filterCondition?: Record; modalTitle: string; - alreadySelected: any[]; // ์ด๋ฏธ ์„ ํƒ๋œ ํ•ญ๋ชฉ๋“ค (์ค‘๋ณต ๋ฐฉ์ง€์šฉ) + alreadySelected: Record[]; // ์ด๋ฏธ ์„ ํƒ๋œ ํ•ญ๋ชฉ๋“ค (์ค‘๋ณต ๋ฐฉ์ง€์šฉ) uniqueField?: string; - onSelect: (items: any[]) => void; + onSelect: (items: Record[]) => void; columnLabels?: Record; // ์ปฌ๋Ÿผ๋ช… -> ๋ผ๋ฒจ๋ช… ๋งคํ•‘ } - diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 11b3aa43..3fa8f623 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -1027,7 +1027,7 @@ export function UniversalFormModalComponent({ }} disabled={isDisabled} > - + @@ -1409,47 +1409,37 @@ export function UniversalFormModalComponent({ {/* ์„น์…˜๋“ค */}
{config.sections.map((section) => renderSection(section))}
- {/* ๋ฒ„ํŠผ ์˜์—ญ */} -
- {config.modal.showResetButton && ( + {/* ๋ฒ„ํŠผ ์˜์—ญ - ์ €์žฅ ๋ฒ„ํŠผ์ด ํ‘œ์‹œ๋  ๋•Œ๋งŒ ๋ Œ๋”๋ง */} + {config.modal.showSaveButton !== false && ( +
+ {config.modal.showResetButton && ( + + )} - )} - - -
+
+ )} {/* ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ */} - + diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index e503ebfb..3ce7477a 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -402,21 +402,26 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor -
- - updateModalConfig({ saveButtonText: e.target.value })} - className="h-7 text-xs mt-1" - /> -
-
- - updateModalConfig({ cancelButtonText: e.target.value })} - className="h-7 text-xs mt-1" - /> +
+
+ ์ €์žฅ ๋ฒ„ํŠผ ํ‘œ์‹œ + updateModalConfig({ showSaveButton: checked })} + /> +
+ ButtonPrimary ์ปดํฌ๋„ŒํŠธ๋กœ ์ €์žฅ ๋ฒ„ํŠผ์„ ๋ณ„๋„ ๊ตฌ์„ฑํ•  ๊ฒฝ์šฐ ๋„์„ธ์š” + + {config.modal.showSaveButton !== false && ( +
+ + updateModalConfig({ saveButtonText: e.target.value })} + className="h-7 text-xs mt-1" + /> +
+ )}
@@ -1896,7 +1901,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
ํ…Œ์ด๋ธ” ์ฐธ์กฐ: DB ํ…Œ์ด๋ธ”์—์„œ ์˜ต์…˜ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
- + - ์˜ˆ: dept_info (๋ถ€์„œ ํ…Œ์ด๋ธ”) + ๋“œ๋กญ๋‹ค์šด ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ฌ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”
- + @@ -1933,13 +1938,17 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor }, }) } - placeholder="dept_code" - className="h-6 text-[10px] mt-1" + placeholder="customer_code" + className="h-7 text-xs mt-1" /> - ์„ ํƒ ์‹œ ์‹ค์ œ ์ €์žฅ๋˜๋Š” ๊ฐ’ (์˜ˆ: D001) + + ์ฐธ์กฐ ํ…Œ์ด๋ธ”์—์„œ ์กฐ์ธํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š” +
+ ์˜ˆ: customer_code, customer_id +
- + @@ -1950,10 +1959,14 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor }, }) } - placeholder="dept_name" - className="h-6 text-[10px] mt-1" + placeholder="customer_name" + className="h-7 text-xs mt-1" /> - ๋“œ๋กญ๋‹ค์šด์— ๋ณด์—ฌ์งˆ ํ…์ŠคํŠธ (์˜ˆ: ์˜์—…๋ถ€) + + ๋“œ๋กญ๋‹ค์šด์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š” +
+ ์˜ˆ: customer_name, company_name +
)} diff --git a/frontend/lib/registry/components/universal-form-modal/config.ts b/frontend/lib/registry/components/universal-form-modal/config.ts index 5383512b..66644576 100644 --- a/frontend/lib/registry/components/universal-form-modal/config.ts +++ b/frontend/lib/registry/components/universal-form-modal/config.ts @@ -12,6 +12,7 @@ export const defaultConfig: UniversalFormModalConfig = { size: "lg", closeOnOutsideClick: false, showCloseButton: true, + showSaveButton: true, saveButtonText: "์ €์žฅ", cancelButtonText: "์ทจ์†Œ", showResetButton: false,