엑셀 업로드 제어로직 설정 가능하도록 수정
This commit is contained in:
@@ -200,7 +200,7 @@ router.post(
|
|||||||
authenticateToken,
|
authenticateToken,
|
||||||
async (req: AuthenticatedRequest, res) => {
|
async (req: AuthenticatedRequest, res) => {
|
||||||
try {
|
try {
|
||||||
const { screenId, detailData, masterFieldValues, numberingRuleId } = req.body;
|
const { screenId, detailData, masterFieldValues, numberingRuleId, afterUploadFlowId, afterUploadFlows } = req.body;
|
||||||
const companyCode = req.user?.companyCode || "*";
|
const companyCode = req.user?.companyCode || "*";
|
||||||
const userId = req.user?.userId || "system";
|
const userId = req.user?.userId || "system";
|
||||||
|
|
||||||
@@ -214,6 +214,7 @@ router.post(
|
|||||||
console.log(`📤 마스터-디테일 간단 모드 업로드: screenId=${screenId}, rows=${detailData.length}`);
|
console.log(`📤 마스터-디테일 간단 모드 업로드: screenId=${screenId}, rows=${detailData.length}`);
|
||||||
console.log(` 마스터 필드 값:`, masterFieldValues);
|
console.log(` 마스터 필드 값:`, masterFieldValues);
|
||||||
console.log(` 채번 규칙 ID:`, numberingRuleId);
|
console.log(` 채번 규칙 ID:`, numberingRuleId);
|
||||||
|
console.log(` 업로드 후 제어:`, afterUploadFlows?.length > 0 ? `${afterUploadFlows.length}개` : afterUploadFlowId || "없음");
|
||||||
|
|
||||||
// 업로드 실행
|
// 업로드 실행
|
||||||
const result = await masterDetailExcelService.uploadSimple(
|
const result = await masterDetailExcelService.uploadSimple(
|
||||||
@@ -222,7 +223,9 @@ router.post(
|
|||||||
masterFieldValues || {},
|
masterFieldValues || {},
|
||||||
numberingRuleId,
|
numberingRuleId,
|
||||||
companyCode,
|
companyCode,
|
||||||
userId
|
userId,
|
||||||
|
afterUploadFlowId, // 업로드 후 제어 실행 (단일, 하위 호환성)
|
||||||
|
afterUploadFlows // 업로드 후 제어 실행 (다중)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`✅ 마스터-디테일 간단 모드 업로드 완료:`, {
|
console.log(`✅ 마스터-디테일 간단 모드 업로드 완료:`, {
|
||||||
|
|||||||
@@ -639,6 +639,8 @@ class MasterDetailExcelService {
|
|||||||
* @param numberingRuleId 채번 규칙 ID (optional)
|
* @param numberingRuleId 채번 규칙 ID (optional)
|
||||||
* @param companyCode 회사 코드
|
* @param companyCode 회사 코드
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
|
* @param afterUploadFlowId 업로드 후 실행할 노드 플로우 ID (optional, 하위 호환성)
|
||||||
|
* @param afterUploadFlows 업로드 후 실행할 노드 플로우 배열 (optional)
|
||||||
*/
|
*/
|
||||||
async uploadSimple(
|
async uploadSimple(
|
||||||
screenId: number,
|
screenId: number,
|
||||||
@@ -646,15 +648,25 @@ class MasterDetailExcelService {
|
|||||||
masterFieldValues: Record<string, any>,
|
masterFieldValues: Record<string, any>,
|
||||||
numberingRuleId: string | undefined,
|
numberingRuleId: string | undefined,
|
||||||
companyCode: string,
|
companyCode: string,
|
||||||
userId: string
|
userId: string,
|
||||||
|
afterUploadFlowId?: string,
|
||||||
|
afterUploadFlows?: Array<{ flowId: string; order: number }>
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
masterInserted: number;
|
masterInserted: number;
|
||||||
detailInserted: number;
|
detailInserted: number;
|
||||||
generatedKey: string;
|
generatedKey: string;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
controlResult?: any;
|
||||||
}> {
|
}> {
|
||||||
const result = {
|
const result: {
|
||||||
|
success: boolean;
|
||||||
|
masterInserted: number;
|
||||||
|
detailInserted: number;
|
||||||
|
generatedKey: string;
|
||||||
|
errors: string[];
|
||||||
|
controlResult?: any;
|
||||||
|
} = {
|
||||||
success: false,
|
success: false,
|
||||||
masterInserted: 0,
|
masterInserted: 0,
|
||||||
detailInserted: 0,
|
detailInserted: 0,
|
||||||
@@ -756,6 +768,68 @@ class MasterDetailExcelService {
|
|||||||
errors: result.errors.length,
|
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) {
|
} catch (error: any) {
|
||||||
await client.query("ROLLBACK");
|
await client.query("ROLLBACK");
|
||||||
result.errors.push(`트랜잭션 실패: ${error.message}`);
|
result.errors.push(`트랜잭션 실패: ${error.message}`);
|
||||||
|
|||||||
@@ -648,7 +648,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||||||
screenId,
|
screenId,
|
||||||
filteredData,
|
filteredData,
|
||||||
masterFieldValues,
|
masterFieldValues,
|
||||||
masterDetailExcelConfig?.numberingRuleId || undefined
|
masterDetailExcelConfig?.numberingRuleId || undefined,
|
||||||
|
masterDetailExcelConfig?.afterUploadFlowId || undefined, // 하위 호환성
|
||||||
|
masterDetailExcelConfig?.afterUploadFlows || undefined // 다중 제어
|
||||||
);
|
);
|
||||||
|
|
||||||
if (uploadResult.success && uploadResult.data) {
|
if (uploadResult.success && uploadResult.data) {
|
||||||
|
|||||||
@@ -3281,10 +3281,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 제어 기능 섹션 */}
|
{/* 제어 기능 섹션 - 엑셀 업로드가 아닐 때만 표시 */}
|
||||||
|
{(component.componentConfig?.action?.type || "save") !== "excel_upload" && (
|
||||||
<div className="border-border mt-8 border-t pt-6">
|
<div className="border-border mt-8 border-t pt-6">
|
||||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 🆕 플로우 단계별 표시 제어 섹션 (플로우 위젯이 있을 때만 표시) */}
|
{/* 🆕 플로우 단계별 표시 제어 섹션 (플로우 위젯이 있을 때만 표시) */}
|
||||||
{hasFlowWidget && (
|
{hasFlowWidget && (
|
||||||
@@ -3724,12 +3726,189 @@ const MasterDetailExcelUploadConfig: React.FC<{
|
|||||||
<p className="text-muted-foreground text-xs">참조 테이블에서 사용자에게 표시할 컬럼을 선택하세요.</p>
|
<p className="text-muted-foreground text-xs">참조 테이블에서 사용자에게 표시할 컬럼을 선택하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 업로드 후 제어 실행 설정 */}
|
||||||
|
<AfterUploadControlConfig
|
||||||
|
config={config}
|
||||||
|
onUpdateProperty={onUpdateProperty}
|
||||||
|
masterDetailConfig={masterDetailConfig}
|
||||||
|
updateMasterDetailConfig={updateMasterDetailConfig}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 detailData 디테일 데이터 배열
|
||||||
* @param masterFieldValues UI에서 선택한 마스터 필드 값
|
* @param masterFieldValues UI에서 선택한 마스터 필드 값
|
||||||
* @param numberingRuleId 채번 규칙 ID (optional)
|
* @param numberingRuleId 채번 규칙 ID (optional)
|
||||||
|
* @param afterUploadFlowId 업로드 후 실행할 제어 ID (optional, 하위 호환성)
|
||||||
|
* @param afterUploadFlows 업로드 후 실행할 제어 목록 (optional)
|
||||||
* @returns 업로드 결과
|
* @returns 업로드 결과
|
||||||
*/
|
*/
|
||||||
static async uploadMasterDetailSimple(
|
static async uploadMasterDetailSimple(
|
||||||
screenId: number,
|
screenId: number,
|
||||||
detailData: Record<string, any>[],
|
detailData: Record<string, any>[],
|
||||||
masterFieldValues: Record<string, any>,
|
masterFieldValues: Record<string, any>,
|
||||||
numberingRuleId?: string
|
numberingRuleId?: string,
|
||||||
|
afterUploadFlowId?: string,
|
||||||
|
afterUploadFlows?: Array<{ flowId: string; order: number }>
|
||||||
): Promise<ApiResponse<MasterDetailSimpleUploadResult>> {
|
): Promise<ApiResponse<MasterDetailSimpleUploadResult>> {
|
||||||
try {
|
try {
|
||||||
console.log("📤 마스터-디테일 간단 모드 업로드:", {
|
console.log("📤 마스터-디테일 간단 모드 업로드:", {
|
||||||
@@ -679,6 +683,7 @@ export class DynamicFormApi {
|
|||||||
detailRowCount: detailData.length,
|
detailRowCount: detailData.length,
|
||||||
masterFieldValues,
|
masterFieldValues,
|
||||||
numberingRuleId,
|
numberingRuleId,
|
||||||
|
afterUploadFlows: afterUploadFlows?.length || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await apiClient.post(`/data/master-detail/upload-simple`, {
|
const response = await apiClient.post(`/data/master-detail/upload-simple`, {
|
||||||
@@ -686,6 +691,8 @@ export class DynamicFormApi {
|
|||||||
detailData,
|
detailData,
|
||||||
masterFieldValues,
|
masterFieldValues,
|
||||||
numberingRuleId,
|
numberingRuleId,
|
||||||
|
afterUploadFlowId,
|
||||||
|
afterUploadFlows,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user