From 1577d028dcbeeff31356e46c65b305eb9b97cfd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 21 Mar 2026 15:23:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EA=B3=B5=EC=A0=95=EA=B4=80=EB=A6=AC]?= =?UTF-8?q?=20parent=5Fid=20=ED=8A=B8=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20=E2=80=94=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98,=20=EB=AA=A8=EB=8D=B8=20=EA=B4=80?= =?UTF-8?q?=EA=B3=84,=202depth=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V1/Process/StoreProcessRequest.php | 1 + .../V1/Process/UpdateProcessRequest.php | 3 ++ app/Models/Process.php | 19 +++++++++++ app/Services/ProcessService.php | 33 ++++++++++++++++-- ...52057_add_parent_id_to_processes_table.php | 34 +++++++++++++++++++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_03_21_152057_add_parent_id_to_processes_table.php diff --git a/app/Http/Requests/V1/Process/StoreProcessRequest.php b/app/Http/Requests/V1/Process/StoreProcessRequest.php index 68eda989..93e173d7 100644 --- a/app/Http/Requests/V1/Process/StoreProcessRequest.php +++ b/app/Http/Requests/V1/Process/StoreProcessRequest.php @@ -14,6 +14,7 @@ public function authorize(): bool public function rules(): array { return [ + 'parent_id' => ['nullable', 'integer', 'exists:processes,id'], 'process_name' => ['required', 'string', 'max:100'], 'description' => ['nullable', 'string'], 'process_type' => ['required', 'string', 'in:생산,검사,포장,조립'], diff --git a/app/Http/Requests/V1/Process/UpdateProcessRequest.php b/app/Http/Requests/V1/Process/UpdateProcessRequest.php index 36579148..041ae778 100644 --- a/app/Http/Requests/V1/Process/UpdateProcessRequest.php +++ b/app/Http/Requests/V1/Process/UpdateProcessRequest.php @@ -13,7 +13,10 @@ public function authorize(): bool public function rules(): array { + $processId = $this->route('id'); + return [ + 'parent_id' => ['nullable', 'integer', 'exists:processes,id', "not_in:{$processId}"], 'process_name' => ['sometimes', 'required', 'string', 'max:100'], 'description' => ['nullable', 'string'], 'process_type' => ['sometimes', 'required', 'string', 'in:생산,검사,포장,조립'], diff --git a/app/Models/Process.php b/app/Models/Process.php index 6dc1636f..070c69c0 100644 --- a/app/Models/Process.php +++ b/app/Models/Process.php @@ -21,6 +21,7 @@ class Process extends Model protected $fillable = [ 'tenant_id', + 'parent_id', 'process_code', 'process_name', 'description', @@ -47,6 +48,24 @@ class Process extends Model 'required_workers' => 'integer', ]; + /** 부모 공정 */ + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id'); + } + + /** 자식 공정 */ + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id')->orderBy('process_code'); + } + + /** 루트 공정만 조회 */ + public function scopeRoots($query) + { + return $query->whereNull('parent_id'); + } + /** * 중간검사 양식 */ diff --git a/app/Services/ProcessService.php b/app/Services/ProcessService.php index 8865cbe5..77ae1e9d 100644 --- a/app/Services/ProcessService.php +++ b/app/Services/ProcessService.php @@ -23,9 +23,11 @@ public function index(array $params) $status = $params['status'] ?? null; $processType = $params['process_type'] ?? null; + $eagerLoad = ['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category', 'parent:id,process_code,process_name', 'children:id,parent_id,process_code,process_name,is_active']; + $query = Process::query() ->where('tenant_id', $tenantId) - ->with(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); + ->with($eagerLoad); // 검색어 if ($q !== '') { @@ -62,7 +64,7 @@ public function show(int $id) $tenantId = $this->tenantId(); $process = Process::where('tenant_id', $tenantId) - ->with(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']) + ->with(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category', 'parent:id,process_code,process_name', 'children:id,parent_id,process_code,process_name,is_active']) ->find($id); if (! $process) { @@ -81,6 +83,16 @@ public function store(array $data) $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { + // 2depth 제한: 부모가 이미 자식이면 거부 + if (! empty($data['parent_id'])) { + $parent = Process::find($data['parent_id']); + if ($parent && $parent->parent_id) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'parent_id' => ['2단계까지만 허용됩니다. 선택한 부모 공정이 이미 하위 공정입니다.'], + ]); + } + } + // 공정코드 자동 생성 $data['process_code'] = $this->generateProcessCode($tenantId); $data['tenant_id'] = $tenantId; @@ -122,6 +134,22 @@ public function update(int $id, array $data) } return DB::transaction(function () use ($process, $data, $userId) { + // parent_id 변경 시 2depth + 순환 참조 검증 + if (array_key_exists('parent_id', $data) && $data['parent_id']) { + $parent = Process::find($data['parent_id']); + if ($parent && $parent->parent_id) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'parent_id' => ['2단계까지만 허용됩니다.'], + ]); + } + // 자기 자식을 부모로 설정하는 것 방지 + if ($process->children()->where('id', $data['parent_id'])->exists()) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'parent_id' => ['하위 공정을 부모로 설정할 수 없습니다.'], + ]); + } + } + $data['updated_by'] = $userId; // work_steps가 문자열이면 배열로 변환 @@ -288,6 +316,7 @@ public function duplicate(int $id) $newProcess = Process::create([ 'tenant_id' => $tenantId, + 'parent_id' => $source->parent_id, 'process_code' => $newCode, 'process_name' => $source->process_name.' (복사)', 'description' => $source->description, diff --git a/database/migrations/2026_03_21_152057_add_parent_id_to_processes_table.php b/database/migrations/2026_03_21_152057_add_parent_id_to_processes_table.php new file mode 100644 index 00000000..d45c97f4 --- /dev/null +++ b/database/migrations/2026_03_21_152057_add_parent_id_to_processes_table.php @@ -0,0 +1,34 @@ +unsignedBigInteger('parent_id') + ->nullable() + ->after('tenant_id') + ->comment('부모 공정 ID (NULL이면 루트)'); + + $table->foreign('parent_id') + ->references('id') + ->on('processes') + ->onDelete('set null'); + + $table->index(['tenant_id', 'parent_id']); + }); + } + + public function down(): void + { + Schema::table('processes', function (Blueprint $table) { + $table->dropForeign(['parent_id']); + $table->dropIndex(['tenant_id', 'parent_id']); + $table->dropColumn('parent_id'); + }); + } +};