feature/screen-management #350
|
|
@ -84,6 +84,11 @@ export interface ExcelUploadModalProps {
|
||||||
};
|
};
|
||||||
// 🆕 마스터-디테일 엑셀 업로드 설정
|
// 🆕 마스터-디테일 엑셀 업로드 설정
|
||||||
masterDetailExcelConfig?: MasterDetailExcelConfig;
|
masterDetailExcelConfig?: MasterDetailExcelConfig;
|
||||||
|
// 🆕 단일 테이블 채번 설정
|
||||||
|
numberingRuleId?: string;
|
||||||
|
numberingTargetColumn?: string;
|
||||||
|
// 🆕 업로드 후 제어 실행 설정
|
||||||
|
afterUploadFlows?: Array<{ flowId: string; order: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColumnMapping {
|
interface ColumnMapping {
|
||||||
|
|
@ -103,6 +108,11 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
isMasterDetail = false,
|
isMasterDetail = false,
|
||||||
masterDetailRelation,
|
masterDetailRelation,
|
||||||
masterDetailExcelConfig,
|
masterDetailExcelConfig,
|
||||||
|
// 단일 테이블 채번 설정
|
||||||
|
numberingRuleId,
|
||||||
|
numberingTargetColumn,
|
||||||
|
// 업로드 후 제어 실행 설정
|
||||||
|
afterUploadFlows,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
|
||||||
|
|
@ -695,13 +705,48 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 기존 단일 테이블 업로드 로직
|
// 기존 단일 테이블 업로드 로직
|
||||||
|
console.log("📊 단일 테이블 업로드 시작:", {
|
||||||
|
tableName,
|
||||||
|
uploadMode,
|
||||||
|
numberingRuleId,
|
||||||
|
numberingTargetColumn,
|
||||||
|
dataCount: filteredData.length,
|
||||||
|
});
|
||||||
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
|
|
||||||
|
// 🆕 단일 테이블 채번 설정 확인
|
||||||
|
const hasNumbering = numberingRuleId && numberingTargetColumn;
|
||||||
|
console.log("📊 채번 설정:", { hasNumbering, numberingRuleId, numberingTargetColumn });
|
||||||
|
|
||||||
for (const row of filteredData) {
|
for (const row of filteredData) {
|
||||||
try {
|
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") {
|
if (uploadMode === "insert") {
|
||||||
const formData = { screenId: 0, tableName, data: row };
|
const formData = { screenId: 0, tableName, data: dataToSave };
|
||||||
const result = await DynamicFormApi.saveFormData(formData);
|
const result = await DynamicFormApi.saveFormData(formData);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
successCount++;
|
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) {
|
if (successCount > 0) {
|
||||||
toast.success(
|
toast.success(
|
||||||
`${successCount}개 행이 업로드되었습니다.${failCount > 0 ? ` (실패: ${failCount}개)` : ""}`
|
`${successCount}개 행이 업로드되었습니다.${failCount > 0 ? ` (실패: ${failCount}개)` : ""}`
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -2026,7 +2025,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
|
|
||||||
{/* 엑셀 업로드 액션 설정 */}
|
{/* 엑셀 업로드 액션 설정 */}
|
||||||
{(component.componentConfig?.action?.type || "save") === "excel_upload" && (
|
{(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;
|
onUpdateProperty: (path: string, value: any) => void;
|
||||||
allComponents: ComponentData[];
|
allComponents: ComponentData[];
|
||||||
}> = ({ config, onUpdateProperty, allComponents }) => {
|
}> = ({ config, onUpdateProperty, allComponents }) => {
|
||||||
const [numberingRules, setNumberingRules] = useState<any[]>([]);
|
|
||||||
const [relationInfo, setRelationInfo] = useState<{
|
const [relationInfo, setRelationInfo] = useState<{
|
||||||
masterTable: string;
|
masterTable: string;
|
||||||
detailTable: string;
|
detailTable: string;
|
||||||
|
|
@ -3319,7 +3322,6 @@ const MasterDetailExcelUploadConfig: React.FC<{
|
||||||
detailFkColumn: string;
|
detailFkColumn: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [numberingRuleOpen, setNumberingRuleOpen] = useState(false);
|
|
||||||
const [masterColumns, setMasterColumns] = useState<
|
const [masterColumns, setMasterColumns] = useState<
|
||||||
Array<{
|
Array<{
|
||||||
columnName: string;
|
columnName: string;
|
||||||
|
|
@ -3357,22 +3359,6 @@ const MasterDetailExcelUploadConfig: React.FC<{
|
||||||
const masterTable = splitPanelInfo?.leftPanel?.tableName || "";
|
const masterTable = splitPanelInfo?.leftPanel?.tableName || "";
|
||||||
const detailTable = splitPanelInfo?.rightPanel?.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(() => {
|
useEffect(() => {
|
||||||
if (!masterTable) {
|
if (!masterTable) {
|
||||||
|
|
@ -3547,86 +3533,12 @@ const MasterDetailExcelUploadConfig: React.FC<{
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 채번 규칙 선택 - 유일하게 사용자가 설정하는 항목 */}
|
{/* 마스터 키 자동 생성 안내 */}
|
||||||
{relationInfo && (
|
{relationInfo && (
|
||||||
<div>
|
<p className="text-muted-foreground border-t pt-2 text-xs">
|
||||||
<Label className="text-xs">채번 규칙</Label>
|
마스터 테이블의 <strong>{relationInfo.masterKeyColumn}</strong> 값은 위에서 설정한 채번 규칙으로 자동
|
||||||
<Popover open={numberingRuleOpen} onOpenChange={setNumberingRuleOpen}>
|
생성됩니다.
|
||||||
<PopoverTrigger asChild>
|
</p>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 마스터 필드 선택 - 사용자가 엑셀 업로드 시 입력할 필드 */}
|
{/* 마스터 필드 선택 - 사용자가 엑셀 업로드 시 입력할 필드 */}
|
||||||
|
|
@ -3726,14 +3638,6 @@ 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>
|
||||||
|
|
@ -3741,23 +3645,181 @@ const MasterDetailExcelUploadConfig: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 업로드 후 제어 실행 설정 컴포넌트
|
* 엑셀 업로드 채번 규칙 설정 (단일 테이블/마스터-디테일 모두 사용 가능)
|
||||||
* 여러 개의 제어를 순서대로 실행할 수 있도록 지원
|
|
||||||
*/
|
*/
|
||||||
const AfterUploadControlConfig: React.FC<{
|
const ExcelNumberingRuleConfig: React.FC<{
|
||||||
config: any;
|
config: { numberingRuleId?: string; numberingTargetColumn?: string };
|
||||||
onUpdateProperty: (path: string, value: any) => void;
|
updateConfig: (updates: { numberingRuleId?: string; numberingTargetColumn?: string }) => void;
|
||||||
masterDetailConfig: any;
|
tableName?: string; // 단일 테이블인 경우 테이블명
|
||||||
updateMasterDetailConfig: (updates: any) => void;
|
hasSplitPanel?: boolean; // 분할 패널 여부 (마스터-디테일)
|
||||||
}> = ({ masterDetailConfig, updateMasterDetailConfig }) => {
|
}> = ({ config, updateConfig, tableName, hasSplitPanel }) => {
|
||||||
const [nodeFlows, setNodeFlows] = useState<
|
const [numberingRules, setNumberingRules] = useState<any[]>([]);
|
||||||
Array<{ flowId: number; flowName: string; flowDescription?: string }>
|
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 [flowSelectOpen, setFlowSelectOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
// 선택된 제어 목록 (배열로 관리)
|
const selectedFlows = config.afterUploadFlows || [];
|
||||||
const selectedFlows: Array<{ flowId: string; order: number }> = masterDetailConfig.afterUploadFlows || [];
|
|
||||||
|
|
||||||
// 노드 플로우 목록 로드
|
// 노드 플로우 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -3779,53 +3841,39 @@ const AfterUploadControlConfig: React.FC<{
|
||||||
loadNodeFlows();
|
loadNodeFlows();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 제어 추가
|
|
||||||
const addFlow = (flowId: string) => {
|
const addFlow = (flowId: string) => {
|
||||||
if (selectedFlows.some((f) => f.flowId === flowId)) return;
|
if (selectedFlows.some((f) => f.flowId === flowId)) return;
|
||||||
const newFlows = [...selectedFlows, { flowId, order: selectedFlows.length + 1 }];
|
const newFlows = [...selectedFlows, { flowId, order: selectedFlows.length + 1 }];
|
||||||
updateMasterDetailConfig({ afterUploadFlows: newFlows });
|
updateConfig({ afterUploadFlows: newFlows });
|
||||||
setFlowSelectOpen(false);
|
setFlowSelectOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 제어 제거
|
|
||||||
const removeFlow = (flowId: string) => {
|
const removeFlow = (flowId: string) => {
|
||||||
const newFlows = selectedFlows
|
const newFlows = selectedFlows.filter((f) => f.flowId !== flowId).map((f, idx) => ({ ...f, order: idx + 1 }));
|
||||||
.filter((f) => f.flowId !== flowId)
|
updateConfig({ afterUploadFlows: newFlows });
|
||||||
.map((f, idx) => ({ ...f, order: idx + 1 }));
|
|
||||||
updateMasterDetailConfig({ afterUploadFlows: newFlows });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 순서 변경 (위로)
|
|
||||||
const moveUp = (index: number) => {
|
const moveUp = (index: number) => {
|
||||||
if (index === 0) return;
|
if (index === 0) return;
|
||||||
const newFlows = [...selectedFlows];
|
const newFlows = [...selectedFlows];
|
||||||
[newFlows[index - 1], newFlows[index]] = [newFlows[index], newFlows[index - 1]];
|
[newFlows[index - 1], newFlows[index]] = [newFlows[index], newFlows[index - 1]];
|
||||||
updateMasterDetailConfig({
|
updateConfig({ afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })) });
|
||||||
afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 순서 변경 (아래로)
|
|
||||||
const moveDown = (index: number) => {
|
const moveDown = (index: number) => {
|
||||||
if (index === selectedFlows.length - 1) return;
|
if (index === selectedFlows.length - 1) return;
|
||||||
const newFlows = [...selectedFlows];
|
const newFlows = [...selectedFlows];
|
||||||
[newFlows[index], newFlows[index + 1]] = [newFlows[index + 1], newFlows[index]];
|
[newFlows[index], newFlows[index + 1]] = [newFlows[index + 1], newFlows[index]];
|
||||||
updateMasterDetailConfig({
|
updateConfig({ afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })) });
|
||||||
afterUploadFlows: newFlows.map((f, idx) => ({ ...f, order: idx + 1 })),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 선택되지 않은 플로우만 필터링
|
|
||||||
const availableFlows = nodeFlows.filter((f) => !selectedFlows.some((s) => s.flowId === String(f.flowId)));
|
const availableFlows = nodeFlows.filter((f) => !selectedFlows.some((s) => s.flowId === String(f.flowId)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t pt-3">
|
<div className="border-t pt-3">
|
||||||
<Label className="text-xs">업로드 후 제어 실행</Label>
|
<Label className="text-xs">업로드 후 제어 실행</Label>
|
||||||
<p className="text-muted-foreground mb-2 text-xs">
|
<p className="text-muted-foreground mb-2 text-xs">엑셀 업로드 완료 후 순서대로 실행할 제어를 추가하세요.</p>
|
||||||
엑셀 업로드 완료 후 순서대로 실행할 제어를 추가하세요.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* 선택된 제어 목록 */}
|
|
||||||
{selectedFlows.length > 0 && (
|
{selectedFlows.length > 0 && (
|
||||||
<div className="mb-2 space-y-1">
|
<div className="mb-2 space-y-1">
|
||||||
{selectedFlows.map((selected, index) => {
|
{selectedFlows.map((selected, index) => {
|
||||||
|
|
@ -3852,7 +3900,12 @@ const AfterUploadControlConfig: React.FC<{
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
</Button>
|
</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" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3861,7 +3914,6 @@ const AfterUploadControlConfig: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 제어 추가 버튼 */}
|
|
||||||
<Popover open={flowSelectOpen} onOpenChange={setFlowSelectOpen}>
|
<Popover open={flowSelectOpen} onOpenChange={setFlowSelectOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -3917,7 +3969,126 @@ const ExcelUploadConfigSection: React.FC<{
|
||||||
config: any;
|
config: any;
|
||||||
onUpdateProperty: (path: string, value: any) => void;
|
onUpdateProperty: (path: string, value: any) => void;
|
||||||
allComponents: ComponentData[];
|
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 (
|
return (
|
||||||
<div className="bg-muted/50 mt-4 space-y-4 rounded-lg border p-4">
|
<div className="bg-muted/50 mt-4 space-y-4 rounded-lg border p-4">
|
||||||
<h4 className="text-foreground text-sm font-medium">엑셀 업로드 설정</h4>
|
<h4 className="text-foreground text-sm font-medium">엑셀 업로드 설정</h4>
|
||||||
|
|
@ -3955,6 +4126,17 @@ const ExcelUploadConfigSection: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 채번 규칙 설정 (항상 표시) */}
|
||||||
|
<ExcelNumberingRuleConfig
|
||||||
|
config={excelUploadConfig}
|
||||||
|
updateConfig={updateExcelUploadConfig}
|
||||||
|
tableName={singleTableName}
|
||||||
|
hasSplitPanel={hasSplitPanel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 업로드 후 제어 실행 (항상 표시) */}
|
||||||
|
<ExcelAfterUploadControlConfig config={excelUploadConfig} updateConfig={updateExcelUploadConfig} />
|
||||||
|
|
||||||
{/* 마스터-디테일 설정 (분할 패널 자동 감지) */}
|
{/* 마스터-디테일 설정 (분할 패널 자동 감지) */}
|
||||||
<MasterDetailExcelUploadConfig
|
<MasterDetailExcelUploadConfig
|
||||||
config={config}
|
config={config}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ export interface ButtonActionConfig {
|
||||||
// 엑셀 업로드 관련
|
// 엑셀 업로드 관련
|
||||||
excelUploadMode?: "insert" | "update" | "upsert"; // 업로드 모드
|
excelUploadMode?: "insert" | "update" | "upsert"; // 업로드 모드
|
||||||
excelKeyColumn?: string; // 업데이트/Upsert 시 키 컬럼
|
excelKeyColumn?: string; // 업데이트/Upsert 시 키 컬럼
|
||||||
|
excelNumberingRuleId?: string; // 채번 규칙 ID (단일 테이블용)
|
||||||
|
excelNumberingTargetColumn?: string; // 채번 적용 컬럼 (단일 테이블용)
|
||||||
|
excelAfterUploadFlows?: Array<{ flowId: string; order: number }>; // 업로드 후 제어 실행
|
||||||
|
|
||||||
// 바코드 스캔 관련
|
// 바코드 스캔 관련
|
||||||
barcodeTargetField?: string; // 스캔 결과를 입력할 필드명
|
barcodeTargetField?: string; // 스캔 결과를 입력할 필드명
|
||||||
|
|
@ -4838,6 +4841,10 @@ export class ButtonActionExecutor {
|
||||||
userId: context.userId,
|
userId: context.userId,
|
||||||
tableName: context.tableName,
|
tableName: context.tableName,
|
||||||
screenId: context.screenId,
|
screenId: context.screenId,
|
||||||
|
// 채번 설정 디버깅
|
||||||
|
numberingRuleId: config.excelNumberingRuleId,
|
||||||
|
numberingTargetColumn: config.excelNumberingTargetColumn,
|
||||||
|
afterUploadFlows: config.excelAfterUploadFlows,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지)
|
// 🆕 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지)
|
||||||
|
|
@ -4909,7 +4916,7 @@ export class ButtonActionExecutor {
|
||||||
savedSize: localStorage.getItem(storageKey),
|
savedSize: localStorage.getItem(storageKey),
|
||||||
});
|
});
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
React.createElement(ExcelUploadModal, {
|
React.createElement(ExcelUploadModal, {
|
||||||
open: true,
|
open: true,
|
||||||
onOpenChange: (open: boolean) => {
|
onOpenChange: (open: boolean) => {
|
||||||
|
|
@ -4931,6 +4938,11 @@ export class ButtonActionExecutor {
|
||||||
isMasterDetail,
|
isMasterDetail,
|
||||||
masterDetailRelation,
|
masterDetailRelation,
|
||||||
masterDetailExcelConfig,
|
masterDetailExcelConfig,
|
||||||
|
// 🆕 단일 테이블 채번 설정
|
||||||
|
numberingRuleId: config.excelNumberingRuleId,
|
||||||
|
numberingTargetColumn: config.excelNumberingTargetColumn,
|
||||||
|
// 🆕 업로드 후 제어 실행 설정
|
||||||
|
afterUploadFlows: config.excelAfterUploadFlows,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
|
// 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
|
||||||
context.onRefresh?.();
|
context.onRefresh?.();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue