From 3677c77da01224b531289df20d1e7414f9906b34 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 9 Jan 2026 17:56:48 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8B=A8=EC=9D=BC=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EC=97=91=EC=85=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=B1=84?= =?UTF-8?q?=EB=B2=88=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/ExcelUploadModal.tsx | 65 ++- .../config-panels/ButtonConfigPanel.tsx | 468 ++++++++++++------ frontend/lib/utils/buttonActions.ts | 14 +- 3 files changed, 402 insertions(+), 145 deletions(-) diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 6785eac8..6eda1594 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -84,6 +84,11 @@ export interface ExcelUploadModalProps { }; // πŸ†• λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ—‘μ…€ μ—…λ‘œλ“œ μ„€μ • masterDetailExcelConfig?: MasterDetailExcelConfig; + // πŸ†• 단일 ν…Œμ΄λΈ” μ±„λ²ˆ μ„€μ • + numberingRuleId?: string; + numberingTargetColumn?: string; + // πŸ†• μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ„€μ • + afterUploadFlows?: Array<{ flowId: string; order: number }>; } interface ColumnMapping { @@ -103,6 +108,11 @@ export const ExcelUploadModal: React.FC = ({ isMasterDetail = false, masterDetailRelation, masterDetailExcelConfig, + // 단일 ν…Œμ΄λΈ” μ±„λ²ˆ μ„€μ • + numberingRuleId, + numberingTargetColumn, + // μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ„€μ • + afterUploadFlows, }) => { const [currentStep, setCurrentStep] = useState(1); @@ -695,13 +705,48 @@ export const ExcelUploadModal: React.FC = ({ } } else { // κΈ°μ‘΄ 단일 ν…Œμ΄λΈ” μ—…λ‘œλ“œ 둜직 + console.log("πŸ“Š 단일 ν…Œμ΄λΈ” μ—…λ‘œλ“œ μ‹œμž‘:", { + tableName, + uploadMode, + numberingRuleId, + numberingTargetColumn, + dataCount: filteredData.length, + }); + let successCount = 0; let failCount = 0; + // πŸ†• 단일 ν…Œμ΄λΈ” μ±„λ²ˆ μ„€μ • 확인 + const hasNumbering = numberingRuleId && numberingTargetColumn; + console.log("πŸ“Š μ±„λ²ˆ μ„€μ •:", { hasNumbering, numberingRuleId, numberingTargetColumn }); + for (const row of filteredData) { try { + let dataToSave = { ...row }; + + // πŸ†• μ±„λ²ˆ 적용: 각 ν–‰λ§ˆλ‹€ μ±„λ²ˆ API 호좜 + if (hasNumbering && uploadMode === "insert") { + try { + const { apiClient } = await import("@/lib/api/client"); + console.log(`πŸ“Š μ±„λ²ˆ API 호좜: /numbering-rules/${numberingRuleId}/allocate`); + const numberingResponse = await apiClient.post(`/numbering-rules/${numberingRuleId}/allocate`); + console.log(`πŸ“Š μ±„λ²ˆ API 응닡:`, numberingResponse.data); + // 응닡 ꡬ쑰: { success: true, data: { generatedCode: "..." } } + const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code; + if (numberingResponse.data?.success && generatedCode) { + dataToSave[numberingTargetColumn] = generatedCode; + console.log(`βœ… μ±„λ²ˆ 적용: ${numberingTargetColumn} = ${generatedCode}`); + } else { + console.warn(`⚠️ μ±„λ²ˆ μ‹€νŒ¨: 응닡에 μ½”λ“œ μ—†μŒ`, numberingResponse.data); + } + } catch (numError) { + console.error("μ±„λ²ˆ 였λ₯˜:", numError); + // μ±„λ²ˆ μ‹€νŒ¨ μ‹œμ—λ„ 계속 μ§„ν–‰ (μ±„λ²ˆ 컬럼만 λΉ„μ›Œλ‘ ) + } + } + if (uploadMode === "insert") { - const formData = { screenId: 0, tableName, data: row }; + const formData = { screenId: 0, tableName, data: dataToSave }; const result = await DynamicFormApi.saveFormData(formData); if (result.success) { successCount++; @@ -714,6 +759,24 @@ export const ExcelUploadModal: React.FC = ({ } } + // πŸ†• μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ + if (afterUploadFlows && afterUploadFlows.length > 0 && successCount > 0) { + console.log("πŸ”„ μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰:", afterUploadFlows); + try { + const { apiClient } = await import("@/lib/api/client"); + // μˆœμ„œλŒ€λ‘œ μ‹€ν–‰ + const sortedFlows = [...afterUploadFlows].sort((a, b) => a.order - b.order); + for (const flow of sortedFlows) { + await apiClient.post(`/dataflow/node-flows/${flow.flowId}/execute`, { + sourceData: { tableName, uploadedCount: successCount }, + }); + console.log(`βœ… μ œμ–΄ μ‹€ν–‰ μ™„λ£Œ: flowId=${flow.flowId}`); + } + } catch (controlError) { + console.error("μ œμ–΄ μ‹€ν–‰ 였λ₯˜:", controlError); + } + } + if (successCount > 0) { toast.success( `${successCount}개 행이 μ—…λ‘œλ“œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.${failCount > 0 ? ` (μ‹€νŒ¨: ${failCount}개)` : ""}` diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 8d8c4df9..eabeb59f 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -5,7 +5,6 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; -import { Checkbox } from "@/components/ui/checkbox"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Button } from "@/components/ui/button"; @@ -2026,7 +2025,12 @@ export const ButtonConfigPanel: React.FC = ({ {/* μ—‘μ…€ μ—…λ‘œλ“œ μ•‘μ…˜ μ„€μ • */} {(component.componentConfig?.action?.type || "save") === "excel_upload" && ( - + )} {/* λ°”μ½”λ“œ μŠ€μΊ” μ•‘μ…˜ μ„€μ • */} @@ -3311,7 +3315,6 @@ const MasterDetailExcelUploadConfig: React.FC<{ onUpdateProperty: (path: string, value: any) => void; allComponents: ComponentData[]; }> = ({ config, onUpdateProperty, allComponents }) => { - const [numberingRules, setNumberingRules] = useState([]); const [relationInfo, setRelationInfo] = useState<{ masterTable: string; detailTable: string; @@ -3319,7 +3322,6 @@ const MasterDetailExcelUploadConfig: React.FC<{ detailFkColumn: string; } | null>(null); const [loading, setLoading] = useState(false); - const [numberingRuleOpen, setNumberingRuleOpen] = useState(false); const [masterColumns, setMasterColumns] = useState< Array<{ columnName: string; @@ -3357,22 +3359,6 @@ const MasterDetailExcelUploadConfig: React.FC<{ const masterTable = splitPanelInfo?.leftPanel?.tableName || ""; const detailTable = splitPanelInfo?.rightPanel?.tableName || ""; - // μ±„λ²ˆ κ·œμΉ™ λ‘œλ“œ - useEffect(() => { - const loadNumberingRules = async () => { - try { - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get("/numbering-rules"); - if (response.data?.success && response.data?.data) { - setNumberingRules(response.data.data); - } - } catch (error) { - console.error("μ±„λ²ˆ κ·œμΉ™ λ‘œλ“œ μ‹€νŒ¨:", error); - } - }; - loadNumberingRules(); - }, []); - // λ§ˆμŠ€ν„° ν…Œμ΄λΈ” 컬럼 λ‘œλ“œ useEffect(() => { if (!masterTable) { @@ -3547,86 +3533,12 @@ const MasterDetailExcelUploadConfig: React.FC<{ )} - {/* μ±„λ²ˆ κ·œμΉ™ 선택 - μœ μΌν•˜κ²Œ μ‚¬μš©μžκ°€ μ„€μ •ν•˜λŠ” ν•­λͺ© */} + {/* λ§ˆμŠ€ν„° ν‚€ μžλ™ 생성 μ•ˆλ‚΄ */} {relationInfo && ( -
- - - - - - - - - - 검색 κ²°κ³Ό μ—†μŒ - - { - updateMasterDetailConfig({ numberingRuleId: undefined }); - setNumberingRuleOpen(false); - }} - className="text-xs" - > - - μ±„λ²ˆ μ—†μŒ (μˆ˜λ™ μž…λ ₯) - - {numberingRules - .filter((rule) => rule.table_name === masterTable || !rule.table_name) - .map((rule, idx) => { - const ruleId = String(rule.rule_id || rule.ruleId || `rule-${idx}`); - const ruleName = rule.rule_name || rule.ruleName || "(이름 μ—†μŒ)"; - return ( - { - updateMasterDetailConfig({ numberingRuleId: ruleId }); - setNumberingRuleOpen(false); - }} - className="text-xs" - > - - {ruleName} - - ); - })} - - - - - -

