ERP-node/docs/DDD1542/CATEGORY_TREE_CONTROLLER_AN...

686 lines
21 KiB
Markdown
Raw Permalink Normal View History

# CategoryTreeController 로직 분석 보고서
> 분석일: 2026-01-26 | 대상 파일: `backend-node/src/controllers/categoryTreeController.ts`
> 검증일: 2026-01-26 | TypeScript 컴파일 검증 완료
---
## 0. 검증 결과 요약
### TypeScript 컴파일 에러 (실제 확인됨)
```bash
$ 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 아키텍처 다이어그램
```mermaid
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. 발견된 문제점 요약
```mermaid
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 현재 라우트 순서 (문제)
```mermaid
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 요청 매칭 시뮬레이션
```mermaid
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는 왜 문제없는가?
```mermaid
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 수정 방안
```typescript
// ✅ 올바른 순서 (더 구체적인 경로 먼저)
// 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 코드 비교
```mermaid
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):**
```typescript
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):**
```typescript
// 🔧 최고 관리자가 특정 회사를 선택한 경우, targetCompanyCode 우선 사용
let companyCode = userCompanyCode;
if (input.targetCompanyCode && userCompanyCode === "*") { // ⚠️ 타입 에러 가능
companyCode = input.targetCompanyCode;
logger.info("🔓 최고 관리자 회사 코드 오버라이드", {
originalCompanyCode: userCompanyCode,
targetCompanyCode: input.targetCompanyCode,
});
}
```
### 4.4 영향
1. TypeScript 컴파일 시 에러 또는 경고 발생 가능
2. 런타임에 `input.targetCompanyCode`가 항상 `undefined`
3. 최고 관리자의 회사 오버라이드 기능이 작동하지 않음
### 4.5 수정 방안
```typescript
// 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 문제 쿼리 패턴
```mermaid
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 수정 방안
```typescript
// ✅ 올바른 멀티테넌시 패턴 (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 코드 분석
```mermaid
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 예상 문제 시나리오
```mermaid
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 수정 방안
```typescript
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 시스템 비교
```mermaid
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[V2Select.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 권장 사항
```mermaid
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 문제 쿼리
```sql
WHERE (company_code = $1 OR company_code = '*')
```
### 8.2 문제점
- `OR` 조건은 인덱스 최적화를 방해
- Full Table Scan 발생 가능
### 8.3 수정 방안
```sql
-- 옵션 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 권장 사항
기능 요구사항 확인 후 결정:
1. **의도적이라면**: 주석으로 의도 명시
2. **누락이라면**: POST와 동일한 로직 추가
---
## 10. 수정 계획
### 10.1 우선순위별 수정 항목
```mermaid
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`