21 KiB
CategoryTreeController 로직 분석 보고서
분석일: 2026-01-26 | 대상 파일:
backend-node/src/controllers/categoryTreeController.ts
검증일: 2026-01-26 | TypeScript 컴파일 검증 완료
0. 검증 결과 요약
TypeScript 컴파일 에러 (실제 확인됨)
$ tsc --noEmit src/controllers/categoryTreeController.ts
src/controllers/categoryTreeController.ts(139,15): error TS2339: Property 'targetCompanyCode' does not exist on type 'CreateCategoryValueInput'.
src/controllers/categoryTreeController.ts(140,27): error TS2339: Property 'targetCompanyCode' does not exist on type 'CreateCategoryValueInput'.
src/controllers/categoryTreeController.ts(143,34): error TS2339: Property 'targetCompanyCode' does not exist on type 'CreateCategoryValueInput'.
결론: targetCompanyCode 타입 정의 누락 문제가 실제로 존재함
1. 시스템 개요
1.1 아키텍처 다이어그램
flowchart TB
subgraph Frontend["프론트엔드"]
UI[카테고리 관리 UI]
end
subgraph Backend["백엔드"]
subgraph Controllers["컨트롤러"]
CTC[categoryTreeController.ts]
end
subgraph Services["서비스"]
CTS[categoryTreeService.ts]
TCVS[tableCategoryValueService.ts]
end
subgraph Database["데이터베이스"]
CVT[(category_values_test)]
TCCV[(table_column_category_values)]
TTC[(table_type_columns)]
end
end
UI --> |"/api/category-tree/*"| CTC
CTC --> CTS
CTS --> CVT
TCVS --> TCCV
TCVS --> TTC
style CTC fill:#ff6b6b,stroke:#c92a2a
style CVT fill:#4ecdc4,stroke:#087f5b
style TCCV fill:#4ecdc4,stroke:#087f5b
1.2 관련 파일 목록
| 파일 | 역할 | 사용 테이블 |
|---|---|---|
categoryTreeController.ts |
카테고리 트리 API 라우트 | - |
categoryTreeService.ts |
카테고리 트리 비즈니스 로직 | category_values_test |
tableCategoryValueService.ts |
테이블별 카테고리 값 관리 | table_column_category_values |
categoryTreeRoutes.ts |
라우트 re-export | - |
2. 발견된 문제점 요약
pie title 문제점 심각도 분류
"🔴 Critical (즉시 수정)" : 3
"🟠 Major (수정 권장)" : 2
"🟡 Minor (검토 필요)" : 2
| 심각도 | 문제 | 영향도 | 검증 |
|---|---|---|---|
| 🔴 Critical | 라우트 순서 충돌 | GET 라우트 2개 호출 불가 | 이론적 분석 |
| 🔴 Critical | 타입 정의 불일치 | TypeScript 컴파일 에러 | ✅ tsc 검증됨 |
| 🔴 Critical | 멀티테넌시 규칙 위반 | 보안 문제 - 데이터 노출 | .cursorrules 규칙 확인 |
| 🟠 Major | 하위 항목 삭제 미구현 | 데이터 정합성 | 주석 vs 구현 비교 |
| 🟠 Major | 카테고리 시스템 이원화 | 유지보수 복잡도 | 코드 분석 |
| 🟡 Minor | 인덱스 비효율 쿼리 | 성능 저하 | 쿼리 패턴 분석 |
| 🟡 Minor | PUT/DELETE 오버라이드 누락 | 기능 제한 | 의도적 설계 가능 |
3. 🔴 Critical: 라우트 순서 충돌
3.1 문제 설명
Express 라우터는 정의 순서대로 매칭합니다. 현재 라우트 순서에서 일부 GET 라우트가 절대 호출되지 않습니다.
3.2 현재 라우트 순서 (문제)
flowchart LR
subgraph Order["현재 정의 순서"]
R1["Line 24<br/>GET /test/all-category-keys"]
R2["Line 48<br/>GET /test/:tableName/:columnName<br/>⚠️ 너무 일찍 정의"]
R3["Line 73<br/>GET /test/:tableName/:columnName/flat"]
R4["Line 98<br/>GET /test/value/:valueId<br/>❌ 가려짐"]
R5["Line 130<br/>POST /test/value"]
R6["Line 174<br/>PUT /test/value/:valueId"]
R7["Line 208<br/>DELETE /test/value/:valueId"]
R8["Line 240<br/>GET /test/columns/:tableName<br/>❌ 가려짐"]
end
R1 --> R2 --> R3 --> R4 --> R5 --> R6 --> R7 --> R8
style R2 fill:#fff3bf,stroke:#f59f00
style R4 fill:#ffe3e3,stroke:#c92a2a
style R8 fill:#ffe3e3,stroke:#c92a2a
3.3 요청 매칭 시뮬레이션
sequenceDiagram
participant Client as 클라이언트
participant Express as Express Router
participant R2 as Line 48<br/>/:tableName/:columnName
participant R4 as Line 98<br/>/value/:valueId
participant R8 as Line 240<br/>/columns/:tableName
Note over Client,Express: 요청: GET /test/value/123
Client->>Express: GET /test/value/123
Express->>R2: 패턴 매칭 시도
Note over R2: tableName="value"<br/>columnName="123"<br/>✅ 매칭됨!
R2-->>Express: 처리 완료
Note over R4: ❌ 검사되지 않음
Note over Client,Express: 요청: GET /test/columns/users
Client->>Express: GET /test/columns/users
Express->>R2: 패턴 매칭 시도
Note over R2: tableName="columns"<br/>columnName="users"<br/>✅ 매칭됨!
R2-->>Express: 처리 완료
Note over R8: ❌ 검사되지 않음
3.4 영향받는 라우트
| 라인 | 경로 | HTTP | 상태 | 원인 |
|---|---|---|---|---|
| 98 | /test/value/:valueId |
GET | ❌ 호출 불가 | Line 48에 의해 가려짐 |
| 240 | /test/columns/:tableName |
GET | ❌ 호출 불가 | Line 48에 의해 가려짐 |
3.5 PUT/DELETE는 왜 문제없는가?
flowchart TB
subgraph Methods["HTTP 메서드별 라우트 분리"]
subgraph GET["GET 메서드"]
G1["Line 24: /test/all-category-keys"]
G2["Line 48: /test/:tableName/:columnName ⚠️"]
G3["Line 73: /test/:tableName/:columnName/flat"]
G4["Line 98: /test/value/:valueId ❌"]
G5["Line 240: /test/columns/:tableName ❌"]
end
subgraph POST["POST 메서드"]
P1["Line 130: /test/value"]
end
subgraph PUT["PUT 메서드"]
U1["Line 174: /test/value/:valueId ✅"]
end
subgraph DELETE["DELETE 메서드"]
D1["Line 208: /test/value/:valueId ✅"]
end
end
Note1[Express는 같은 HTTP 메서드 내에서만<br/>순서대로 매칭함]
style G2 fill:#fff3bf
style G4 fill:#ffe3e3
style G5 fill:#ffe3e3
style U1 fill:#d3f9d8
style D1 fill:#d3f9d8
결론: PUT /test/value/:valueId와 DELETE /test/value/:valueId는 GET 라우트와 HTTP 메서드가 다르므로 충돌하지 않습니다.
3.6 수정 방안
// ✅ 올바른 순서 (더 구체적인 경로 먼저)
// 1. 리터럴 경로 (가장 먼저)
router.get("/test/all-category-keys", ...);
// 2. 부분 리터럴 경로 (리터럴 + 파라미터)
router.get("/test/value/:valueId", ...); // "value"가 고정
router.get("/test/columns/:tableName", ...); // "columns"가 고정
// 3. 더 긴 동적 경로
router.get("/test/:tableName/:columnName/flat", ...); // 4세그먼트
// 4. 가장 일반적인 동적 경로 (마지막에)
router.get("/test/:tableName/:columnName", ...); // 3세그먼트
4. 🔴 Critical: 타입 정의 불일치
4.1 문제 설명
컨트롤러에서 input.targetCompanyCode를 사용하지만, 인터페이스에 해당 필드가 없습니다.
4.2 코드 비교
flowchart LR
subgraph Interface["CreateCategoryValueInput 인터페이스"]
I1[tableName: string]
I2[columnName: string]
I3[valueCode: string]
I4[valueLabel: string]
I5[valueOrder?: number]
I6[parentValueId?: number]
I7[description?: string]
I8[color?: string]
I9[icon?: string]
I10[isActive?: boolean]
I11[isDefault?: boolean]
Missing["❌ targetCompanyCode 없음"]
end
subgraph Controller["컨트롤러 (Line 139)"]
C1["input.targetCompanyCode 사용"]
end
Controller -.-> |"타입 불일치"| Missing
style Missing fill:#ffe3e3,stroke:#c92a2a
4.3 문제 코드
인터페이스 정의 (categoryTreeService.ts Line 34-46):
export interface CreateCategoryValueInput {
tableName: string;
columnName: string;
valueCode: string;
valueLabel: string;
valueOrder?: number;
parentValueId?: number | null;
description?: string;
color?: string;
icon?: string;
isActive?: boolean;
isDefault?: boolean;
// ❌ targetCompanyCode 필드 없음!
}
컨트롤러 사용 (categoryTreeController.ts Line 136-145):
// 🔧 최고 관리자가 특정 회사를 선택한 경우, targetCompanyCode 우선 사용
let companyCode = userCompanyCode;
if (input.targetCompanyCode && userCompanyCode === "*") { // ⚠️ 타입 에러 가능
companyCode = input.targetCompanyCode;
logger.info("🔓 최고 관리자 회사 코드 오버라이드", {
originalCompanyCode: userCompanyCode,
targetCompanyCode: input.targetCompanyCode,
});
}
4.4 영향
- TypeScript 컴파일 시 에러 또는 경고 발생 가능
- 런타임에
input.targetCompanyCode가 항상undefined - 최고 관리자의 회사 오버라이드 기능이 작동하지 않음
4.5 수정 방안
// categoryTreeService.ts - 인터페이스 수정
export interface CreateCategoryValueInput {
tableName: string;
columnName: string;
valueCode: string;
valueLabel: string;
valueOrder?: number;
parentValueId?: number | null;
description?: string;
color?: string;
icon?: string;
isActive?: boolean;
isDefault?: boolean;
targetCompanyCode?: string; // ✅ 추가
}
5. 🔴 Critical: 멀티테넌시 규칙 위반 (심각도 상향)
5.1 규칙 위반 설명
.cursorrules 파일에 명시된 프로젝트 규칙:
중요:
company_code = "*"는 최고 관리자 전용 데이터를 의미합니다.
- ❌ 잘못된 이해:
company_code = "*"= 모든 회사가 공유하는 공통 데이터- ✅ 올바른 이해:
company_code = "*"= 최고 관리자만 관리하는 전용 데이터핵심: 일반 회사 사용자는
company_code = "*"데이터를 볼 수 없습니다!
현재 상태: 서비스 코드에서 일반 회사도 company_code = '*' 데이터를 조회할 수 있음 → 보안 위반
5.2 문제 쿼리 패턴
flowchart TB
subgraph Current["현재 구현 (문제)"]
Q1["WHERE (company_code = $1 OR company_code = '*')"]
subgraph Result1["일반 회사 'COMPANY_A' 조회 시"]
R1A["✅ COMPANY_A 데이터"]
R1B["⚠️ * 데이터도 조회됨 (규칙 위반)"]
end
end
subgraph Expected["올바른 구현"]
Q2["if (companyCode === '*')<br/> 전체 조회<br/>else<br/> WHERE company_code = $1"]
subgraph Result2["일반 회사 'COMPANY_A' 조회 시"]
R2A["✅ COMPANY_A 데이터만"]
end
end
style R1B fill:#ffe3e3,stroke:#c92a2a
style R2A fill:#d3f9d8,stroke:#087f5b
5.3 영향받는 함수 목록
| 서비스 | 함수 | 라인 | 문제 쿼리 |
|---|---|---|---|
categoryTreeService.ts |
getCategoryTree |
93 | WHERE (company_code = $1 OR company_code = '*') |
categoryTreeService.ts |
getCategoryList |
146 | WHERE (company_code = $1 OR company_code = '*') |
categoryTreeService.ts |
getCategoryValue |
188 | WHERE (company_code = $1 OR company_code = '*') |
categoryTreeService.ts |
updateCategoryValue |
352 | WHERE (company_code = $1 OR company_code = '*') |
categoryTreeService.ts |
deleteCategoryValue |
415 | WHERE (company_code = $1 OR company_code = '*') |
categoryTreeService.ts |
updateChildrenPaths |
443 | WHERE (company_code = $1 OR company_code = '*') |
categoryTreeService.ts |
getCategoryColumns |
498 | WHERE (company_code = $2 OR company_code = '*') |
categoryTreeService.ts |
getAllCategoryKeys |
530 | WHERE cv.company_code = $1 OR cv.company_code = '*' |
5.4 수정 방안
// ✅ 올바른 멀티테넌시 패턴 (tableCategoryValueService.ts 참고)
async getCategoryTree(companyCode: string, tableName: string, columnName: string) {
let query: string;
let params: any[];
if (companyCode === "*") {
// 최고 관리자: 모든 데이터 조회
query = `
SELECT * FROM category_values_test
WHERE table_name = $1 AND column_name = $2
ORDER BY depth ASC, value_order ASC
`;
params = [tableName, columnName];
} else {
// 일반 회사: 자신의 데이터만 조회 (company_code = '*' 제외)
query = `
SELECT * FROM category_values_test
WHERE table_name = $1 AND column_name = $2
AND company_code = $3
ORDER BY depth ASC, value_order ASC
`;
params = [tableName, columnName, companyCode];
}
return await pool.query(query, params);
}
6. 🟠 Major: 하위 항목 삭제 미구현
6.1 문제 설명
주석에는 "하위 항목도 함께 삭제"라고 되어 있지만, 실제 구현에서는 단일 레코드만 삭제합니다.
6.2 코드 분석
flowchart TB
subgraph Comment["주석 (Line 407)"]
C1["카테고리 값 삭제 (하위 항목도 함께 삭제)"]
end
subgraph Implementation["실제 구현 (Line 413-416)"]
I1["DELETE FROM category_values_test<br/>WHERE ... AND value_id = $2"]
I2["단일 레코드만 삭제"]
end
Comment -.-> |"불일치"| Implementation
style Comment fill:#e7f5ff,stroke:#1971c2
style Implementation fill:#ffe3e3,stroke:#c92a2a
6.3 예상 문제 시나리오
flowchart TB
subgraph Before["삭제 전"]
P["대분류 (value_id=1)"]
C1["중분류 A (parent_value_id=1)"]
C2["중분류 B (parent_value_id=1)"]
C3["소분류 X (parent_value_id=C1)"]
P --> C1
P --> C2
C1 --> C3
end
subgraph After["'대분류' 삭제 후"]
C1o["중분류 A ⚠️ 고아"]
C2o["중분류 B ⚠️ 고아"]
C3o["소분류 X ⚠️ 고아"]
Orphan["parent_value_id가 존재하지 않는<br/>부모를 가리킴"]
end
Before --> |"DELETE"| After
style C1o fill:#ffe3e3
style C2o fill:#ffe3e3
style C3o fill:#ffe3e3
6.4 수정 방안
async deleteCategoryValue(companyCode: string, valueId: number): Promise<boolean> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
// 1. 재귀적으로 모든 하위 항목 ID 조회
const descendantsQuery = `
WITH RECURSIVE descendants AS (
SELECT value_id FROM category_values_test
WHERE value_id = $1 AND (company_code = $2 OR company_code = '*')
UNION ALL
SELECT c.value_id FROM category_values_test c
JOIN descendants d ON c.parent_value_id = d.value_id
WHERE c.company_code = $2 OR c.company_code = '*'
)
SELECT value_id FROM descendants
`;
const descendants = await client.query(descendantsQuery, [valueId, companyCode]);
const idsToDelete = descendants.rows.map(r => r.value_id);
// 2. 하위 항목 포함 일괄 삭제
if (idsToDelete.length > 0) {
await client.query(
`DELETE FROM category_values_test WHERE value_id = ANY($1::int[])`,
[idsToDelete]
);
}
await client.query("COMMIT");
logger.info("카테고리 값 및 하위 항목 삭제 완료", {
valueId,
totalDeleted: idsToDelete.length
});
return true;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
7. 🟠 Major: 카테고리 시스템 이원화
7.1 문제 설명
동일한 목적의 두 개의 카테고리 시스템이 존재합니다.
7.2 시스템 비교
flowchart TB
subgraph System1["시스템 1: categoryTreeService"]
S1C[categoryTreeController.ts]
S1S[categoryTreeService.ts]
S1T[(category_values_test)]
S1C --> S1S --> S1T
end
subgraph System2["시스템 2: tableCategoryValueService"]
S2S[tableCategoryValueService.ts]
S2T[(table_column_category_values)]
S2S --> S2T
end
subgraph Usage["사용처"]
U1[NumberingRuleDesigner.tsx]
U2[UnifiedSelect.tsx]
U3[screenManagementService.ts]
end
U1 --> S1T
U2 --> S1T
U3 --> S1T
style S1T fill:#4ecdc4,stroke:#087f5b
style S2T fill:#4ecdc4,stroke:#087f5b
7.3 테이블 비교
| 속성 | category_values_test |
table_column_category_values |
|---|---|---|
| 서비스 | categoryTreeService | tableCategoryValueService |
| menu_objid | ❌ 없음 | ✅ 있음 |
| 계층 구조 | ✅ 지원 (최대 3단계) | ✅ 지원 |
| path 컬럼 | ✅ 있음 | ❌ 없음 |
| 사용 빈도 | 높음 (108건) | 낮음 (0건 추정) |
| 명칭 | "테스트" | "정식" |
7.4 권장 사항
flowchart LR
subgraph Current["현재 상태"]
C1[category_values_test<br/>실제 사용 중]
C2[table_column_category_values<br/>거의 미사용]
end
subgraph Recommended["권장 조치"]
R1["1. 테이블명 정리:<br/>_test 접미사 제거"]
R2["2. 서비스 통합:<br/>하나의 서비스로"]
R3["3. 미사용 테이블 정리"]
end
Current --> Recommended
8. 🟡 Minor: 인덱스 비효율 쿼리
8.1 문제 쿼리
WHERE (company_code = $1 OR company_code = '*')
8.2 문제점
OR조건은 인덱스 최적화를 방해- Full Table Scan 발생 가능
8.3 수정 방안
-- 옵션 1: UNION 사용 (권장)
SELECT * FROM category_values_test WHERE company_code = $1
UNION ALL
SELECT * FROM category_values_test WHERE company_code = '*'
-- 옵션 2: IN 연산자 사용
WHERE company_code IN ($1, '*')
-- 옵션 3: 조건별 분기 (가장 권장)
-- 최고 관리자와 일반 사용자 쿼리 분리 (멀티테넌시 규칙 준수와 함께)
9. 🟡 Minor: PUT/DELETE 오버라이드 누락
9.1 문제 설명
POST에서만 targetCompanyCode 오버라이드 로직이 있고, PUT/DELETE에는 없습니다.
9.2 비교 표
| 메서드 | 라인 | targetCompanyCode 처리 |
|---|---|---|
POST /test/value |
136-145 | ✅ 있음 |
PUT /test/value/:valueId |
174-201 | ❌ 없음 |
DELETE /test/value/:valueId |
208-233 | ❌ 없음 |
9.3 영향
- 최고 관리자가 다른 회사의 카테고리 값을 수정/삭제할 때 제한될 수 있음
- 단, 의도적 설계일 수 있음 (생성만 회사 지정, 수정/삭제는 기존 레코드의 company_code 사용)
9.4 권장 사항
기능 요구사항 확인 후 결정:
- 의도적이라면: 주석으로 의도 명시
- 누락이라면: POST와 동일한 로직 추가
10. 수정 계획
10.1 우선순위별 수정 항목
gantt
title 수정 우선순위
dateFormat YYYY-MM-DD
section 🔴 Critical
라우트 순서 수정 :crit, a1, 2026-01-26, 1d
타입 정의 수정 :crit, a2, 2026-01-26, 1d
멀티테넌시 규칙 준수 :crit, a3, 2026-01-26, 1d
section 🟠 Major
하위 항목 삭제 구현 :b1, 2026-01-27, 2d
section 🟡 Minor
쿼리 최적화 :c1, 2026-01-29, 1d
PUT/DELETE 검토 :c2, 2026-01-29, 1d
10.2 수정 체크리스트
🔴 Critical (즉시 수정)
-
라우트 순서 수정 (Line 48, 98, 240)
/test/value/:valueId를/test/:tableName/:columnName앞으로 이동/test/columns/:tableName를/test/:tableName/:columnName앞으로 이동
-
타입 정의 수정 (categoryTreeService.ts Line 34-46)
CreateCategoryValueInput에targetCompanyCode?: string추가- TypeScript 컴파일 에러 해결
-
멀티테넌시 규칙 준수 (categoryTreeService.ts 모든 쿼리)
WHERE (company_code = $1 OR company_code = '*')패턴 제거- 최고 관리자 분기와 일반 사용자 분기 분리
- 일반 사용자는
company_code = '*'데이터 조회 불가 - 영향받는 함수: getCategoryTree, getCategoryList, getCategoryValue, updateCategoryValue, deleteCategoryValue, updateChildrenPaths, getCategoryColumns, getAllCategoryKeys
🟠 Major (수정 권장)
- 하위 항목 삭제 구현 (deleteCategoryValue 함수)
- 재귀적 하위 항목 조회 및 삭제 로직 추가
- 또는 주석 수정 (실제 동작과 일치하도록)
🟡 Minor (검토 필요)
- PUT/DELETE 오버라이드 검토
- 필요 시 POST와 동일한 로직 추가
- 불필요 시 의도 주석 추가
11. 참고 자료
- 멀티테넌시 가이드:
.cursor/rules/multi-tenancy-guide.mdc - DB 비효율성 분석:
docs/DB_INEFFICIENCY_ANALYSIS.md - 보안 가이드:
.cursor/rules/security-guide.mdc