Compare commits
7 Commits
5c9dda6826
...
31e87e0bca
| Author | SHA1 | Date |
|---|---|---|
|
|
31e87e0bca | |
|
|
8aa6008351 | |
|
|
47b61a9a35 | |
|
|
d22c2ec96e | |
|
|
3677c77da0 | |
|
|
c11e80a43c | |
|
|
f8fb7d687e |
|
|
@ -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}개)` : ""}`
|
||||
|
|
|
|||
|
|
@ -175,13 +175,21 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
if (editData) {
|
||||
console.log("📝 [ScreenModal] 수정 데이터 설정:", editData);
|
||||
|
||||
// 🆕 배열인 경우 (그룹 레코드) vs 단일 객체 처리
|
||||
// 🆕 배열인 경우 두 가지 데이터를 설정:
|
||||
// 1. formData: 첫 번째 요소(객체) - 일반 입력 필드용 (TextInput 등)
|
||||
// 2. selectedData: 전체 배열 - 다중 항목 컴포넌트용 (SelectedItemsDetailInput 등)
|
||||
if (Array.isArray(editData)) {
|
||||
console.log(`📝 [ScreenModal] 그룹 레코드 ${editData.length}개 설정`);
|
||||
setFormData(editData as any); // 배열 그대로 전달 (SelectedItemsDetailInput에서 처리)
|
||||
setOriginalData(editData[0] || null); // 첫 번째 레코드를 원본으로 저장
|
||||
const firstRecord = editData[0] || {};
|
||||
console.log(`📝 [ScreenModal] 그룹 레코드 ${editData.length}개 설정:`, {
|
||||
formData: "첫 번째 레코드 (일반 입력 필드용)",
|
||||
selectedData: "전체 배열 (다중 항목 컴포넌트용)",
|
||||
});
|
||||
setFormData(firstRecord); // 🔧 일반 입력 필드용 (객체)
|
||||
setSelectedData(editData); // 🔧 다중 항목 컴포넌트용 (배열) - groupedData로 전달됨
|
||||
setOriginalData(firstRecord); // 첫 번째 레코드를 원본으로 저장
|
||||
} else {
|
||||
setFormData(editData);
|
||||
setSelectedData([editData]); // 🔧 단일 객체도 배열로 변환하여 저장
|
||||
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -281,10 +281,12 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
// 컴포넌트의 columnName에 해당하는 formData 값 추출
|
||||
const fieldName = (component as any).columnName || component.id;
|
||||
|
||||
// modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화
|
||||
// 다중 레코드를 다루는 컴포넌트는 배열 데이터로 초기화
|
||||
let currentValue;
|
||||
if (componentType === "modal-repeater-table" || componentType === "repeat-screen-modal") {
|
||||
// EditModal에서 전달된 groupedData가 있으면 우선 사용
|
||||
if (componentType === "modal-repeater-table" ||
|
||||
componentType === "repeat-screen-modal" ||
|
||||
componentType === "selected-items-detail-input") {
|
||||
// EditModal/ScreenModal에서 전달된 groupedData가 있으면 우선 사용
|
||||
currentValue = props.groupedData || formData?.[fieldName] || [];
|
||||
} else {
|
||||
currentValue = formData?.[fieldName] || "";
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
screenId,
|
||||
...props
|
||||
}) => {
|
||||
// 🆕 groupedData 추출 (DynamicComponentRenderer에서 전달)
|
||||
const groupedData = (props as any).groupedData || (props as any)._groupedData;
|
||||
// 🆕 URL 파라미터에서 dataSourceId 읽기
|
||||
const searchParams = useSearchParams();
|
||||
const urlDataSourceId = searchParams?.get("dataSourceId") || undefined;
|
||||
|
|
@ -225,24 +227,32 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
// 🆕 모달 데이터를 ItemData 구조로 변환 (그룹별 구조)
|
||||
useEffect(() => {
|
||||
// 🆕 수정 모드: formData에서 데이터 로드 (URL에 mode=edit이 있으면)
|
||||
// 🆕 수정 모드: groupedData 또는 formData에서 데이터 로드 (URL에 mode=edit이 있으면)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mode = urlParams.get("mode");
|
||||
|
||||
if (mode === "edit" && formData) {
|
||||
// 🔧 데이터 소스 우선순위: groupedData > formData (배열) > formData (객체)
|
||||
const sourceData = groupedData && Array.isArray(groupedData) && groupedData.length > 0
|
||||
? groupedData
|
||||
: formData;
|
||||
|
||||
if (mode === "edit" && sourceData) {
|
||||
// 배열인지 단일 객체인지 확인
|
||||
const isArray = Array.isArray(formData);
|
||||
const dataArray = isArray ? formData : [formData];
|
||||
const isArray = Array.isArray(sourceData);
|
||||
const dataArray = isArray ? sourceData : [sourceData];
|
||||
|
||||
if (dataArray.length === 0 || (dataArray.length === 1 && Object.keys(dataArray[0]).length === 0)) {
|
||||
console.warn("⚠️ [SelectedItemsDetailInput] formData가 비어있음");
|
||||
console.warn("⚠️ [SelectedItemsDetailInput] 데이터가 비어있음");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`📝 [SelectedItemsDetailInput] 수정 모드 - ${isArray ? "그룹 레코드" : "단일 레코드"} (${dataArray.length}개)`,
|
||||
);
|
||||
console.log("📝 [SelectedItemsDetailInput] formData (JSON):", JSON.stringify(dataArray, null, 2));
|
||||
console.log("📝 [SelectedItemsDetailInput] 데이터 소스:", {
|
||||
fromGroupedData: groupedData && Array.isArray(groupedData) && groupedData.length > 0,
|
||||
dataArray: JSON.stringify(dataArray, null, 2),
|
||||
});
|
||||
|
||||
const groups = componentConfig.fieldGroups || [];
|
||||
const additionalFields = componentConfig.additionalFields || [];
|
||||
|
|
@ -423,7 +433,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [modalData, component.id, componentConfig.fieldGroups, formData]); // formData 의존성 추가
|
||||
}, [modalData, component.id, componentConfig.fieldGroups, formData, groupedData]); // groupedData 의존성 추가
|
||||
|
||||
// 🆕 Cartesian Product 생성 함수 (items에서 모든 그룹의 조합을 생성)
|
||||
const generateCartesianProduct = useCallback(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
|||
// 숨김 상태 (props에서 전달받은 값 우선 사용)
|
||||
const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false;
|
||||
|
||||
// 수정 모드 여부 확인 (originalData가 있으면 수정 모드)
|
||||
const originalData = props.originalData || (props as any)._originalData;
|
||||
const isEditMode = originalData && Object.keys(originalData).length > 0;
|
||||
|
||||
// 자동생성된 값 상태
|
||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||
|
||||
|
|
@ -99,6 +103,16 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// 🆕 수정 모드일 때는 채번 규칙 스킵 (기존 값 유지)
|
||||
if (isEditMode) {
|
||||
console.log("⏭️ 수정 모드 - 채번 규칙 스킵:", {
|
||||
columnName: component.columnName,
|
||||
originalValue: originalData?.[component.columnName],
|
||||
});
|
||||
hasGeneratedRef.current = true; // 생성 완료로 표시하여 재실행 방지
|
||||
return;
|
||||
}
|
||||
|
||||
if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") {
|
||||
// 폼 데이터에 이미 값이 있으면 자동생성하지 않음
|
||||
const currentFormValue = formData?.[component.columnName];
|
||||
|
|
@ -171,7 +185,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
|||
};
|
||||
|
||||
generateAutoValue();
|
||||
}, [testAutoGeneration.enabled, testAutoGeneration.type, component.columnName, isInteractive]);
|
||||
}, [testAutoGeneration.enabled, testAutoGeneration.type, component.columnName, isInteractive, isEditMode]);
|
||||
|
||||
// 실제 화면에서 숨김 처리된 컴포넌트는 렌더링하지 않음
|
||||
if (isHidden && !isDesignMode) {
|
||||
|
|
|
|||
|
|
@ -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?.();
|
||||
|
|
|
|||
Loading…
Reference in New Issue