686 lines
21 KiB
Markdown
686 lines
21 KiB
Markdown
# 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[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<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`
|