feat: 공정 관리 API 개선 및 ProcessItem 추가

- ProcessItem 모델 및 마이그레이션 추가
- Process 요청/서비스 로직 수정
- Swagger API 문서 추가

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-08 20:23:52 +09:00
parent 26c071805a
commit 18aa745518
7 changed files with 338 additions and 17 deletions

View File

@@ -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'],
];
}

View File

@@ -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'],
];
}

View File

@@ -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');
}
}

View 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);
}
}

View File

@@ -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,
]);
}
}
/**
* 드롭다운용 공정 옵션 목록
*/

View 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() {}
}

View File

@@ -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');
}
};