From 97c0d8245e833bccc9020fb99bd70403ef44314e Mon Sep 17 00:00:00 2001 From: hskwon Date: Fri, 7 Nov 2025 02:44:11 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20UserApi.php=20Swagger=20=EC=A0=90?= =?UTF-8?q?=EA=B2=80=20=EB=B0=8F=20=EA=B0=9C=EC=84=A0=20(Phase=203-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 준수 완료 --- .../Controllers/Api/V1/UserController.php | 45 +-- .../Requests/User/PasswordChangeRequest.php | 22 ++ .../Requests/User/SwitchTenantRequest.php | 20 ++ app/Http/Requests/User/UserUpdateRequest.php | 22 ++ app/Swagger/v1/UserApi.php | 30 ++ claudedocs/SWAGGER_PHASE3_3_CLIENT.md | 284 ++++++++++++++++++ lang/ko/message.php | 10 + 7 files changed, 412 insertions(+), 21 deletions(-) create mode 100644 app/Http/Requests/User/PasswordChangeRequest.php create mode 100644 app/Http/Requests/User/SwitchTenantRequest.php create mode 100644 app/Http/Requests/User/UserUpdateRequest.php create mode 100644 claudedocs/SWAGGER_PHASE3_3_CLIENT.md diff --git a/app/Http/Controllers/Api/V1/UserController.php b/app/Http/Controllers/Api/V1/UserController.php index 3830ec4..9b8fc73 100644 --- a/app/Http/Controllers/Api/V1/UserController.php +++ b/app/Http/Controllers/Api/V1/UserController.php @@ -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')); } -} +} \ No newline at end of file diff --git a/app/Http/Requests/User/PasswordChangeRequest.php b/app/Http/Requests/User/PasswordChangeRequest.php new file mode 100644 index 0000000..17e250c --- /dev/null +++ b/app/Http/Requests/User/PasswordChangeRequest.php @@ -0,0 +1,22 @@ + 'required|string', + 'new_password' => 'required|string|min:8|confirmed', + 'new_password_confirmation' => 'required|string', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Requests/User/SwitchTenantRequest.php b/app/Http/Requests/User/SwitchTenantRequest.php new file mode 100644 index 0000000..78a0a2d --- /dev/null +++ b/app/Http/Requests/User/SwitchTenantRequest.php @@ -0,0 +1,20 @@ + 'required|integer|exists:tenants,id', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Requests/User/UserUpdateRequest.php b/app/Http/Requests/User/UserUpdateRequest.php new file mode 100644 index 0000000..71f1584 --- /dev/null +++ b/app/Http/Requests/User/UserUpdateRequest.php @@ -0,0 +1,22 @@ + 'sometimes|string|max:100', + 'phone' => 'nullable|string|max:20', + 'email' => 'sometimes|email|max:100', + ]; + } +} \ No newline at end of file diff --git a/app/Swagger/v1/UserApi.php b/app/Swagger/v1/UserApi.php index c272696..d7a1a9d 100644 --- a/app/Swagger/v1/UserApi.php +++ b/app/Swagger/v1/UserApi.php @@ -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 { diff --git a/claudedocs/SWAGGER_PHASE3_3_CLIENT.md b/claudedocs/SWAGGER_PHASE3_3_CLIENT.md new file mode 100644 index 0000000..ccb5db9 --- /dev/null +++ b/claudedocs/SWAGGER_PHASE3_3_CLIENT.md @@ -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 완료 ✅** \ No newline at end of file diff --git a/lang/ko/message.php b/lang/ko/message.php index 0586f2e..223f80b 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -100,6 +100,16 @@ 'toggled' => '거래처 상태가 변경되었습니다.', ], + // 사용자 관리 + 'user' => [ + 'fetched' => '사용자를 조회했습니다.', + 'me_fetched' => '나의 정보를 조회했습니다.', + 'me_updated' => '나의 정보가 수정되었습니다.', + 'password_changed' => '비밀번호가 변경되었습니다.', + 'tenants_fetched' => '나의 테넌트 목록을 조회했습니다.', + 'tenant_switched' => '활성 테넌트가 전환되었습니다.', + ], + // 파일 관리 'file' => [ 'uploaded' => '파일이 업로드되었습니다.',