From 35ec16084fe950cf6156f90d7754662a6085ba35 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 11 Nov 2025 18:24:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B1=84=EB=B2=88=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EB=A9=94=EB=89=B4=EB=B3=84?= =?UTF-8?q?=20=EA=B2=A9=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **주요 변경사항:** 1. **메뉴 스코프 변경 (getSiblingMenuObjids)** - 기존: 형제 메뉴 + 모든 형제의 자식 메뉴 포함 - 변경: 자신 + 자신의 자식 메뉴만 포함 - 결과: 각 2레벨 메뉴가 완전히 독립적으로 격리됨 2. **채번 규칙 메뉴 선택 상태 유지** - useState 초기값 함수에서 저장된 selectedMenuObjid 복원 - 속성창 닫았다 열어도 선택한 메뉴와 채번 규칙 유지 - config.autoGeneration.selectedMenuObjid에 저장 3. **로그 정리** - 프론트엔드: 디버깅 로그 제거 - 백엔드: info 레벨 로그를 debug 레벨로 변경 - 운영 환경에서 불필요한 로그 출력 최소화 **영향:** - 영업관리 메뉴: 영업관리의 채번 규칙/코드만 조회 - 기준정보 메뉴: 기준정보의 채번 규칙/코드만 조회 - 각 메뉴 그룹이 독립적으로 데이터 관리 가능 --- .../controllers/numberingRuleController.ts | 2 +- backend-node/src/services/menuService.ts | 58 ++++++++----------- .../src/services/numberingRuleService.ts | 2 +- .../src/services/screenManagementService.ts | 2 +- .../text-input/TextInputConfigPanel.tsx | 30 ++++++---- .../lib/utils/getComponentConfigPanel.tsx | 3 + 6 files changed, 48 insertions(+), 49 deletions(-) diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index d00db2c4..55c19353 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -27,7 +27,7 @@ router.get("/available/:menuObjid?", authenticateToken, async (req: Authenticate const companyCode = req.user!.companyCode; const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined; - logger.info("📥 메뉴별 채번 규칙 조회 요청", { companyCode, menuObjid }); + logger.info("메뉴별 채번 규칙 조회 요청", { menuObjid, companyCode }); try { const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid); diff --git a/backend-node/src/services/menuService.ts b/backend-node/src/services/menuService.ts index 9a9be99c..b22beb88 100644 --- a/backend-node/src/services/menuService.ts +++ b/backend-node/src/services/menuService.ts @@ -8,71 +8,59 @@ import { logger } from "../utils/logger"; */ /** - * 메뉴의 형제 메뉴 OBJID 목록 조회 - * (같은 부모를 가진 메뉴들) + * 메뉴의 형제 메뉴 및 자식 메뉴 OBJID 목록 조회 + * (같은 부모를 가진 메뉴들 + 자식 메뉴들) * * 메뉴 스코프 규칙: * - 같은 부모를 가진 형제 메뉴들은 카테고리/채번규칙을 공유 + * - 자식 메뉴의 데이터도 부모 메뉴에서 조회 가능 (3레벨까지만 존재) * - 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환 * - 메뉴를 찾을 수 없으면 안전하게 자기 자신만 반환 * * @param menuObjid 현재 메뉴의 OBJID - * @returns 형제 메뉴 OBJID 배열 (자기 자신 포함, 정렬됨) + * @returns 형제 메뉴 + 자식 메뉴 OBJID 배열 (자기 자신 포함, 정렬됨) * * @example * // 영업관리 (200) * // ├── 고객관리 (201) + * // │ └── 고객등록 (211) * // ├── 계약관리 (202) * // └── 주문관리 (203) * * await getSiblingMenuObjids(201); - * // 결과: [201, 202, 203] - 모두 같은 부모(200)를 가진 형제 + * // 결과: [201, 202, 203, 211] - 형제(202, 203) + 자식(211) */ export async function getSiblingMenuObjids(menuObjid: number): Promise { const pool = getPool(); try { - logger.info("형제 메뉴 조회 시작", { menuObjid }); + logger.debug("메뉴 스코프 조회 시작", { menuObjid }); - // 1. 현재 메뉴의 부모 찾기 - const parentQuery = ` - SELECT parent_obj_id FROM menu_info WHERE objid = $1 - `; - const parentResult = await pool.query(parentQuery, [menuObjid]); + // 1. 현재 메뉴 자신을 포함 + const menuObjids = [menuObjid]; - if (parentResult.rows.length === 0) { - logger.warn("메뉴를 찾을 수 없음, 자기 자신만 반환", { menuObjid }); - return [menuObjid]; // 메뉴가 없으면 안전하게 자기 자신만 반환 - } - - const parentObjId = parentResult.rows[0].parent_obj_id; - - if (!parentObjId || parentObjId === 0) { - // 최상위 메뉴인 경우 자기 자신만 반환 - logger.info("최상위 메뉴 (형제 없음)", { menuObjid, parentObjId }); - return [menuObjid]; - } - - // 2. 같은 부모를 가진 형제 메뉴들 조회 - const siblingsQuery = ` + // 2. 현재 메뉴의 자식 메뉴들 조회 + const childrenQuery = ` SELECT objid FROM menu_info - WHERE parent_obj_id = $1 + WHERE parent_obj_id = $1 ORDER BY objid `; - const siblingsResult = await pool.query(siblingsQuery, [parentObjId]); + const childrenResult = await pool.query(childrenQuery, [menuObjid]); - const siblingObjids = siblingsResult.rows.map((row) => Number(row.objid)); + const childObjids = childrenResult.rows.map((row) => Number(row.objid)); - logger.info("형제 메뉴 조회 완료", { - menuObjid, - parentObjId, - siblingCount: siblingObjids.length, - siblings: siblingObjids, + // 3. 자신 + 자식을 합쳐서 정렬 + const allObjids = Array.from(new Set([...menuObjids, ...childObjids])).sort((a, b) => a - b); + + logger.debug("메뉴 스코프 조회 완료", { + menuObjid, + childCount: childObjids.length, + totalCount: allObjids.length }); - return siblingObjids; + return allObjids; } catch (error: any) { - logger.error("형제 메뉴 조회 실패", { + logger.error("메뉴 스코프 조회 실패", { menuObjid, error: error.message, stack: error.stack diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 4fb27e52..db76bbee 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -360,7 +360,7 @@ class NumberingRuleService { const result = await pool.query(query, params); - logger.info("✅ 채번 규칙 쿼리 성공", { rowCount: result.rows.length }); + logger.debug("채번 규칙 쿼리 성공", { ruleCount: result.rows.length }); // 파트 정보 추가 for (const rule of result.rows) { diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 50e62420..85081cd3 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -1565,7 +1565,7 @@ export class ScreenManagementService { WHERE sma.screen_id = $1 AND sma.company_code = $2 AND sma.is_active = 'Y' - ORDER BY sma.created_at ASC + ORDER BY sma.created_date ASC LIMIT 1`, [screenId, companyCode] ); diff --git a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx index 817baf57..69088e96 100644 --- a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx +++ b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx @@ -29,7 +29,12 @@ export const TextInputConfigPanel: React.FC = ({ conf // 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택) const [parentMenus, setParentMenus] = useState([]); - const [selectedMenuObjid, setSelectedMenuObjid] = useState(menuObjid); + + // useState 초기값에서 저장된 값 복원 (우선순위: 저장된 값 > menuObjid prop) + const [selectedMenuObjid, setSelectedMenuObjid] = useState(() => { + return config.autoGeneration?.selectedMenuObjid || menuObjid; + }); + const [loadingMenus, setLoadingMenus] = useState(false); // 부모 메뉴 목록 로드 (사용자 메뉴의 레벨 2만) @@ -49,7 +54,6 @@ export const TextInputConfigPanel: React.FC = ({ conf ); setParentMenus(level2UserMenus); - console.log("✅ 부모 메뉴 로드 완료:", level2UserMenus.length, "개", level2UserMenus); } } catch (error) { console.error("부모 메뉴 로드 실패:", error); @@ -63,21 +67,23 @@ export const TextInputConfigPanel: React.FC = ({ conf // 채번 규칙 목록 로드 (선택된 메뉴 기준) useEffect(() => { const loadRules = async () => { + // autoGeneration.type이 numbering_rule이 아니면 로드하지 않음 + if (config.autoGeneration?.type !== "numbering_rule") { + return; + } + // 메뉴가 선택되지 않았으면 로드하지 않음 if (!selectedMenuObjid) { - console.log("⚠️ 메뉴가 선택되지 않아 채번 규칙을 로드하지 않습니다"); setNumberingRules([]); return; } setLoadingRules(true); try { - console.log("🔍 선택된 메뉴 기반 채번 규칙 로드", { selectedMenuObjid }); const response = await getAvailableNumberingRules(selectedMenuObjid); if (response.success && response.data) { setNumberingRules(response.data); - console.log("✅ 채번 규칙 로드 완료:", response.data.length, "개"); } } catch (error) { console.error("채번 규칙 목록 로드 실패:", error); @@ -87,11 +93,8 @@ export const TextInputConfigPanel: React.FC = ({ conf } }; - // autoGeneration.type이 numbering_rule일 때만 로드 - if (config.autoGeneration?.type === "numbering_rule") { - loadRules(); - } - }, [config.autoGeneration?.type, selectedMenuObjid]); + loadRules(); + }, [selectedMenuObjid, config.autoGeneration?.type]); const handleChange = (key: keyof TextInputConfig, value: any) => { onChange({ [key]: value }); @@ -202,7 +205,12 @@ export const TextInputConfigPanel: React.FC = ({ conf onValueChange={(value) => { const menuObjid = parseInt(value); setSelectedMenuObjid(menuObjid); - console.log("✅ 메뉴 선택됨:", menuObjid); + + // 컴포넌트 설정에 저장하여 언마운트 시에도 유지 + handleChange("autoGeneration", { + ...config.autoGeneration, + selectedMenuObjid: menuObjid, + }); }} disabled={loadingMenus} > diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index 9093a480..b4af1632 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -107,6 +107,7 @@ export interface ComponentConfigPanelProps { screenTableName?: string; // 화면에서 지정한 테이블명 tableColumns?: any[]; // 테이블 컬럼 정보 tables?: any[]; // 전체 테이블 목록 + menuObjid?: number; // 🆕 메뉴 OBJID (코드/카테고리/채번규칙 스코프용) } export const DynamicComponentConfigPanel: React.FC = ({ @@ -116,6 +117,7 @@ export const DynamicComponentConfigPanel: React.FC = screenTableName, tableColumns, tables, + menuObjid, }) => { // 모든 useState를 최상단에 선언 (Hooks 규칙) const [ConfigPanelComponent, setConfigPanelComponent] = React.useState | null>(null); @@ -259,6 +261,7 @@ export const DynamicComponentConfigPanel: React.FC = tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만) allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블 onTableChange={handleTableChange} // 테이블 변경 핸들러 전달 + menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 /> ); };