9.9 KiB
9.9 KiB
카테고리 멀티테넌시 버그 수정 완료
작성일: 2025-11-06
상태: ✅ 완료
🐛 문제 발견
증상
- 다른 회사 계정으로 로그인했는데
company_code = "*"(최고 관리자 전용) 카테고리 값이 보임 - 채번 규칙과 동일한 멀티테넌시 버그
원인
backend-node/src/services/tableCategoryValueService.ts의 7개 메서드에서 잘못된 WHERE 조건 사용:
// ❌ 잘못된 쿼리 (버그)
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:
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:
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:
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:
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:
const updateQuery = `
UPDATE table_column_category_values
SET ...
WHERE value_id = $${paramIndex++}
AND (company_code = $${paramIndex++} OR company_code = '*') -- 🔴 버그!
`;
After:
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++}
`;
}
🔍 데이터베이스 현황
현재 카테고리 값 (수정 전)
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: 최고 관리자로 카테고리 값 조회
# 로그인
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: 일반 회사로 카테고리 값 조회
# 로그인
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: 카테고리 값 수정 (권한 체크)
# 일반 회사 A로 로그인
# company_code="*" 데이터 수정 시도
PUT /api/table-category-values/1
{ "valueLabel": "해킹 시도" }
# 수정 전: 성공 (보안 취약)
# 수정 후: 실패 (권한 없음)
{ "success": false, "message": "카테고리 값을 찾을 수 없습니다" }
📝 수정 상세 내역
공통 패턴
모든 메서드에 다음 패턴 적용:
// 멀티테넌시: 최고 관리자만 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 });
}
로깅 추가
각 메서드에 멀티테넌시 로깅 추가:
// 최고 관리자
logger.info("최고 관리자 카테고리 컬럼 조회");
logger.info("최고 관리자 카테고리 값 조회");
// 일반 회사
logger.info("회사별 카테고리 컬럼 조회", { companyCode });
logger.info("회사별 카테고리 값 조회", { companyCode });
🎯 멀티테넌시 원칙 재확인
핵심 원칙
company_code = "*"는 최고 관리자 전용 데이터이며, 일반 회사는 절대 접근할 수 없습니다.
| 작업 | 최고 관리자 (*) | 일반 회사 (COMPANY_A) |
|---|---|---|
| 조회 | 모든 데이터 | 자신의 데이터만 |
| 생성 | 모든 회사에 | 자신의 회사에만 |
| 수정 | 모든 데이터 | 자신의 데이터만 |
| 삭제 | 모든 데이터 | 자신의 데이터만 |
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(수정 완료)
🚀 배포 전 체크리스트
- 코드 수정 완료 (7개 메서드)
- 린트 에러 없음
- 로깅 추가 (최고 관리자 vs 일반 회사 구분)
- 단위 테스트 작성 (선택)
- 통합 테스트 (필수)
- 최고 관리자로 로그인하여 모든 카테고리 값 조회 확인
- 일반 회사로 로그인하여 자신의 카테고리 값만 조회 확인
- 다른 회사 카테고리 값 접근 불가능 확인
- 카테고리 값 생성/수정/삭제 권한 확인
- 프론트엔드에서 카테고리 값 목록 재확인
- 백엔드 재실행 (코드 변경 사항 반영)
📚 관련 문서
🔍 다른 서비스 확인 결과
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개 메서드)
린트 에러: 없음