# 카테고리 멀티테넌시 버그 수정 완료

> **작성일**: 2025-11-06  
> **상태**: ✅ 완료

---

## 🐛 문제 발견

### 증상
- 다른 회사 계정으로 로그인했는데 `company_code = "*"` (최고 관리자 전용) 카테고리 값이 보임
- 채번 규칙과 동일한 멀티테넌시 버그

### 원인
`backend-node/src/services/tableCategoryValueService.ts`의 **7개 메서드**에서 잘못된 WHERE 조건 사용:

```typescript
// ❌ 잘못된 쿼리 (버그)
AND (company_code = $3 OR company_code = '*')
```

---

## ✅ 수정 내용

### 수정된 메서드 (7개)

| 메서드 | 라인 | 작업 유형 | 수정 내용 |
|--------|------|-----------|-----------|
| `getCategoryColumns()` | 12-77 | READ (JOIN) | 멀티테넌시 분기 추가 |
| `getCategoryValues()` | 82-183 | READ | 멀티테넌시 분기 추가 |
| `addCategoryValue()` | 188-269 | CREATE (중복 체크) | 멀티테넌시 분기 추가 |
| `updateCategoryValue()` | 274-403 | UPDATE | 멀티테넌시 분기 추가 |
| `deleteCategoryValue()` | 409-485 | DELETE | 멀티테넌시 분기 추가 |
| `bulkDeleteCategoryValues()` | 490-531 | DELETE (일괄) | 멀티테넌시 분기 추가 |
| `reorderCategoryValues()` | 536-586 | UPDATE (순서) | 멀티테넌시 분기 추가 |

---

## 📊 수정 전후 비교

### 1. getCategoryValues() - 카테고리 값 목록 조회

**Before:**
```typescript
const query = `
  SELECT * FROM table_column_category_values
  WHERE table_name = $1
    AND column_name = $2
    AND (company_code = $3 OR company_code = '*')  -- 🔴 버그!
`;
const params = [tableName, columnName, companyCode];
```

**After:**
```typescript
let query: string;
let params: any[];

if (companyCode === "*") {
  // 최고 관리자: 모든 카테고리 값 조회
  query = `
    SELECT * FROM table_column_category_values
    WHERE table_name = $1 AND column_name = $2
  `;
  params = [tableName, columnName];
} else {
  // 일반 회사: 자신의 카테고리 값만 조회
  query = `
    SELECT * FROM table_column_category_values
    WHERE table_name = $1 
      AND column_name = $2 
      AND company_code = $3
  `;
  params = [tableName, columnName, companyCode];
}
```

### 2. getCategoryColumns() - 카테고리 컬럼 목록 조회 (JOIN)

**Before:**
```typescript
const query = `
  SELECT ...
  FROM table_type_columns tc
  LEFT JOIN table_column_category_values cv 
    ON tc.table_name = cv.table_name 
    AND tc.column_name = cv.column_name
    AND cv.is_active = true
    AND (cv.company_code = $2 OR cv.company_code = '*')  -- 🔴 버그!
  WHERE tc.table_name = $1
`;
```

**After:**
```typescript
if (companyCode === "*") {
  // 최고 관리자: JOIN 조건에서 company_code 제외
  query = `
    SELECT ...
    FROM table_type_columns tc
    LEFT JOIN table_column_category_values cv 
      ON tc.table_name = cv.table_name 
      AND tc.column_name = cv.column_name
      AND cv.is_active = true
    WHERE tc.table_name = $1
  `;
} else {
  // 일반 회사: JOIN 조건에 company_code 추가
  query = `
    SELECT ...
    FROM table_type_columns tc
    LEFT JOIN table_column_category_values cv 
      ON tc.table_name = cv.table_name 
      AND tc.column_name = cv.column_name
      AND cv.is_active = true
      AND cv.company_code = $2
    WHERE tc.table_name = $1
  `;
}
```

