# 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`
