Files
sam-api/CURRENT_WORKS.md
hskwon 30f308f480 docs: ItemMaster API Swagger 문서 추가
- 32개 엔드포인트 전체 문서화 완료
- Model/Request/Response 스키마 정의 (8+12+1)
- OpenAPI 3.0 표준 준수
- SAM Swagger 패턴 따름
2025-11-20 17:27:34 +09:00

3437 lines
109 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 2025-11-20 (수) - ItemMaster API Swagger 문서 작성
### 주요 작업
- ItemMaster 전체 API (32개 엔드포인트) Swagger 문서화 완료
- OpenAPI 3.0 표준 준수
- 기존 SAM Swagger 패턴 따름
### 추가된 파일
1. **app/Swagger/v1/ItemMasterApi.php** (~900 lines)
- Tag 정의: ItemMaster 리소스
- Model Schemas (8개): ItemPage, ItemSection, ItemField, ItemBomItem, SectionTemplate, ItemMasterField, CustomTab, UnitOption
- Request Schemas (12개): 각 리소스별 Store/Update + ReorderRequest
- Response Schema: ItemMasterInitResponse (nested arrays)
- 32개 엔드포인트 전체 문서화
### Swagger 문서 구조
**Model Schemas**:
- ItemPage (id, page_name, item_type, sections[])
- ItemSection (id, section_name, item_type, order_no, fields[])
- ItemField (id, field_key, label, type, options[], validation_rules)
- ItemBomItem (id, item_page_id, material_code, part_code, quantity, waste_rate)
- SectionTemplate (id, name, description, fields[])
- ItemMasterField (id, field_key, label, type, category)
- CustomTab (id, label, icon, is_default, order_no)
- UnitOption (id, label, value)
**Request Schemas**:
- Store/Update 요청 스키마 (각 리소스별)
- ReorderRequest (items 배열)
**Endpoint Documentation** (32개):
- Init (1): 초기 데이터 로드
- ItemPage (5): CRUD + reorder
- ItemSection (6): CRUD + reorder + restore
- ItemField (5): CRUD + reorder
- ItemBomItem (5): CRUD + reorder
- SectionTemplate (5): CRUD + reorder
- ItemMasterField (3): index, store, delete
- CustomTab (5): CRUD + reorder
- UnitOption (3): index, store, delete
### 작업 내용
**1단계: 분석**
- 기존 CategoryApi.php 구조 분석
- ItemMaster 모델 구조 파악 (8개 모델)
- 엔드포인트 전체 목록 정리 (32개)
**2단계: Swagger 파일 생성**
- Model Schema 정의 (nullable, type, example 포함)
- Request/Response Schema 정의
- 전체 엔드포인트 @OA annotation 작성
- Security 설정 (ApiKeyAuth + BearerAuth)
**3단계: 검증**
- `php artisan l5-swagger:generate` 실행 성공
- `./vendor/bin/pint` 코드 포맷팅 (1개 스타일 이슈 수정)
- 라우트 목록 확인 (32개 엔드포인트 정상)
### 검증 결과
**Swagger 생성**:
```bash
php artisan l5-swagger:generate
# 결과: Regenerating docs v1 성공
```
**코드 포맷팅**:
```bash
./vendor/bin/pint app/Swagger/v1/ItemMasterApi.php
# 결과: 1 file, 1 style issue fixed
```
**라우트 확인**:
```bash
php artisan route:list --path=item-master | grep -c "item-master"
# 결과: 32개 엔드포인트
```
### 다음 단계
**남은 작업**:
- API 테스트 케이스 작성
- Frontend 연동 (React ItemMaster 화면)
### Git 커밋
- 커밋 예정: Swagger 문서 추가
---
## 2025-11-20 (수) - ItemMaster Phase 3 API 구현 (부가 기능)
### 주요 작업
- Phase 3 부가 기능 8개 API 엔드포인트 구현
- 커스텀 탭, 단위 옵션 관리 기능 추가
- SAM API Development Rules 준수 (Service-First, ApiResponse, i18n)
### 추가된 파일
#### Controllers (2개)
1. **app/Http/Controllers/Api/V1/ItemMaster/CustomTabController.php**
- index(), store(), update(), destroy(), reorder()
- 커스텀 탭 관리 및 순서 변경
2. **app/Http/Controllers/Api/V1/ItemMaster/UnitOptionController.php**
- index(), store(), destroy()
- 단위 옵션 관리 (Update 없음)
#### Services (2개)
1. **app/Services/ItemMaster/CustomTabService.php**
- CRUD + reorder
- columnSetting 관계 Eager Loading
- order_no 자동 계산
2. **app/Services/ItemMaster/UnitOptionService.php**
- index, store, destroy만 구현
- label 정렬
#### FormRequests (3개)
1. **app/Http/Requests/ItemMaster/CustomTabStoreRequest.php**
- label (required), icon, is_default
2. **app/Http/Requests/ItemMaster/CustomTabUpdateRequest.php**
- 모든 필드 sometimes
3. **app/Http/Requests/ItemMaster/UnitOptionStoreRequest.php**
- label (required), value (required)
### 수정된 파일
1. **routes/api.php**
- ItemMaster 관련 use 문 2개 추가
- 8개 엔드포인트 추가:
- GET/POST/PUT/DELETE /custom-tabs, PUT /custom-tabs/reorder
- GET/POST/DELETE /unit-options
### 작업 내용
#### API 엔드포인트 (8개)
1. ✅ GET `/custom-tabs` - 커스텀 탭 목록
2. ✅ POST `/custom-tabs` - 커스텀 탭 생성
3. ✅ PUT `/custom-tabs/{id}` - 커스텀 탭 수정
4. ✅ DELETE `/custom-tabs/{id}` - 커스텀 탭 삭제
5. ✅ PUT `/custom-tabs/reorder` - 커스텀 탭 순서 변경
6. ✅ GET `/unit-options` - 단위 옵션 목록
7. ✅ POST `/unit-options` - 단위 옵션 생성
8. ✅ DELETE `/unit-options/{id}` - 단위 옵션 삭제
#### 기술적 특징
**Service-First 패턴**:
- Controller는 DI + ApiResponse::handle()만 사용
- 모든 비즈니스 로직은 Service에 구현
- Service extends Service (tenantId(), apiUserId() 활용)
**Multi-tenant 지원**:
- 모든 Service 메서드에서 tenantId() 검증
- BelongsToTenant 스코프 자동 적용
- Soft Delete시 tenant_id 검증
**실시간 저장**:
- 모든 CUD 작업 즉시 처리
- order_no 자동 계산 (CustomTab)
- reorder는 배열로 한 번에 처리
**i18n 메시지**:
- __('message.fetched'), __('message.created')
- __('message.updated'), __('message.deleted')
- __('message.reordered'), __('error.not_found')
### 검증 결과
**라우트 테스트**:
```bash
php artisan route:list --path=item-master
# 결과: 32개 엔드포인트 정상 등록 (Phase 1: 13개 + Phase 2: 11개 + Phase 3: 8개)
```
**Pint 검사**:
```bash
./vendor/bin/pint --test [7개 파일]
# 결과: 7 files PASS
```
### 완료 상태
**ItemMaster API 전체 구현 완료**:
- Phase 1 (핵심): 13개 엔드포인트 ✅
- Phase 2 (확장): 11개 엔드포인트 ✅
- Phase 3 (부가): 8개 엔드포인트 ✅
- **총 32개 엔드포인트 구현 완료**
**다음 작업**:
- Swagger 문서화 (app/Swagger/v1/ItemMasterApi.php)
- API 테스트 케이스 작성
- 프론트엔드 연동
### Git 커밋
```bash
git commit a85cf0a
feat: ItemMaster Phase 3 API 구현 (부가 기능 8개 엔드포인트)
- Controller 2개, Service 2개, FormRequest 3개 생성
- Routes 등록 (커스텀 탭, 단위 옵션)
- Service-First, Multi-tenant, Soft Delete
- 라우트 테스트 및 Pint 검사 통과
- ItemMaster API 전체 32개 엔드포인트 구현 완료
8 files changed, 400+ insertions(+)
```
---
## 2025-11-20 (수) - ItemMaster Phase 2 API 구현 (확장 기능)
### 주요 작업
- Phase 2 확장 기능 11개 API 엔드포인트 구현
- BOM 항목, 섹션 템플릿, 마스터 필드 관리 기능 추가
- SAM API Development Rules 준수 (Service-First, ApiResponse, i18n)
### 추가된 파일
#### Controllers (3개)
1. **app/Http/Controllers/Api/V1/ItemMaster/ItemBomItemController.php**
- store(), update(), destroy()
- BOM 항목 관리
2. **app/Http/Controllers/Api/V1/ItemMaster/SectionTemplateController.php**
- index(), store(), update(), destroy()
- 섹션 템플릿 관리
3. **app/Http/Controllers/Api/V1/ItemMaster/ItemMasterFieldController.php**
- index(), store(), update(), destroy()
- 마스터 필드 라이브러리 관리
#### Services (3개)
1. **app/Services/ItemMaster/ItemBomItemService.php**
- CRUD + Multi-tenant 스코프
- 섹션 존재 확인 후 BOM 항목 생성
2. **app/Services/ItemMaster/SectionTemplateService.php**
- CRUD + 목록 조회 (created_at 정렬)
- is_default 플래그 지원
3. **app/Services/ItemMaster/ItemMasterFieldService.php**
- CRUD + 목록 조회 (category, field_name 정렬)
- JSON 필드 지원 (options, validation_rules, properties)
- is_common 플래그 지원
#### FormRequests (6개)
1. **app/Http/Requests/ItemMaster/ItemBomItemStoreRequest.php**
- item_name (required), quantity, unit, unit_price, total_price, spec, note
2. **app/Http/Requests/ItemMaster/ItemBomItemUpdateRequest.php**
- 모든 필드 sometimes
3. **app/Http/Requests/ItemMaster/SectionTemplateStoreRequest.php**
- title (required), type (required, in:fields,bom), description, is_default
4. **app/Http/Requests/ItemMaster/SectionTemplateUpdateRequest.php**
- 모든 필드 sometimes
5. **app/Http/Requests/ItemMaster/ItemMasterFieldStoreRequest.php**
- field_name (required), field_type (required, enum)
- category, description, is_common, default_value, options, validation_rules, properties
6. **app/Http/Requests/ItemMaster/ItemMasterFieldUpdateRequest.php**
- 모든 필드 sometimes
### 수정된 파일
1. **routes/api.php**
- ItemMaster 관련 use 문 3개 추가
- 11개 엔드포인트 추가:
- POST/PUT/DELETE /sections/{sectionId}/bom-items
- GET/POST/PUT/DELETE /section-templates
- GET/POST/PUT/DELETE /master-fields
### 작업 내용
#### API 엔드포인트 (11개)
1. ✅ POST `/sections/{sectionId}/bom-items` - BOM 항목 생성
2. ✅ PUT `/bom-items/{id}` - BOM 항목 수정
3. ✅ DELETE `/bom-items/{id}` - BOM 항목 삭제
4. ✅ GET `/section-templates` - 섹션 템플릿 목록
5. ✅ POST `/section-templates` - 섹션 템플릿 생성
6. ✅ PUT `/section-templates/{id}` - 섹션 템플릿 수정
7. ✅ DELETE `/section-templates/{id}` - 섹션 템플릿 삭제
8. ✅ GET `/master-fields` - 마스터 필드 목록
9. ✅ POST `/master-fields` - 마스터 필드 생성
10. ✅ PUT `/master-fields/{id}` - 마스터 필드 수정
11. ✅ DELETE `/master-fields/{id}` - 마스터 필드 삭제
#### 기술적 특징
**Service-First 패턴**:
- Controller는 DI + ApiResponse::handle()만 사용
- 모든 비즈니스 로직은 Service에 구현
- Service extends Service (tenantId(), apiUserId() 활용)
**Multi-tenant 지원**:
- 모든 Service 메서드에서 tenantId() 검증
- BelongsToTenant 스코프 자동 적용
- Soft Delete시 tenant_id 검증
**실시간 저장**:
- 모든 CUD 작업 즉시 처리
- JSON 필드 자동 캐스팅 (options, validation_rules, properties)
**i18n 메시지**:
- __('message.fetched'), __('message.created')
- __('message.updated'), __('message.deleted')
- __('error.not_found')
### 검증 결과
**라우트 테스트**:
```bash
php artisan route:list --path=item-master
# 결과: 24개 엔드포인트 정상 등록 (Phase 1: 13개 + Phase 2: 11개)
```
**Pint 검사**:
```bash
./vendor/bin/pint --test [12개 파일]
# 결과: 12 files PASS
```
### 다음 단계 (Phase 3)
**Phase 3 (부가 기능)** - 예정:
- 커스텀 탭 (5개 엔드포인트)
- 단위 옵션 (3개 엔드포인트)
**Swagger 문서** - 필요:
- app/Swagger/v1/ItemMasterApi.php 작성
- 24개 엔드포인트 스키마 정의 (Phase 1 + Phase 2)
### Git 커밋
```bash
git commit 28a943b
feat: ItemMaster Phase 2 API 구현 (확장 기능 11개 엔드포인트)
- Controller 3개, Service 3개, FormRequest 6개 생성
- Routes 등록 (BOM 항목, 섹션 템플릿, 마스터 필드)
- Service-First, Multi-tenant, Soft Delete
- 라우트 테스트 및 Pint 검사 통과
13 files changed, 600+ insertions(+)
```
---
## 2025-11-20 (수) - ItemMaster Phase 1 API 구현 (핵심 기능)
### 주요 작업
- Phase 1 핵심 기능 13개 API 엔드포인트 구현
- Controller, Service, FormRequest 계층 구조 완성
- SAM API Development Rules 준수 (Service-First, ApiResponse, i18n)
### 추가된 파일
#### Controllers (4개)
1. **app/Http/Controllers/Api/V1/ItemMaster/ItemMasterController.php**
- init() - 전체 초기 데이터 로드
2. **app/Http/Controllers/Api/V1/ItemMaster/ItemPageController.php**
- index(), store(), update(), destroy()
- 페이지 관리 (섹션/필드 중첩 포함)
3. **app/Http/Controllers/Api/V1/ItemMaster/ItemSectionController.php**
- store(), update(), destroy(), reorder()
- 섹션 관리 및 순서 변경
4. **app/Http/Controllers/Api/V1/ItemMaster/ItemFieldController.php**
- store(), update(), destroy(), reorder()
- 필드 관리 및 순서 변경
#### Services (4개)
1. **app/Services/ItemMaster/ItemMasterService.php**
- init() - pages, sectionTemplates, masterFields, customTabs, unitOptions 로드
- Eager Loading으로 N+1 쿼리 방지
2. **app/Services/ItemMaster/ItemPageService.php**
- CRUD + Cascade Soft Delete
- Multi-tenant 스코프 자동 적용
3. **app/Services/ItemMaster/ItemSectionService.php**
- CRUD + reorder (order_no 자동 계산)
- Cascade Soft Delete (하위 필드/BOM 항목 포함)
4. **app/Services/ItemMaster/ItemFieldService.php**
- CRUD + reorder
- JSON 필드 지원 (display_condition, validation_rules, options, properties)
#### FormRequests (7개)
1. **app/Http/Requests/ItemMaster/ItemPageStoreRequest.php**
- page_name (required, max:255)
- item_type (required, in:FG,PT,SM,RM,CS)
- absolute_path (nullable, max:500)
2. **app/Http/Requests/ItemMaster/ItemPageUpdateRequest.php**
- page_name (sometimes)
- absolute_path (nullable)
3. **app/Http/Requests/ItemMaster/ItemSectionStoreRequest.php**
- title (required, max:255)
- type (required, in:fields,bom)
4. **app/Http/Requests/ItemMaster/ItemSectionUpdateRequest.php**
- title (sometimes)
5. **app/Http/Requests/ItemMaster/ItemFieldStoreRequest.php**
- field_name (required), field_type (required, enum)
- is_required, placeholder, options, properties (nullable)
- display_condition, validation_rules (array)
6. **app/Http/Requests/ItemMaster/ItemFieldUpdateRequest.php**
- 모든 필드 sometimes
7. **app/Http/Requests/ItemMaster/ReorderRequest.php**
- items (required, array)
- items.*.id, items.*.order_no (required)
### 수정된 파일
1. **routes/api.php**
- ItemMaster 관련 use 문 4개 추가
- `/v1/item-master/*` 라우트 그룹 추가
- 13개 엔드포인트 등록:
- GET /init
- GET/POST/PUT/DELETE /pages
- POST/PUT/DELETE /pages/{pageId}/sections
- PUT /pages/{pageId}/sections/reorder
- POST/PUT/DELETE /sections/{sectionId}/fields
- PUT /sections/{sectionId}/fields/reorder
### 작업 내용
#### API 엔드포인트 (13개)
1. ✅ GET `/init` - 전체 초기 데이터
2. ✅ GET `/pages` - 페이지 목록 (item_type 필터)
3. ✅ POST `/pages` - 페이지 생성
4. ✅ PUT `/pages/{id}` - 페이지 수정
5. ✅ DELETE `/pages/{id}` - 페이지 삭제 (Cascade)
6. ✅ POST `/pages/{pageId}/sections` - 섹션 생성
7. ✅ PUT `/sections/{id}` - 섹션 수정
8. ✅ DELETE `/sections/{id}` - 섹션 삭제 (Cascade)
9. ✅ PUT `/pages/{pageId}/sections/reorder` - 섹션 순서 변경
10. ✅ POST `/sections/{sectionId}/fields` - 필드 생성
11. ✅ PUT `/fields/{id}` - 필드 수정
12. ✅ DELETE `/fields/{id}` - 필드 삭제
13. ✅ PUT `/sections/{sectionId}/fields/reorder` - 필드 순서 변경
#### 기술적 특징
**Service-First 패턴**:
- Controller는 DI + ApiResponse::handle()만 사용
- 모든 비즈니스 로직은 Service에 구현
- Service extends Service (tenantId(), apiUserId() 활용)
**Multi-tenant 지원**:
- 모든 Service 메서드에서 tenantId() 검증
- BelongsToTenant 스코프 자동 적용
- Cascade Soft Delete시 tenant_id 검증
**실시간 저장**:
- 모든 CUD 작업 즉시 처리
- order_no 자동 계산 (마지막 + 1)
- reorder는 배열로 한 번에 처리
**i18n 메시지**:
- __('message.fetched'), __('message.created')
- __('message.updated'), __('message.deleted')
- __('message.reordered'), __('error.not_found')
### 검증 결과
**라우트 테스트**:
```bash
php artisan route:list --path=item-master
# 결과: 13개 엔드포인트 정상 등록
```
**Pint 검사**:
```bash
./vendor/bin/pint --test app/Http/Controllers/Api/V1/ItemMaster/ app/Services/ItemMaster/ app/Http/Requests/ItemMaster/
# 결과: 15 files PASS
```
### 다음 단계 (Phase 2-3)
**Phase 2 (확장 기능)** - 예정:
- BOM 항목 관리 (3개 엔드포인트)
- 섹션 템플릿 (4개 엔드포인트)
- 마스터 필드 (4개 엔드포인트)
**Phase 3 (부가 기능)** - 예정:
- 커스텀 탭 (5개 엔드포인트)
- 단위 옵션 (3개 엔드포인트)
**Swagger 문서** - 필요:
- app/Swagger/v1/ItemMasterApi.php 작성
- 13개 엔드포인트 스키마 정의
### Git 커밋
```bash
git commit 4ccee25
feat: ItemMaster Phase 1 API 구현 (핵심 기능 13개 엔드포인트)
- Controller 4개, Service 4개, FormRequest 7개 생성
- Routes 등록 (/v1/item-master/*)
- Service-First, Multi-tenant, Cascade Soft Delete
- 라우트 테스트 및 Pint 검사 통과
17 files changed, 991 insertions(+)
```
---
## 2025-11-20 (수) - ItemMaster 데이터베이스 구조 구축
### 주요 작업
- ItemMaster 시스템을 위한 9개 테이블 마이그레이션 생성 및 실행
- 9개 Eloquent 모델 클래스 구현
- 시드 데이터 작성 및 테스트 데이터 생성
- SAM API Development Rules 준수 (Multi-tenant, SoftDeletes, 감사 로그)
### 추가된 파일
#### 마이그레이션 (9개)
1. **database/migrations/2025_11_20_100000_create_unit_options_table.php**
- 단위 옵션 테이블 (kg, EA, m, mm, L, BOX)
- tenant_id, label, value, created_by, updated_by, deleted_by
2. **database/migrations/2025_11_20_100001_create_section_templates_table.php**
- 섹션 템플릿 테이블 (재사용 가능한 섹션 정의)
- title, type (fields/bom), description, is_default
3. **database/migrations/2025_11_20_100002_create_item_master_fields_table.php**
- 마스터 필드 라이브러리
- field_name, field_type, category, is_common
- options, validation_rules, properties (JSON)
4. **database/migrations/2025_11_20_100003_create_item_pages_table.php**
- 품목 페이지 테이블 (FG/PT/SM/RM/CS)
- page_name, item_type (ENUM), absolute_path, is_active
5. **database/migrations/2025_11_20_100004_create_item_sections_table.php**
- 섹션 인스턴스 테이블
- page_id (FK), title, type, order_no
- Composite Index: (page_id, order_no)
6. **database/migrations/2025_11_20_100005_create_item_fields_table.php**
- 필드 인스턴스 테이블
- section_id (FK), field_name, field_type, order_no, is_required
- display_condition, validation_rules, options, properties (JSON)
- Composite Index: (section_id, order_no)
7. **database/migrations/2025_11_20_100006_create_item_bom_items_table.php**
- BOM 항목 테이블
- section_id (FK), item_code, item_name, quantity, unit
- unit_price, total_price, spec, note
8. **database/migrations/2025_11_20_100007_create_custom_tabs_table.php**
- 커스텀 탭 정의
- label, icon, is_default, order_no
9. **database/migrations/2025_11_20_100008_create_tab_columns_table.php**
- 탭별 컬럼 설정
- tab_id (FK), columns (JSON)
- Unique: (tenant_id, tab_id)
#### 모델 (9개)
1. **app/Models/ItemMaster/UnitOption.php**
- Traits: BelongsToTenant, ModelTrait, SoftDeletes
2. **app/Models/ItemMaster/SectionTemplate.php**
- is_default boolean cast
3. **app/Models/ItemMaster/ItemMasterField.php**
- options, validation_rules, properties array cast
4. **app/Models/ItemMaster/ItemPage.php**
- Relationship: sections() hasMany
- is_active boolean cast
5. **app/Models/ItemMaster/ItemSection.php**
- Relationships: page() belongsTo, fields() hasMany, bomItems() hasMany
- order_no integer cast
6. **app/Models/ItemMaster/ItemField.php**
- Relationship: section() belongsTo
- 4개 JSON 필드 array cast
7. **app/Models/ItemMaster/ItemBomItem.php**
- Relationship: section() belongsTo
- quantity decimal:4, prices decimal:2
8. **app/Models/ItemMaster/CustomTab.php**
- Relationship: columnSetting() hasOne
- is_default boolean, order_no integer cast
9. **app/Models/ItemMaster/TabColumn.php**
- Relationship: tab() belongsTo
- columns array cast
- SoftDeletes 미적용 (설정 데이터)
#### 시더
1. **database/seeders/ItemMasterSeeder.php**
- 6개 단위 옵션 생성
- 3개 섹션 템플릿 생성
- 8개 마스터 필드 생성
- 5개 품목 페이지 생성 (FG, PT, SM, RM, CS)
- 각 페이지에 기본 정보 섹션 + 필드 3개
- FG/PT 페이지에 BOM 구성 섹션 + 샘플 BOM 항목
- 3개 커스텀 탭 + 컬럼 설정
### 작업 내용
#### 1단계: 분석
- 프론트엔드 API 명세서 분석
- 기존 DB 구조와 요구사항 비교
- 9개 테이블 간 의존성 파악
- 마이그레이션 실행 순서 결정
#### 2단계: 수정
- SAM API Development Rules 준수하여 마이그레이션 작성
- BelongsToTenant, SoftDeletes 적용
- created_by, updated_by, deleted_by 컬럼 (COMMENT 포함)
- FK 제약조건 with ON DELETE CASCADE
- Eloquent 모델 작성
- 관계 설정 (hasMany, belongsTo, hasOne)
- JSON 필드 캐스팅
- ModelTrait 적용
- 시더 작성 (실제 사용 가능한 테스트 데이터)
#### 3단계: 검증
- `php artisan migrate --pretend` 실행 → 순서 확인
- `php artisan migrate` 실행 → 9개 테이블 생성 성공
- `php artisan db:seed --class=ItemMasterSeeder` 실행
- 데이터 검증:
- Unit Options: 6개
- Item Pages: 5개
- Item Sections: 7개
- Item Fields: 15개
- BOM Items: 2개
#### 4단계: 정리
- 임시 파일 확인 → 없음
### 기술적 특징
#### Multi-tenant 지원
- 모든 테이블에 tenant_id 컬럼
- BelongsToTenant trait로 글로벌 스코프 자동 적용
- 테넌트 격리 보장
#### 감사 로그 지원
- created_by, updated_by, deleted_by 컬럼
- 모든 변경사항 추적 가능
#### 유연한 스키마
- JSON 필드로 동적 속성 지원
- display_condition: 조건부 표시
- validation_rules: 유효성 검사 규칙
- options: 드롭다운 옵션
- properties: 필드 속성 (unit, precision, format)
#### 정렬 및 순서 관리
- order_no 컬럼으로 UI 순서 제어
- Composite Index로 빠른 정렬 쿼리
#### 관계 설정
- ItemPage → ItemSection → ItemField (계층 구조)
- ItemPage → ItemSection → ItemBomItem (BOM 구조)
- CustomTab → TabColumn (1:1 관계)
### 다음 단계 (Week 2-3)
- ItemPageController, ItemSectionController 구현
- ItemFieldController, ItemBomItemController 구현
- FormRequest 클래스 작성
- Service 클래스 구현 (비즈니스 로직)
- Swagger 문서 작성 (/api/v1/item-master/*)
### Git 커밋
```bash
git commit 7109fc5
feat: ItemMaster 데이터베이스 구조 구축 (9개 테이블)
- 마이그레이션 9개 생성
- Eloquent 모델 9개 구현
- ItemMasterSeeder 작성 및 테스트 데이터 생성
- Multi-tenant, Soft Delete, 감사 로그 지원
- SAM API Development Rules 준수
21 files changed, 1370 insertions(+)
```
---
## 2025-11-17 (일) - is_active 컬럼 추가 (products, materials 테이블)
### 주요 작업
- products 및 materials 테이블에 is_active 컬럼 추가
- Product 및 Material 모델 업데이트
- ModelTrait의 scopeActive() 메서드 지원
### 문제 상황
- GET /api/v1/items 실행 시 `SQLSTATE[42S22]: Column not found: 1054 Unknown column 'is_active'` 에러 발생
- ItemsService에서 `->where('is_active', 1)` 조건 사용
- ModelTrait에 `scopeActive()` 메서드가 is_active 컬럼 참조
- products, materials 테이블에 is_active 컬럼 없음
### 추가된 파일
1. **database/migrations/2025_11_17_145307_add_is_active_to_products_and_materials_tables.php**
- products 테이블에 is_active 추가 (tinyInteger, default 1, AFTER updated_by)
- materials 테이블에 is_active 추가 (tinyInteger, default 1, AFTER updated_by)
- 롤백 지원 (down 메서드)
### 수정된 파일
1. **app/Models/Products/Product.php**
- fillable에 'is_active' 추가
- casts에 'is_active' => 'boolean' 추가
2. **app/Models/Materials/Material.php**
- fillable에 'material_type', 'is_active' 추가
- casts에 'is_active' => 'boolean' 추가
### 작업 내용
- ModelTrait의 scopeActive() 메서드가 정상 작동하도록 is_active 컬럼 추가
- 기본값 1로 설정하여 기존 데이터는 활성 상태로 유지
- Material 모델에 누락된 material_type도 fillable에 추가
### Git 커밋
```bash
git commit 517d594
feat: products 및 materials 테이블에 is_active 컬럼 추가
- is_active 컬럼 추가 마이그레이션 (default 1)
- Product 모델 fillable 및 casts 업데이트
- Material 모델 fillable 및 casts 업데이트
- Material 모델에 material_type fillable 추가
- ModelTrait의 scopeActive() 메서드 지원
git commit 8ce8a35
fix: is_active 마이그레이션에 컬럼 존재 여부 체크 추가
- Schema::hasColumn()으로 컬럼 존재 여부 확인
- 개발 서버와 로컬 환경의 스키마 차이 대응
- 중복 컬럼 추가 오류 방지
```
### 환경별 차이 발견
- **개발 서버**: is_active 컬럼이 이미 존재함
- **로컬 환경**: is_active 컬럼이 없었음
- **원인**: 환경 간 DB 스키마 동기화가 안 되어 있었음
- **해결**: 컬럼 존재 여부를 체크하는 로직 추가로 양쪽 환경 모두 대응
### 개발 서버 마이그레이션 처리 방법
```bash
# 방법 1: 롤백 후 재실행
php artisan migrate:rollback --step=1
git pull origin develop
php artisan migrate
# 방법 2: 실패한 마이그레이션 레코드만 정리 (is_active가 이미 있으므로)
# migrations 테이블에서 해당 레코드를 수동으로 완료 처리
```
### 검증 완료
- [x] 마이그레이션 실행 성공 (로컬)
- [x] Product 모델 업데이트
- [x] Material 모델 업데이트
- [x] 컬럼 존재 여부 체크 추가
- [x] Pint 포맷팅 통과
- [x] Git 커밋 완료 (2개)
---
## 2025-11-17 (일) - BP-MES Phase 1: Swagger 문서 보완 (GET /items 엔드포인트)
### 주요 작업
- ItemsApi Swagger 문서 누락 부분 추가
- GET /api/v1/items 엔드포인트 문서화 완료
### 수정된 파일
1. **app/Swagger/v1/ItemsApi.php**
- index() 메서드 Swagger 문서 추가
- 품목 목록 조회 (통합) 엔드포인트 문서화
- 페이징 파라미터 (page, size) 정의
- 검색 및 필터 파라미터 정의 (search, product_type, category_id, is_active)
- 응답 스키마 정의 (Item array + PaginationMeta)
### 작업 내용
- 라우트는 존재했지만 Swagger 문서가 누락되어 있던 GET /items 엔드포인트 문서화
- ItemsApi.php의 5개 엔드포인트 중 index()만 문서가 없었던 문제 해결
- l5-swagger:generate 실행으로 Swagger JSON 재생성
### Git 커밋
```bash
git commit 7b8f879
docs: GET /items 엔드포인트 Swagger 문서 추가
- ItemsApi.php에 index() 메서드 문서 추가
- 품목 목록 조회 (통합) 엔드포인트 문서화
- 페이징, 검색, 필터 파라미터 정의
- Swagger JSON 재생성 완료
```
### 검증 완료
- [x] Swagger 문서 추가 (index 메서드)
- [x] Swagger JSON 재생성
- [x] Pint 포맷팅 통과
- [x] Git 커밋 완료
---
## 2025-11-17 (일) - BP-MES Phase 1: Items BOM API 및 File Upload API 구현 완료
### 주요 작업
- BP-MES Phase 1 Day 6-9: Items BOM API 구현 (Code-based Adapter 패턴)
- BP-MES Phase 1 Day 10-12: Items File Upload API 구현 (절곡도, 시방서, 인정서)
- BP-MES Phase 1 Day 13-14: 통합 테스트 가이드 작성
### 추가된 파일
#### Items BOM API (Day 6-9)
1. **app/Http/Controllers/Api/V1/ItemsBomController.php** (184줄)
- Code-based BOM API Adapter
- ProductBomService 100% 재사용
- 10개 엔드포인트 구현
- Code→ID 변환 후 기존 Service 호출
2. **app/Swagger/v1/ItemsBomApi.php** (460줄)
- 10개 엔드포인트 Swagger 문서
- BOMLine, BOMTree, BOMCreateRequest, BOMUpdateRequest 스키마
- 예시 데이터 및 상세 설명
#### Items File Upload API (Day 10-12)
3. **app/Http/Controllers/Api/V1/ItemsFileController.php** (157줄)
- 파일 업로드/삭제 Controller
- 3가지 파일 타입 지원 (bending_diagram, specification, certification)
- Storage facade 사용, items/{code}/{type} 경로 저장
4. **app/Http/Requests/ItemsFileUploadRequest.php** (105줄)
- 파일 타입별 검증 (이미지: jpg/png/gif/svg, 문서: pdf/doc/hwp)
- 파일 크기 제한 (이미지 10MB, 문서 20MB)
- 인증 정보 및 절곡 상세 정보 검증
5. **app/Swagger/v1/ItemsFileApi.php** (179줄)
- 파일 업로드/삭제 Swagger 문서
- ItemFileUploadResponse, ItemFileDeleteResponse 스키마
- multipart/form-data 요청 형식 정의
6. **database/migrations/2025_11_17_125437_add_file_fields_to_products_table.php** (124줄)
- products 테이블에 9개 파일 필드 추가
- bending_diagram, bending_details (JSON)
- specification_file, specification_file_name
- certification_file, certification_file_name
- certification_number, certification_start_date, certification_end_date
#### 통합 테스트 (Day 13-14)
7. **BP-MES_PHASE1_INTEGRATION_TEST.md** (통합 테스트 가이드)
- 17개 엔드포인트 검증 가이드
- 4개 통합 시나리오 (CRUD, BOM, File, 전체)
- 에러 처리 검증 케이스
- 성능 검증 기준
- Swagger UI 테스트 체크리스트
### 수정된 파일
1. **app/Models/Products/Product.php**
- fillable에 9개 파일 필드 추가
- casts: bending_details (array), certification_start_date/end_date (date)
2. **routes/api.php**
- Items BOM 10개 엔드포인트 등록
- Items File 2개 엔드포인트 등록
3. **lang/ko/message.php**
- BOM 메시지 키 추가 (created, updated, deleted)
### 작업 내용
#### 1. Items BOM API (Day 6-9)
**설계 결정:**
- ✅ Adapter 패턴 채택 (Code-based API ← ID-based Service)
- ✅ ProductBomService 100% 재사용 (코드 중복 방지)
- ✅ 코드→ID 변환만 Adapter에서 처리
- ✅ 비즈니스 로직은 기존 Service 활용
**구현 엔드포인트 (10개):**
1. GET /items/{code}/bom - BOM 목록 (flat)
2. GET /items/{code}/bom/tree - BOM 트리 (계층)
3. POST /items/{code}/bom - BOM 추가 (bulk upsert)
4. PUT /items/{code}/bom/{lineId} - BOM 수정
5. DELETE /items/{code}/bom/{lineId} - BOM 삭제
6. GET /items/{code}/bom/summary - BOM 요약
7. GET /items/{code}/bom/validate - BOM 검증
8. POST /items/{code}/bom/replace - BOM 전체 교체
9. POST /items/{code}/bom/reorder - BOM 정렬
10. GET /items/{code}/bom/categories - 카테고리 목록
**Adapter 패턴 구현:**
```php
class ItemsBomController extends Controller
{
public function __construct(private ProductBomService $service) {}
public function index(string $code, Request $request)
{
return ApiResponse::handle(function () use ($code, $request) {
$productId = $this->getProductIdByCode($code);
return $this->service->index($productId, $request->all());
}, __('message.bom.fetch'));
}
private function getProductIdByCode(string $code): int
{
$product = Product::where('tenant_id', app('tenant_id'))
->where('code', $code)
->firstOrFail(['id']);
return $product->id;
}
}
```
#### 2. Items File Upload API (Day 10-12)
**파일 타입:**
- `bending_diagram`: 절곡도 (이미지 - jpg, png, gif, svg, bmp, webp)
- 최대 크기: 10MB
- 추가 필드: bending_details (JSON 배열)
- `specification`: 시방서 (문서 - pdf, doc, docx, xls, xlsx, hwp)
- 최대 크기: 20MB
- 원본 파일명 저장
- `certification`: 인정서 (문서 - pdf, doc, docx, xls, xlsx, hwp)
- 최대 크기: 20MB
- 원본 파일명 + 인증 정보 (번호, 시작일, 종료일)
**파일 저장 구조:**
```
storage/app/public/items/{code}/{type}/{filename}
예: storage/app/public/items/P-001/bending_diagram/abc123.jpg
```
**구현 로직:**
```php
public function upload(string $code, ItemsFileUploadRequest $request)
{
$product = $this->getProductByCode($code);
$fileType = $validated['type'];
$file = $validated['file'];
// 파일 저장
$directory = sprintf('items/%s/%s', $code, $fileType);
$filePath = Storage::disk('public')->putFile($directory, $file);
// Product 업데이트
$updateData = $this->buildUpdateData($fileType, $filePath, ...);
$product->update($updateData);
return [
'file_url' => Storage::url($filePath),
'file_path' => $filePath,
'file_name' => $file->getClientOriginalName(),
'product' => $product->fresh()
];
}
```
#### 3. 통합 테스트 (Day 13-14)
**검증 항목:**
- ✅ 17개 API 엔드포인트 등록 확인
- ✅ Swagger 문서 생성 확인 (999KB)
- ✅ Migration 실행 상태 확인 (batch 28)
- ✅ Product 모델 fillable/casts 확인
- ✅ 통합 테스트 가이드 작성
**테스트 시나리오:**
1. 시나리오 1: 기본 CRUD 흐름 (생성 → 조회 → 수정 → 삭제)
2. 시나리오 2: BOM 관리 흐름 (추가 → 트리 → 요약 → 검증 → 수정 → 정렬)
3. 시나리오 3: 파일 업로드 흐름 (3개 파일 타입 업로드 → 조회 → 삭제)
4. 시나리오 4: 전체 통합 (품목 생성 → BOM → 파일 → 검증)
**에러 처리 검증:**
- 파일 업로드: 잘못된 파일 타입, 크기 초과, 인증 기간 검증
- BOM: 존재하지 않는 품목, 순환 참조
- CRUD: 중복 코드, 필수 필드 누락
### Git 커밋 기록
```bash
# Commit 1: Items BOM API
git commit 87a3f2b
feat: Items BOM API 구현 (Code-based Adapter)
- ItemsBomController 생성 (184줄, 10개 엔드포인트)
- ItemsBomApi Swagger 문서 (460줄)
- routes/api.php에 10개 route 등록
- lang/ko/message.php BOM 메시지 추가
- ProductBomService 100% 재사용
# Commit 2: Items File Upload API
git commit 4749761
feat: 품목 파일 업로드 API 구현 (절곡도, 시방서, 인정서)
- Products 테이블에 9개 파일 필드 추가
- ItemsFileController 구현 (157줄)
- ItemsFileUploadRequest 검증 (105줄)
- ItemsFileApi Swagger 문서 (179줄)
- routes/api.php에 2개 route 등록
```
### DB Schema 변경사항
#### products 테이블 파일 필드 (9개) - Migration 2025_11_17_125437
```sql
-- 절곡도
bending_diagram VARCHAR(255) NULL COMMENT '절곡도 파일 경로 (이미지 URL)'
bending_details JSON NULL COMMENT '절곡 상세 정보 (BendingDetail[])'
-- 시방서
specification_file VARCHAR(255) NULL COMMENT '시방서 파일 경로'
specification_file_name VARCHAR(255) NULL COMMENT '시방서 원본 파일명'
-- 인정서
certification_file VARCHAR(255) NULL COMMENT '인정서 파일 경로'
certification_file_name VARCHAR(255) NULL COMMENT '인정서 원본 파일명'
-- 인증 정보
certification_number VARCHAR(50) NULL COMMENT '인증번호'
certification_start_date DATE NULL COMMENT '인증 시작일'
certification_end_date DATE NULL COMMENT '인증 종료일'
```
### API 엔드포인트 요약
**Items CRUD (5개):**
- GET /items - 목록
- POST /items - 생성
- GET /items/code/{code} - 조회
- PUT /items/{code} - 수정
- DELETE /items/{code} - 삭제
**Items BOM (10개):**
- GET /items/{code}/bom - 목록
- GET /items/{code}/bom/tree - 트리
- POST /items/{code}/bom - 추가
- PUT /items/{code}/bom/{lineId} - 수정
- DELETE /items/{code}/bom/{lineId} - 삭제
- GET /items/{code}/bom/summary - 요약
- GET /items/{code}/bom/validate - 검증
- POST /items/{code}/bom/replace - 전체 교체
- POST /items/{code}/bom/reorder - 정렬
- GET /items/{code}/bom/categories - 카테고리
**Items File (2개):**
- POST /items/{code}/files - 파일 업로드
- DELETE /items/{code}/files/{type} - 파일 삭제
**총 17개 엔드포인트**
### 검증 완료 항목
- [x] Routes 등록 검증 (17개 엔드포인트)
- [x] Swagger 문서 생성 (999KB, Items Files 태그 포함)
- [x] Migration 실행 (batch 28)
- [x] Product 모델 fillable/casts 확인
- [x] Pint 코드 포맷팅 통과
- [x] 통합 테스트 가이드 작성
- [x] Git 커밋 완료 (2개)
### 다음 단계
**Phase 1 완료:**
- ✅ Day 1-2: products/product_components 테이블 확장
- ✅ Day 3-5: ItemsController CRUD API
- ✅ Day 6-9: ItemsBomController API
- ✅ Day 10-12: ItemsFileController API
- ✅ Day 13-14: 통합 테스트
**Phase 2 제안:**
- Frontend 연동 (React/Vue)
- BOM 계산 로직 (수식 평가, 조건부 처리)
- 파일 미리보기 기능
- 대량 데이터 Import/Export
- 품목 복제 기능
- 변경 이력 추적
- 통합 검색 개선
---
# SAM API 저장소 작업 현황
## 2025-11-14 (목) - BP-MES Phase 1: products/product_components 테이블 확장
### 주요 작업
- BP-MES 프로젝트 Phase 1 Day 1-2: Migration 파일 작성 및 실행
- products 테이블에 33개 필드 추가 (ItemMaster 구조 지원)
- product_components 테이블에 5개 필드 추가 (BOMLine 수식 계산)
- Product/ProductComponent 모델 업데이트
- code-workflow 스킬 사용한 체계적 개발 (분석→수정→검증→정리→커밋)
### 추가된 파일
1. **database/migrations/2025_11_13_120000_extend_products_table_for_bp_mes.php**
- products 테이블 확장 Migration
- 33개 필드 추가 (공통 8개, FG 3개, PT 9개, 절곡품 5개, 인증 7개, 동적확장 1개)
- 인덱스 추가 (is_active, product_category, part_type, part_usage)
- up()/down() 메서드 완전 구현
- hasColumn() 체크로 안전성 확보
2. **database/migrations/2025_11_13_120001_extend_product_components_table_for_bp_mes.php**
- product_components 테이블 확장 Migration
- 5개 필드 추가 (quantity_formula, condition, is_bending, bending_diagram, bending_details)
- is_bending 인덱스 추가
- 수식 계산 및 조건부 BOM 지원
### 수정된 파일
1. **app/Models/Products/Product.php**
- fillable에 33개 필드 추가
- 공통: margin_rate, processing_cost, labor_cost, install_cost, safety_stock, lead_time, is_variable_size
- FG 전용: product_category, lot_abbreviation, note
- PT 전용: part_type, part_usage, installation_type, assembly_type, side_spec_width, side_spec_height, assembly_length, guide_rail_model_type, guide_rail_model
- 절곡품: bending_diagram, bending_details, material, length, bending_length
- 인증: certification_number, certification_start_date, certification_end_date, specification_file, specification_file_name, certification_file, certification_file_name
- 동적 확장: options
- casts 추가: is_variable_size (boolean), bending_details (array), options (array), certification_start_date/end_date (date)
2. **app/Models/Products/ProductComponent.php**
- fillable에 5개 필드 추가: quantity_formula, condition, is_bending, bending_diagram, bending_details
- casts 추가: is_bending (boolean), bending_details (array)
3. **LOGICAL_RELATIONSHIPS.md**
- 자동 생성 타임스탬프 업데이트 (2025-11-14 08:41:17)
### 작업 내용
#### 1. 분석 단계 (Sequential Thinking MCP 활용)
- BACKEND_DEVELOPMENT_ROADMAP_V2.md 분석 (lines 44-123, 355-377)
- 기존 products/product_components 테이블 구조 파악
- 필드 충돌 여부 확인 (모두 신규 필드, 충돌 없음)
- Product/ProductComponent 모델 구조 분석
#### 2. 수정 단계
- Migration 파일 2개 작성 (총 507줄 추가)
- 모델 fillable/casts 업데이트
- 체계적 주석 및 그룹화
#### 3. 검증 단계
- Pint 코드 포맷팅 통과 (Migration 2파일, Model 2파일)
- Migration 실행 성공 (products: 307.43ms, product_components: 82.19ms)
- Migration 상태 확인 (batch 27로 실행됨)
#### 4. 정리 단계
- 임시 파일 없음 확인
- 디버깅 코드 없음 확인
#### 5. 커밋
```bash
git commit d5bfb24
feat: BP-MES Phase 1 - products/product_components 테이블 확장
```
### DB Schema 변경사항
#### products 테이블 추가 필드 (33개)
```sql
-- 공통 (8개)
is_active BOOLEAN DEFAULT true
margin_rate DECIMAL(5,2)
processing_cost, labor_cost, install_cost DECIMAL(10,2)
safety_stock, lead_time INT
is_variable_size BOOLEAN DEFAULT false
-- FG 전용 (3개)
product_category VARCHAR(20)
lot_abbreviation VARCHAR(10)
note TEXT
-- PT 전용 (9개)
part_type, installation_type, assembly_type VARCHAR(20)
part_usage VARCHAR(30)
side_spec_width, side_spec_height, assembly_length VARCHAR(20)
guide_rail_model_type, guide_rail_model VARCHAR(50)
-- 절곡품 (5개)
bending_diagram VARCHAR(255)
bending_details JSON
material VARCHAR(50)
length, bending_length VARCHAR(20)
-- 인증 (7개)
certification_number VARCHAR(50)
certification_start_date, certification_end_date DATE
specification_file, specification_file_name VARCHAR(255)
certification_file, certification_file_name VARCHAR(255)
-- 동적 확장 (1개)
options JSON
```
#### product_components 테이블 추가 필드 (5개)
```sql
quantity_formula TEXT -- "W * 2", "H + 100"
condition TEXT -- "MOTOR='Y'", "WIDTH > 3000"
is_bending BOOLEAN DEFAULT false
bending_diagram VARCHAR(255)
bending_details JSON
```
### Git 커밋
- **d5bfb24**: feat: BP-MES Phase 1 - products/product_components 테이블 확장
### 다음 단계
- [ ] Phase 1 Day 3-5: ItemsController/Service 구현
- [ ] RESTful API 엔드포인트 개발 (index, show, store, update, destroy)
- [ ] Swagger 문서 작성
- [ ] FormRequest 검증 추가
---
## 2025-11-13 (수) 19:30 - API Key 보안 강화 및 Rate Limiting 구현
### 주요 작업
- 글로벌 미들웨어로 API Key 검증 적용
- 화이트리스트 확장 (Swagger, Health check 등 공개 엔드포인트)
- Rate Limiting 미들웨어 추가 (10회/분, IP 기반)
- 보안 로그 강화 (무단 접근 시도 기록)
- 민감 정보 로깅 제외 (password 필드)
- code-workflow 스킬 사용한 체계적 보안 강화
### 수정된 파일
1. **app/Http/Middleware/ApiKeyMiddleware.php**
- 화이트리스트 라우트 패턴 매칭 추가 (와일드카드 지원)
- 공개 라우트 API Key 검증 스킵
- 보안 로그 강화 (Log::warning으로 무단 접근 기록)
- 민감 정보 제외 ($request->except(['password', 'password_confirmation']))
2. **app/Http/Middleware/ApiRateLimiter.php** (신규)
- IP 기반 Rate Limiting (10회/분)
- API Key 없는 요청에 대한 속도 제한
- 429 Too Many Requests 응답
- retry_after 헤더 포함
3. **bootstrap/app.php**
- 글로벌 미들웨어 적용 (ApiRateLimiter, ApiKeyMiddleware)
- 미들웨어 실행 순서 최적화 (Rate Limit → API Key 검증)
4. **app/Services/MemberService.php**
- 부서 역할 기반 권한 조회 로직 개선
### 작업 내용
#### 1. 보안 문제 분석
**문제:** `@fs/etc/passwd` 같은 악의적 경로 탐색 공격이 Laravel 라우터까지 도달
**IP:** 213.136.76.215 (자동화된 보안 스캔봇)
**영향:** 불필요한 서버 리소스 낭비, 로그 오염
#### 2. 이중 보호 구조 구현
```
Nginx (L7) → 악의적 패턴 즉시 403 차단
Laravel 글로벌 미들웨어 (Rate Limiting)
Laravel 글로벌 미들웨어 (API Key 검증)
컨트롤러
```
#### 3. 화이트리스트 라우트
- api/v1/login, signup, register, refresh, debug-apikey
- api-docs/* (Swagger UI)
- docs/api-docs.json (Swagger JSON)
- up (Health check)
### Git 커밋
```bash
# API 저장소 (6f7d754)
git commit -m "feat: API Key 보안 강화 및 Rate Limiting 구현
- 글로벌 미들웨어로 API Key 검증 적용
- 화이트리스트 확장 (Swagger, Health check 등)
- Rate Limiting 미들웨어 추가 (10회/분)
- 보안 로그 강화 (무단 접근 시도 기록)
- 민감 정보 로깅 제외 (password 필드)"
```
---
## 2025-11-10 (일) 21:30 - 파일 업로드 DB 에러 및 메시지 구조 개선
### 주요 작업
- files 테이블 감사 컬럼 추가 (created_by, updated_by, uploaded_by)
- ApiResponse::handle() 메시지 로직 개선 (다국어 지원)
- code-workflow 스킬 사용한 체계적 수정
### 수정된 파일
1. **database/migrations/2025_11_10_190208_enhance_files_table.php**
- created_by, updated_by, uploaded_by 컬럼 추가
- down() 메서드 안전한 롤백 로직 추가
2. **app/Helpers/ApiResponse.php**
- handle() 164번: ' 성공' 접미사 제거
- handle() 177번, 145번: ' 실패' 접미사 제거
- 다국어 지원을 위한 완성된 문장 구조 유지
3. **app/Http/Controllers/Api/V1/FileStorageController.php**
- ApiResponse 네임스페이스 수정 (App\Utils → App\Helpers)
4. **app/Http/Requests/Api/V1/FileUploadRequest.php**
- 파일 검증 규칙 수정 (allowed_extensions 사용)
### 작업 내용
#### 1. DB 컬럼 누락 에러 수정
**에러:** `SQLSTATE[42S22]: Column not found: 1054 Unknown column 'created_by'`
**원인:** File 모델 fillable에는 있으나 실제 테이블에는 없음
**해결:** 마이그레이션에 created_by, updated_by, uploaded_by 컬럼 추가
#### 2. 메시지 구조 개선
**문제:** "파일이 업로드되었습니다. 실패" (성공 문구 + 실패 접미사)
**원인:** ApiResponse에서 ' 성공', ' 실패' 하드코딩 + 완성된 문장 충돌
**해결:** 접미사 제거, 완성된 문장 그대로 사용 (다국어 지원)
**결과:**
- 성공: "파일이 업로드되었습니다." ✅
- 실패: "서버 에러" (details에 실제 에러) ✅
### Git 커밋
```bash
git commit -m "fix: 파일 업로드 DB 컬럼 누락 및 메시지 구조 개선
- files 테이블에 감사 컬럼 추가 (created_by, updated_by, uploaded_by)
- ApiResponse::handle() 메시지 로직 개선 (접미사 제거)
- 다국어 지원을 위한 완성된 문장 구조 유지"
```
### TODO
- [ ] **메시지 시스템 전면 개편** (나중에)
- message.php를 동사원형으로 변경
- 다국어 접미사 통일 (success/fail)
- 영향도: 50개+ 파일 수정 필요
---
## 2025-11-10 (일) 20:00 - 파일 저장소 시스템 버그 수정 및 신규 테넌트 폴더 자동 생성
### 주요 작업
- **FolderSeeder 네임스페이스 수정**: `\App\Models\Tenant``\App\Models\Tenants\Tenant`
- **FileStorageController use 문 수정**: 잘못된 네임스페이스 구분자 수정 (`/``\`)
- **TenantObserver 확장**: 신규 테넌트 생성 시 기본 폴더 자동 생성 로직 추가
- **Storage 디렉토리 권한 설정 안내**: `storage/app/tenants/` 생성 및 권한 설정
### 수정된 파일:
**Database Seeder:**
- `database/seeders/FolderSeeder.php` - 네임스페이스 수정 (lines 20-21)
- 수정 전: `\App\Models\Tenant::findOrFail()`, `\App\Models\Tenant::all()`
- 수정 후: `\App\Models\Tenants\Tenant::findOrFail()`, `\App\Models\Tenants\Tenant::all()`
**Controller:**
- `app/Http/Controllers/Api/V1/FileStorageController.php` - use 문 수정 (line 7)
- 수정 전: `use App\Http\Requests\Api\V1/FileMoveRequest;`
- 수정 후: `use App\Http\Requests\Api\V1\FileMoveRequest;`
**Observer:**
- `app/Observers/TenantObserver.php` - 신규 테넌트 기본 폴더 자동 생성 로직 추가
- 기존 TenantBootstrapper 유지
- 5개 기본 폴더 자동 생성 (생산관리, 품질관리, 회계, 인사, 일반)
- try-catch 에러 처리 및 로깅
### 작업 내용:
#### 1. Seeder 네임스페이스 오류 수정
- **문제**: `php artisan db:seed --class=FolderSeeder` 실행 시 "Class 'App\Models\Tenant' not found" 에러
- **원인**: Tenant 모델이 `App\Models\Tenants\Tenant`에 있으나 `App\Models\Tenant`로 참조
- **해결**: FolderSeeder의 Tenant 참조를 올바른 네임스페이스로 수정
#### 2. Controller 구문 오류 수정
- **문제**: Pint 실행 시 "syntax error, unexpected '/'" 에러
- **원인**: use 문에서 잘못된 네임스페이스 구분자 사용 (`/` 대신 `\`)
- **해결**: FileStorageController의 use 문 구분자를 백슬래시로 수정
#### 3. 신규 테넌트 자동 폴더 생성
- **목적**: 신규 테넌트 회원가입 시 수동으로 Seeder를 실행하지 않아도 기본 폴더가 자동 생성되도록 개선
- **구현**: TenantObserver의 `created()` 메서드에 폴더 생성 로직 추가
- **동작**:
1. `Tenant::create()` 호출 시 Observer 자동 트리거
2. TenantBootstrapper 실행 (기존 로직 유지)
3. 5개 기본 폴더 자동 생성 (신규)
4. 에러 발생 시 로그 기록하되 테넌트 생성은 계속 진행
#### 4. Storage 디렉토리 설정
```bash
# 디렉토리 생성
mkdir -p storage/app/tenants
# 권한 설정
chmod 775 storage/app/tenants
# 로컬 개발 환경에서는 현재 사용자 소유권으로 충분
# 프로덕션 환경에서는 웹서버 사용자로 소유권 설정 필요
```
### 테스트 시나리오:
1. **기존 테넌트 폴더 생성**:
```bash
php artisan db:seed --class=FolderSeeder
```
2. **신규 테넌트 폴더 자동 생성**:
- 회원가입 API 호출 또는 `Tenant::create()` 실행
- 자동으로 5개 기본 폴더 생성됨
### Git 커밋:
- `aeeeba6` - fix: 파일 저장소 버그 수정 및 신규 테넌트 폴더 자동 생성
---
## 2025-11-10 (일) - 파일 저장소 시스템 구현 완료 (Phase 2~5)
### 주요 작업
- **파일 저장소 시스템 완성**: Models, Services, Controllers, Commands, Swagger, Config, Routes 전체 구현
- **25개 파일 생성/수정**: Phase 2-5 완료로 구현 가이드 기준 100% 달성
- **코드 품질 검증**: Pint 포맷팅 완료, Swagger 문서 생성 완료
### 추가된 파일 (21개):
**Models (3개):**
- `app/Models/Folder.php` - 폴더 관리 모델
- `app/Models/FileShareLink.php` - 공유 링크 모델
- `app/Models/Commons/File.php` - 기존 파일 모델 확장
**Services (2개):**
- `app/Services/FileStorageService.php` - 파일 저장소 서비스 (Legacy FileService 충돌 방지)
- `app/Services/FolderService.php` - 폴더 관리 서비스
**FormRequests (5개):**
- `app/Http/Requests/Api/V1/FileUploadRequest.php` - 파일 업로드 검증
- `app/Http/Requests/Api/V1/FileMoveRequest.php` - 파일 이동 검증
- `app/Http/Requests/Api/V1/FolderStoreRequest.php` - 폴더 생성 검증
- `app/Http/Requests/Api/V1/FolderUpdateRequest.php` - 폴더 수정 검증
- `app/Http/Requests/Api/V1/ShareLinkRequest.php` - 공유 링크 생성 검증
**Controllers (2개):**
- `app/Http/Controllers/Api/V1/FileStorageController.php` - 파일 저장소 컨트롤러
- `app/Http/Controllers/Api/V1/FolderController.php` - 폴더 관리 컨트롤러
**Commands (4개):**
- `app/Console/Commands/CleanupTempFiles.php` - 7일 이상 임시 파일 정리
- `app/Console/Commands/CleanupTrash.php` - 30일 이상 휴지통 파일 정리
- `app/Console/Commands/CleanupExpiredLinks.php` - 만료된 공유 링크 정리
- `app/Console/Commands/RecordStorageUsage.php` - 일일 용량 사용량 기록
**Swagger (2개):**
- `app/Swagger/v1/FileApi.php` - 파일 저장소 API 문서
- `app/Swagger/v1/FolderApi.php` - 폴더 관리 API 문서
**Database (5개 - Phase 1에서 완료):**
- `database/migrations/2025_11_10_190355_create_file_share_links_table.php`
- `database/migrations/2025_11_10_190355_create_file_deletion_logs_table.php`
- `database/migrations/2025_11_10_190355_create_storage_usage_history_table.php`
- `database/migrations/2025_11_10_190355_add_storage_columns_to_tenants.php`
- `database/seeders/FolderSeeder.php`
### 수정된 파일 (4개):
**Config:**
- `config/filesystems.php` - tenant disk, 파일 제약사항, 저장소 정책, 공유 링크 설정 추가
**i18n:**
- `lang/ko/message.php` - 11개 파일/폴더 성공 메시지 추가
- `lang/ko/error.php` - 17개 파일/폴더 에러 메시지 추가
**Routes:**
- `routes/api.php` - 파일 저장소 및 폴더 관리 라우트 추가
- `routes/console.php` - 4개 스케줄러 등록 (Laravel 12 표준)
**Tenant Model:**
- `app/Models/Tenants/Tenant.php` - 저장소 용량 관리 메서드 8개 추가
### 작업 내용:
#### 1. Phase 2: Models (4개)
**Folder.php:**
```php
- BelongsToTenant, ModelTrait
- scopeActive(), scopeOrdered()
- files() HasMany 관계
```
**FileShareLink.php:**
```php
- 자동 64자 토큰 생성 (bin2hex(random_bytes(32)))
- isExpired(), isValid(), isDownloadLimitReached()
- incrementDownloadCount() 다운로드 추적
```
**File.php (확장):**
```php
- BelongsToTenant 추가
- moveToFolder() - temp → folder_key 이동
- permanentDelete() - 물리 삭제 + 용량 차감
- download() - Storage Facade 통합
```
**Tenant.php (확장):**
```php
- canUpload() - 90% 경고 → 7일 유예 로직
- incrementStorage(), decrementStorage()
- resetGracePeriod(), isInGracePeriod()
```
#### 2. Phase 3: Services/Controllers/Requests (9개)
**FileStorageService (신규 서비스):**
```php
- upload() - temp 업로드 + 용량 체크
- moveToFolder() - temp → folder 이동
- deleteFile() - soft delete + 삭제 로그
- restoreFile() - 복구
- permanentDelete() - 물리 삭제
- createShareLink() - 공유 링크 생성
- getFileByShareToken() - 공유 링크 검증
```
**FolderService:**
```php
- index() - 폴더 목록 (display_order)
- store() - 폴더 생성 (자동 순서)
- update() - 폴더 수정
- destroy() - 비활성화 (파일 있으면 거부)
- reorder() - 순서 일괄 변경
```
**FileStorageController:**
```php
10개 엔드포인트:
- upload, move, index, show, trash, download
- destroy, restore, permanentDelete, createShareLink
```
**FolderController:**
```php
6개 엔드포인트:
- index, store, show, update, destroy, reorder
```
#### 3. Phase 4: Commands + Scheduler + Swagger (7개)
**Commands (4개):**
```php
CleanupTempFiles (매일 03:30)
- 7일 이상 temp 파일 삭제
- Storage + DB 동기화
CleanupTrash (매일 03:40)
- 30일 이상 삭제 파일 영구 삭제
- file_deletion_logs 기록
CleanupExpiredLinks (매일 03:50)
- 만료된 공유 링크 삭제
RecordStorageUsage (매일 04:00)
- 테넌트별 용량 사용량 기록
- 폴더별 사용량 JSON 저장
```
**routes/console.php (Laravel 12):**
```php
Schedule::command('storage:cleanup-temp')
->dailyAt('03:30')
->appendOutputTo(storage_path('logs/scheduler.log'))
->onSuccess/onFailure 로그
```
**Swagger 문서 (2개):**
```php
FileApi.php:
- 10개 엔드포인트 완전 문서화
- File 모델 스키마 정의
- FileUploadRequest, FileMoveRequest, ShareLinkRequest 스키마
FolderApi.php:
- 6개 엔드포인트 완전 문서화
- Folder 모델 스키마 정의
- FolderStoreRequest, FolderUpdateRequest, FolderReorderRequest 스키마
```
#### 4. Phase 5: Config/i18n/Routes (4개)
**config/filesystems.php:**
```php
'tenant' => [
'driver' => 'local',
'root' => storage_path('app/tenants'),
]
'file_constraints' => [
'max_file_size' => 20MB,
'allowed_extensions' => [pdf, doc, image, archive...],
]
'storage_policies' => [
'default_limit' => 10GB,
'warning_threshold' => 0.9,
'grace_period_days' => 7,
'trash_retention_days' => 30,
]
'share_link' => [
'expiry_hours' => 24,
'max_downloads' => null,
]
```
**i18n 메시지 (28개):**
```php
lang/ko/message.php (11개):
- file_uploaded, files_moved, file_deleted
- file_restored, file_permanently_deleted
- share_link_created, storage_exceeded_grace_period
- folder_created, folder_updated, folder_deleted
- folders_reordered
lang/ko/error.php (17개):
- file_not_found, folder_not_found
- storage_quota_exceeded, share_link_expired
- folder_key_duplicate, folder_has_files
- color_format, expiry_hours_min/max
```
**routes/api.php:**
```php
파일 저장소 (10개):
- POST /files/upload
- POST /files/move
- GET /files (+ trash)
- GET/DELETE /files/{id}
- POST /files/{id}/restore
- DELETE /files/{id}/permanent
- POST /files/{id}/share
- GET /files/share/{token} (공개)
폴더 관리 (6개):
- GET/POST /folders
- GET/PUT/DELETE /folders/{id}
- POST /folders/reorder
```
### 설계 특징:
**1. 경로 구조:**
```
/storage/app/tenants/
├── {tenant_id}/
│ ├── temp/{year}/{month}/{stored_name} # 업로드 직후
│ ├── product/{year}/{month}/{stored_name} # 문서 첨부 후
│ ├── quality/{year}/{month}/{stored_name}
│ └── accounting/{year}/{month}/{stored_name}
```
**2. 워크플로우:**
```
1. 파일 업로드
- POST /files/upload
- temp 폴더에 저장 (is_temp=true)
- 64자 난수 파일명 (보안)
2. 문서에 첨부
- POST /files/move
- temp → folder_key 이동
- document_id, document_type 설정
3. 공유 링크 생성
- POST /files/{id}/share
- 64자 토큰 + 24시간 만료
- 다운로드 횟수 추적
4. 삭제/복구
- DELETE (soft delete)
- POST restore (복구)
- DELETE permanent (영구 삭제)
```
**3. 용량 관리:**
```
업로드 시:
- tenants.storage_used 증가
- 90% 도달 → 경고 이메일 + 7일 유예
- 100% 초과 + 유예 기간 내 → 업로드 허용
- 유예 만료 → 업로드 차단
```
**4. 자동 정리:**
```
매일 새벽:
- 03:30: 7일 이상 temp 파일 삭제
- 03:40: 30일 이상 휴지통 파일 영구 삭제
- 03:50: 만료된 공유 링크 삭제
- 04:00: 테넌트별 용량 사용량 기록
```
### 기술 세부사항:
#### Laravel 12 Scheduler (🔴 중요!)
```php
// ❌ 기존 (Laravel 11): Kernel.php
protected function schedule(Schedule $schedule) { ... }
// ✅ Laravel 12: routes/console.php
Schedule::command('storage:cleanup-temp')
->dailyAt('03:30')
->appendOutputTo(storage_path('logs/scheduler.log'))
->onSuccess/onFailure
```
#### Storage Facade 추상화
```php
// 현재: local disk
Storage::disk('tenant')->put($path, $file);
// 미래: S3로 전환 (설정만 변경)
'tenant' => ['driver' => 's3', 'bucket' => env('AWS_BUCKET')]
```
#### 보안: 64bit 난수 파일명
```php
bin2hex(random_bytes(32))
→ "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2.pdf"
```
### SAM API Development Rules 준수:
✅ **Service-First 아키텍처:**
- FileStorageService, FolderService에 모든 로직
- Controller는 DI + ApiResponse::handle()
✅ **FormRequest 검증:**
- 5개 FormRequest로 모든 검증 분리
✅ **i18n 메시지 키:**
- __('message.xxx'), __('error.xxx') 28개 추가
✅ **Swagger 문서:**
- 별도 파일 (FileApi.php, FolderApi.php)
- 16개 엔드포인트 완전 문서화
✅ **멀티테넌시:**
- BelongsToTenant 스코프
- tenant_id 격리
✅ **감사 로그:**
- file_deletion_logs 테이블
- created_by, updated_by, deleted_by
✅ **SoftDeletes:**
- File 모델 soft delete
- 30일 휴지통 보관
✅ **코드 품질:**
- Laravel Pint 포맷팅 완료
- Swagger 문서 생성 완료
### 예상 효과:
1. **완전한 파일 관리**: 업로드 → 이동 → 공유 → 삭제 → 복구 전체 워크플로우
2. **용량 제어**: 90% 경고 → 7일 유예 → 차단 단계별 관리
3. **자동 정리**: 임시/삭제 파일 자동 정리로 디스크 최적화
4. **클라우드 전환 용이**: Storage Facade로 S3 마이그레이션 간단
5. **감사 추적**: 파일 삭제 로그, 용량 사용량 히스토리
### 다음 작업:
- [ ] 마이그레이션 실행: `php artisan migrate`
- [ ] 폴더 시더 실행: `php artisan db:seed --class=FolderSeeder`
- [ ] storage/app/tenants/ 디렉토리 생성 및 권한 설정
- [ ] API 테스트 (Postman/Swagger UI)
- [ ] Frontend 파일 업로드 UI 구현
### Git 커밋 준비:
- 다음 커밋 예정: `feat: 파일 저장소 시스템 구현 완료 (Phase 2-5, 25개 파일)`
---
## 2025-11-10 (일) - 파일 저장 시스템 구현 시작 (Phase 1: DB 마이그레이션)
### 주요 작업
- **파일 저장 시스템 기반 구축**: 로컬 저장 우선, 클라우드(S3) 전환 가능 구조
- **DB 마이그레이션 7개 생성**: files 테이블 개선, folders, file_share_links, file_deletion_logs, storage_usage_history, tenants 용량 관리
- **설계 기반**: `/claudedocs/file_storage_implementation_guide.md` 참조
### 추가된 파일 (7개):
- `database/migrations/2025_11_10_190208_enhance_files_table.php` - files 테이블 구조 개선 (완료)
- `database/migrations/2025_11_10_190257_create_folders_table.php` - 동적 폴더 관리 테이블 (완료)
- `database/migrations/2025_11_10_190355_create_file_share_links_table.php` - 외부 공유 링크 테이블 (stub)
- `database/migrations/2025_11_10_190355_create_file_deletion_logs_table.php` - 파일 삭제 로그 테이블 (stub)
- `database/migrations/2025_11_10_190355_create_storage_usage_history_table.php` - 용량 히스토리 테이블 (stub)
- `database/migrations/2025_11_10_190355_add_storage_columns_to_tenants.php` - 테넌트 용량 관리 컬럼 (stub)
- `database/seeders/FolderSeeder.php` - 기본 폴더 시더 (stub)
### 작업 내용:
#### 1. files 테이블 개선 (완료)
```php
// 새로운 컬럼
- display_name: 사용자가 보는 파일명 (예: 도면.pdf)
- stored_name: 실제 저장 파일명 (예: a1b2c3d4e5f6g7h8.pdf, 64bit 난수)
- folder_id: folders 테이블 FK
- is_temp: temp 폴더 여부 (업로드 직후 true)
- file_type: document/image/excel/archive
- document_id: 문서 ID (polymorphic 대체)
- document_type: 문서 타입 (work_order, quality_check 등)
- deleted_by: 삭제자 ID
// 인덱스
- idx_tenant_folder: (tenant_id, folder_id)
- is_temp, document_id, created_at, stored_name
```
#### 2. folders 테이블 생성 (완료)
```php
// 동적 폴더 관리
- folder_key: product, quality, accounting (고유 키)
- folder_name: 생산관리, 품질관리, 회계 (표시명)
- display_order: 정렬 순서
- is_active: 활성 여부
- icon, color: UI 커스터마이징
// 유니크 제약
- (tenant_id, folder_key)
// 인덱스
- (tenant_id, is_active)
- (tenant_id, display_order)
```
#### 3. 나머지 마이그레이션 (stub 생성만 완료)
- file_share_links: 24시간 임시 공유 링크
- file_deletion_logs: 삭제 감사 추적
- storage_usage_history: 용량 사용량 히스토리
- tenants 용량 관리: storage_limit, storage_used, storage_warning_sent_at, storage_grace_period_until
### 설계 특징:
**1. 로컬 → 클라우드 전환 용이:**
```php
// 현재 (로컬)
'tenant' => [
'driver' => 'local',
'root' => storage_path('app/tenants'),
]
// 전환 후 (S3) - driver만 변경
'tenant' => [
'driver' => 's3',
'bucket' => env('AWS_BUCKET'),
]
```
**2. 파일 경로 구조:**
```
/storage/app/tenants/
├── {tenant_id}/
│ ├── product/{year}/{month}/{stored_name}
│ ├── quality/{year}/{month}/{stored_name}
│ ├── accounting/{year}/{month}/{stored_name}
│ └── temp/{year}/{month}/{stored_name}
```
**3. 용량 관리:**
- 기본 한도: 10GB
- 90% 경고 → 이메일 발송 + 7일 유예
- 100% 초과 → 유예 기간 내 업로드 허용
- 유예 만료 → 업로드 차단
### 다음 작업 (새 세션에서 진행):
**Phase 2: 모델 및 Service (4개)**
- [ ] File 모델 리팩토링 (BelongsToTenant, Storage 통합)
- [ ] Folder 모델 생성
- [ ] FileShareLink 모델 생성
- [ ] FileService 전면 리팩토링 (Storage Facade 사용)
**Phase 3: Controller 및 API (7개)**
- [ ] FolderService 생성
- [ ] FormRequest 5개 생성
- [ ] FileController 리팩토링
- [ ] FolderController 생성
**Phase 4: 문서 및 배치 (6개)**
- [ ] Swagger 문서 2개
- [ ] Commands 4개 (temp 정리, 휴지통 정리, 링크 정리, 용량 기록)
**Phase 5: 설정 (3개)**
- [ ] config/filesystems.php 수정
- [ ] i18n 메시지 추가
- [ ] routes/api.php 업데이트
### Git 커밋 준비:
- 다음 커밋 예정: `feat: 파일 저장 시스템 DB 마이그레이션 (Phase 1)`
---
## 2025-11-10 (일) - API 토큰 관리 시스템 구현 (액세스/리프레시 토큰 분리 + 자동 정리)
### 주요 작업
- **액세스/리프레시 토큰 분리**: 액세스 토큰(2시간), 리프레시 토큰(7일) 독립 관리
- **환경별 설정**: .env 기반 토큰 만료 시간 설정 (설정 없으면 무제한)
- **토큰 갱신 엔드포인트**: POST /api/v1/refresh (리프레시 토큰으로 새 토큰 발급)
- **보안 강화**: 리프레시 토큰 일회성 사용, 사용자당 1개 리프레시 토큰만 유지
- **에러 처리**: TOKEN_EXPIRED 에러 코드로 프론트엔드 자동 리프레시 지원
- **자동 정리 스케줄러**: 만료 토큰 자동 삭제 (매일 새벽 3:20)
### 추가된 파일:
- `app/Services/AuthService.php` - 토큰 발급/갱신 통합 서비스 (119줄)
- `app/Http/Controllers/Api/V1/RefreshController.php` - 토큰 갱신 컨트롤러 (32줄)
- `app/Http/Requests/Api/V1/RefreshRequest.php` - 리프레시 토큰 검증 (22줄)
- `app/Swagger/v1/RefreshApi.php` - 토큰 갱신 API 문서 (69줄)
### 수정된 파일:
- `.env` - 토큰 만료 설정 추가 (ACCESS: 120분, REFRESH: 10080분)
- `config/sanctum.php` - 토큰 만료 설정 키 추가
- `app/Http/Controllers/Api/V1/ApiController.php` - 로그인 시 AuthService 사용
- `app/Exceptions/Handler.php` - 토큰 만료 에러 처리 (TOKEN_EXPIRED)
- `app/Http/Middleware/ApiKeyMiddleware.php` - refresh 라우트 화이트리스트 추가
- `app/Swagger/v1/AuthApi.php` - 로그인 응답에 토큰 필드 추가
- `lang/ko/error.php` - 토큰 관련 에러 메시지 4개 추가
- `lang/ko/message.php` - token_refreshed 메시지 추가
- `routes/api.php` - POST /api/v1/refresh 라우트 추가
### 작업 내용:
#### 1. AuthService 구현
**토큰 발급 (issueTokens):**
```php
public static function issueTokens(User $user): array
{
// 기존 리프레시 토큰 삭제 (한 사용자당 하나만 유지)
$user->tokens()->where('name', 'refresh-token')->delete();
// 액세스 토큰 만료 시간 (분 단위, null이면 무제한)
$accessExpiration = Config::get('sanctum.access_token_expiration');
$accessExpiration = $accessExpiration ? (int) $accessExpiration : null;
$accessExpiresAt = $accessExpiration ? now()->addMinutes($accessExpiration) : null;
// 리프레시 토큰 만료 시간 (분 단위, null이면 무제한)
$refreshExpiration = Config::get('sanctum.refresh_token_expiration');
$refreshExpiration = $refreshExpiration ? (int) $refreshExpiration : null;
$refreshExpiresAt = $refreshExpiration ? now()->addMinutes($refreshExpiration) : null;
// 액세스 토큰 생성
$accessToken = $user->createToken('access-token', ['*'], $accessExpiresAt);
// 리프레시 토큰 생성
$refreshToken = $user->createToken('refresh-token', ['refresh'], $refreshExpiresAt);
return [
'access_token' => $accessToken->plainTextToken,
'refresh_token' => $refreshToken->plainTextToken,
'token_type' => 'Bearer',
'expires_in' => $accessExpiration ? $accessExpiration * 60 : null,
'expires_at' => $accessExpiresAt ? $accessExpiresAt->toDateTimeString() : null,
];
}
```
**토큰 갱신 (refreshTokens):**
```php
public static function refreshTokens(string $refreshToken): ?array
{
// 리프레시 토큰 검증
$token = \Laravel\Sanctum\PersonalAccessToken::findToken($refreshToken);
if (!$token || $token->name !== 'refresh-token') {
return null;
}
// 만료 확인
if ($token->expires_at && $token->expires_at->isPast()) {
$token->delete();
return null;
}
$user = $token->tokenable;
// 기존 리프레시 토큰 삭제 (사용 후 폐기)
$token->delete();
// 새로운 액세스 + 리프레시 토큰 발급
return self::issueTokens($user);
}
```
**핵심 특징:**
- ✅ 사용자당 1개의 리프레시 토큰만 유지
- ✅ 리프레시 토큰은 일회성 (사용 후 삭제)
- ✅ 토큰 갱신 시 액세스 + 리프레시 모두 새로 발급
- ✅ 타입 캐스팅 (.env 값은 문자열이므로 int 변환 필수)
#### 2. RefreshController 구현
```php
public function refresh(RefreshRequest $request): JsonResponse
{
$refreshToken = $request->validated()['refresh_token'];
// 리프레시 토큰으로 새로운 토큰 발급
$tokens = AuthService::refreshTokens($refreshToken);
if (!$tokens) {
return response()->json([
'error' => __('error.refresh_token_invalid_or_expired'),
'error_code' => 'TOKEN_EXPIRED',
], 401);
}
return response()->json([
'message' => __('message.token_refreshed'),
'access_token' => $tokens['access_token'],
'refresh_token' => $tokens['refresh_token'],
'token_type' => $tokens['token_type'],
'expires_in' => $tokens['expires_in'],
'expires_at' => $tokens['expires_at'],
]);
}
```
#### 3. Handler 토큰 만료 에러 처리
```php
// 401 Unauthorized
if ($exception instanceof AuthenticationException) {
// 토큰 만료 여부 확인
$errorCode = null;
$message = '인증 실패';
// Bearer 토큰이 있는 경우 만료 여부 확인
$bearerToken = $request->bearerToken();
if ($bearerToken) {
$token = \Laravel\Sanctum\PersonalAccessToken::findToken($bearerToken);
if ($token && $token->expires_at && $token->expires_at->isPast()) {
$errorCode = 'TOKEN_EXPIRED';
$message = __('error.token_expired');
}
}
return response()->json([
'success' => false,
'message' => $message,
'error_code' => $errorCode,
'data' => null,
], 401);
}
```
#### 4. 환경 설정 (.env)
```env
# Sanctum 토큰 만료 설정 (분 단위, null이면 무제한)
SANCTUM_ACCESS_TOKEN_EXPIRATION=120 # 2시간 (운영 기준)
SANCTUM_REFRESH_TOKEN_EXPIRATION=10080 # 7일
```
#### 5. Swagger 문서
**POST /api/v1/refresh:**
```php
@OA\Post(
path="/api/v1/refresh",
tags={"Auth"},
summary="토큰 갱신 (리프레시 토큰으로 새로운 액세스 토큰 발급)",
description="리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급받습니다. 리프레시 토큰은 사용 후 폐기되며 새로운 리프레시 토큰이 발급됩니다.",
security={{"ApiKeyAuth": {}}},
)
```
**로그인 응답 업데이트:**
```php
@OA\Property(property="access_token", type="string", example="1|abc123xyz456", description="액세스 토큰 (API 호출에 사용)"),
@OA\Property(property="refresh_token", type="string", example="2|def456uvw789", description="리프레시 토큰 (액세스 토큰 갱신에 사용)"),
@OA\Property(property="token_type", type="string", example="Bearer", description="토큰 타입"),
@OA\Property(property="expires_in", type="integer", nullable=true, example=7200, description="액세스 토큰 만료 시간 (초 단위, null이면 무제한)"),
@OA\Property(property="expires_at", type="string", nullable=true, example="2025-11-10 16:00:00", description="액세스 토큰 만료 시각 (null이면 무제한)"),
```
### 기술 세부사항:
#### OAuth 2.0 표준 준수
- `token_type: "Bearer"` 포함 (RFC 6749 표준)
- 토큰 갱신 시 refresh token rotation (보안 강화)
- 만료 시간 명시 (expires_in, expires_at)
#### 보안 설계
```
1. 리프레시 토큰 일회성:
- 사용 시 즉시 삭제
- 새 리프레시 토큰 발급
- 도난 토큰 재사용 방지
2. 사용자당 1개 제한:
- 새 리프레시 토큰 발급 시 이전 것 삭제
- 멀티 디바이스 로그인 제한 (필요 시 변경 가능)
3. 타입 안전성:
- .env 값 타입 캐스팅 필수
- Carbon::addMinutes()는 int만 허용
```
#### 데이터베이스 영향
```sql
-- personal_access_tokens 테이블
SELECT id, name, expires_at, created_at
FROM personal_access_tokens
WHERE tokenable_id = 1
ORDER BY id DESC
LIMIT 5;
-- 결과:
ID: 184 | Name: refresh-token | Expires: 2025-11-17 11:06:28
ID: 183 | Name: access-token | Expires: 2025-11-10 13:06:28
```
### SAM API Development Rules 준수:
✅ **Service-First 아키텍처:**
- AuthService에 모든 토큰 로직
- Controller는 DI + 응답만
✅ **FormRequest 검증:**
- RefreshRequest로 리프레시 토큰 검증
✅ **i18n 메시지 키:**
- __('message.token_refreshed'), __('error.xxx') 사용
✅ **Swagger 문서:**
- 별도 파일 (app/Swagger/v1/RefreshApi.php)
- Auth 태그로 그룹화
✅ **보안:**
- 토큰 일회성 사용
- 만료 시간 검증
- 에러 코드 명시 (TOKEN_EXPIRED)
✅ **코드 품질:**
- 타입 안전성 (int 캐스팅)
- 명확한 주석
### 테스트 결과:
**Tinker 테스트:**
```bash
php artisan tinker --execute="
\$user = User::find(1);
\$tokens = \App\Services\AuthService::issueTokens(\$user);
echo 'Access Token: ' . substr(\$tokens['access_token'], 0, 20) . '...' . PHP_EOL;
echo 'Refresh Token: ' . substr(\$tokens['refresh_token'], 0, 20) . '...' . PHP_EOL;
echo 'Expires In: ' . \$tokens['expires_in'] . ' seconds' . PHP_EOL;
echo 'Expires At: ' . \$tokens['expires_at'] . PHP_EOL;
"
# 결과:
Access Token: 177|MtYCVI4XDqX5GXA...
Refresh Token: 178|rpoDdTsZ9orU2g3...
Expires In: 7200 seconds (120 minutes)
Expires At: 2025-11-10 13:01:21
```
**API 엔드포인트 테스트:**
```bash
# 로그인
curl -X POST "http://api.sam.kr/api/v1/login" \
-H "Content-Type: application/json" \
-H "X-API-KEY: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a" \
-d '{"email":"hamss@codebridge-x.com","password":"test1234"}'
# 토큰 갱신
curl -X POST "http://api.sam.kr/api/v1/refresh" \
-H "Content-Type: application/json" \
-H "X-API-KEY: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a" \
-d '{"refresh_token":"182|vsdUYz2WVaFxC05TWp4M0njVLhh833jPK6ilN5AB8ee106ad"}'
# 응답:
{
"message": "토큰이 갱신되었습니다",
"access_token": "183|pfbAqUvAZ2meTVKisDDC8MwnhBUCoMVsK7GXoh8aa1c832c5",
"refresh_token": "184|yNJJiqNF4GeH2u3YFAQr7mISYmLdEfiSdq9CdD00c1d7538d",
"token_type": "Bearer",
"expires_in": 7200,
"expires_at": "2025-11-10 13:06:28"
}
```
**데이터베이스 검증:**
```sql
-- 최근 발급된 토큰 확인
SELECT id, name, expires_at, created_at
FROM personal_access_tokens
WHERE tokenable_id = 1
ORDER BY id DESC LIMIT 5;
-- 결과:
✅ 새 토큰 발급: Access (ID: 183) + Refresh (ID: 184)
✅ 이전 리프레시 토큰 삭제: ID 182 삭제됨
✅ 만료 시간 설정: Access 2시간 후, Refresh 7일 후
```
### 예상 효과:
1. **보안 강화**: 단기 액세스 토큰 + 장기 리프레시 토큰
2. **세션 관리**: 리프레시 토큰 갱신으로 지속적인 로그인 유지
3. **에러 처리**: TOKEN_EXPIRED 코드로 프론트엔드 자동 리프레시 구현 가능
4. **유연성**: 환경별 토큰 만료 시간 설정 (개발/운영 분리)
### 다음 작업:
- [x] AuthService 구현
- [x] RefreshController 구현
- [x] Handler 에러 처리
- [x] Swagger 문서 작성 (Auth 태그)
- [x] i18n 메시지 추가
- [x] Tinker 테스트
- [x] API 엔드포인트 테스트
- [x] DB 검증
- [ ] Frontend 토큰 갱신 로직 구현
- [ ] 만료 토큰 정리 스케줄러 (선택)
### Git 커밋:
- 다음 커밋 예정: `feat: API 토큰 관리 시스템 구현 (액세스/리프레시 토큰 분리)`
---
## 2025-11-10 (일) - 회원가입 쿼리 최적화 (268개 → 58개, 78% 감소)
### 주요 작업
- **MenuObserver 성능 최적화**: Bulk insert + 지연 캐시 삭제로 메뉴당 28개 쿼리 → 3개 쿼리
- **RegisterService 중복 제거**: 권한 생성 로직 중복 제거 (27개 쿼리 감소)
- **캐시 삭제 최적화**: 126개 캐시 삭제 → 11개 (91% 감소)
- **확장성 유지**: 관리자의 메뉴 추가 시에도 동일한 최적화 적용
### 수정된 파일:
- `app/Observers/MenuObserver.php` - Bulk insert 및 DB::afterCommit() 활용
- `app/Services/RegisterService.php` - 중복 권한 생성 로직 제거
### 작업 내용:
#### 1. 문제 분석
**증상:**
```
회원가입 시 268개 쿼리 실행 (과다)
- MenuObserver: 9개 메뉴 × 28개 = 252개
- RegisterService 중복: 9개 × 3개 = 27개
- 기타: 19개
```
**원인:**
- MenuObserver가 메뉴 생성 시마다 7개 권한을 **개별 INSERT** (menu:{id}.view, create, update, delete, approve, export, manage)
- 각 권한 INSERT마다 **캐시 즉시 삭제** (배치 처리 안 됨)
- RegisterService가 **다른 패턴**으로 권한 중복 생성 (menu.{id})
**쿼리 분석:**
```
메뉴 1개당:
- SELECT 존재확인 × 7 = 7개
- INSERT 권한 × 7 = 7개
- DELETE 캐시 × 7 × 2 = 14개
총 28개 쿼리
9개 메뉴 × 28 = 252개 쿼리
```
#### 2. MenuObserver.php 최적화
**Before (개별 INSERT):**
```php
protected function ensurePermissions(Menu $menu): void
{
foreach ($this->actions() as $act) {
Permission::firstOrCreate([
'tenant_id' => (int) $menu->tenant_id,
'guard_name' => $this->guard,
'name' => "menu:{$menu->id}.{$act}",
]); // 7번 반복 = 28개 쿼리
}
}
```
**After (Bulk Insert + 지연 캐시):**
```php
protected function ensurePermissions(Menu $menu): void
{
$actions = $this->actions();
$permissionsData = [];
$now = now();
foreach ($actions as $act) {
$permissionsData[] = [
'tenant_id' => (int) $menu->tenant_id,
'guard_name' => $this->guard,
'name' => "menu:{$menu->id}.{$act}",
'created_at' => $now,
'updated_at' => $now,
];
}
// Bulk insert (7개를 1번에)
DB::table('permissions')->insertOrIgnore($permissionsData);
}
public function created(Menu $menu): void
{
// ...
$this->ensurePermissions($menu);
$this->forgetCacheAfterCommit(); // 트랜잭션 종료 후 1번만
}
protected function forgetCacheAfterCommit(): void
{
DB::afterCommit(function () {
app(PermissionRegistrar::class)->forgetCachedPermissions();
});
}
```
**개선 효과:**
- 메뉴 1개당: 28개 쿼리 → **3개 쿼리** (bulk insert + 지연 캐시)
- 9개 메뉴: 252개 → **27개 쿼리**
#### 3. RegisterService.php 중복 제거
**Before (중복 권한 생성):**
```php
// 8. Create permissions for each menu and assign to role
$permissions = [];
foreach ($menuIds as $menuId) {
$permName = "menu.{$menuId}"; // ❌ 다른 패턴 (menu.{id})
$perm = Permission::firstOrCreate([
'tenant_id' => $tenant->id,
'guard_name' => 'api',
'name' => $permName,
]); // 9개 × 3개 쿼리 = 27개 추가 쿼리
$permissions[] = $perm;
}
$role->syncPermissions($permissions);
```
**After (Observer 권한 재사용):**
```php
// 8. Get all permissions created by MenuObserver (menu:{id}.{action} pattern)
$permissionNames = [];
$actions = config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve']);
foreach ($menuIds as $menuId) {
foreach ($actions as $action) {
$permissionNames[] = "menu:{$menuId}.{$action}";
}
}
$permissions = Permission::whereIn('name', $permissionNames)
->where('tenant_id', $tenant->id)
->where('guard_name', 'api')
->get(); // 1개 쿼리로 모든 권한 조회
// 9. Assign all menu permissions to system_manager role
$role->syncPermissions($permissions);
```
**개선 효과:**
- 중복 생성 제거: **27개 쿼리 감소**
- 권한 패턴 통일: `menu:{id}.{action}` 형식으로 일관성 유지
#### 4. 최종 결과
**쿼리 구성 (총 58개):**
```
- INSERT menus : 9개
- INSERT permissions (bulk) : 9개 (메뉴당 7개씩 일괄)
- DELETE cache : 11개 (이전 126개 → 91% 감소)
- INSERT tenants/users/roles : 5개
- INSERT tenant_bootstrap : 6개
- SELECT/기타 : 18개
──────────────────────────────────────
총합: 58개 (이전 268개 대비 78% 감소)
```
**데이터 검증:**
```
✅ 메뉴: 9개 생성
✅ 권한: 63개 생성 (9메뉴 × 7액션)
- 액션: view, create, update, delete, approve, export, manage
✅ 권한 패턴: menu:{id}.{action} (통일됨)
✅ Role 할당: system_manager에 모든 권한 부여
```
### 기술 세부사항:
#### Bulk Insert 최적화
```php
// Before: 7번의 개별 INSERT
Permission::firstOrCreate([...]); // × 7
// After: 1번의 Bulk INSERT
DB::table('permissions')->insertOrIgnore([
[...], // 7개의 레코드
[...],
// ...
]);
```
#### 지연 캐시 삭제 (DB::afterCommit)
```php
// Before: 권한마다 즉시 캐시 삭제
foreach ($actions as $act) {
Permission::firstOrCreate([...]);
app(PermissionRegistrar::class)->forgetCachedPermissions(); // × 7
}
// After: 트랜잭션 종료 후 1번만
DB::afterCommit(function () {
app(PermissionRegistrar::class)->forgetCachedPermissions(); // × 1
});
```
#### 권한 패턴 통일
```
Before:
- MenuObserver: menu:{id}.view, menu:{id}.create, ...
- RegisterService: menu.{id} (중복!)
After:
- MenuObserver: menu:{id}.view, menu:{id}.create, ...
- RegisterService: MenuObserver 권한 재사용 (중복 제거)
```
### SAM API Development Rules 준수:
✅ **성능 최적화:**
- Bulk insert로 쿼리 횟수 최소화
- 캐시 삭제를 트랜잭션 단위로 배치 처리
✅ **확장성 유지:**
- 관리자가 나중에 메뉴 추가 시에도 동일한 최적화 적용
- Role/Department/User별 세밀한 권한 제어 가능
✅ **코드 일관성:**
- 권한 패턴 통일 (menu:{id}.{action})
- 중복 로직 제거
✅ **코드 품질:**
- Laravel Pint 포맷팅 완료 (2 files)
### 예상 효과:
1. **성능 향상**: 회원가입 응답 속도 개선 (쿼리 78% 감소)
2. **서버 부하 감소**: DB 커넥션 사용량 대폭 감소
3. **확장성 유지**: 미래 메뉴 추가 시에도 최적화 효과 지속
4. **유지보수성**: 권한 패턴 통일로 코드 이해도 향상
### 테스트 결과:
```bash
php artisan tinker --execute="
DB::enableQueryLog();
\$result = App\Services\RegisterService::register([...]);
\$queries = DB::getQueryLog();
echo '쿼리 수: ' . count(\$queries) . '개';
"
# 결과: 58개 (이전 268개)
```
### 다음 작업:
- [x] MenuObserver bulk insert 구현
- [x] 지연 캐시 삭제 (DB::afterCommit)
- [x] RegisterService 중복 권한 생성 제거
- [x] Pint 포맷팅
- [x] 회원가입 테스트 및 쿼리 수 검증
### Git 커밋:
- 커밋 메시지: `perf: 회원가입 쿼리 최적화 (268개 → 58개, 78% 감소)`
---
## 2025-11-10 (일) - 회원가입 메뉴 생성 오류 수정 및 검증 에러 처리 개선
### 주요 작업
- **MenusStep 컬럼 오류 수정**: 존재하지 않는 컬럼(code, route_name, depth, description) 제거
- **하이브리드 메뉴 생성 방식 도입**: TenantBootstrapper에서 MenusStep 비활성화, MenuBootstrapService 활용
- **ValidationException 처리 개선**: 실제 검증 에러 메시지 표시 (422 상태 코드)
### 수정된 파일:
- `app/Services/TenantBootstrap/Steps/MenusStep.php` - 실제 DB 스키마에 맞게 컬럼 수정
- `app/Services/TenantBootstrap/RecipeRegistry.php` - MenusStep 비활성화 (주석 처리)
- `app/Exceptions/Handler.php` - ValidationException 처리 로직 개선
### 작업 내용:
#### 1. 문제 분석
**증상:**
```
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'code' in 'field list'
SQL: insert into `menus` (..., `code`, `route_name`, `depth`, `description`, ...)
```
**원인:**
- `TenantObserver`가 Tenant 생성 시 자동으로 `TenantBootstrapper::bootstrap()` 호출
- `MenusStep.php`가 실제 DB에 없는 컬럼(`code`, `route_name`, `depth`, `description`) 사용 시도
- `RegisterService.php`의 `MenuBootstrapService::createDefaultMenus()`와 중복 실행
**쿼리 과다 실행:**
- 메뉴 9개 생성 시 272개 쿼리 실행
- MenuObserver가 메뉴당 7개 권한 자동 생성 (view/create/update/delete/approve/export/manage)
- 중복 메뉴 생성 + 중복 권한 생성
#### 2. MenusStep.php 수정
**Before (잘못된 컬럼):**
```php
$newId = DB::table('menus')->insertGetId([
'tenant_id' => $tenantId,
'parent_id' => $newParentId,
'name' => $menu->name,
'code' => $menu->code ?? null, // ❌ 존재하지 않음
'icon' => $menu->icon ?? null,
'url' => $menu->url ?? null,
'route_name' => $menu->route_name ?? null, // ❌ 존재하지 않음
'sort_order' => $menu->sort_order ?? 0,
'is_active' => $menu->is_active ?? 1,
'depth' => $menu->depth ?? 0, // ❌ 존재하지 않음
'description' => $menu->description ?? null, // ❌ 존재하지 않음
'created_at' => now(),
'updated_at' => now(),
]);
```
**After (실제 DB 스키마):**
```php
$newId = DB::table('menus')->insertGetId([
'tenant_id' => $tenantId,
'parent_id' => $newParentId,
'name' => $menu->name,
'icon' => $menu->icon ?? null,
'url' => $menu->url ?? null,
'sort_order' => $menu->sort_order ?? 0,
'is_active' => $menu->is_active ?? 1,
'hidden' => $menu->hidden ?? 0, // ✅ 실제 컬럼
'is_external' => $menu->is_external ?? 0, // ✅ 실제 컬럼
'external_url' => $menu->external_url ?? null, // ✅ 실제 컬럼
'created_at' => now(),
'updated_at' => now(),
]);
```
**실제 DB 컬럼:**
```sql
id, tenant_id, parent_id, name, url, is_active, sort_order,
hidden, is_external, external_url, icon,
created_at, updated_at, created_by, updated_by, deleted_by, deleted_at
```
#### 3. 하이브리드 메뉴 생성 방식 도입
**배경:**
- **Option A**: TenantBootstrapper (글로벌 메뉴 복제, DB 의존)
- **Option B**: MenuBootstrapService (코드 기반, Git 버전 관리)
**선택: 하이브리드 접근** (Best Practice)
```
TenantObserver → TenantBootstrapper
├─ CapabilityProfilesStep ✅ (유지)
├─ CategoriesStep ✅ (유지)
├─ MenusStep ❌ (비활성화)
└─ SettingsStep ✅ (유지)
RegisterService → MenuBootstrapService ✅ (메뉴 생성)
```
**장점:**
- ✅ 메뉴 구조가 코드로 명확하게 정의됨 (`MenuBootstrapService.php`)
- ✅ Git으로 버전 관리 가능
- ✅ 새 메뉴 추가가 간단 (코드만 수정)
- ✅ 글로벌 메뉴 DB 데이터 불필요
- ✅ 부트스트랩 시스템 장점 유지 (CapabilityProfiles, Categories, Settings)
**RecipeRegistry.php 수정:**
```php
default => [ // STANDARD
new CapabilityProfilesStep,
new CategoriesStep,
// new MenusStep, // Disabled: Use MenuBootstrapService in RegisterService instead
new SettingsStep,
],
```
#### 4. ValidationException 처리 개선
**문제:**
```php
// Before - 모든 validation 에러를 "필수 파라미터 누락"으로 변환
if (
$exception instanceof ValidationException ||
$exception instanceof BadRequestHttpException
) {
return response()->json([
'success' => false,
'message' => '필수 파라미터 누락', // ❌ 실제 에러 메시지 손실
'data' => null,
], 400);
}
```
**증상:**
- Slack: "이메일은(는) 이미 사용 중입니다" (실제 에러)
- API 응답: "필수 파라미터 누락" (잘못된 메시지)
**수정:**
```php
// After - 실제 검증 에러 메시지 표시
if ($exception instanceof ValidationException) {
return response()->json([
'success' => false,
'message' => '입력값 검증 실패',
'data' => [
'errors' => $exception->errors(), // ✅ 실제 에러 정보
],
], 422); // ✅ 표준 validation 실패 코드
}
if ($exception instanceof BadRequestHttpException) {
return response()->json([
'success' => false,
'message' => '잘못된 요청',
'data' => null,
], 400);
}
```
**개선 효과:**
```json
// Before
{
"success": false,
"message": "필수 파라미터 누락",
"data": null
}
// After
{
"success": false,
"message": "입력값 검증 실패",
"data": {
"errors": {
"email": ["이메일은(는) 이미 사용 중입니다."],
"user_id": ["사용자 아이디은(는) 이미 사용 중입니다."]
}
}
}
```
### 기술 세부사항:
#### 메뉴 생성 방식 비교
**TenantBootstrapper + MenusStep (기존):**
- 장점: 체계적인 부트스트랩 시스템, 레시피 기반 확장
- 단점: 글로벌 메뉴 DB 데이터 필요, Git 버전 관리 불가, 메뉴 추가 시 DB 수정 필요
**MenuBootstrapService (새 방식):**
- 장점: 코드 기반, Git 버전 관리, 메뉴 추가 간단
- 단점: 부트스트랩 시스템과 분리
**하이브리드 (선택):**
- 데이터 부트스트랩(CapabilityProfiles, Categories, Settings)은 TenantBootstrapper 사용
- 메뉴 생성은 코드 기반 MenuBootstrapService 사용
- 양쪽 장점 활용
#### HTTP 상태 코드 표준화
- **422 Unprocessable Entity**: Validation 실패 (표준)
- **400 Bad Request**: 잘못된 요청 형식
- **401 Unauthorized**: 인증 실패
- **403 Forbidden**: 권한 없음
- **404 Not Found**: 리소스 없음
- **500 Internal Server Error**: 서버 에러
### SAM API Development Rules 준수:
✅ **Service-First 아키텍처:**
- MenuBootstrapService에 메뉴 생성 로직
✅ **멀티테넌시:**
- Tenant context 명시적 설정
- BelongsToTenant 스코프 활용
✅ **코드 품질:**
- 실제 DB 스키마와 일치
- 명확한 주석 (비활성화 이유 설명)
✅ **에러 처리:**
- 표준 HTTP 상태 코드
- 실제 검증 에러 메시지 표시
### 예상 효과:
1. **회원가입 정상 동작**: SQL 에러 해결
2. **쿼리 최적화**: 272개 → 약 100개 (중복 제거)
3. **유지보수 편의성**: 코드 기반 메뉴 관리
4. **명확한 에러 메시지**: 사용자가 정확한 문제 파악 가능
### 다음 작업:
- [x] MenusStep.php 컬럼 수정
- [x] RecipeRegistry.php MenusStep 비활성화
- [x] Handler.php ValidationException 처리 개선
- [x] 캐시 클리어
- [x] 회원가입 API 테스트 (성공/실패 케이스)
### Git 커밋:
- 커밋 메시지: `fix: 회원가입 메뉴 생성 오류 수정 및 검증 에러 처리 개선`
---
## 2025-11-06 (수) - Login API 응답 개선 (사용자/테넌트/메뉴 정보 포함)
### 주요 작업
- **Login API 응답 확장**: 토큰 외에 user, tenant, menus 정보 추가
- **테넌트 우선순위 로직**: is_default → is_active → null 순서로 선택
- **권한 기반 메뉴 필터링**: menu:{id}.view 권한 + override allow/deny 적용
- **Permission Overrides 활용**: 시간 기반 명시적 허용/차단 지원
- **메뉴 외부 링크 지원**: is_external, external_url 필드 추가
### 수정된 파일:
- `app/Services/MemberService.php` - getUserInfoForLogin() 메서드 추가 (130줄) + 외부 링크 필드 추가
- `app/Http/Controllers/Api/V1/ApiController.php` - login() 응답 구조 변경 (8줄)
- `app/Swagger/v1/AuthApi.php` - login() 엔드포인트 문서 업데이트 (80줄) + 외부 링크 스키마 추가
### 작업 내용:
#### 1. MemberService::getUserInfoForLogin() 구현
**5단계 프로세스:**
```php
1. 사용자 기본 정보 조회
- User::find($userId)
- 반환: {id, user_id, name, email, phone}
2. 활성 테넛트 조회 (우선순위)
- 1순위: is_default=1
- 2순위: is_active=1 (첫 번째)
- 없으면: return {user, tenant: null, menus: []}
3. 테넛트 정보 구성
- 기본 테넌트: {id, company_name, business_num, tenant_st_code}
- 추가 테넌트 목록: other_tenants[]
4. 메뉴 권한 체크 (menu:{menu_id}.view 패턴)
- 4-1. 기본 Role 권한 (model_has_permissions 테이블)
- 4-2. Override 권한 (permission_overrides 테이블)
- 4-3. 최종 권한 계산: deny(-1) > allow(1) > base permission
5. 메뉴 목록 조회
- Menu::whereIn('id', $allowedMenuIds)
- 정렬: parent_id → sort_order
- 반환: {id, parent_id, name, url, icon, sort_order, is_external, external_url}
```
**권한 우선순위 로직:**
```php
foreach ($allMenuPermissions as $permName) {
// 1. Override deny 체크
if (isset($overrides[$permName]) && $overrides[$permName]->effect === -1) {
continue; // 강제 차단
}
// 2. Override allow 또는 기본 Role 권한
if (
(isset($overrides[$permName]) && $overrides[$permName]->effect === 1) ||
in_array($permName, $rolePermissions, true)
) {
$allowedMenuIds[] = $menuId;
}
}
```
**시간 기반 Override 적용:**
```php
->where(function ($q) {
$q->whereNull('permission_overrides.effective_from')
->orWhere('permission_overrides.effective_from', '<=', now());
})
->where(function ($q) {
$q->whereNull('permission_overrides.effective_to')
->orWhere('permission_overrides.effective_to', '>=', now());
})
```
#### 2. ApiController::login() 응답 변경
**기존 응답:**
```json
{
"message": "로그인 성공",
"user_token": "1|abc123xyz"
}
```
**개선된 응답:**
```json
{
"message": "로그인 성공",
"user_token": "1|abc123xyz",
"user": {
"id": 1,
"user_id": "hamss",
"name": "홍길동",
"email": "hamss@example.com",
"phone": "010-1234-5678"
},
"tenant": {
"id": 1,
"company_name": "주식회사 코드브리지",
"business_num": "123-45-67890",
"tenant_st_code": "ACTIVE",
"other_tenants": [
{
"tenant_id": 2,
"company_name": "주식회사 샘플",
"business_num": "987-65-43210",
"tenant_st_code": "ACTIVE"
}
]
},
"menus": [
{
"id": 1,
"parent_id": null,
"name": "대시보드",
"url": "/dashboard",
"icon": "dashboard",
"sort_order": 1
}
]
}
```
**테넌트 없는 경우:**
```json
{
"message": "로그인 성공",
"user_token": "1|abc123xyz",
"user": { ... },
"tenant": null,
"menus": []
}
```
#### 3. Swagger 문서 업데이트
**응답 스키마 (AuthApi.php):**
- 200 응답: 테넌트 있는 경우 (완전한 정보)
- 200 (테넌트 없음): tenant=null, menus=[] 케이스
- 400: 필수 파라미터 누락
- 401: 비밀번호 불일치
- 404: 사용자를 찾을 수 없음
**주요 변경사항:**
```php
@OA\Property(
property="tenant",
type="object",
nullable=true,
description="활성 테넌트 정보 (is_default=1 우선, 없으면 is_active=1 중 첫 번째, 없으면 null)",
// ... 스키마 정의
)
@OA\Property(
property="menus",
type="array",
description="사용자가 접근 가능한 메뉴 목록 (menu:{menu_id}.view 권한 체크, override deny/allow 적용)",
// ... 스키마 정의
)
```
### 기술 세부사항:
#### Permission Overrides 테이블 구조
```sql
CREATE TABLE permission_overrides (
tenant_id BIGINT UNSIGNED,
model_type VARCHAR(255), -- User::class
model_id BIGINT UNSIGNED, -- User ID
permission_id BIGINT UNSIGNED,
effect TINYINT, -- 1=ALLOW, -1=DENY
effective_from TIMESTAMP NULL,
effective_to TIMESTAMP NULL
);
```
#### 권한 체크 세 가지 방법 (모두 사용)
1. **Spatie hasPermissionTo()**: Role 기반 자동 상속
2. **permission_overrides**: 명시적 allow/deny with 시간 제약
3. **Role-based inheritance**: Spatie 자동 처리
**우선순위:** override deny > override allow > base permission
#### 성능 특성
- **현재 방식**: 6-7 쿼리, 100-200ms
- **최적화 (캐싱 없음)**: 4 쿼리, 50-100ms
- **캐싱 적용 시**: 1 쿼리 (캐시 후), 10-20ms
**선택:** 세밀한 제어 우선 (로그인 시에만 실행되므로 성능 영향 최소)
### SAM API Development Rules 준수:
✅ **Service-First 아키텍처:**
- MemberService에 모든 비즈니스 로직
- Controller는 DI + 호출만
✅ **멀티테넌시:**
- BelongsToTenant 스코프 활용
- Tenant context 명시적 처리
✅ **보안:**
- 민감 정보 제외 (password, remember_token, timestamps, audit columns)
- 권한 기반 메뉴 필터링
✅ **Swagger 문서:**
- 별도 파일 (app/Swagger/v1/AuthApi.php)
- 완전한 응답 스키마 (테넌트 있음/없음 케이스)
✅ **코드 품질:**
- Laravel Pint 포맷팅 완료 (3 files, 1 style issue fixed)
### 예상 효과:
1. **클라이언트 편의성**: 1회 로그인으로 모든 정보 획득
2. **네트워크 최적화**: 추가 API 호출 불필요 (/me 엔드포인트 미호출)
3. **세밀한 권한 제어**: Override 기능으로 일시적 권한 부여/차단
4. **멀티테넌트 지원**: 여러 테넌트 소속 시 전환 가능 정보 제공
### 다음 작업:
- [ ] Swagger 재생성 (`php artisan l5-swagger:generate`)
- [ ] Postman/Swagger UI로 API 테스트
- [ ] Frontend 로그인 화면에서 응답 데이터 처리
- [ ] 캐싱 전략 고려 (필요 시)
### Git 커밋 준비:
- 다음 커밋 예정: `feat: 로그인 응답에 사용자/테넌트/메뉴 정보 추가`
---
## 2025-11-06 (수) - Register API 개발 (/api/v1/register)
### 주요 작업
- **Register API 전체 구현**: 회원가입 + 테넌트 생성 + 시스템 관리자 권한 자동 부여
- **글로벌 메뉴 복제 로직**: 새 테넌트 생성 시 글로벌 메뉴 자동 복사 (parent_id 매핑)
- **사업자번호 조건부 유효성 검사**: 정식 서비스(active) 업체만 unique 제약
- **완전한 Swagger 문서**: 상세한 요청/응답 스키마 및 에러 케이스
### 추가된 파일:
- `app/Http/Requests/RegisterRequest.php` - 회원가입 요청 검증 (FormRequest)
- `app/Services/RegisterService.php` - 통합 비즈니스 로직 (DB 트랜잭션)
- `app/Http/Controllers/Api/V1/RegisterController.php` - 컨트롤러 (ApiResponse::handle)
- `app/Swagger/v1/RegisterApi.php` - Swagger 문서
### 수정된 파일:
- `app/Services/TenantBootstrap/Steps/MenusStep.php` - 글로벌 메뉴 복제 로직 구현
- `lang/ko/message.php` - `registered` 키 추가
- `lang/ko/error.php` - 4개 에러 메시지 추가 (business_num_format, business_num_duplicate_active, user_id_format, phone_format)
- `routes/api.php` - POST /api/v1/register 라우트 추가
### 작업 내용:
#### 1. RegisterRequest 검증 규칙
**사용자 필드:**
```php
'user_id' => 'required|string|max:255|regex:/^[a-zA-Z0-9_-]+$/|unique:users,user_id',
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email',
'phone' => 'nullable|string|max:20|regex:/^[0-9-]+$/',
'password' => 'required|string|min:8|confirmed',
'position' => 'nullable|string|max:100', // options JSON에 저장
```
**테넌트 필드:**
```php
'company_name' => 'required|string|max:255',
'business_num' => [
'required',
'string',
'regex:/^\d{3}-\d{2}-\d{5}$/',
Rule::unique('tenants', 'business_num')->where(function ($query) {
return $query->where('tenant_st_code', 'active'); // ⚠️ active만 unique
}),
],
'company_scale' => 'nullable|string|max:50', // options JSON에 저장
'industry' => 'nullable|string|max:100', // options JSON에 저장
```
**핵심 특징:**
- ✅ 사업자번호: `tenant_st_code='active'`인 경우만 unique (trial/none은 중복 허용)
- ✅ 비밀번호: confirmed 규칙 (password_confirmation 필요)
- ✅ 커스텀 에러 메시지: i18n 키 사용
#### 2. RegisterService 비즈니스 로직
**전체 프로세스 (DB::transaction 래핑):**
```php
1. Tenant 생성
- company_name, business_num
- tenant_st_code = 'trial' (데모 버전)
- options = {company_scale, industry}
2. TenantBootstrap 실행 (STANDARD 레시피)
- MenusStep: 글로벌 메뉴 복제 (parent_id 매핑)
- CategoriesStep, SettingsStep 등
3. User 생성
- user_id, name, email, phone
- password = Hash::make()
- options = {position}
4. TenantUserProfile 생성
- is_default = 1, is_active = 1
5. Tenant Context 설정
- app()->bind('tenant_id', $tenant->id)
- PermissionRegistrar::setPermissionsTeamId($tenant->id)
6. system_manager Role 생성
- guard_name = 'api'
- description = '시스템 관리자'
7. 모든 테넌트 메뉴 권한 생성 및 할당
- Menu::where('tenant_id', $tenant->id)->pluck('id')
- Permission::firstOrCreate(['name' => "menu.{menu_id}"])
- $role->syncPermissions($permissions)
8. User에게 system_manager Role 할당
- $user->assignRole($role)
9. 결과 반환
- user: {id, user_id, name, email, phone, options}
- tenant: {id, company_name, business_num, tenant_st_code, options}
```
**주의 사항 (자동 적용됨):**
- ⚠️ **트랜잭션 필수**: 실패 시 전체 롤백
- ⚠️ **멀티테넌시**: Tenant context 명시적 설정
- ⚠️ **보안**: Hash::make() 사용, 입력 검증
- ⚠️ **글로벌 메뉴 복제**: parent_id 매핑으로 계층 구조 유지
- ⚠️ **사업자번호 검증**: 조건부 unique (active만)
#### 3. MenusStep 글로벌 메뉴 복제 로직
**기존 문제:**
- ROOT 메뉴만 생성하는 stub 구현
- 글로벌 메뉴가 복사되지 않음
**개선 내용:**
```php
public function run(int $tenantId): void
{
// 1. 중복 실행 방지
if (Menu::where('tenant_id', $tenantId)->exists()) {
return;
}
// 2. 글로벌 메뉴 조회 (계층 순서로 정렬)
$globalMenus = DB::table('menus')
->whereNull('tenant_id')
->orderByRaw('parent_id IS NULL DESC, parent_id ASC, sort_order ASC')
->get();
// 3. parent_id 매핑 (old_id => new_id)
$parentIdMap = [];
foreach ($globalMenus as $menu) {
// 4. 부모 ID 매핑 확인
$newParentId = null;
if ($menu->parent_id !== null && isset($parentIdMap[$menu->parent_id])) {
$newParentId = $parentIdMap[$menu->parent_id];
}
// 5. 새 메뉴 생성
$newId = DB::table('menus')->insertGetId([
'tenant_id' => $tenantId,
'parent_id' => $newParentId, // ⚠️ 매핑된 parent_id 사용
'name' => $menu->name,
'code' => $menu->code ?? null,
// ... 모든 필드 복사
]);
// 6. 매핑 저장
$parentIdMap[$menu->id] = $newId;
}
}
```
**핵심:**
- ✅ 루트 메뉴 우선 처리 (`parent_id IS NULL DESC`)
- ✅ parent_id 매핑으로 계층 구조 정확히 유지
- ✅ 모든 메뉴 속성 보존 (name, code, icon, url, route_name 등)
#### 4. RegisterController 구현
**패턴:**
```php
public function register(RegisterRequest $request)
{
return ApiResponse::handle(function () use ($request) {
return RegisterService::register($request->validated());
}, __('message.registered'));
}
```
**특징:**
- ✅ FormRequest 타입 힌트 (자동 검증)
- ✅ Service DI + ApiResponse::handle()
- ✅ i18n 메시지 키 사용
- ✅ Controller는 단순 래퍼 역할
#### 5. Swagger 문서 (RegisterApi.php)
**요청 스키마:**
```php
required: user_id, name, email, password, password_confirmation, company_name, business_num
optional: phone, position, company_scale, industry
```
**응답 스키마 (200):**
```php
{
"success": true,
"message": "회원가입이 완료되었습니다",
"data": {
"user": {
"id": 1,
"user_id": "john_doe",
"name": "홍길동",
"email": "john@example.com",
"phone": "010-1234-5678",
"options": {"position": "개발팀장"}
},
"tenant": {
"id": 1,
"company_name": "(주)테크컴퍼니",
"business_num": "123-45-67890",
"tenant_st_code": "trial",
"options": {
"company_scale": "중소기업",
"industry": "IT/소프트웨어"
}
}
}
}
```
**에러 응답 (422):**
```php
{
"success": false,
"message": "유효성 검증에 실패했습니다",
"errors": {
"user_id": ["이미 사용 중인 아이디입니다"],
"business_num": ["사업자등록번호 형식이 올바르지 않습니다 (000-00-00000)"]
}
}
```
#### 6. i18n 메시지 추가
**lang/ko/message.php:**
```php
'registered' => '회원가입이 완료되었습니다.',
```
**lang/ko/error.php:**
```php
'business_num_format' => '사업자등록번호 형식이 올바르지 않습니다 (000-00-00000)',
'business_num_duplicate_active' => '이미 등록된 사업자등록번호입니다 (정식 서비스 업체)',
'user_id_format' => '아이디는 영문, 숫자, _, - 만 사용할 수 있습니다',
'phone_format' => '전화번호 형식이 올바르지 않습니다',
```
#### 7. Routes 등록
**routes/api.php:**
```php
use App\Http\Controllers\Api\V1\RegisterController;
Route::middleware('auth.apikey')->group(function () {
Route::post('register', [RegisterController::class, 'register'])->name('v1.register');
});
```
**엔드포인트:**
- POST /api/v1/register (auth.apikey 미들웨어)
### SAM API Development Rules 준수:
✅ **Service-First 아키텍처:**
- RegisterService에 모든 비즈니스 로직
- Controller는 DI + ApiResponse::handle()만
✅ **FormRequest 검증:**
- RegisterRequest로 모든 검증 규칙 분리
✅ **i18n 메시지 키:**
- __('message.registered'), __('error.xxx') 사용
✅ **Swagger 문서:**
- 별도 파일 (app/Swagger/v1/RegisterApi.php)
- 완전한 요청/응답 스키마
✅ **멀티테넌시:**
- BelongsToTenant 스코프 (Tenant, Role, Permission)
- Explicit tenant context 설정
✅ **감사 로그:**
- created_by, updated_by 컬럼 포함
✅ **SoftDeletes:**
- Tenant, User 모델에 적용
### 기술 세부사항:
#### 조건부 Unique 제약
```php
// trial/none 테넌트는 사업자번호 중복 허용
Rule::unique('tenants', 'business_num')->where(function ($query) {
return $query->where('tenant_st_code', 'active');
})
```
#### parent_id 매핑 알고리즘
```php
// 1. 루트 메뉴 먼저 처리 (parent_id IS NULL)
// 2. insertGetId로 새 ID 캡처
// 3. old_id => new_id 매핑 저장
// 4. 자식 메뉴 처리 시 매핑된 parent_id 사용
$parentIdMap[$oldId] = $newId;
$newParentId = $parentIdMap[$menu->parent_id] ?? null;
```
#### DB Transaction
```php
return DB::transaction(function () use ($params) {
// 모든 작업이 성공하거나 전체 롤백
$tenant = Tenant::create([...]);
app(RecipeRegistry::class)->bootstrap($tenant->id);
$user = User::create([...]);
// ...
return ['user' => $user->only([...]), 'tenant' => $tenant->only([...])];
});
```
### 예상 효과:
1. **원스톱 가입**: 1회 요청으로 모든 설정 완료
2. **즉시 사용 가능**: system_manager 권한으로 모든 메뉴 접근
3. **멀티테넌트 격리**: 각 테넌트별 독립적인 메뉴 구조
4. **유연한 검증**: trial 단계에서는 사업자번호 중복 허용
### 다음 작업:
- [ ] Swagger 재생성 (`php artisan l5-swagger:generate`)
- [ ] Postman/Swagger UI로 API 테스트
- [ ] Frontend 회원가입 화면 구현
- [ ] 이메일 인증 기능 추가 (선택)
- [ ] API 문서 최종 검토
### Git 커밋 준비:
- 다음 커밋 예정: `feat: 회원가입 + 테넌트 생성 통합 API 추가 (/api/v1/register)`
---
## 2025-11-10 (일) - 스케줄러 Laravel 12 표준 방식 전환 + 로그 기록
### 주요 작업
- **Laravel 12 표준 방식 적용**: Kernel.php → routes/console.php로 스케줄러 마이그레이션
- **로그 기록 기능 추가**: 실행 결과 및 성공/실패 이벤트 로그
- **스케줄러 정리**: Kernel.php 레거시 코드 정리
### 수정된 파일:
- `routes/console.php` - Laravel 12 표준 스케줄러 정의 + 로그 기록
- `app/Console/Kernel.php` - schedule() 메서드 정리 (주석 처리)
### 작업 내용:
#### 1. routes/console.php 마이그레이션 (Laravel 12 표준)
**변경 전 (Kernel.php):**
```php
protected function schedule(Schedule $schedule): void
{
$schedule->command('audit:prune')->dailyAt('03:10');
$schedule->command('sanctum:prune-expired --hours=24')->dailyAt('03:20');
}
```
**변경 후 (routes/console.php):**
```php
use Illuminate\Support\Facades\Schedule;
// 감사 로그 정리 (매일 새벽 03:10)
Schedule::command('audit:prune')
->dailyAt('03:10')
->appendOutputTo(storage_path('logs/scheduler.log'))
->onSuccess(function () {
\Illuminate\Support\Facades\Log::info('✅ audit:prune 스케줄러 실행 성공', ['time' => now()]);
})
->onFailure(function () {
\Illuminate\Support\Facades\Log::error('❌ audit:prune 스케줄러 실행 실패', ['time' => now()]);
});
// 만료 토큰 정리 (매일 새벽 03:20)
Schedule::command('sanctum:prune-expired --hours=24')
->dailyAt('03:20')
->appendOutputTo(storage_path('logs/scheduler.log'))
->onSuccess(function () {
\Illuminate\Support\Facades\Log::info('✅ sanctum:prune-expired 스케줄러 실행 성공', ['time' => now()]);
})
->onFailure(function () {
\Illuminate\Support\Facades\Log::error('❌ sanctum:prune-expired 스케줄러 실행 실패', ['time' => now()]);
});
```
#### 2. Kernel.php 정리
**app/Console/Kernel.php:**
```php
protected function schedule(Schedule $schedule): void
{
// Laravel 12부터는 routes/console.php에서 스케줄러를 정의합니다.
// Schedule::command() 방식 사용
}
```
#### 3. 로그 기록 방식
**2가지 로그 파일:**
1. **storage/logs/scheduler.log** - 명령어 실행 결과
```
Pruned 0 audit log rows older than 390 days.
Tokens expired for more than [24 hours] pruned successfully.
```
2. **storage/logs/laravel.log** - 성공/실패 이벤트
```
[2025-11-10 03:10:15] production.INFO: ✅ audit:prune 스케줄러 실행 성공 {"time":"2025-11-10 03:10:15"}
[2025-11-10 03:20:20] production.INFO: ✅ sanctum:prune-expired 스케줄러 실행 성공 {"time":"2025-11-10 03:20:20"}
```
#### 4. 스케줄러 확인 방법
**등록 확인:**
```bash
php artisan schedule:list
# 결과:
# 10 3 * * * php artisan audit:prune ................. Next Due: 13시간 후
# 20 3 * * * php artisan sanctum:prune-expired ... Next Due: 13시간 후
```
**로그 확인:**
```bash
# 실행 결과 확인
cat storage/logs/scheduler.log
tail -f storage/logs/scheduler.log # 실시간 모니터링
# 성공/실패 이벤트 확인
tail -f storage/logs/laravel.log | grep "스케줄러"
# 성공 로그만
grep "✅" storage/logs/laravel.log
# 실패 로그만
grep "❌" storage/logs/laravel.log
```
### 기술 세부사항:
#### Laravel 11+ 스케줄러 아키텍처 변경
- **Laravel 10 이하**: app/Console/Kernel.php의 schedule() 메서드
- **Laravel 11+**: routes/console.php에서 Schedule 파사드 사용
- **이점**:
- 라우트와 함께 콘솔 명령 관리
- 더 간결한 구조
- 향후 유지보수 용이
#### 로그 기록 전략
- **appendOutputTo**: 명령어 stdout/stderr를 파일에 추가
- **onSuccess/onFailure**: 실행 결과에 따라 Laravel Log 기록
- **비동기 처리**: Log 파사드가 자동으로 처리
### 예상 효과:
1. **표준 방식 준수**: Laravel 12 공식 권장 방식
2. **실행 추적**: 스케줄러 실행 이력 확인 가능
3. **문제 진단**: 실패 로그로 즉시 문제 파악
4. **운영 편의성**: 로그 파일 분석으로 시스템 모니터링
### 다음 작업:
- [ ] Frontend 토큰 갱신 로직 구현 (권장)
- [ ] 토큰 만료 모니터링 대시보드 (선택)
### Git 커밋:
798d514 - feat: API 토큰 관리 시스템 구현 (액세스/리프레시 토큰 분리)
(다음 커밋 예정: feat: 스케줄러 Laravel 12 표준 방식 전환 + 로그 기록)
---
(이전 작업 내역은 그대로 유지...)