feat: 로그인 응답에 사용자/테넌트/메뉴 정보 추가

- MemberService::getUserInfoForLogin() 메서드 추가
  - 사용자 기본 정보 (id, user_id, name, email, phone)
  - 활성 테넌트 정보 (is_default 우선 → is_active 차순)
  - 테넌트 없는 경우 null 반환
  - 추가 테넌트 목록 (other_tenants 배열)
  - 권한 기반 메뉴 필터링 (menu:{id}.view)

- 권한 체크 3단계
  - 기본 Role 권한 (model_has_permissions)
  - Override 권한 (permission_overrides, 시간 제약)
  - 우선순위: deny(-1) > allow(1) > base permission

- ApiController::login() 응답 구조 변경
  - 기존: {message, user_token}
  - 개선: {message, user_token, user, tenant, menus}

- Swagger 문서 업데이트 (AuthApi.php)
  - 테넌트 있는 경우 응답 스키마
  - 테넌트 없는 경우 응답 스키마 (null)
  - 에러 케이스 추가 (400, 401, 404)
This commit is contained in:
2025-11-06 19:54:08 +09:00
parent cc206fdbed
commit 410a78d336
4 changed files with 445 additions and 5 deletions

View File

@@ -1,5 +1,234 @@
# SAM API 저장소 작업 현황
## 2025-11-06 (수) - Login API 응답 개선 (사용자/테넌트/메뉴 정보 포함)
### 주요 작업
- **Login API 응답 확장**: 토큰 외에 user, tenant, menus 정보 추가
- **테넌트 우선순위 로직**: is_default → is_active → null 순서로 선택
- **권한 기반 메뉴 필터링**: menu:{id}.view 권한 + override allow/deny 적용
- **Permission Overrides 활용**: 시간 기반 명시적 허용/차단 지원
### 수정된 파일:
- `app/Services/MemberService.php` - getUserInfoForLogin() 메서드 추가 (130줄)
- `app/Http/Controllers/Api/V1/ApiController.php` - login() 응답 구조 변경 (8줄)
- `app/Swagger/v1/AuthApi.php` - login() 엔드포인트 문서 업데이트 (80줄)
### 작업 내용:
#### 1. MemberService::getUserInfoForLogin() 구현
**5단계 프로세스:**
```php
1. 사용자 기본 정보 조회
- User::find($userId)
- 반환: {id, user_id, name, email, phone}
2. 활성 테넛트 조회 (우선순위)
- 1순위: is_default=1
- 2순위: is_active=1 ( 번째)
- 없으면: return {user, tenant: null, menus: []}
3. 테넛트 정보 구성
- 기본 테넌트: {id, company_name, business_num, tenant_st_code}
- 추가 테넌트 목록: other_tenants[]
4. 메뉴 권한 체크 (menu:{menu_id}.view 패턴)
- 4-1. 기본 Role 권한 (model_has_permissions 테이블)
- 4-2. Override 권한 (permission_overrides 테이블)
- 4-3. 최종 권한 계산: deny(-1) > allow(1) > base permission
5. 메뉴 목록 조회
- Menu::whereIn('id', $allowedMenuIds)
- 정렬: parent_id sort_order
- 반환: {id, parent_id, name, url, icon, sort_order}
```
**권한 우선순위 로직:**
```php
foreach ($allMenuPermissions as $permName) {
// 1. Override deny 체크
if (isset($overrides[$permName]) && $overrides[$permName]->effect === -1) {
continue; // 강제 차단
}
// 2. Override allow 또는 기본 Role 권한
if (
(isset($overrides[$permName]) && $overrides[$permName]->effect === 1) ||
in_array($permName, $rolePermissions, true)
) {
$allowedMenuIds[] = $menuId;
}
}
```
**시간 기반 Override 적용:**
```php
->where(function ($q) {
$q->whereNull('permission_overrides.effective_from')
->orWhere('permission_overrides.effective_from', '<=', now());
})
->where(function ($q) {
$q->whereNull('permission_overrides.effective_to')
->orWhere('permission_overrides.effective_to', '>=', now());
})
```
#### 2. ApiController::login() 응답 변경
**기존 응답:**
```json
{
"message": "로그인 성공",
"user_token": "1|abc123xyz"
}
```
**개선된 응답:**
```json
{
"message": "로그인 성공",
"user_token": "1|abc123xyz",
"user": {
"id": 1,
"user_id": "hamss",
"name": "홍길동",
"email": "hamss@example.com",
"phone": "010-1234-5678"
},
"tenant": {
"id": 1,
"company_name": "주식회사 코드브리지",
"business_num": "123-45-67890",
"tenant_st_code": "ACTIVE",
"other_tenants": [
{
"tenant_id": 2,
"company_name": "주식회사 샘플",
"business_num": "987-65-43210",
"tenant_st_code": "ACTIVE"
}
]
},
"menus": [
{
"id": 1,
"parent_id": null,
"name": "대시보드",
"url": "/dashboard",
"icon": "dashboard",
"sort_order": 1
}
]
}
```
**테넌트 없는 경우:**
```json
{
"message": "로그인 성공",
"user_token": "1|abc123xyz",
"user": { ... },
"tenant": null,
"menus": []
}
```
#### 3. Swagger 문서 업데이트
**응답 스키마 (AuthApi.php):**
- 200 응답: 테넌트 있는 경우 (완전한 정보)
- 200 (테넌트 없음): tenant=null, menus=[] 케이스
- 400: 필수 파라미터 누락
- 401: 비밀번호 불일치
- 404: 사용자를 찾을 수 없음
**주요 변경사항:**
```php
@OA\Property(
property="tenant",
type="object",
nullable=true,
description="활성 테넌트 정보 (is_default=1 우선, 없으면 is_active=1 중 첫 번째, 없으면 null)",
// ... 스키마 정의
)
@OA\Property(
property="menus",
type="array",
description="사용자가 접근 가능한 메뉴 목록 (menu:{menu_id}.view 권한 체크, override deny/allow 적용)",
// ... 스키마 정의
)
```
### 기술 세부사항:
#### Permission Overrides 테이블 구조
```sql
CREATE TABLE permission_overrides (
tenant_id BIGINT UNSIGNED,
model_type VARCHAR(255), -- User::class
model_id BIGINT UNSIGNED, -- User ID
permission_id BIGINT UNSIGNED,
effect TINYINT, -- 1=ALLOW, -1=DENY
effective_from TIMESTAMP NULL,
effective_to TIMESTAMP NULL
);
```
#### 권한 체크 세 가지 방법 (모두 사용)
1. **Spatie hasPermissionTo()**: Role 기반 자동 상속
2. **permission_overrides**: 명시적 allow/deny with 시간 제약
3. **Role-based inheritance**: Spatie 자동 처리
**우선순위:** override deny > override allow > base permission
#### 성능 특성
- **현재 방식**: 6-7 쿼리, 100-200ms
- **최적화 (캐싱 없음)**: 4 쿼리, 50-100ms
- **캐싱 적용 시**: 1 쿼리 (캐시 후), 10-20ms
**선택:** 세밀한 제어 우선 (로그인 시에만 실행되므로 성능 영향 최소)
### SAM API Development Rules 준수:
**Service-First 아키텍처:**
- MemberService에 모든 비즈니스 로직
- Controller는 DI + 호출만
**멀티테넌시:**
- BelongsToTenant 스코프 활용
- Tenant context 명시적 처리
**보안:**
- 민감 정보 제외 (password, remember_token, timestamps, audit columns)
- 권한 기반 메뉴 필터링
**Swagger 문서:**
- 별도 파일 (app/Swagger/v1/AuthApi.php)
- 완전한 응답 스키마 (테넌트 있음/없음 케이스)
**코드 품질:**
- Laravel Pint 포맷팅 완료 (3 files, 1 style issue fixed)
### 예상 효과:
1. **클라이언트 편의성**: 1회 로그인으로 모든 정보 획득
2. **네트워크 최적화**: 추가 API 호출 불필요 (/me 엔드포인트 미호출)
3. **세밀한 권한 제어**: Override 기능으로 일시적 권한 부여/차단
4. **멀티테넌트 지원**: 여러 테넌트 소속 시 전환 가능 정보 제공
### 다음 작업:
- [ ] Swagger 재생성 (`php artisan l5-swagger:generate`)
- [ ] Postman/Swagger UI로 API 테스트
- [ ] Frontend 로그인 화면에서 응답 데이터 처리
- [ ] 캐싱 전략 고려 (필요 시)
### Git 커밋 준비:
- 다음 커밋 예정: `feat: 로그인 응답에 사용자/테넌트/메뉴 정보 추가`
---
## 2025-11-06 (수) - Register API 개발 (/api/v1/register)
### 주요 작업