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:
42
.serena/memories/profile-image-upload-api.md
Normal file
42
.serena/memories/profile-image-upload-api.md
Normal 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` 디렉토리 미포함 → 심볼릭 링크 추가
|
||||||
@@ -1,5 +1,71 @@
|
|||||||
# SAM API 작업 현황
|
# 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 수정
|
## 2025-12-28 (토) - 시스템 게시판 tenant_id 및 custom_fields 수정
|
||||||
|
|
||||||
### 작업 목표
|
### 작업 목표
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 논리적 데이터베이스 관계 문서
|
# 논리적 데이터베이스 관계 문서
|
||||||
|
|
||||||
> **자동 생성**: 2025-12-29 18:06:50
|
> **자동 생성**: 2025-12-30 16:08:45
|
||||||
> **소스**: Eloquent 모델 관계 분석
|
> **소스**: Eloquent 모델 관계 분석
|
||||||
|
|
||||||
## 📊 모델별 관계 현황
|
## 📊 모델별 관계 현황
|
||||||
@@ -767,6 +767,8 @@ ### tenant_user_profiles
|
|||||||
- **user()**: belongsTo → `users`
|
- **user()**: belongsTo → `users`
|
||||||
- **department()**: belongsTo → `departments`
|
- **department()**: belongsTo → `departments`
|
||||||
- **manager()**: belongsTo → `users`
|
- **manager()**: belongsTo → `users`
|
||||||
|
- **rankPosition()**: belongsTo → `positions`
|
||||||
|
- **titlePosition()**: belongsTo → `positions`
|
||||||
|
|
||||||
### withdrawals
|
### withdrawals
|
||||||
**모델**: `App\Models\Tenants\Withdrawal`
|
**모델**: `App\Models\Tenants\Withdrawal`
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ public function __construct(
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$params = $request->only(['page', 'size', 'q', 'status', 'process_type']);
|
return ApiResponse::handle(
|
||||||
$result = $this->processService->index($params);
|
fn () => $this->processService->index(
|
||||||
|
$request->only(['page', 'size', 'q', 'status', 'process_type'])
|
||||||
return ApiResponse::handle($result, 'message.fetched');
|
),
|
||||||
|
'message.fetched'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,9 +34,10 @@ public function index(Request $request): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function show(int $id): JsonResponse
|
public function show(int $id): JsonResponse
|
||||||
{
|
{
|
||||||
$result = $this->processService->show($id);
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->processService->show($id),
|
||||||
return ApiResponse::handle($result, 'message.fetched');
|
'message.fetched'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,9 +45,10 @@ public function show(int $id): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function store(StoreProcessRequest $request): JsonResponse
|
public function store(StoreProcessRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$result = $this->processService->store($request->validated());
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->processService->store($request->validated()),
|
||||||
return ApiResponse::handle($result, 'message.created', 201);
|
'message.created'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,9 +56,10 @@ public function store(StoreProcessRequest $request): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function update(UpdateProcessRequest $request, int $id): JsonResponse
|
public function update(UpdateProcessRequest $request, int $id): JsonResponse
|
||||||
{
|
{
|
||||||
$result = $this->processService->update($id, $request->validated());
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->processService->update($id, $request->validated()),
|
||||||
return ApiResponse::handle($result, 'message.updated');
|
'message.updated'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,9 +67,10 @@ public function update(UpdateProcessRequest $request, int $id): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function destroy(int $id): JsonResponse
|
public function destroy(int $id): JsonResponse
|
||||||
{
|
{
|
||||||
$this->processService->destroy($id);
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->processService->destroy($id),
|
||||||
return ApiResponse::handle(null, 'message.deleted');
|
'message.deleted'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,10 +78,10 @@ public function destroy(int $id): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function destroyMany(Request $request): JsonResponse
|
public function destroyMany(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$ids = $request->input('ids', []);
|
return ApiResponse::handle(
|
||||||
$count = $this->processService->destroyMany($ids);
|
fn () => ['deleted_count' => $this->processService->destroyMany($request->input('ids', []))],
|
||||||
|
'message.deleted'
|
||||||
return ApiResponse::handle(['deleted_count' => $count], 'message.deleted');
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,9 +89,10 @@ public function destroyMany(Request $request): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function toggleActive(int $id): JsonResponse
|
public function toggleActive(int $id): JsonResponse
|
||||||
{
|
{
|
||||||
$result = $this->processService->toggleActive($id);
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->processService->toggleActive($id),
|
||||||
return ApiResponse::handle($result, 'message.updated');
|
'message.updated'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,9 +100,10 @@ public function toggleActive(int $id): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function options(): JsonResponse
|
public function options(): JsonResponse
|
||||||
{
|
{
|
||||||
$result = $this->processService->options();
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->processService->options(),
|
||||||
return ApiResponse::handle($result, 'message.fetched');
|
'message.fetched'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,8 +111,9 @@ public function options(): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function stats(): JsonResponse
|
public function stats(): JsonResponse
|
||||||
{
|
{
|
||||||
$result = $this->processService->getStats();
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->processService->getStats(),
|
||||||
return ApiResponse::handle($result, 'message.fetched');
|
'message.fetched'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,17 +251,28 @@
|
|||||||
Route::prefix('roles')->group(function () {
|
Route::prefix('roles')->group(function () {
|
||||||
Route::get('/', [RoleController::class, 'index'])->name('v1.roles.index'); // view
|
Route::get('/', [RoleController::class, 'index'])->name('v1.roles.index'); // view
|
||||||
Route::post('/', [RoleController::class, 'store'])->name('v1.roles.store'); // create
|
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::get('/{id}', [RoleController::class, 'show'])->name('v1.roles.show'); // view
|
||||||
Route::patch('/{id}', [RoleController::class, 'update'])->name('v1.roles.update'); // update
|
Route::patch('/{id}', [RoleController::class, 'update'])->name('v1.roles.update'); // update
|
||||||
Route::delete('/{id}', [RoleController::class, 'destroy'])->name('v1.roles.destroy'); // delete
|
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::prefix('roles/{id}/permissions')->group(function () {
|
||||||
Route::get('/', [RolePermissionController::class, 'index'])->name('v1.roles.perms.index'); // list
|
Route::get('/', [RolePermissionController::class, 'index'])->name('v1.roles.perms.index'); // list
|
||||||
Route::post('/', [RolePermissionController::class, 'grant'])->name('v1.roles.perms.grant'); // grant
|
Route::post('/', [RolePermissionController::class, 'grant'])->name('v1.roles.perms.grant'); // grant
|
||||||
Route::delete('/', [RolePermissionController::class, 'revoke'])->name('v1.roles.perms.revoke'); // revoke
|
Route::delete('/', [RolePermissionController::class, 'revoke'])->name('v1.roles.perms.revoke'); // revoke
|
||||||
Route::put('/sync', [RolePermissionController::class, 'sync'])->name('v1.roles.perms.sync'); // sync
|
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
|
// User Role API
|
||||||
|
|||||||
Reference in New Issue
Block a user