### 3. updateCategoryValue() - 카테고리 값 수정

**Before:**
```typescript
const updateQuery = `
  UPDATE table_column_category_values
  SET ...
  WHERE value_id = $${paramIndex++}
    AND (company_code = $${paramIndex++} OR company_code = '*')  -- 🔴 버그!
`;
```

**After:**
```typescript
if (companyCode === "*") {
  // 최고 관리자: company_code 조건 제외
  updateQuery = `
    UPDATE table_column_category_values
    SET ...
    WHERE value_id = $${paramIndex++}
  `;
} else {
  // 일반 회사: company_code 조건 포함
  updateQuery = `
    UPDATE table_column_category_values
    SET ...
    WHERE value_id = $${paramIndex++}
      AND company_code = $${paramIndex++}
  `;
}
```

---

## 🔍 데이터베이스 현황

### 현재 카테고리 값 (수정 전)

```sql
SELECT value_id, table_name, column_name, value_label, company_code
FROM table_column_category_values
ORDER BY created_at DESC
LIMIT 10;
```

| value_id | table_name | column_name | value_label | company_code |
|----------|------------|-------------|-------------|--------------|
| 1-8 | projects | project_type/status | 개발/유지보수/... | * |
| 15-16 | item_info | material | 원자재/153 | * |

**문제**: 일반 회사 사용자도 이 데이터를 볼 수 있음!

### 수정 후 동작

| 사용자 | 수정 전 | 수정 후 |
|--------|---------|---------|
| **최고 관리자 (*)** | 모든 데이터 조회 ✅ | 모든 데이터 조회 ✅ |
| **일반 회사 A** | A데이터 + `*` 데이터 ❌ | A데이터만 ✅ |
| **일반 회사 B** | B데이터 + `*` 데이터 ❌ | B데이터만 ✅ |

---

## 🧪 테스트 시나리오

### 시나리오 1: 최고 관리자로 카테고리 값 조회

```bash
# 로그인
POST /api/auth/login
{ "userId": "admin", "companyCode": "*" }

# 카테고리 값 조회
GET /api/table-category-values/projects/project_type

# 예상 결과: 모든 카테고리 값 조회 가능
[
  { "valueId": 1, "valueLabel": "개발", "companyCode": "*" },
  { "valueId": 2, "valueLabel": "유지보수", "companyCode": "*" },
  { "valueId": 100, "valueLabel": "COMPANY_A 전용", "companyCode": "COMPANY_A" }
]
```

### 시나리오 2: 일반 회사로 카테고리 값 조회

```bash
# 로그인
POST /api/auth/login
{ "userId": "user_a", "companyCode": "COMPANY_A" }

# 카테고리 값 조회
GET /api/table-category-values/projects/project_type

# 수정 전 (버그): company_code="*" 포함
[
  { "valueId": 1, "valueLabel": "개발", "companyCode": "*" },  ← 보면 안 됨!
  { "valueId": 100, "valueLabel": "COMPANY_A 전용", "companyCode": "COMPANY_A" }
]

# 수정 후 (정상): 자신의 데이터만
[
  { "valueId": 100, "valueLabel": "COMPANY_A 전용", "companyCode": "COMPANY_A" }
]
```

### 시나리오 3: 카테고리 값 수정 (권한 체크)

```bash
# 일반 회사 A로 로그인
# company_code="*" 데이터 수정 시도
PUT /api/table-category-values/1
{ "valueLabel": "해킹 시도" }

# 수정 전: 성공 (보안 취약)
# 수정 후: 실패 (권한 없음)
{ "success": false, "message": "카테고리 값을 찾을 수 없습니다" }
```

---

## 📝 수정 상세 내역

### 공통 패턴

모든 메서드에 다음 패턴 적용:

