From 4f45a5dbd84be11d6068271a675deecf127ac368 Mon Sep 17 00:00:00 2001 From: kent Date: Tue, 30 Dec 2025 17:25:29 +0900 Subject: [PATCH] =?UTF-8?q?chore(API):=20=EA=B3=B5=EC=A0=95=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProcessController: 공정 관리 API 개선 - routes/api.php: API 라우팅 정리 - CURRENT_WORKS.md: 작업 현황 업데이트 - LOGICAL_RELATIONSHIPS.md: 논리적 관계 문서화 - profile-image-upload-api.md: 프로필 이미지 업로드 API 메모 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .serena/memories/profile-image-upload-api.md | 42 ++++++++++++ CURRENT_WORKS.md | 66 ++++++++++++++++++ LOGICAL_RELATIONSHIPS.md | 4 +- app/Http/Controllers/V1/ProcessController.php | 67 +++++++++++-------- routes/api.php | 13 +++- 5 files changed, 161 insertions(+), 31 deletions(-) create mode 100644 .serena/memories/profile-image-upload-api.md diff --git a/.serena/memories/profile-image-upload-api.md b/.serena/memories/profile-image-upload-api.md new file mode 100644 index 0000000..30fc2ab --- /dev/null +++ b/.serena/memories/profile-image-upload-api.md @@ -0,0 +1,42 @@ +# 프로필 이미지 업로드 API 연동 (2025-12-29) + +## 개요 +계정정보 페이지(`/settings/account-info`)에서 프로필 이미지 업로드 API 연동 완료 + +## 수정 파일 + +### API (api/) +- `app/Services/TenantUserProfileService.php` + - `updateMe()` 메서드 수정 + - 고정 필드(`profile_photo_path`, `display_name`)를 동적 필드 처리 전에 직접 저장 + - 기존 `effectiveFieldMap()` 로직은 동적 필드만 처리 + +### React (react/) +- `src/components/settings/AccountInfoManagement/actions.ts` + - `uploadProfileImage()` 함수 추가 + - FormData로 파일 업로드 후 profile_photo_path 업데이트 + +- `src/components/settings/AccountInfoManagement/types.ts` + - `getProfileImageUrl()` 수정: `/storage/${path}` → `/storage/tenants/${path}` + - DB에 저장된 경로(`287/temp/...`)와 실제 파일 위치(`tenants/287/temp/...`) 매핑 + +- `src/components/settings/AccountInfoManagement/index.tsx` + - `handleImageUpload()` API 호출 연동 + - Optimistic UI 업데이트 구현 + +### 환경변수 정리 +- 65개 소스 파일에서 `NEXT_PUBLIC_API_URL` → `API_URL` 변경 +- `.env.local`, `.env.production`, `.env.example` 업데이트 + +### 심볼릭 링크 +- `storage/app/public/tenants -> ../tenants` 생성 +- 테넌트 파일 공개 접근 가능하도록 설정 + +## API 엔드포인트 +- 파일 업로드: `POST /api/v1/files/upload` +- 프로필 업데이트: `PATCH /api/v1/profiles/me` + +## 핵심 이슈 해결 +1. **이미지 지속성 문제**: `updateMe()`가 동적 필드만 처리하여 `profile_photo_path` 무시됨 → 고정 필드 별도 처리 +2. **URL 경로 불일치**: DB 저장 경로와 실제 파일 경로 차이 → `/storage/tenants/` 접두사 추가 +3. **심볼릭 링크 누락**: `public/storage`가 `tenants` 디렉토리 미포함 → 심볼릭 링크 추가 diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 7f63b02..f0da463 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,71 @@ # SAM API 작업 현황 +## 2025-12-30 (월) - Phase L 설정 및 기준정보 API 개발 + +### 작업 목표 +- L-2 권한관리 API 개발 +- L-3 직급관리 + L-4 직책관리 API 개발 (통합 positions 테이블) + +### 생성된 파일 + +#### L-2 권한관리 +| 파일명 | 설명 | +|--------|------| +| `database/migrations/2025_12_30_160802_add_is_hidden_to_roles_table.php` | roles 테이블 is_hidden 컬럼 추가 | +| `app/Http/Controllers/Api/V1/RoleController.php` | Role CRUD API | +| `app/Http/Controllers/Api/V1/RolePermissionController.php` | 권한 매트릭스 API | +| `app/Services/RoleService.php` | Role 비즈니스 로직 | +| `app/Swagger/v1/RoleApi.php` | Swagger 문서 | +| `app/Swagger/v1/RolePermissionApi.php` | Swagger 문서 | + +#### L-3/L-4 직급/직책 관리 +| 파일명 | 설명 | +|--------|------| +| `database/migrations/2025_12_30_091821_create_positions_table.php` | positions 테이블 생성 | +| `database/migrations/2025_12_30_091822_add_position_type_to_common_codes.php` | position_type 코드 추가 | +| `app/Models/Tenants/Position.php` | Position 모델 | +| `app/Services/PositionService.php` | Position 비즈니스 로직 | +| `app/Http/Controllers/Api/V1/PositionController.php` | Position CRUD API | +| `app/Http/Requests/PositionRequest.php` | 생성/수정 요청 검증 | +| `app/Http/Requests/PositionReorderRequest.php` | 순서 변경 요청 검증 | +| `app/Swagger/v1/PositionApi.php` | Swagger 문서 | + +### API 엔드포인트 + +#### Role API (9개) +``` +GET /api/v1/roles # 역할 목록 +POST /api/v1/roles # 역할 생성 +GET /api/v1/roles/{id} # 역할 상세 +PATCH /api/v1/roles/{id} # 역할 수정 +DELETE /api/v1/roles/{id} # 역할 삭제 +GET /api/v1/roles/{id}/permissions # 역할 권한 조회 +POST /api/v1/roles/{id}/permissions # 권한 추가 +DELETE /api/v1/roles/{id}/permissions # 권한 제거 +PUT /api/v1/roles/{id}/permissions/sync # 권한 동기화 +``` + +#### Position API (6개) +``` +GET /api/v1/positions?type=rank # 직급 목록 +GET /api/v1/positions?type=title # 직책 목록 +POST /api/v1/positions # 생성 (type 필수) +PUT /api/v1/positions/{id} # 수정 +DELETE /api/v1/positions/{id} # 삭제 +POST /api/v1/positions/reorder # 순서 변경 (bulk) +``` + +### 설계 결정사항 +- **통합 테이블**: 직급(rank)과 직책(title)을 `positions` 테이블로 통합 +- **구분 컬럼**: `type` 컬럼으로 rank/title 구분 +- **정렬 지원**: `sort_order` 컬럼 + reorder API로 드래그 앤 드롭 지원 + +### 참고 +- 계획 문서: `docs/plans/l2-permission-management-plan.md` +- 세레나 메모리: `position-api-development`, `l2-permission-state` + +--- + ## 2025-12-28 (토) - 시스템 게시판 tenant_id 및 custom_fields 수정 ### 작업 목표 diff --git a/LOGICAL_RELATIONSHIPS.md b/LOGICAL_RELATIONSHIPS.md index 97ac244..c95aa8b 100644 --- a/LOGICAL_RELATIONSHIPS.md +++ b/LOGICAL_RELATIONSHIPS.md @@ -1,6 +1,6 @@ # 논리적 데이터베이스 관계 문서 -> **자동 생성**: 2025-12-29 18:06:50 +> **자동 생성**: 2025-12-30 16:08:45 > **소스**: Eloquent 모델 관계 분석 ## 📊 모델별 관계 현황 @@ -767,6 +767,8 @@ ### tenant_user_profiles - **user()**: belongsTo → `users` - **department()**: belongsTo → `departments` - **manager()**: belongsTo → `users` +- **rankPosition()**: belongsTo → `positions` +- **titlePosition()**: belongsTo → `positions` ### withdrawals **모델**: `App\Models\Tenants\Withdrawal` diff --git a/app/Http/Controllers/V1/ProcessController.php b/app/Http/Controllers/V1/ProcessController.php index 3c6b4fb..dec4687 100644 --- a/app/Http/Controllers/V1/ProcessController.php +++ b/app/Http/Controllers/V1/ProcessController.php @@ -21,10 +21,12 @@ public function __construct( */ public function index(Request $request): JsonResponse { - $params = $request->only(['page', 'size', 'q', 'status', 'process_type']); - $result = $this->processService->index($params); - - return ApiResponse::handle($result, 'message.fetched'); + return ApiResponse::handle( + fn () => $this->processService->index( + $request->only(['page', 'size', 'q', 'status', 'process_type']) + ), + 'message.fetched' + ); } /** @@ -32,9 +34,10 @@ public function index(Request $request): JsonResponse */ public function show(int $id): JsonResponse { - $result = $this->processService->show($id); - - return ApiResponse::handle($result, 'message.fetched'); + return ApiResponse::handle( + fn () => $this->processService->show($id), + 'message.fetched' + ); } /** @@ -42,9 +45,10 @@ public function show(int $id): JsonResponse */ public function store(StoreProcessRequest $request): JsonResponse { - $result = $this->processService->store($request->validated()); - - return ApiResponse::handle($result, 'message.created', 201); + return ApiResponse::handle( + fn () => $this->processService->store($request->validated()), + 'message.created' + ); } /** @@ -52,9 +56,10 @@ public function store(StoreProcessRequest $request): JsonResponse */ public function update(UpdateProcessRequest $request, int $id): JsonResponse { - $result = $this->processService->update($id, $request->validated()); - - return ApiResponse::handle($result, 'message.updated'); + return ApiResponse::handle( + fn () => $this->processService->update($id, $request->validated()), + 'message.updated' + ); } /** @@ -62,9 +67,10 @@ public function update(UpdateProcessRequest $request, int $id): JsonResponse */ public function destroy(int $id): JsonResponse { - $this->processService->destroy($id); - - return ApiResponse::handle(null, 'message.deleted'); + return ApiResponse::handle( + fn () => $this->processService->destroy($id), + 'message.deleted' + ); } /** @@ -72,10 +78,10 @@ public function destroy(int $id): JsonResponse */ public function destroyMany(Request $request): JsonResponse { - $ids = $request->input('ids', []); - $count = $this->processService->destroyMany($ids); - - return ApiResponse::handle(['deleted_count' => $count], 'message.deleted'); + return ApiResponse::handle( + fn () => ['deleted_count' => $this->processService->destroyMany($request->input('ids', []))], + 'message.deleted' + ); } /** @@ -83,9 +89,10 @@ public function destroyMany(Request $request): JsonResponse */ public function toggleActive(int $id): JsonResponse { - $result = $this->processService->toggleActive($id); - - return ApiResponse::handle($result, 'message.updated'); + return ApiResponse::handle( + fn () => $this->processService->toggleActive($id), + 'message.updated' + ); } /** @@ -93,9 +100,10 @@ public function toggleActive(int $id): JsonResponse */ public function options(): JsonResponse { - $result = $this->processService->options(); - - return ApiResponse::handle($result, 'message.fetched'); + return ApiResponse::handle( + fn () => $this->processService->options(), + 'message.fetched' + ); } /** @@ -103,8 +111,9 @@ public function options(): JsonResponse */ public function stats(): JsonResponse { - $result = $this->processService->getStats(); - - return ApiResponse::handle($result, 'message.fetched'); + return ApiResponse::handle( + fn () => $this->processService->getStats(), + 'message.fetched' + ); } } diff --git a/routes/api.php b/routes/api.php index cf61953..f3a301d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -251,17 +251,28 @@ Route::prefix('roles')->group(function () { Route::get('/', [RoleController::class, 'index'])->name('v1.roles.index'); // view Route::post('/', [RoleController::class, 'store'])->name('v1.roles.store'); // create + Route::get('/stats', [RoleController::class, 'stats'])->name('v1.roles.stats'); // stats + Route::get('/active', [RoleController::class, 'active'])->name('v1.roles.active'); // active list Route::get('/{id}', [RoleController::class, 'show'])->name('v1.roles.show'); // view Route::patch('/{id}', [RoleController::class, 'update'])->name('v1.roles.update'); // update Route::delete('/{id}', [RoleController::class, 'destroy'])->name('v1.roles.destroy'); // delete }); - // Role Permission API + // Role Permission API - 공통 + Route::get('/role-permissions/menus', [RolePermissionController::class, 'menus'])->name('v1.roles.perms.menus'); // 메뉴 트리 + + // Role Permission API - 역할별 Route::prefix('roles/{id}/permissions')->group(function () { Route::get('/', [RolePermissionController::class, 'index'])->name('v1.roles.perms.index'); // list Route::post('/', [RolePermissionController::class, 'grant'])->name('v1.roles.perms.grant'); // grant Route::delete('/', [RolePermissionController::class, 'revoke'])->name('v1.roles.perms.revoke'); // revoke Route::put('/sync', [RolePermissionController::class, 'sync'])->name('v1.roles.perms.sync'); // sync + // 권한 매트릭스 API + Route::get('/matrix', [RolePermissionController::class, 'matrix'])->name('v1.roles.perms.matrix'); // 권한 매트릭스 조회 + Route::post('/toggle', [RolePermissionController::class, 'toggle'])->name('v1.roles.perms.toggle'); // 개별 권한 토글 + Route::post('/allow-all', [RolePermissionController::class, 'allowAll'])->name('v1.roles.perms.allowAll'); // 전체 허용 + Route::post('/deny-all', [RolePermissionController::class, 'denyAll'])->name('v1.roles.perms.denyAll'); // 전체 거부 + Route::post('/reset', [RolePermissionController::class, 'reset'])->name('v1.roles.perms.reset'); // 기본값 초기화 }); // User Role API