feat: G-2 작업실적 관리 API 구현
- WorkResult 모델 생성 (Production 네임스페이스)
- WorkResultService 서비스 구현 (CRUD + 통계 + 토글)
- WorkResultController 컨트롤러 생성 (8개 엔드포인트)
- FormRequest 검증 클래스 (Store/Update)
- Swagger 문서 작성 (WorkResultApi.php)
- 라우트 추가 (/api/v1/work-results)
- i18n 메시지 추가 (work_result 키)
API Endpoints:
- GET /work-results - 목록 조회 (페이징, 필터링)
- GET /work-results/stats - 통계 조회
- GET /work-results/{id} - 상세 조회
- POST /work-results - 등록
- PUT /work-results/{id} - 수정
- DELETE /work-results/{id} - 삭제
- PATCH /work-results/{id}/inspection - 검사 상태 토글
- PATCH /work-results/{id}/packaging - 포장 상태 토글
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
97
app/Http/Controllers/Api/V1/WorkResultController.php
Normal file
97
app/Http/Controllers/Api/V1/WorkResultController.php
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\WorkResult\WorkResultStoreRequest;
|
||||||
|
use App\Http\Requests\WorkResult\WorkResultUpdateRequest;
|
||||||
|
use App\Services\WorkResultService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class WorkResultController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private WorkResultService $service) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 목록 조회
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->index($request->all());
|
||||||
|
}, __('message.work_result.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 통계 조회
|
||||||
|
*/
|
||||||
|
public function stats(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->stats($request->all());
|
||||||
|
}, __('message.work_result.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단건 조회
|
||||||
|
*/
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->show($id);
|
||||||
|
}, __('message.work_result.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성
|
||||||
|
*/
|
||||||
|
public function store(WorkResultStoreRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->store($request->validated());
|
||||||
|
}, __('message.work_result.created'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정
|
||||||
|
*/
|
||||||
|
public function update(WorkResultUpdateRequest $request, int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request, $id) {
|
||||||
|
return $this->service->update($id, $request->validated());
|
||||||
|
}, __('message.work_result.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 삭제
|
||||||
|
*/
|
||||||
|
public function destroy(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
$this->service->destroy($id);
|
||||||
|
|
||||||
|
return 'success';
|
||||||
|
}, __('message.work_result.deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검사 상태 토글
|
||||||
|
*/
|
||||||
|
public function toggleInspection(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->toggleInspection($id);
|
||||||
|
}, __('message.work_result.inspection_toggled'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포장 상태 토글
|
||||||
|
*/
|
||||||
|
public function togglePackaging(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->togglePackaging($id);
|
||||||
|
}, __('message.work_result.packaging_toggled'));
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Http/Requests/WorkResult/WorkResultStoreRequest.php
Normal file
46
app/Http/Requests/WorkResult/WorkResultStoreRequest.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\WorkResult;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class WorkResultStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'work_order_id' => 'required|integer|exists:work_orders,id',
|
||||||
|
'work_order_item_id' => 'nullable|integer|exists:work_order_items,id',
|
||||||
|
'lot_no' => 'required|string|max:50',
|
||||||
|
'work_date' => 'required|date',
|
||||||
|
'process_type' => 'nullable|in:screen,slat,bending',
|
||||||
|
'product_name' => 'required|string|max:200',
|
||||||
|
'specification' => 'nullable|string|max:100',
|
||||||
|
'production_qty' => 'required|integer|min:0',
|
||||||
|
'good_qty' => 'nullable|integer|min:0',
|
||||||
|
'defect_qty' => 'required|integer|min:0',
|
||||||
|
'is_inspected' => 'nullable|boolean',
|
||||||
|
'is_packaged' => 'nullable|boolean',
|
||||||
|
'worker_id' => 'nullable|integer|exists:users,id',
|
||||||
|
'memo' => 'nullable|string|max:2000',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'work_order_id.required' => __('validation.required', ['attribute' => '작업지시']),
|
||||||
|
'work_order_id.exists' => __('validation.exists', ['attribute' => '작업지시']),
|
||||||
|
'lot_no.required' => __('validation.required', ['attribute' => '로트번호']),
|
||||||
|
'work_date.required' => __('validation.required', ['attribute' => '작업일']),
|
||||||
|
'product_name.required' => __('validation.required', ['attribute' => '품목명']),
|
||||||
|
'production_qty.required' => __('validation.required', ['attribute' => '생산수량']),
|
||||||
|
'defect_qty.required' => __('validation.required', ['attribute' => '불량수량']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Http/Requests/WorkResult/WorkResultUpdateRequest.php
Normal file
43
app/Http/Requests/WorkResult/WorkResultUpdateRequest.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\WorkResult;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class WorkResultUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'work_order_id' => 'sometimes|integer|exists:work_orders,id',
|
||||||
|
'work_order_item_id' => 'nullable|integer|exists:work_order_items,id',
|
||||||
|
'lot_no' => 'sometimes|string|max:50',
|
||||||
|
'work_date' => 'sometimes|date',
|
||||||
|
'process_type' => 'sometimes|in:screen,slat,bending',
|
||||||
|
'product_name' => 'sometimes|string|max:200',
|
||||||
|
'specification' => 'nullable|string|max:100',
|
||||||
|
'production_qty' => 'sometimes|integer|min:0',
|
||||||
|
'good_qty' => 'nullable|integer|min:0',
|
||||||
|
'defect_qty' => 'sometimes|integer|min:0',
|
||||||
|
'is_inspected' => 'nullable|boolean',
|
||||||
|
'is_packaged' => 'nullable|boolean',
|
||||||
|
'worker_id' => 'nullable|integer|exists:users,id',
|
||||||
|
'memo' => 'nullable|string|max:2000',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'work_order_id.exists' => __('validation.exists', ['attribute' => '작업지시']),
|
||||||
|
'lot_no.max' => __('validation.max.string', ['attribute' => '로트번호', 'max' => 50]),
|
||||||
|
'work_date.date' => __('validation.date', ['attribute' => '작업일']),
|
||||||
|
'product_name.max' => __('validation.max.string', ['attribute' => '품목명', 'max' => 200]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
242
app/Models/Production/WorkResult.php
Normal file
242
app/Models/Production/WorkResult.php
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Production;
|
||||||
|
|
||||||
|
use App\Models\Members\User;
|
||||||
|
use App\Traits\BelongsToTenant;
|
||||||
|
use App\Traits\ModelTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업실적 모델
|
||||||
|
*
|
||||||
|
* 생산 작업의 실적을 기록하고 추적하는 엔티티
|
||||||
|
*/
|
||||||
|
class WorkResult extends Model
|
||||||
|
{
|
||||||
|
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'work_results';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'work_order_id',
|
||||||
|
'work_order_item_id',
|
||||||
|
'lot_no',
|
||||||
|
'work_date',
|
||||||
|
'process_type',
|
||||||
|
'product_name',
|
||||||
|
'specification',
|
||||||
|
'production_qty',
|
||||||
|
'good_qty',
|
||||||
|
'defect_qty',
|
||||||
|
'defect_rate',
|
||||||
|
'is_inspected',
|
||||||
|
'is_packaged',
|
||||||
|
'worker_id',
|
||||||
|
'memo',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'work_date' => 'date',
|
||||||
|
'production_qty' => 'integer',
|
||||||
|
'good_qty' => 'integer',
|
||||||
|
'defect_qty' => 'integer',
|
||||||
|
'defect_rate' => 'decimal:2',
|
||||||
|
'is_inspected' => 'boolean',
|
||||||
|
'is_packaged' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'deleted_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 상수
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공정 유형 (WorkOrder와 동일)
|
||||||
|
*/
|
||||||
|
public const PROCESS_SCREEN = 'screen';
|
||||||
|
|
||||||
|
public const PROCESS_SLAT = 'slat';
|
||||||
|
|
||||||
|
public const PROCESS_BENDING = 'bending';
|
||||||
|
|
||||||
|
public const PROCESS_TYPES = [
|
||||||
|
self::PROCESS_SCREEN,
|
||||||
|
self::PROCESS_SLAT,
|
||||||
|
self::PROCESS_BENDING,
|
||||||
|
];
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 관계
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업지시
|
||||||
|
*/
|
||||||
|
public function workOrder(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(WorkOrder::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업지시 품목
|
||||||
|
*/
|
||||||
|
public function workOrderItem(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(WorkOrderItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업자
|
||||||
|
*/
|
||||||
|
public function worker(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'worker_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성자
|
||||||
|
*/
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정자
|
||||||
|
*/
|
||||||
|
public function updater(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 스코프
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공정유형별 필터
|
||||||
|
*/
|
||||||
|
public function scopeProcessType($query, string $type)
|
||||||
|
{
|
||||||
|
return $query->where('process_type', $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업일 범위 필터
|
||||||
|
*/
|
||||||
|
public function scopeWorkDateBetween($query, $from, $to)
|
||||||
|
{
|
||||||
|
return $query->whereBetween('work_date', [$from, $to]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업지시별 필터
|
||||||
|
*/
|
||||||
|
public function scopeWorkOrder($query, int $workOrderId)
|
||||||
|
{
|
||||||
|
return $query->where('work_order_id', $workOrderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업자별 필터
|
||||||
|
*/
|
||||||
|
public function scopeWorker($query, int $workerId)
|
||||||
|
{
|
||||||
|
return $query->where('worker_id', $workerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검사 완료 필터
|
||||||
|
*/
|
||||||
|
public function scopeInspected($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_inspected', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포장 완료 필터
|
||||||
|
*/
|
||||||
|
public function scopePackaged($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_packaged', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 불량 있음
|
||||||
|
*/
|
||||||
|
public function scopeHasDefects($query)
|
||||||
|
{
|
||||||
|
return $query->where('defect_qty', '>', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 헬퍼 메서드
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 불량률 계산
|
||||||
|
*/
|
||||||
|
public function calculateDefectRate(): float
|
||||||
|
{
|
||||||
|
if ($this->production_qty <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round(($this->defect_qty / $this->production_qty) * 100, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 불량률 업데이트
|
||||||
|
*/
|
||||||
|
public function updateDefectRate(): void
|
||||||
|
{
|
||||||
|
$this->defect_rate = $this->calculateDefectRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 양품수량 자동 계산 및 설정
|
||||||
|
*/
|
||||||
|
public function calculateGoodQty(): int
|
||||||
|
{
|
||||||
|
return max(0, $this->production_qty - $this->defect_qty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 불량이 있는지 확인
|
||||||
|
*/
|
||||||
|
public function hasDefects(): bool
|
||||||
|
{
|
||||||
|
return $this->defect_qty > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검사 및 포장 모두 완료인지 확인
|
||||||
|
*/
|
||||||
|
public function isFullyProcessed(): bool
|
||||||
|
{
|
||||||
|
return $this->is_inspected && $this->is_packaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 부팅
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
// 저장 전 불량률 자동 계산
|
||||||
|
static::saving(function (WorkResult $model) {
|
||||||
|
$model->updateDefectRate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
279
app/Services/WorkResultService.php
Normal file
279
app/Services/WorkResultService.php
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Production\WorkOrder;
|
||||||
|
use App\Models\Production\WorkResult;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
class WorkResultService extends Service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 목록 조회 (검색/필터링/페이징)
|
||||||
|
*/
|
||||||
|
public function index(array $params)
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$page = (int) ($params['page'] ?? 1);
|
||||||
|
$size = (int) ($params['size'] ?? 20);
|
||||||
|
$q = trim((string) ($params['q'] ?? ''));
|
||||||
|
$processType = $params['process_type'] ?? null;
|
||||||
|
$workOrderId = $params['work_order_id'] ?? null;
|
||||||
|
$workerId = $params['worker_id'] ?? null;
|
||||||
|
$workDateFrom = $params['work_date_from'] ?? null;
|
||||||
|
$workDateTo = $params['work_date_to'] ?? null;
|
||||||
|
$isInspected = isset($params['is_inspected']) ? filter_var($params['is_inspected'], FILTER_VALIDATE_BOOLEAN) : null;
|
||||||
|
$isPackaged = isset($params['is_packaged']) ? filter_var($params['is_packaged'], FILTER_VALIDATE_BOOLEAN) : null;
|
||||||
|
|
||||||
|
$query = WorkResult::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->with([
|
||||||
|
'workOrder:id,work_order_no',
|
||||||
|
'worker:id,name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 검색어
|
||||||
|
if ($q !== '') {
|
||||||
|
$query->where(function ($qq) use ($q) {
|
||||||
|
$qq->where('lot_no', 'like', "%{$q}%")
|
||||||
|
->orWhere('product_name', 'like', "%{$q}%")
|
||||||
|
->orWhereHas('workOrder', fn ($wo) => $wo->where('work_order_no', 'like', "%{$q}%"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공정유형 필터
|
||||||
|
if ($processType !== null) {
|
||||||
|
$query->where('process_type', $processType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작업지시 필터
|
||||||
|
if ($workOrderId !== null) {
|
||||||
|
$query->where('work_order_id', $workOrderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작업자 필터
|
||||||
|
if ($workerId !== null) {
|
||||||
|
$query->where('worker_id', $workerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작업일 범위
|
||||||
|
if ($workDateFrom !== null) {
|
||||||
|
$query->where('work_date', '>=', $workDateFrom);
|
||||||
|
}
|
||||||
|
if ($workDateTo !== null) {
|
||||||
|
$query->where('work_date', '<=', $workDateTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검사 완료 필터
|
||||||
|
if ($isInspected !== null) {
|
||||||
|
$query->where('is_inspected', $isInspected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 포장 완료 필터
|
||||||
|
if ($isPackaged !== null) {
|
||||||
|
$query->where('is_packaged', $isPackaged);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->orderByDesc('work_date')->orderByDesc('created_at');
|
||||||
|
|
||||||
|
return $query->paginate($size, ['*'], 'page', $page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 통계 조회
|
||||||
|
*/
|
||||||
|
public function stats(array $params = []): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$workDateFrom = $params['work_date_from'] ?? null;
|
||||||
|
$workDateTo = $params['work_date_to'] ?? null;
|
||||||
|
$processType = $params['process_type'] ?? null;
|
||||||
|
|
||||||
|
$query = WorkResult::where('tenant_id', $tenantId);
|
||||||
|
|
||||||
|
// 작업일 범위
|
||||||
|
if ($workDateFrom !== null) {
|
||||||
|
$query->where('work_date', '>=', $workDateFrom);
|
||||||
|
}
|
||||||
|
if ($workDateTo !== null) {
|
||||||
|
$query->where('work_date', '<=', $workDateTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공정유형 필터
|
||||||
|
if ($processType !== null) {
|
||||||
|
$query->where('process_type', $processType);
|
||||||
|
}
|
||||||
|
|
||||||
|
$totals = $query->select([
|
||||||
|
DB::raw('SUM(production_qty) as total_production'),
|
||||||
|
DB::raw('SUM(good_qty) as total_good'),
|
||||||
|
DB::raw('SUM(defect_qty) as total_defect'),
|
||||||
|
])->first();
|
||||||
|
|
||||||
|
$totalProduction = (int) ($totals->total_production ?? 0);
|
||||||
|
$totalGood = (int) ($totals->total_good ?? 0);
|
||||||
|
$totalDefect = (int) ($totals->total_defect ?? 0);
|
||||||
|
|
||||||
|
$defectRate = $totalProduction > 0
|
||||||
|
? round(($totalDefect / $totalProduction) * 100, 1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_production' => $totalProduction,
|
||||||
|
'total_good' => $totalGood,
|
||||||
|
'total_defect' => $totalDefect,
|
||||||
|
'defect_rate' => $defectRate,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단건 조회
|
||||||
|
*/
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$workResult = WorkResult::where('tenant_id', $tenantId)
|
||||||
|
->with([
|
||||||
|
'workOrder:id,work_order_no,project_name,status',
|
||||||
|
'workOrderItem:id,item_name,specification,quantity',
|
||||||
|
'worker:id,name',
|
||||||
|
])
|
||||||
|
->find($id);
|
||||||
|
|
||||||
|
if (! $workResult) {
|
||||||
|
throw new NotFoundHttpException(__('error.not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $workResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성
|
||||||
|
*/
|
||||||
|
public function store(array $data)
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||||
|
$data['tenant_id'] = $tenantId;
|
||||||
|
$data['created_by'] = $userId;
|
||||||
|
$data['updated_by'] = $userId;
|
||||||
|
|
||||||
|
// 양품수량 자동 계산 (입력 안 된 경우)
|
||||||
|
if (! isset($data['good_qty'])) {
|
||||||
|
$data['good_qty'] = max(0, ($data['production_qty'] ?? 0) - ($data['defect_qty'] ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작업지시 정보로 자동 채움
|
||||||
|
if (! empty($data['work_order_id'])) {
|
||||||
|
$workOrder = WorkOrder::find($data['work_order_id']);
|
||||||
|
if ($workOrder) {
|
||||||
|
$data['process_type'] = $data['process_type'] ?? $workOrder->process_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WorkResult::create($data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정
|
||||||
|
*/
|
||||||
|
public function update(int $id, array $data)
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
$workResult = WorkResult::where('tenant_id', $tenantId)->find($id);
|
||||||
|
|
||||||
|
if (! $workResult) {
|
||||||
|
throw new NotFoundHttpException(__('error.not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($workResult, $data, $userId) {
|
||||||
|
$data['updated_by'] = $userId;
|
||||||
|
|
||||||
|
// 양품수량 재계산 (생산수량 또는 불량수량 변경 시)
|
||||||
|
if (isset($data['production_qty']) || isset($data['defect_qty'])) {
|
||||||
|
$productionQty = $data['production_qty'] ?? $workResult->production_qty;
|
||||||
|
$defectQty = $data['defect_qty'] ?? $workResult->defect_qty;
|
||||||
|
|
||||||
|
if (! isset($data['good_qty'])) {
|
||||||
|
$data['good_qty'] = max(0, $productionQty - $defectQty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$workResult->update($data);
|
||||||
|
|
||||||
|
return $workResult->fresh([
|
||||||
|
'workOrder:id,work_order_no',
|
||||||
|
'worker:id,name',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 삭제
|
||||||
|
*/
|
||||||
|
public function destroy(int $id): void
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$workResult = WorkResult::where('tenant_id', $tenantId)->find($id);
|
||||||
|
|
||||||
|
if (! $workResult) {
|
||||||
|
throw new NotFoundHttpException(__('error.not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$workResult->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검사 상태 토글
|
||||||
|
*/
|
||||||
|
public function toggleInspection(int $id)
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
$workResult = WorkResult::where('tenant_id', $tenantId)->find($id);
|
||||||
|
|
||||||
|
if (! $workResult) {
|
||||||
|
throw new NotFoundHttpException(__('error.not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$workResult->update([
|
||||||
|
'is_inspected' => ! $workResult->is_inspected,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $workResult->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포장 상태 토글
|
||||||
|
*/
|
||||||
|
public function togglePackaging(int $id)
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
$workResult = WorkResult::where('tenant_id', $tenantId)->find($id);
|
||||||
|
|
||||||
|
if (! $workResult) {
|
||||||
|
throw new NotFoundHttpException(__('error.not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$workResult->update([
|
||||||
|
'is_packaged' => ! $workResult->is_packaged,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $workResult->fresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
313
app/Swagger/v1/WorkResultApi.php
Normal file
313
app/Swagger/v1/WorkResultApi.php
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(name="WorkResult", description="작업실적 관리")
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="WorkResult",
|
||||||
|
* type="object",
|
||||||
|
* required={"id","lot_no","work_date","work_order_id","process_type","product_name"},
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="work_order_id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="work_order_item_id", type="integer", nullable=true, example=1),
|
||||||
|
* @OA\Property(property="lot_no", type="string", example="KD-TS-250212-01-01"),
|
||||||
|
* @OA\Property(property="work_date", type="string", format="date", example="2025-02-12"),
|
||||||
|
* @OA\Property(property="process_type", type="string", enum={"screen","slat","bending"}, example="screen"),
|
||||||
|
* @OA\Property(property="product_name", type="string", example="스크린 셔터 (프리미엄)"),
|
||||||
|
* @OA\Property(property="specification", type="string", nullable=true, example="8000x2800"),
|
||||||
|
* @OA\Property(property="production_qty", type="integer", example=10),
|
||||||
|
* @OA\Property(property="good_qty", type="integer", example=9),
|
||||||
|
* @OA\Property(property="defect_qty", type="integer", example=1),
|
||||||
|
* @OA\Property(property="defect_rate", type="number", format="float", example=10.0),
|
||||||
|
* @OA\Property(property="is_inspected", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="is_packaged", type="boolean", example=false),
|
||||||
|
* @OA\Property(property="worker_id", type="integer", nullable=true, example=10),
|
||||||
|
* @OA\Property(property="memo", type="string", nullable=true),
|
||||||
|
* @OA\Property(property="created_at", type="string", example="2025-02-12T10:00:00Z"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", example="2025-02-12T10:00:00Z"),
|
||||||
|
* @OA\Property(property="work_order", type="object", nullable=true, @OA\Property(property="id", type="integer"), @OA\Property(property="work_order_no", type="string")),
|
||||||
|
* @OA\Property(property="worker", type="object", nullable=true, @OA\Property(property="id", type="integer"), @OA\Property(property="name", type="string"))
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="WorkResultStats",
|
||||||
|
* type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="total_production", type="integer", example=100, description="총 생산수량"),
|
||||||
|
* @OA\Property(property="total_good", type="integer", example=95, description="총 양품수량"),
|
||||||
|
* @OA\Property(property="total_defect", type="integer", example=5, description="총 불량수량"),
|
||||||
|
* @OA\Property(property="defect_rate", type="number", format="float", example=5.0, description="불량률 (%)")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="WorkResultPagination",
|
||||||
|
* type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="current_page", type="integer", example=1),
|
||||||
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/WorkResult")),
|
||||||
|
* @OA\Property(property="first_page_url", type="string"),
|
||||||
|
* @OA\Property(property="from", type="integer"),
|
||||||
|
* @OA\Property(property="last_page", type="integer"),
|
||||||
|
* @OA\Property(property="per_page", type="integer"),
|
||||||
|
* @OA\Property(property="total", type="integer")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="WorkResultCreateRequest",
|
||||||
|
* type="object",
|
||||||
|
* required={"work_order_id","lot_no","work_date","product_name","production_qty","defect_qty"},
|
||||||
|
*
|
||||||
|
* @OA\Property(property="work_order_id", type="integer", description="작업지시 ID"),
|
||||||
|
* @OA\Property(property="work_order_item_id", type="integer", nullable=true, description="작업지시 품목 ID"),
|
||||||
|
* @OA\Property(property="lot_no", type="string", maxLength=50, description="로트번호"),
|
||||||
|
* @OA\Property(property="work_date", type="string", format="date", description="작업일"),
|
||||||
|
* @OA\Property(property="process_type", type="string", enum={"screen","slat","bending"}, nullable=true, description="공정구분"),
|
||||||
|
* @OA\Property(property="product_name", type="string", maxLength=200, description="품목명"),
|
||||||
|
* @OA\Property(property="specification", type="string", maxLength=100, nullable=true, description="규격"),
|
||||||
|
* @OA\Property(property="production_qty", type="integer", minimum=0, description="생산수량"),
|
||||||
|
* @OA\Property(property="good_qty", type="integer", minimum=0, nullable=true, description="양품수량 (미입력 시 자동계산)"),
|
||||||
|
* @OA\Property(property="defect_qty", type="integer", minimum=0, description="불량수량"),
|
||||||
|
* @OA\Property(property="is_inspected", type="boolean", nullable=true, description="검사완료 여부"),
|
||||||
|
* @OA\Property(property="is_packaged", type="boolean", nullable=true, description="포장완료 여부"),
|
||||||
|
* @OA\Property(property="worker_id", type="integer", nullable=true, description="작업자 ID"),
|
||||||
|
* @OA\Property(property="memo", type="string", maxLength=2000, nullable=true, description="비고")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="WorkResultUpdateRequest",
|
||||||
|
* type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="work_order_id", type="integer", nullable=true),
|
||||||
|
* @OA\Property(property="work_order_item_id", type="integer", nullable=true),
|
||||||
|
* @OA\Property(property="lot_no", type="string", maxLength=50, nullable=true),
|
||||||
|
* @OA\Property(property="work_date", type="string", format="date", nullable=true),
|
||||||
|
* @OA\Property(property="process_type", type="string", enum={"screen","slat","bending"}, nullable=true),
|
||||||
|
* @OA\Property(property="product_name", type="string", maxLength=200, nullable=true),
|
||||||
|
* @OA\Property(property="specification", type="string", maxLength=100, nullable=true),
|
||||||
|
* @OA\Property(property="production_qty", type="integer", minimum=0, nullable=true),
|
||||||
|
* @OA\Property(property="good_qty", type="integer", minimum=0, nullable=true),
|
||||||
|
* @OA\Property(property="defect_qty", type="integer", minimum=0, nullable=true),
|
||||||
|
* @OA\Property(property="is_inspected", type="boolean", nullable=true),
|
||||||
|
* @OA\Property(property="is_packaged", type="boolean", nullable=true),
|
||||||
|
* @OA\Property(property="worker_id", type="integer", nullable=true),
|
||||||
|
* @OA\Property(property="memo", type="string", maxLength=2000, nullable=true)
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class WorkResultApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/work-results",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="작업실적 목록 조회",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", default=1)),
|
||||||
|
* @OA\Parameter(name="size", in="query", required=false, @OA\Schema(type="integer", default=20)),
|
||||||
|
* @OA\Parameter(name="q", in="query", required=false, @OA\Schema(type="string"), description="검색어 (로트번호, 품목명, 작업지시번호)"),
|
||||||
|
* @OA\Parameter(name="process_type", in="query", required=false, @OA\Schema(type="string", enum={"screen","slat","bending"})),
|
||||||
|
* @OA\Parameter(name="work_order_id", in="query", required=false, @OA\Schema(type="integer")),
|
||||||
|
* @OA\Parameter(name="worker_id", in="query", required=false, @OA\Schema(type="integer")),
|
||||||
|
* @OA\Parameter(name="work_date_from", in="query", required=false, @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="work_date_to", in="query", required=false, @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="is_inspected", in="query", required=false, @OA\Schema(type="boolean")),
|
||||||
|
* @OA\Parameter(name="is_packaged", in="query", required=false, @OA\Schema(type="boolean")),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkResultPagination")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function index() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/work-results/stats",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="작업실적 통계 조회",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="work_date_from", in="query", required=false, @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="work_date_to", in="query", required=false, @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="process_type", in="query", required=false, @OA\Schema(type="string", enum={"screen","slat","bending"})),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkResultStats")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function stats() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/work-results/{id}",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="작업실적 상세 조회",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkResult")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="리소스 없음")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function show() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/work-results",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="작업실적 등록",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/WorkResultCreateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="등록 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkResult")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=422, description="유효성 검증 실패")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function store() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Put(
|
||||||
|
* path="/api/v1/work-results/{id}",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="작업실적 수정",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/WorkResultUpdateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="수정 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkResult")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="리소스 없음"),
|
||||||
|
* @OA\Response(response=422, description="유효성 검증 실패")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function update() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Delete(
|
||||||
|
* path="/api/v1/work-results/{id}",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="작업실적 삭제",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="삭제 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", type="string", example="success")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="리소스 없음")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function destroy() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Patch(
|
||||||
|
* path="/api/v1/work-results/{id}/inspection",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="검사 상태 토글",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkResult")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="리소스 없음")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function toggleInspection() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Patch(
|
||||||
|
* path="/api/v1/work-results/{id}/packaging",
|
||||||
|
* tags={"WorkResult"},
|
||||||
|
* summary="포장 상태 토글",
|
||||||
|
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=200, description="성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkResult")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="리소스 없음")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function togglePackaging() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?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('work_results', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->comment('테넌트 ID');
|
||||||
|
$table->foreignId('work_order_id')->constrained()->comment('작업지시 ID');
|
||||||
|
$table->foreignId('work_order_item_id')->nullable()->constrained()->comment('작업지시 품목 ID');
|
||||||
|
|
||||||
|
$table->string('lot_no', 50)->comment('로트번호');
|
||||||
|
$table->date('work_date')->comment('작업일');
|
||||||
|
$table->enum('process_type', ['screen', 'slat', 'bending'])->comment('공정구분');
|
||||||
|
$table->string('product_name', 200)->comment('품목명');
|
||||||
|
$table->string('specification', 100)->nullable()->comment('규격');
|
||||||
|
|
||||||
|
$table->integer('production_qty')->default(0)->comment('생산수량');
|
||||||
|
$table->integer('good_qty')->default(0)->comment('양품수량');
|
||||||
|
$table->integer('defect_qty')->default(0)->comment('불량수량');
|
||||||
|
$table->decimal('defect_rate', 5, 2)->default(0)->comment('불량률 (%)');
|
||||||
|
|
||||||
|
$table->boolean('is_inspected')->default(false)->comment('검사 완료 여부');
|
||||||
|
$table->boolean('is_packaged')->default(false)->comment('포장 완료 여부');
|
||||||
|
|
||||||
|
$table->foreignId('worker_id')->nullable()->constrained('users')->comment('작업자 ID');
|
||||||
|
$table->text('memo')->nullable()->comment('비고');
|
||||||
|
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users')->comment('생성자');
|
||||||
|
$table->foreignId('updated_by')->nullable()->constrained('users')->comment('수정자');
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
// 인덱스
|
||||||
|
$table->index(['tenant_id', 'work_date']);
|
||||||
|
$table->index(['tenant_id', 'process_type']);
|
||||||
|
$table->index(['tenant_id', 'work_order_id']);
|
||||||
|
$table->index('lot_no');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('work_results');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -122,6 +122,20 @@
|
|||||||
'deleted' => '자재가 삭제되었습니다.',
|
'deleted' => '자재가 삭제되었습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 입고 관리
|
||||||
|
'receiving' => [
|
||||||
|
'fetched' => '입고를 조회했습니다.',
|
||||||
|
'created' => '입고가 등록되었습니다.',
|
||||||
|
'updated' => '입고가 수정되었습니다.',
|
||||||
|
'deleted' => '입고가 삭제되었습니다.',
|
||||||
|
'processed' => '입고처리가 완료되었습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 재고 관리
|
||||||
|
'stock' => [
|
||||||
|
'fetched' => '재고를 조회했습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
// 단가 관리
|
// 단가 관리
|
||||||
'pricing' => [
|
'pricing' => [
|
||||||
'created' => '단가가 등록되었습니다.',
|
'created' => '단가가 등록되었습니다.',
|
||||||
@@ -391,4 +405,27 @@
|
|||||||
'cancelled' => '휴가가 취소되었습니다.',
|
'cancelled' => '휴가가 취소되었습니다.',
|
||||||
'granted' => '휴가가 부여되었습니다.',
|
'granted' => '휴가가 부여되었습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 작업지시 관리
|
||||||
|
'work_order' => [
|
||||||
|
'fetched' => '작업지시를 조회했습니다.',
|
||||||
|
'created' => '작업지시가 등록되었습니다.',
|
||||||
|
'updated' => '작업지시가 수정되었습니다.',
|
||||||
|
'deleted' => '작업지시가 삭제되었습니다.',
|
||||||
|
'status_updated' => '작업지시 상태가 변경되었습니다.',
|
||||||
|
'assigned' => '작업지시가 배정되었습니다.',
|
||||||
|
'bending_toggled' => '벤딩 항목이 변경되었습니다.',
|
||||||
|
'issue_added' => '이슈가 등록되었습니다.',
|
||||||
|
'issue_resolved' => '이슈가 해결되었습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 작업실적 관리
|
||||||
|
'work_result' => [
|
||||||
|
'fetched' => '작업실적을 조회했습니다.',
|
||||||
|
'created' => '작업실적이 등록되었습니다.',
|
||||||
|
'updated' => '작업실적이 수정되었습니다.',
|
||||||
|
'deleted' => '작업실적이 삭제되었습니다.',
|
||||||
|
'inspection_toggled' => '검사 상태가 변경되었습니다.',
|
||||||
|
'packaging_toggled' => '포장 상태가 변경되었습니다.',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
use App\Http\Controllers\Api\V1\VendorLedgerController;
|
use App\Http\Controllers\Api\V1\VendorLedgerController;
|
||||||
use App\Http\Controllers\Api\V1\BankTransactionController;
|
use App\Http\Controllers\Api\V1\BankTransactionController;
|
||||||
use App\Http\Controllers\Api\V1\CardTransactionController;
|
use App\Http\Controllers\Api\V1\CardTransactionController;
|
||||||
|
use App\Http\Controllers\Api\V1\ReceivablesController;
|
||||||
|
use App\Http\Controllers\Api\V1\DailyReportController;
|
||||||
|
use App\Http\Controllers\Api\V1\ComprehensiveAnalysisController;
|
||||||
use App\Http\Controllers\Api\V1\EstimateController;
|
use App\Http\Controllers\Api\V1\EstimateController;
|
||||||
use App\Http\Controllers\Api\V1\FileStorageController;
|
use App\Http\Controllers\Api\V1\FileStorageController;
|
||||||
use App\Http\Controllers\Api\V1\FolderController;
|
use App\Http\Controllers\Api\V1\FolderController;
|
||||||
@@ -56,6 +59,7 @@
|
|||||||
use App\Http\Controllers\Api\V1\ItemsController;
|
use App\Http\Controllers\Api\V1\ItemsController;
|
||||||
use App\Http\Controllers\Api\V1\ItemsFileController;
|
use App\Http\Controllers\Api\V1\ItemsFileController;
|
||||||
use App\Http\Controllers\Api\V1\LeaveController;
|
use App\Http\Controllers\Api\V1\LeaveController;
|
||||||
|
use App\Http\Controllers\Api\V1\LeavePolicyController;
|
||||||
use App\Http\Controllers\Api\V1\LoanController;
|
use App\Http\Controllers\Api\V1\LoanController;
|
||||||
use App\Http\Controllers\Api\V1\MenuController;
|
use App\Http\Controllers\Api\V1\MenuController;
|
||||||
use App\Http\Controllers\Api\V1\ModelSetController;
|
use App\Http\Controllers\Api\V1\ModelSetController;
|
||||||
@@ -96,6 +100,7 @@
|
|||||||
use App\Http\Controllers\Api\V1\UserRoleController;
|
use App\Http\Controllers\Api\V1\UserRoleController;
|
||||||
use App\Http\Controllers\Api\V1\WithdrawalController;
|
use App\Http\Controllers\Api\V1\WithdrawalController;
|
||||||
use App\Http\Controllers\Api\V1\WorkOrderController;
|
use App\Http\Controllers\Api\V1\WorkOrderController;
|
||||||
|
use App\Http\Controllers\Api\V1\WorkResultController;
|
||||||
use App\Http\Controllers\Api\V1\WorkSettingController;
|
use App\Http\Controllers\Api\V1\WorkSettingController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@@ -328,6 +333,10 @@
|
|||||||
Route::post('/{id}/cancel', [LeaveController::class, 'cancel'])->name('v1.leaves.cancel');
|
Route::post('/{id}/cancel', [LeaveController::class, 'cancel'])->name('v1.leaves.cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Leave Policy API (휴가 정책)
|
||||||
|
Route::get('/leave-policy', [LeavePolicyController::class, 'show'])->name('v1.leave-policy.show');
|
||||||
|
Route::put('/leave-policy', [LeavePolicyController::class, 'update'])->name('v1.leave-policy.update');
|
||||||
|
|
||||||
// Approval Form API (결재 양식)
|
// Approval Form API (결재 양식)
|
||||||
Route::prefix('approval-forms')->group(function () {
|
Route::prefix('approval-forms')->group(function () {
|
||||||
Route::get('', [ApprovalFormController::class, 'index'])->name('v1.approval-forms.index');
|
Route::get('', [ApprovalFormController::class, 'index'])->name('v1.approval-forms.index');
|
||||||
@@ -499,6 +508,22 @@
|
|||||||
Route::get('/accounts', [BankTransactionController::class, 'accounts'])->name('v1.bank-transactions.accounts');
|
Route::get('/accounts', [BankTransactionController::class, 'accounts'])->name('v1.bank-transactions.accounts');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Receivables API (채권 현황)
|
||||||
|
Route::prefix('receivables')->group(function () {
|
||||||
|
Route::get('', [ReceivablesController::class, 'index'])->name('v1.receivables.index');
|
||||||
|
Route::get('/summary', [ReceivablesController::class, 'summary'])->name('v1.receivables.summary');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Daily Report API (일일 보고서)
|
||||||
|
Route::prefix('daily-report')->group(function () {
|
||||||
|
Route::get('/note-receivables', [DailyReportController::class, 'noteReceivables'])->name('v1.daily-report.note-receivables');
|
||||||
|
Route::get('/daily-accounts', [DailyReportController::class, 'dailyAccounts'])->name('v1.daily-report.daily-accounts');
|
||||||
|
Route::get('/summary', [DailyReportController::class, 'summary'])->name('v1.daily-report.summary');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Comprehensive Analysis API (종합 분석 보고서)
|
||||||
|
Route::get('/comprehensive-analysis', [ComprehensiveAnalysisController::class, 'index'])->name('v1.comprehensive-analysis.index');
|
||||||
|
|
||||||
// Plan API (요금제 관리)
|
// Plan API (요금제 관리)
|
||||||
Route::prefix('plans')->group(function () {
|
Route::prefix('plans')->group(function () {
|
||||||
Route::get('', [PlanController::class, 'index'])->name('v1.plans.index');
|
Route::get('', [PlanController::class, 'index'])->name('v1.plans.index');
|
||||||
@@ -1008,6 +1033,21 @@
|
|||||||
Route::patch('/{id}/issues/{issueId}/resolve', [WorkOrderController::class, 'resolveIssue'])->whereNumber('id')->name('v1.work-orders.issues.resolve'); // 이슈 해결
|
Route::patch('/{id}/issues/{issueId}/resolve', [WorkOrderController::class, 'resolveIssue'])->whereNumber('id')->name('v1.work-orders.issues.resolve'); // 이슈 해결
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 작업실적 관리 API (Production)
|
||||||
|
Route::prefix('work-results')->group(function () {
|
||||||
|
// 기본 CRUD
|
||||||
|
Route::get('', [WorkResultController::class, 'index'])->name('v1.work-results.index'); // 목록
|
||||||
|
Route::get('/stats', [WorkResultController::class, 'stats'])->name('v1.work-results.stats'); // 통계
|
||||||
|
Route::post('', [WorkResultController::class, 'store'])->name('v1.work-results.store'); // 생성
|
||||||
|
Route::get('/{id}', [WorkResultController::class, 'show'])->whereNumber('id')->name('v1.work-results.show'); // 상세
|
||||||
|
Route::put('/{id}', [WorkResultController::class, 'update'])->whereNumber('id')->name('v1.work-results.update'); // 수정
|
||||||
|
Route::delete('/{id}', [WorkResultController::class, 'destroy'])->whereNumber('id')->name('v1.work-results.destroy'); // 삭제
|
||||||
|
|
||||||
|
// 상태 토글
|
||||||
|
Route::patch('/{id}/inspection', [WorkResultController::class, 'toggleInspection'])->whereNumber('id')->name('v1.work-results.inspection'); // 검사 상태 토글
|
||||||
|
Route::patch('/{id}/packaging', [WorkResultController::class, 'togglePackaging'])->whereNumber('id')->name('v1.work-results.packaging'); // 포장 상태 토글
|
||||||
|
});
|
||||||
|
|
||||||
// 파일 저장소 API
|
// 파일 저장소 API
|
||||||
Route::prefix('files')->group(function () {
|
Route::prefix('files')->group(function () {
|
||||||
Route::post('/upload', [FileStorageController::class, 'upload'])->name('v1.files.upload'); // 파일 업로드 (임시)
|
Route::post('/upload', [FileStorageController::class, 'upload'])->name('v1.files.upload'); // 파일 업로드 (임시)
|
||||||
|
|||||||
Reference in New Issue
Block a user