ERP-node/docs/카테고리_멀티테넌시_버그_수정_완료.md

363 lines
9.9 KiB
Markdown
Raw Permalink Normal View History

2025-11-06 17:01:13 +09:00
# 카테고리 멀티테넌시 버그 수정 완료
> **작성일**: 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개 메서드)
**린트 에러**: 없음