# 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
GET /test/all-category-keys"] R2["Line 48
GET /test/:tableName/:columnName
⚠️ 너무 일찍 정의"] R3["Line 73
GET /test/:tableName/:columnName/flat"] R4["Line 98
GET /test/value/:valueId
❌ 가려짐"] R5["Line 130
POST /test/value"] R6["Line 174
PUT /test/value/:valueId"] R7["Line 208
DELETE /test/value/:valueId"] R8["Line 240
GET /test/columns/:tableName
❌ 가려짐"] 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
/:tableName/:columnName participant R4 as Line 98
/value/:valueId participant R8 as Line 240
/columns/:tableName Note over Client,Express: 요청: GET /test/value/123 Client->>Express: GET /test/value/123 Express->>R2: 패턴 매칭 시도 Note over R2: tableName="value"
columnName="123"
✅ 매칭됨! 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"
columnName="users"
✅ 매칭됨! 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 메서드 내에서만
순서대로 매칭함] 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 === '*')
전체 조회
else
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
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가 존재하지 않는
부모를 가리킴"] 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 { 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[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 권장 사항 ```mermaid flowchart LR subgraph Current["현재 상태"] C1[category_values_test
실제 사용 중] C2[table_column_category_values
거의 미사용] end subgraph Recommended["권장 조치"] R1["1. 테이블명 정리:
_test 접미사 제거"] R2["2. 서비스 통합:
하나의 서비스로"] 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`