333 lines
8.3 KiB
Markdown
333 lines
8.3 KiB
Markdown
# 채번 규칙 멀티테넌시 버그 수정 완료
|
|
|
|
> **작성일**: 2025-11-06
|
|
> **상태**: ✅ 완료
|
|
|
|
---
|
|
|
|
## 🐛 문제 발견
|
|
|
|
### 증상
|
|
- 다른 회사 계정으로 로그인했는데 `company_code = "*"` (최고 관리자 전용) 채번 규칙이 보임
|
|
- 멀티테넌시 원칙 위반
|
|
|
|
### 원인
|
|
`backend-node/src/services/numberingRuleService.ts`의 SQL 쿼리에서 **잘못된 WHERE 조건** 사용:
|
|
|
|
```typescript
|
|
// ❌ 잘못된 쿼리 (버그)
|
|
WHERE company_code = $1 OR company_code = '*'
|
|
```
|
|
|
|
**문제점:**
|
|
- `OR company_code = '*'` 조건이 **항상 최고 관리자 데이터를 포함**시킴
|
|
- 일반 회사 사용자도 `company_code = "*"` 데이터를 볼 수 있음
|
|
- 멀티테넌시 보안 위반
|
|
|
|
---
|
|
|
|
## ✅ 수정 내용
|
|
|
|
### 수정된 로직
|
|
|
|
```typescript
|
|
// ✅ 올바른 쿼리 (수정 후)
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 회사 데이터 조회 가능
|
|
query = `SELECT * FROM numbering_rules`;
|
|
params = [];
|
|
} else {
|
|
// 일반 회사: 자신의 데이터만 조회 (company_code="*" 제외)
|
|
query = `SELECT * FROM numbering_rules WHERE company_code = $1`;
|
|
params = [companyCode];
|
|
}
|
|
```
|
|
|
|
### 수정된 메서드 목록
|
|
|
|
| 메서드 | 수정 내용 | 라인 |
|
|
|--------|-----------|------|
|
|
| `getRuleList()` | 멀티테넌시 필터링 추가 | 40-150 |
|
|
| `getAvailableRulesForMenu()` | 멀티테넌시 필터링 추가 | 155-402 |
|
|
| `getRuleById()` | 멀티테넌시 필터링 추가 | 407-506 |
|
|
|
|
---
|
|
|
|
## 📊 수정 전후 비교
|
|
|
|
### 수정 전 (버그)
|
|
|
|
```sql
|
|
-- 일반 회사 (COMPANY_A) 로그인 시
|
|
SELECT * FROM numbering_rules
|
|
WHERE company_code = 'COMPANY_A' OR company_code = '*';
|
|
|
|
-- 결과: 3건
|
|
-- 1. SAMPLE_RULE (company_code = '*') ← 보면 안 됨!
|
|
-- 2. 사번코드 (company_code = '*') ← 보면 안 됨!
|
|
-- 3. COMPANY_A 전용 규칙 (있다면)
|
|
```
|
|
|
|
### 수정 후 (정상)
|
|
|
|
```sql
|
|
-- 일반 회사 (COMPANY_A) 로그인 시
|
|
SELECT * FROM numbering_rules
|
|
WHERE company_code = 'COMPANY_A';
|
|
|
|
-- 결과: 1건 (또는 0건)
|
|
-- 1. COMPANY_A 전용 규칙만 조회
|
|
-- company_code="*" 데이터는 제외됨!
|
|
```
|
|
|
|
```sql
|
|
-- 최고 관리자 (company_code = '*') 로그인 시
|
|
SELECT * FROM numbering_rules;
|
|
|
|
-- 결과: 모든 규칙 조회 가능
|
|
-- - SAMPLE_RULE (company_code = '*')
|
|
-- - 사번코드 (company_code = '*')
|
|
-- - COMPANY_A 전용 규칙
|
|
-- - COMPANY_B 전용 규칙
|
|
-- 등 모든 회사 데이터
|
|
```
|
|
|
|
---
|
|
|
|
## 🔍 상세 수정 내역
|
|
|
|
### 1. `getRuleList()` 메서드
|
|
|
|
**Before:**
|
|
```typescript
|
|
const query = `
|
|
SELECT * FROM numbering_rules
|
|
WHERE company_code = $1 OR company_code = '*'
|
|
`;
|
|
const result = await pool.query(query, [companyCode]);
|
|
```
|
|
|
|
**After:**
|
|
```typescript
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 데이터 조회
|
|
query = `SELECT * FROM numbering_rules ORDER BY created_at DESC`;
|
|
params = [];
|
|
logger.info("최고 관리자 전체 채번 규칙 조회");
|
|
} else {
|
|
// 일반 회사: 자신의 데이터만 조회
|
|
query = `SELECT * FROM numbering_rules WHERE company_code = $1 ORDER BY created_at DESC`;
|
|
params = [companyCode];
|
|
logger.info("회사별 채번 규칙 조회", { companyCode });
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
```
|
|
|
|
### 2. `getAvailableRulesForMenu()` 메서드
|
|
|
|
**Before:**
|
|
```typescript
|
|
// menuObjid 없을 때
|
|
const query = `
|
|
SELECT * FROM numbering_rules
|
|
WHERE (company_code = $1 OR company_code = '*')
|
|
AND scope_type = 'global'
|
|
`;
|
|
|
|
// menuObjid 있을 때
|
|
const query = `
|
|
SELECT * FROM numbering_rules
|
|
WHERE (company_code = $1 OR company_code = '*')
|
|
AND (scope_type = 'global' OR (scope_type = 'menu' AND menu_objid = $2))
|
|
`;
|
|
```
|
|
|
|
**After:**
|
|
```typescript
|
|
// 최고 관리자와 일반 회사를 명확히 구분
|
|
if (companyCode === "*") {
|
|
// 최고 관리자 쿼리
|
|
query = `SELECT * FROM numbering_rules WHERE scope_type = 'global'`;
|
|
} else {
|
|
// 일반 회사 쿼리 (company_code="*" 제외)
|
|
query = `SELECT * FROM numbering_rules WHERE company_code = $1 AND scope_type = 'global'`;
|
|
}
|
|
```
|
|
|
|
### 3. `getRuleById()` 메서드
|
|
|
|
**Before:**
|
|
```typescript
|
|
const query = `
|
|
SELECT * FROM numbering_rules
|
|
WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*')
|
|
`;
|
|
const result = await pool.query(query, [ruleId, companyCode]);
|
|
```
|
|
|
|
**After:**
|
|
```typescript
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: rule_id만 체크
|
|
query = `SELECT * FROM numbering_rules WHERE rule_id = $1`;
|
|
params = [ruleId];
|
|
} else {
|
|
// 일반 회사: rule_id + company_code 체크
|
|
query = `SELECT * FROM numbering_rules WHERE rule_id = $1 AND company_code = $2`;
|
|
params = [ruleId, companyCode];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 테스트 시나리오
|
|
|
|
### 시나리오 1: 최고 관리자 로그인
|
|
|
|
```bash
|
|
# 로그인
|
|
POST /api/auth/login
|
|
{
|
|
"userId": "admin",
|
|
"password": "****"
|
|
}
|
|
# → JWT 토큰에 companyCode = "*" 포함
|
|
|
|
# 채번 규칙 조회
|
|
GET /api/numbering-rules
|
|
Authorization: Bearer {token}
|
|
|
|
# 예상 결과: 모든 회사의 규칙 조회 가능
|
|
[
|
|
{ "ruleId": "SAMPLE_RULE", "companyCode": "*" },
|
|
{ "ruleId": "사번코드", "companyCode": "*" },
|
|
{ "ruleId": "COMPANY_A_RULE", "companyCode": "COMPANY_A" },
|
|
{ "ruleId": "COMPANY_B_RULE", "companyCode": "COMPANY_B" }
|
|
]
|
|
```
|
|
|
|
### 시나리오 2: 일반 회사 (COMPANY_A) 로그인
|
|
|
|
```bash
|
|
# 로그인
|
|
POST /api/auth/login
|
|
{
|
|
"userId": "user_a",
|
|
"password": "****"
|
|
}
|
|
# → JWT 토큰에 companyCode = "COMPANY_A" 포함
|
|
|
|
# 채번 규칙 조회
|
|
GET /api/numbering-rules
|
|
Authorization: Bearer {token}
|
|
|
|
# 예상 결과: 자신의 회사 규칙만 조회 (company_code="*" 제외)
|
|
[
|
|
{ "ruleId": "COMPANY_A_RULE", "companyCode": "COMPANY_A" }
|
|
]
|
|
```
|
|
|
|
### 시나리오 3: 일반 회사 (COMPANY_B) 로그인
|
|
|
|
```bash
|
|
# 로그인
|
|
POST /api/auth/login
|
|
{
|
|
"userId": "user_b",
|
|
"password": "****"
|
|
}
|
|
# → JWT 토큰에 companyCode = "COMPANY_B" 포함
|
|
|
|
# 채번 규칙 조회
|
|
GET /api/numbering-rules
|
|
Authorization: Bearer {token}
|
|
|
|
# 예상 결과: COMPANY_B 규칙만 조회
|
|
[
|
|
{ "ruleId": "COMPANY_B_RULE", "companyCode": "COMPANY_B" }
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 멀티테넌시 원칙 재확인
|
|
|
|
### 핵심 원칙
|
|
|
|
**company_code = "*"는 최고 관리자 전용 데이터이며, 일반 회사는 절대 접근할 수 없습니다.**
|
|
|
|
| 회사 코드 | 조회 가능 데이터 | 설명 |
|
|
|-----------|------------------|------|
|
|
| `*` (최고 관리자) | 모든 회사 데이터 | `company_code = "*"`, `"COMPANY_A"`, `"COMPANY_B"` 등 모두 조회 |
|
|
| `COMPANY_A` | `COMPANY_A` 데이터만 | `company_code = "*"` 데이터는 **절대 조회 불가** |
|
|
| `COMPANY_B` | `COMPANY_B` 데이터만 | `company_code = "*"` 데이터는 **절대 조회 불가** |
|
|
|
|
### SQL 패턴
|
|
|
|
```sql
|
|
-- ❌ 잘못된 패턴 (버그)
|
|
WHERE company_code = $1 OR company_code = '*'
|
|
|
|
-- ✅ 올바른 패턴 (최고 관리자)
|
|
WHERE 1=1 -- 모든 데이터
|
|
|
|
-- ✅ 올바른 패턴 (일반 회사)
|
|
WHERE company_code = $1 -- company_code="*" 자동 제외
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 추가 확인 사항
|
|
|
|
### 다른 서비스에도 같은 버그가 있을 가능성
|
|
|
|
다음 서비스들도 동일한 패턴으로 멀티테넌시 버그가 있는지 확인 필요:
|
|
|
|
- [ ] `backend-node/src/services/screenService.ts`
|
|
- [ ] `backend-node/src/services/tableService.ts`
|
|
- [ ] `backend-node/src/services/flowService.ts`
|
|
- [ ] `backend-node/src/services/adminService.ts`
|
|
- [ ] 기타 `company_code` 필터링을 사용하는 모든 서비스
|
|
|
|
### 확인 방법
|
|
|
|
```bash
|
|
# 잘못된 패턴 검색
|
|
cd backend-node/src/services
|
|
grep -n "OR company_code = '\*'" *.ts
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 배포 전 체크리스트
|
|
|
|
- [x] 코드 수정 완료
|
|
- [x] 린트 에러 없음
|
|
- [x] 로깅 추가 (최고 관리자 vs 일반 회사 구분)
|
|
- [ ] 단위 테스트 작성 (선택)
|
|
- [ ] 통합 테스트 (필수)
|
|
- [ ] 최고 관리자로 로그인하여 모든 규칙 조회 확인
|
|
- [ ] 일반 회사로 로그인하여 자신의 규칙만 조회 확인
|
|
- [ ] 다른 회사 규칙에 접근 불가능 확인
|
|
- [ ] 프론트엔드에서 채번 규칙 목록 재확인
|
|
- [ ] 백엔드 재실행 (코드 변경 사항 반영)
|
|
|
|
---
|
|
|
|
## 📚 관련 문서
|
|
|
|
- [멀티테넌시 필수 규칙](../README.md#멀티테넌시-필수-규칙)
|
|
- [채번 규칙 컴포넌트 구현 완료](./채번규칙_컴포넌트_구현_완료.md)
|
|
- [데이터베이스 스키마](../db/migrations/034_create_numbering_rules.sql)
|
|
|
|
---
|
|
|
|
**수정 완료일**: 2025-11-06
|
|
**수정자**: AI Assistant
|
|
**영향 범위**: `numberingRuleService.ts` 전체
|
|
|