chore(API): 공정 컨트롤러 및 라우팅 업데이트

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 17:25:29 +09:00
parent d6e18fb54e
commit 4f45a5dbd8
5 changed files with 161 additions and 31 deletions

View File

@@ -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` 디렉토리 미포함 → 심볼릭 링크 추가

View File

@@ -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 수정
### 작업 목표

View File

@@ -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`

View File

@@ -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'
);
}
}

View File

@@ -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