feat(labor): 노임관리 API 구현
- Labor 모델 (BelongsToTenant, SoftDeletes)
- LaborController 7개 엔드포인트
- LaborService 비즈니스 로직
- FormRequest 4개 (Index/Store/Update/BulkDelete)
- 마이그레이션 및 라우트 등록
API: GET/POST /labor, GET/PUT/DELETE /labor/{id}, DELETE /labor/bulk, GET /labor/stats
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
48
.serena/memories/labor-api-implementation.md
Normal file
48
.serena/memories/labor-api-implementation.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Labor (노임관리) API Implementation
|
||||
|
||||
## Overview
|
||||
시공관리 > 노임관리 API 구현 완료 (2026-01-11)
|
||||
|
||||
## Database Schema
|
||||
```sql
|
||||
CREATE TABLE labors (
|
||||
id BIGINT PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
labor_number VARCHAR(50) NOT NULL, -- 노임번호
|
||||
category ENUM('가로', '세로할증'), -- 구분
|
||||
min_m DECIMAL(10,2) DEFAULT 0, -- 최소(m)
|
||||
max_m DECIMAL(10,2) DEFAULT 0, -- 최대(m)
|
||||
labor_price INT NULL, -- 노임단가
|
||||
status ENUM('사용', '중지') DEFAULT '사용', -- 상태
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_by, updated_by, deleted_by,
|
||||
timestamps, soft_deletes
|
||||
);
|
||||
```
|
||||
|
||||
## Files Structure
|
||||
```
|
||||
app/
|
||||
├── Models/Labor.php # 모델 + Scopes
|
||||
├── Http/
|
||||
│ ├── Controllers/Api/V1/LaborController.php
|
||||
│ └── Requests/Labor/
|
||||
│ ├── LaborIndexRequest.php # 목록 파라미터
|
||||
│ ├── LaborStoreRequest.php # 등록 검증
|
||||
│ ├── LaborUpdateRequest.php # 수정 검증
|
||||
│ └── LaborBulkDeleteRequest.php # 일괄삭제 검증
|
||||
└── Services/LaborService.php # 비즈니스 로직
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
- GET /api/v1/labor - 목록 (search, category, status, pagination)
|
||||
- GET /api/v1/labor/stats - 통계 (total, active)
|
||||
- POST /api/v1/labor - 등록
|
||||
- GET /api/v1/labor/{id} - 상세
|
||||
- PUT /api/v1/labor/{id} - 수정
|
||||
- DELETE /api/v1/labor/{id} - 삭제
|
||||
- DELETE /api/v1/labor/bulk - 일괄삭제
|
||||
|
||||
## Frontend Integration
|
||||
- react/src/components/business/construction/labor-management/actions.ts
|
||||
- Mock → API 호출 변환 완료
|
||||
@@ -1,5 +1,92 @@
|
||||
## 2026-01-11 (토) - Labor(노임관리) API 구현
|
||||
|
||||
### 작업 목표
|
||||
- 시공관리 > 노임관리 API 백엔드 구현
|
||||
- Frontend actions.ts API 연동
|
||||
|
||||
### 생성된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `app/Models/Labor.php` | 노임 모델 (BelongsToTenant, SoftDeletes) |
|
||||
| `app/Http/Controllers/Api/V1/LaborController.php` | 노임 컨트롤러 (7개 메서드) |
|
||||
| `app/Services/LaborService.php` | 노임 서비스 (비즈니스 로직) |
|
||||
| `app/Http/Requests/Labor/LaborIndexRequest.php` | 목록 조회 검증 |
|
||||
| `app/Http/Requests/Labor/LaborStoreRequest.php` | 등록 요청 검증 |
|
||||
| `app/Http/Requests/Labor/LaborUpdateRequest.php` | 수정 요청 검증 |
|
||||
| `app/Http/Requests/Labor/LaborBulkDeleteRequest.php` | 일괄 삭제 검증 |
|
||||
| `database/migrations/2026_01_11_000000_create_labors_table.php` | 마이그레이션 |
|
||||
|
||||
### 수정된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `routes/api.php` | Labor 라우트 7개 추가 (line 1005-1014) |
|
||||
|
||||
### API 엔드포인트
|
||||
| Method | Endpoint | 설명 |
|
||||
|--------|----------|------|
|
||||
| GET | `/api/v1/labor` | 목록 조회 |
|
||||
| GET | `/api/v1/labor/stats` | 통계 조회 |
|
||||
| POST | `/api/v1/labor` | 등록 |
|
||||
| GET | `/api/v1/labor/{id}` | 상세 조회 |
|
||||
| PUT | `/api/v1/labor/{id}` | 수정 |
|
||||
| DELETE | `/api/v1/labor/{id}` | 삭제 |
|
||||
| DELETE | `/api/v1/labor/bulk` | 일괄 삭제 |
|
||||
|
||||
### 검증 완료
|
||||
- [x] 마이그레이션 실행 완료
|
||||
- [x] Pint 코드 스타일 통과
|
||||
- [x] Service-First 아키텍처 준수
|
||||
- [x] FormRequest 검증 사용
|
||||
- [x] Multi-tenancy (BelongsToTenant) 적용
|
||||
|
||||
---
|
||||
|
||||
# SAM API 작업 현황
|
||||
|
||||
## 2025-01-09 (목) - 작업지시 코드 리뷰 기반 전면 개선
|
||||
|
||||
### 작업 목표
|
||||
- 작업지시(Work Orders) 기능 코드 리뷰 결과 기반 전면 개선
|
||||
- Critical, High, Medium 우선순위 항목 전체 수정
|
||||
|
||||
### 수정된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `app/Models/Production/WorkOrderItem.php` | BelongsToTenant 트레이트 적용 |
|
||||
| `app/Models/Production/WorkOrderBendingDetail.php` | BelongsToTenant 트레이트 적용 |
|
||||
| `app/Models/Production/WorkOrderIssue.php` | BelongsToTenant 트레이트 적용 |
|
||||
| `app/Models/Production/WorkOrder.php` | 상태 전이 규칙 (STATUS_TRANSITIONS, canTransitionTo, transitionTo) |
|
||||
| `app/Services/WorkOrderService.php` | 감사 로그, 다중 담당자, 부분 수정 지원 |
|
||||
|
||||
### 생성된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `app/Models/Production/WorkOrderAssignee.php` | 다중 담당자 피벗 모델 |
|
||||
| `database/migrations/*_create_work_order_assignees_table.php` | 다중 담당자 테이블 마이그레이션 |
|
||||
|
||||
### 주요 변경 내용
|
||||
1. **Multi-tenancy 적용**: 하위 모델 4개에 BelongsToTenant 트레이트 적용
|
||||
2. **감사 로그 적용**: 품목 삭제, 상태 변경, 이슈 등록/해결, 담당자 배정 시 기록
|
||||
3. **상태 전이 규칙**: STATUS_TRANSITIONS 상수 + canTransitionTo(), transitionTo() 메서드
|
||||
4. **다중 담당자 지원**:
|
||||
- WorkOrderAssignee 피벗 모델 생성
|
||||
- assign() 메서드에서 배열 담당자 지원
|
||||
- is_primary 플래그로 주 담당자 구분
|
||||
5. **부분 수정 지원**: 품목 업데이트 시 ID 기반 upsert/delete (기존 삭제 후 재생성 → ID 기반 부분 수정)
|
||||
|
||||
### 검증 완료
|
||||
- [x] Pint 코드 스타일 (3개 파일)
|
||||
- [x] Service-First 아키텍처 준수
|
||||
- [x] Eager loading에 assignees.user:id,name 추가
|
||||
|
||||
### Git 커밋
|
||||
- `349917f refactor(work-orders): 코드 리뷰 기반 전면 개선`
|
||||
|
||||
### 관련 문서
|
||||
- 계획: `~/.claude/plans/purring-sparking-pinwheel.md`
|
||||
|
||||
---
|
||||
|
||||
## 2026-01-08 (수) - Order Management API Phase 1.1 구현
|
||||
|
||||
### 작업 목표
|
||||
|
||||
103
app/Http/Controllers/Api/V1/LaborController.php
Normal file
103
app/Http/Controllers/Api/V1/LaborController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Labor\LaborBulkDeleteRequest;
|
||||
use App\Http\Requests\Labor\LaborIndexRequest;
|
||||
use App\Http\Requests\Labor\LaborStoreRequest;
|
||||
use App\Http\Requests\Labor\LaborUpdateRequest;
|
||||
use App\Services\LaborService;
|
||||
|
||||
class LaborController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected LaborService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 노임 목록 조회
|
||||
*/
|
||||
public function index(LaborIndexRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$data = $this->service->index($request->validated());
|
||||
|
||||
return ['data' => $data, 'message' => __('message.fetched')];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 통계 조회
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$data = $this->service->stats();
|
||||
|
||||
return ['data' => $data, 'message' => __('message.fetched')];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 상세 조회
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$data = $this->service->show($id);
|
||||
|
||||
return ['data' => $data, 'message' => __('message.fetched')];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 등록
|
||||
*/
|
||||
public function store(LaborStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$data = $this->service->store($request->validated());
|
||||
|
||||
return ['data' => $data, 'message' => __('message.created'), 'statusCode' => 201];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 수정
|
||||
*/
|
||||
public function update(LaborUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$data = $this->service->update($id, $request->validated());
|
||||
|
||||
return ['data' => $data, 'message' => __('message.updated')];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->service->destroy($id);
|
||||
|
||||
return ['data' => null, 'message' => __('message.deleted')];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 일괄 삭제
|
||||
*/
|
||||
public function bulkDestroy(LaborBulkDeleteRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validated();
|
||||
$deletedCount = $this->service->bulkDestroy($validated['ids']);
|
||||
|
||||
return ['data' => ['deleted_count' => $deletedCount], 'message' => __('message.deleted')];
|
||||
});
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/Labor/LaborBulkDeleteRequest.php
Normal file
31
app/Http/Requests/Labor/LaborBulkDeleteRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Labor;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LaborBulkDeleteRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => ['required', 'array', 'min:1'],
|
||||
'ids.*' => ['required', 'integer', 'exists:labors,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ids.required' => __('validation.required', ['attribute' => '삭제 대상']),
|
||||
'ids.array' => __('validation.array', ['attribute' => '삭제 대상']),
|
||||
'ids.min' => __('validation.min.array', ['attribute' => '삭제 대상', 'min' => 1]),
|
||||
'ids.*.exists' => __('validation.exists', ['attribute' => '노임 ID']),
|
||||
];
|
||||
}
|
||||
}
|
||||
37
app/Http/Requests/Labor/LaborIndexRequest.php
Normal file
37
app/Http/Requests/Labor/LaborIndexRequest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Labor;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LaborIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['nullable', 'string', 'max:100'],
|
||||
'category' => ['nullable', 'string', 'in:가로,세로할증'],
|
||||
'status' => ['nullable', 'string', 'in:사용,중지'],
|
||||
'start_date' => ['nullable', 'date'],
|
||||
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
'sort_by' => ['nullable', 'string', 'in:created_at,labor_number,category,min_m,max_m,labor_price'],
|
||||
'sort_dir' => ['nullable', 'string', 'in:asc,desc'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'category.in' => __('validation.in', ['attribute' => '구분']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
'end_date.after_or_equal' => __('validation.after_or_equal', ['attribute' => '종료일', 'date' => '시작일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/Labor/LaborStoreRequest.php
Normal file
39
app/Http/Requests/Labor/LaborStoreRequest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Labor;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LaborStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'labor_number' => ['required', 'string', 'max:50'],
|
||||
'category' => ['required', 'string', 'in:가로,세로할증'],
|
||||
'min_m' => ['required', 'numeric', 'min:0', 'max:9999.99'],
|
||||
'max_m' => ['required', 'numeric', 'min:0', 'max:9999.99', 'gte:min_m'],
|
||||
'labor_price' => ['nullable', 'integer', 'min:0'],
|
||||
'status' => ['required', 'string', 'in:사용,중지'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'labor_number.required' => __('validation.required', ['attribute' => '노임번호']),
|
||||
'category.required' => __('validation.required', ['attribute' => '구분']),
|
||||
'category.in' => __('validation.in', ['attribute' => '구분']),
|
||||
'min_m.required' => __('validation.required', ['attribute' => '최소(m)']),
|
||||
'max_m.required' => __('validation.required', ['attribute' => '최대(m)']),
|
||||
'max_m.gte' => __('validation.gte.numeric', ['attribute' => '최대(m)', 'value' => '최소(m)']),
|
||||
'status.required' => __('validation.required', ['attribute' => '상태']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/Labor/LaborUpdateRequest.php
Normal file
39
app/Http/Requests/Labor/LaborUpdateRequest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Labor;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LaborUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'labor_number' => ['sometimes', 'required', 'string', 'max:50'],
|
||||
'category' => ['sometimes', 'required', 'string', 'in:가로,세로할증'],
|
||||
'min_m' => ['sometimes', 'required', 'numeric', 'min:0', 'max:9999.99'],
|
||||
'max_m' => ['sometimes', 'required', 'numeric', 'min:0', 'max:9999.99', 'gte:min_m'],
|
||||
'labor_price' => ['nullable', 'integer', 'min:0'],
|
||||
'status' => ['sometimes', 'required', 'string', 'in:사용,중지'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'labor_number.required' => __('validation.required', ['attribute' => '노임번호']),
|
||||
'category.required' => __('validation.required', ['attribute' => '구분']),
|
||||
'category.in' => __('validation.in', ['attribute' => '구분']),
|
||||
'min_m.required' => __('validation.required', ['attribute' => '최소(m)']),
|
||||
'max_m.required' => __('validation.required', ['attribute' => '최대(m)']),
|
||||
'max_m.gte' => __('validation.gte.numeric', ['attribute' => '최대(m)', 'value' => '최소(m)']),
|
||||
'status.required' => __('validation.required', ['attribute' => '상태']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
];
|
||||
}
|
||||
}
|
||||
67
app/Models/Labor.php
Normal file
67
app/Models/Labor.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Labor extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'labors';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'labor_number',
|
||||
'category',
|
||||
'min_m',
|
||||
'max_m',
|
||||
'labor_price',
|
||||
'status',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'min_m' => 'decimal:2',
|
||||
'max_m' => 'decimal:2',
|
||||
'labor_price' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 상태별 필터 Scope
|
||||
*/
|
||||
public function scopeByStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 구분별 필터 Scope
|
||||
*/
|
||||
public function scopeByCategory($query, string $category)
|
||||
{
|
||||
return $query->where('category', $category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색 Scope
|
||||
*/
|
||||
public function scopeSearch($query, ?string $search)
|
||||
{
|
||||
if (empty($search)) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
return $query->where(function ($q) use ($search) {
|
||||
$q->where('labor_number', 'like', "%{$search}%")
|
||||
->orWhere('category', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
196
app/Services/LaborService.php
Normal file
196
app/Services/LaborService.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Labor;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class LaborService extends Service
|
||||
{
|
||||
/**
|
||||
* 노임 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$size = (int) ($params['size'] ?? 20);
|
||||
$search = trim((string) ($params['search'] ?? ''));
|
||||
$category = $params['category'] ?? null;
|
||||
$status = $params['status'] ?? null;
|
||||
$startDate = $params['start_date'] ?? null;
|
||||
$endDate = $params['end_date'] ?? null;
|
||||
$sortOrder = $params['sort_order'] ?? 'latest';
|
||||
|
||||
$query = Labor::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 검색어 필터
|
||||
if ($search !== '') {
|
||||
$query->search($search);
|
||||
}
|
||||
|
||||
// 구분 필터
|
||||
if ($category && $category !== 'all') {
|
||||
$query->byCategory($category);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if ($status && $status !== 'all') {
|
||||
$query->byStatus($status);
|
||||
}
|
||||
|
||||
// 날짜 필터
|
||||
if ($startDate) {
|
||||
$query->whereDate('created_at', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->whereDate('created_at', '<=', $endDate);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
if ($sortOrder === 'oldest') {
|
||||
$query->orderBy('created_at', 'asc');
|
||||
} else {
|
||||
$query->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
return $query->paginate($size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 통계 조회
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$baseQuery = Labor::where('tenant_id', $tenantId);
|
||||
|
||||
$total = (clone $baseQuery)->count();
|
||||
$active = (clone $baseQuery)->byStatus('사용')->count();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'active' => $active,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 상세 조회
|
||||
*/
|
||||
public function show(int $id): Labor
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$labor = Labor::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->find($id);
|
||||
|
||||
if (! $labor) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return $labor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 등록
|
||||
*/
|
||||
public function store(array $data): Labor
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
$payload = array_merge($data, [
|
||||
'tenant_id' => $tenantId,
|
||||
'status' => $data['status'] ?? '사용',
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
return Labor::create($payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 수정
|
||||
*/
|
||||
public function update(int $id, array $data): Labor
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$labor = Labor::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->find($id);
|
||||
|
||||
if (! $labor) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$data['updated_by'] = $userId;
|
||||
$labor->update($data);
|
||||
$labor->refresh();
|
||||
|
||||
return $labor;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 삭제 (soft delete)
|
||||
*/
|
||||
public function destroy(int $id): void
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$labor = Labor::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->find($id);
|
||||
|
||||
if (! $labor) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$labor->deleted_by = $userId;
|
||||
$labor->save();
|
||||
$labor->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노임 일괄 삭제 (soft delete)
|
||||
*/
|
||||
public function bulkDestroy(array $ids): int
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($ids, $tenantId, $userId) {
|
||||
$labors = Labor::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->get();
|
||||
|
||||
if ($labors->isEmpty()) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
foreach ($labors as $labor) {
|
||||
$labor->deleted_by = $userId;
|
||||
$labor->save();
|
||||
$labor->delete();
|
||||
$deletedCount++;
|
||||
}
|
||||
|
||||
return $deletedCount;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('labors', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->string('labor_number', 50)->comment('노임번호');
|
||||
$table->enum('category', ['가로', '세로할증'])->comment('구분');
|
||||
$table->decimal('min_m', 10, 2)->default(0)->comment('최소 m');
|
||||
$table->decimal('max_m', 10, 2)->default(0)->comment('최대 m');
|
||||
$table->unsignedInteger('labor_price')->nullable()->comment('노임단가');
|
||||
$table->enum('status', ['사용', '중지'])->default('사용')->comment('상태');
|
||||
$table->boolean('is_active')->default(true)->comment('활성화 여부 (ModelTrait::scopeActive() 사용)');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['tenant_id', 'category'], 'idx_tenant_category');
|
||||
$table->index(['tenant_id', 'status'], 'idx_tenant_status');
|
||||
$table->index(['tenant_id', 'labor_number'], 'idx_tenant_labor_number');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('labors');
|
||||
}
|
||||
};
|
||||
@@ -72,6 +72,7 @@
|
||||
use App\Http\Controllers\Api\V1\PopupController;
|
||||
// use App\Http\Controllers\Api\V1\ProductBomItemController; // REMOVED: products 테이블 삭제됨
|
||||
// use App\Http\Controllers\Api\V1\ProductController; // REMOVED: products 테이블 삭제됨
|
||||
use App\Http\Controllers\Api\V1\LaborController;
|
||||
use App\Http\Controllers\Api\V1\PositionController;
|
||||
use App\Http\Controllers\Api\V1\PostController;
|
||||
use App\Http\Controllers\Api\V1\PricingController;
|
||||
@@ -1001,6 +1002,17 @@
|
||||
Route::get('/{id}/revisions', [PricingController::class, 'revisions'])->whereNumber('id')->name('v1.pricing.revisions'); // 변경이력
|
||||
});
|
||||
|
||||
// Labor (노임관리)
|
||||
Route::prefix('labor')->group(function () {
|
||||
Route::get('', [LaborController::class, 'index'])->name('v1.labor.index'); // 목록
|
||||
Route::get('/stats', [LaborController::class, 'stats'])->name('v1.labor.stats'); // 통계
|
||||
Route::delete('/bulk', [LaborController::class, 'bulkDestroy'])->name('v1.labor.bulk-destroy'); // 일괄 삭제
|
||||
Route::post('', [LaborController::class, 'store'])->name('v1.labor.store'); // 등록
|
||||
Route::get('/{id}', [LaborController::class, 'show'])->whereNumber('id')->name('v1.labor.show'); // 상세
|
||||
Route::put('/{id}', [LaborController::class, 'update'])->whereNumber('id')->name('v1.labor.update'); // 수정
|
||||
Route::delete('/{id}', [LaborController::class, 'destroy'])->whereNumber('id')->name('v1.labor.destroy'); // 삭제
|
||||
});
|
||||
|
||||
// REMOVED: Products & Materials 라우트 삭제됨 (products/materials 테이블 삭제)
|
||||
// 모든 품목 관리는 /items 엔드포인트 사용
|
||||
|
||||
|
||||
Reference in New Issue
Block a user