feat: Add Numbering Rule APIs and Frontend Integration
- Implemented a new API endpoint to retrieve numbering rules based on table and column names, enhancing the flexibility of numbering rule management. - Added a new service method to handle the retrieval of numbering columns specific to a company, ensuring proper company code filtering. - Updated the frontend to load and display numbering columns, allowing users to select and manage numbering rules effectively. - Refactored existing logic to improve the handling of numbering rules, including fallback mechanisms for legacy data. These changes enhance the functionality and user experience in managing numbering rules within the application.
This commit is contained in:
@@ -669,38 +669,6 @@ export default function TableManagementPage() {
|
||||
console.log("🔧 Code 계층 역할 설정 JSON 생성:", codeSettings);
|
||||
}
|
||||
|
||||
// 🆕 Numbering 타입인 경우 numberingRuleId를 detailSettings에 포함
|
||||
console.log("🔍 Numbering 저장 체크:", {
|
||||
inputType: column.inputType,
|
||||
numberingRuleId: column.numberingRuleId,
|
||||
hasNumberingRuleId: !!column.numberingRuleId,
|
||||
});
|
||||
|
||||
if (column.inputType === "numbering") {
|
||||
let existingSettings: Record<string, unknown> = {};
|
||||
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
|
||||
try {
|
||||
existingSettings = JSON.parse(finalDetailSettings);
|
||||
} catch {
|
||||
existingSettings = {};
|
||||
}
|
||||
}
|
||||
|
||||
// numberingRuleId가 있으면 저장, 없으면 제거
|
||||
if (column.numberingRuleId) {
|
||||
const numberingSettings = {
|
||||
...existingSettings,
|
||||
numberingRuleId: column.numberingRuleId,
|
||||
};
|
||||
finalDetailSettings = JSON.stringify(numberingSettings);
|
||||
console.log("🔧 Numbering 설정 JSON 생성:", numberingSettings);
|
||||
} else {
|
||||
// numberingRuleId가 없으면 빈 객체
|
||||
finalDetailSettings = JSON.stringify(existingSettings);
|
||||
console.log("🔧 Numbering 규칙 없이 저장:", existingSettings);
|
||||
}
|
||||
}
|
||||
|
||||
const columnSetting = {
|
||||
columnName: column.columnName,
|
||||
columnLabel: column.displayName,
|
||||
@@ -844,28 +812,6 @@ export default function TableManagementPage() {
|
||||
// detailSettings 계산
|
||||
let finalDetailSettings = column.detailSettings || "";
|
||||
|
||||
// 🆕 Numbering 타입인 경우 numberingRuleId를 detailSettings에 포함
|
||||
if (column.inputType === "numbering" && column.numberingRuleId) {
|
||||
let existingSettings: Record<string, unknown> = {};
|
||||
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
|
||||
try {
|
||||
existingSettings = JSON.parse(finalDetailSettings);
|
||||
} catch {
|
||||
existingSettings = {};
|
||||
}
|
||||
}
|
||||
const numberingSettings = {
|
||||
...existingSettings,
|
||||
numberingRuleId: column.numberingRuleId,
|
||||
};
|
||||
finalDetailSettings = JSON.stringify(numberingSettings);
|
||||
console.log("🔧 전체저장 - Numbering 설정 JSON 생성:", {
|
||||
columnName: column.columnName,
|
||||
numberingRuleId: column.numberingRuleId,
|
||||
finalDetailSettings,
|
||||
});
|
||||
}
|
||||
|
||||
// 🆕 Entity 타입인 경우 detailSettings에 엔티티 설정 포함
|
||||
if (column.inputType === "entity" && column.referenceTable) {
|
||||
let existingSettings: Record<string, unknown> = {};
|
||||
@@ -1987,118 +1933,7 @@ export default function TableManagementPage() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* 입력 타입이 'numbering'인 경우 채번규칙 선택 */}
|
||||
{column.inputType === "numbering" && (
|
||||
<div className="w-64">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">채번규칙</label>
|
||||
<Popover
|
||||
open={numberingComboboxOpen[column.columnName] || false}
|
||||
onOpenChange={(open) =>
|
||||
setNumberingComboboxOpen((prev) => ({
|
||||
...prev,
|
||||
[column.columnName]: open,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={numberingComboboxOpen[column.columnName] || false}
|
||||
disabled={numberingRulesLoading}
|
||||
className="bg-background h-8 w-full justify-between text-xs"
|
||||
>
|
||||
<span className="truncate">
|
||||
{numberingRulesLoading
|
||||
? "로딩 중..."
|
||||
: column.numberingRuleId
|
||||
? numberingRules.find((r) => r.ruleId === column.numberingRuleId)
|
||||
?.ruleName || column.numberingRuleId
|
||||
: "채번규칙 선택..."}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="규칙 검색..." className="h-8 text-xs" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="py-2 text-center text-xs">
|
||||
채번규칙을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="none"
|
||||
onSelect={() => {
|
||||
const columnIndex = columns.findIndex(
|
||||
(c) => c.columnName === column.columnName,
|
||||
);
|
||||
handleColumnChange(columnIndex, "numberingRuleId", undefined);
|
||||
setNumberingComboboxOpen((prev) => ({
|
||||
...prev,
|
||||
[column.columnName]: false,
|
||||
}));
|
||||
// 자동 저장 제거 - 전체 저장 버튼으로 저장
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
!column.numberingRuleId ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
-- 선택 안함 --
|
||||
</CommandItem>
|
||||
{numberingRules.map((rule) => (
|
||||
<CommandItem
|
||||
key={rule.ruleId}
|
||||
value={`${rule.ruleName} ${rule.ruleId}`}
|
||||
onSelect={() => {
|
||||
const columnIndex = columns.findIndex(
|
||||
(c) => c.columnName === column.columnName,
|
||||
);
|
||||
// 상태 업데이트만 (자동 저장 제거)
|
||||
handleColumnChange(columnIndex, "numberingRuleId", rule.ruleId);
|
||||
setNumberingComboboxOpen((prev) => ({
|
||||
...prev,
|
||||
[column.columnName]: false,
|
||||
}));
|
||||
// 전체 저장 버튼으로 저장
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
column.numberingRuleId === rule.ruleId
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{rule.ruleName}</span>
|
||||
{rule.tableName && (
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{rule.tableName}.{rule.columnName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{column.numberingRuleId && (
|
||||
<div className="bg-primary/10 text-primary mt-1 flex items-center gap-1 rounded px-2 py-0.5 text-[10px]">
|
||||
<Check className="h-2.5 w-2.5" />
|
||||
<span>규칙 설정됨</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 채번 타입은 옵션설정 > 채번설정에서 관리 (별도 선택 불필요) */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-4">
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Plus, Save, Edit2, Trash2, FolderTree, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Plus, Save, Edit2, FolderTree } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorType } from "@/types/numbering-rule";
|
||||
import { NumberingRuleCard } from "./NumberingRuleCard";
|
||||
import { NumberingRulePreview } from "./NumberingRulePreview";
|
||||
import {
|
||||
saveNumberingRuleToTest,
|
||||
deleteNumberingRuleFromTest,
|
||||
getNumberingRulesFromTest,
|
||||
} from "@/lib/api/numberingRule";
|
||||
import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { saveNumberingRuleToTest } from "@/lib/api/numberingRule";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// 카테고리 값 트리 노드 타입
|
||||
interface CategoryValueNode {
|
||||
valueId: number;
|
||||
valueCode: string;
|
||||
valueLabel: string;
|
||||
depth: number;
|
||||
path: string;
|
||||
parentValueId: number | null;
|
||||
children?: CategoryValueNode[];
|
||||
interface NumberingColumn {
|
||||
tableName: string;
|
||||
tableLabel: string;
|
||||
columnName: string;
|
||||
columnLabel: string;
|
||||
}
|
||||
|
||||
interface GroupedColumns {
|
||||
tableLabel: string;
|
||||
columns: NumberingColumn[];
|
||||
}
|
||||
|
||||
interface NumberingRuleDesignerProps {
|
||||
@@ -54,138 +48,100 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
currentTableName,
|
||||
menuObjid,
|
||||
}) => {
|
||||
const [savedRules, setSavedRules] = useState<NumberingRuleConfig[]>([]);
|
||||
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
|
||||
const [numberingColumns, setNumberingColumns] = useState<NumberingColumn[]>([]);
|
||||
const [selectedColumn, setSelectedColumn] = useState<{ tableName: string; columnName: string } | null>(null);
|
||||
const [currentRule, setCurrentRule] = useState<NumberingRuleConfig | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [leftTitle, setLeftTitle] = useState("저장된 규칙 목록");
|
||||
const [columnSearch, setColumnSearch] = useState("");
|
||||
const [rightTitle, setRightTitle] = useState("규칙 편집");
|
||||
const [editingLeftTitle, setEditingLeftTitle] = useState(false);
|
||||
const [editingRightTitle, setEditingRightTitle] = useState(false);
|
||||
|
||||
// 구분자 관련 상태 (개별 파트 사이 구분자)
|
||||
const [separatorTypes, setSeparatorTypes] = useState<Record<number, SeparatorType>>({});
|
||||
const [customSeparators, setCustomSeparators] = useState<Record<number, string>>({});
|
||||
|
||||
// 카테고리 조건 관련 상태 - 모든 카테고리를 테이블.컬럼 단위로 조회
|
||||
interface CategoryOption {
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
displayName: string; // "테이블명.컬럼명" 형식
|
||||
}
|
||||
const [allCategoryOptions, setAllCategoryOptions] = useState<CategoryOption[]>([]);
|
||||
const [selectedCategoryKey, setSelectedCategoryKey] = useState<string>(""); // "tableName.columnName"
|
||||
const [categoryValues, setCategoryValues] = useState<CategoryValueNode[]>([]);
|
||||
const [categoryKeyOpen, setCategoryKeyOpen] = useState(false);
|
||||
const [categoryValueOpen, setCategoryValueOpen] = useState(false);
|
||||
const [loadingCategories, setLoadingCategories] = useState(false);
|
||||
|
||||
// 좌측: 채번 타입 컬럼 목록 로드
|
||||
useEffect(() => {
|
||||
loadRules();
|
||||
loadAllCategoryOptions(); // 전체 카테고리 옵션 로드
|
||||
loadNumberingColumns();
|
||||
}, []);
|
||||
|
||||
// currentRule의 categoryColumn이 변경되면 selectedCategoryKey 동기화
|
||||
useEffect(() => {
|
||||
if (currentRule?.categoryColumn) {
|
||||
setSelectedCategoryKey(currentRule.categoryColumn);
|
||||
} else {
|
||||
setSelectedCategoryKey("");
|
||||
}
|
||||
}, [currentRule?.categoryColumn]);
|
||||
|
||||
// 카테고리 키 선택 시 해당 카테고리 값 로드
|
||||
useEffect(() => {
|
||||
if (selectedCategoryKey) {
|
||||
const [tableName, columnName] = selectedCategoryKey.split(".");
|
||||
if (tableName && columnName) {
|
||||
loadCategoryValues(tableName, columnName);
|
||||
}
|
||||
} else {
|
||||
setCategoryValues([]);
|
||||
}
|
||||
}, [selectedCategoryKey]);
|
||||
|
||||
// 전체 카테고리 옵션 로드 (모든 테이블의 category 타입 컬럼)
|
||||
const loadAllCategoryOptions = async () => {
|
||||
try {
|
||||
// category_values 테이블에서 고유한 테이블.컬럼 조합 조회
|
||||
const response = await getAllCategoryKeys();
|
||||
if (response.success && response.data) {
|
||||
const options: CategoryOption[] = response.data.map((item) => ({
|
||||
tableName: item.tableName,
|
||||
columnName: item.columnName,
|
||||
displayName: `${item.tableName}.${item.columnName}`,
|
||||
}));
|
||||
setAllCategoryOptions(options);
|
||||
console.log("전체 카테고리 옵션 로드:", options);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("카테고리 옵션 목록 조회 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 특정 카테고리 컬럼의 값 트리 조회
|
||||
const loadCategoryValues = async (tableName: string, columnName: string) => {
|
||||
setLoadingCategories(true);
|
||||
try {
|
||||
const response = await getCategoryTree(tableName, columnName);
|
||||
if (response.success && response.data) {
|
||||
setCategoryValues(response.data);
|
||||
console.log("카테고리 값 로드:", { tableName, columnName, count: response.data.length });
|
||||
} else {
|
||||
setCategoryValues([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("카테고리 값 트리 조회 실패:", error);
|
||||
setCategoryValues([]);
|
||||
} finally {
|
||||
setLoadingCategories(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 카테고리 값을 플랫 리스트로 변환 (UI에서 선택용)
|
||||
const flattenCategoryValues = (nodes: CategoryValueNode[], result: CategoryValueNode[] = []): CategoryValueNode[] => {
|
||||
for (const node of nodes) {
|
||||
result.push(node);
|
||||
if (node.children && node.children.length > 0) {
|
||||
flattenCategoryValues(node.children, result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const flatCategoryValues = flattenCategoryValues(categoryValues);
|
||||
|
||||
const loadRules = useCallback(async () => {
|
||||
const loadNumberingColumns = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작 (test 테이블):", {
|
||||
menuObjid,
|
||||
hasMenuObjid: !!menuObjid,
|
||||
});
|
||||
|
||||
// test 테이블에서 조회
|
||||
const response = await getNumberingRulesFromTest(menuObjid);
|
||||
|
||||
console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답 (test 테이블):", {
|
||||
menuObjid,
|
||||
success: response.success,
|
||||
rulesCount: response.data?.length || 0,
|
||||
rules: response.data,
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
setSavedRules(response.data);
|
||||
} else {
|
||||
toast.error(response.error || "규칙 목록을 불러올 수 없습니다");
|
||||
const response = await apiClient.get("/table-management/numbering-columns");
|
||||
if (response.data.success && response.data.data) {
|
||||
setNumberingColumns(response.data.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`로딩 실패: ${error.message}`);
|
||||
console.error("채번 컬럼 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [menuObjid]);
|
||||
};
|
||||
|
||||
// 컬럼 선택 시 해당 컬럼의 채번 규칙 로드
|
||||
const handleSelectColumn = async (tableName: string, columnName: string) => {
|
||||
setSelectedColumn({ tableName, columnName });
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get(`/numbering-rules/by-column/${tableName}/${columnName}`);
|
||||
if (response.data.success && response.data.data) {
|
||||
const rule = response.data.data as NumberingRuleConfig;
|
||||
setCurrentRule(JSON.parse(JSON.stringify(rule)));
|
||||
} else {
|
||||
// 규칙 없으면 신규 생성 모드
|
||||
const newRule: NumberingRuleConfig = {
|
||||
ruleId: `rule-${Date.now()}`,
|
||||
ruleName: `${columnName} 채번`,
|
||||
parts: [],
|
||||
separator: "-",
|
||||
resetPeriod: "none",
|
||||
currentSequence: 1,
|
||||
scopeType: "table",
|
||||
tableName,
|
||||
columnName,
|
||||
};
|
||||
setCurrentRule(newRule);
|
||||
}
|
||||
} catch {
|
||||
const newRule: NumberingRuleConfig = {
|
||||
ruleId: `rule-${Date.now()}`,
|
||||
ruleName: `${columnName} 채번`,
|
||||
parts: [],
|
||||
separator: "-",
|
||||
resetPeriod: "none",
|
||||
currentSequence: 1,
|
||||
scopeType: "table",
|
||||
tableName,
|
||||
columnName,
|
||||
};
|
||||
setCurrentRule(newRule);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블별로 그룹화
|
||||
const groupedColumns = numberingColumns.reduce<Record<string, GroupedColumns>>((acc, col) => {
|
||||
if (!acc[col.tableName]) {
|
||||
acc[col.tableName] = { tableLabel: col.tableLabel, columns: [] };
|
||||
}
|
||||
acc[col.tableName].columns.push(col);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 검색 필터 적용
|
||||
const filteredGroups = Object.entries(groupedColumns).filter(([tableName, group]) => {
|
||||
if (!columnSearch) return true;
|
||||
const search = columnSearch.toLowerCase();
|
||||
return (
|
||||
tableName.toLowerCase().includes(search) ||
|
||||
group.tableLabel.toLowerCase().includes(search) ||
|
||||
group.columns.some(
|
||||
(c) => c.columnName.toLowerCase().includes(search) || c.columnLabel.toLowerCase().includes(search)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRule) {
|
||||
@@ -343,60 +299,20 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
return part;
|
||||
});
|
||||
|
||||
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
|
||||
// menuObjid가 있으면 menu 스코프, 없으면 기존 scopeType 유지
|
||||
const effectiveMenuObjid = menuObjid || currentRule.menuObjid || null;
|
||||
const effectiveScopeType = effectiveMenuObjid ? "menu" : (currentRule.scopeType || "global");
|
||||
|
||||
const ruleToSave = {
|
||||
...currentRule,
|
||||
parts: partsWithDefaults,
|
||||
scopeType: effectiveScopeType as "menu" | "global", // menuObjid 유무에 따라 결정
|
||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용)
|
||||
menuObjid: effectiveMenuObjid, // 메뉴 OBJID (필터링 기준)
|
||||
scopeType: "table" as const,
|
||||
tableName: selectedColumn?.tableName || currentRule.tableName || "",
|
||||
columnName: selectedColumn?.columnName || currentRule.columnName || "",
|
||||
};
|
||||
|
||||
console.log("💾 채번 규칙 저장:", {
|
||||
currentTableName,
|
||||
menuObjid,
|
||||
"currentRule.tableName": currentRule.tableName,
|
||||
"currentRule.menuObjid": currentRule.menuObjid,
|
||||
"ruleToSave.tableName": ruleToSave.tableName,
|
||||
"ruleToSave.menuObjid": ruleToSave.menuObjid,
|
||||
"ruleToSave.scopeType": ruleToSave.scopeType,
|
||||
ruleToSave,
|
||||
});
|
||||
|
||||
// 테스트 테이블에 저장 (numbering_rules)
|
||||
const response = await saveNumberingRuleToTest(ruleToSave);
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 깊은 복사하여 savedRules와 currentRule이 다른 객체를 참조하도록 함
|
||||
const currentData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig;
|
||||
|
||||
// setSavedRules 내부에서 prev를 사용해서 existing 확인 (클로저 문제 방지)
|
||||
setSavedRules((prev) => {
|
||||
const savedData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig;
|
||||
const existsInPrev = prev.some((r) => r.ruleId === ruleToSave.ruleId);
|
||||
|
||||
console.log("🔍 [handleSave] setSavedRules:", {
|
||||
ruleId: ruleToSave.ruleId,
|
||||
existsInPrev,
|
||||
prevCount: prev.length,
|
||||
});
|
||||
|
||||
if (existsInPrev) {
|
||||
// 기존 규칙 업데이트
|
||||
return prev.map((r) => (r.ruleId === ruleToSave.ruleId ? savedData : r));
|
||||
} else {
|
||||
// 새 규칙 추가
|
||||
return [...prev, savedData];
|
||||
}
|
||||
});
|
||||
|
||||
setCurrentRule(currentData);
|
||||
setSelectedRuleId(response.data.ruleId);
|
||||
|
||||
await onSave?.(response.data);
|
||||
toast.success("채번 규칙이 저장되었습니다");
|
||||
} else {
|
||||
@@ -407,143 +323,62 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentRule, onSave, currentTableName, menuObjid]);
|
||||
|
||||
const handleSelectRule = useCallback((rule: NumberingRuleConfig) => {
|
||||
console.log("🔍 [handleSelectRule] 규칙 선택:", {
|
||||
ruleId: rule.ruleId,
|
||||
ruleName: rule.ruleName,
|
||||
partsCount: rule.parts?.length || 0,
|
||||
parts: rule.parts?.map(p => ({ id: p.id, order: p.order, partType: p.partType })),
|
||||
});
|
||||
|
||||
setSelectedRuleId(rule.ruleId);
|
||||
// 깊은 복사하여 객체 참조 분리 (좌측 목록과 편집 영역의 객체가 공유되지 않도록)
|
||||
const ruleCopy = JSON.parse(JSON.stringify(rule)) as NumberingRuleConfig;
|
||||
|
||||
console.log("🔍 [handleSelectRule] 깊은 복사 후:", {
|
||||
ruleId: ruleCopy.ruleId,
|
||||
partsCount: ruleCopy.parts?.length || 0,
|
||||
parts: ruleCopy.parts?.map(p => ({ id: p.id, order: p.order, partType: p.partType })),
|
||||
});
|
||||
|
||||
setCurrentRule(ruleCopy);
|
||||
toast.info(`"${rule.ruleName}" 규칙을 불러왔습니다`);
|
||||
}, []);
|
||||
|
||||
const handleDeleteSavedRule = useCallback(
|
||||
async (ruleId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await deleteNumberingRuleFromTest(ruleId);
|
||||
|
||||
if (response.success) {
|
||||
setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId));
|
||||
|
||||
if (selectedRuleId === ruleId) {
|
||||
setSelectedRuleId(null);
|
||||
setCurrentRule(null);
|
||||
}
|
||||
|
||||
toast.success("규칙이 삭제되었습니다");
|
||||
} else {
|
||||
showErrorToast("채번 규칙 삭제에 실패했습니다", response.error, { guidance: "잠시 후 다시 시도해 주세요." });
|
||||
}
|
||||
} catch (error: any) {
|
||||
showErrorToast("채번 규칙 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[selectedRuleId],
|
||||
);
|
||||
|
||||
const handleNewRule = useCallback(() => {
|
||||
console.log("📋 새 규칙 생성:", { currentTableName, menuObjid });
|
||||
|
||||
const newRule: NumberingRuleConfig = {
|
||||
ruleId: `rule-${Date.now()}`,
|
||||
ruleName: "새 채번 규칙",
|
||||
parts: [],
|
||||
separator: "-",
|
||||
resetPeriod: "none",
|
||||
currentSequence: 1,
|
||||
scopeType: "table", // ⚠️ 임시: DB 제약 조건 때문에 table 유지
|
||||
tableName: currentTableName || "", // 현재 화면의 테이블명 자동 설정
|
||||
menuObjid: menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
|
||||
};
|
||||
|
||||
console.log("📋 생성된 규칙 정보:", newRule);
|
||||
|
||||
setSelectedRuleId(newRule.ruleId);
|
||||
setCurrentRule(newRule);
|
||||
|
||||
toast.success("새 규칙이 생성되었습니다");
|
||||
}, [currentTableName, menuObjid]);
|
||||
}, [currentRule, onSave, selectedColumn]);
|
||||
|
||||
return (
|
||||
<div className={`flex h-full gap-4 ${className}`}>
|
||||
{/* 좌측: 저장된 규칙 목록 */}
|
||||
<div className="flex w-80 flex-shrink-0 flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{editingLeftTitle ? (
|
||||
<Input
|
||||
value={leftTitle}
|
||||
onChange={(e) => setLeftTitle(e.target.value)}
|
||||
onBlur={() => setEditingLeftTitle(false)}
|
||||
onKeyDown={(e) => e.key === "Enter" && setEditingLeftTitle(false)}
|
||||
className="h-8 text-sm font-semibold"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<h2 className="text-sm font-semibold sm:text-base">{leftTitle}</h2>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditingLeftTitle(true)}>
|
||||
<Edit2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* 좌측: 채번 컬럼 목록 (카테고리 패턴) */}
|
||||
<div className="flex w-72 flex-shrink-0 flex-col gap-3">
|
||||
<h2 className="text-sm font-semibold sm:text-base">채번 컬럼</h2>
|
||||
|
||||
<Button onClick={handleNewRule} variant="outline" className="h-9 w-full text-sm">
|
||||
<Plus className="mr-2 h-4 w-4" />새 규칙 생성
|
||||
</Button>
|
||||
<Input
|
||||
value={columnSearch}
|
||||
onChange={(e) => setColumnSearch(e.target.value)}
|
||||
placeholder="검색..."
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
|
||||
<div className="flex-1 space-y-2 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex-1 space-y-1 overflow-y-auto">
|
||||
{loading && numberingColumns.length === 0 ? (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<p className="text-muted-foreground text-xs">로딩 중...</p>
|
||||
</div>
|
||||
) : savedRules.length === 0 ? (
|
||||
) : filteredGroups.length === 0 ? (
|
||||
<div className="border-border bg-muted/50 flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||
<p className="text-muted-foreground text-xs">저장된 규칙이 없습니다</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{numberingColumns.length === 0
|
||||
? "채번 타입 컬럼이 없습니다"
|
||||
: "검색 결과가 없습니다"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
savedRules.map((rule) => (
|
||||
<Card
|
||||
key={rule.ruleId}
|
||||
className={`border-border hover:bg-accent cursor-pointer py-2 transition-colors ${
|
||||
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
|
||||
}`}
|
||||
onClick={() => handleSelectRule(rule)}
|
||||
>
|
||||
<CardHeader className="px-3 py-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteSavedRule(rule.ruleId);
|
||||
}}
|
||||
filteredGroups.map(([tableName, group]) => (
|
||||
<div key={tableName} className="mb-2">
|
||||
<div className="text-muted-foreground mb-1 flex items-center gap-1 px-1 text-[11px] font-medium">
|
||||
<FolderTree className="h-3 w-3" />
|
||||
<span>{group.tableLabel}</span>
|
||||
<span className="text-muted-foreground/60">({group.columns.length})</span>
|
||||
</div>
|
||||
{group.columns.map((col) => {
|
||||
const isSelected =
|
||||
selectedColumn?.tableName === col.tableName &&
|
||||
selectedColumn?.columnName === col.columnName;
|
||||
return (
|
||||
<div
|
||||
key={`${col.tableName}.${col.columnName}`}
|
||||
className={cn(
|
||||
"cursor-pointer rounded-md px-3 py-1.5 text-xs transition-colors",
|
||||
isSelected
|
||||
? "bg-primary/10 text-primary border-primary border font-medium"
|
||||
: "hover:bg-accent"
|
||||
)}
|
||||
onClick={() => handleSelectColumn(col.tableName, col.columnName)}
|
||||
>
|
||||
<Trash2 className="text-destructive h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
{col.columnLabel}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
@@ -557,8 +392,9 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
{!currentRule ? (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground mb-2 text-lg font-medium">규칙을 선택해주세요</p>
|
||||
<p className="text-muted-foreground text-sm">좌측에서 규칙을 선택하거나 새로 생성하세요</p>
|
||||
<FolderTree className="text-muted-foreground mx-auto mb-3 h-10 w-10" />
|
||||
<p className="text-muted-foreground mb-2 text-lg font-medium">컬럼을 선택해주세요</p>
|
||||
<p className="text-muted-foreground text-sm">좌측에서 채번 컬럼을 선택하면 규칙을 편집할 수 있습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user