From 874bf97b8fa0e584ce93b3dceff24be894fe981b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 6 Feb 2026 20:06:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EC=88=98=EC=A3=BC=EA=B4=80=EB=A6=AC]?= =?UTF-8?q?=20order=5Fnodes=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1=20(N-depth=20=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - order_nodes 마이그레이션: 자기참조 parent_id, 고정코어(통계용) + options JSON(하이브리드) - order_items에 order_node_id nullable FK 추가 - OrderNode 모델: BelongsToTenant, Auditable, SoftDeletes, 트리 관계(parent/children) - Order 모델: nodes(), rootNodes() HasMany 관계 추가 - OrderItem 모델: order_node_id fillable + node() BelongsTo 관계 추가 Co-Authored-By: Claude Opus 4.6 --- app/Models/Orders/Order.php | 16 +++ app/Models/Orders/OrderItem.php | 9 ++ app/Models/Orders/OrderNode.php | 127 ++++++++++++++++++ ..._02_06_200000_create_order_nodes_table.php | 56 ++++++++ ...add_order_node_id_to_order_items_table.php | 27 ++++ 5 files changed, 235 insertions(+) create mode 100644 app/Models/Orders/OrderNode.php create mode 100644 database/migrations/2026_02_06_200000_create_order_nodes_table.php create mode 100644 database/migrations/2026_02_06_200001_add_order_node_id_to_order_items_table.php diff --git a/app/Models/Orders/Order.php b/app/Models/Orders/Order.php index 434c834..647ea4b 100644 --- a/app/Models/Orders/Order.php +++ b/app/Models/Orders/Order.php @@ -161,6 +161,22 @@ public function items(): HasMany return $this->hasMany(OrderItem::class)->orderBy('sort_order'); } + /** + * 수주 노드 (전체, depth/sort_order 순) + */ + public function nodes(): HasMany + { + return $this->hasMany(OrderNode::class)->orderBy('depth')->orderBy('sort_order'); + } + + /** + * 수주 루트 노드 (parent_id가 NULL인 최상위) + */ + public function rootNodes(): HasMany + { + return $this->hasMany(OrderNode::class)->whereNull('parent_id')->orderBy('sort_order'); + } + /** * 수주 이력 */ diff --git a/app/Models/Orders/OrderItem.php b/app/Models/Orders/OrderItem.php index 11932e7..7b1762c 100644 --- a/app/Models/Orders/OrderItem.php +++ b/app/Models/Orders/OrderItem.php @@ -26,6 +26,7 @@ class OrderItem extends Model protected $fillable = [ 'tenant_id', 'order_id', + 'order_node_id', 'quote_id', 'quote_item_id', 'serial_no', @@ -82,6 +83,14 @@ public function order(): BelongsTo return $this->belongsTo(Order::class); } + /** + * 수주 노드 (개소/구역/층 등) + */ + public function node(): BelongsTo + { + return $this->belongsTo(OrderNode::class, 'order_node_id'); + } + /** * 품목 마스터 */ diff --git a/app/Models/Orders/OrderNode.php b/app/Models/Orders/OrderNode.php new file mode 100644 index 0000000..5b967d7 --- /dev/null +++ b/app/Models/Orders/OrderNode.php @@ -0,0 +1,127 @@ + 'integer', + 'unit_price' => 'decimal:2', + 'total_price' => 'decimal:2', + 'options' => 'array', + 'depth' => 'integer', + 'sort_order' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + // ---- 트리 관계 ---- + + /** + * 상위 노드 + */ + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id'); + } + + /** + * 하위 노드 + */ + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); + } + + // ---- 비즈니스 관계 ---- + + /** + * 수주 마스터 + */ + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } + + /** + * 해당 노드에 속한 수주 품목 + */ + public function items(): HasMany + { + return $this->hasMany(OrderItem::class, 'order_node_id'); + } + + // ---- 트리 헬퍼 ---- + + public function isRoot(): bool + { + return $this->parent_id === null; + } + + public function isLeaf(): bool + { + return $this->children()->count() === 0; + } + + /** + * 하위 노드 포함 전체 트리 재귀 로드 + */ + public function scopeWithRecursiveChildren($query) + { + return $query->with(['children' => function ($q) { + $q->orderBy('sort_order')->with('children', 'items'); + }, 'items']); + } +} diff --git a/database/migrations/2026_02_06_200000_create_order_nodes_table.php b/database/migrations/2026_02_06_200000_create_order_nodes_table.php new file mode 100644 index 0000000..45f4788 --- /dev/null +++ b/database/migrations/2026_02_06_200000_create_order_nodes_table.php @@ -0,0 +1,56 @@ +id()->comment('ID'); + $table->foreignId('tenant_id')->comment('테넌트 ID'); + $table->foreignId('order_id')->comment('수주 ID'); + + // ---- 트리 구조 ---- + $table->foreignId('parent_id')->nullable()->comment('상위 노드 ID (NULL=루트)'); + + // ---- 고정 코어 (통계/집계용) ---- + $table->string('node_type', 50)->comment('노드 유형 (location, zone, floor, room, process...)'); + $table->string('code', 100)->comment('식별 코드'); + $table->string('name', 200)->comment('표시명'); + $table->string('status_code', 30)->default('PENDING') + ->comment('상태 (PENDING/CONFIRMED/IN_PRODUCTION/PRODUCED/SHIPPED/COMPLETED/CANCELLED)'); + $table->integer('quantity')->default(1)->comment('수량'); + $table->decimal('unit_price', 15, 2)->default(0)->comment('단가'); + $table->decimal('total_price', 15, 2)->default(0)->comment('합계'); + + // ---- 유연 확장 (유형별 상세) ---- + $table->json('options')->nullable()->comment('유형별 동적 속성 JSON'); + + // ---- 정렬 ---- + $table->integer('depth')->default(0)->comment('트리 깊이 (0=루트)'); + $table->integer('sort_order')->default(0)->comment('정렬 순서'); + + // ---- 감사 ---- + $table->foreignId('created_by')->nullable()->comment('생성자 ID'); + $table->foreignId('updated_by')->nullable()->comment('수정자 ID'); + $table->foreignId('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes(); + + // ---- 인덱스 ---- + $table->index('tenant_id'); + $table->index('parent_id'); + $table->index(['order_id', 'depth', 'sort_order']); + $table->index(['order_id', 'node_type']); + $table->index(['tenant_id', 'node_type', 'status_code']); // 통계용 + }); + } + + public function down(): void + { + Schema::dropIfExists('order_nodes'); + } +}; diff --git a/database/migrations/2026_02_06_200001_add_order_node_id_to_order_items_table.php b/database/migrations/2026_02_06_200001_add_order_node_id_to_order_items_table.php new file mode 100644 index 0000000..af42b23 --- /dev/null +++ b/database/migrations/2026_02_06_200001_add_order_node_id_to_order_items_table.php @@ -0,0 +1,27 @@ +foreignId('order_node_id') + ->nullable() + ->after('order_id') + ->comment('수주 노드 ID (order_nodes)'); + $table->index('order_node_id'); + }); + } + + public function down(): void + { + Schema::table('order_items', function (Blueprint $table) { + $table->dropIndex(['order_node_id']); + $table->dropColumn('order_node_id'); + }); + } +};