feat: Enhance master-detail Excel upload functionality with detail update tracking

- Added support for tracking updated detail records during the Excel upload process, improving feedback to users on the number of records inserted and updated.
- Updated response messages to provide clearer information about the processing results, including the number of newly inserted and updated detail records.
- Refactored related components to ensure consistency in handling detail updates and improve overall user experience during uploads.
This commit is contained in:
kjs
2026-02-11 18:29:36 +09:00
parent e065835c4d
commit 56d069f853
5 changed files with 454 additions and 42 deletions

View File

@@ -453,6 +453,48 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
}
}
// 채번 정보 병합: table_type_columns에서 inputType 가져오기
try {
const { getTableColumns } = await import("@/lib/api/tableManagement");
const targetTables = isMasterDetail && masterDetailRelation
? [masterDetailRelation.masterTable, masterDetailRelation.detailTable]
: [tableName];
// 테이블별 채번 컬럼 수집
const numberingColSet = new Set<string>();
for (const tbl of targetTables) {
const typeResponse = await getTableColumns(tbl);
if (typeResponse.success && typeResponse.data?.columns) {
for (const tc of typeResponse.data.columns) {
if (tc.inputType === "numbering") {
try {
const settings = typeof tc.detailSettings === "string"
? JSON.parse(tc.detailSettings) : tc.detailSettings;
if (settings?.numberingRuleId) {
numberingColSet.add(tc.columnName);
}
} catch { /* 파싱 실패 무시 */ }
}
}
}
}
// systemColumns에 isNumbering 플래그 추가
if (numberingColSet.size > 0) {
allColumns = allColumns.map((col) => {
const rawName = (col as any).originalName || col.name;
const colName = rawName.includes(".") ? rawName.split(".")[1] : rawName;
if (numberingColSet.has(colName)) {
return { ...col, isNumbering: true } as any;
}
return col;
});
console.log("✅ 채번 컬럼 감지:", Array.from(numberingColSet));
}
} catch (error) {
console.warn("채번 정보 로드 실패 (무시):", error);
}
console.log("✅ 시스템 컬럼 로드 완료 (자동 생성 컬럼 제외):", allColumns);
setSystemColumns(allColumns);
@@ -613,6 +655,34 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
}
}
// 2단계 → 3단계 전환 시: NOT NULL 컬럼 매핑 필수 검증
if (currentStep === 2) {
// 매핑된 시스템 컬럼 (원본 이름 그대로 + dot 뒤 이름 둘 다 저장)
const mappedSystemCols = new Set<string>();
columnMappings.filter((m) => m.systemColumn).forEach((m) => {
const colName = m.systemColumn!;
mappedSystemCols.add(colName); // 원본 (예: user_info.user_id)
if (colName.includes(".")) {
mappedSystemCols.add(colName.split(".")[1]); // dot 뒤 (예: user_id)
}
});
const unmappedRequired = systemColumns.filter((col) => {
const rawName = col.name.includes(".") ? col.name.split(".")[1] : col.name;
if (AUTO_GENERATED_COLUMNS.includes(rawName.toLowerCase())) return false;
if (col.nullable) return false;
if (mappedSystemCols.has(col.name) || mappedSystemCols.has(rawName)) return false;
if ((col as any).isNumbering) return false;
return true;
});
if (unmappedRequired.length > 0) {
const colNames = unmappedRequired.map((c) => c.label || c.name).join(", ");
toast.error(`필수(NOT NULL) 컬럼이 매핑되지 않았습니다: ${colNames}`);
return;
}
}
setCurrentStep((prev) => Math.min(prev + 1, 3));
};
@@ -1397,15 +1467,19 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
<SelectItem value="none" className="text-xs sm:text-sm">
</SelectItem>
{systemColumns.map((col) => (
{systemColumns.map((col) => {
const isRequired = !col.nullable && !AUTO_GENERATED_COLUMNS.includes(col.name.toLowerCase()) && !(col as any).isNumbering;
return (
<SelectItem
key={col.name}
value={col.name}
className="text-xs sm:text-sm"
>
{isRequired && <span className="text-destructive mr-1">*</span>}
{col.label || col.name} ({col.type})
</SelectItem>
))}
);
})}
</SelectContent>
</Select>
{/* 중복 체크 체크박스 */}
@@ -1427,6 +1501,38 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
</div>
</div>
{/* 미매핑 필수(NOT NULL) 컬럼 경고 */}
{(() => {
const mappedCols = new Set<string>();
columnMappings.filter((m) => m.systemColumn).forEach((m) => {
const n = m.systemColumn!;
mappedCols.add(n);
if (n.includes(".")) mappedCols.add(n.split(".")[1]);
});
const missing = systemColumns.filter((col) => {
const rawName = col.name.includes(".") ? col.name.split(".")[1] : col.name;
if (AUTO_GENERATED_COLUMNS.includes(rawName.toLowerCase())) return false;
if (col.nullable) return false;
if (mappedCols.has(col.name) || mappedCols.has(rawName)) return false;
if ((col as any).isNumbering) return false;
return true;
});
if (missing.length === 0) return null;
return (
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3">
<div className="flex items-start gap-2">
<AlertCircle className="mt-0.5 h-4 w-4 text-destructive" />
<div className="text-[10px] text-destructive sm:text-xs">
<p className="font-medium">(NOT NULL) :</p>
<p className="mt-1">
{missing.map((c) => c.label || c.name).join(", ")}
</p>
</div>
</div>
</div>
);
})()}
{/* 중복 체크 안내 */}
{duplicateCheckCount > 0 ? (
<div className="rounded-md border border-blue-200 bg-blue-50 p-3">