# 사용자-사원 삭제 동기화 계획 > **작성일**: 2026-03-12 > **목적**: MNG 사용자 관리 ↔ React 사원관리 간 삭제 동기화 수정 (멀티테넌트 고려) > **상태**: 🔄 진행중 --- ## 📍 현재 진행 상태 | 항목 | 내용 | |------|------| | **마지막 완료 작업** | Phase 4 데이터 정리 완료 | | **다음 작업** | 검증 및 커밋 | | **진행률** | 4/4 (100%) | | **마지막 업데이트** | 2026-03-13 01:10 | --- ## 1. 개요 ### 1.1 배경 MNG 사용자 관리(`/users`)와 React 사원관리(`/hr/employee-management`)가 동일한 DB 테이블을 공유하지만, 삭제 시 동기화가 불완전하여 다음 문제 발생: 1. **MNG 영구 삭제 시 orphan 발생**: `users` hard delete → `tenant_user_profiles` 잔존 → React에 유령 사원 노출 2. **React 퇴직 처리 시 계정 미차단**: `employee_status='resigned'`만 변경 → 해당 테넌트에 여전히 로그인 가능 3. **멀티테넌트 미고려**: 사용자가 여러 테넌트 소속 가능 → 특정 테넌트 퇴직이 전체 시스템에 영향 주면 안 됨 ### 1.2 테이블 관계 ``` users (전역 계정) ├── user_tenants (테넌트별 소속) ← is_active로 테넌트 접근 차단 └── tenant_user_profiles (테넌트별 사원 정보) ← employee_status로 HR 상태 관리 ``` ### 1.3 차단 레벨 정리 | 레벨 | 테이블.필드 | 범위 | 용도 | |------|-----------|------|------| | 전체 시스템 | `users.is_active` | 모든 테넌트 | 계정 완전 정지 | | **특정 테넌트** | **`user_tenants.is_active`** | **해당 테넌트만** | **퇴직자 접근 차단** | | HR 상태 | `tenant_user_profiles.employee_status` | 해당 테넌트 | 재직/휴직/퇴직 표시 | ### 1.4 성공 기준 - [ ] MNG 영구 삭제 시 orphan `tenant_user_profiles` 발생하지 않음 - [ ] React 퇴직 처리 시 해당 테넌트 접근 불가 (다른 테넌트는 정상) - [ ] 인증 시 `user_tenants.is_active` 체크하여 비활성 테넌트 차단 - [ ] 기존 orphan 데이터 정리 완료 ### 1.5 변경 승인 정책 | 분류 | 예시 | 승인 | |------|------|------| | ✅ 즉시 가능 | 기존 메서드에 DELETE 쿼리 추가 | 불필요 | | ⚠️ 컨펌 필요 | 인증 로직 변경 (미들웨어) | **필수** | | 🔴 금지 | users 테이블 구조 변경 | 별도 협의 | --- ## 2. 대상 범위 ### Phase 1: MNG forceDelete 수정 (영향: mng) — 위험도 🟢 | # | 작업 항목 | 상태 | 파일 | |---|----------|:----:|------| | 1.1 | `forceDeleteUser()`에 `tenant_user_profiles` DELETE 추가 | ✅ | `mng/app/Services/UserService.php` | **영향도 분석:** - 기존 `DB::transaction()` 내부에 1줄 추가 → 실패 시 자동 롤백 - `bulkForceDelete()`는 내부에서 `forceDeleteUser()` 루프 호출 → 자동 적용 - `deleteUser()` (소프트 삭제)는 수정 불필요 — 복구 가능성 유지 - 부작용 없음 ### Phase 2: React 퇴직 처리 수정 (영향: api) — 위험도 🟢 | # | 작업 항목 | 상태 | 파일 | |---|----------|:----:|------| | 2.1 | `destroy()`에서 `user_tenants.is_active = false` 추가 | ✅ | `api/app/Services/EmployeeService.php` | | 2.2 | `bulkDelete()`에도 동일 적용 (user_id 추출 필요) | ✅ | `api/app/Services/EmployeeService.php` | | 2.3 | `update()`에서 employee_status 변경 시 is_active 동기화 | ✅ | `api/app/Services/EmployeeService.php` | **영향도 분석:** - `destroy()`: 이미 `$tenantId`, `$profile->user_id` 확보됨 → 바로 사용 가능 - `bulkDelete()`: mass update 방식이라 `user_id` 목록 추출 후 `user_tenants` 업데이트 필요 - `getUserInfoForLogin()`이 이미 `is_active = 1` 필터링 → Phase 2 적용 즉시 로그인 시 해당 테넌트 미노출 - ⚠️ 복직 로직이 있다면 `is_active = true` 동기화 필요 (확인 필요) - 다른 테넌트 영향 없음 (WHERE에 `tenant_id` 포함) ### Phase 3: 테넌트 전환 차단 수정 (영향: api) — 위험도 🟡 ⚠️ 컨펌 필요 | # | 작업 항목 | 상태 | 파일 | |---|----------|:----:|------| | 3.1 | `SwitchTenantRequest`에 `user_tenants.is_active` 검증 추가 (방안 A 채택) | ✅ | `api/app/Http/Requests/User/SwitchTenantRequest.php` | | 3.2 | i18n 에러 메시지 추가 (`tenant_access_denied`) | ✅ | `api/lang/{ko,en}/error.php` | **영향도 분석:** - `getUserInfoForLogin()`: 이미 `is_active = 1` 필터링 ✅ (수정 불필요) - `switchMyTenant()`: `is_active` 체크 없음 ❌ → API 직접 호출로 비활성 테넌트 전환 가능 (보안 Gap) - `SwitchTenantRequest`: `exists:tenants,id`만 검증 → `user_tenants.is_active` 미검증 - **수정 방안 A (권장)**: `SwitchTenantRequest`에서 `Rule::exists('user_tenants')` + `is_active = 1` 검증 - **수정 방안 B**: `switchMyTenant()` 내부에서 `is_active` 체크 후 예외 throw - 정상 사용자 영향 없음 (UI에서 활성 테넌트만 노출) ### Phase 4: 기존 데이터 정리 (영향: DB) — 위험도 🟢 | # | 작업 항목 | 상태 | 대상 | |---|----------|:----:|------| | 4.1 | 개발서버 orphan profiles 11건 삭제 | ✅ | 개발 DB (sam) | | 4.2 | 로컬 데이터 정합성 확인 | ✅ | 로컬 DB (정리 완료) | | 4.3 | 운영 DB orphan 7건 삭제 | ✅ | 운영 DB (sam-prod) | --- ## 3. 작업 절차 ### Phase 1: MNG forceDelete 수정 ``` Step 1: UserService::forceDeleteUser() 분석 ├── 현재 트랜잭션 내 삭제 순서 확인 └── tenant_user_profiles DELETE 삽입 위치 결정 Step 2: tenant_user_profiles DELETE 추가 └── $user->forceDelete() 직전에 추가: DB::table('tenant_user_profiles') ->where('user_id', $user->id) ->delete(); ``` ### Phase 2: React 퇴직 처리 수정 ``` Step 1: EmployeeService::destroy() 수정 ├── employee_status = 'resigned' 후 └── user_tenants.is_active = false (해당 테넌트만) DB::table('user_tenants') ->where('user_id', $profile->user_id) ->where('tenant_id', $tenantId) ->update(['is_active' => false]); Step 2: EmployeeService::bulkDelete() 동일 적용 Step 3: 복직 처리 확인 └── 상태를 active로 되돌릴 때 user_tenants.is_active = true도 함께 ``` ### Phase 3: 인증 미들웨어 수정 ``` Step 1: 현재 인증 흐름 분석 ├── 로그인 시 테넌트 선택 로직 확인 └── 테넌트 전환 시 체크 로직 확인 Step 2: user_tenants.is_active 체크 추가 ├── 테넌트 접근 시 is_active = false면 거부 └── 에러 메시지: "해당 테넌트에 대한 접근 권한이 없습니다" ``` --- ## 4. 의존성 및 실행 순서 ### 4.1 의존성 맵 ``` Phase 1 (MNG forceDelete) ─── 독립 실행 가능 (mng 저장소) Phase 2 (React 퇴직처리) ──┐ ├── Phase 3 적용 시 완전한 차단 (api 저장소) Phase 3 (switchMyTenant) ──┘ Phase 4 (데이터 정리) ─── Phase 1 이후 실행 권장 (DB) ``` ### 4.2 권장 실행 순서 | 순서 | Phase | 이유 | |------|-------|------| | 1 | Phase 1 + Phase 2 병렬 | 서로 다른 저장소(mng/api), 독립적 | | 2 | Phase 3 | Phase 2의 `is_active = false` 설정을 전환 시 차단 (⚠️ 컨펌 필요) | | 3 | Phase 4 | Phase 1 적용 후 orphan 재발 방지 확인 → 기존 orphan 정리 | --- ## 5. 상세 작업 내용 > 각 Phase 진행 후 이 섹션에 상세 내용 추가 --- ## 6. 컨펌 대기 목록 | # | 항목 | 변경 내용 | 영향 범위 | 상태 | |---|------|----------|----------|------| | 1 | Phase 3 switchMyTenant | 테넌트 전환 시 is_active 체크 | 전체 테넌트 전환 흐름 | ⚠️ 컨펌 필요 | | 2 | Phase 3 수정 방안 선택 | A: SwitchTenantRequest / B: switchMyTenant() 내부 | 인증 흐름 | ⚠️ 선택 필요 | | 3 | Phase 2.3 복직 로직 | is_active = true 복원 동기화 | 퇴직→복직 흐름 | ⚠️ 확인 필요 | --- ## 7. 변경 이력 | 날짜 | 항목 | 변경 내용 | 파일 | 승인 | |------|------|----------|------|------| | 2026-03-12 | - | 문서 초안 작성 | - | - | | 2026-03-13 | 영향도 분석 | 4개 Phase 코드 분석 + 위험도 평가 + 실행 순서 결정 | - | - | --- ## 8. 참고 문서 - `docs/system/database/tenants.md` — 테넌트/사용자 DB 구조 - `docs/rules/employee-api.md` — 사원관리 API 규칙 - `docs/system/security-policy.md` — 보안 아키텍처 (인증 레이어) - `docs/dev/standards/quality-checklist.md` — 품질 체크리스트 --- ## 9. 검증 결과 > 작업 완료 후 이 섹션에 검증 결과 추가 ### 9.1 테스트 케이스 | 시나리오 | 예상 결과 | 실제 결과 | 상태 | |--------|----------|----------|------| | MNG에서 사용자 영구 삭제 | tenant_user_profiles도 삭제됨 | | ⏳ | | React에서 퇴직 처리 | user_tenants.is_active = false | | ⏳ | | 퇴직자가 해당 테넌트 접근 시도 | 접근 거부 | | ⏳ | | 퇴직자가 다른 테넌트 접근 | 정상 접근 | | ⏳ | | 복직 처리 후 테넌트 접근 | 정상 접근 | | ⏳ | --- *이 문서는 /plan 스킬로 생성되었습니다.*