feat: ItemMaster Phase 1 API 구현 (핵심 기능 13개 엔드포인트)

- Controller 4개 생성 (ItemMaster, ItemPage, ItemSection, ItemField)
- Service 4개 생성 (비즈니스 로직 및 Multi-tenant 지원)
- FormRequest 7개 생성 (Validation)
- Routes 등록 (/v1/item-master/*)

주요 API:
- GET /init - 전체 초기 데이터 로드
- 페이지 관리 (GET/POST/PUT/DELETE)
- 섹션 관리 (POST/PUT/DELETE/reorder)
- 필드 관리 (POST/PUT/DELETE/reorder)

기술적 특징:
- Service-First 패턴 (Controller는 DI + ApiResponse만)
- Multi-tenant 지원 (tenantId() 검증 + BelongsToTenant)
- Cascade Soft Delete (하위 엔티티 자동 처리)
- i18n 메시지 키 사용 (__('message.xxx'))
- order_no 자동 계산 및 reorder 지원

검증:
- 라우트 테스트: 13개 엔드포인트 정상 등록
- Pint 검사: 15 files PASS

SAM API Development Rules 준수
This commit is contained in:
2025-11-20 16:55:57 +09:00
parent 5cbfb42c0a
commit 4ccee253b6
17 changed files with 991 additions and 0 deletions

View File

@@ -1,3 +1,162 @@
## 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
# 커밋 예정
```
---
## 2025-11-20 (수) - ItemMaster 데이터베이스 구조 구축
### 주요 작업

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Api\V1\ItemMaster;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemMaster\ItemFieldStoreRequest;
use App\Http\Requests\ItemMaster\ItemFieldUpdateRequest;
use App\Http\Requests\ItemMaster\ReorderRequest;
use App\Services\ItemMaster\ItemFieldService;
class ItemFieldController extends Controller
{
public function __construct(private ItemFieldService $service) {}
/**
* 필드 생성
*
* POST /api/v1/item-master/sections/{sectionId}/fields
*/
public function store(int $sectionId, ItemFieldStoreRequest $request)
{
return ApiResponse::handle(function () use ($sectionId, $request) {
return $this->service->store($sectionId, $request->validated());
}, __('message.created'));
}
/**
* 필드 수정
*
* PUT /api/v1/item-master/fields/{id}
*/
public function update(int $id, ItemFieldUpdateRequest $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->update($id, $request->validated());
}, __('message.updated'));
}
/**
* 필드 삭제 (Soft Delete)
*
* DELETE /api/v1/item-master/fields/{id}
*/
public function destroy(int $id)
{
return ApiResponse::handle(function () use ($id) {
$this->service->destroy($id);
return 'success';
}, __('message.deleted'));
}
/**
* 필드 순서 변경
*
* PUT /api/v1/item-master/sections/{sectionId}/fields/reorder
*/
public function reorder(int $sectionId, ReorderRequest $request)
{
return ApiResponse::handle(function () use ($sectionId, $request) {
$this->service->reorder($sectionId, $request->validated()['items']);
return 'success';
}, __('message.reordered'));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Api\V1\ItemMaster;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Services\ItemMaster\ItemMasterService;
class ItemMasterController extends Controller
{
public function __construct(private ItemMasterService $service) {}
/**
* 초기화 API - 전체 데이터 로드
*
* GET /api/v1/item-master/init
*/
public function init()
{
return ApiResponse::handle(function () {
return $this->service->init();
}, __('message.fetched'));
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Api\V1\ItemMaster;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemMaster\ItemPageStoreRequest;
use App\Http\Requests\ItemMaster\ItemPageUpdateRequest;
use App\Services\ItemMaster\ItemPageService;
use Illuminate\Http\Request;
class ItemPageController extends Controller
{
public function __construct(private ItemPageService $service) {}
/**
* 페이지 목록 조회 (섹션/필드 중첩 포함)
*
* GET /api/v1/item-master/pages?item_type=FG
*/
public function index(Request $request)
{
return ApiResponse::handle(function () use ($request) {
$itemType = $request->input('item_type');
return $this->service->index($itemType);
}, __('message.fetched'));
}
/**
* 페이지 생성
*
* POST /api/v1/item-master/pages
*/
public function store(ItemPageStoreRequest $request)
{
return ApiResponse::handle(function () use ($request) {
return $this->service->store($request->validated());
}, __('message.created'));
}
/**
* 페이지 수정
*
* PUT /api/v1/item-master/pages/{id}
*/
public function update(int $id, ItemPageUpdateRequest $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->update($id, $request->validated());
}, __('message.updated'));
}
/**
* 페이지 삭제 (Soft Delete)
*
* DELETE /api/v1/item-master/pages/{id}
*/
public function destroy(int $id)
{
return ApiResponse::handle(function () use ($id) {
$this->service->destroy($id);
return 'success';
}, __('message.deleted'));
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Api\V1\ItemMaster;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemMaster\ItemSectionStoreRequest;
use App\Http\Requests\ItemMaster\ItemSectionUpdateRequest;
use App\Http\Requests\ItemMaster\ReorderRequest;
use App\Services\ItemMaster\ItemSectionService;
class ItemSectionController extends Controller
{
public function __construct(private ItemSectionService $service) {}
/**
* 섹션 생성
*
* POST /api/v1/item-master/pages/{pageId}/sections
*/
public function store(int $pageId, ItemSectionStoreRequest $request)
{
return ApiResponse::handle(function () use ($pageId, $request) {
return $this->service->store($pageId, $request->validated());
}, __('message.created'));
}
/**
* 섹션 수정
*
* PUT /api/v1/item-master/sections/{id}
*/
public function update(int $id, ItemSectionUpdateRequest $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->service->update($id, $request->validated());
}, __('message.updated'));
}
/**
* 섹션 삭제 (Soft Delete)
*
* DELETE /api/v1/item-master/sections/{id}
*/
public function destroy(int $id)
{
return ApiResponse::handle(function () use ($id) {
$this->service->destroy($id);
return 'success';
}, __('message.deleted'));
}
/**
* 섹션 순서 변경
*
* PUT /api/v1/item-master/pages/{pageId}/sections/reorder
*/
public function reorder(int $pageId, ReorderRequest $request)
{
return ApiResponse::handle(function () use ($pageId, $request) {
$this->service->reorder($pageId, $request->validated()['items']);
return 'success';
}, __('message.reordered'));
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class ItemFieldStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'field_name' => 'required|string|max:255',
'field_type' => 'required|in:textbox,number,dropdown,checkbox,date,textarea',
'is_required' => 'nullable|boolean',
'default_value' => 'nullable|string',
'placeholder' => 'nullable|string|max:255',
'display_condition' => 'nullable|array',
'validation_rules' => 'nullable|array',
'options' => 'nullable|array',
'properties' => 'nullable|array',
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class ItemFieldUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'field_name' => 'sometimes|string|max:255',
'field_type' => 'sometimes|in:textbox,number,dropdown,checkbox,date,textarea',
'is_required' => 'nullable|boolean',
'default_value' => 'nullable|string',
'placeholder' => 'nullable|string|max:255',
'display_condition' => 'nullable|array',
'validation_rules' => 'nullable|array',
'options' => 'nullable|array',
'properties' => 'nullable|array',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class ItemPageStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'page_name' => 'required|string|max:255',
'item_type' => 'required|in:FG,PT,SM,RM,CS',
'absolute_path' => 'nullable|string|max:500',
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class ItemPageUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'page_name' => 'sometimes|string|max:255',
'absolute_path' => 'nullable|string|max:500',
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class ItemSectionStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'type' => 'required|in:fields,bom',
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class ItemSectionUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'sometimes|string|max:255',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\ItemMaster;
use Illuminate\Foundation\Http\FormRequest;
class ReorderRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'items' => 'required|array|min:1',
'items.*.id' => 'required|integer|exists:item_sections,id',
'items.*.order_no' => 'required|integer|min:0',
];
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Services\ItemMaster;
use App\Models\ItemMaster\ItemField;
use App\Services\Service;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ItemFieldService extends Service
{
/**
* 필드 생성
*/
public function store(int $sectionId, array $data): ItemField
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
// order_no 자동 계산 (해당 섹션의 마지막 필드 + 1)
$maxOrder = ItemField::where('tenant_id', $tenantId)
->where('section_id', $sectionId)
->max('order_no');
$field = ItemField::create([
'tenant_id' => $tenantId,
'section_id' => $sectionId,
'field_name' => $data['field_name'],
'field_type' => $data['field_type'],
'order_no' => ($maxOrder ?? -1) + 1,
'is_required' => $data['is_required'] ?? false,
'default_value' => $data['default_value'] ?? null,
'placeholder' => $data['placeholder'] ?? null,
'display_condition' => $data['display_condition'] ?? null,
'validation_rules' => $data['validation_rules'] ?? null,
'options' => $data['options'] ?? null,
'properties' => $data['properties'] ?? null,
'created_by' => $userId,
]);
return $field;
}
/**
* 필드 수정
*/
public function update(int $id, array $data): ItemField
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$field = ItemField::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $field) {
throw new NotFoundHttpException(__('error.not_found'));
}
$field->update([
'field_name' => $data['field_name'] ?? $field->field_name,
'field_type' => $data['field_type'] ?? $field->field_type,
'is_required' => $data['is_required'] ?? $field->is_required,
'default_value' => $data['default_value'] ?? $field->default_value,
'placeholder' => $data['placeholder'] ?? $field->placeholder,
'display_condition' => $data['display_condition'] ?? $field->display_condition,
'validation_rules' => $data['validation_rules'] ?? $field->validation_rules,
'options' => $data['options'] ?? $field->options,
'properties' => $data['properties'] ?? $field->properties,
'updated_by' => $userId,
]);
return $field;
}
/**
* 필드 삭제 (Soft Delete)
*/
public function destroy(int $id): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$field = ItemField::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $field) {
throw new NotFoundHttpException(__('error.not_found'));
}
$field->update(['deleted_by' => $userId]);
$field->delete();
}
/**
* 필드 순서 변경
*
* @param array $items [['id' => 1, 'order_no' => 0], ['id' => 2, 'order_no' => 1], ...]
*/
public function reorder(int $sectionId, array $items): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
foreach ($items as $item) {
ItemField::where('tenant_id', $tenantId)
->where('section_id', $sectionId)
->where('id', $item['id'])
->update([
'order_no' => $item['order_no'],
'updated_by' => $userId,
]);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Services\ItemMaster;
use App\Models\ItemMaster\CustomTab;
use App\Models\ItemMaster\ItemMasterField;
use App\Models\ItemMaster\ItemPage;
use App\Models\ItemMaster\SectionTemplate;
use App\Models\ItemMaster\UnitOption;
use App\Services\Service;
class ItemMasterService extends Service
{
/**
* 초기화 데이터 로드
*
* - pages (섹션/필드 중첩)
* - sectionTemplates
* - masterFields
* - customTabs (columnSetting 포함)
* - unitOptions
*/
public function init(): array
{
$tenantId = $this->tenantId();
// 1. 페이지 (섹션 → 필드 중첩)
$pages = ItemPage::with([
'sections' => function ($query) {
$query->orderBy('order_no');
},
'sections.fields' => function ($query) {
$query->orderBy('order_no');
},
'sections.bomItems',
])
->where('tenant_id', $tenantId)
->where('is_active', 1)
->get();
// 2. 섹션 템플릿
$sectionTemplates = SectionTemplate::where('tenant_id', $tenantId)->get();
// 3. 마스터 필드
$masterFields = ItemMasterField::where('tenant_id', $tenantId)->get();
// 4. 커스텀 탭 (컬럼 설정 포함)
$customTabs = CustomTab::with('columnSetting')
->where('tenant_id', $tenantId)
->orderBy('order_no')
->get();
// 5. 단위 옵션
$unitOptions = UnitOption::where('tenant_id', $tenantId)->get();
return [
'pages' => $pages,
'sectionTemplates' => $sectionTemplates,
'masterFields' => $masterFields,
'customTabs' => $customTabs,
'unitOptions' => $unitOptions,
];
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Services\ItemMaster;
use App\Models\ItemMaster\ItemPage;
use App\Services\Service;
use Illuminate\Support\Collection;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ItemPageService extends Service
{
/**
* 페이지 목록 조회 (섹션/필드 중첩)
*/
public function index(?string $itemType = null): Collection
{
$tenantId = $this->tenantId();
$query = ItemPage::with([
'sections' => function ($query) {
$query->orderBy('order_no');
},
'sections.fields' => function ($query) {
$query->orderBy('order_no');
},
'sections.bomItems',
])
->where('tenant_id', $tenantId)
->where('is_active', 1);
if ($itemType) {
$query->where('item_type', strtoupper($itemType));
}
return $query->get();
}
/**
* 페이지 생성
*/
public function store(array $data): ItemPage
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$page = ItemPage::create([
'tenant_id' => $tenantId,
'page_name' => $data['page_name'],
'item_type' => $data['item_type'],
'absolute_path' => $data['absolute_path'] ?? null,
'is_active' => true,
'created_by' => $userId,
]);
// 관계 로드
$page->load(['sections']);
return $page;
}
/**
* 페이지 수정
*/
public function update(int $id, array $data): ItemPage
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$page = ItemPage::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $page) {
throw new NotFoundHttpException(__('error.not_found'));
}
$page->update([
'page_name' => $data['page_name'] ?? $page->page_name,
'absolute_path' => $data['absolute_path'] ?? $page->absolute_path,
'updated_by' => $userId,
]);
$page->load(['sections.fields', 'sections.bomItems']);
return $page;
}
/**
* 페이지 삭제 (Soft Delete)
*/
public function destroy(int $id): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$page = ItemPage::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $page) {
throw new NotFoundHttpException(__('error.not_found'));
}
$page->update(['deleted_by' => $userId]);
$page->delete();
// Cascade: 하위 섹션/필드도 Soft Delete
foreach ($page->sections as $section) {
$section->update(['deleted_by' => $userId]);
$section->delete();
foreach ($section->fields as $field) {
$field->update(['deleted_by' => $userId]);
$field->delete();
}
foreach ($section->bomItems as $bomItem) {
$bomItem->update(['deleted_by' => $userId]);
$bomItem->delete();
}
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Services\ItemMaster;
use App\Models\ItemMaster\ItemSection;
use App\Services\Service;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ItemSectionService extends Service
{
/**
* 섹션 생성
*/
public function store(int $pageId, array $data): ItemSection
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
// order_no 자동 계산 (해당 페이지의 마지막 섹션 + 1)
$maxOrder = ItemSection::where('tenant_id', $tenantId)
->where('page_id', $pageId)
->max('order_no');
$section = ItemSection::create([
'tenant_id' => $tenantId,
'page_id' => $pageId,
'title' => $data['title'],
'type' => $data['type'],
'order_no' => ($maxOrder ?? -1) + 1,
'created_by' => $userId,
]);
$section->load(['fields', 'bomItems']);
return $section;
}
/**
* 섹션 수정
*/
public function update(int $id, array $data): ItemSection
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$section = ItemSection::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $section) {
throw new NotFoundHttpException(__('error.not_found'));
}
$section->update([
'title' => $data['title'] ?? $section->title,
'updated_by' => $userId,
]);
$section->load(['fields', 'bomItems']);
return $section;
}
/**
* 섹션 삭제 (Soft Delete)
*/
public function destroy(int $id): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$section = ItemSection::where('tenant_id', $tenantId)
->where('id', $id)
->first();
if (! $section) {
throw new NotFoundHttpException(__('error.not_found'));
}
$section->update(['deleted_by' => $userId]);
$section->delete();
// Cascade: 하위 필드도 Soft Delete
foreach ($section->fields as $field) {
$field->update(['deleted_by' => $userId]);
$field->delete();
}
foreach ($section->bomItems as $bomItem) {
$bomItem->update(['deleted_by' => $userId]);
$bomItem->delete();
}
}
/**
* 섹션 순서 변경
*
* @param array $items [['id' => 1, 'order_no' => 0], ['id' => 2, 'order_no' => 1], ...]
*/
public function reorder(int $pageId, array $items): void
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
foreach ($items as $item) {
ItemSection::where('tenant_id', $tenantId)
->where('page_id', $pageId)
->where('id', $item['id'])
->update([
'order_no' => $item['order_no'],
'updated_by' => $userId,
]);
}
}
}

