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:
parent
be0e63e577
commit
2772c2296c
|
|
@ -1586,6 +1586,20 @@ export default function TableManagementPage() {
|
|||
selectedColumn={selectedColumn}
|
||||
onSelectColumn={setSelectedColumn}
|
||||
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);
|
||||
if (idx >= 0) handleColumnChange(idx, field, value);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@ import {
|
|||
Zap,
|
||||
Copy,
|
||||
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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport";
|
||||
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
||||
|
|
@ -100,17 +104,31 @@ interface ColumnMapping {
|
|||
checkDuplicate?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +168,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
// 중복 처리 방법 (전역 설정)
|
||||
const [duplicateAction, setDuplicateAction] = useState<"overwrite" | "skip">("skip");
|
||||
|
||||
// 검증 화면에서 DB 중복 처리 방법 (null이면 미선택 = 업로드 차단)
|
||||
const [dbDuplicateAction, setDbDuplicateAction] = useState<"overwrite" | "skip" | null>(null);
|
||||
|
||||
// 엑셀 데이터 사전 검증 결과
|
||||
const [isDataValidating, setIsDataValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<import("@/lib/api/tableManagement").ExcelValidationResult | null>(null);
|
||||
|
|
@ -162,7 +183,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
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[];
|
||||
}>>
|
||||
>({});
|
||||
|
|
@ -723,6 +744,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
const options = validValues.map((v) => ({
|
||||
code: v.valueCode,
|
||||
label: v.valueLabel,
|
||||
depth: v.depth,
|
||||
ancestors: v.ancestors,
|
||||
}));
|
||||
|
||||
mismatches[`${catCol.systemCol}|||${catCol.displayName}`] = Array.from(invalidMap.entries()).map(
|
||||
|
|
@ -795,8 +818,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
setDisplayData(newData);
|
||||
setShowCategoryValidation(false);
|
||||
setCategoryMismatches({});
|
||||
toast.success("카테고리 값이 대체되었습니다.");
|
||||
setCurrentStep(3);
|
||||
toast.success("카테고리 값이 대체되었습니다. '다음'을 눌러 진행해주세요.");
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -890,6 +912,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
}
|
||||
|
||||
// 데이터 사전 검증 (NOT NULL 값 누락, UNIQUE 중복)
|
||||
setDbDuplicateAction(null);
|
||||
setIsDataValidating(true);
|
||||
try {
|
||||
const { validateExcelData: validateExcel } = await import("@/lib/api/tableManagement");
|
||||
|
|
@ -1105,9 +1128,33 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
const hasNumbering = !!numberingInfo;
|
||||
|
||||
// 중복 체크 설정 확인
|
||||
const duplicateCheckMappings = columnMappings.filter(
|
||||
let duplicateCheckMappings = columnMappings.filter(
|
||||
(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;
|
||||
|
||||
// 중복 체크를 위한 기존 데이터 조회 (중복 체크가 설정된 경우에만)
|
||||
|
|
@ -1170,7 +1217,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
|
||||
if (existingDataMap.has(key)) {
|
||||
existingRow = existingDataMap.get(key);
|
||||
if (duplicateAction === "skip") {
|
||||
if (effectiveDuplicateAction === "skip") {
|
||||
shouldSkip = true;
|
||||
skipCount++;
|
||||
console.log(`⏭️ [행 ${rowIdx + 1}] 중복으로 건너뛰기: ${key}`);
|
||||
|
|
@ -1352,6 +1399,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
setSystemColumns([]);
|
||||
setColumnMappings([]);
|
||||
setDuplicateAction("skip");
|
||||
setDbDuplicateAction(null);
|
||||
// 검증 상태 초기화
|
||||
setValidationResult(null);
|
||||
setIsDataValidating(false);
|
||||
|
|
@ -1366,7 +1414,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
|
||||
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={{
|
||||
|
|
@ -1974,12 +2022,50 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
|
||||
{/* DB 기존 데이터 중복 */}
|
||||
{validationResult.uniqueInDbErrors.length > 0 && (
|
||||
<div className="rounded-md border border-destructive bg-destructive/10 p-4">
|
||||
<h3 className="flex items-center gap-2 text-sm font-medium text-destructive sm:text-base">
|
||||
<XCircle className="h-4 w-4" />
|
||||
<div className={cn(
|
||||
"rounded-md border p-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}건)
|
||||
</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[] }[]>();
|
||||
for (const err of validationResult.uniqueInDbErrors) {
|
||||
|
|
@ -1993,7 +2079,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<div key={label}>
|
||||
{items.slice(0, 5).map((item, i) => (
|
||||
<p key={i}>
|
||||
<span className="font-medium">{label}</span> "{item.value}": 행 {item.rows.join(", ")}
|
||||
<span className="font-medium">{label}</span> "{item.value}": 행 {item.rows.join(", ")}
|
||||
</p>
|
||||
))}
|
||||
{items.length > 5 && <p className="font-medium">...외 {items.length - 5}건</p>}
|
||||
|
|
@ -2001,6 +2087,13 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
));
|
||||
})()}
|
||||
</div>
|
||||
{dbDuplicateAction && (
|
||||
<p className="mt-2 text-[10px] text-primary sm:text-xs font-medium">
|
||||
{dbDuplicateAction === "skip"
|
||||
? "중복 데이터는 건너뛰고 신규 데이터만 업로드합니다."
|
||||
: "중복 데이터는 새 값으로 덮어씁니다."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -2114,11 +2207,24 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
disabled={
|
||||
isUploading ||
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
|
@ -2165,9 +2271,41 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
</span>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
<Select
|
||||
value={item.replacement || ""}
|
||||
onValueChange={(val) => {
|
||||
<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) =>
|
||||
|
|
@ -2176,22 +2314,20 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
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"
|
||||
>
|
||||
<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}
|
||||
</SelectItem>
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -2210,17 +2346,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
>
|
||||
취소
|
||||
</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"
|
||||
|
|
|
|||
|
|
@ -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,9 +1039,41 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
|
|||
</span>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
<Select
|
||||
value={item.replacement || ""}
|
||||
onValueChange={(val) => {
|
||||
<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) =>
|
||||
|
|
@ -1031,22 +1082,20 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
|
|||
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"
|
||||
>
|
||||
<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}
|
||||
</SelectItem>
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue