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