diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index 8a9f6b56..a3887ab8 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -405,6 +405,30 @@ router.post( } ); +// 테이블+컬럼 기반 채번 규칙 조회 (메인 API) +router.get( + "/by-column/:tableName/:columnName", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { tableName, columnName } = req.params; + + try { + const rule = await numberingRuleService.getNumberingRuleByColumn( + companyCode, + tableName, + columnName + ); + return res.json({ success: true, data: rule }); + } catch (error: any) { + logger.error("테이블+컬럼 기반 채번 규칙 조회 실패", { + error: error.message, + }); + return res.status(500).json({ success: false, error: error.message }); + } + } +); + // ==================== 테스트 테이블용 API ==================== // [테스트] 테스트 테이블에서 채번 규칙 목록 조회 diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index b8436176..0c35fdbd 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -3019,3 +3019,72 @@ export async function toggleColumnUnique( }); } } + +/** + * 회사별 채번 타입 컬럼 조회 (카테고리 패턴과 동일) + * + * @route GET /api/table-management/numbering-columns + */ +export async function getNumberingColumnsByCompany( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const companyCode = req.user?.companyCode; + + logger.info("회사별 채번 컬럼 조회 요청", { companyCode }); + + if (!companyCode) { + res.status(400).json({ + success: false, + message: "회사 코드를 확인할 수 없습니다.", + }); + return; + } + + const { getPool } = await import("../database/db"); + const pool = getPool(); + + const targetCompanyCode = companyCode === "*" ? "*" : companyCode; + + const columnsQuery = ` + SELECT DISTINCT + ttc.table_name AS "tableName", + COALESCE( + tl.table_label, + initcap(replace(ttc.table_name, '_', ' ')) + ) AS "tableLabel", + ttc.column_name AS "columnName", + COALESCE( + ttc.column_label, + initcap(replace(ttc.column_name, '_', ' ')) + ) AS "columnLabel", + ttc.input_type AS "inputType" + FROM table_type_columns ttc + LEFT JOIN table_labels tl + ON ttc.table_name = tl.table_name + WHERE ttc.input_type = 'numbering' + AND ttc.company_code = $1 + ORDER BY ttc.table_name, ttc.column_name + `; + + const columnsResult = await pool.query(columnsQuery, [targetCompanyCode]); + + logger.info("채번 컬럼 조회 완료", { + companyCode, + rowCount: columnsResult.rows.length, + }); + + res.json({ + success: true, + data: columnsResult.rows, + }); + } catch (error: any) { + logger.error("채번 컬럼 조회 실패", { error: error.message }); + res.status(500).json({ + success: false, + message: "채번 컬럼 조회 중 오류가 발생했습니다.", + error: error.message, + }); + } +} diff --git a/backend-node/src/routes/tableManagementRoutes.ts b/backend-node/src/routes/tableManagementRoutes.ts index a8964e99..92449cf6 100644 --- a/backend-node/src/routes/tableManagementRoutes.ts +++ b/backend-node/src/routes/tableManagementRoutes.ts @@ -25,6 +25,7 @@ import { toggleLogTable, getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회 getCategoryColumnsByCompany, // 🆕 회사별 카테고리 컬럼 조회 + getNumberingColumnsByCompany, // 채번 타입 컬럼 조회 multiTableSave, // 🆕 범용 다중 테이블 저장 getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회 getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회 @@ -254,6 +255,12 @@ router.post("/tables/:tableName/log/toggle", toggleLogTable); */ router.get("/category-columns", getCategoryColumnsByCompany); +/** + * 회사 기준 모든 채번 타입 컬럼 조회 + * GET /api/table-management/numbering-columns + */ +router.get("/numbering-columns", getNumberingColumnsByCompany); + /** * 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회 * GET /api/table-management/menu/:menuObjid/category-columns diff --git a/backend-node/src/services/masterDetailExcelService.ts b/backend-node/src/services/masterDetailExcelService.ts index 40cd58e3..a0370ed6 100644 --- a/backend-node/src/services/masterDetailExcelService.ts +++ b/backend-node/src/services/masterDetailExcelService.ts @@ -494,7 +494,7 @@ class MasterDetailExcelService { /** * 특정 테이블의 특정 컬럼이 채번 타입인지 확인하고, 채번 규칙 ID를 반환 - * 회사별 설정을 우선 조회하고, 없으면 공통(*) 설정으로 fallback + * numbering_rules 테이블에서 table_name + column_name + company_code로 직접 조회 */ private async detectNumberingRuleForColumn( tableName: string, @@ -502,32 +502,58 @@ class MasterDetailExcelService { companyCode?: string ): Promise<{ numberingRuleId: string } | null> { try { - // 회사별 설정 우선, 공통 설정 fallback (company_code DESC로 회사별이 먼저) + // 1. table_type_columns에서 numbering 타입인지 확인 const companyCondition = companyCode && companyCode !== "*" ? `AND company_code IN ($3, '*')` : `AND company_code = '*'`; - const params = companyCode && companyCode !== "*" + const ttcParams = companyCode && companyCode !== "*" ? [tableName, columnName, companyCode] : [tableName, columnName]; - const result = await query( - `SELECT input_type, detail_settings, company_code - FROM table_type_columns + const ttcResult = await query( + `SELECT input_type FROM table_type_columns WHERE table_name = $1 AND column_name = $2 ${companyCondition} - ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, - params + AND input_type = 'numbering' LIMIT 1`, + ttcParams ); - // 채번 타입인 행 찾기 (회사별 우선) - for (const row of result) { - if (row.input_type === "numbering") { - const settings = typeof row.detail_settings === "string" - ? JSON.parse(row.detail_settings || "{}") - : row.detail_settings; - - if (settings?.numberingRuleId) { - return { numberingRuleId: settings.numberingRuleId }; - } + if (ttcResult.length === 0) return null; + + // 2. numbering_rules에서 table_name + column_name으로 규칙 조회 + const ruleCompanyCondition = companyCode && companyCode !== "*" + ? `AND company_code IN ($3, '*')` + : `AND company_code = '*'`; + const ruleParams = companyCode && companyCode !== "*" + ? [tableName, columnName, companyCode] + : [tableName, columnName]; + + const ruleResult = await query( + `SELECT rule_id FROM numbering_rules + WHERE table_name = $1 AND column_name = $2 ${ruleCompanyCondition} + ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END + LIMIT 1`, + ruleParams + ); + + if (ruleResult.length > 0) { + return { numberingRuleId: ruleResult[0].rule_id }; + } + + // 3. fallback: detail_settings.numberingRuleId (하위 호환) + const fallbackResult = await query( + `SELECT detail_settings FROM table_type_columns + WHERE table_name = $1 AND column_name = $2 ${companyCondition} + AND input_type = 'numbering' + ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, + ttcParams + ); + + for (const row of fallbackResult) { + const settings = typeof row.detail_settings === "string" + ? JSON.parse(row.detail_settings || "{}") + : row.detail_settings; + if (settings?.numberingRuleId) { + return { numberingRuleId: settings.numberingRuleId }; } } @@ -540,7 +566,7 @@ class MasterDetailExcelService { /** * 특정 테이블의 모든 채번 컬럼을 한 번에 조회 - * 회사별 설정 우선, 공통(*) 설정 fallback + * numbering_rules 테이블에서 table_name + column_name으로 직접 조회 * @returns Map */ private async detectAllNumberingColumns( @@ -549,6 +575,7 @@ class MasterDetailExcelService { ): Promise> { const numberingCols = new Map(); try { + // 1. table_type_columns에서 numbering 타입 컬럼 목록 조회 const companyCondition = companyCode && companyCode !== "*" ? `AND company_code IN ($2, '*')` : `AND company_code = '*'`; @@ -556,22 +583,26 @@ class MasterDetailExcelService { ? [tableName, companyCode] : [tableName]; - const result = await query( - `SELECT column_name, detail_settings, company_code - FROM table_type_columns - WHERE table_name = $1 AND input_type = 'numbering' ${companyCondition} - ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, + const ttcResult = await query( + `SELECT DISTINCT column_name FROM table_type_columns + WHERE table_name = $1 AND input_type = 'numbering' ${companyCondition}`, params ); - // 컬럼별로 회사 설정 우선 적용 - for (const row of result) { - if (numberingCols.has(row.column_name)) continue; // 이미 회사별 설정이 있으면 스킵 - const settings = typeof row.detail_settings === "string" - ? JSON.parse(row.detail_settings || "{}") - : row.detail_settings; - if (settings?.numberingRuleId) { - numberingCols.set(row.column_name, settings.numberingRuleId); + // 2. 각 컬럼에 대해 numbering_rules에서 규칙 조회 + for (const row of ttcResult) { + const ruleResult = await query( + `SELECT rule_id FROM numbering_rules + WHERE table_name = $1 AND column_name = $2 ${companyCondition} + ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END + LIMIT 1`, + companyCode && companyCode !== "*" + ? [tableName, row.column_name, companyCode] + : [tableName, row.column_name] + ); + + if (ruleResult.length > 0) { + numberingCols.set(row.column_name, ruleResult[0].rule_id); } } diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 6f6fe81c..34acc44f 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -1747,7 +1747,53 @@ class NumberingRuleService { `; const params = [companyCode, tableName, columnName]; - const result = await pool.query(query, params); + let result = await pool.query(query, params); + + // fallback: column_name이 비어있는 레거시 규칙 검색 + if (result.rows.length === 0) { + const fallbackQuery = ` + SELECT + r.rule_id AS "ruleId", + r.rule_name AS "ruleName", + r.description, + r.separator, + r.reset_period AS "resetPeriod", + r.current_sequence AS "currentSequence", + r.table_name AS "tableName", + r.column_name AS "columnName", + r.company_code AS "companyCode", + r.category_column AS "categoryColumn", + r.category_value_id AS "categoryValueId", + cv.value_label AS "categoryValueLabel", + r.created_at AS "createdAt", + r.updated_at AS "updatedAt", + r.created_by AS "createdBy" + FROM numbering_rules r + LEFT JOIN category_values cv ON r.category_value_id = cv.value_id + WHERE r.company_code = $1 + AND r.table_name = $2 + AND (r.column_name IS NULL OR r.column_name = '') + AND r.category_value_id IS NULL + ORDER BY r.updated_at DESC + LIMIT 1 + `; + result = await pool.query(fallbackQuery, [companyCode, tableName]); + + // 찾으면 column_name 자동 업데이트 (레거시 데이터 마이그레이션) + if (result.rows.length > 0) { + const foundRule = result.rows[0]; + await pool.query( + `UPDATE numbering_rules SET column_name = $1 WHERE rule_id = $2 AND company_code = $3`, + [columnName, foundRule.ruleId, companyCode] + ); + result.rows[0].columnName = columnName; + logger.info("레거시 채번 규칙 자동 매핑 완료", { + ruleId: foundRule.ruleId, + tableName, + columnName, + }); + } + } if (result.rows.length === 0) { logger.info("테이블+컬럼 기반 채번 규칙을 찾을 수 없음", { @@ -1760,7 +1806,6 @@ class NumberingRuleService { const rule = result.rows[0]; - // 파트 정보 조회 (테스트 테이블) const partsQuery = ` SELECT id, @@ -1779,7 +1824,7 @@ class NumberingRuleService { ]); rule.parts = extractSeparatorAfterFromParts(partsResult.rows); - logger.info("테이블+컬럼 기반 채번 규칙 조회 성공 (테스트)", { + logger.info("테이블+컬럼 기반 채번 규칙 조회 성공", { ruleId: rule.ruleId, ruleName: rule.ruleName, }); diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index a8d58662..ec6aabae 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -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 = {}; - 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 = {}; - 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 = {}; @@ -1987,118 +1933,7 @@ export default function TableManagementPage() { )} )} - {/* 입력 타입이 'numbering'인 경우 채번규칙 선택 */} - {column.inputType === "numbering" && ( -
- - - setNumberingComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: open, - })) - } - > - - - - - - - - - 채번규칙을 찾을 수 없습니다. - - - { - const columnIndex = columns.findIndex( - (c) => c.columnName === column.columnName, - ); - handleColumnChange(columnIndex, "numberingRuleId", undefined); - setNumberingComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: false, - })); - // 자동 저장 제거 - 전체 저장 버튼으로 저장 - }} - className="text-xs" - > - - -- 선택 안함 -- - - {numberingRules.map((rule) => ( - { - const columnIndex = columns.findIndex( - (c) => c.columnName === column.columnName, - ); - // 상태 업데이트만 (자동 저장 제거) - handleColumnChange(columnIndex, "numberingRuleId", rule.ruleId); - setNumberingComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: false, - })); - // 전체 저장 버튼으로 저장 - }} - className="text-xs" - > - -
- {rule.ruleName} - {rule.tableName && ( - - {rule.tableName}.{rule.columnName} - - )} -
-
- ))} -
-
-
-
-
- {column.numberingRuleId && ( -
- - 규칙 설정됨 -
- )} -
- )} + {/* 채번 타입은 옵션설정 > 채번설정에서 관리 (별도 선택 불필요) */}
diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index fbae903f..377869cc 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -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 = ({ currentTableName, menuObjid, }) => { - const [savedRules, setSavedRules] = useState([]); - const [selectedRuleId, setSelectedRuleId] = useState(null); + const [numberingColumns, setNumberingColumns] = useState([]); + const [selectedColumn, setSelectedColumn] = useState<{ tableName: string; columnName: string } | null>(null); const [currentRule, setCurrentRule] = useState(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>({}); const [customSeparators, setCustomSeparators] = useState>({}); - // 카테고리 조건 관련 상태 - 모든 카테고리를 테이블.컬럼 단위로 조회 - interface CategoryOption { - tableName: string; - columnName: string; - displayName: string; // "테이블명.컬럼명" 형식 - } - const [allCategoryOptions, setAllCategoryOptions] = useState([]); - const [selectedCategoryKey, setSelectedCategoryKey] = useState(""); // "tableName.columnName" - const [categoryValues, setCategoryValues] = useState([]); - 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>((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 = ({ 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 = ({ } 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 (
- {/* 좌측: 저장된 규칙 목록 */} -
-
- {editingLeftTitle ? ( - setLeftTitle(e.target.value)} - onBlur={() => setEditingLeftTitle(false)} - onKeyDown={(e) => e.key === "Enter" && setEditingLeftTitle(false)} - className="h-8 text-sm font-semibold" - autoFocus - /> - ) : ( -

{leftTitle}

- )} - -
+ {/* 좌측: 채번 컬럼 목록 (카테고리 패턴) */} +
+

채번 컬럼

- + setColumnSearch(e.target.value)} + placeholder="검색..." + className="h-8 text-xs" + /> -
- {loading ? ( +
+ {loading && numberingColumns.length === 0 ? (

로딩 중...

- ) : savedRules.length === 0 ? ( + ) : filteredGroups.length === 0 ? (
-

저장된 규칙이 없습니다

+

+ {numberingColumns.length === 0 + ? "채번 타입 컬럼이 없습니다" + : "검색 결과가 없습니다"} +

) : ( - savedRules.map((rule) => ( - handleSelectRule(rule)} - > - -
-
- {rule.ruleName} -
- -
-
-
+ {col.columnLabel} +
+ ); + })} +
)) )}
@@ -557,8 +392,9 @@ export const NumberingRuleDesigner: React.FC = ({ {!currentRule ? (
-

규칙을 선택해주세요

-

좌측에서 규칙을 선택하거나 새로 생성하세요

+ +

컬럼을 선택해주세요

+

좌측에서 채번 컬럼을 선택하면 규칙을 편집할 수 있습니다

) : (