jskim-node #407

Merged
kjs merged 3 commits from jskim-node into main 2026-03-09 22:21:47 +09:00
3 changed files with 622 additions and 11 deletions
Showing only changes of commit 316ce30663 - Show all commits

View File

@ -3463,10 +3463,12 @@ export class TableManagementService {
}
// ORDER BY 절 구성
// sortBy가 없으면 created_date 컬럼이 있는 경우에만 기본 정렬 적
// sortBy가 메인 테이블 컬럼이면 main. 접두사, 조인 별칭이면 접두사 없이 사
const hasCreatedDateColumn = selectColumns.includes("created_date");
const orderBy = options.sortBy
? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
? selectColumns.includes(options.sortBy)
? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
: `"${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
: hasCreatedDateColumn
? `main."created_date" DESC`
: "";
@ -3710,7 +3712,9 @@ export class TableManagementService {
selectColumns,
"", // WHERE 절은 나중에 추가
options.sortBy
? `main."${options.sortBy}" ${options.sortOrder || "ASC"}`
? selectColumns.includes(options.sortBy)
? `main."${options.sortBy}" ${options.sortOrder || "ASC"}`
: `"${options.sortBy}" ${options.sortOrder || "ASC"}`
: hasCreatedDateForSearch
? `main."created_date" DESC`
: undefined,
@ -3901,7 +3905,9 @@ export class TableManagementService {
const whereClause = whereConditions.join(" AND ");
const hasCreatedDateForOrder = selectColumns.includes("created_date");
const orderBy = options.sortBy
? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
? selectColumns.includes(options.sortBy)
? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
: `"${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
: hasCreatedDateForOrder
? `main."created_date" DESC`
: "";

View File

