363 lines
9.9 KiB
Markdown
363 lines
9.9 KiB
Markdown
|
|
# 카테고리 멀티테넌시 버그 수정 완료
|
||
|
|
|
||
|
|
> **작성일**: 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개 메서드)
|
||
|
|
**린트 에러**: 없음
|
||
|
|
|