feat: 공정 관리 API 개선 및 ProcessItem 추가
- ProcessItem 모델 및 마이그레이션 추가 - Process 요청/서비스 로직 수정 - Swagger API 문서 추가 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,15 +25,18 @@ public function rules(): array
|
||||
'note' => ['nullable', 'string'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
|
||||
// 분류 규칙
|
||||
// 분류 규칙 (패턴 규칙)
|
||||
'classification_rules' => ['nullable', 'array'],
|
||||
'classification_rules.*.registration_type' => ['nullable', 'string', 'in:pattern,individual'],
|
||||
'classification_rules.*.rule_type' => ['required_with:classification_rules.*', 'string', 'in:품목코드,품목명,품목구분'],
|
||||
'classification_rules.*.matching_type' => ['required_with:classification_rules.*', 'string', 'in:startsWith,endsWith,contains,equals'],
|
||||
'classification_rules.*.condition_value' => ['required_with:classification_rules.*', 'string', 'max:255'],
|
||||
'classification_rules.*.priority' => ['nullable', 'integer', 'min:0'],
|
||||
'classification_rules.*.description' => ['nullable', 'string', 'max:255'],
|
||||
'classification_rules.*.is_active' => ['nullable', 'boolean'],
|
||||
|
||||
// 개별 품목 연결
|
||||
'item_ids' => ['nullable', 'array'],
|
||||
'item_ids.*' => ['integer', 'exists:items,id'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -25,15 +25,18 @@ public function rules(): array
|
||||
'note' => ['nullable', 'string'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
|
||||
// 분류 규칙
|
||||
// 분류 규칙 (패턴 규칙)
|
||||
'classification_rules' => ['nullable', 'array'],
|
||||
'classification_rules.*.registration_type' => ['nullable', 'string', 'in:pattern,individual'],
|
||||
'classification_rules.*.rule_type' => ['required_with:classification_rules.*', 'string', 'in:품목코드,품목명,품목구분'],
|
||||
'classification_rules.*.matching_type' => ['required_with:classification_rules.*', 'string', 'in:startsWith,endsWith,contains,equals'],
|
||||
'classification_rules.*.condition_value' => ['required_with:classification_rules.*', 'string', 'max:255'],
|
||||
'classification_rules.*.priority' => ['nullable', 'integer', 'min:0'],
|
||||
'classification_rules.*.description' => ['nullable', 'string', 'max:255'],
|
||||
'classification_rules.*.is_active' => ['nullable', 'boolean'],
|
||||
|
||||
// 개별 품목 연결
|
||||
'item_ids' => ['nullable', 'array'],
|
||||
'item_ids.*' => ['integer', 'exists:items,id'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@@ -41,10 +42,29 @@ class Process extends Model
|
||||
];
|
||||
|
||||
/**
|
||||
* 공정 자동 분류 규칙
|
||||
* 공정 자동 분류 규칙 (패턴 규칙)
|
||||
*/
|
||||
public function classificationRules(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProcessClassificationRule::class)->orderBy('priority');
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정-품목 연결 (중간 테이블)
|
||||
*/
|
||||
public function processItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProcessItem::class)->orderBy('priority');
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결된 품목 (다대다)
|
||||
*/
|
||||
public function items(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Items\Item::class, 'process_items')
|
||||
->withPivot(['priority', 'is_active'])
|
||||
->withTimestamps()
|
||||
->orderByPivot('priority');
|
||||
}
|
||||
}
|
||||
|
||||
44
app/Models/ProcessItem.php
Normal file
44
app/Models/ProcessItem.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 공정-품목 연결 모델 (개별 품목 지정용)
|
||||
*/
|
||||
class ProcessItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'process_id',
|
||||
'item_id',
|
||||
'priority',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'priority' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 공정
|
||||
*/
|
||||
public function process(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Process::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목
|
||||
*/
|
||||
public function item(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Process;
|
||||
use App\Models\ProcessClassificationRule;
|
||||
use App\Models\ProcessItem;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@@ -24,7 +25,7 @@ public function index(array $params)
|
||||
|
||||
$query = Process::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('classificationRules');
|
||||
->with(['classificationRules', 'processItems.item:id,code,name']);
|
||||
|
||||
// 검색어
|
||||
if ($q !== '') {
|
||||
@@ -61,7 +62,7 @@ public function show(int $id)
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$process = Process::where('tenant_id', $tenantId)
|
||||
->with('classificationRules')
|
||||
->with(['classificationRules', 'processItems.item:id,code,name'])
|
||||
->find($id);
|
||||
|
||||
if (! $process) {
|
||||
@@ -92,14 +93,18 @@ public function store(array $data)
|
||||
}
|
||||
|
||||
$rules = $data['classification_rules'] ?? [];
|
||||
unset($data['classification_rules']);
|
||||
$itemIds = $data['item_ids'] ?? [];
|
||||
unset($data['classification_rules'], $data['item_ids']);
|
||||
|
||||
$process = Process::create($data);
|
||||
|
||||
// 분류 규칙 저장
|
||||
// 분류 규칙 저장 (패턴 규칙)
|
||||
$this->syncClassificationRules($process, $rules);
|
||||
|
||||
return $process->load('classificationRules');
|
||||
// 개별 품목 연결
|
||||
$this->syncProcessItems($process, $itemIds);
|
||||
|
||||
return $process->load(['classificationRules', 'processItems.item:id,code,name']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,7 +130,8 @@ public function update(int $id, array $data)
|
||||
}
|
||||
|
||||
$rules = $data['classification_rules'] ?? null;
|
||||
unset($data['classification_rules']);
|
||||
$itemIds = $data['item_ids'] ?? null;
|
||||
unset($data['classification_rules'], $data['item_ids']);
|
||||
|
||||
$process->update($data);
|
||||
|
||||
@@ -134,7 +140,12 @@ public function update(int $id, array $data)
|
||||
$this->syncClassificationRules($process, $rules);
|
||||
}
|
||||
|
||||
return $process->fresh('classificationRules');
|
||||
// 개별 품목 동기화 (전달된 경우만)
|
||||
if ($itemIds !== null) {
|
||||
$this->syncProcessItems($process, $itemIds);
|
||||
}
|
||||
|
||||
return $process->fresh(['classificationRules', 'processItems.item:id,code,name']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,7 +201,7 @@ public function toggleActive(int $id)
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return $process->fresh('classificationRules');
|
||||
return $process->fresh(['classificationRules', 'processItems.item:id,code,name']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +211,7 @@ private function generateProcessCode(int $tenantId): string
|
||||
{
|
||||
$lastProcess = Process::where('tenant_id', $tenantId)
|
||||
->withTrashed()
|
||||
->orderByRaw("CAST(SUBSTRING(process_code, 3) AS UNSIGNED) DESC")
|
||||
->orderByRaw('CAST(SUBSTRING(process_code, 3) AS UNSIGNED) DESC')
|
||||
->first();
|
||||
|
||||
if ($lastProcess && preg_match('/^P-(\d+)$/', $lastProcess->process_code, $matches)) {
|
||||
@@ -213,18 +224,18 @@ private function generateProcessCode(int $tenantId): string
|
||||
}
|
||||
|
||||
/**
|
||||
* 분류 규칙 동기화
|
||||
* 분류 규칙 동기화 (패턴 규칙용)
|
||||
*/
|
||||
private function syncClassificationRules(Process $process, array $rules): void
|
||||
{
|
||||
// 기존 규칙 삭제
|
||||
$process->classificationRules()->delete();
|
||||
|
||||
// 새 규칙 생성
|
||||
// 새 규칙 생성 (패턴 규칙만)
|
||||
foreach ($rules as $index => $rule) {
|
||||
ProcessClassificationRule::create([
|
||||
'process_id' => $process->id,
|
||||
'registration_type' => $rule['registration_type'] ?? 'pattern',
|
||||
'registration_type' => 'pattern',
|
||||
'rule_type' => $rule['rule_type'],
|
||||
'matching_type' => $rule['matching_type'],
|
||||
'condition_value' => $rule['condition_value'],
|
||||
@@ -235,6 +246,27 @@ private function syncClassificationRules(Process $process, array $rules): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 품목 연결 동기화
|
||||
*
|
||||
* @param array $itemIds 품목 ID 배열
|
||||
*/
|
||||
private function syncProcessItems(Process $process, array $itemIds): void
|
||||
{
|
||||
// 기존 연결 삭제
|
||||
$process->processItems()->delete();
|
||||
|
||||
// 새 연결 생성
|
||||
foreach ($itemIds as $index => $itemId) {
|
||||
ProcessItem::create([
|
||||
'process_id' => $process->id,
|
||||
'item_id' => $itemId,
|
||||
'priority' => $index,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 드롭다운용 공정 옵션 목록
|
||||
*/
|
||||
|
||||
175
app/Swagger/v1/ProcessApi.php
Normal file
175
app/Swagger/v1/ProcessApi.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(
|
||||
* name="Process",
|
||||
* description="공정 관리"
|
||||
* )
|
||||
*/
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="ProcessItem",
|
||||
* type="object",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="process_id", type="integer", example=1),
|
||||
* @OA\Property(property="item_id", type="integer", example=123),
|
||||
* @OA\Property(property="priority", type="integer", example=0),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="item", type="object",
|
||||
* @OA\Property(property="id", type="integer", example=123),
|
||||
* @OA\Property(property="code", type="string", example="ITEM-001"),
|
||||
* @OA\Property(property="name", type="string", example="품목명")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ProcessClassificationRule",
|
||||
* type="object",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="process_id", type="integer", example=1),
|
||||
* @OA\Property(property="registration_type", type="string", example="pattern"),
|
||||
* @OA\Property(property="rule_type", type="string", enum={"품목코드", "품목명", "품목구분"}),
|
||||
* @OA\Property(property="matching_type", type="string", enum={"startsWith", "endsWith", "contains", "equals"}),
|
||||
* @OA\Property(property="condition_value", type="string", example="SCR-"),
|
||||
* @OA\Property(property="priority", type="integer", example=0),
|
||||
* @OA\Property(property="description", type="string", nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Process",
|
||||
* type="object",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="process_code", type="string", example="P-001"),
|
||||
* @OA\Property(property="process_name", type="string", example="스크린 생산"),
|
||||
* @OA\Property(property="description", type="string", nullable=true),
|
||||
* @OA\Property(property="process_type", type="string", enum={"생산", "검사", "포장", "조립"}),
|
||||
* @OA\Property(property="department", type="string", nullable=true),
|
||||
* @OA\Property(property="work_log_template", type="string", nullable=true),
|
||||
* @OA\Property(property="required_workers", type="integer", example=1),
|
||||
* @OA\Property(property="equipment_info", type="string", nullable=true),
|
||||
* @OA\Property(property="work_steps", type="array", @OA\Items(type="string"), nullable=true),
|
||||
* @OA\Property(property="note", type="string", nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="classification_rules", type="array", @OA\Items(ref="#/components/schemas/ProcessClassificationRule")),
|
||||
* @OA\Property(property="process_items", type="array", @OA\Items(ref="#/components/schemas/ProcessItem"))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ProcessCreateRequest",
|
||||
* type="object",
|
||||
* required={"process_name", "process_type"},
|
||||
* @OA\Property(property="process_name", type="string", maxLength=100, example="스크린 생산"),
|
||||
* @OA\Property(property="description", type="string", nullable=true),
|
||||
* @OA\Property(property="process_type", type="string", enum={"생산", "검사", "포장", "조립"}),
|
||||
* @OA\Property(property="department", type="string", maxLength=100, nullable=true),
|
||||
* @OA\Property(property="work_log_template", type="string", maxLength=100, nullable=true),
|
||||
* @OA\Property(property="required_workers", type="integer", minimum=1, nullable=true),
|
||||
* @OA\Property(property="equipment_info", type="string", maxLength=255, nullable=true),
|
||||
* @OA\Property(property="work_steps", type="array", @OA\Items(type="string"), nullable=true),
|
||||
* @OA\Property(property="note", type="string", nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean", nullable=true),
|
||||
* @OA\Property(property="classification_rules", type="array", nullable=true,
|
||||
* description="패턴 기반 분류 규칙",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* required={"rule_type", "matching_type", "condition_value"},
|
||||
* @OA\Property(property="rule_type", type="string", enum={"품목코드", "품목명", "품목구분"}),
|
||||
* @OA\Property(property="matching_type", type="string", enum={"startsWith", "endsWith", "contains", "equals"}),
|
||||
* @OA\Property(property="condition_value", type="string", maxLength=255),
|
||||
* @OA\Property(property="priority", type="integer", minimum=0),
|
||||
* @OA\Property(property="description", type="string", maxLength=255, nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="item_ids", type="array", nullable=true,
|
||||
* description="개별 품목 ID 배열",
|
||||
* @OA\Items(type="integer", example=123)
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
class ProcessApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/processes",
|
||||
* tags={"Process"},
|
||||
* summary="공정 목록 조회",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", default=1)),
|
||||
* @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", default=20)),
|
||||
* @OA\Parameter(name="q", in="query", description="검색어", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="status", in="query", @OA\Schema(type="string", enum={"active", "inactive"})),
|
||||
* @OA\Parameter(name="process_type", in="query", @OA\Schema(type="string", enum={"생산", "검사", "포장", "조립"})),
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Process"))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/processes/{id}",
|
||||
* tags={"Process"},
|
||||
* 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="data", ref="#/components/schemas/Process")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/processes",
|
||||
* tags={"Process"},
|
||||
* summary="공정 생성",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
* @OA\RequestBody(required=true,
|
||||
* @OA\JsonContent(ref="#/components/schemas/ProcessCreateRequest")
|
||||
* ),
|
||||
* @OA\Response(response=201, description="생성됨",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="message.created"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Process")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/processes/{id}",
|
||||
* tags={"Process"},
|
||||
* 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/ProcessCreateRequest")
|
||||
* ),
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="message.updated"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Process")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
}
|
||||
@@ -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('process_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('process_id')->comment('공정 ID');
|
||||
$table->unsignedBigInteger('item_id')->comment('품목 ID');
|
||||
$table->unsignedInteger('priority')->default(0)->comment('우선순위');
|
||||
$table->boolean('is_active')->default(true)->comment('활성여부');
|
||||
$table->timestamps();
|
||||
|
||||
// 외래키
|
||||
$table->foreign('process_id')->references('id')->on('processes')->onDelete('cascade');
|
||||
$table->foreign('item_id')->references('id')->on('items')->onDelete('cascade');
|
||||
|
||||
// 유니크 제약 (동일 공정-품목 중복 방지)
|
||||
$table->unique(['process_id', 'item_id']);
|
||||
|
||||
// 인덱스
|
||||
$table->index('process_id');
|
||||
$table->index('item_id');
|
||||
$table->index(['process_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('process_items');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user