diff --git a/app/Http/Requests/V1/Process/StoreProcessRequest.php b/app/Http/Requests/V1/Process/StoreProcessRequest.php index 22d5df6..1f2d4d3 100644 --- a/app/Http/Requests/V1/Process/StoreProcessRequest.php +++ b/app/Http/Requests/V1/Process/StoreProcessRequest.php @@ -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'], ]; } diff --git a/app/Http/Requests/V1/Process/UpdateProcessRequest.php b/app/Http/Requests/V1/Process/UpdateProcessRequest.php index 2099516..9d103d1 100644 --- a/app/Http/Requests/V1/Process/UpdateProcessRequest.php +++ b/app/Http/Requests/V1/Process/UpdateProcessRequest.php @@ -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'], ]; } diff --git a/app/Models/Process.php b/app/Models/Process.php index 679b5e1..7eecf78 100644 --- a/app/Models/Process.php +++ b/app/Models/Process.php @@ -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'); + } } diff --git a/app/Models/ProcessItem.php b/app/Models/ProcessItem.php new file mode 100644 index 0000000..ffdc0b3 --- /dev/null +++ b/app/Models/ProcessItem.php @@ -0,0 +1,44 @@ + 'boolean', + 'priority' => 'integer', + ]; + + /** + * 공정 + */ + public function process(): BelongsTo + { + return $this->belongsTo(Process::class); + } + + /** + * 품목 + */ + public function item(): BelongsTo + { + return $this->belongsTo(Item::class); + } +} diff --git a/app/Services/ProcessService.php b/app/Services/ProcessService.php index af24aae..34fec91 100644 --- a/app/Services/ProcessService.php +++ b/app/Services/ProcessService.php @@ -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, + ]); + } + } + /** * 드롭다운용 공정 옵션 목록 */ diff --git a/app/Swagger/v1/ProcessApi.php b/app/Swagger/v1/ProcessApi.php new file mode 100644 index 0000000..3186223 --- /dev/null +++ b/app/Swagger/v1/ProcessApi.php @@ -0,0 +1,175 @@ +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'); + } +}; \ No newline at end of file