View File

@@ -19,6 +19,10 @@
use App\Http\Controllers\Api\V1\EstimateController;
use App\Http\Controllers\Api\V1\FileStorageController;
use App\Http\Controllers\Api\V1\FolderController;
use App\Http\Controllers\Api\V1\ItemMaster\ItemFieldController;
use App\Http\Controllers\Api\V1\ItemMaster\ItemMasterController;
use App\Http\Controllers\Api\V1\ItemMaster\ItemPageController;
use App\Http\Controllers\Api\V1\ItemMaster\ItemSectionController;
use App\Http\Controllers\Api\V1\MaterialController;
use App\Http\Controllers\Api\V1\MenuController;
use App\Http\Controllers\Api\V1\ModelSetController;
@@ -469,6 +473,30 @@
Route::post('/reorder', [FolderController::class, 'reorder'])->name('v1.folders.reorder'); // 폴더 순서 변경
});
// 품목기준관리 (ItemMaster) API
Route::prefix('item-master')->group(function () {
// 초기화
Route::get('/init', [ItemMasterController::class, 'init'])->name('v1.item-master.init');
// 페이지 관리
Route::get('/pages', [ItemPageController::class, 'index'])->name('v1.item-master.pages.index');
Route::post('/pages', [ItemPageController::class, 'store'])->name('v1.item-master.pages.store');
Route::put('/pages/{id}', [ItemPageController::class, 'update'])->name('v1.item-master.pages.update');
Route::delete('/pages/{id}', [ItemPageController::class, 'destroy'])->name('v1.item-master.pages.destroy');
// 섹션 관리
Route::post('/pages/{pageId}/sections', [ItemSectionController::class, 'store'])->name('v1.item-master.sections.store');
Route::put('/sections/{id}', [ItemSectionController::class, 'update'])->name('v1.item-master.sections.update');
Route::delete('/sections/{id}', [ItemSectionController::class, 'destroy'])->name('v1.item-master.sections.destroy');
Route::put('/pages/{pageId}/sections/reorder', [ItemSectionController::class, 'reorder'])->name('v1.item-master.sections.reorder');
// 필드 관리
Route::post('/sections/{sectionId}/fields', [ItemFieldController::class, 'store'])->name('v1.item-master.fields.store');
Route::put('/fields/{id}', [ItemFieldController::class, 'update'])->name('v1.item-master.fields.update');
Route::delete('/fields/{id}', [ItemFieldController::class, 'destroy'])->name('v1.item-master.fields.destroy');
Route::put('/sections/{sectionId}/fields/reorder', [ItemFieldController::class, 'reorder'])->name('v1.item-master.fields.reorder');
});
});
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)