@ -27,6 +27,7 @@ import {
ArrowRight,
Zap,
Copy,
Loader2,
} from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport";
@ -35,6 +36,8 @@ import { getTableSchema, TableColumn } from "@/lib/api/tableSchema";
import { cn } from "@/lib/utils";
import { findMappingByColumns, saveMappingTemplate } from "@/lib/api/excelMapping";
import { EditableSpreadsheet } from "./EditableSpreadsheet";
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
import { getTableColumns } from "@/lib/api/tableManagement";
// 마스터-디테일 엑셀 업로드 설정 (버튼 설정에서 설정)
export interface MasterDetailExcelConfig {
@ -133,6 +136,19 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
// 중복 처리 방법 (전역 설정)
const [duplicateAction, setDuplicateAction] = useState<"overwrite" | "skip">("skip");
// 카테고리 검증 관련
const [showCategoryValidation, setShowCategoryValidation] = useState(false);
const [isCategoryValidating, setIsCategoryValidating] = useState(false);
// { [columnName]: { invalidValue: string, replacement: string | null, validOptions: {code: string, label: string}[], rowIndices: number[] }[] }
const [categoryMismatches, setCategoryMismatches] = useState<
Record<string, Array<{
invalidValue: string;
replacement: string | null;
validOptions: Array<{ code: string; label: string }>;
rowIndices: number[];
}>>
>({});
// 3단계: 확인
const [isUploading, setIsUploading] = useState(false);
@ -601,8 +617,177 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
// 중복 체크 설정된 컬럼 수
const duplicateCheckCount = columnMappings.filter((m) => m.checkDuplicate && m.systemColumn).length;
// 카테고리 컬럼 검증: 엑셀 데이터에서 유효하지 않은 카테고리 값 감지
const validateCategoryColumns = async () => {
try {
setIsCategoryValidating(true);
const targetTableName = isMasterDetail && masterDetailRelation
? masterDetailRelation.detailTable
: tableName;
// 테이블의 카테고리 타입 컬럼 조회
const colResponse = await getTableColumns(targetTableName);
if (!colResponse.success || !colResponse.data?.columns) {
return null;
}
const categoryColumns = colResponse.data.columns.filter(
(col: any) => col.inputType === "category"
);
if (categoryColumns.length === 0) {
return null;
}
// 매핑된 컬럼 중 카테고리 타입인 것 찾기
const mappedCategoryColumns: Array<{
systemCol: string;
excelCol: string;
displayName: string;
}> = [];
for (const mapping of columnMappings) {
if (!mapping.systemColumn) continue;
const rawName = mapping.systemColumn.includes(".")
? mapping.systemColumn.split(".")[1]
: mapping.systemColumn;
const catCol = categoryColumns.find(
(cc: any) => (cc.columnName || cc.column_name) === rawName
);
if (catCol) {
mappedCategoryColumns.push({
systemCol: rawName,
excelCol: mapping.excelColumn,
displayName: catCol.displayName || catCol.display_name || rawName,
});
}
}
if (mappedCategoryColumns.length === 0) {
return null;
}
// 각 카테고리 컬럼의 유효값 조회 및 엑셀 데이터 검증
const mismatches: typeof categoryMismatches = {};
for (const catCol of mappedCategoryColumns) {
const valuesResponse = await getCategoryValues(targetTableName, catCol.systemCol);
if (!valuesResponse.success || !valuesResponse.data) continue;
const validValues = valuesResponse.data as Array<{
valueCode: string;
valueLabel: string;
}>;
// 유효한 코드와 라벨 Set 생성
const validCodes = new Set(validValues.map((v) => v.valueCode));
const validLabels = new Set(validValues.map((v) => v.valueLabel));
const validLabelsLower = new Set(validValues.map((v) => v.valueLabel.toLowerCase()));
// 엑셀 데이터에서 유효하지 않은 값 수집
const invalidMap = new Map<string, number[]>();
allData.forEach((row, rowIdx) => {
const val = row[catCol.excelCol];
if (val === undefined || val === null || String(val).trim() === "") return;
const strVal = String(val).trim();
// 코드 매칭 → 라벨 매칭 → 소문자 라벨 매칭
if (validCodes.has(strVal)) return;
if (validLabels.has(strVal)) return;
if (validLabelsLower.has(strVal.toLowerCase())) return;
if (!invalidMap.has(strVal)) {
invalidMap.set(strVal, []);
}
invalidMap.get(strVal)!.push(rowIdx);
});
if (invalidMap.size > 0) {
const options = validValues.map((v) => ({
code: v.valueCode,
label: v.valueLabel,
}));
mismatches[`${catCol.systemCol}|||${catCol.displayName}`] = Array.from(invalidMap.entries()).map(
([invalidValue, rowIndices]) => ({
invalidValue,
replacement: null,
validOptions: options,
rowIndices,
})
);
}
}
if (Object.keys(mismatches).length > 0) {
return mismatches;
}
return null;
} catch (error) {
console.error("카테고리 검증 실패:", error);
return null;
} finally {
setIsCategoryValidating(false);
}
};
// 카테고리 대체값 선택 후 데이터에 적용
const applyCategoryReplacements = () => {
// 모든 대체값이 선택되었는지 확인
for (const [key, items] of Object.entries(categoryMismatches)) {
for (const item of items) {
if (item.replacement === null) {
toast.error("모든 항목의 대체 값을 선택해주세요.");
return false;
}
}
}
// 엑셀 컬럼명 → 시스템 컬럼명 매핑 구축
const systemToExcelMap = new Map<string, string>();
for (const mapping of columnMappings) {
if (!mapping.systemColumn) continue;
const rawName = mapping.systemColumn.includes(".")
? mapping.systemColumn.split(".")[1]
: mapping.systemColumn;
systemToExcelMap.set(rawName, mapping.excelColumn);
}
const newData = allData.map((row) => ({ ...row }));
for (const [key, items] of Object.entries(categoryMismatches)) {
const systemCol = key.split("|||")[0];
const excelCol = systemToExcelMap.get(systemCol);
if (!excelCol) continue;
for (const item of items) {
if (!item.replacement) continue;
// 선택된 대체값의 라벨 찾기
const selectedOption = item.validOptions.find((opt) => opt.code === item.replacement);
const replacementLabel = selectedOption?.label || item.replacement;
for (const rowIdx of item.rowIndices) {
if (newData[rowIdx]) {
newData[rowIdx][excelCol] = replacementLabel;
}
}
}
}
setAllData(newData);
setDisplayData(newData);
setShowCategoryValidation(false);
setCategoryMismatches({});
toast.success("카테고리 값이 대체되었습니다.");
setCurrentStep(3);
return true;
};
// 다음 단계
const handleNext = () => {
const handleNext = async () => {
if (currentStep === 1 && !file) {
toast.error("파일을 선택해주세요.");
return;
@ -655,7 +840,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
}
}
// 2단계 → 3단계 전환 시: NOT NULL 컬럼 매핑 필수 검증
// 2단계 → 3단계 전환 시: NOT NULL 컬럼 매핑 필수 검증 + 카테고리 검증
if (currentStep === 2) {
// 매핑된 시스템 컬럼 (원본 이름 그대로 + dot 뒤 이름 둘 다 저장)
const mappedSystemCols = new Set<string>();
@ -681,6 +866,14 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
toast.error(`필수(NOT NULL) 컬럼이 매핑되지 않았습니다: ${colNames}`);
return;
}
// 카테고리 컬럼 검증
const mismatches = await validateCategoryColumns();
if (mismatches) {
setCategoryMismatches(mismatches);
setShowCategoryValidation(true);
return;
}
}
setCurrentStep((prev) => Math.min(prev + 1, 3));
@ -1108,12 +1301,17 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
setSystemColumns([]);
setColumnMappings([]);
setDuplicateAction("skip");
// 카테고리 검증 초기화
setShowCategoryValidation(false);
setCategoryMismatches({});
setIsCategoryValidating(false);
// 🆕 마스터-디테일 모드 초기화
setMasterFieldValues({});
}
}, [open]);
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]"
@ -1750,10 +1948,17 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
{currentStep < 3 ? (
<Button
onClick={handleNext}
disabled={isUploading || (currentStep === 1 && !file)}
disabled={isUploading || isCategoryValidating || (currentStep === 1 && !file)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{isCategoryValidating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
"다음"
)}
</Button>
) : (
<Button
@ -1769,5 +1974,112 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
</DialogFooter>
</DialogContent>
</Dialog>
{/* 카테고리 대체값 선택 다이얼로그 */}
<Dialog open={showCategoryValidation} onOpenChange={(open) => {
if (!open) {
setShowCategoryValidation(false);
setCategoryMismatches({});
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
<AlertCircle className="h-5 w-5 text-warning" />
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
. .
</DialogDescription>
</DialogHeader>
<div className="max-h-[400px] space-y-4 overflow-y-auto pr-1">
{Object.entries(categoryMismatches).map(([key, items]) => {
const [columnName, displayName] = key.split("|||");
return (
<div key={key} className="space-y-2">
<h4 className="text-sm font-semibold text-foreground">
{displayName || columnName}
</h4>
{items.map((item, idx) => (
<div
key={`${key}-${idx}`}
className="grid grid-cols-[1fr_auto_1fr] items-center gap-2 rounded-md border border-border bg-muted/30 p-2"
>
<div className="flex flex-col">
<span className="text-xs font-medium text-destructive line-through">
{item.invalidValue}
</span>
<span className="text-[10px] text-muted-foreground">
{item.rowIndices.length}
</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>
</div>
))}
</div>
);
})}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => {
setShowCategoryValidation(false);
setCategoryMismatches({});
}}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</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"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -36,6 +36,8 @@ import {
TableChainConfig,
uploadMultiTableExcel,
} from "@/lib/api/multiTableExcel";
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
import { getTableColumns } from "@/lib/api/tableManagement";
export interface MultiTableExcelUploadModalProps {
open: boolean;
@ -79,6 +81,18 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
// 업로드
const [isUploading, setIsUploading] = useState(false);
// 카테고리 검증 관련
const [showCategoryValidation, setShowCategoryValidation] = useState(false);
const [isCategoryValidating, setIsCategoryValidating] = useState(false);
const [categoryMismatches, setCategoryMismatches] = useState<
Record<string, Array<{
invalidValue: string;
replacement: string | null;
validOptions: Array<{ code: string; label: string }>;
rowIndices: number[];
}>>
>({});
const selectedMode = config.uploadModes.find((m) => m.id === selectedModeId);
// 선택된 모드에서 활성화되는 컬럼 목록
@ -302,8 +316,161 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
}
};
// 카테고리 검증: 매핑된 컬럼 중 카테고리 타입인 것의 유효하지 않은 값 감지
const validateCategoryColumns = async () => {
try {
setIsCategoryValidating(true);
if (!selectedMode) return null;
const mismatches: typeof categoryMismatches = {};
// 활성 레벨별로 카테고리 컬럼 검증
for (const levelIdx of selectedMode.activeLevels) {
const level = config.levels[levelIdx];
if (!level) continue;
// 해당 테이블의 카테고리 타입 컬럼 조회
const colResponse = await getTableColumns(level.tableName);
if (!colResponse.success || !colResponse.data?.columns) continue;
const categoryColumns = colResponse.data.columns.filter(
(col: any) => col.inputType === "category"
);
if (categoryColumns.length === 0) continue;
// 매핑된 컬럼 중 카테고리 타입인 것 찾기
for (const catCol of categoryColumns) {
const catColName = catCol.columnName || catCol.column_name;
const catDisplayName = catCol.displayName || catCol.display_name || catColName;
// level.columns에서 해당 dbColumn 찾기
const levelCol = level.columns.find((lc) => lc.dbColumn === catColName);
if (!levelCol) continue;
// 매핑에서 해당 excelHeader에 연결된 엑셀 컬럼 찾기
const mapping = columnMappings.find((m) => m.targetColumn === levelCol.excelHeader);
if (!mapping) continue;
// 유효한 카테고리 값 조회
const valuesResponse = await getCategoryValues(level.tableName, catColName);
if (!valuesResponse.success || !valuesResponse.data) continue;
const validValues = valuesResponse.data as Array<{
valueCode: string;
valueLabel: string;
}>;
const validCodes = new Set(validValues.map((v) => v.valueCode));
const validLabels = new Set(validValues.map((v) => v.valueLabel));
const validLabelsLower = new Set(validValues.map((v) => v.valueLabel.toLowerCase()));
// 엑셀 데이터에서 유효하지 않은 값 수집
const invalidMap = new Map<string, number[]>();
allData.forEach((row, rowIdx) => {
const val = row[mapping.excelColumn];
if (val === undefined || val === null || String(val).trim() === "") return;
const strVal = String(val).trim();
if (validCodes.has(strVal)) return;
if (validLabels.has(strVal)) return;
if (validLabelsLower.has(strVal.toLowerCase())) return;
if (!invalidMap.has(strVal)) {
invalidMap.set(strVal, []);
}
invalidMap.get(strVal)!.push(rowIdx);
});
if (invalidMap.size > 0) {
const options = validValues.map((v) => ({
code: v.valueCode,
label: v.valueLabel,
}));
const key = `${catColName}|||[${level.label}] ${catDisplayName}`;
mismatches[key] = Array.from(invalidMap.entries()).map(
([invalidValue, rowIndices]) => ({
invalidValue,
replacement: null,
validOptions: options,
rowIndices,
})
);
}
}
}
if (Object.keys(mismatches).length > 0) {
return mismatches;
}
return null;
} catch (error) {
console.error("카테고리 검증 실패:", error);
return null;
} finally {
setIsCategoryValidating(false);
}
};
// 카테고리 대체값 적용
const applyCategoryReplacements = () => {
for (const [, items] of Object.entries(categoryMismatches)) {
for (const item of items) {
if (item.replacement === null) {
toast.error("모든 항목의 대체 값을 선택해주세요.");
return false;
}
}
}
// 시스템 컬럼명 → 엑셀 컬럼명 역매핑 구축
const dbColToExcelCol = new Map<string, string>();
if (selectedMode) {
for (const levelIdx of selectedMode.activeLevels) {
const level = config.levels[levelIdx];
if (!level) continue;
for (const lc of level.columns) {
const mapping = columnMappings.find((m) => m.targetColumn === lc.excelHeader);
if (mapping) {
dbColToExcelCol.set(lc.dbColumn, mapping.excelColumn);
}
}
}
}
const newData = allData.map((row) => ({ ...row }));
for (const [key, items] of Object.entries(categoryMismatches)) {
const systemCol = key.split("|||")[0];
const excelCol = dbColToExcelCol.get(systemCol);
if (!excelCol) continue;
for (const item of items) {
if (!item.replacement) continue;
const selectedOption = item.validOptions.find((opt) => opt.code === item.replacement);
const replacementLabel = selectedOption?.label || item.replacement;
for (const rowIdx of item.rowIndices) {
if (newData[rowIdx]) {
newData[rowIdx][excelCol] = replacementLabel;
}
}
}
}
setAllData(newData);
setDisplayData(newData);
setShowCategoryValidation(false);
setCategoryMismatches({});
toast.success("카테고리 값이 대체되었습니다.");
setCurrentStep(3);
return true;
};
// 다음/이전 단계
const handleNext = () => {
const handleNext = async () => {
if (currentStep === 1) {
if (!file) {
toast.error("파일을 선택해주세요.");
@ -328,6 +495,14 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
toast.error(`필수 컬럼이 매핑되지 않았습니다: ${unmappedRequired.join(", ")}`);
return;
}
// 카테고리 컬럼 검증
const mismatches = await validateCategoryColumns();
if (mismatches) {
setCategoryMismatches(mismatches);
setShowCategoryValidation(true);
return;
}
}
setCurrentStep((prev) => Math.min(prev + 1, 3));
@ -349,10 +524,14 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
setDisplayData([]);
setExcelColumns([]);
setColumnMappings([]);
setShowCategoryValidation(false);
setCategoryMismatches({});
setIsCategoryValidating(false);
}
}, [open, config.uploadModes]);
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]"
@ -758,10 +937,17 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
{currentStep < 3 ? (
<Button
onClick={handleNext}
disabled={isUploading || (currentStep === 1 && !file)}
disabled={isUploading || isCategoryValidating || (currentStep === 1 && !file)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{isCategoryValidating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
"다음"
)}
</Button>
) : (
<Button
@ -782,5 +968,112 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
</DialogFooter>
</DialogContent>
</Dialog>
{/* 카테고리 대체값 선택 다이얼로그 */}
<Dialog open={showCategoryValidation} onOpenChange={(open) => {
if (!open) {
setShowCategoryValidation(false);
setCategoryMismatches({});
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
<AlertCircle className="h-5 w-5 text-warning" />
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
. .
</DialogDescription>
</DialogHeader>
<div className="max-h-[400px] space-y-4 overflow-y-auto pr-1">
{Object.entries(categoryMismatches).map(([key, items]) => {
const [, displayName] = key.split("|||");
return (
<div key={key} className="space-y-2">
<h4 className="text-sm font-semibold text-foreground">
{displayName}
</h4>
{items.map((item, idx) => (
<div
key={`${key}-${idx}`}
className="grid grid-cols-[1fr_auto_1fr] items-center gap-2 rounded-md border border-border bg-muted/30 p-2"
>
<div className="flex flex-col">
<span className="text-xs font-medium text-destructive line-through">
{item.invalidValue}
</span>
<span className="text-[10px] text-muted-foreground">
{item.rowIndices.length}
</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>
</div>
))}
</div>
);
})}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => {
setShowCategoryValidation(false);
setCategoryMismatches({});
}}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</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"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};