feat: 채번 규칙 및 코드 메뉴별 격리 구현

**주요 변경사항:**

1. **메뉴 스코프 변경 (getSiblingMenuObjids)**
   - 기존: 형제 메뉴 + 모든 형제의 자식 메뉴 포함
   - 변경: 자신 + 자신의 자식 메뉴만 포함
   - 결과: 각 2레벨 메뉴가 완전히 독립적으로 격리됨

2. **채번 규칙 메뉴 선택 상태 유지**
   - useState 초기값 함수에서 저장된 selectedMenuObjid 복원
   - 속성창 닫았다 열어도 선택한 메뉴와 채번 규칙 유지
   - config.autoGeneration.selectedMenuObjid에 저장

3. **로그 정리**
   - 프론트엔드: 디버깅 로그 제거
   - 백엔드: info 레벨 로그를 debug 레벨로 변경
   - 운영 환경에서 불필요한 로그 출력 최소화

**영향:**
- 영업관리 메뉴: 영업관리의 채번 규칙/코드만 조회
- 기준정보 메뉴: 기준정보의 채번 규칙/코드만 조회
- 각 메뉴 그룹이 독립적으로 데이터 관리 가능
This commit is contained in:
kjs 2025-11-11 18:24:24 +09:00
parent 84f3ae4e6f
commit 35ec16084f
6 changed files with 48 additions and 49 deletions

View File

@ -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);

View File

@ -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<number[]> {
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

View File

@ -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) {

View File

@ -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]
);

View File

@ -29,7 +29,12 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
const [parentMenus, setParentMenus] = useState<any[]>([]);
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(menuObjid);
// useState 초기값에서 저장된 값 복원 (우선순위: 저장된 값 > menuObjid prop)
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
return config.autoGeneration?.selectedMenuObjid || menuObjid;
});
const [loadingMenus, setLoadingMenus] = useState(false);
// 부모 메뉴 목록 로드 (사용자 메뉴의 레벨 2만)
@ -49,7 +54,6 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
);
setParentMenus(level2UserMenus);
console.log("✅ 부모 메뉴 로드 완료:", level2UserMenus.length, "개", level2UserMenus);
}
} catch (error) {
console.error("부모 메뉴 로드 실패:", error);
@ -63,21 +67,23 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ 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<TextInputConfigPanelProps> = ({ 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<TextInputConfigPanelProps> = ({ conf
onValueChange={(value) => {
const menuObjid = parseInt(value);
setSelectedMenuObjid(menuObjid);
console.log("✅ 메뉴 선택됨:", menuObjid);
// 컴포넌트 설정에 저장하여 언마운트 시에도 유지
handleChange("autoGeneration", {
...config.autoGeneration,
selectedMenuObjid: menuObjid,
});
}}
disabled={loadingMenus}
>

View File

@ -107,6 +107,7 @@ export interface ComponentConfigPanelProps {
screenTableName?: string; // 화면에서 지정한 테이블명
tableColumns?: any[]; // 테이블 컬럼 정보
tables?: any[]; // 전체 테이블 목록
menuObjid?: number; // 🆕 메뉴 OBJID (코드/카테고리/채번규칙 스코프용)
}
export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> = ({
@ -116,6 +117,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
screenTableName,
tableColumns,
tables,
menuObjid,
}) => {
// 모든 useState를 최상단에 선언 (Hooks 규칙)
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
@ -259,6 +261,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만)
allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블
onTableChange={handleTableChange} // 테이블 변경 핸들러 전달
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
/>
);
};