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