엑셀 업로드 제어로직 설정 가능하도록 수정
This commit is contained in:
parent
aa0698556e
commit
ba2a281245
|
|
@ -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(`✅ 마스터-디테일 간단 모드 업로드 완료:`, {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 엑셀 업로드 설정 섹션 컴포넌트
|
||||
* 마스터-디테일 설정은 분할 패널 자동 감지
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue