Files
sam-api/CURRENT_WORKS.md
hskwon cbed92a95c feat: 매출/매입 관리 API 구현
- 매출(Sale) 및 매입(Purchase) CRUD API 구현
- 문서번호 자동 생성 (SL/PU + YYYYMMDD + 시퀀스)
- 상태 관리 (draft → confirmed → invoiced)
- 확정(confirm) 및 요약(summary) 기능 추가
- BelongsToTenant, SoftDeletes 적용
- Swagger API 문서 작성 완료

추가된 파일:
- 마이그레이션: sales, purchases 테이블
- 모델: Sale, Purchase
- 서비스: SaleService, PurchaseService
- 컨트롤러: SaleController, PurchaseController
- FormRequest: Store/Update 4개
- Swagger: SaleApi.php, PurchaseApi.php

API 엔드포인트 (14개):
- GET/POST /v1/sales, /v1/purchases
- GET/PUT/DELETE /v1/{sales,purchases}/{id}
- POST /v1/{sales,purchases}/{id}/confirm
- GET /v1/{sales,purchases}/summary
2025-12-17 22:14:48 +09:00

1551 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# SAM API 작업 현황
## 2025-12-17 (화) - 매출/매입 관리 API 개발
### 작업 목표
- `docs/plans/erp-api-development-plan.md` Phase 1의 2.5 매출/매입 관리
- 매출(Sale) 및 매입(Purchase) CRUD API 구현
- 확정(confirm) 기능 및 요약(summary) 조회 기능 포함
### 생성된 마이그레이션 (2개)
| 파일명 | 설명 |
|--------|------|
| `2025_12_17_100001_create_sales_table.php` | 매출 테이블 (sale_number, sale_date, client_id, 금액, 상태) |
| `2025_12_17_100002_create_purchases_table.php` | 매입 테이블 (purchase_number, purchase_date, client_id, 금액, 상태) |
### 생성된 모델 (2개)
**app/Models/Tenants/Sale.php:**
- 매출 모델 (BelongsToTenant, SoftDeletes)
- 상태: draft → confirmed → invoiced
- Relations: client(), deposit(), creator()
- Methods: canConfirm(), canEdit(), canDelete()
**app/Models/Tenants/Purchase.php:**
- 매입 모델 (BelongsToTenant, SoftDeletes)
- 상태: draft → confirmed
- Relations: client(), withdrawal(), creator()
- Methods: canConfirm(), canEdit(), canDelete()
### 생성된 서비스 (2개)
**app/Services/SaleService.php:**
- CRUD, confirm(), summary()
- 문서번호 자동 생성: SL{YYYYMMDD}{0001}
- 상태 검증 (수정/삭제는 draft만 가능)
**app/Services/PurchaseService.php:**
- CRUD, confirm(), summary()
- 문서번호 자동 생성: PU{YYYYMMDD}{0001}
- 상태 검증 (수정/삭제는 draft만 가능)
### 생성된 FormRequest (4개)
| 파일 | 설명 |
|------|------|
| `app/Http/Requests/V1/Sale/StoreSaleRequest.php` | 매출 등록 검증 |
| `app/Http/Requests/V1/Sale/UpdateSaleRequest.php` | 매출 수정 검증 |
| `app/Http/Requests/V1/Purchase/StorePurchaseRequest.php` | 매입 등록 검증 |
| `app/Http/Requests/V1/Purchase/UpdatePurchaseRequest.php` | 매입 수정 검증 |
### 생성된 컨트롤러 (2개)
| 파일 | 엔드포인트 |
|------|-----------|
| `SaleController.php` | index, store, show, update, destroy, confirm, summary |
| `PurchaseController.php` | index, store, show, update, destroy, confirm, summary |
### 수정된 파일
**routes/api.php:**
- Sales 라우트 그룹 추가 (7개 라우트)
- Purchases 라우트 그룹 추가 (7개 라우트)
### 생성된 Swagger 문서 (2개)
| 파일 | 설명 |
|------|------|
| `app/Swagger/v1/SaleApi.php` | 매출 API 문서 (전체 엔드포인트) |
| `app/Swagger/v1/PurchaseApi.php` | 매입 API 문서 (전체 엔드포인트) |
### API 엔드포인트
**매출 API (Sales):**
- `GET /api/v1/sales` - 목록 조회
- `POST /api/v1/sales` - 등록
- `GET /api/v1/sales/{id}` - 상세 조회
- `PUT /api/v1/sales/{id}` - 수정
- `DELETE /api/v1/sales/{id}` - 삭제
- `POST /api/v1/sales/{id}/confirm` - 확정
- `GET /api/v1/sales/summary` - 요약
**매입 API (Purchases):**
- `GET /api/v1/purchases` - 목록 조회
- `POST /api/v1/purchases` - 등록
- `GET /api/v1/purchases/{id}` - 상세 조회
- `PUT /api/v1/purchases/{id}` - 수정
- `DELETE /api/v1/purchases/{id}` - 삭제
- `POST /api/v1/purchases/{id}/confirm` - 확정
- `GET /api/v1/purchases/summary` - 요약
### 검증 완료
- ✅ Pint 스타일 검사 통과
- ✅ 라우트 등록 확인 (14개)
- ✅ 마이그레이션 실행 성공
- ✅ Swagger 문서 생성 완료
---
## 2025-12-13 (금) - Items 테이블 통합 마이그레이션 작성
### 작업 목표
- `docs/plans/items-table-unification-plan.md` 기반 작업
- products + materials 테이블을 items 단일 테이블로 통합
- BOM 관리 단순화 (child_item_type + child_item_id → child_item_id만)
### 생성된 마이그레이션 파일 (6개)
| 순서 | 파일명 | Phase | 설명 |
|------|--------|-------|------|
| 1 | `2025_12_13_152423_normalize_item_types_before_unification.php` | 0 | 비표준 item_type 삭제 (PRODUCT, SUBASSEMBLY, PART 등) |
| 2 | `2025_12_13_152507_create_items_table.php` | 1.1 | items 테이블 생성 |
| 3 | `2025_12_13_152553_create_item_details_table.php` | 1.2 | item_details 테이블 생성 (1:1 확장 필드) |
| 4 | `2025_12_13_152631_migrate_products_materials_to_items.php` | 1.3 | 데이터 이관 + item_id_mappings 매핑 테이블 |
| 5 | `2025_12_13_153116_update_item_pages_source_table_to_items.php` | 3 | item_pages.source_table 업데이트 |
| 6 | `2025_12_13_153544_update_reference_tables_to_items.php` | 5 | 참조 테이블 item_id 컬럼 추가 및 매핑 |
### 생성된 모델 (2개)
**app/Models/Items/Item.php:**
- 통합 품목 모델 (FG, PT, SM, RM, CS)
- BelongsToTenant, SoftDeletes
- 스코프: products(), materials(), type(), active()
- BOM 헬퍼: getBomChildIds(), loadBomChildren()
**app/Models/Items/ItemDetail.php:**
- 품목 상세 정보 (1:1 관계)
- Products 전용: is_sellable, is_purchasable, is_producible, 파일 필드
- Materials 전용: is_inspection, specification
### 생성된 서비스
**app/Services/ItemService.php:**
- items 단일 테이블 CRUD
- 동적 필드 → options JSON 병합
- 카테고리 트리 조회
- 활성/비활성 토글
### 수정된 파일
**app/Models/ItemMaster/ItemPage.php:**
- `getTargetModelClass()`: items 테이블 지원 추가
- `isItemPage()`, `isProductType()`, `isMaterialType()` 헬퍼 추가
### ID 매핑 전략
**item_id_mappings 테이블:**
- 기존 products/materials ID → 새 items ID 매핑
- Phase 5 참조 테이블 마이그레이션에서 활용
### 참조 테이블 업데이트 대상 (Phase 5)
| 테이블 | 기존 | 추가 컬럼 |
|--------|------|----------|
| product_components | ref_type + ref_id | item_id, parent_item_id |
| bom_template_items | ref_type + ref_id | item_id |
| orders | product_id | item_id |
| order_items | product_id | item_id |
| material_receipts | material_id | item_id |
| lots | material_id | item_id |
| price_histories | item_type + item_id | new_item_id |
| item_fields | source_table | → 'items' |
### 실행 명령어
```bash
# 마이그레이션 실행 (순서대로)
php artisan migrate
# 롤백 (전체)
php artisan migrate:rollback --step=6
```
### Phase 6: 마이그레이션 실행 완료 ✅
**실행 결과:**
```
items: 362개 (FG:4, PT:4, RM:133, SM:217, CS:4)
item_details: 362개 (1:1 관계)
item_id_mappings: 362개 (ID 매핑 완료)
item_pages: 47개 → source_table='items'로 통합
product_components: 4개 중 2개 item_id 매핑 완료
```
**수정 사항:**
- `migrate_products_materials_to_items.php`: null material_code 자동 생성 로직 추가
### 다음 작업 (Phase 7 이후)
- [ ] ItemsController → ItemService 교체
- [ ] CRUD 테스트 (전체 item_type)
- [ ] BOM 계산 테스트
- [ ] Item-Master 연동 테스트
- [ ] 기존 products/materials 테이블 삭제 (확인 후)
### 참조 문서
- `docs/plans/items-table-unification-plan.md`
- `docs/INDEX.md`
---
## 2025-12-09 (월) - HR API 개발 완료 (Employee, Attendance, Department Tree)
### 작업 목표
- `docs/features/HR_API_ANALYSIS.md` 기반 HR API 구현
- Employee 관리 API (tenant_user_profiles 활용)
- Attendance 근태 관리 API (attendances 테이블)
- Department 트리 조회 API
### Phase 1: 마이그레이션 ✅
**추가된 마이그레이션:**
- `2025_12_09_084138_add_employee_status_to_tenant_user_profiles_table.php`
- `employee_status` ENUM('active', 'leave', 'resigned') DEFAULT 'active'
- `json_extra` JSON nullable (유연한 사원 정보)
- `2025_12_09_084231_create_attendances_table.php`
- `user_id`, `base_date`, `status`, `json_details`, `remarks`
- `json_details`: check_in, check_out, gps_data, work_minutes 등
### Phase 2: Employee API ✅
**수정된 파일:**
- `app/Models/Tenants/TenantUserProfile.php` - employee_status, json_extra 헬퍼 추가
**추가된 파일:**
- `app/Services/EmployeeService.php` - 사원 CRUD, 통계, 계정 생성
- `app/Http/Controllers/Api/V1/EmployeeController.php`
- `app/Http/Requests/Employee/` (5개 FormRequest)
- IndexRequest, StoreRequest, UpdateRequest, BulkDeleteRequest, CreateAccountRequest
- `app/Swagger/v1/EmployeeApi.php` - 8개 엔드포인트 문서
**API 엔드포인트 (8개):**
| Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/v1/employees` | 사원 목록 |
| POST | `/v1/employees` | 사원 등록 |
| GET | `/v1/employees/stats` | 사원 통계 |
| GET | `/v1/employees/{id}` | 사원 상세 |
| PATCH | `/v1/employees/{id}` | 사원 수정 |
| DELETE | `/v1/employees/{id}` | 사원 삭제 |
| POST | `/v1/employees/bulk-delete` | 일괄 삭제 |
| POST | `/v1/employees/{id}/create-account` | 계정 생성 |
### Phase 3: Department Tree API ✅
**수정된 파일:**
- `app/Services/DepartmentService.php` - getTree(), buildTreeNode() 추가
- `app/Http/Controllers/Api/V1/DepartmentController.php` - tree() 액션 추가
- `routes/api.php` - `/v1/departments/tree` 라우트 추가
### Phase 4: Attendance API ✅
**추가된 파일:**
- `app/Models/Tenants/Attendance.php` - 근태 모델
- BelongsToTenant, SoftDeletes
- json_details 헬퍼 (check_in, check_out, gps_data, work_minutes 등)
- 스코프: onDate, betweenDates, forUser, withStatus
- `app/Services/AttendanceService.php` - 근태 CRUD, 출퇴근, 월간 통계
- `app/Http/Controllers/Api/V1/AttendanceController.php`
- `app/Http/Requests/Attendance/` (6개 FormRequest)
- IndexRequest, StoreRequest, UpdateRequest, CheckInRequest, CheckOutRequest, MonthlyStatsRequest
- `app/Swagger/v1/AttendanceApi.php` - 9개 엔드포인트 문서
**API 엔드포인트 (9개):**
| Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/v1/attendances` | 근태 목록 |
| POST | `/v1/attendances` | 근태 등록 |
| GET | `/v1/attendances/monthly-stats` | 월간 통계 |
| POST | `/v1/attendances/check-in` | 출근 기록 |
| POST | `/v1/attendances/check-out` | 퇴근 기록 |
| GET | `/v1/attendances/{id}` | 근태 상세 |
| PATCH | `/v1/attendances/{id}` | 근태 수정 |
| DELETE | `/v1/attendances/{id}` | 근태 삭제 |
| POST | `/v1/attendances/bulk-delete` | 일괄 삭제 |
### 검증 결과
- ✅ Pint 코드 포맷팅 완료 (13개 파일 수정)
- ✅ 마이그레이션 실행 완료 (Batch 48)
- ✅ 라우트 등록 확인 (Employee 8개, Attendance 9개, Department Tree 1개)
- ✅ Swagger 문서 생성 완료
### 수정된 파일 목록
**routes/api.php:**
- EmployeeController, AttendanceController import 추가
- Employee API 라우트 그룹 (8개)
- Attendance API 라우트 그룹 (9개)
- Department /tree 라우트 추가
**버그 수정:**
- `app/Models/Tenants/Attendance.php` - BelongsToTenant 경로 수정
- `App\Models\Scopes\BelongsToTenant``App\Traits\BelongsToTenant`
### 다음 작업
- [ ] React 프론트엔드 연동
- [ ] 휴가 관리 API 구현 (향후)
---
## 2025-12-08 (일) - Flow Tester Error #61 해결 (GET query 파라미터 처리)
### 문제
- `GET /pricing/cost` 요청 시 422 에러 발생
- 에러: `item_id``item_type_code`가 필수 항목인데 누락
### 원인
- Flow 정의에 `query` 필드로 파라미터 정의되어 있음
- **FlowExecutor**가 `query` 필드를 처리하지 않고 `body`만 처리
- GET 요청은 query string으로 파라미터를 전달해야 하는데 누락됨
### 해결
**수정 파일**: `mng/app/Services/FlowTester/FlowExecutor.php`
```php
// Line 226: query 변수 바인딩 추가
$query = $this->binder->bind($step['query'] ?? []);
// Line 230-234: HTTP 요청에 query 옵션 전달
$response = $this->httpClient->request($method, $endpoint, [
'headers' => $headers,
'body' => $body,
'query' => $query, // 추가
]);
// 결과 로그에도 query 정보 포함
'request' => [
'method' => $method,
'endpoint' => $endpoint,
'headers' => $headers,
'body' => $body,
'query' => $query, // 추가
],
```
### 검증
- PHP 문법 검사: ✅ 통과
---
## 2025-12-08 (일) - Flow Tester Error #60 해결 (중복 테스트 데이터 삭제)
### 문제
- Flow Tester 재실행 시 `error.duplicate_key` 오류 발생
- 이전 테스트 실행(Error #59)이 중간에 실패하면서 테스트 데이터가 남아있음
- 동적 날짜(`{{$date}}`)가 정상 작동 중이지만, 같은 날 재실행 시 중복 발생
### 원인
- Error #59 실행 시 create_price 단계까지 진행 후 실패
- cleanup 단계(delete_price)에 도달하지 못해 테스트 데이터 잔류
- checkDuplicate() 메서드가 기존 데이터 발견
### 해결
```bash
# 잔류 테스트 데이터 삭제
docker exec -i sam-api-1 php artisan tinker --execute="
use App\Models\Products\Price;
Price::where('effective_from', '2025-12-08')->delete();
"
# 결과: 1건 삭제 완료
```
### 추가 권장사항
- Flow Tester 재실행 전 cleanup 또는 setup 스크립트 추가 고려
- 또는 effective_from에 `{{$uuid}}``{{$timestamp}}`를 조합하여 고유성 보장
---
## 2025-12-08 (일) - User 모델 경로 오류 수정 (500 에러 해결)
### 문제
- Flow Tester #59 실행 시 `GET /pricing/{id}/revisions` 에서 500 에러
- 에러: `Class "App\Models\User" not found`
### 원인
- `PriceRevision.php`, `Board.php`에서 잘못된 User 모델 경로 참조
- 실제 경로: `App\Models\Members\User`
### 수정된 파일
- `app/Models/Products/PriceRevision.php` - `\App\Models\User``\App\Models\Members\User`
- `app/Models/Boards/Board.php` - `use App\Models\User``use App\Models\Members\User`
---
## 2025-12-08 (일) - Flow Tester 동적 날짜 변수 적용 (duplicate key 해결)
### 문제
- Flow Tester #58 실행 시 `error.duplicate_key` 에러 발생
- 원인: 하드코딩된 `effective_from: "2025-01-01"`로 인해 이전 테스트 데이터와 중복
### 해결
`{{$date}}` 동적 변수를 사용하여 매번 테스트 시 오늘 날짜 사용
### 수정 내용 (Flow ID: 8 - 단가 관리 CRUD 테스트)
| 스텝 | 필드 | 변경 전 | 변경 후 |
|------|------|---------|---------|
| create_price | effective_from | "2025-01-01" | "{{$date}}" |
| create_price | effective_to | "2025-12-31" | "2099-12-31" |
| create_price_for_finalize | effective_from | "2025-01-01" | "{{$date}}" |
| get_cost | date | "2025-06-15" | "{{$date}}" |
| by_items | date | "2025-06-15" | "{{$date}}" |
### VariableBinder 지원 변수
| 변수 | 설명 | 예시 |
|------|------|------|
| `{{$date}}` | 현재 날짜 (Y-m-d) | 2025-12-08 |
| `{{$datetime}}` | 현재 날짜시간 (Y-m-d H:i:s) | 2025-12-08 14:30:00 |
| `{{$timestamp}}` | Unix 타임스탬프 | 1733637000 |
| `{{$uuid}}` | 랜덤 UUID | 550e8400-e29b-41d4-a716-... |
| `{{$random:N}}` | N자리 랜덤 숫자 | 123456 |
| `{{$faker.xxx}}` | Faker 랜덤 데이터 | 회사명, 이름 등 |
### 해결 원리
- 매번 테스트 시 오늘 날짜가 사용되어 다른 날의 중복 데이터와 충돌 없음
- 플로우 마지막에 `delete_price``cleanup_finalized` 스텝이 테스트 데이터 정리
- 별도의 cleanup 스텝 없이도 반복 실행 가능
---
## 2025-12-08 (일) - Flow Tester HTTP 상태 코드 수정
### 문제
- Flow Tester `POST /pricing` 요청에서 예상 상태 코드 201, 실제 200 반환
- HTTP 표준: POST 리소스 생성 시 201 Created 반환 필요
### 수정 내용
**ApiResponse 클래스 개선:**
- `ApiResponse::success()`: `$statusCode` 파라미터 추가 (기본값 200)
- `ApiResponse::handle()`: 콜백에서 `statusCode` 키로 상태 코드 지정 가능
**PricingController 수정:**
- `store()` 메서드: `'statusCode' => 201` 반환
### 수정된 파일
- `app/Helpers/ApiResponse.php` - 상태 코드 파라미터 추가
- `app/Http/Controllers/Api/V1/PricingController.php` - store 201 반환
### 사용 예시
```php
// Controller에서 201 반환
return ApiResponse::handle(function () use ($request) {
$data = $this->service->store($request->validated());
return ['data' => $data, 'message' => __('message.created'), 'statusCode' => 201];
});
```
### 검증 필요
- [x] Flow Tester 재실행하여 201 응답 확인 ✅
- [x] duplicate key 에러 해결 (동적 날짜 변수 적용) ✅
### 남은 작업 (TODO)
다른 Controller store 메서드 일괄 수정 (27개):
- AdminController, BoardController, CategoryController, CategoryFieldController
- CategoryTemplateController, ClassificationController, ClientGroupController
- CommonController, DesignModelController, EstimateController, FolderController
- ItemsController, MaterialController, MenuController, ModelSetController
- PostController, QuoteController, RoleController, TenantController
- TenantOptionGroupController, TenantOptionValueController, TenantStatFieldController
- ItemMaster 하위: CustomTabController, ItemBomItemController, ItemFieldController
- ItemMaster 하위: ItemSectionController, UnitOptionController
---
## 2025-12-08 (일) - 단가 관리 API 전면 재설계 (prices + price_revisions)
### 작업 목표
- `price_histories``prices` + `price_revisions` 구조로 전면 재설계
- 원가 조회 시 수입검사 입고단가 우선, 표준원가 폴백 로직 구현
- 가격 확정(finalize) 기능으로 불변성 보장
- 리비전 관리로 변경 이력 추적
### 테이블 구조 변경
**prices (단가 마스터):**
| 컬럼 | 설명 |
|------|------|
| item_type_code | 품목유형 (PRODUCT/MATERIAL) |
| item_id | 품목 ID |
| client_group_id | 고객그룹 ID (NULL=기본가) |
| purchase_price | 매입단가 (표준원가) |
| processing_cost | 가공비 |
| loss_rate | LOSS율 (%) |
| margin_rate | 마진율 (%) |
| sales_price | 판매단가 |
| rounding_rule | 반올림 규칙 (round/ceil/floor) |
| rounding_unit | 반올림 단위 (1,10,100,1000) |
| effective_from/to | 적용 기간 |
| status | 상태 (draft/active/inactive/finalized) |
| is_final | 최종 확정 여부 |
**price_revisions (변경 이력):**
| 컬럼 | 설명 |
|------|------|
| price_id | 단가 FK |
| revision_number | 리비전 번호 |
| changed_at | 변경 일시 |
| changed_by | 변경자 ID |
| change_reason | 변경 사유 |
| before_snapshot | 변경 전 JSON |
| after_snapshot | 변경 후 JSON |
### 생성된 파일
**마이그레이션 (4개):**
- `2025_12_08_154633_create_prices_table.php`
- `2025_12_08_154634_create_price_revisions_table.php`
- `2025_12_08_154635_migrate_price_histories_to_prices.php`
- `2025_12_08_154636_drop_price_histories_table.php`
**모델 (2개):**
- `app/Models/Price.php` - BelongsToTenant, SoftDeletes
- `app/Models/PriceRevision.php`
**서비스 (1개):**
- `app/Services/PricingService.php`
- index, show, store, update, destroy (CRUD)
- byItems: 다중 품목 단가 조회
- revisions: 변경 이력 조회
- finalize: 가격 확정 (불변 처리)
- getCost: 원가 조회 (receipt > standard 폴백)
**FormRequest (5개):**
- `app/Http/Requests/Pricing/PriceIndexRequest.php`
- `app/Http/Requests/Pricing/PriceStoreRequest.php`
- `app/Http/Requests/Pricing/PriceUpdateRequest.php`
- `app/Http/Requests/Pricing/PriceByItemsRequest.php`
- `app/Http/Requests/Pricing/PriceCostRequest.php`
**Swagger (1개):**
- `app/Swagger/v1/PricingApi.php` - 전면 재작성
### 삭제된 파일
- `app/Models/PriceHistory.php`
### API 엔드포인트 (9개)
| Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/api/v1/pricing` | 단가 목록 (페이지네이션) |
| POST | `/api/v1/pricing` | 단가 생성 |
| GET | `/api/v1/pricing/cost` | 원가 조회 (receipt > standard) |
| POST | `/api/v1/pricing/by-items` | 다중 품목 단가 조회 |
| GET | `/api/v1/pricing/{id}` | 단가 상세 |
| PUT | `/api/v1/pricing/{id}` | 단가 수정 |
| DELETE | `/api/v1/pricing/{id}` | 단가 삭제 |
| POST | `/api/v1/pricing/{id}/finalize` | 가격 확정 |
| GET | `/api/v1/pricing/{id}/revisions` | 변경 이력 조회 |
### 원가 조회 로직 (getCost)
```
1순위: 자재인 경우 → material_receipts.purchase_price_excl_vat
(수입검사 완료된 최신 입고단가)
2순위: prices.purchase_price (표준원가)
총원가 계산:
total_cost = (purchase_price + processing_cost) × (1 + loss_rate/100)
판매가 계산:
sales_price = round(total_cost × (1 + margin_rate/100), rounding_unit, rounding_rule)
```
### 검증 결과
- PHP 문법 검사: ✅ 모든 파일 통과
- Pint 코드 포맷팅: ✅ 14개 파일 수정 완료
- Swagger 문서 생성: ✅ 완료
### 다음 작업
- [ ] 마이그레이션 실행 (php artisan migrate)
- [ ] API 테스트 (Swagger UI)
- [ ] React 프론트엔드 연동
### 참조 문서
- `docs/front/[API-2025-12-08] pricing-api-enhancement-request.md`
- `docs/rules/pricing-policy.md`
---
## 2025-12-04 (수) - 견적 API Phase 3: Controller + FormRequest + Routes + Swagger 완료
### 작업 목표
- 견적 API Phase 3 Controller Layer 구현
- 16개 API 엔드포인트 구현 완료
### 생성된 파일
**Controller (1개):**
- `app/Http/Controllers/Api/V1/QuoteController.php`
- 16개 메서드: index, show, store, update, destroy, bulkDestroy, finalize, cancelFinalize, convertToOrder, previewNumber, calculate, calculationSchema, generatePdf, sendEmail, sendKakao, sendHistory
- 4개 Service DI: QuoteService, QuoteNumberService, QuoteCalculationService, QuoteDocumentService
- ApiResponse::handle() 패턴 적용
**FormRequest (7개):**
| 파일 | 설명 |
|------|------|
| `QuoteIndexRequest.php` | 목록 조회 파라미터 검증 |
| `QuoteStoreRequest.php` | 견적 생성 검증 (items 배열 포함) |
| `QuoteUpdateRequest.php` | 견적 수정 검증 |
| `QuoteBulkDeleteRequest.php` | 일괄 삭제 IDs 검증 |
| `QuoteCalculateRequest.php` | 자동산출 입력값 검증 |
| `QuoteSendEmailRequest.php` | 이메일 발송 검증 |
| `QuoteSendKakaoRequest.php` | 카카오 발송 검증 |
**Swagger (1개):**
- `app/Swagger/v1/QuoteApi.php`
- 12개 스키마: Quote, QuoteItem, QuotePagination, QuoteCreateRequest, QuoteUpdateRequest 등
- 16개 엔드포인트 문서화
### API 엔드포인트 (16개)
| Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/api/v1/quotes` | 견적 목록 (페이지네이션) |
| POST | `/api/v1/quotes` | 견적 생성 |
| GET | `/api/v1/quotes/number/preview` | 견적번호 미리보기 |
| POST | `/api/v1/quotes/calculate` | 자동산출 |
| GET | `/api/v1/quotes/calculate/schema` | 산출 스키마 조회 |
| DELETE | `/api/v1/quotes/bulk` | 일괄 삭제 |
| GET | `/api/v1/quotes/{id}` | 견적 상세 |
| PUT | `/api/v1/quotes/{id}` | 견적 수정 |
| DELETE | `/api/v1/quotes/{id}` | 견적 삭제 |
| POST | `/api/v1/quotes/{id}/finalize` | 확정 |
| POST | `/api/v1/quotes/{id}/cancel-finalize` | 확정 취소 |
| POST | `/api/v1/quotes/{id}/convert` | 주문 전환 |
| GET | `/api/v1/quotes/{id}/pdf` | PDF 생성 |
| POST | `/api/v1/quotes/{id}/send/email` | 이메일 발송 |
| POST | `/api/v1/quotes/{id}/send/kakao` | 카카오 발송 |
| GET | `/api/v1/quotes/{id}/send/history` | 발송 이력 |
### 검증 결과
- PHP 문법 검사: ✅ 9개 파일 통과
- Pint 코드 포맷팅: ✅ 완료
- Swagger 문서 생성: ✅ 완료
- 라우트 등록: ✅ 16개 라우트 확인
### 다음 작업 (Phase 4)
- [ ] 단위 테스트 작성
- [ ] 통합 테스트 작성
- [ ] 마이그레이션 실행 및 실제 데이터 검증
---
## 2025-12-04 (수) - 견적 API Phase 2: Service Layer 구현 완료
### 작업 목표
- 견적 API Phase 2 Service Layer 구현
- 5개 Service 파일 생성 완료
### 생성된 파일
**Service Layer (5개):**
| 파일 | 설명 | 주요 기능 |
|------|------|----------|
| `QuoteService.php` | 견적 CRUD + 상태관리 | index, show, store, update, destroy, bulkDestroy, finalize, cancelFinalize, convertToOrder |
| `QuoteNumberService.php` | 견적번호 채번 | generate, preview, validate, parse, isUnique |
| `FormulaEvaluatorService.php` | 수식 평가 엔진 | validateFormula, evaluate, evaluateMultiple, evaluateRange, evaluateMapping |
| `QuoteCalculationService.php` | 견적 자동산출 | calculate, preview, recalculate, getInputSchema |
| `QuoteDocumentService.php` | 문서 생성/발송 | generatePdf, sendEmail, sendKakao, getSendHistory |
### 견적번호 형식
```
KD-{PREFIX}-{YYMMDD}-{SEQ}
예: KD-SC-251204-01 (스크린), KD-ST-251204-01 (철재)
```
### FormulaEvaluatorService 지원 함수
- 수학: `SUM`, `ROUND`, `CEIL`, `FLOOR`, `ABS`, `MIN`, `MAX`
- 논리: `IF`, `AND`, `OR`, `NOT`
### QuoteCalculationService 입력 스키마
**공통 입력:**
- `W0`: 개구부 폭 (mm)
- `H0`: 개구부 높이 (mm)
- `QTY`: 수량
**스크린 제품 추가:**
- `INSTALL_TYPE`: 설치 유형 (wall/ceiling/floor)
- `MOTOR_TYPE`: 모터 유형 (standard/heavy)
- `CONTROL_TYPE`: 제어 방식 (switch/remote/smart)
- `CHAIN_SIDE`: 체인 위치 (left/right)
**철재 제품 추가:**
- `MATERIAL`: 재질 (ss304/ss316/galvanized)
- `THICKNESS`: 두께 (mm)
- `FINISH`: 표면처리 (hairline/mirror/matte)
- `WELDING`: 용접 방식 (tig/mig/spot)
### i18n 키 추가
**에러 메시지 (error.php):**
- `quote_not_found`, `quote_not_editable`, `quote_not_deletable`
- `quote_not_finalizable`, `quote_not_finalized`, `quote_already_converted`
- `quote_not_convertible`, `quote_email_not_found`, `quote_phone_not_found`
- `formula_empty`, `formula_parentheses_mismatch`, `formula_unsupported_function`, `formula_calculation_error`
**성공 메시지 (message.php):**
- `quote.fetched`, `quote.created`, `quote.updated`, `quote.deleted`
- `quote.bulk_deleted`, `quote.finalized`, `quote.finalize_cancelled`
- `quote.converted`, `quote.calculated`, `quote.pdf_generated`
- `quote_email_sent`, `quote_kakao_sent`
### 검증 결과
- PHP 문법 검사: ✅ 5개 파일 통과
- Pint 코드 포맷팅: ✅ 완료
### 다음 작업 (Phase 3)
- [ ] QuoteController.php 생성
- [ ] FormRequest 생성 (QuoteStoreRequest, QuoteUpdateRequest 등)
- [ ] Swagger 문서 작성 (QuoteApi.php)
- [ ] 라우트 등록
---
## 2025-12-04 (수) - 거래처 API 2차 필드 추가 및 견적 API 계획 업데이트
### 작업 목표
- 거래처 API에 2차 필드 추가 (17개 신규 필드)
- 견적 API 변경사항 분석 및 계획 문서 업데이트
### 거래처 API 2차 필드 추가
**추가된 필드 (7개 섹션, 20개 필드):**
| 섹션 | 필드 | 설명 |
|------|------|------|
| 거래처 유형 | `client_type` | 매입/매출/매입매출 |
| 연락처 | `mobile`, `fax` | 모바일, 팩스 |
| 담당자 | `manager_name`, `manager_tel`, `system_manager` | 담당자 정보 |
| 발주처 설정 | `account_id`, `account_password`, `purchase_payment_day`, `sales_payment_day` | 계정 및 결제일 |
| 약정 세금 | `tax_agreement`, `tax_amount`, `tax_start_date`, `tax_end_date` | 세금 약정 정보 |
| 악성채권 | `bad_debt`, `bad_debt_amount`, `bad_debt_receive_date`, `bad_debt_end_date`, `bad_debt_progress` | 채권 정보 |
| 기타 | `memo` | 메모 |
**수정된 파일:**
- `database/migrations/2025_12_04_205603_add_extended_fields_to_clients_table.php` (NEW)
- `app/Models/Orders/Client.php` - fillable, casts, hidden 업데이트
- `app/Http/Requests/Client/ClientStoreRequest.php` - 검증 규칙 추가
- `app/Http/Requests/Client/ClientUpdateRequest.php` - 검증 규칙 추가
- `app/Services/ClientService.php` - store/update 검증 추가
- `app/Swagger/v1/ClientApi.php` - 3개 스키마 업데이트
### 견적 API 계획 업데이트
**신규 요청 - 문서 발송 API (Section 3.5):**
| Method | Endpoint | 설명 |
|--------|----------|------|
| POST | `/api/v1/quotes/{id}/send/email` | 이메일 발송 |
| POST | `/api/v1/quotes/{id}/send/fax` | 팩스 발송 |
| POST | `/api/v1/quotes/{id}/send/kakao` | 카카오톡 발송 |
**계획 문서 업데이트 내용:**
- Phase 2: `QuoteDocumentService` 추가
- Phase 3: `QuoteSendEmailRequest`, `QuoteSendFaxRequest`, `QuoteSendKakaoRequest` 추가
- Service 5개, FormRequest 8개로 조정
### Git 커밋
```
commit d164bb4
feat: [client] 거래처 API 2차 필드 추가 및 견적 계획 업데이트
```
### 다음 작업
- 견적 API Phase 2: Service Layer 구현
---
## 2025-12-04 (수) - 견적수식 시드 데이터 구현
### 작업 목표
- design/src/components/utils/formulaSampleData.ts의 데이터를 MNG에서 관리할 수 있도록 시드 데이터 구현
- 26개 수식 규칙, 11개 카테고리를 DB에 입력
### 추가된 파일
**Seeder (2개):**
- `database/seeders/QuoteFormulaCategorySeeder.php`
- 11개 카테고리 시드 (OPEN_SIZE, MAKE_SIZE, AREA, WEIGHT, GUIDE_RAIL, CASE, MOTOR, CONTROLLER, EDGE_WING, INSPECTION, PRICE_FORMULA)
- updateOrInsert 패턴으로 멱등성 보장
- `database/seeders/QuoteFormulaSeeder.php`
- 29개 수식 시드 (input 2개, calculation 18개, range 3개, mapping 1개, 단가수식 8개)
- 8개 범위 데이터 (quote_formula_ranges)
- 카테고리 코드 → ID 매핑으로 FK 참조
### 시드 데이터 상세
| 카테고리 | 코드 | 수식 수 | 설명 |
|----------|------|---------|------|
| 오픈사이즈 | OPEN_SIZE | 2 | W0, H0 입력 |
| 제작사이즈 | MAKE_SIZE | 4 | W1/H1 (스크린/철재) |
| 면적 | AREA | 1 | W1 × H1 / 1000000 |
| 중량 | WEIGHT | 2 | 스크린/철재 중량 계산 |
| 가이드레일 | GUIDE_RAIL | 5 | 길이, 자동선택, 설치유형별 수량 |
| 케이스 | CASE | 3 | 사이즈, 자재 자동선택 |
| 모터 | MOTOR | 1 | 중량 기반 자동선택 |
| 제어기 | CONTROLLER | 1 | 유형별 자동선택 |
| 마구리 | EDGE_WING | 1 | 날개 수량 계산 |
| 검사 | INSPECTION | 1 | 검사비 고정 |
| 단가수식 | PRICE_FORMULA | 8 | 품목별 단가 계산 |
### 실행 명령어
```bash
# 순서대로 실행
php artisan db:seed --class=QuoteFormulaCategorySeeder
php artisan db:seed --class=QuoteFormulaSeeder
```
### 검증 결과
- 카테고리: 11개 생성 완료 ✅
- 수식: 29개 생성 완료 ✅
- 범위 데이터: 8개 생성 완료 ✅
### 참조 문서
- `mng/docs/QUOTE_FORMULA_SEED_PLAN.md` - 구현 계획서
- `design/src/components/utils/formulaSampleData.ts` - 소스 데이터
---
## 2025-12-02 (월) - 메뉴 통합관리 시스템 구현 (Phase 1-2)
### 작업 목표
- 글로벌 메뉴-테넌트 메뉴 연결 시스템 구현
- Phase 1: DB 스키마 변경 및 모델 수정
- Phase 2: 서비스 및 API 엔드포인트 개발
### Phase 1 완료 (DB 스키마 및 모델)
**추가된 파일:**
- `database/migrations/2025_12_02_100000_add_global_menu_link_columns_to_menus_table.php`
- `global_menu_id` 컬럼: 원본 글로벌 메뉴 ID
- `is_customized` 컬럼: 테넌트 커스터마이징 여부
- 인덱스 추가
**수정된 파일:**
- `app/Models/Commons/Menu.php`
- fillable: `global_menu_id`, `is_customized` 추가
- casts: `is_customized => boolean` 등 추가
- 관계 메서드: `globalMenu()`, `tenantMenus()`
- 헬퍼 메서드: `isGlobal()`, `isClonedFromGlobal()`, `isCustomized()`
- 스코프: `scopeGlobal()`, `scopeActive()`, `scopeVisible()`, `scopeRoots()`
- `getSyncFields()`: 동기화 비교 대상 필드 목록
- `app/Services/MenuBootstrapService.php`
- `cloneGlobalMenusForTenant()`: global_menu_id 저장 추가
- 활성 메뉴만 복제 (is_active=true)
### Phase 2 완료 (서비스 및 API)
**추가된 파일:**
- `app/Services/GlobalMenuService.php` (신규)
- 글로벌 메뉴 CRUD (tenant_id = NULL)
- `syncToAllTenants()`: 특정 메뉴를 모든 테넌트에 동기화
- `stats()`: 글로벌 메뉴 통계
- `app/Services/MenuSyncService.php` (신규)
- 동기화 상태 상수: NEW, UP_TO_DATE, UPDATABLE, CUSTOMIZED, DELETED
- `getSyncStatus()`: 동기화 상태 목록 조회
- `syncMenus()`: 선택 동기화 (신규/업데이트)
- `importNewMenus()`: 신규 글로벌 메뉴 일괄 가져오기
- `syncUpdates()`: 변경된 메뉴 일괄 업데이트 (커스텀 제외)
- `getAvailableGlobalMenus()`: 복제 가능한 글로벌 메뉴 목록
- `app/Http/Controllers/Api/Admin/GlobalMenuController.php` (신규)
- 시스템 관리자용 글로벌 메뉴 관리
- index, tree, show, store, update, destroy, reorder
- syncToTenants, stats
**수정된 파일:**
- `app/Services/MenuService.php`
- `update()`: 글로벌 복제 메뉴 수정 시 is_customized=true 자동 설정
- `restore()`: 삭제된 메뉴 복원 추가
- `trashedList()`: 삭제된 메뉴 목록 조회 추가
- `app/Http/Controllers/Api/V1/MenuController.php`
- MenuSyncService DI 추가
- restore, trashed, availableGlobal, syncStatus, sync, syncNew, syncUpdates 메서드 추가
- `routes/api.php`
- GlobalMenuController use 문 추가
- 테넌트 메뉴 동기화 라우트 6개 추가 (trashed, available-global, sync-status, sync, sync-new, sync-updates, restore)
- 글로벌 메뉴 관리 라우트 9개 추가 (admin/global-menus/*)
### API 엔드포인트
**테넌트 메뉴 동기화 (V1):**
| Method | Path | 설명 |
|--------|------|------|
| GET | /v1/menus/trashed | 삭제된 메뉴 목록 |
| GET | /v1/menus/available-global | 복제 가능한 글로벌 메뉴 |
| GET | /v1/menus/sync-status | 동기화 상태 조회 |
| POST | /v1/menus/sync | 선택 동기화 |
| POST | /v1/menus/sync-new | 신규 메뉴 일괄 가져오기 |
| POST | /v1/menus/sync-updates | 변경된 메뉴 일괄 업데이트 |
| POST | /v1/menus/{id}/restore | 삭제된 메뉴 복원 |
**글로벌 메뉴 관리 (Admin):**
| Method | Path | 설명 |
|--------|------|------|
| GET | /v1/admin/global-menus | 글로벌 메뉴 목록 |
| POST | /v1/admin/global-menus | 글로벌 메뉴 생성 |
| GET | /v1/admin/global-menus/tree | 글로벌 메뉴 트리 |
| GET | /v1/admin/global-menus/stats | 통계 조회 |
| POST | /v1/admin/global-menus/reorder | 순서 변경 |
| GET | /v1/admin/global-menus/{id} | 단건 조회 |
| PUT | /v1/admin/global-menus/{id} | 수정 |
| DELETE | /v1/admin/global-menus/{id} | 삭제 |
| POST | /v1/admin/global-menus/{id}/sync-to-tenants | 모든 테넌트에 동기화 |
### 검증 결과
- PHP 문법 검사: ✅ 모든 파일 통과
- 라우트 등록: ✅ 9개 글로벌 메뉴 + 7개 테넌트 동기화 라우트 확인
### 다음 작업 (Phase 3-4)
- [ ] Phase 3: MNG 글로벌 메뉴 관리 화면
- [ ] Phase 3: MNG 동기화 센터 화면
- [ ] Phase 4: 마이그레이션 실행 및 테스트
---
## 2025-12-01 (일) - 메뉴 통합관리 시스템 설계
### 작업 목표
- PDF 기획서(SAM_ERP_인사관리전자결재_Storyboard)에서 메뉴 추출
- 글로벌 메뉴와 테넌트 메뉴 간의 연결(링크) 시스템 설계
- 메뉴 추가 SQL 쿼리 생성
### 추가된 파일
- `claudedocs/MENU_INTEGRATION_SYSTEM_DESIGN.md` (신규)
- 글로벌-테넌트 메뉴 연결 시스템 설계서
- global_menu_id, is_customized 컬럼 추가 계획
- API 엔드포인트 설계 (글로벌/테넌트 메뉴 관리)
- MNG 화면 설계 (복제, 동기화 기능)
- 구현 Phase 1~4 계획
- `claudedocs/MENU_INSERT_QUERIES.sql` (신규)
- PDF 기획서 기반 신규 메뉴 23개 INSERT 쿼리
- 인사관리 (근태/휴가/급여)
- 전자결재 (기안함/결재함/참조함)
- 게시판, 보고서, 계정정보, 회사정보, 구독관리, 결제내역, 고객센터
- 기준정보 관리 하위 8개 메뉴
### 정책 결정 사항
| 항목 | 결정 내용 |
|------|----------|
| 글로벌 메뉴 삭제 시 | 테넌트 메뉴 유지 (global_menu_id = NULL) |
| 활성 메뉴 (is_active=1) | 새 테넌트 생성 시 자동 복사 |
| 비활성 메뉴 (is_active=0) | 테넌트가 수동으로 복제 가능 |
| 숨김 메뉴 (hidden=1) | 복사되지만 테넌트에서 안 보임 |
| 기존 데이터 | 신규 테넌트부터 적용 |
### 다음 작업 (Phase별)
- [ ] Phase 1: 마이그레이션 (global_menu_id, is_customized)
- [ ] Phase 1: Menu 모델 수정
- [ ] Phase 1: MenuBootstrapService 수정
- [ ] Phase 2: GlobalMenuService 생성
- [ ] Phase 2: MenuService 메서드 추가
- [ ] Phase 2: API 엔드포인트 추가
- [ ] Phase 3: MNG 글로벌 메뉴 관리 화면
- [ ] Phase 3: MNG 테넌트 메뉴 관리 화면 개선
- [ ] Phase 4: 테스트
### Git 커밋
```
commit d7fdfa8
docs: 메뉴 통합관리 시스템 설계서 및 SQL 쿼리 추가
```
### 참고 문서
- PDF: SAM_ERP_인사관리전자결재_Storyboard_D0.6_251201.pdf
- 설계서: claudedocs/MENU_INTEGRATION_SYSTEM_DESIGN.md
---
## 2025-11-27 (수) - 시스템 게시판 기능 확장
### 작업 목표
- 기존 boards 테이블에 시스템 게시판 지원 추가
- mng에서 시스템 게시판 생성, sam에서 테넌트 게시판 + 시스템 게시판 조회
### 수정된 파일
**Migration**:
- `database/migrations/2025_11_27_205429_add_system_fields_to_boards_table.php` (NEW)
- `tenant_id` nullable 변경
- `is_system` boolean 컬럼 추가
- `board_type` VARCHAR(50) 컬럼 추가
- `deleted_at`, `deleted_by` SoftDeletes 추가
- 인덱스 추가
**Model**:
- `app/Models/Boards/Board.php`
- SoftDeletes 추가
- fillable, casts 업데이트
- `scopeAccessible(tenantId)`, `scopeSystemOnly()`, `scopeTenantOnly(tenantId)` 스코프 추가
- 권한 헬퍼 메서드 (canRead, canWrite, canManage)
- `app/Models/Boards/BoardSetting.php`
- casts, fillable 업데이트
- getMeta() 헬퍼 추가
**Service**:
- `app/Services/Boards/BoardService.php` (NEW)
- 시스템/테넌트 게시판 CRUD
- 필드 관리 (add, update, delete, reorder)
- 유틸리티 메서드
### DB 스키마 변경
```sql
-- boards 테이블 추가 컬럼
is_system TINYINT(1) DEFAULT 0 COMMENT '시스템 게시판 여부'
board_type VARCHAR(50) NULL COMMENT '게시판 유형'
deleted_at TIMESTAMP NULL
deleted_by BIGINT UNSIGNED NULL
```
### 다음 작업
- mng 게시판 관리 화면 개발 ✅
- sam API 개발 (Swagger 포함) ✅
- 검증 및 테스트 ✅
---
## 2025-11-27 (수) - sam API 게시판/게시글 API 개발
### 작업 목표
- 테넌트용 게시판/게시글 V1 API 개발
- Swagger 문서 작성
### 추가된 파일
**Service**:
- `app/Services/Boards/PostService.php` (NEW)
- 게시글 CRUD (boardCode 기반)
- 댓글 CRUD
- 커스텀 필드 관리
- 조회수 증가
**Controller**:
- `app/Http/Controllers/Api/V1/BoardController.php` (NEW)
- index: 접근 가능한 게시판 목록 (시스템 + 테넌트)
- tenantBoards: 테넌트 게시판만
- show: 게시판 상세 (코드 기반)
- store/update/destroy: 테넌트 게시판 CRUD
- fields: 게시판 필드 목록
- `app/Http/Controllers/Api/V1/PostController.php` (NEW)
- index/show/store/update/destroy: 게시글 CRUD
- comments/storeComment/updateComment/destroyComment: 댓글 CRUD
**FormRequest**:
- `app/Http/Requests/Boards/BoardStoreRequest.php` (NEW)
- `app/Http/Requests/Boards/BoardUpdateRequest.php` (NEW)
- `app/Http/Requests/Boards/PostStoreRequest.php` (NEW)
- `app/Http/Requests/Boards/PostUpdateRequest.php` (NEW)
- `app/Http/Requests/Boards/CommentStoreRequest.php` (NEW)
**Swagger**:
- `app/Swagger/v1/BoardApi.php` (NEW)
- Board, BoardField 스키마
- BoardCreateRequest, BoardUpdateRequest 스키마
- 7개 엔드포인트 문서화
- `app/Swagger/v1/PostApi.php` (NEW)
- Post, PostPagination, Comment 스키마
- PostCreateRequest, PostUpdateRequest, CommentCreateRequest 스키마
- 9개 엔드포인트 문서화
### 수정된 파일
- `routes/api.php`
- BoardController, PostController import 추가
- /v1/boards 프리픽스로 16개 라우트 등록
### API 엔드포인트 (16개)
**게시판 관리**:
| Method | Path | 설명 |
|--------|------|------|
| GET | /v1/boards | 접근 가능한 게시판 목록 |
| GET | /v1/boards/tenant | 테넌트 게시판만 |
| POST | /v1/boards | 테넌트 게시판 생성 |
| GET | /v1/boards/{code} | 게시판 상세 (코드 기반) |
| PUT | /v1/boards/{id} | 테넌트 게시판 수정 |
| DELETE | /v1/boards/{id} | 테넌트 게시판 삭제 |
| GET | /v1/boards/{code}/fields | 게시판 필드 목록 |
**게시글 관리**:
| Method | Path | 설명 |
|--------|------|------|
| GET | /v1/boards/{code}/posts | 게시글 목록 |
| POST | /v1/boards/{code}/posts | 게시글 작성 |
| GET | /v1/boards/{code}/posts/{id} | 게시글 상세 |
| PUT | /v1/boards/{code}/posts/{id} | 게시글 수정 |
| DELETE | /v1/boards/{code}/posts/{id} | 게시글 삭제 |
**댓글 관리**:
| Method | Path | 설명 |
|--------|------|------|
| GET | /v1/boards/{code}/posts/{postId}/comments | 댓글 목록 |
| POST | /v1/boards/{code}/posts/{postId}/comments | 댓글 작성 |
| PUT | /v1/boards/{code}/posts/{postId}/comments/{commentId} | 댓글 수정 |
| DELETE | /v1/boards/{code}/posts/{postId}/comments/{commentId} | 댓글 삭제 |
### 검증 결과
- PHP 문법 검사: ✅ 통과
- Pint 코드 포맷팅: ✅ 완료
- Swagger 문서 생성: ✅ 완료
- 라우트 등록: ✅ 16개 라우트 확인
---
## 2025-11-27 (수) - ItemMaster API group_id(계층번호) 추가 및 Swagger 보완
### 작업 목표
- FormRequest에 누락된 `group_id` 필드 추가
- Swagger 스키마에 `group_id` description="계층번호" 추가
- API 문서 정확도 개선
### 배경
- `POST /api/v1/item-master/pages/{pageId}/sections` Swagger 문서 점검 중 `group_id` 누락 발견
- Service에서 `$data['group_id'] ?? 1`로 사용하지만 FormRequest와 Swagger에 미정의
- `group_id`는 "계층번호"로 통일하여 주석 및 description 작성
### 수정된 파일 (4개)
**FormRequest (group_id 필드 추가)**:
- `app/Http/Requests/ItemMaster/ItemSectionStoreRequest.php`
- `app/Http/Requests/ItemMaster/ItemFieldStoreRequest.php`
- `app/Http/Requests/ItemMaster/ItemBomItemStoreRequest.php`
**Swagger**:
- `app/Swagger/v1/ItemMasterApi.php`
### Swagger 스키마 업데이트 상세
**모델 스키마 (3개)** - `description="계층번호"` 추가:
- `ItemSection`
- `ItemField`
- `ItemBomItem`
**Request 스키마 (6개)** - `group_id` 추가 또는 description 추가:
- `ItemSectionStoreRequest` - group_id 신규 추가
- `ItemFieldStoreRequest` - group_id 신규 추가
- `ItemBomItemStoreRequest` - group_id 신규 추가
- `IndependentSectionStoreRequest` - description 추가
- `IndependentFieldStoreRequest` - description 추가
- `IndependentBomItemStoreRequest` - description 추가
### 12개 EntityRelationship 엔드포인트
`EntityRelationshipApi.php`에 이미 문서화 완료 확인:
- POST/DELETE `/pages/{pageId}/link-section`, `/unlink-section/{sectionId}`
- POST/DELETE `/pages/{pageId}/link-field`, `/unlink-field/{fieldId}`
- GET `/pages/{pageId}/relationships`, `/structure`
- POST/DELETE `/sections/{sectionId}/link-field`, `/unlink-field/{fieldId}`
- POST/DELETE `/sections/{sectionId}/link-bom`, `/unlink-bom/{bomId}`
- GET `/sections/{sectionId}/relationships`
- POST `/relationships/reorder`
### 검증 결과
- Pint 코드 포맷팅: ✅ 통과
- Swagger 문서 생성: ✅ 완료
### Git 커밋
```
commit 4f78eed
feat: ItemMaster API group_id(계층번호) 추가 및 Swagger 보완
```
---
## 2025-11-26 (화) - AccessService permission_overrides 테이블 사용으로 수정
### 주요 작업
- API AccessService가 존재하지 않는 테이블(user_permission_overrides, department_permissions)을 참조하던 문제 수정
- 실제 DB의 `permission_overrides` 테이블을 사용하도록 수정
### 수정된 파일:
- `app/Services/Authz/AccessService.php`
- `hasUserOverride()`: `user_permission_overrides``permission_overrides` (model_type='App\Models\Members\User')
- `departmentAllows()`: `department_permissions``permission_overrides` (model_type='App\Models\Tenants\Department')
- 필드명 변경: `is_allowed``effect`, `user_id``model_id`
### 기술 상세:
**permission_overrides 테이블 구조:**
- `model_type`: 폴리모픽 타입 (User, Department)
- `model_id`: 대상 ID
- `permission_id`: 권한 ID
- `effect`: 0=DENY, 1=ALLOW
- `effective_from`, `effective_to`: 유효 기간
### 코드 품질:
- ✅ PHP 문법 검사 통과
- ✅ Pint 포맷팅 통과
---
## 2025-11-26 (화) - Item Master 독립 엔티티 API 추가 ✅ 완료
### 작업 목표
- 독립 엔티티(섹션, 필드, BOM) CRUD API 10개 추가
- `SectionTemplate` 모델 삭제 → `ItemSection.is_template` 플래그로 통합
- Swagger 문서 업데이트
### 변경 내용
**1. SectionTemplate → ItemSection 통합**
- `section_templates` 테이블 삭제
- `item_sections` 테이블에 `is_template` 컬럼 추가
- 기존 `/section-templates` API는 유지 (내부적으로 `is_template=true` 사용)
**2. 10개 독립 API 추가**
| API | 메서드 | 설명 |
|-----|--------|------|
| `/sections` | GET | 섹션 목록 (is_template 필터) |
| `/sections` | POST | 독립 섹션 생성 |
| `/sections/{id}/clone` | POST | 섹션 복제 |
| `/sections/{id}/usage` | GET | 섹션 사용처 조회 |
| `/fields` | GET | 필드 목록 |
| `/fields` | POST | 독립 필드 생성 |
| `/fields/{id}/clone` | POST | 필드 복제 |
| `/fields/{id}/usage` | GET | 필드 사용처 조회 |
| `/bom-items` | GET | BOM 항목 목록 |
| `/bom-items` | POST | 독립 BOM 생성 |
### 추가된 파일
- `app/Http/Requests/ItemMaster/IndependentSectionStoreRequest.php`
- `app/Http/Requests/ItemMaster/IndependentFieldStoreRequest.php`
- `app/Http/Requests/ItemMaster/IndependentBomItemStoreRequest.php`
### 삭제된 파일
- `app/Models/ItemMaster/SectionTemplate.php`
### 수정된 파일
- `app/Http/Controllers/Api/V1/ItemMaster/ItemSectionController.php`
- `app/Http/Controllers/Api/V1/ItemMaster/ItemFieldController.php`
- `app/Http/Controllers/Api/V1/ItemMaster/ItemBomItemController.php`
- `app/Services/ItemMaster/ItemSectionService.php`
- `app/Services/ItemMaster/ItemFieldService.php`
- `app/Services/ItemMaster/ItemBomItemService.php`
- `app/Models/ItemMaster/ItemSection.php` (is_template, scopeTemplates 추가)
- `routes/api.php`
- `app/Swagger/v1/ItemMasterApi.php`
### 마이그레이션
```bash
# 실행된 마이그레이션
2025_11_26_120000_add_is_template_to_item_sections_and_drop_section_templates.php
```
### 검증 결과
- PHP 문법 검사: ✅ 통과
- Pint 코드 포맷팅: ✅ 통과
- Swagger 문서 생성: ✅ 완료
---
## 2025-11-26 (화) - Item Master 하이브리드 구조 전환 (독립 엔티티 + 링크 테이블) ✅ 완료
### 작업 목표
기존 CASCADE FK 기반 계층 구조를 **독립 엔티티 + 링크 테이블** 구조로 전환
### 배경
- **문제점**: 현재 구조에서 섹션 삭제 시 항목(필드)도 함께 삭제됨 (CASCADE)
- **요구사항**:
- 페이지, 섹션, 항목은 독립적으로 존재
- 관계는 링크 테이블로 관리 (Many-to-Many)
- 페이지에서 섹션/항목 모두 직접 연결 가능
- 섹션에서 항목 연결 가능
- 엔티티 삭제 시 링크만 제거, 다른 엔티티는 유지
- `group_id`로 카테고리 격리 (품목관리=1, 향후 확장)
### 변경 구조
**Before (CASCADE FK)**:
```
item_pages
↓ page_id FK (CASCADE)
item_sections
↓ section_id FK (CASCADE)
item_fields / item_bom_items
```
**After (독립 + 링크)**:
```
item_pages (독립)
item_sections (독립)
item_fields (독립)
item_bom_items (독립)
⇄ entity_relationships (링크 테이블)
```
### Phase 계획
| Phase | 작업 내용 | 상태 |
|-------|----------|------|
| 1 | 마이그레이션: FK 제거 + group_id 추가 | ✅ 완료 |
| 2 | 마이그레이션: entity_relationships 테이블 생성 | ✅ 완료 |
| 3 | 마이그레이션: 기존 데이터 이관 | ✅ 완료 |
| 4 | 모델 및 Service 수정 | ✅ 완료 |
| 5 | 새로운 API 엔드포인트 추가 | ✅ 완료 |
| 6 | Swagger 문서 업데이트 | ✅ 완료 |
| 7 | 테스트 및 검증 | ✅ 완료 |
### 추가된 파일
**마이그레이션** (Batch 26으로 실행):
- `database/migrations/2025_11_26_100001_convert_item_tables_to_independent_entities.php`
- `database/migrations/2025_11_26_100002_create_entity_relationships_table.php`
- `database/migrations/2025_11_26_100003_migrate_existing_relationships_to_entity_relationships.php`
**모델**:
- `app/Models/ItemMaster/EntityRelationship.php` (신규)
**서비스**:
- `app/Services/ItemMaster/EntityRelationshipService.php` (신규)
**컨트롤러**:
- `app/Http/Controllers/Api/V1/ItemMaster/EntityRelationshipController.php` (신규)
**Request**:
- `app/Http/Requests/ItemMaster/LinkEntityRequest.php` (신규)
- `app/Http/Requests/ItemMaster/ReorderRelationshipsRequest.php` (신규)
**Swagger**:
- `app/Swagger/v1/EntityRelationshipApi.php` (신규)
### 수정된 파일
**모델 (group_id 추가 + relationship 메서드)**:
- `app/Models/ItemMaster/ItemPage.php`
- `app/Models/ItemMaster/ItemSection.php`
- `app/Models/ItemMaster/ItemField.php`
- `app/Models/ItemMaster/ItemBomItem.php`
- `app/Models/ItemMaster/SectionTemplate.php`
- `app/Models/ItemMaster/ItemMasterField.php`
**라우트**:
- `routes/api.php` (새로운 엔드포인트 추가)
**언어 파일**:
- `lang/ko/message.php` (linked, unlinked 추가)
- `lang/ko/error.php` (page_not_found, section_not_found, field_not_found, bom_not_found 추가)
### 새로운 API 엔드포인트 (14개)
**페이지-섹션 연결**:
- `POST /api/v1/item-master/pages/{pageId}/link-section`
- `DELETE /api/v1/item-master/pages/{pageId}/unlink-section/{sectionId}`
**페이지-필드 직접 연결**:
- `POST /api/v1/item-master/pages/{pageId}/link-field`
- `DELETE /api/v1/item-master/pages/{pageId}/unlink-field/{fieldId}`
**페이지 관계 조회**:
- `GET /api/v1/item-master/pages/{pageId}/relationships`
- `GET /api/v1/item-master/pages/{pageId}/structure`
**섹션-필드 연결**:
- `POST /api/v1/item-master/sections/{sectionId}/link-field`
- `DELETE /api/v1/item-master/sections/{sectionId}/unlink-field/{fieldId}`
**섹션-BOM 연결**:
- `POST /api/v1/item-master/sections/{sectionId}/link-bom`
- `DELETE /api/v1/item-master/sections/{sectionId}/unlink-bom/{bomId}`
**섹션 관계 조회**:
- `GET /api/v1/item-master/sections/{sectionId}/relationships`
**관계 순서 변경**:
- `POST /api/v1/item-master/relationships/reorder`
### 검증 결과
- PHP 문법 검사: ✅ 통과
- Pint 코드 포맷팅: ✅ 통과 (9개 신규 파일)
- Swagger 문서 생성: ✅ 완료
- 라우트 등록: ✅ 44개 item-master 라우트 확인
### 롤백 방법
```bash
php artisan migrate:rollback --step=3
```
### 다음 작업 (옵션)
- 기존 API (POST /pages/{pageId}/sections 등) 내부적으로 entity_relationships 사용하도록 수정
- 독립 엔티티 CRUD API 추가 (POST /sections, POST /fields 등)
---
## 2025-11-25 (월) - API 인증 에러 처리 개선 및 요청 로그 강화
### 문제 상황
- GET /item-master/init 요청 시 `Route [login] not defined` 에러 발생
- user_id null (인증 실패 상태)
- API Request 로그에 헤더 정보 누락
### 근본 원인
1. **Handler.php**: `expectsJson()` 체크만 있어서, Accept 헤더 없는 API 요청이 기본 동작(로그인 리다이렉트)으로 처리됨
2. **ApiKeyMiddleware**: 요청 로그에 헤더 정보 (X-API-KEY, Authorization) 미포함
### 해결 방안
**Handler.php (app/Exceptions/Handler.php)**
- Line 60: API 라우트 체크 추가 (`str_starts_with($request->path(), 'api/')`)
- Line 62: `$request->expectsJson() || $isApiRoute` 조건으로 변경
- 효과: Accept 헤더 없어도 API 라우트는 무조건 JSON 응답 반환
**ApiKeyMiddleware (app/Http/Middleware/ApiKeyMiddleware.php)**
- Line 52-57: headers 필드 추가
- X-API-KEY: 마스킹 ('***')
- Authorization: Bearer 토큰 마스킹 ('Bearer ***')
- Accept, Content-Type: 원본 그대로
- 효과: 인증 문제 디버깅 용이
### 수정된 파일
- `app/Exceptions/Handler.php`
- `app/Http/Middleware/ApiKeyMiddleware.php`
### 검증 결과
- PHP 문법 체크: ✅ 통과
- Pint 코드 포맷팅: ✅ 통과
### 기대 효과
1. API 요청 시 Accept 헤더 없어도 정상 JSON 응답
2. 인증 실패 시 로그인 리다이렉트 대신 401 JSON 응답
3. 요청 로그에서 헤더 정보 확인 가능 (디버깅 개선)
---
## 2025-11-24 (일) - 소프트삭제 및 타임스탬프 감사 컬럼 추가
### 작업 목표
- deleted_at이 있는 테이블에 deleted_by 컬럼 추가
- created_at, updated_at이 있는 테이블에 created_by, updated_by 컬럼 추가
### 작업 내용
**1. DB 스키마 분석 (INFORMATION_SCHEMA 쿼리)**
- deleted_at은 있지만 deleted_by가 없는 테이블: 30개
- created_at은 있지만 created_by, updated_by가 없는 테이블: 45개
**2. 마이그레이션 생성**
- `2025_11_24_192518_add_deleted_by_to_soft_delete_tables.php`
- 30개 테이블에 deleted_by 추가
- nullable, COMMENT('삭제자 사용자 ID')
- after('deleted_at') 배치
- `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 모두 정상
### 추가된 파일
- `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`
### 마이그레이션 상태
- Batch 25로 실행 완료
- 롤백 가능 (down 메서드 구현)
---
## 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 표준 준수
- Model Schemas 8개, Request Schemas 12개 작성
---
## 2025-11-18 (월) - Category API 테스트 및 개선
### 주요 작업
- Category CRUD 테스트 작성 (9개 테스트, 98개 assertion)
- 계층 구조 및 필드 관리 테스트
- Validation 로직 개선
---