# 사용자-사원 삭제 동기화 계획

> **작성일**: 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 스킬로 생성되었습니다.*