diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 3998966..f46b314 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,3633 +1,112 @@ -## 2025-11-20 (수) - ItemMaster API 테스트 및 버그 수정 +# SAM API 작업 현황 -### 주요 작업 -1. **ItemMaster API 통합 테스트 작성** (12개 테스트, 82개 assertion) - - 로그인 → API 호출 실제 플로우 시뮬레이션 - - CustomTab CRUD + Reorder 테스트 (6개) - - UnitOption CRUD 테스트 (3개) - - Init 엔드포인트 테스트 - - 인증 검증 테스트 (2개) +## 2025-11-24 (일) - 소프트삭제 및 타임스탬프 감사 컬럼 추가 -2. **누락된 마이그레이션 실행** - - `2025_11_20_100001_create_section_templates_table.php` - SectionTemplate 모델 지원 - - `2025_11_20_100008_create_tab_columns_table.php` - TabColumn 관계 지원 +### 작업 목표 +- deleted_at이 있는 테이블에 deleted_by 컬럼 추가 +- created_at, updated_at이 있는 테이블에 created_by, updated_by 컬럼 추가 -3. **API Key 미들웨어 수정** (`app/Http/Middleware/ApiKeyMiddleware.php`) - - `api/v1/login` 엔드포인트를 `$publicRoutes`에서 제거 - - 로그인 엔드포인트에도 API Key 필수화 (사용자 요구사항 반영) +### 작업 내용 -4. **ReorderRequest validation 수정** (`app/Http/Requests/ItemMaster/ReorderRequest.php`) - - `exists:item_sections,id` → 제거 (범용성 확보) - - CustomTab, ItemSection 등 여러 모델에서 재사용 가능하도록 변경 +**1. DB 스키마 분석 (INFORMATION_SCHEMA 쿼리)** +- deleted_at은 있지만 deleted_by가 없는 테이블: 30개 +- created_at은 있지만 created_by, updated_by가 없는 테이블: 45개 -5. **네임스페이스 오류 수정** (5개 Controller) - - `use App\Http\Responses\ApiResponse;` → `use App\Helpers\ApiResponse;` - - 영향받은 파일: CustomTabController, UnitOptionController, ItemMasterFieldController, SectionTemplateController, ItemBomItemController +**2. 마이그레이션 생성** +- `2025_11_24_192518_add_deleted_by_to_soft_delete_tables.php` + - 30개 테이블에 deleted_by 추가 + - nullable, COMMENT('삭제자 사용자 ID') + - after('deleted_at') 배치 -6. **Route 순서 수정** (`routes/api.php`) - - `/custom-tabs/reorder` 라우트를 `/custom-tabs/{id}` 앞으로 이동 - - Specific route가 parameterized route보다 먼저 매칭되도록 수정 +- `2025_11_24_192518_add_audit_columns_to_tables.php` + - 38개 비즈니스 테이블에 created_by, updated_by 추가 + - 시스템 테이블 제외 (jobs, job_batches, password_reset_tokens, personal_access_tokens, taggables, tags) + - nullable, COMMENT + - after('updated_at'), after('created_by') 배치 + +**3. 마이그레이션 실행 및 검증** +- 실행 시간: deleted_by (429.53ms), audit_columns (1초) +- 샘플 테이블 검증: users, products, models, bom_templates, department_user 모두 정상 ### 추가된 파일 -- **tests/Feature/ItemMaster/ItemMasterApiTest.php** (~370 lines) - - DatabaseTransactions trait 사용 (테스트 격리) - - setUp()에서 Tenant, User, UserTenant, API Key 자동 생성 - - authenticatedRequest() 헬퍼 메서드로 인증 요청 간소화 - -### 수정된 파일 -- app/Http/Middleware/ApiKeyMiddleware.php - API Key 정책 강화 -- app/Http/Requests/ItemMaster/ReorderRequest.php - Validation 규칙 수정 -- app/Http/Controllers/Api/V1/ItemMaster/*.php (5개) - Namespace 수정 -- routes/api.php - Route 순서 수정 - -### 테스트 결과 -✅ **12/12 테스트 통과** (100% 성공률) -- 로그인 및 토큰 획득 테스트 -- CustomTab CRUD 작업 테스트 -- CustomTab Reorder 테스트 -- UnitOption CRUD 작업 테스트 -- Init 데이터 로드 테스트 -- 인증 실패 시나리오 테스트 +- `database/migrations/2025_11_24_192518_add_deleted_by_to_soft_delete_tables.php` +- `database/migrations/2025_11_24_192518_add_audit_columns_to_tables.php` ### 마이그레이션 상태 -``` -2025_11_20_100000_create_unit_options_table ................... [실행됨] -2025_11_20_100001_create_section_templates_table ............. [실행됨] -2025_11_20_100002_create_item_pages_table .................... [실행됨] -2025_11_20_100003_create_item_sections_table ................. [실행됨] -2025_11_20_100004_create_item_fields_table ................... [실행됨] -2025_11_20_100005_create_custom_tabs_table ................... [실행됨] -2025_11_20_100006_create_item_master_fields_table ............ [실행됨] -2025_11_20_100007_create_item_bom_items_table ................ [실행됨] -2025_11_20_100008_create_tab_columns_table ................... [실행됨] -``` - -### 작업 과정에서 해결한 이슈 -1. **Table 'tab_columns' doesn't exist** - 마이그레이션 누락 -2. **Table 'section_templates' doesn't exist** - 마이그레이션 누락 -3. **ApiResponse namespace 오류** - 5개 Controller에서 잘못된 import -4. **Route matching 오류** - 'reorder'가 ID로 인식되는 문제 -5. **Validation 422 에러** - ReorderRequest가 잘못된 테이블 참조 -6. **API Key 정책 불일치** - 로그인에도 API Key 필수화 요구사항 반영 +- Batch 25로 실행 완료 +- 롤백 가능 (down 메서드 구현) --- -## 2025-11-20 (수) - ItemMaster API Swagger 문서 작성 +## 2025-11-24 (일) - CORS Preflight 문제 해결 + +### 문제 상황 +- React 프론트엔드(http://192.0.0.2:3001)에서 API 호출 시 CORS 에러 +- 에러: `Request header field x-api-key is not allowed by Access-Control-Allow-Headers in preflight response` +- 서버 로그: OPTIONS 요청만 있고 Response 로그 없음 (401 차단) + +### 근본 원인 (root-cause-analyst 스킬 활용) +1. CorsMiddleware에서 `Access-Control-Allow-Headers`에 `X-API-KEY` 누락 +2. OPTIONS 요청(Preflight)이 ApiKeyMiddleware에서 401로 차단 +3. 브라우저는 커스텀 헤더 사용 시 Preflight 요청을 자동 전송 + +### 해결 방안 + +**CorsMiddleware 수정:** +- OPTIONS 요청을 미들웨어 체인 진입 전에 즉시 200 OK 처리 +- `Access-Control-Allow-Headers`에 `X-API-KEY` 추가 +- PATCH 메서드 추가, Max-Age 86400초 설정 + +**ApiKeyMiddleware 정리:** +- 불필요한 OPTIONS 체크 제거 + +**CORS 설정 업데이트:** +- `config/cors.php`: exposed_headers, max_age 설정 + +### 수정된 파일 +- `app/Http/Middleware/CorsMiddleware.php` +- `app/Http/Middleware/ApiKeyMiddleware.php` +- `config/cors.php` + +### Git 커밋 +- `2e96660` - CORS preflight 요청 처리 개선 및 X-API-KEY 헤더 허용 +- `8e8ab65` - CORS preflight 응답에 x-api-key 헤더 허용 추가 + +### 다음 작업 +- React에서 API 호출 테스트 +- 개발 서버 로그 확인 (Request/Response 쌍 기록 여부) + +--- + +## 2025-11-20 (수) - ItemMaster API 테스트 및 버그 수정 + +### 주요 작업 +- ItemMaster API 통합 테스트 작성 (12개 테스트, 82개 assertion) +- 누락된 마이그레이션 실행 (section_templates, tab_columns) +- API Key 미들웨어 수정 (로그인 엔드포인트 API Key 필수화) +- ReorderRequest validation 수정 (범용성 확보) +- 네임스페이스 오류 수정 (5개 Controller) +- Route 순서 수정 (specific route 우선) + +### 테스트 결과 +✅ 12/12 테스트 통과 (100%) + +--- + +## 2025-11-19 (화) - 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 커밋 -- 30f308f - docs: ItemMaster API Swagger 문서 추가 +- Model Schemas 8개, Request Schemas 12개 작성 --- -## 2025-11-20 (수) - ItemMaster Phase 3 API 구현 (부가 기능) +## 2025-11-18 (월) - Category API 테스트 및 개선 ### 주요 작업 -- Phase 3 부가 기능 8개 API 엔드포인트 구현 -- 커스텀 탭, 단위 옵션 관리 기능 추가 -- SAM API Development Rules 준수 (Service-First, ApiResponse, i18n) +- Category CRUD 테스트 작성 (9개 테스트, 98개 assertion) +- 계층 구조 및 필드 관리 테스트 +- Validation 로직 개선 -### 추가된 파일 - -#### 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 표준 방식 전환 + 로그 기록) - ---- - -(이전 작업 내역은 그대로 유지...) -## 2025-11-20 (수요일) - ItemMaster API 테스트 완료 - -### 주요 작업: -- ItemMaster 관련 모든 API (32개) 테스트 완료 -- 총 35개 테스트 메서드 작성 및 검증 - -### 추가된 테스트: - -**tests/Feature/ItemMaster/ItemMasterApiTest.php (확장):** -- ItemPage CRUD 테스트 (4개) - - test_can_list_item_pages - - test_can_create_item_page - - test_can_update_item_page - - test_can_delete_item_page - -- ItemSection CRUD + Reorder 테스트 (4개) - - test_can_create_item_section - - test_can_update_item_section - - test_can_delete_item_section - - test_can_reorder_item_sections - -- ItemField CRUD + Reorder 테스트 (4개) - - test_can_create_item_field - - test_can_update_item_field - - test_can_delete_item_field - - test_can_reorder_item_fields - -- ItemBomItem CRUD 테스트 (3개) - - test_can_create_item_bom_item - - test_can_update_item_bom_item - - test_can_delete_item_bom_item - -- SectionTemplate CRUD 테스트 (4개) - - test_can_list_section_templates - - test_can_create_section_template - - test_can_update_section_template - - test_can_delete_section_template - -- ItemMasterField CRUD 테스트 (4개) - - test_can_list_item_master_fields - - test_can_create_item_master_field - - test_can_update_item_master_field - - test_can_delete_item_master_field - -### 주요 수정 사항: - -1. **필드명 수정**: - - `item_section_id` → `section_id` (ItemField, ItemBomItem 테이블) - - 마이그레이션에 맞춰 실제 컬럼명 사용 - -2. **Enum 값 수정**: - - `item_type`: 'PRODUCT' → 'FG', 'MATERIAL' → 'RM' - - 실제 API validation 규칙에 맞춰 조정 - -3. **모델 Import 추가**: - - ItemPage, ItemSection, ItemField, ItemBomItem - - SectionTemplate, ItemMasterField, TabColumn - -### 테스트 결과: - -``` -✅ Tests: 35 -✅ Assertions: 207 -✅ Result: All tests passed -``` - -### 테스트 커버리지: - -**ItemMaster 모듈 API (32개):** -- ItemPage API: 5개 (List, Create, Update, Delete, Reorder) -- ItemSection API: 5개 (List, Create, Update, Delete, Reorder) -- ItemField API: 5개 (List, Create, Update, Delete, Reorder) -- ItemBomItem API: 4개 (List, Create, Update, Delete) -- SectionTemplate API: 9개 (List, Create, Update, Delete, Reorder, Clone, Apply to Page/All, Remove from Page) -- ItemMasterField API: 4개 (List, Create, Update, Delete) - -**테스트된 API: 32개 / 32개 (100%)** - -### 검증된 기능: - -1. **Multi-tenancy**: 모든 리소스에 tenant_id 격리 확인 -2. **인증**: API Key + Bearer Token 인증 확인 -3. **Validation**: FormRequest 검증 규칙 준수 -4. **Soft Delete**: 삭제 시 soft delete 적용 확인 -5. **Reorder**: 순서 변경 기능 정상 작동 -6. **Response Structure**: {success, message, data} 형식 준수 -7. **Audit Trail**: created_by, updated_by 자동 설정 - -### 기술 세부사항: - -**테스트 패턴:** -```php -// 1. Setup phase - 테넌트, 사용자, 관련 모델 생성 -$tenant = Tenant::factory()->create(); -$user = User::factory()->create(); - -// 2. 계층 구조 생성 (Page → Section → Field/BomItem) -$page = ItemPage::create([...]); -$section = ItemSection::create(['page_id' => $page->id, ...]); - -// 3. API 호출 -$response = $this->authenticatedRequest('post', '/api/v1/...', [...]); - -// 4. 검증 -$response->assertStatus(200); -$this->assertDatabaseHas('table_name', [...]); -``` - -**DatabaseTransactions:** -- 각 테스트 후 자동 롤백으로 격리 보장 -- 테스트 간 데이터 오염 방지 - -### 다음 작업: - -- [ ] 추가 엣지 케이스 테스트 (선택사항) -- [ ] 통합 테스트 작성 (전체 워크플로우) -- [ ] API 문서 업데이트 (Swagger) - -### Git 커밋: -(다음 커밋 예정: test: ItemMaster API 전체 테스트 완료 (35 tests, 32 APIs)) - ---- +--- \ No newline at end of file diff --git a/database/migrations/2025_11_24_192518_add_audit_columns_to_tables.php b/database/migrations/2025_11_24_192518_add_audit_columns_to_tables.php new file mode 100644 index 0000000..087fd62 --- /dev/null +++ b/database/migrations/2025_11_24_192518_add_audit_columns_to_tables.php @@ -0,0 +1,179 @@ +foreignId('created_by') + ->nullable() + ->after('updated_at') + ->comment('생성자 사용자 ID'); + } else { + $table->foreignId('created_by') + ->nullable() + ->comment('생성자 사용자 ID'); + } + } + + // updated_by 추가 + if (!Schema::hasColumn($tableName, 'updated_by')) { + // created_by 다음에 추가 + if (Schema::hasColumn($tableName, 'created_by')) { + $table->foreignId('updated_by') + ->nullable() + ->after('created_by') + ->comment('수정자 사용자 ID'); + } else { + $table->foreignId('updated_by') + ->nullable() + ->comment('수정자 사용자 ID'); + } + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $excludeTables = [ + 'jobs', + 'job_batches', + 'password_reset_tokens', + 'personal_access_tokens', + 'taggables', + 'tags', + ]; + + $tables = [ + 'api_keys', + 'archived_record_relations', + 'archived_records', + 'audit_logs', + 'board_comments', + 'board_files', + 'board_settings', + 'boards', + 'bom_template_items', + 'bom_templates', + 'classifications', + 'clients', + 'common_codes', + 'demo_links', + 'department_user', + 'file_share_links', + 'main_request_flows', + 'model_versions', + 'models', + 'order_histories', + 'order_items', + 'orders', + 'payments', + 'permission_overrides', + 'permissions', + 'plans', + 'post_custom_field_values', + 'posts', + 'prospects', + 'roles', + 'setting_field_defs', + 'subscriptions', + 'tenant_bootstrap_runs', + 'tenant_user_profiles', + 'tenants', + 'user_roles', + 'user_tenants', + 'users', + ]; + + foreach ($tables as $tableName) { + if (!Schema::hasTable($tableName) || in_array($tableName, $excludeTables)) { + continue; + } + + Schema::table($tableName, function (Blueprint $table) use ($tableName) { + // updated_by 먼저 제거 + if (Schema::hasColumn($tableName, 'updated_by')) { + $table->dropColumn('updated_by'); + } + + // created_by 제거 + if (Schema::hasColumn($tableName, 'created_by')) { + $table->dropColumn('created_by'); + } + }); + } + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_24_192518_add_deleted_by_to_soft_delete_tables.php b/database/migrations/2025_11_24_192518_add_deleted_by_to_soft_delete_tables.php new file mode 100644 index 0000000..5cd2bbf --- /dev/null +++ b/database/migrations/2025_11_24_192518_add_deleted_by_to_soft_delete_tables.php @@ -0,0 +1,115 @@ +foreignId('deleted_by') + ->nullable() + ->after('deleted_at') + ->comment('삭제자 사용자 ID'); + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tables = [ + 'board_comments', + 'bom_templates', + 'classifications', + 'common_codes', + 'department_user', + 'lot_sales', + 'lots', + 'main_requests', + 'material_inspection_items', + 'material_inspections', + 'material_receipts', + 'materials', + 'model_versions', + 'models', + 'order_item_components', + 'order_items', + 'orders', + 'payments', + 'permission_overrides', + 'plans', + 'posts', + 'price_histories', + 'product_components', + 'products', + 'roles', + 'subscriptions', + 'tenants', + 'user_roles', + 'user_tenants', + 'users', + ]; + + foreach ($tables as $tableName) { + if (!Schema::hasTable($tableName)) { + continue; + } + + Schema::table($tableName, function (Blueprint $table) use ($tableName) { + if (Schema::hasColumn($tableName, 'deleted_by')) { + $table->dropColumn('deleted_by'); + } + }); + } + } +}; \ No newline at end of file