feat: UserApi.php Swagger 점검 및 개선 (Phase 3-4)
- UserUpdateRequest.php 생성 (검증 로직 분리) - PasswordChangeRequest.php 생성 (비밀번호 변경 검증) - SwitchTenantRequest.php 생성 (테넌트 전환 검증) - UserApi.php에 Request 스키마 추가 - UserController.php FormRequest 적용 및 DI 패턴 적용 - MemberService static 호출 → DI 인스턴스 호출 - lang/ko/message.php user 메시지 키 추가 - SAM API Development Rules 준수 완료
This commit is contained in:
@@ -4,59 +4,62 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\PasswordChangeRequest;
|
||||
use App\Http\Requests\User\SwitchTenantRequest;
|
||||
use App\Http\Requests\User\UserUpdateRequest;
|
||||
use App\Services\MemberService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct(private MemberService $service) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return MemberService::getMembers($request->all());
|
||||
}, '회원목록 조회');
|
||||
return $this->service->getMembers($request->all());
|
||||
}, __('message.user.fetched'));
|
||||
}
|
||||
|
||||
public function show($userNo)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($userNo) {
|
||||
return MemberService::getMember($userNo);
|
||||
}, '회원 상세조회');
|
||||
return $this->service->getMember($userNo);
|
||||
}, __('message.user.fetched'));
|
||||
}
|
||||
|
||||
public function me(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return MemberService::getMyInfo($request);
|
||||
}, '나의 정보 조회');
|
||||
return $this->service->getMyInfo($request);
|
||||
}, __('message.user.me_fetched'));
|
||||
}
|
||||
|
||||
public function meUpdate(Request $request)
|
||||
public function meUpdate(UserUpdateRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return MemberService::getMyUpdate($request);
|
||||
}, '나의 정보 수정');
|
||||
return $this->service->getMyUpdate($request);
|
||||
}, __('message.user.me_updated'));
|
||||
}
|
||||
|
||||
public function changePassword(Request $request)
|
||||
public function changePassword(PasswordChangeRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return MemberService::setMyPassword($request);
|
||||
}, '나의 비밀번호 수정');
|
||||
return $this->service->setMyPassword($request);
|
||||
}, __('message.user.password_changed'));
|
||||
}
|
||||
|
||||
public function tenants(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return MemberService::getMyTenants($request);
|
||||
}, '나의 테넌트 목록 조회');
|
||||
return $this->service->getMyTenants($request);
|
||||
}, __('message.user.tenants_fetched'));
|
||||
}
|
||||
|
||||
public function switchTenant(Request $request)
|
||||
public function switchTenant(SwitchTenantRequest $request)
|
||||
{
|
||||
$tenant_id = $request->tenant_id;
|
||||
|
||||
return ApiResponse::handle(function () use ($tenant_id) {
|
||||
return MemberService::switchMyTenant($tenant_id);
|
||||
}, '활성 테넌트 전환');
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->switchMyTenant($request->validated()['tenant_id']);
|
||||
}, __('message.user.tenant_switched'));
|
||||
}
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/User/PasswordChangeRequest.php
Normal file
22
app/Http/Requests/User/PasswordChangeRequest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PasswordChangeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'current_password' => 'required|string',
|
||||
'new_password' => 'required|string|min:8|confirmed',
|
||||
'new_password_confirmation' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
20
app/Http/Requests/User/SwitchTenantRequest.php
Normal file
20
app/Http/Requests/User/SwitchTenantRequest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SwitchTenantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => 'required|integer|exists:tenants,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/User/UserUpdateRequest.php
Normal file
22
app/Http/Requests/User/UserUpdateRequest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UserUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'email' => 'sometimes|email|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,36 @@
|
||||
* @OA\Property(property="user", ref="#/components/schemas/Member"),
|
||||
* @OA\Property(property="tenant", ref="#/components/schemas/TenantBrief")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="UserUpdateRequest",
|
||||
* type="object",
|
||||
* description="사용자 정보 수정 요청",
|
||||
*
|
||||
* @OA\Property(property="name", type="string", maxLength=100, example="홍길동"),
|
||||
* @OA\Property(property="phone", type="string", nullable=true, maxLength=20, example="010-1234-5678"),
|
||||
* @OA\Property(property="email", type="string", maxLength=100, example="user@example.com")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PasswordChangeRequest",
|
||||
* type="object",
|
||||
* required={"current_password","new_password","new_password_confirmation"},
|
||||
* description="비밀번호 변경 요청",
|
||||
*
|
||||
* @OA\Property(property="current_password", type="string", format="password", example="current123"),
|
||||
* @OA\Property(property="new_password", type="string", format="password", minLength=8, example="newpass123"),
|
||||
* @OA\Property(property="new_password_confirmation", type="string", format="password", example="newpass123")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="SwitchTenantRequest",
|
||||
* type="object",
|
||||
* required={"tenant_id"},
|
||||
* description="테넌트 전환 요청",
|
||||
*
|
||||
* @OA\Property(property="tenant_id", type="integer", example=2)
|
||||
* )
|
||||
*/
|
||||
class UserApi
|
||||
{
|
||||
|
||||
284
claudedocs/SWAGGER_PHASE3_3_CLIENT.md
Normal file
284
claudedocs/SWAGGER_PHASE3_3_CLIENT.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# Client API Swagger 점검 및 개선 (Phase 3-3)
|
||||
|
||||
**날짜:** 2025-11-07
|
||||
**작업자:** Claude Code
|
||||
**이슈:** Phase 3-3: ClientApi.php Swagger 점검 및 개선
|
||||
|
||||
## 📋 변경 개요
|
||||
|
||||
ClientApi.php Swagger 문서 점검 후, SAM API Development Rules에 따라:
|
||||
- **FormRequest 적용**: ClientStoreRequest, ClientUpdateRequest 생성
|
||||
- **i18n 메시지 키 적용**: 리소스별 키 사용 (`message.client.xxx`)
|
||||
- **Controller 패턴 통일**: ApiResponse::handle 두 번째 인자로 메시지 전달
|
||||
- **코드 간소화**: 불필요한 배열 래핑 제거
|
||||
|
||||
## 🔍 분석 결과
|
||||
|
||||
### ✅ 좋은 점
|
||||
1. **Swagger 구조**: ClientApi.php가 별도 파일로 잘 분리되어 있음
|
||||
2. **스키마 완성도**: Client, ClientPagination, ClientCreateRequest, ClientUpdateRequest 모두 정의됨
|
||||
3. **Controller 간결함**: Swagger 주석이 없어서 깔끔함 (Phase 3-1, 3-2와 동일)
|
||||
4. **경로 일치성**: Swagger와 실제 Route 경로가 일치 (`/api/v1/clients`)
|
||||
|
||||
### ⚠️ 개선이 필요한 점
|
||||
1. **FormRequest 누락**: ClientStoreRequest, ClientUpdateRequest 없음
|
||||
2. **i18n 메시지**: 공통 키만 사용 (`message.fetched`, `message.created`) - 리소스별 키 필요
|
||||
3. **Controller 패턴 불일치**:
|
||||
- 기존: `return ['data' => $data, 'message' => __('message.xxx')]` (배열 래핑)
|
||||
- Product/Material: `return $this->service->xxx()` + ApiResponse::handle 두 번째 인자
|
||||
4. **Constructor 스타일**: `protected` + 수동 할당 (PHP 8.2+ constructor property promotion 미사용)
|
||||
|
||||
## 📁 수정된 파일
|
||||
|
||||
### 1. `app/Http/Requests/Client/ClientStoreRequest.php` (신규 생성)
|
||||
**목적:** 거래처 생성 시 입력 검증을 FormRequest로 분리
|
||||
|
||||
**주요 내용:**
|
||||
```php
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client_group_id' => 'nullable|integer',
|
||||
'client_code' => 'required|string|max:50',
|
||||
'name' => 'required|string|max:100',
|
||||
'contact_person' => 'nullable|string|max:100',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'email' => 'nullable|email|max:100',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'is_active' => 'nullable|in:Y,N',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
**검증 규칙:**
|
||||
- **필수 필드**: client_code, name
|
||||
- **이메일 검증**: email 규칙 적용
|
||||
- **제약 조건**: is_active는 Y/N만 허용
|
||||
|
||||
### 2. `app/Http/Requests/Client/ClientUpdateRequest.php` (신규 생성)
|
||||
**목적:** 거래처 수정 시 입력 검증을 FormRequest로 분리
|
||||
|
||||
**주요 내용:**
|
||||
- StoreRequest와 동일한 필드 구조
|
||||
- client_code, name에 'sometimes' 규칙 적용 (부분 업데이트 지원)
|
||||
- 나머지 필드는 nullable (선택적 업데이트)
|
||||
|
||||
### 3. `app/Http/Controllers/Api/V1/ClientController.php` (수정)
|
||||
**변경 전:** 73줄
|
||||
```php
|
||||
class ClientController extends Controller
|
||||
{
|
||||
protected ClientService $service;
|
||||
|
||||
public function __construct(ClientService $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$data = $this->service->index($request->all());
|
||||
|
||||
return ['data' => $data, 'message' => __('message.fetched')];
|
||||
});
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$data = $this->service->store($request->all());
|
||||
|
||||
return ['data' => $data, 'message' => __('message.created')];
|
||||
});
|
||||
}
|
||||
// ... 나머지 메서드들도 동일한 패턴
|
||||
}
|
||||
```
|
||||
|
||||
**변경 후:** 58줄
|
||||
```php
|
||||
class ClientController extends Controller
|
||||
{
|
||||
public function __construct(private ClientService $service) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.client.fetched'));
|
||||
}
|
||||
|
||||
public function store(ClientStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->store($request->validated());
|
||||
}, __('message.client.created'));
|
||||
}
|
||||
|
||||
public function update(ClientUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.client.updated'));
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->service->destroy($id);
|
||||
return 'success';
|
||||
}, __('message.client.deleted'));
|
||||
}
|
||||
|
||||
public function toggle(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->toggle($id);
|
||||
}, __('message.client.toggled'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**주요 변경사항:**
|
||||
1. **Constructor Property Promotion**: `protected` + 수동 할당 → `private` + 자동 할당
|
||||
2. **FormRequest 적용**: `Request` → `ClientStoreRequest`, `ClientUpdateRequest`
|
||||
3. **validated() 사용**: `$request->all()` → `$request->validated()` (보안 강화)
|
||||
4. **패턴 통일**: 배열 래핑 제거, ApiResponse::handle 두 번째 인자로 메시지 전달
|
||||
5. **i18n 키 사용**: `message.xxx` → `message.client.xxx` (리소스별 키)
|
||||
|
||||
**적용된 메서드:**
|
||||
- index(): `__('message.client.fetched')`
|
||||
- show(): `__('message.client.fetched')`
|
||||
- store(): `__('message.client.created')`
|
||||
- update(): `__('message.client.updated')`
|
||||
- destroy(): `__('message.client.deleted')`
|
||||
- toggle(): `__('message.client.toggled')`
|
||||
|
||||
### 4. `lang/ko/message.php` (수정)
|
||||
**추가된 내용:**
|
||||
```php
|
||||
// 거래처 관리
|
||||
'client' => [
|
||||
'fetched' => '거래처를 조회했습니다.',
|
||||
'created' => '거래처가 등록되었습니다.',
|
||||
'updated' => '거래처가 수정되었습니다.',
|
||||
'deleted' => '거래처가 삭제되었습니다.',
|
||||
'toggled' => '거래처 상태가 변경되었습니다.',
|
||||
],
|
||||
```
|
||||
|
||||
**추가 이유:**
|
||||
- Product, Material과 일관성 유지
|
||||
- 리소스별 명확한 메시지 제공
|
||||
- toggle() 메서드용 메시지 추가 (상태 변경 전용)
|
||||
|
||||
## 🔍 SAM API Rules 준수 확인
|
||||
|
||||
### ✅ 준수 항목
|
||||
|
||||
1. **Swagger 주석 분리**
|
||||
- ClientApi.php에 모든 Swagger 주석 집중 (이미 준수됨)
|
||||
- Controller는 비즈니스 로직만 유지
|
||||
|
||||
2. **FormRequest 사용**
|
||||
- ClientStoreRequest, ClientUpdateRequest 생성
|
||||
- Controller에서 타입 힌트 적용
|
||||
- `$request->validated()` 사용
|
||||
|
||||
3. **i18n 메시지 키 사용**
|
||||
- 모든 공통 키를 리소스별 키로 변경
|
||||
- `__('message.client.xxx')` 형식 적용
|
||||
|
||||
4. **Controller 패턴 통일**
|
||||
- Product, Material과 동일한 패턴 적용
|
||||
- ApiResponse::handle 두 번째 인자로 메시지 전달
|
||||
- 불필요한 배열 래핑 제거
|
||||
|
||||
5. **Service-First 패턴**
|
||||
- 비즈니스 로직은 ClientService에 유지
|
||||
- Controller는 DI + ApiResponse::handle()만 사용
|
||||
|
||||
6. **Modern PHP 문법**
|
||||
- Constructor Property Promotion 사용 (PHP 8.0+)
|
||||
- Private property로 캡슐화 강화
|
||||
|
||||
## ✅ 테스트 체크리스트
|
||||
|
||||
- [x] PHP 문법 체크 (php -l)
|
||||
- [x] ClientStoreRequest 문법 확인
|
||||
- [x] ClientUpdateRequest 문법 확인
|
||||
- [x] ClientController 문법 확인
|
||||
- [x] message.php 문법 확인
|
||||
- [ ] Swagger 재생성 (`php artisan l5-swagger:generate`)
|
||||
- [ ] Swagger UI 확인 (http://api.sam.kr/api-docs/index.html)
|
||||
- [ ] 실제 API 호출 테스트
|
||||
- [ ] GET /api/v1/clients (목록 조회)
|
||||
- [ ] POST /api/v1/clients (FormRequest 검증 확인)
|
||||
- [ ] GET /api/v1/clients/{id} (단건 조회)
|
||||
- [ ] PUT /api/v1/clients/{id} (FormRequest 검증 확인)
|
||||
- [ ] DELETE /api/v1/clients/{id} (삭제)
|
||||
- [ ] PATCH /api/v1/clients/{id}/toggle (상태 토글)
|
||||
|
||||
## ⚠️ 배포 시 주의사항
|
||||
|
||||
1. **FormRequest 적용으로 검증 로직 변경**
|
||||
- 기존: Service에서 모든 검증 (추정)
|
||||
- 변경 후: FormRequest(기본 검증) + Service(비즈니스 검증)
|
||||
- **영향:** 검증 에러 응답 형식 동일 (422 Unprocessable Entity)
|
||||
|
||||
2. **i18n 메시지 변경**
|
||||
- 기존: `__('message.fetched')`, `__('message.created')` (공통 키)
|
||||
- 변경 후: `__('message.client.xxx')` (리소스별 키)
|
||||
- **영향:** 응답 메시지 내용 약간 변경 (의미는 동일)
|
||||
|
||||
3. **Controller 응답 패턴 변경**
|
||||
- 기존: `return ['data' => $data, 'message' => __('message.xxx')]`
|
||||
- 변경 후: `return $data` + ApiResponse::handle 두 번째 인자
|
||||
- **영향:** 응답 JSON 구조는 동일 (ApiResponse::handle이 래핑 처리)
|
||||
|
||||
4. **코드 간소화**
|
||||
- 73줄 → 58줄 (15줄 감소)
|
||||
- **영향:** 유지보수성 향상, 기능은 동일
|
||||
|
||||
## 📊 변경 통계
|
||||
|
||||
- **신규 파일:** 2개 (FormRequest)
|
||||
- **수정 파일:** 2개 (ClientController.php, message.php)
|
||||
- **삭제 파일:** 0개
|
||||
- **코드 감소:** -15줄 (Controller 간소화)
|
||||
- **실질 추가:** +60줄 (FormRequest)
|
||||
- **SAM 규칙 준수:** 100%
|
||||
|
||||
## 🎯 다음 작업
|
||||
|
||||
1. Swagger 재생성 및 검증
|
||||
2. 실제 API 테스트
|
||||
3. Phase 3-4: UserApi.php Swagger 점검
|
||||
|
||||
## 🔗 관련 문서
|
||||
|
||||
- `CLAUDE.md` - SAM 프로젝트 가이드
|
||||
- `SWAGGER_AUDIT.md` - Swagger 전체 점검 현황
|
||||
- `SWAGGER_PHASE3_1_PRODUCT.md` - Phase 3-1 작업 문서
|
||||
- `SWAGGER_PHASE3_2_MATERIAL.md` - Phase 3-2 작업 문서
|
||||
- SAM API Development Rules (CLAUDE.md 내 섹션)
|
||||
|
||||
## 📝 커밋 정보
|
||||
|
||||
**커밋 해시:** c87aadc
|
||||
**커밋 메시지:**
|
||||
```
|
||||
feat: ClientApi.php Swagger 점검 및 개선 (Phase 3-3)
|
||||
|
||||
- ClientStoreRequest.php 생성 (검증 로직 분리)
|
||||
- ClientUpdateRequest.php 생성 (검증 로직 분리)
|
||||
- ClientController.php FormRequest 적용 및 패턴 통일
|
||||
- lang/ko/message.php client 메시지 키 추가
|
||||
- ApiResponse::handle 패턴 통일 (메시지 두 번째 인자)
|
||||
- SAM API Development Rules 준수 완료
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Phase 3-3 완료 ✅**
|
||||
@@ -100,6 +100,16 @@
|
||||
'toggled' => '거래처 상태가 변경되었습니다.',
|
||||
],
|
||||
|
||||
// 사용자 관리
|
||||
'user' => [
|
||||
'fetched' => '사용자를 조회했습니다.',
|
||||
'me_fetched' => '나의 정보를 조회했습니다.',
|
||||
'me_updated' => '나의 정보가 수정되었습니다.',
|
||||
'password_changed' => '비밀번호가 변경되었습니다.',
|
||||
'tenants_fetched' => '나의 테넌트 목록을 조회했습니다.',
|
||||
'tenant_switched' => '활성 테넌트가 전환되었습니다.',
|
||||
],
|
||||
|
||||
// 파일 관리
|
||||
'file' => [
|
||||
'uploaded' => '파일이 업로드되었습니다.',
|
||||
|
||||
Reference in New Issue
Block a user