From 1753822211f88a820784371e7d998d37d304fc15 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 26 Jan 2026 16:32:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EC=97=90=EC=84=9C=20=EC=B1=84=EB=B2=88=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=9A=8C=EC=82=AC?= =?UTF-8?q?=EB=B3=84=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 테이블에서 채번 규칙 목록을 조회하는 API를 추가하였습니다. 이 API는 회사 코드와 선택적 메뉴 OBJID를 기반으로 규칙을 반환합니다. - 회사별 카테고리 컬럼을 조회하는 API를 추가하여, 회사 코드에 따라 카테고리 컬럼을 필터링하여 반환하도록 개선하였습니다. - 관련된 서비스 및 라우터를 업데이트하여 새로운 기능을 통합하였습니다. --- .../controllers/numberingRuleController.ts | 17 ++ .../controllers/tableManagementController.ts | 276 ++++++++++-------- .../src/routes/tableManagementRoutes.ts | 7 + .../src/services/numberingRuleService.ts | 103 +++++++ .../numbering-rule/NumberingRuleDesigner.tsx | 9 +- .../components/screen/toolbar/SlimToolbar.tsx | 214 +++++++------- .../table-category/CategoryColumnList.tsx | 71 ++++- .../UnifiedRepeaterConfigPanel.tsx | 104 ++++--- frontend/lib/api/numberingRule.ts | 24 +- 9 files changed, 552 insertions(+), 273 deletions(-) diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index 9a53a1cb..f5cbc91a 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -269,6 +269,23 @@ router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedReques // ==================== 테스트 테이블용 API ==================== +// [테스트] 테스트 테이블에서 채번 규칙 목록 조회 +router.get("/test/list/:menuObjid?", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined; + + logger.info("[테스트] 채번 규칙 목록 조회 요청", { companyCode, menuObjid }); + + try { + const rules = await numberingRuleService.getRulesFromTest(companyCode, menuObjid); + logger.info("[테스트] 채번 규칙 목록 조회 성공", { companyCode, menuObjid, count: rules.length }); + return res.json({ success: true, data: rules }); + } catch (error: any) { + logger.error("[테스트] 채번 규칙 목록 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } +}); + // [테스트] 테이블+컬럼 기반 채번 규칙 조회 router.get("/test/by-column/:tableName/:columnName", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { const companyCode = req.user!.companyCode; diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index db301ec8..9b282c41 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -1642,6 +1642,113 @@ export async function toggleLogTable( } } +/** + * 회사별 카테고리 컬럼 조회 (메뉴 종속 없음) + * + * @route GET /api/table-management/category-columns + * @description table_type_columns에서 회사 코드 기준으로 input_type = 'category'인 컬럼을 조회 + */ +export async function getCategoryColumnsByCompany( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const companyCode = req.user?.companyCode; + + logger.info("📥 회사별 카테고리 컬럼 조회 요청", { companyCode }); + + if (!companyCode) { + logger.error("❌ 회사 코드가 없습니다", { user: req.user }); + res.status(400).json({ + success: false, + message: "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요.", + }); + return; + } + + const { getPool } = await import("../database/db"); + const pool = getPool(); + + let columnsResult; + + // 최고 관리자인 경우 company_code = '*'인 카테고리 컬럼 조회 + if (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( + cl.column_label, + initcap(replace(ttc.column_name, '_', ' ')) + ) AS "columnLabel", + ttc.input_type AS "inputType" + FROM table_type_columns ttc + LEFT JOIN column_labels cl + ON ttc.table_name = cl.table_name + AND ttc.column_name = cl.column_name + LEFT JOIN table_labels tl + ON ttc.table_name = tl.table_name + WHERE ttc.input_type = 'category' + AND ttc.company_code = '*' + ORDER BY ttc.table_name, ttc.column_name + `; + + columnsResult = await pool.query(columnsQuery); + logger.info("✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료", { + rowCount: columnsResult.rows.length + }); + } else { + // 일반 회사: 해당 회사의 카테고리 컬럼만 조회 + 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( + cl.column_label, + initcap(replace(ttc.column_name, '_', ' ')) + ) AS "columnLabel", + ttc.input_type AS "inputType" + FROM table_type_columns ttc + LEFT JOIN column_labels cl + ON ttc.table_name = cl.table_name + AND ttc.column_name = cl.column_name + LEFT JOIN table_labels tl + ON ttc.table_name = tl.table_name + WHERE ttc.input_type = 'category' + AND ttc.company_code = $1 + ORDER BY ttc.table_name, ttc.column_name + `; + + columnsResult = await pool.query(columnsQuery, [companyCode]); + logger.info("✅ 회사별 카테고리 컬럼 조회 완료", { + companyCode, + rowCount: columnsResult.rows.length + }); + } + + res.json({ + success: true, + data: columnsResult.rows, + message: "카테고리 컬럼 조회 성공", + }); + } catch (error: any) { + logger.error("❌ 회사별 카테고리 컬럼 조회 실패", { error: error.message }); + res.status(500).json({ + success: false, + message: "카테고리 컬럼 조회 중 오류가 발생했습니다.", + error: error.message, + }); + } +} + /** * 메뉴의 상위 메뉴들이 설정한 모든 카테고리 타입 컬럼 조회 (계층 구조 상속) * @@ -1670,125 +1777,28 @@ export async function getCategoryColumnsByMenu( return; } + if (!companyCode) { + logger.error("❌ 회사 코드가 없습니다", { menuObjid, user: req.user }); + res.status(400).json({ + success: false, + message: "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요.", + }); + return; + } + const { getPool } = await import("../database/db"); const pool = getPool(); - // 1. category_column_mapping 테이블 존재 여부 확인 - const tableExistsResult = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'category_column_mapping' - ) as table_exists - `); - const mappingTableExists = tableExistsResult.rows[0]?.table_exists === true; + // 🆕 table_type_columns에서 직접 input_type = 'category'인 컬럼들을 조회 + // category_column_mapping 대신 table_type_columns 기준으로 조회 + logger.info("🔍 table_type_columns 기반 카테고리 컬럼 조회", { menuObjid, companyCode }); let columnsResult; - - if (mappingTableExists) { - // 🆕 category_column_mapping을 사용한 계층 구조 기반 조회 - logger.info("🔍 category_column_mapping 기반 카테고리 컬럼 조회 (계층 구조 상속)", { menuObjid, companyCode }); - - // 현재 메뉴와 모든 상위 메뉴의 objid 조회 (재귀) - const ancestorMenuQuery = ` - WITH RECURSIVE menu_hierarchy AS ( - -- 현재 메뉴 - SELECT objid, parent_obj_id, menu_type, menu_name_kor - FROM menu_info - WHERE objid = $1 - - UNION ALL - - -- 부모 메뉴 재귀 조회 - SELECT m.objid, m.parent_obj_id, m.menu_type, m.menu_name_kor - FROM menu_info m - INNER JOIN menu_hierarchy mh ON m.objid = mh.parent_obj_id - WHERE m.parent_obj_id != 0 -- 최상위 메뉴(parent_obj_id=0) 제외 - ) - SELECT - ARRAY_AGG(objid) as menu_objids, - ARRAY_AGG(menu_name_kor) as menu_names - FROM menu_hierarchy - `; - - const ancestorMenuResult = await pool.query(ancestorMenuQuery, [parseInt(menuObjid)]); - const ancestorMenuObjids = ancestorMenuResult.rows[0]?.menu_objids || [parseInt(menuObjid)]; - const ancestorMenuNames = ancestorMenuResult.rows[0]?.menu_names || []; - - logger.info("✅ 상위 메뉴 계층 조회 완료", { - ancestorMenuObjids, - ancestorMenuNames, - hierarchyDepth: ancestorMenuObjids.length - }); - - // 상위 메뉴들에 설정된 모든 카테고리 컬럼 조회 (테이블 필터링 제거) + + // 최고 관리자인 경우 모든 회사의 카테고리 컬럼 조회 + if (companyCode === "*") { const columnsQuery = ` SELECT DISTINCT - ttc.table_name AS "tableName", - COALESCE( - tl.table_label, - initcap(replace(ttc.table_name, '_', ' ')) - ) AS "tableLabel", - ccm.logical_column_name AS "columnName", - COALESCE( - cl.column_label, - initcap(replace(ccm.logical_column_name, '_', ' ')) - ) AS "columnLabel", - ttc.input_type AS "inputType", - ccm.menu_objid AS "definedAtMenuObjid" - FROM category_column_mapping ccm - INNER JOIN table_type_columns ttc - ON ccm.table_name = ttc.table_name - AND ccm.physical_column_name = ttc.column_name - LEFT JOIN column_labels cl - ON ttc.table_name = cl.table_name - AND ttc.column_name = cl.column_name - LEFT JOIN table_labels tl - ON ttc.table_name = tl.table_name - WHERE ccm.company_code = $1 - AND ccm.menu_objid = ANY($2) - AND ttc.input_type = 'category' - ORDER BY ttc.table_name, ccm.logical_column_name - `; - - columnsResult = await pool.query(columnsQuery, [companyCode, ancestorMenuObjids]); - logger.info("✅ category_column_mapping 기반 조회 완료 (계층 구조 상속)", { - rowCount: columnsResult.rows.length, - columns: columnsResult.rows.map((r: any) => `${r.tableName}.${r.columnName}`) - }); - } else { - // 🔄 레거시 방식: 형제 메뉴들의 테이블에서 모든 카테고리 컬럼 조회 - logger.info("🔍 레거시 방식: 형제 메뉴 테이블 기반 카테고리 컬럼 조회", { menuObjid, companyCode }); - - // 형제 메뉴 조회 - const { getSiblingMenuObjids } = await import("../services/menuService"); - const siblingObjids = await getSiblingMenuObjids(parseInt(menuObjid)); - - // 형제 메뉴들이 사용하는 테이블 조회 - const tablesQuery = ` - SELECT DISTINCT sd.table_name - FROM screen_menu_assignments sma - INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id - WHERE sma.menu_objid = ANY($1) - AND sma.company_code = $2 - AND sd.table_name IS NOT NULL - `; - - const tablesResult = await pool.query(tablesQuery, [siblingObjids, companyCode]); - const tableNames = tablesResult.rows.map((row: any) => row.table_name); - - logger.info("✅ 형제 메뉴 테이블 조회 완료", { tableNames, count: tableNames.length }); - - if (tableNames.length === 0) { - res.json({ - success: true, - data: [], - message: "형제 메뉴에 연결된 테이블이 없습니다.", - }); - return; - } - - const columnsQuery = ` - SELECT ttc.table_name AS "tableName", COALESCE( tl.table_label, @@ -1806,14 +1816,46 @@ export async function getCategoryColumnsByMenu( AND ttc.column_name = cl.column_name LEFT JOIN table_labels tl ON ttc.table_name = tl.table_name - WHERE ttc.table_name = ANY($1) - AND ttc.company_code = $2 - AND ttc.input_type = 'category' + WHERE ttc.input_type = 'category' + AND ttc.company_code = '*' ORDER BY ttc.table_name, ttc.column_name `; - columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]); - logger.info("✅ 레거시 방식 조회 완료", { rowCount: columnsResult.rows.length }); + columnsResult = await pool.query(columnsQuery); + logger.info("✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료", { + rowCount: columnsResult.rows.length + }); + } else { + // 일반 회사: 해당 회사의 카테고리 컬럼만 조회 + 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( + cl.column_label, + initcap(replace(ttc.column_name, '_', ' ')) + ) AS "columnLabel", + ttc.input_type AS "inputType" + FROM table_type_columns ttc + LEFT JOIN column_labels cl + ON ttc.table_name = cl.table_name + AND ttc.column_name = cl.column_name + LEFT JOIN table_labels tl + ON ttc.table_name = tl.table_name + WHERE ttc.input_type = 'category' + AND ttc.company_code = $1 + ORDER BY ttc.table_name, ttc.column_name + `; + + columnsResult = await pool.query(columnsQuery, [companyCode]); + logger.info("✅ 회사별 카테고리 컬럼 조회 완료", { + companyCode, + rowCount: columnsResult.rows.length + }); } logger.info("✅ 카테고리 컬럼 조회 완료", { diff --git a/backend-node/src/routes/tableManagementRoutes.ts b/backend-node/src/routes/tableManagementRoutes.ts index 1af6d87d..59b977e4 100644 --- a/backend-node/src/routes/tableManagementRoutes.ts +++ b/backend-node/src/routes/tableManagementRoutes.ts @@ -24,6 +24,7 @@ import { getLogData, toggleLogTable, getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회 + getCategoryColumnsByCompany, // 🆕 회사별 카테고리 컬럼 조회 multiTableSave, // 🆕 범용 다중 테이블 저장 getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회 getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회 @@ -212,6 +213,12 @@ router.post("/tables/:tableName/log/toggle", toggleLogTable); // 메뉴 기반 카테고리 관리 API // ======================================== +/** + * 회사 기준 모든 카테고리 타입 컬럼 조회 (메뉴 종속 없음) + * GET /api/table-management/category-columns + */ +router.get("/category-columns", getCategoryColumnsByCompany); + /** * 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회 * GET /api/table-management/menu/:menuObjid/category-columns diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 8ff7a2e7..397a402d 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -1196,6 +1196,109 @@ class NumberingRuleService { logger.info("시퀀스 초기화 완료", { ruleId, companyCode }); } + /** + * [테스트] 테스트 테이블에서 채번 규칙 목록 조회 + * numbering_rules_test 테이블 사용 + */ + async getRulesFromTest( + companyCode: string, + menuObjid?: number + ): Promise { + try { + logger.info("[테스트] 채번 규칙 목록 조회 시작", { companyCode, menuObjid }); + + const pool = getPool(); + + // 멀티테넌시: 최고 관리자 vs 일반 회사 + let query: string; + let params: any[]; + + if (companyCode === "*") { + // 최고 관리자: 모든 규칙 조회 + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules_test + ORDER BY created_at DESC + `; + params = []; + } else { + // 일반 회사: 자신의 규칙만 조회 + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules_test + WHERE company_code = $1 + ORDER BY created_at DESC + `; + params = [companyCode]; + } + + const result = await pool.query(query, params); + + // 각 규칙의 파트 정보 조회 + for (const rule of result.rows) { + const partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts_test + WHERE rule_id = $1 AND company_code = $2 + ORDER BY part_order + `; + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode === "*" ? rule.companyCode : companyCode, + ]); + rule.parts = partsResult.rows; + } + + logger.info("[테스트] 채번 규칙 목록 조회 완료", { + companyCode, + menuObjid, + count: result.rows.length, + }); + + return result.rows; + } catch (error: any) { + logger.error("[테스트] 채번 규칙 목록 조회 실패", { + error: error.message, + stack: error.stack, + }); + throw error; + } + } + /** * [테스트] 테이블명 + 컬럼명 기반으로 채번규칙 조회 (menu_objid 없이) * numbering_rules_test 테이블 사용 diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 60bbd247..57e4896b 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -12,9 +12,9 @@ import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorTyp import { NumberingRuleCard } from "./NumberingRuleCard"; import { NumberingRulePreview } from "./NumberingRulePreview"; import { - getAvailableNumberingRules, saveNumberingRuleToTest, deleteNumberingRuleFromTest, + getNumberingRulesFromTest, } from "@/lib/api/numberingRule"; import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -159,14 +159,15 @@ export const NumberingRuleDesigner: React.FC = ({ const loadRules = useCallback(async () => { setLoading(true); try { - console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작:", { + console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작 (test 테이블):", { menuObjid, hasMenuObjid: !!menuObjid, }); - const response = await getAvailableNumberingRules(menuObjid); + // test 테이블에서 조회 + const response = await getNumberingRulesFromTest(menuObjid); - console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답:", { + console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답 (test 테이블):", { menuObjid, success: response.success, rulesCount: response.data?.length || 0, diff --git a/frontend/components/screen/toolbar/SlimToolbar.tsx b/frontend/components/screen/toolbar/SlimToolbar.tsx index 5f5b32d9..d71ed93a 100644 --- a/frontend/components/screen/toolbar/SlimToolbar.tsx +++ b/frontend/components/screen/toolbar/SlimToolbar.tsx @@ -33,10 +33,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; interface GridSettings { columns: number; @@ -173,112 +175,118 @@ export const SlimToolbar: React.FC = ({ {screenResolution && ( <>
- - - - - - {onResolutionChange && ( - - 데스크톱 - {SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => ( - onResolutionChange(resolution)} - className="flex items-center space-x-2" - > - - {resolution.name} - - {resolution.width}×{resolution.height} - - - ))} - - 태블릿 - {SCREEN_RESOLUTIONS.filter((r) => r.category === "tablet").map((resolution) => ( - onResolutionChange(resolution)} - className="flex items-center space-x-2" - > - - {resolution.name} - - {resolution.width}×{resolution.height} - - - ))} - - 모바일 - {SCREEN_RESOLUTIONS.filter((r) => r.category === "mobile").map((resolution) => ( - onResolutionChange(resolution)} - className="flex items-center space-x-2" - > - - {resolution.name} - - {resolution.width}×{resolution.height} - - - ))} - - 사용자 정의 + + + + + {onResolutionChange && ( + + 데스크톱 + {SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => ( { - e.preventDefault(); - setCustomWidth(screenResolution.width.toString()); - setCustomHeight(screenResolution.height.toString()); - setShowCustomInput(true); - }} + key={resolution.name} + onClick={() => onResolutionChange(resolution)} className="flex items-center space-x-2" > - - 사용자 정의... + + {resolution.name} + + {resolution.width}×{resolution.height} + - - )} - - -
-
사용자 정의 해상도
-
-
- - setCustomWidth(e.target.value)} - placeholder="1920" - className="h-8 text-xs" - /> -
-
- - setCustomHeight(e.target.value)} - placeholder="1080" - className="h-8 text-xs" - /> -
+ ))} + + 태블릿 + {SCREEN_RESOLUTIONS.filter((r) => r.category === "tablet").map((resolution) => ( + onResolutionChange(resolution)} + className="flex items-center space-x-2" + > + + {resolution.name} + + {resolution.width}×{resolution.height} + + + ))} + + 모바일 + {SCREEN_RESOLUTIONS.filter((r) => r.category === "mobile").map((resolution) => ( + onResolutionChange(resolution)} + className="flex items-center space-x-2" + > + + {resolution.name} + + {resolution.width}×{resolution.height} + + + ))} + + 사용자 정의 + { + setCustomWidth(screenResolution.width.toString()); + setCustomHeight(screenResolution.height.toString()); + setShowCustomInput(true); + }} + className="flex items-center space-x-2" + > + + 사용자 정의... + + + )} + + + {/* 사용자 정의 해상도 다이얼로그 */} + + + + 사용자 정의 해상도 + +
+
+ + setCustomWidth(e.target.value)} + placeholder="1920" + />
-
+ + + -
- - + + + )} diff --git a/frontend/components/table-category/CategoryColumnList.tsx b/frontend/components/table-category/CategoryColumnList.tsx index ac4fa960..3be70840 100644 --- a/frontend/components/table-category/CategoryColumnList.tsx +++ b/frontend/components/table-category/CategoryColumnList.tsx @@ -1,9 +1,10 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { apiClient } from "@/lib/api/client"; import { getCategoryValues } from "@/lib/api/tableCategoryValue"; -import { FolderTree, Loader2 } from "lucide-react"; +import { FolderTree, Loader2, Search, X } from "lucide-react"; +import { Input } from "@/components/ui/input"; interface CategoryColumn { tableName: string; @@ -28,17 +29,29 @@ interface CategoryColumnListProps { export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) { const [columns, setColumns] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + + // 검색어로 필터링된 컬럼 목록 + const filteredColumns = useMemo(() => { + if (!searchQuery.trim()) return columns; + + const query = searchQuery.toLowerCase(); + return columns.filter((col) => { + const columnName = (col.columnName || "").toLowerCase(); + const columnLabel = (col.columnLabel || "").toLowerCase(); + const tableName = (col.tableName || "").toLowerCase(); + const tableLabel = (col.tableLabel || "").toLowerCase(); + + return columnName.includes(query) || + columnLabel.includes(query) || + tableName.includes(query) || + tableLabel.includes(query); + }); + }, [columns, searchQuery]); useEffect(() => { - if (menuObjid) { - loadCategoryColumnsByMenu(); - } else if (tableName) { - // menuObjid가 없으면 tableName 기반으로 조회 - loadCategoryColumnsByTable(); - } else { - console.warn("⚠️ menuObjid와 tableName 모두 없어서 카테고리 컬럼을 로드할 수 없습니다"); - setColumns([]); - } + // 메뉴 종속 없이 항상 회사 기준으로 카테고리 컬럼 조회 + loadCategoryColumnsByMenu(); }, [menuObjid, tableName]); // tableName 기반으로 카테고리 컬럼 조회 @@ -126,10 +139,13 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, const loadCategoryColumnsByMenu = async () => { setIsLoading(true); try { - console.log("🔍 형제 메뉴의 카테고리 컬럼 조회 시작", { menuObjid }); + console.log("🔍 회사 기준 카테고리 컬럼 조회 시작", { menuObjid }); - // 새 API: 형제 메뉴들의 카테고리 컬럼 조회 - const response = await apiClient.get(`/table-management/menu/${menuObjid}/category-columns`); + // 회사 기준 카테고리 컬럼 조회 (menuObjid는 선택사항) + const url = menuObjid + ? `/table-management/menu/${menuObjid}/category-columns` + : `/table-management/category-columns`; + const response = await apiClient.get(url); console.log("✅ 메뉴별 카테고리 컬럼 API 응답:", { menuObjid, @@ -242,8 +258,33 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,

관리할 카테고리 컬럼을 선택하세요

+ {/* 검색 입력 필드 */} +
+ + setSearchQuery(e.target.value)} + className="h-8 pl-8 pr-8 text-xs" + /> + {searchQuery && ( + + )} +
+
- {columns.map((column) => { + {filteredColumns.length === 0 && searchQuery ? ( +
+ '{searchQuery}'에 대한 검색 결과가 없습니다 +
+ ) : null} + {filteredColumns.map((column) => { const uniqueKey = `${column.tableName}.${column.columnName}`; const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교 return ( diff --git a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx index 19be9518..eca39bd3 100644 --- a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx +++ b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx @@ -251,29 +251,57 @@ export const UnifiedRepeaterConfigPanel: React.FC { const loadRelatedTables = async () => { - if (!currentTableName) { + // 화면 메인 테이블 또는 저장 테이블 중 하나라도 있어야 함 + const baseTable = currentTableName || config.mainTableName; + if (!baseTable) { setRelatedTables([]); return; } setLoadingRelations(true); try { - // column_labels에서 현재 테이블을 reference_table로 참조하는 테이블 찾기 const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`); + const allRelations: TableRelation[] = []; - if (response.data.success && response.data.data) { - const relations: TableRelation[] = response.data.data.map((rel: any) => ({ - tableName: rel.tableName || rel.table_name, - tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name, - foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼 - referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼 - })); - setRelatedTables(relations); + // 1. 화면 메인 테이블을 참조하는 테이블 조회 (자식 테이블) + if (currentTableName) { + const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`); + if (response.data.success && response.data.data) { + const relations: TableRelation[] = response.data.data.map((rel: any) => ({ + tableName: rel.tableName || rel.table_name, + tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name, + foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼 (자식 테이블의) + referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼 (부모 테이블의) + })); + allRelations.push(...relations); + } } + + // 2. 저장 테이블이 화면 메인 테이블과 다르면, 저장 테이블을 참조하는 테이블도 조회 + if (config.mainTableName && config.mainTableName !== currentTableName) { + const response2 = await apiClient.get(`/table-management/columns/${config.mainTableName}/referenced-by`); + if (response2.data.success && response2.data.data) { + const relations2: TableRelation[] = response2.data.data.map((rel: any) => ({ + tableName: rel.tableName || rel.table_name, + tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name, + foreignKeyColumn: rel.columnName || rel.column_name, + referenceColumn: rel.referenceColumn || rel.reference_column || "id", + })); + // 중복 제거 후 추가 + relations2.forEach(rel => { + if (!allRelations.some(r => r.tableName === rel.tableName)) { + allRelations.push(rel); + } + }); + } + } + + setRelatedTables(allRelations); } catch (error) { console.error("연관 테이블 로드 실패:", error); setRelatedTables([]); @@ -282,7 +310,7 @@ export const UnifiedRepeaterConfigPanel: React.FC - {/* FK 직접 입력 (연관 테이블이 아닌 경우만) */} - {config.useCustomTable && config.mainTableName && + {/* FK 직접 입력 - 화면 메인 테이블이 있고 연관 테이블이 아닌 경우만 표시 */} + {/* 화면 메인 테이블이 없으면 FK 설정 불필요 (독립 저장) */} + {config.useCustomTable && config.mainTableName && currentTableName && !relatedTables.some(r => r.tableName === config.mainTableName) && (

- 엔티티 관계가 설정되지 않은 테이블입니다. FK 컬럼을 직접 입력하세요. + 화면 테이블({currentTableName})과의 엔티티 관계가 없습니다. FK 컬럼을 직접 입력하세요.

- + updateConfig({ foreignKeyColumn: e.target.value })} @@ -824,7 +853,7 @@ export const UnifiedRepeaterConfigPanel: React.FC
- + updateConfig({ foreignKeySourceColumn: e.target.value })} @@ -835,26 +864,31 @@ export const UnifiedRepeaterConfigPanel: React.FC
)} + + {/* 화면 메인 테이블이 없을 때 안내 */} + {config.useCustomTable && config.mainTableName && !currentTableName && ( +
+

+ 독립 저장 모드: 화면 테이블 없이 직접 저장합니다. +

+
+ )}
- {/* 현재 화면 정보 */} -
- - {currentTableName ? ( -
-

{currentTableName}

-

+ {/* 현재 화면 정보 (메인 테이블이 설정된 경우에만 표시) */} + {currentTableName && ( +

+ +
+

{currentTableName}

+

컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개

- ) : ( -
-

화면에 테이블을 먼저 설정해주세요

-
- )} -
+
+ )} {/* 모달 모드: 엔티티 컬럼 선택 */} {isModalMode && ( @@ -870,7 +904,7 @@ export const UnifiedRepeaterConfigPanel: React.FC @@ -891,7 +925,11 @@ export const UnifiedRepeaterConfigPanel: React.FC

- {loadingColumns ? "로딩 중..." : "엔티티 타입 컬럼이 없습니다"} + {loadingColumns + ? "로딩 중..." + : !targetTableForColumns + ? "저장 테이블을 먼저 선택하세요" + : "엔티티 타입 컬럼이 없습니다"}

)} diff --git a/frontend/lib/api/numberingRule.ts b/frontend/lib/api/numberingRule.ts index 983cb490..8b7f47bd 100644 --- a/frontend/lib/api/numberingRule.ts +++ b/frontend/lib/api/numberingRule.ts @@ -173,7 +173,29 @@ export async function resetSequence(ruleId: string): Promise> } } -// ====== 테스트용 API (menu_objid 없는 방식) ====== +// ====== 테스트용 API (numbering_rules_test 테이블 사용) ====== + +/** + * [테스트] 테스트 테이블에서 채번규칙 목록 조회 + * numbering_rules_test 테이블 사용 + * @param menuObjid 메뉴 OBJID (선택) - 필터링용 + */ +export async function getNumberingRulesFromTest( + menuObjid?: number +): Promise> { + try { + const url = menuObjid + ? `/numbering-rules/test/list/${menuObjid}` + : "/numbering-rules/test/list"; + const response = await apiClient.get(url); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || "테스트 규칙 목록 조회 실패", + }; + } +} /** * [테스트] 테이블+컬럼 기반 채번규칙 조회