단일테이블 엑셀 업로드 채번 구현

This commit is contained in:
kjs 2026-01-09 17:56:48 +09:00
parent 85ae1c1521
commit 3677c77da0
3 changed files with 402 additions and 145 deletions

View File

@ -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<ExcelUploadModalProps> = ({
isMasterDetail = false,
masterDetailRelation,
masterDetailExcelConfig,
// 단일 테이블 채번 설정
numberingRuleId,
numberingTargetColumn,
// 업로드 후 제어 실행 설정
afterUploadFlows,
}) => {
const [currentStep, setCurrentStep] = useState(1);
@ -695,13 +705,48 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
}
} 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<ExcelUploadModalProps> = ({
}
}
// 🆕 업로드 후 제어 실행
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}개)` : ""}`

View File

@ -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<ButtonConfigPanelProps> = ({
{/* 엑셀 업로드 액션 설정 */}
{(component.componentConfig?.action?.type || "save") === "excel_upload" && (
<ExcelUploadConfigSection config={config} onUpdateProperty={onUpdateProperty} allComponents={allComponents} />
<ExcelUploadConfigSection
config={config}
onUpdateProperty={onUpdateProperty}
allComponents={allComponents}
currentTableName={currentTableName}
/>
)}
{/* 바코드 스캔 액션 설정 */}
@ -3311,7 +3315,6 @@ const MasterDetailExcelUploadConfig: React.FC<{
onUpdateProperty: (path: string, value: any) => void;
allComponents: ComponentData[];
}> = ({ config, onUpdateProperty, allComponents }) => {
const [numberingRules, setNumberingRules] = useState<any[]>([]);
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<{
)}
</div>
{/* 채번 규칙 선택 - 유일하게 사용자가 설정하는 항목 */}
{/* 마스터 키 자동 생성 안내 */}
{relationInfo && (
<div>
<Label className="text-xs"> </Label>
<Popover open={numberingRuleOpen} onOpenChange={setNumberingRuleOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={numberingRuleOpen}
className="h-8 w-full justify-between text-xs"
>
{masterDetailConfig.numberingRuleId
? numberingRules.find(
(rule) => String(rule.rule_id || rule.ruleId) === String(masterDetailConfig.numberingRuleId),
)?.rule_name ||
numberingRules.find(
(rule) => String(rule.rule_id || rule.ruleId) === String(masterDetailConfig.numberingRuleId),
)?.ruleName ||
"선택됨"
: "채번 없음 (수동 입력)"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</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>
<CommandItem
value="__none__"
onSelect={() => {
updateMasterDetailConfig({ numberingRuleId: undefined });
setNumberingRuleOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
!masterDetailConfig.numberingRuleId ? "opacity-100" : "opacity-0",
)}
/>
( )
</CommandItem>
{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 (
<CommandItem
key={ruleId}
value={ruleName}
onSelect={() => {
updateMasterDetailConfig({ numberingRuleId: ruleId });
setNumberingRuleOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
String(masterDetailConfig.numberingRuleId) === ruleId ? "opacity-100" : "opacity-0",
)}
/>
{ruleName}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="text-muted-foreground mt-1 text-xs">
<strong>{relationInfo.masterKeyColumn}</strong>
</p>
</div>
<p className="text-muted-foreground border-t pt-2 text-xs">
<strong>{relationInfo.masterKeyColumn}</strong>
.
</p>
)}
{/* 마스터 필드 선택 - 사용자가 엑셀 업로드 시 입력할 필드 */}
@ -3726,14 +3638,6 @@ const MasterDetailExcelUploadConfig: React.FC<{
<p className="text-muted-foreground text-xs"> .</p>
</div>
)}
{/* 업로드 후 제어 실행 설정 */}
<AfterUploadControlConfig
config={config}
onUpdateProperty={onUpdateProperty}
masterDetailConfig={masterDetailConfig}
updateMasterDetailConfig={updateMasterDetailConfig}
/>
</div>
)}
</div>
@ -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<any[]>([]);
const [ruleSelectOpen, setRuleSelectOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [tableColumns, setTableColumns] = useState<Array<{ columnName: string; columnLabel: string }>>([]);
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 (
<div className="border-t pt-3">
<Label className="text-xs"> </Label>
<p className="text-muted-foreground mb-2 text-xs">
/ .
</p>
<Popover open={ruleSelectOpen} onOpenChange={setRuleSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={ruleSelectOpen}
className="h-8 w-full justify-between text-xs"
disabled={isLoading}
>
{isLoading ? "로딩 중..." : selectedRule?.rule_name || selectedRule?.ruleName || "채번 없음"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</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>
<CommandItem
value="__none__"
onSelect={() => {
updateConfig({ numberingRuleId: undefined, numberingTargetColumn: undefined });
setRuleSelectOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-4 w-4", !config.numberingRuleId ? "opacity-100" : "opacity-0")} />
</CommandItem>
{numberingRules.map((rule, idx) => {
const ruleId = String(rule.rule_id || rule.ruleId || `rule-${idx}`);
const ruleName = rule.rule_name || rule.ruleName || "(이름 없음)";
return (
<CommandItem
key={ruleId}
value={ruleName}
onSelect={() => {
updateConfig({ numberingRuleId: ruleId });
setRuleSelectOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
String(config.numberingRuleId) === ruleId ? "opacity-100" : "opacity-0",
)}
/>
{ruleName}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* 단일 테이블이고 채번 규칙이 선택된 경우, 적용할 컬럼 선택 */}
{config.numberingRuleId && !hasSplitPanel && tableName && (
<div className="mt-2">
<Label className="text-xs"> </Label>
<Select
value={config.numberingTargetColumn || ""}
onValueChange={(value) => updateConfig({ numberingTargetColumn: value || undefined })}
disabled={columnsLoading}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={columnsLoading ? "로딩 중..." : "컬럼 선택"} />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel} ({col.columnName})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-xs"> .</p>
</div>
)}
{/* 분할 패널인 경우 안내 메시지 */}
{config.numberingRuleId && hasSplitPanel && (
<p className="text-muted-foreground mt-1 text-xs">- .</p>
)}
</div>
);
};
/**
* ( /- )
*/
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<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 || [];
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 (
<div className="border-t pt-3">
<Label className="text-xs"> </Label>
<p className="text-muted-foreground mb-2 text-xs">
.
</p>
<p className="text-muted-foreground mb-2 text-xs"> .</p>
{/* 선택된 제어 목록 */}
{selectedFlows.length > 0 && (
<div className="mb-2 space-y-1">
{selectedFlows.map((selected, index) => {
@ -3852,7 +3900,12 @@ const AfterUploadControlConfig: React.FC<{
>
<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)}>
<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>
@ -3861,7 +3914,6 @@ const AfterUploadControlConfig: React.FC<{
</div>
)}
{/* 제어 추가 버튼 */}
<Popover open={flowSelectOpen} onOpenChange={setFlowSelectOpen}>
<PopoverTrigger asChild>
<Button
@ -3917,7 +3969,126 @@ const ExcelUploadConfigSection: React.FC<{
config: any;
onUpdateProperty: (path: string, value: any) => void;
allComponents: ComponentData[];
}> = ({ config, onUpdateProperty, allComponents }) => {
currentTableName?: string; // 현재 화면의 테이블명 (ButtonConfigPanel에서 전달)
}> = ({ config, onUpdateProperty, allComponents, currentTableName: propTableName }) => {
// 엑셀 업로드 설정 상태 관리
const [excelUploadConfig, setExcelUploadConfig] = useState<{
numberingRuleId?: string;
numberingTargetColumn?: string;
afterUploadFlows?: Array<{ flowId: string; order: number }>;
}>({
numberingRuleId: config.action?.excelNumberingRuleId,
numberingTargetColumn: config.action?.excelNumberingTargetColumn,
afterUploadFlows: config.action?.excelAfterUploadFlows || [],
});
// 분할 패널 감지
const splitPanelInfo = useMemo(() => {
const findSplitPanel = (components: any[]): any => {
for (const comp of components) {
const compId = comp.componentId || comp.componentType;
if (compId === "split-panel-layout") {
return comp.componentConfig;
}
if (comp.children && comp.children.length > 0) {
const found = findSplitPanel(comp.children);
if (found) return found;
}
}
return null;
};
return findSplitPanel(allComponents as any[]);
}, [allComponents]);
const hasSplitPanel = !!splitPanelInfo;
// 단일 테이블 감지 (props 우선, 없으면 컴포넌트에서 탐색)
const singleTableName = useMemo(() => {
if (hasSplitPanel) return undefined;
// props로 전달된 테이블명 우선 사용
if (propTableName) return propTableName;
// 컴포넌트에서 테이블명 탐색
const findTableName = (components: any[]): string | undefined => {
for (const comp of components) {
const compId = comp.componentId || comp.componentType;
const compConfig = comp.componentConfig || comp.config || comp;
// 테이블 패널이나 데이터 테이블에서 테이블명 찾기
if (
compId === "table-panel" ||
compId === "data-table" ||
compId === "table-list" ||
compId === "simple-table"
) {
const tableName = compConfig?.tableName || compConfig?.table;
if (tableName) return tableName;
}
// 폼 컴포넌트에서 테이블명 찾기
if (compId === "form-panel" || compId === "input-form" || compId === "form" || compId === "detail-form") {
const tableName = compConfig?.tableName || compConfig?.table;
if (tableName) return tableName;
}
// 범용적으로 tableName 속성이 있는 컴포넌트 찾기
if (compConfig?.tableName) {
return compConfig.tableName;
}
if (comp.children && comp.children.length > 0) {
const found = findTableName(comp.children);
if (found) return found;
}
}
return undefined;
};
return findTableName(allComponents as any[]);
}, [allComponents, hasSplitPanel, propTableName]);
// 디버깅: 감지된 테이블명 로그
useEffect(() => {
console.log(
"[ExcelUploadConfigSection] 분할 패널:",
hasSplitPanel,
"단일 테이블:",
singleTableName,
"(props:",
propTableName,
")",
);
}, [hasSplitPanel, singleTableName, propTableName]);
// 설정 업데이트 함수
const updateExcelUploadConfig = (updates: Partial<typeof excelUploadConfig>) => {
const newConfig = { ...excelUploadConfig, ...updates };
setExcelUploadConfig(newConfig);
if (updates.numberingRuleId !== undefined) {
onUpdateProperty("componentConfig.action.excelNumberingRuleId", updates.numberingRuleId);
}
if (updates.numberingTargetColumn !== undefined) {
onUpdateProperty("componentConfig.action.excelNumberingTargetColumn", updates.numberingTargetColumn);
}
if (updates.afterUploadFlows !== undefined) {
onUpdateProperty("componentConfig.action.excelAfterUploadFlows", updates.afterUploadFlows);
}
};
// config 변경 시 로컬 상태 동기화
useEffect(() => {
setExcelUploadConfig({
numberingRuleId: config.action?.excelNumberingRuleId,
numberingTargetColumn: config.action?.excelNumberingTargetColumn,
afterUploadFlows: config.action?.excelAfterUploadFlows || [],
});
}, [
config.action?.excelNumberingRuleId,
config.action?.excelNumberingTargetColumn,
config.action?.excelAfterUploadFlows,
]);
return (
<div className="bg-muted/50 mt-4 space-y-4 rounded-lg border p-4">
<h4 className="text-foreground text-sm font-medium"> </h4>
@ -3955,6 +4126,17 @@ const ExcelUploadConfigSection: React.FC<{
</div>
)}
{/* 채번 규칙 설정 (항상 표시) */}
<ExcelNumberingRuleConfig
config={excelUploadConfig}
updateConfig={updateExcelUploadConfig}
tableName={singleTableName}
hasSplitPanel={hasSplitPanel}
/>
{/* 업로드 후 제어 실행 (항상 표시) */}
<ExcelAfterUploadControlConfig config={excelUploadConfig} updateConfig={updateExcelUploadConfig} />
{/* 마스터-디테일 설정 (분할 패널 자동 감지) */}
<MasterDetailExcelUploadConfig
config={config}

View File

@ -88,6 +88,9 @@ export interface ButtonActionConfig {
// 엑셀 업로드 관련
excelUploadMode?: "insert" | "update" | "upsert"; // 업로드 모드
excelKeyColumn?: string; // 업데이트/Upsert 시 키 컬럼
excelNumberingRuleId?: string; // 채번 규칙 ID (단일 테이블용)
excelNumberingTargetColumn?: string; // 채번 적용 컬럼 (단일 테이블용)
excelAfterUploadFlows?: Array<{ flowId: string; order: number }>; // 업로드 후 제어 실행
// 바코드 스캔 관련
barcodeTargetField?: string; // 스캔 결과를 입력할 필드명
@ -4838,6 +4841,10 @@ export class ButtonActionExecutor {
userId: context.userId,
tableName: context.tableName,
screenId: context.screenId,
// 채번 설정 디버깅
numberingRuleId: config.excelNumberingRuleId,
numberingTargetColumn: config.excelNumberingTargetColumn,
afterUploadFlows: config.excelAfterUploadFlows,
});
// 🆕 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지)
@ -4909,7 +4916,7 @@ export class ButtonActionExecutor {
savedSize: localStorage.getItem(storageKey),
});
root.render(
root.render(
React.createElement(ExcelUploadModal, {
open: true,
onOpenChange: (open: boolean) => {
@ -4931,6 +4938,11 @@ export class ButtonActionExecutor {
isMasterDetail,
masterDetailRelation,
masterDetailExcelConfig,
// 🆕 단일 테이블 채번 설정
numberingRuleId: config.excelNumberingRuleId,
numberingTargetColumn: config.excelNumberingTargetColumn,
// 🆕 업로드 후 제어 실행 설정
afterUploadFlows: config.excelAfterUploadFlows,
onSuccess: () => {
// 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
context.onRefresh?.();