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

@@ -28,7 +28,11 @@ import {
Zap,
Download,
Loader2,
Check,
ChevronsUpDown,
} 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 { cn } from "@/lib/utils";
import { EditableSpreadsheet } from "./EditableSpreadsheet";
@@ -51,17 +55,31 @@ interface ColumnMapping {
targetColumn: string | null;
}
interface FlatCategoryValue {
valueCode: string;
valueLabel: string;
depth: number;
ancestors: string[];
}
function flattenCategoryValues(
values: Array<{ valueCode: string; valueLabel: string; children?: any[] }>
): Array<{ valueCode: string; valueLabel: string }> {
const result: Array<{ valueCode: string; valueLabel: string }> = [];
const traverse = (items: any[]) => {
): FlatCategoryValue[] {
const result: FlatCategoryValue[] = [];
const traverse = (items: any[], depth: number, ancestors: string[]) => {
for (const item of items) {
result.push({ valueCode: item.valueCode, valueLabel: item.valueLabel });
if (item.children?.length > 0) traverse(item.children);
result.push({
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;
}
@@ -102,7 +120,7 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
Record<string, Array<{
invalidValue: string;
replacement: string | null;
validOptions: Array<{ code: string; label: string }>;
validOptions: Array<{ code: string; label: string; depth: number; ancestors: string[] }>;
rowIndices: number[];
}>>
>({});
@@ -398,6 +416,8 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
const options = validValues.map((v) => ({
code: v.valueCode,
label: v.valueLabel,
depth: v.depth,
ancestors: v.ancestors,
}));
const key = `${catColName}|||[${level.label}] ${catDisplayName}`;
@@ -475,8 +495,7 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
setDisplayData(newData);
setShowCategoryValidation(false);
setCategoryMismatches({});
toast.success("카테고리 값이 대체되었습니다.");
setCurrentStep(3);
toast.success("카테고리 값이 대체되었습니다. '다음'을 눌러 진행해주세요.");
return true;
};
@@ -543,7 +562,7 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog open={open} onOpenChange={(v) => { if (!showCategoryValidation) onOpenChange(v); }}>
<DialogContent
className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]"
style={{ width: "1000px", height: "700px", minWidth: "700px", minHeight: "500px", maxWidth: "1400px", maxHeight: "900px" }}
@@ -1020,33 +1039,63 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
</span>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
<Select
value={item.replacement || ""}
onValueChange={(val) => {
setCategoryMismatches((prev) => {
const updated = { ...prev };
updated[key] = updated[key].map((it, i) =>
i === idx ? { ...it, replacement: val } : it
);
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"
>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover>
<PopoverTrigger asChild>
<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) => {
const updated = { ...prev };
updated[key] = updated[key].map((it, i) =>
i === idx ? { ...it, replacement: val } : it
);
return updated;
});
}}
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}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
))}
</div>
@@ -1065,17 +1114,6 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
>
</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
onClick={applyCategoryReplacements}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"