diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index 5e4cdbaf..574f1cf8 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -200,7 +200,7 @@ router.post( authenticateToken, async (req: AuthenticatedRequest, res) => { try { - const { screenId, detailData, masterFieldValues, numberingRuleId } = req.body; + const { screenId, detailData, masterFieldValues, numberingRuleId, afterUploadFlowId, afterUploadFlows } = req.body; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; @@ -214,6 +214,7 @@ router.post( console.log(`πŸ“€ λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 간단 λͺ¨λ“œ μ—…λ‘œλ“œ: screenId=${screenId}, rows=${detailData.length}`); console.log(` λ§ˆμŠ€ν„° ν•„λ“œ κ°’:`, masterFieldValues); console.log(` μ±„λ²ˆ κ·œμΉ™ ID:`, numberingRuleId); + console.log(` μ—…λ‘œλ“œ ν›„ μ œμ–΄:`, afterUploadFlows?.length > 0 ? `${afterUploadFlows.length}개` : afterUploadFlowId || "μ—†μŒ"); // μ—…λ‘œλ“œ μ‹€ν–‰ const result = await masterDetailExcelService.uploadSimple( @@ -222,7 +223,9 @@ router.post( masterFieldValues || {}, numberingRuleId, companyCode, - userId + userId, + afterUploadFlowId, // μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ (단일, ν•˜μœ„ ν˜Έν™˜μ„±) + afterUploadFlows // μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ (닀쀑) ); console.log(`βœ… λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 간단 λͺ¨λ“œ μ—…λ‘œλ“œ μ™„λ£Œ:`, { diff --git a/backend-node/src/services/masterDetailExcelService.ts b/backend-node/src/services/masterDetailExcelService.ts index 6a267765..4b1a7218 100644 --- a/backend-node/src/services/masterDetailExcelService.ts +++ b/backend-node/src/services/masterDetailExcelService.ts @@ -639,6 +639,8 @@ class MasterDetailExcelService { * @param numberingRuleId μ±„λ²ˆ κ·œμΉ™ ID (optional) * @param companyCode νšŒμ‚¬ μ½”λ“œ * @param userId μ‚¬μš©μž ID + * @param afterUploadFlowId μ—…λ‘œλ“œ ν›„ μ‹€ν–‰ν•  λ…Έλ“œ ν”Œλ‘œμš° ID (optional, ν•˜μœ„ ν˜Έν™˜μ„±) + * @param afterUploadFlows μ—…λ‘œλ“œ ν›„ μ‹€ν–‰ν•  λ…Έλ“œ ν”Œλ‘œμš° λ°°μ—΄ (optional) */ async uploadSimple( screenId: number, @@ -646,15 +648,25 @@ class MasterDetailExcelService { masterFieldValues: Record, numberingRuleId: string | undefined, companyCode: string, - userId: string + userId: string, + afterUploadFlowId?: string, + afterUploadFlows?: Array<{ flowId: string; order: number }> ): Promise<{ success: boolean; masterInserted: number; detailInserted: number; generatedKey: string; errors: string[]; + controlResult?: any; }> { - const result = { + const result: { + success: boolean; + masterInserted: number; + detailInserted: number; + generatedKey: string; + errors: string[]; + controlResult?: any; + } = { success: false, masterInserted: 0, detailInserted: 0, @@ -756,6 +768,68 @@ class MasterDetailExcelService { errors: result.errors.length, }); + // μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ (단일 λ˜λŠ” 닀쀑) + const flowsToExecute = afterUploadFlows && afterUploadFlows.length > 0 + ? afterUploadFlows // 닀쀑 μ œμ–΄ + : afterUploadFlowId + ? [{ flowId: afterUploadFlowId, order: 1 }] // 단일 (ν•˜μœ„ ν˜Έν™˜μ„±) + : []; + + if (flowsToExecute.length > 0 && result.success) { + try { + const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService"); + + // λ§ˆμŠ€ν„° 데이터λ₯Ό μ œμ–΄μ— 전달 + const masterData = { + ...masterFieldValues, + [relation!.masterKeyColumn]: result.generatedKey, + company_code: companyCode, + }; + + const controlResults: any[] = []; + + // μˆœμ„œλŒ€λ‘œ μ œμ–΄ μ‹€ν–‰ + for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) { + logger.info(`μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰: flowId=${flow.flowId}, order=${flow.order}`); + + const controlResult = await NodeFlowExecutionService.executeFlow( + parseInt(flow.flowId), + { + sourceData: [masterData], + dataSourceType: "formData", + buttonId: "excel-upload-button", + screenId: screenId, + userId: userId, + companyCode: companyCode, + formData: masterData, + } + ); + + controlResults.push({ + flowId: flow.flowId, + order: flow.order, + success: controlResult.success, + message: controlResult.message, + executedNodes: controlResult.nodes?.length || 0, + }); + } + + result.controlResult = { + success: controlResults.every(r => r.success), + executedFlows: controlResults.length, + results: controlResults, + }; + + logger.info(`μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ™„λ£Œ: ${controlResults.length}개 μ‹€ν–‰`, result.controlResult); + } catch (controlError: any) { + logger.error(`μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ μ‹€νŒ¨:`, controlError); + result.controlResult = { + success: false, + message: `μ œμ–΄ μ‹€ν–‰ μ‹€νŒ¨: ${controlError.message}`, + }; + } + } + } catch (error: any) { await client.query("ROLLBACK"); result.errors.push(`νŠΈλžœμž­μ…˜ μ‹€νŒ¨: ${error.message}`); diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index f4b0056e..6785eac8 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -648,7 +648,9 @@ export const ExcelUploadModal: React.FC = ({ screenId, filteredData, masterFieldValues, - masterDetailExcelConfig?.numberingRuleId || undefined + masterDetailExcelConfig?.numberingRuleId || undefined, + masterDetailExcelConfig?.afterUploadFlowId || undefined, // ν•˜μœ„ ν˜Έν™˜μ„± + masterDetailExcelConfig?.afterUploadFlows || undefined // 닀쀑 μ œμ–΄ ); if (uploadResult.success && uploadResult.data) { diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 30bf70e1..8d8c4df9 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -3281,10 +3281,12 @@ export const ButtonConfigPanel: React.FC = ({ )} - {/* μ œμ–΄ κΈ°λŠ₯ μ„Ήμ…˜ */} -
- -
+ {/* μ œμ–΄ κΈ°λŠ₯ μ„Ήμ…˜ - μ—‘μ…€ μ—…λ‘œλ“œκ°€ 아닐 λ•Œλ§Œ ν‘œμ‹œ */} + {(component.componentConfig?.action?.type || "save") !== "excel_upload" && ( +
+ +
+ )} {/* πŸ†• ν”Œλ‘œμš° 단계별 ν‘œμ‹œ μ œμ–΄ μ„Ήμ…˜ (ν”Œλ‘œμš° μœ„μ ―μ΄ μžˆμ„ λ•Œλ§Œ ν‘œμ‹œ) */} {hasFlowWidget && ( @@ -3724,12 +3726,189 @@ 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 [flowSelectOpen, setFlowSelectOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // μ„ νƒλœ μ œμ–΄ λͺ©λ‘ (λ°°μ—΄λ‘œ 관리) + const selectedFlows: Array<{ flowId: string; order: number }> = masterDetailConfig.afterUploadFlows || []; + + // λ…Έλ“œ ν”Œλ‘œμš° λͺ©λ‘ λ‘œλ“œ + useEffect(() => { + const loadNodeFlows = async () => { + setIsLoading(true); + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get("/dataflow/node-flows"); + if (response.data?.success && response.data?.data) { + setNodeFlows(response.data.data); + } + } catch (error) { + console.error("λ…Έλ“œ ν”Œλ‘œμš° λͺ©λ‘ λ‘œλ“œ μ‹€νŒ¨:", error); + } finally { + setIsLoading(false); + } + }; + + loadNodeFlows(); + }, []); + + // μ œμ–΄ μΆ”κ°€ + const addFlow = (flowId: string) => { + if (selectedFlows.some((f) => f.flowId === flowId)) return; + const newFlows = [...selectedFlows, { flowId, order: selectedFlows.length + 1 }]; + updateMasterDetailConfig({ 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 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 })), + }); + }; + + // μˆœμ„œ λ³€κ²½ (μ•„λž˜λ‘œ) + 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 })), + }); + }; + + // μ„ νƒλ˜μ§€ μ•Šμ€ ν”Œλ‘œμš°λ§Œ 필터링 + const availableFlows = nodeFlows.filter((f) => !selectedFlows.some((s) => s.flowId === String(f.flowId))); + + return ( +
+ +

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

+ + {/* μ„ νƒλœ μ œμ–΄ λͺ©λ‘ */} + {selectedFlows.length > 0 && ( +
+ {selectedFlows.map((selected, index) => { + const flow = nodeFlows.find((f) => String(f.flowId) === selected.flowId); + return ( +
+ {index + 1} + {flow?.flowName || `Flow ${selected.flowId}`} + + + +
+ ); + })} +
+ )} + + {/* μ œμ–΄ μΆ”κ°€ λ²„νŠΌ */} + + + + + + + + + 검색 κ²°κ³Ό μ—†μŒ + + {availableFlows.map((flow) => ( + addFlow(String(flow.flowId))} + className="text-xs" + > +
+ {flow.flowName} + {flow.flowDescription && ( + {flow.flowDescription} + )} +
+
+ ))} +
+
+
+
+
+ + {selectedFlows.length > 0 && ( +

+ μ—…λ‘œλ“œ μ™„λ£Œ ν›„ μœ„ μˆœμ„œλŒ€λ‘œ {selectedFlows.length}개의 μ œμ–΄κ°€ μ‹€ν–‰λ©λ‹ˆλ‹€. +

+ )} +
+ ); +}; + /** * μ—‘μ…€ μ—…λ‘œλ“œ μ„€μ • μ„Ήμ…˜ μ»΄ν¬λ„ŒνŠΈ * λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 섀정은 λΆ„ν•  νŒ¨λ„ μžλ™ 감지 diff --git a/frontend/lib/api/dynamicForm.ts b/frontend/lib/api/dynamicForm.ts index 8e975de4..c9e4cf33 100644 --- a/frontend/lib/api/dynamicForm.ts +++ b/frontend/lib/api/dynamicForm.ts @@ -665,13 +665,17 @@ export class DynamicFormApi { * @param detailData λ””ν…ŒμΌ 데이터 λ°°μ—΄ * @param masterFieldValues UIμ—μ„œ μ„ νƒν•œ λ§ˆμŠ€ν„° ν•„λ“œ κ°’ * @param numberingRuleId μ±„λ²ˆ κ·œμΉ™ ID (optional) + * @param afterUploadFlowId μ—…λ‘œλ“œ ν›„ μ‹€ν–‰ν•  μ œμ–΄ ID (optional, ν•˜μœ„ ν˜Έν™˜μ„±) + * @param afterUploadFlows μ—…λ‘œλ“œ ν›„ μ‹€ν–‰ν•  μ œμ–΄ λͺ©λ‘ (optional) * @returns μ—…λ‘œλ“œ κ²°κ³Ό */ static async uploadMasterDetailSimple( screenId: number, detailData: Record[], masterFieldValues: Record, - numberingRuleId?: string + numberingRuleId?: string, + afterUploadFlowId?: string, + afterUploadFlows?: Array<{ flowId: string; order: number }> ): Promise> { try { console.log("πŸ“€ λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 간단 λͺ¨λ“œ μ—…λ‘œλ“œ:", { @@ -679,6 +683,7 @@ export class DynamicFormApi { detailRowCount: detailData.length, masterFieldValues, numberingRuleId, + afterUploadFlows: afterUploadFlows?.length || 0, }); const response = await apiClient.post(`/data/master-detail/upload-simple`, { @@ -686,6 +691,8 @@ export class DynamicFormApi { detailData, masterFieldValues, numberingRuleId, + afterUploadFlowId, + afterUploadFlows, }); return {