- λ§ˆμŠ€ν„° ν…Œμ΄λΈ”μ˜ {relationInfo.masterKeyColumn} 값을 μžλ™ μƒμ„±ν•©λ‹ˆλ‹€ -

-
+

+ λ§ˆμŠ€ν„° ν…Œμ΄λΈ”μ˜ {relationInfo.masterKeyColumn} 값은 μœ„μ—μ„œ μ„€μ •ν•œ μ±„λ²ˆ κ·œμΉ™μœΌλ‘œ μžλ™ + μƒμ„±λ©λ‹ˆλ‹€. +

)} {/* λ§ˆμŠ€ν„° ν•„λ“œ 선택 - μ‚¬μš©μžκ°€ μ—‘μ…€ μ—…λ‘œλ“œ μ‹œ μž…λ ₯ν•  ν•„λ“œ */} @@ -3726,14 +3638,6 @@ const MasterDetailExcelUploadConfig: React.FC<{

μ°Έμ‘° ν…Œμ΄λΈ”μ—μ„œ μ‚¬μš©μžμ—κ²Œ ν‘œμ‹œν•  μ»¬λŸΌμ„ μ„ νƒν•˜μ„Έμš”.

)} - - {/* μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ„€μ • */} - )} @@ -3741,23 +3645,181 @@ const MasterDetailExcelUploadConfig: React.FC<{ }; /** - * μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ„€μ • μ»΄ν¬λ„ŒνŠΈ - * μ—¬λŸ¬ 개의 μ œμ–΄λ₯Ό μˆœμ„œλŒ€λ‘œ μ‹€ν–‰ν•  수 μžˆλ„λ‘ 지원 + * μ—‘μ…€ μ—…λ‘œλ“œ μ±„λ²ˆ κ·œμΉ™ μ„€μ • (단일 ν…Œμ΄λΈ”/λ§ˆμŠ€ν„°-λ””ν…ŒμΌ λͺ¨λ‘ μ‚¬μš© κ°€λŠ₯) */ -const AfterUploadControlConfig: React.FC<{ - config: any; - onUpdateProperty: (path: string, value: any) => void; - masterDetailConfig: any; - updateMasterDetailConfig: (updates: any) => void; -}> = ({ masterDetailConfig, updateMasterDetailConfig }) => { - const [nodeFlows, setNodeFlows] = useState< - Array<{ flowId: number; flowName: string; flowDescription?: string }> - >([]); +const ExcelNumberingRuleConfig: React.FC<{ + config: { numberingRuleId?: string; numberingTargetColumn?: string }; + updateConfig: (updates: { numberingRuleId?: string; numberingTargetColumn?: string }) => void; + tableName?: string; // 단일 ν…Œμ΄λΈ”μΈ 경우 ν…Œμ΄λΈ”λͺ… + hasSplitPanel?: boolean; // λΆ„ν•  νŒ¨λ„ μ—¬λΆ€ (λ§ˆμŠ€ν„°-λ””ν…ŒμΌ) +}> = ({ config, updateConfig, tableName, hasSplitPanel }) => { + const [numberingRules, setNumberingRules] = useState([]); + const [ruleSelectOpen, setRuleSelectOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [tableColumns, setTableColumns] = useState>([]); + const [columnsLoading, setColumnsLoading] = useState(false); + + // μ±„λ²ˆ κ·œμΉ™ λͺ©λ‘ λ‘œλ“œ + useEffect(() => { + const loadNumberingRules = async () => { + setIsLoading(true); + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get("/numbering-rules"); + if (response.data?.success && response.data?.data) { + setNumberingRules(response.data.data); + } + } catch (error) { + console.error("μ±„λ²ˆ κ·œμΉ™ λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:", error); + } finally { + setIsLoading(false); + } + }; + + loadNumberingRules(); + }, []); + + // 단일 ν…Œμ΄λΈ”μΈ 경우 컬럼 λͺ©λ‘ λ‘œλ“œ + useEffect(() => { + if (!tableName || hasSplitPanel) { + setTableColumns([]); + return; + } + + const loadColumns = async () => { + setColumnsLoading(true); + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); + if (response.data?.success && response.data?.data?.columns) { + const cols = response.data.data.columns.map((col: any) => ({ + columnName: col.columnName || col.column_name, + columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, + })); + setTableColumns(cols); + } + } catch (error) { + console.error("컬럼 λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:", error); + } finally { + setColumnsLoading(false); + } + }; + + loadColumns(); + }, [tableName, hasSplitPanel]); + + const selectedRule = numberingRules.find((r) => String(r.rule_id || r.ruleId) === String(config.numberingRuleId)); + + return ( +
+ +

+ μ—…λ‘œλ“œ μ‹œ μžλ™μœΌλ‘œ 생성할 μ½”λ“œ/번호의 μ±„λ²ˆ κ·œμΉ™μ„ μ„ νƒν•˜μ„Έμš”. +

+ + + + + + + + + + 검색 κ²°κ³Ό μ—†μŒ + + { + updateConfig({ numberingRuleId: undefined, numberingTargetColumn: undefined }); + setRuleSelectOpen(false); + }} + className="text-xs" + > + + μ±„λ²ˆ μ—†μŒ + + {numberingRules.map((rule, idx) => { + const ruleId = String(rule.rule_id || rule.ruleId || `rule-${idx}`); + const ruleName = rule.rule_name || rule.ruleName || "(이름 μ—†μŒ)"; + return ( + { + updateConfig({ numberingRuleId: ruleId }); + setRuleSelectOpen(false); + }} + className="text-xs" + > + + {ruleName} + + ); + })} + + + + + + + {/* 단일 ν…Œμ΄λΈ”μ΄κ³  μ±„λ²ˆ κ·œμΉ™μ΄ μ„ νƒλœ 경우, μ μš©ν•  컬럼 선택 */} + {config.numberingRuleId && !hasSplitPanel && tableName && ( +
+ + +

μ±„λ²ˆ 값이 μž…λ ₯될 μ»¬λŸΌμ„ μ„ νƒν•˜μ„Έμš”.

+
+ )} + + {/* λΆ„ν•  νŒ¨λ„μΈ 경우 μ•ˆλ‚΄ λ©”μ‹œμ§€ */} + {config.numberingRuleId && hasSplitPanel && ( +

λ§ˆμŠ€ν„°-λ””ν…ŒμΌ κ΅¬μ‘°μ—μ„œλŠ” λ§ˆμŠ€ν„° ν‚€ μ»¬λŸΌμ— μžλ™ μ μš©λ©λ‹ˆλ‹€.

+ )} +
+ ); +}; + +/** + * μ—‘μ…€ μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ„€μ • (단일 ν…Œμ΄λΈ”/λ§ˆμŠ€ν„°-λ””ν…ŒμΌ λͺ¨λ‘ μ‚¬μš© κ°€λŠ₯) + */ +const ExcelAfterUploadControlConfig: React.FC<{ + config: { afterUploadFlows?: Array<{ flowId: string; order: number }> }; + updateConfig: (updates: { afterUploadFlows?: Array<{ flowId: string; order: number }> }) => void; +}> = ({ config, updateConfig }) => { + const [nodeFlows, setNodeFlows] = useState>([]); const [flowSelectOpen, setFlowSelectOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); - // μ„ νƒλœ μ œμ–΄ λͺ©λ‘ (λ°°μ—΄λ‘œ 관리) - const selectedFlows: Array<{ flowId: string; order: number }> = masterDetailConfig.afterUploadFlows || []; + const selectedFlows = config.afterUploadFlows || []; // λ…Έλ“œ ν”Œλ‘œμš° λͺ©λ‘ λ‘œλ“œ useEffect(() => { @@ -3779,53 +3841,39 @@ const AfterUploadControlConfig: React.FC<{ loadNodeFlows(); }, []); - // μ œμ–΄ μΆ”κ°€ const addFlow = (flowId: string) => { if (selectedFlows.some((f) => f.flowId === flowId)) return; const newFlows = [...selectedFlows, { flowId, order: selectedFlows.length + 1 }]; - updateMasterDetailConfig({ afterUploadFlows: newFlows }); + updateConfig({ afterUploadFlows: newFlows }); setFlowSelectOpen(false); }; - // μ œμ–΄ 제거 const removeFlow = (flowId: string) => { - const newFlows = selectedFlows - .filter((f) => f.flowId !== flowId) - .map((f, idx) => ({ ...f, order: idx + 1 })); - updateMasterDetailConfig({ afterUploadFlows: newFlows }); + const newFlows = selectedFlows.filter((f) => f.flowId !== flowId).map((f, idx) => ({ ...f, order: idx + 1 })); + updateConfig({ afterUploadFlows: newFlows }); }; - // μˆœμ„œ λ³€κ²½ (μœ„λ‘œ) const moveUp = (index: number) => { if (index === 0) return; const newFlows = [...selectedFlows]; [newFlows[index - 1], newFlows[index]] = [newFlows[index], newFlows[index - 1]]; - updateMasterDetailConfig({ - afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })), - }); + updateConfig({ afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })) }); }; - // μˆœμ„œ λ³€κ²½ (μ•„λž˜λ‘œ) const moveDown = (index: number) => { if (index === selectedFlows.length - 1) return; const newFlows = [...selectedFlows]; [newFlows[index], newFlows[index + 1]] = [newFlows[index + 1], newFlows[index]]; - updateMasterDetailConfig({ - afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })), - }); + updateConfig({ afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })) }); }; - // μ„ νƒλ˜μ§€ μ•Šμ€ ν”Œλ‘œμš°λ§Œ 필터링 const availableFlows = nodeFlows.filter((f) => !selectedFlows.some((s) => s.flowId === String(f.flowId))); return (
-

- μ—‘μ…€ μ—…λ‘œλ“œ μ™„λ£Œ ν›„ μˆœμ„œλŒ€λ‘œ μ‹€ν–‰ν•  μ œμ–΄λ₯Ό μΆ”κ°€ν•˜μ„Έμš”. -

+

μ—‘μ…€ μ—…λ‘œλ“œ μ™„λ£Œ ν›„ μˆœμ„œλŒ€λ‘œ μ‹€ν–‰ν•  μ œμ–΄λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

- {/* μ„ νƒλœ μ œμ–΄ λͺ©λ‘ */} {selectedFlows.length > 0 && (
{selectedFlows.map((selected, index) => { @@ -3852,7 +3900,12 @@ const AfterUploadControlConfig: React.FC<{ > -
@@ -3861,7 +3914,6 @@ const AfterUploadControlConfig: React.FC<{
)} - {/* μ œμ–΄ μΆ”κ°€ λ²„νŠΌ */}