feat: enhance TableManagementPage and ExcelUploadModal for improved functionality

- Added handling for unique and nullable column toggles in TableManagementPage, allowing for better column configuration.
- Updated ExcelUploadModal to include depth and ancestors in valid options for category values, enhancing the categorization process.
- Improved user feedback in ExcelUploadModal by clarifying success messages and ensuring proper handling of duplicate actions.
- Refactored category value flattening logic to maintain depth and ancestor information, improving data structure for better usability.

These enhancements aim to provide users with a more flexible and intuitive experience when managing table configurations and uploading Excel data.
This commit is contained in:
kjs 2026-03-17 22:37:13 +09:00
parent be0e63e577
commit 2772c2296c
3 changed files with 284 additions and 107 deletions

View File

@ -1586,6 +1586,20 @@ export default function TableManagementPage() {
selectedColumn={selectedColumn} selectedColumn={selectedColumn}
onSelectColumn={setSelectedColumn} onSelectColumn={setSelectedColumn}
onColumnChange={(columnName, field, value) => { onColumnChange={(columnName, field, value) => {
if (field === "isUnique") {
const currentColumn = columns.find((c) => c.columnName === columnName);
if (currentColumn) {
handleUniqueToggle(columnName, currentColumn.isUnique || "NO");
}
return;
}
if (field === "isNullable") {
const currentColumn = columns.find((c) => c.columnName === columnName);
if (currentColumn) {
handleNullableToggle(columnName, currentColumn.isNullable || "YES");
}
return;
}
const idx = columns.findIndex((c) => c.columnName === columnName); const idx = columns.findIndex((c) => c.columnName === columnName);
if (idx >= 0) handleColumnChange(idx, field, value); if (idx >= 0) handleColumnChange(idx, field, value);
}} }}

View File

@ -29,7 +29,11 @@ import {
Zap, Zap,
Copy, Copy,
Loader2, Loader2,
Check,
ChevronsUpDown,
} from "lucide-react"; } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport"; import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport";
import { DynamicFormApi } from "@/lib/api/dynamicForm"; import { DynamicFormApi } from "@/lib/api/dynamicForm";
@ -100,17 +104,31 @@ interface ColumnMapping {
checkDuplicate?: boolean; checkDuplicate?: boolean;
} }
interface FlatCategoryValue {
valueCode: string;
valueLabel: string;
depth: number;
ancestors: string[];
}
function flattenCategoryValues( function flattenCategoryValues(
values: Array<{ valueCode: string; valueLabel: string; children?: any[] }> values: Array<{ valueCode: string; valueLabel: string; children?: any[] }>
): Array<{ valueCode: string; valueLabel: string }> { ): FlatCategoryValue[] {
const result: Array<{ valueCode: string; valueLabel: string }> = []; const result: FlatCategoryValue[] = [];
const traverse = (items: any[]) => { const traverse = (items: any[], depth: number, ancestors: string[]) => {
for (const item of items) { for (const item of items) {
result.push({ valueCode: item.valueCode, valueLabel: item.valueLabel }); result.push({
if (item.children?.length > 0) traverse(item.children); valueCode: item.valueCode,
valueLabel: item.valueLabel,
depth,
ancestors,
});
if (item.children?.length > 0) {
traverse(item.children, depth + 1, [...ancestors, item.valueLabel]);
}
} }
}; };
traverse(values); traverse(values, 0, []);
return result; return result;
} }
@ -150,6 +168,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
// 중복 처리 방법 (전역 설정) // 중복 처리 방법 (전역 설정)
const [duplicateAction, setDuplicateAction] = useState<"overwrite" | "skip">("skip"); const [duplicateAction, setDuplicateAction] = useState<"overwrite" | "skip">("skip");
// 검증 화면에서 DB 중복 처리 방법 (null이면 미선택 = 업로드 차단)
const [dbDuplicateAction, setDbDuplicateAction] = useState<"overwrite" | "skip" | null>(null);
// 엑셀 데이터 사전 검증 결과 // 엑셀 데이터 사전 검증 결과
const [isDataValidating, setIsDataValidating] = useState(false); const [isDataValidating, setIsDataValidating] = useState(false);
const [validationResult, setValidationResult] = useState<import("@/lib/api/tableManagement").ExcelValidationResult | null>(null); const [validationResult, setValidationResult] = useState<import("@/lib/api/tableManagement").ExcelValidationResult | null>(null);
@ -162,7 +183,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
Record<string, Array<{ Record<string, Array<{
invalidValue: string; invalidValue: string;
replacement: string | null; replacement: string | null;
validOptions: Array<{ code: string; label: string }>; validOptions: Array<{ code: string; label: string; depth: number; ancestors: string[] }>;
rowIndices: number[]; rowIndices: number[];
}>> }>>
>({}); >({});
@ -723,6 +744,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
const options = validValues.map((v) => ({ const options = validValues.map((v) => ({
code: v.valueCode, code: v.valueCode,
label: v.valueLabel, label: v.valueLabel,
depth: v.depth,
ancestors: v.ancestors,
})); }));
mismatches[`${catCol.systemCol}|||${catCol.displayName}`] = Array.from(invalidMap.entries()).map( mismatches[`${catCol.systemCol}|||${catCol.displayName}`] = Array.from(invalidMap.entries()).map(
@ -795,8 +818,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
setDisplayData(newData); setDisplayData(newData);
setShowCategoryValidation(false); setShowCategoryValidation(false);
setCategoryMismatches({}); setCategoryMismatches({});
toast.success("카테고리 값이 대체되었습니다."); toast.success("카테고리 값이 대체되었습니다. '다음'을 눌러 진행해주세요.");
setCurrentStep(3);
return true; return true;
}; };
@ -890,6 +912,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
} }
// 데이터 사전 검증 (NOT NULL 값 누락, UNIQUE 중복) // 데이터 사전 검증 (NOT NULL 값 누락, UNIQUE 중복)
setDbDuplicateAction(null);
setIsDataValidating(true); setIsDataValidating(true);
try { try {
const { validateExcelData: validateExcel } = await import("@/lib/api/tableManagement"); const { validateExcelData: validateExcel } = await import("@/lib/api/tableManagement");
@ -1105,9 +1128,33 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
const hasNumbering = !!numberingInfo; const hasNumbering = !!numberingInfo;
// 중복 체크 설정 확인 // 중복 체크 설정 확인
const duplicateCheckMappings = columnMappings.filter( let duplicateCheckMappings = columnMappings.filter(
(m) => m.checkDuplicate && m.systemColumn (m) => m.checkDuplicate && m.systemColumn
); );
let effectiveDuplicateAction = duplicateAction;
// 검증 화면에서 DB 중복 처리 방법을 선택한 경우, 유니크 컬럼을 자동으로 중복 체크에 추가
if (dbDuplicateAction && validationResult?.uniqueInDbErrors && validationResult.uniqueInDbErrors.length > 0) {
effectiveDuplicateAction = dbDuplicateAction;
const uniqueColumns = new Set(validationResult.uniqueInDbErrors.map((e) => e.column));
for (const colName of uniqueColumns) {
const alreadyAdded = duplicateCheckMappings.some((m) => {
const mapped = m.systemColumn?.includes(".") ? m.systemColumn.split(".")[1] : m.systemColumn;
return mapped === colName;
});
if (!alreadyAdded) {
const mapping = columnMappings.find((m) => {
const mapped = m.systemColumn?.includes(".") ? m.systemColumn.split(".")[1] : m.systemColumn;
return mapped === colName;
});
if (mapping) {
duplicateCheckMappings = [...duplicateCheckMappings, { ...mapping, checkDuplicate: true }];
}
}
}
console.log(`📊 검증 화면 DB 중복 처리: ${dbDuplicateAction}, 체크 컬럼: ${[...uniqueColumns].join(", ")}`);
}
const hasDuplicateCheck = duplicateCheckMappings.length > 0; const hasDuplicateCheck = duplicateCheckMappings.length > 0;
// 중복 체크를 위한 기존 데이터 조회 (중복 체크가 설정된 경우에만) // 중복 체크를 위한 기존 데이터 조회 (중복 체크가 설정된 경우에만)
@ -1170,7 +1217,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
if (existingDataMap.has(key)) { if (existingDataMap.has(key)) {
existingRow = existingDataMap.get(key); existingRow = existingDataMap.get(key);
if (duplicateAction === "skip") { if (effectiveDuplicateAction === "skip") {
shouldSkip = true; shouldSkip = true;
skipCount++; skipCount++;
console.log(`⏭️ [행 ${rowIdx + 1}] 중복으로 건너뛰기: ${key}`); console.log(`⏭️ [행 ${rowIdx + 1}] 중복으로 건너뛰기: ${key}`);
@ -1352,6 +1399,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
setSystemColumns([]); setSystemColumns([]);
setColumnMappings([]); setColumnMappings([]);
setDuplicateAction("skip"); setDuplicateAction("skip");
setDbDuplicateAction(null);
// 검증 상태 초기화 // 검증 상태 초기화
setValidationResult(null); setValidationResult(null);
setIsDataValidating(false); setIsDataValidating(false);
@ -1366,7 +1414,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
return ( return (
<> <>
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={(v) => { if (!showCategoryValidation) onOpenChange(v); }}>
<DialogContent <DialogContent
className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]" className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]"
style={{ style={{
@ -1974,12 +2022,50 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
{/* DB 기존 데이터 중복 */} {/* DB 기존 데이터 중복 */}
{validationResult.uniqueInDbErrors.length > 0 && ( {validationResult.uniqueInDbErrors.length > 0 && (
<div className="rounded-md border border-destructive bg-destructive/10 p-4"> <div className={cn(
<h3 className="flex items-center gap-2 text-sm font-medium text-destructive sm:text-base"> "rounded-md border p-4",
<XCircle className="h-4 w-4" /> dbDuplicateAction
? "border-primary/30 bg-primary/5"
: "border-destructive bg-destructive/10"
)}>
<div className="flex items-start justify-between gap-3">
<h3 className={cn(
"flex items-center gap-2 text-sm font-medium sm:text-base",
dbDuplicateAction ? "text-primary" : "text-destructive"
)}>
{dbDuplicateAction ? <CheckCircle2 className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
DB ({validationResult.uniqueInDbErrors.length}) DB ({validationResult.uniqueInDbErrors.length})
</h3> </h3>
<div className="mt-2 max-h-[150px] space-y-0.5 overflow-y-auto text-[10px] text-destructive sm:text-xs"> <div className="flex items-center gap-2 shrink-0">
<span className={cn(
"text-[10px] sm:text-xs",
dbDuplicateAction ? "text-primary" : "text-destructive"
)}>
:
</span>
<Select
value={dbDuplicateAction || ""}
onValueChange={(value) => setDbDuplicateAction(value as "overwrite" | "skip")}
>
<SelectTrigger className={cn(
"h-7 w-[100px] text-[10px] sm:text-xs",
dbDuplicateAction
? "border-primary/40 bg-white"
: "border-destructive/40 bg-white animate-pulse"
)}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="skip" className="text-xs"></SelectItem>
<SelectItem value="overwrite" className="text-xs"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className={cn(
"mt-2 max-h-[150px] space-y-0.5 overflow-y-auto text-[10px] sm:text-xs",
dbDuplicateAction ? "text-primary/80" : "text-destructive"
)}>
{(() => { {(() => {
const grouped = new Map<string, { value: string; rows: number[] }[]>(); const grouped = new Map<string, { value: string; rows: number[] }[]>();
for (const err of validationResult.uniqueInDbErrors) { for (const err of validationResult.uniqueInDbErrors) {
@ -1993,7 +2079,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
<div key={label}> <div key={label}>
{items.slice(0, 5).map((item, i) => ( {items.slice(0, 5).map((item, i) => (
<p key={i}> <p key={i}>
<span className="font-medium">{label}</span> "{item.value}": {item.rows.join(", ")} <span className="font-medium">{label}</span> &quot;{item.value}&quot;: {item.rows.join(", ")}
</p> </p>
))} ))}
{items.length > 5 && <p className="font-medium">... {items.length - 5}</p>} {items.length > 5 && <p className="font-medium">... {items.length - 5}</p>}
@ -2001,6 +2087,13 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
)); ));
})()} })()}
</div> </div>
{dbDuplicateAction && (
<p className="mt-2 text-[10px] text-primary sm:text-xs font-medium">
{dbDuplicateAction === "skip"
? "중복 데이터는 건너뛰고 신규 데이터만 업로드합니다."
: "중복 데이터는 새 값으로 덮어씁니다."}
</p>
)}
</div> </div>
)} )}
</div> </div>
@ -2114,11 +2207,24 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
disabled={ disabled={
isUploading || isUploading ||
columnMappings.filter((m) => m.systemColumn).length === 0 || columnMappings.filter((m) => m.systemColumn).length === 0 ||
(validationResult !== null && !validationResult.isValid) (validationResult !== null && !validationResult.isValid && !(
validationResult.notNullErrors.length === 0 &&
validationResult.uniqueInExcelErrors.length === 0 &&
validationResult.uniqueInDbErrors.length > 0 &&
dbDuplicateAction !== null
))
} }
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm" className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
> >
{isUploading ? "업로드 중..." : validationResult && !validationResult.isValid ? "검증 실패 - 이전으로 돌아가 수정" : "업로드"} {isUploading ? "업로드 중..." :
validationResult && !validationResult.isValid && !(
validationResult.notNullErrors.length === 0 &&
validationResult.uniqueInExcelErrors.length === 0 &&
validationResult.uniqueInDbErrors.length > 0 &&
dbDuplicateAction !== null
) ? "검증 실패 - 이전으로 돌아가 수정" :
dbDuplicateAction === "skip" ? "업로드 (중복 건너뛰기)" :
dbDuplicateAction === "overwrite" ? "업로드 (중복 덮어쓰기)" : "업로드"}
</Button> </Button>
)} )}
</DialogFooter> </DialogFooter>
@ -2165,9 +2271,41 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
</span> </span>
</div> </div>
<ArrowRight className="h-4 w-4 text-muted-foreground" /> <ArrowRight className="h-4 w-4 text-muted-foreground" />
<Select <Popover>
value={item.replacement || ""} <PopoverTrigger asChild>
onValueChange={(val) => { <Button
variant="outline"
role="combobox"
className="h-8 w-full justify-between text-xs sm:h-9 sm:text-sm"
>
<span className="truncate">
{item.replacement
? item.validOptions.find((o) => o.code === item.replacement)?.label || item.replacement
: "대체 값 선택"}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] p-0" align="start">
<Command
filter={(value, search) => {
const opt = item.validOptions.find((o) => o.code === value);
if (!opt) return 0;
const s = search.toLowerCase();
if (opt.label.toLowerCase().includes(s)) return 1;
if (opt.ancestors.some((a) => a.toLowerCase().includes(s))) return 1;
return 0;
}}
>
<CommandInput placeholder="카테고리 검색..." className="text-xs" />
<CommandList className="max-h-52">
<CommandEmpty className="py-3 text-xs"> </CommandEmpty>
<CommandGroup>
{item.validOptions.map((opt) => (
<CommandItem
key={opt.code}
value={opt.code}
onSelect={(val) => {
setCategoryMismatches((prev) => { setCategoryMismatches((prev) => {
const updated = { ...prev }; const updated = { ...prev };
updated[key] = updated[key].map((it, i) => updated[key] = updated[key].map((it, i) =>
@ -2176,22 +2314,20 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
return updated; return updated;
}); });
}} }}
>
<SelectTrigger className="h-8 text-xs sm:h-9 sm:text-sm">
<SelectValue placeholder="대체 값 선택" />
</SelectTrigger>
<SelectContent>
{item.validOptions.map((opt) => (
<SelectItem
key={opt.code}
value={opt.code}
className="text-xs sm:text-sm" className="text-xs sm:text-sm"
> >
<Check className={cn("mr-2 h-3 w-3", item.replacement === opt.code ? "opacity-100" : "opacity-0")} />
<span style={{ paddingLeft: `${opt.depth * 12}px` }}>
{opt.depth > 0 && <span className="mr-1 text-muted-foreground"></span>}
{opt.label} {opt.label}
</SelectItem> </span>
</CommandItem>
))} ))}
</SelectContent> </CommandGroup>
</Select> </CommandList>
</Command>
</PopoverContent>
</Popover>
</div> </div>
))} ))}
</div> </div>
@ -2210,17 +2346,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
> >
</Button> </Button>
<Button
variant="outline"
onClick={() => {
setShowCategoryValidation(false);
setCategoryMismatches({});
setCurrentStep(3);
}}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button <Button
onClick={applyCategoryReplacements} onClick={applyCategoryReplacements}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm" className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"

View File

@ -28,7 +28,11 @@ import {
Zap, Zap,
Download, Download,
Loader2, Loader2,
Check,
ChevronsUpDown,
} from "lucide-react"; } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { importFromExcel, getExcelSheetNames, exportToExcel } from "@/lib/utils/excelExport"; import { importFromExcel, getExcelSheetNames, exportToExcel } from "@/lib/utils/excelExport";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { EditableSpreadsheet } from "./EditableSpreadsheet"; import { EditableSpreadsheet } from "./EditableSpreadsheet";
@ -51,17 +55,31 @@ interface ColumnMapping {
targetColumn: string | null; targetColumn: string | null;
} }
interface FlatCategoryValue {
valueCode: string;
valueLabel: string;
depth: number;
ancestors: string[];
}
function flattenCategoryValues( function flattenCategoryValues(
values: Array<{ valueCode: string; valueLabel: string; children?: any[] }> values: Array<{ valueCode: string; valueLabel: string; children?: any[] }>
): Array<{ valueCode: string; valueLabel: string }> { ): FlatCategoryValue[] {
const result: Array<{ valueCode: string; valueLabel: string }> = []; const result: FlatCategoryValue[] = [];
const traverse = (items: any[]) => { const traverse = (items: any[], depth: number, ancestors: string[]) => {
for (const item of items) { for (const item of items) {
result.push({ valueCode: item.valueCode, valueLabel: item.valueLabel }); result.push({
if (item.children?.length > 0) traverse(item.children); valueCode: item.valueCode,
valueLabel: item.valueLabel,
depth,
ancestors,
});
if (item.children?.length > 0) {
traverse(item.children, depth + 1, [...ancestors, item.valueLabel]);
}
} }
}; };
traverse(values); traverse(values, 0, []);
return result; return result;
} }
@ -102,7 +120,7 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
Record<string, Array<{ Record<string, Array<{
invalidValue: string; invalidValue: string;
replacement: string | null; replacement: string | null;
validOptions: Array<{ code: string; label: string }>; validOptions: Array<{ code: string; label: string; depth: number; ancestors: string[] }>;
rowIndices: number[]; rowIndices: number[];
}>> }>>
>({}); >({});
@ -398,6 +416,8 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
const options = validValues.map((v) => ({ const options = validValues.map((v) => ({
code: v.valueCode, code: v.valueCode,
label: v.valueLabel, label: v.valueLabel,
depth: v.depth,
ancestors: v.ancestors,
})); }));
const key = `${catColName}|||[${level.label}] ${catDisplayName}`; const key = `${catColName}|||[${level.label}] ${catDisplayName}`;
@ -475,8 +495,7 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
setDisplayData(newData); setDisplayData(newData);
setShowCategoryValidation(false); setShowCategoryValidation(false);
setCategoryMismatches({}); setCategoryMismatches({});
toast.success("카테고리 값이 대체되었습니다."); toast.success("카테고리 값이 대체되었습니다. '다음'을 눌러 진행해주세요.");
setCurrentStep(3);
return true; return true;
}; };
@ -543,7 +562,7 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
return ( return (
<> <>
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={(v) => { if (!showCategoryValidation) onOpenChange(v); }}>
<DialogContent <DialogContent
className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]" className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]"
style={{ width: "1000px", height: "700px", minWidth: "700px", minHeight: "500px", maxWidth: "1400px", maxHeight: "900px" }} style={{ width: "1000px", height: "700px", minWidth: "700px", minHeight: "500px", maxWidth: "1400px", maxHeight: "900px" }}
@ -1020,9 +1039,41 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
</span> </span>
</div> </div>
<ArrowRight className="h-4 w-4 text-muted-foreground" /> <ArrowRight className="h-4 w-4 text-muted-foreground" />
<Select <Popover>
value={item.replacement || ""} <PopoverTrigger asChild>
onValueChange={(val) => { <Button
variant="outline"
role="combobox"
className="h-8 w-full justify-between text-xs sm:h-9 sm:text-sm"
>
<span className="truncate">
{item.replacement
? item.validOptions.find((o) => o.code === item.replacement)?.label || item.replacement
: "대체 값 선택"}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] p-0" align="start">
<Command
filter={(value, search) => {
const opt = item.validOptions.find((o) => o.code === value);
if (!opt) return 0;
const s = search.toLowerCase();
if (opt.label.toLowerCase().includes(s)) return 1;
if (opt.ancestors.some((a) => a.toLowerCase().includes(s))) return 1;
return 0;
}}
>
<CommandInput placeholder="카테고리 검색..." className="text-xs" />
<CommandList className="max-h-52">
<CommandEmpty className="py-3 text-xs"> </CommandEmpty>
<CommandGroup>
{item.validOptions.map((opt) => (
<CommandItem
key={opt.code}
value={opt.code}
onSelect={(val) => {
setCategoryMismatches((prev) => { setCategoryMismatches((prev) => {
const updated = { ...prev }; const updated = { ...prev };
updated[key] = updated[key].map((it, i) => updated[key] = updated[key].map((it, i) =>
@ -1031,22 +1082,20 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
return updated; return updated;
}); });
}} }}
>
<SelectTrigger className="h-8 text-xs sm:h-9 sm:text-sm">
<SelectValue placeholder="대체 값 선택" />
</SelectTrigger>
<SelectContent>
{item.validOptions.map((opt) => (
<SelectItem
key={opt.code}
value={opt.code}
className="text-xs sm:text-sm" className="text-xs sm:text-sm"
> >
<Check className={cn("mr-2 h-3 w-3", item.replacement === opt.code ? "opacity-100" : "opacity-0")} />
<span style={{ paddingLeft: `${opt.depth * 12}px` }}>
{opt.depth > 0 && <span className="mr-1 text-muted-foreground"></span>}
{opt.label} {opt.label}
</SelectItem> </span>
</CommandItem>
))} ))}
</SelectContent> </CommandGroup>
</Select> </CommandList>
</Command>
</PopoverContent>
</Popover>
</div> </div>
))} ))}
</div> </div>
@ -1065,17 +1114,6 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
> >
</Button> </Button>
<Button
variant="outline"
onClick={() => {
setShowCategoryValidation(false);
setCategoryMismatches({});
setCurrentStep(3);
}}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button <Button
onClick={applyCategoryReplacements} onClick={applyCategoryReplacements}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm" className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"