```typescript
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
let query: string;
let params: any[];

if (companyCode === "*") {
  // 최고 관리자: company_code 필터링 제외
  query = `SELECT * FROM table WHERE ...`;
  params = [...];
  logger.info("최고 관리자 카테고리 작업");
} else {
  // 일반 회사: company_code 필터링 포함
  query = `SELECT * FROM table WHERE ... AND company_code = $N`;
  params = [..., companyCode];
  logger.info("회사별 카테고리 작업", { companyCode });
}
```

### 로깅 추가

각 메서드에 멀티테넌시 로깅 추가:

```typescript
// 최고 관리자
logger.info("최고 관리자 카테고리 컬럼 조회");
logger.info("최고 관리자 카테고리 값 조회");

// 일반 회사
logger.info("회사별 카테고리 컬럼 조회", { companyCode });
logger.info("회사별 카테고리 값 조회", { companyCode });
```

---

## 🎯 멀티테넌시 원칙 재확인

### 핵심 원칙

**company_code = "*"는 최고 관리자 전용 데이터이며, 일반 회사는 절대 접근할 수 없습니다.**

| 작업 | 최고 관리자 (*) | 일반 회사 (COMPANY_A) |
|------|-----------------|----------------------|
| **조회** | 모든 데이터 | 자신의 데이터만 |
| **생성** | 모든 회사에 | 자신의 회사에만 |
| **수정** | 모든 데이터 | 자신의 데이터만 |
| **삭제** | 모든 데이터 | 자신의 데이터만 |

### SQL 패턴

```sql
-- ❌ 잘못된 패턴 (버그)
WHERE company_code = $1 OR company_code = '*'

-- ✅ 올바른 패턴 (최고 관리자)
WHERE 1=1  -- company_code 필터링 없음

-- ✅ 올바른 패턴 (일반 회사)
WHERE company_code = $1  -- company_code="*" 자동 제외
```

---

## 🔗 관련 파일

- **수정 완료**: `backend-node/src/services/tableCategoryValueService.ts`
- **정상 참고**: `backend-node/src/services/commonCodeService.ts` (이미 올바르게 구현됨)
- **정상 참고**: `backend-node/src/services/numberingRuleService.ts` (수정 완료)

---

## 🚀 배포 전 체크리스트

- [x] 코드 수정 완료 (7개 메서드)
- [x] 린트 에러 없음
- [x] 로깅 추가 (최고 관리자 vs 일반 회사 구분)
- [ ] 단위 테스트 작성 (선택)
- [ ] 통합 테스트 (필수)
  - [ ] 최고 관리자로 로그인하여 모든 카테고리 값 조회 확인
  - [ ] 일반 회사로 로그인하여 자신의 카테고리 값만 조회 확인
  - [ ] 다른 회사 카테고리 값 접근 불가능 확인
  - [ ] 카테고리 값 생성/수정/삭제 권한 확인
- [ ] 프론트엔드에서 카테고리 값 목록 재확인
- [ ] 백엔드 재실행 (코드 변경 사항 반영)

---

## 📚 관련 문서

- [멀티테넌시 필수 규칙](../README.md#멀티테넌시-필수-규칙)
- [채번 규칙 멀티테넌시 버그 수정](./채번규칙_멀티테넌시_버그_수정_완료.md)
- [카테고리 시스템 구현 완료](./카테고리_시스템_최종_완료_보고서.md)

---

## 🔍 다른 서비스 확인 결과

```bash
cd backend-node/src/services
grep -n "OR company_code = '\*'" *.ts
```

**결과**: `tableCategoryValueService.ts`에만 버그 존재 (수정 완료)

**확인된 정상 서비스**:
- ✅ `commonCodeService.ts` - 이미 올바르게 구현됨
- ✅ `numberingRuleService.ts` - 수정 완료
- ✅ `tableCategoryValueService.ts` - 수정 완료

---

**수정 완료일**: 2025-11-06  
**수정자**: AI Assistant  
**영향 범위**: `tableCategoryValueService.ts` 전체 (7개 메서드)  
**린트 에러**: 없음

