엑셀 업로드 제어로직 설정 가능하도록 수정

This commit is contained in:
kjs
2026-01-09 15:46:09 +09:00
parent aa0698556e
commit ba2a281245
5 changed files with 275 additions and 10 deletions

View File

@@ -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(`✅ 마스터-디테일 간단 모드 업로드 완료:`, {

View File

@@ -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<string, any>,
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}`);

View File

@@ -648,7 +648,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
screenId,
filteredData,
masterFieldValues,
masterDetailExcelConfig?.numberingRuleId || undefined
masterDetailExcelConfig?.numberingRuleId || undefined,
masterDetailExcelConfig?.afterUploadFlowId || undefined, // 하위 호환성
masterDetailExcelConfig?.afterUploadFlows || undefined // 다중 제어
);
if (uploadResult.success && uploadResult.data) {

View File

@@ -3281,10 +3281,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
)}
</div>
{/* 제어 기능 섹션 */}
<div className="border-border mt-8 border-t pt-6">
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
</div>
{/* 제어 기능 섹션 - 엑셀 업로드가 아닐 때만 표시 */}
{(component.componentConfig?.action?.type || "save") !== "excel_upload" && (
<div className="border-border mt-8 border-t pt-6">
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
</div>
)}
{/* 🆕 플로우 단계별 표시 제어 섹션 (플로우 위젯이 있을 때만 표시) */}
{hasFlowWidget && (
@@ -3724,12 +3726,189 @@ const MasterDetailExcelUploadConfig: React.FC<{
<p className="text-muted-foreground text-xs"> .</p>
</div>
)}
{/* 업로드 후 제어 실행 설정 */}
<AfterUploadControlConfig
config={config}
onUpdateProperty={onUpdateProperty}
masterDetailConfig={masterDetailConfig}
updateMasterDetailConfig={updateMasterDetailConfig}
/>
</div>
)}
</div>
);
};
/**
* 업로드 후 제어 실행 설정 컴포넌트
* 여러 개의 제어를 순서대로 실행할 수 있도록 지원
*/
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 (
<div className="border-t pt-3">
<Label className="text-xs"> </Label>
<p className="text-muted-foreground mb-2 text-xs">
.
</p>
{/* 선택된 제어 목록 */}
{selectedFlows.length > 0 && (
<div className="mb-2 space-y-1">
{selectedFlows.map((selected, index) => {
const flow = nodeFlows.find((f) => String(f.flowId) === selected.flowId);
return (
<div key={selected.flowId} className="flex items-center gap-1 rounded border bg-white p-1.5">
<span className="text-muted-foreground w-5 text-center text-xs">{index + 1}</span>
<span className="flex-1 truncate text-xs">{flow?.flowName || `Flow ${selected.flowId}`}</span>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => moveUp(index)}
disabled={index === 0}
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => moveDown(index)}
disabled={index === selectedFlows.length - 1}
>
<ChevronDown className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-5 w-5 p-0 text-red-500" onClick={() => removeFlow(selected.flowId)}>
<X className="h-3 w-3" />
</Button>
</div>
);
})}
</div>
)}
{/* 제어 추가 버튼 */}
<Popover open={flowSelectOpen} onOpenChange={setFlowSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-8 w-full justify-between text-xs"
disabled={isLoading || availableFlows.length === 0}
>
{isLoading ? "로딩 중..." : availableFlows.length === 0 ? "추가 가능한 제어 없음" : "제어 추가..."}
<Plus className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="제어 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
{availableFlows.map((flow) => (
<CommandItem
key={flow.flowId}
value={flow.flowName}
onSelect={() => addFlow(String(flow.flowId))}
className="text-xs"
>
<div className="flex flex-col">
<span>{flow.flowName}</span>
{flow.flowDescription && (
<span className="text-muted-foreground text-[10px]">{flow.flowDescription}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{selectedFlows.length > 0 && (
<p className="text-muted-foreground mt-1 text-xs">
{selectedFlows.length} .
</p>
)}
</div>
);
};
/**
* 엑셀 업로드 설정 섹션 컴포넌트
* 마스터-디테일 설정은 분할 패널 자동 감지

View File

@@ -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<string, any>[],
masterFieldValues: Record<string, any>,
numberingRuleId?: string
numberingRuleId?: string,
afterUploadFlowId?: string,
afterUploadFlows?: Array<{ flowId: string; order: number }>
): Promise<ApiResponse<MasterDetailSimpleUploadResult>> {
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 {