diff --git a/app/Http/Controllers/Api/V1/Design/BomTemplateController.php b/app/Http/Controllers/Api/V1/Design/BomTemplateController.php new file mode 100644 index 0000000..cdc79e8 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Design/BomTemplateController.php @@ -0,0 +1,63 @@ +service->paginate($versionId, page: 1, size: PHP_INT_MAX)->items(); + }, __('message.fetched')); + } + + public function upsertTemplate(Request $request, int $versionId) + { + return ApiResponse::handle(function () use ($request, $versionId) { + $payload = $request->validate([ + 'name' => 'nullable|string|max:100', + 'is_primary' => 'boolean', + 'notes' => 'nullable|string', + ]); + return $this->service->upsertTemplate( + modelVersionId: $versionId, + name: $payload['name'] ?? 'Main', + isPrimary: (bool)($payload['is_primary'] ?? true), + notes: $payload['notes'] ?? null + ); + }, __('message.created')); + } + + public function show(int $templateId) + { + return ApiResponse::handle(fn() => $this->service->show($templateId, true), __('message.fetched')); + } + + public function replaceItems(Request $request, int $templateId) + { + return ApiResponse::handle(function () use ($request, $templateId) { + $payload = $request->validate([ + 'items' => 'required|array|min:1', + 'items.*.ref_type' => 'required|string|in:MATERIAL,PRODUCT', + 'items.*.ref_id' => 'required|integer|min:1', + 'items.*.qty' => 'required|numeric|gt:0', + 'items.*.waste_rate' => 'nullable|numeric|min:0', + 'items.*.uom_id' => 'nullable|integer|min:1', + 'items.*.notes' => 'nullable|string|max:255', + 'items.*.sort_order' => 'nullable|integer', + ]); + $this->service->replaceItems($templateId, $payload['items']); + return null; + }, __('message.bom.bulk_upsert')); + } +} diff --git a/app/Http/Controllers/Api/V1/Design/DesignModelController.php b/app/Http/Controllers/Api/V1/Design/DesignModelController.php new file mode 100644 index 0000000..2c16a31 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Design/DesignModelController.php @@ -0,0 +1,68 @@ +query('q', ''); + $page = (int) $request->query('page', 1); + $size = (int) $request->query('size', 20); + return $this->service->list($q, $page, $size); + }, __('message.fetched')); + } + + public function store(Request $request) + { + return ApiResponse::handle(function () use ($request) { + $payload = $request->validate([ + 'code' => 'required|string|max:100', + 'name' => 'required|string|max:200', + 'category_id' => 'nullable|integer', + 'lifecycle' => 'nullable|string|max:30', + 'description' => 'nullable|string', + 'is_active' => 'boolean', + ]); + return $this->service->create($payload); + }, __('message.created')); + } + + public function show(int $id) + { + return ApiResponse::handle(fn() => $this->service->find($id), __('message.fetched')); + } + + public function update(Request $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + $payload = $request->validate([ + 'code' => 'sometimes|string|max:100', + 'name' => 'sometimes|string|max:200', + 'category_id' => 'nullable|integer', + 'lifecycle' => 'nullable|string|max:30', + 'description' => 'nullable|string', + 'is_active' => 'boolean', + ]); + return $this->service->update($id, $payload); + }, __('message.updated')); + } + + public function destroy(int $id) + { + return ApiResponse::handle(function () use ($id) { + $this->service->delete($id); + return null; + }, __('message.deleted')); + } +} diff --git a/app/Http/Controllers/Api/V1/Design/ModelVersionController.php b/app/Http/Controllers/Api/V1/Design/ModelVersionController.php new file mode 100644 index 0000000..b5ef49f --- /dev/null +++ b/app/Http/Controllers/Api/V1/Design/ModelVersionController.php @@ -0,0 +1,38 @@ + $this->service->listByModel($modelId), __('message.fetched')); + } + + public function createDraft(Request $request, int $modelId) + { + return ApiResponse::handle(function () use ($request, $modelId) { + $payload = $request->validate([ + 'version_no' => 'nullable|integer|min:1', + 'notes' => 'nullable|string', + 'effective_from' => 'nullable|date', + 'effective_to' => 'nullable|date|after:effective_from', + ]); + return $this->service->createDraft($modelId, $payload); + }, __('message.created')); + } + + public function release(int $versionId) + { + return ApiResponse::handle(fn() => $this->service->release($versionId), __('message.updated')); + } +} diff --git a/app/Models/Design/BomTemplate.php b/app/Models/Design/BomTemplate.php new file mode 100644 index 0000000..45a944f --- /dev/null +++ b/app/Models/Design/BomTemplate.php @@ -0,0 +1,29 @@ + 'boolean', + ]; + + public function modelVersion() { + return $this->belongsTo(ModelVersion::class, 'model_version_id'); + } + + public function items() { + return $this->hasMany(BomTemplateItem::class, 'bom_template_id'); + } +} diff --git a/app/Models/Design/BomTemplateItem.php b/app/Models/Design/BomTemplateItem.php new file mode 100644 index 0000000..fbfba7f --- /dev/null +++ b/app/Models/Design/BomTemplateItem.php @@ -0,0 +1,23 @@ + 'decimal:6', + 'waste_rate' => 'decimal:6', + ]; + + public function template() { + return $this->belongsTo(BomTemplate::class, 'bom_template_id'); + } +} diff --git a/app/Models/Design/DesignModel.php b/app/Models/Design/DesignModel.php new file mode 100644 index 0000000..0a1a325 --- /dev/null +++ b/app/Models/Design/DesignModel.php @@ -0,0 +1,26 @@ + 'boolean', + ]; + + // 관계: 모델은 여러 버전을 가짐 + public function versions() { + return $this->hasMany(ModelVersion::class, 'model_id'); + } +} diff --git a/app/Models/Design/ModelVersion.php b/app/Models/Design/ModelVersion.php new file mode 100644 index 0000000..b58ff7d --- /dev/null +++ b/app/Models/Design/ModelVersion.php @@ -0,0 +1,35 @@ + 'boolean', + 'effective_from' => 'datetime', + 'effective_to' => 'datetime', + ]; + + public function model() { + return $this->belongsTo(DesignModel::class, 'model_id'); + } + + public function bomTemplates() { + return $this->hasMany(BomTemplate::class, 'model_version_id'); + } + + public function scopeReleased($q) { + return $q->where('status', 'RELEASED'); + } +} diff --git a/app/Services/Design/BomTemplateService.php b/app/Services/Design/BomTemplateService.php new file mode 100644 index 0000000..b934340 --- /dev/null +++ b/app/Services/Design/BomTemplateService.php @@ -0,0 +1,221 @@ +tenantId(); + + $q = BomTemplate::query()->where('tenant_id', $tenantId); + + if ($modelVersionId) { + $q->where('model_version_id', $modelVersionId); + } + + return $q->orderByDesc('is_primary') + ->orderBy('name') + ->paginate($size, ['*'], 'page', $page); + } + + /** 템플릿 upsert(name 기준) */ + public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool $isPrimary = true, ?string $notes = null): BomTemplate + { + $tenantId = $this->tenantId(); + + $mv = ModelVersion::query() + ->where('tenant_id', $tenantId) + ->find($modelVersionId); + + if (!$mv) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return DB::transaction(function () use ($tenantId, $mv, $name, $isPrimary, $notes) { + $tpl = BomTemplate::query() + ->where('model_version_id', $mv->id) + ->where('name', $name) + ->first(); + + if (!$tpl) { + $tpl = BomTemplate::create([ + 'tenant_id' => $tenantId, + 'model_version_id' => $mv->id, + 'name' => $name, + 'is_primary' => $isPrimary, + 'notes' => $notes, + ]); + } else { + $tpl->fill(['is_primary' => $isPrimary, 'notes' => $notes])->save(); + } + + if ($isPrimary) { + BomTemplate::query() + ->where('model_version_id', $mv->id) + ->where('id', '<>', $tpl->id) + ->update(['is_primary' => false]); + } + + return $tpl; + }); + } + + /** 템플릿 메타 수정 */ + public function updateTemplate(int $templateId, array $data): BomTemplate + { + $tenantId = $this->tenantId(); + + $tpl = BomTemplate::query() + ->where('tenant_id', $tenantId) + ->find($templateId); + + if (!$tpl) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $name = $data['name'] ?? $tpl->name; + if ($name !== $tpl->name) { + $dup = BomTemplate::query() + ->where('model_version_id', $tpl->model_version_id) + ->where('name', $name) + ->where('id', '<>', $tpl->id) + ->exists(); + if ($dup) { + throw ValidationException::withMessages(['name' => __('error.duplicate')]); + } + } + + $tpl->fill($data)->save(); + + if (array_key_exists('is_primary', $data) && $data['is_primary']) { + // 다른 템플릿 대표 해제 + BomTemplate::query() + ->where('model_version_id', $tpl->model_version_id) + ->where('id', '<>', $tpl->id) + ->update(['is_primary' => false]); + } + + return $tpl; + } + + /** 템플릿 삭제(soft) */ + public function deleteTemplate(int $templateId): void + { + $tenantId = $this->tenantId(); + + $tpl = BomTemplate::query() + ->where('tenant_id', $tenantId) + ->find($templateId); + + if (!$tpl) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $tpl->delete(); + } + + /** 템플릿 상세 (옵션: 항목 포함) */ + public function show(int $templateId, bool $withItems = false): BomTemplate + { + $tenantId = $this->tenantId(); + + $q = BomTemplate::query()->where('tenant_id', $tenantId); + if ($withItems) { + $q->with(['items' => fn($w) => $w->orderBy('sort_order')]); + } + + $tpl = $q->find($templateId); + if (!$tpl) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return $tpl; + } + + /** 항목 일괄 치환 */ + public function replaceItems(int $templateId, array $items): void + { + $tenantId = $this->tenantId(); + + $tpl = BomTemplate::query() + ->where('tenant_id', $tenantId) + ->find($templateId); + + if (!$tpl) { + throw new NotFoundHttpException(__('error.not_found')); + } + + // 1차 검증 + foreach ($items as $i => $row) { + $refType = strtoupper((string) Arr::get($row, 'ref_type')); + $qty = (float) Arr::get($row, 'qty', 0); + if (!in_array($refType, ['MATERIAL','PRODUCT'], true)) { + throw ValidationException::withMessages(["items.$i.ref_type" => __('error.validation_failed')]); + } + if ($qty <= 0) { + throw ValidationException::withMessages(["items.$i.qty" => __('error.validation_failed')]); + } + } + + DB::transaction(function () use ($tenantId, $tpl, $items) { + BomTemplateItem::query() + ->where('tenant_id', $tenantId) + ->where('bom_template_id', $tpl->id) + ->delete(); + + $now = now(); + $payloads = []; + foreach ($items as $row) { + $payloads[] = [ + 'tenant_id' => $tenantId, + 'bom_template_id' => $tpl->id, + 'ref_type' => strtoupper($row['ref_type']), + 'ref_id' => (int) $row['ref_id'], + 'qty' => (string) ($row['qty'] ?? 1), + 'waste_rate' => (string) ($row['waste_rate'] ?? 0), + 'uom_id' => $row['uom_id'] ?? null, + 'notes' => $row['notes'] ?? null, + 'sort_order' => (int) ($row['sort_order'] ?? 0), + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + if (!empty($payloads)) { + BomTemplateItem::insert($payloads); + } + }); + } + + /** 항목 조회 */ + public function listItems(int $templateId) + { + $tenantId = $this->tenantId(); + + $tpl = BomTemplate::query() + ->where('tenant_id', $tenantId) + ->find($templateId); + + if (!$tpl) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return BomTemplateItem::query() + ->where('tenant_id', $tenantId) + ->where('bom_template_id', $tpl->id) + ->orderBy('sort_order') + ->get(); + } +} diff --git a/app/Services/Design/ModelService.php b/app/Services/Design/ModelService.php new file mode 100644 index 0000000..27b4e8c --- /dev/null +++ b/app/Services/Design/ModelService.php @@ -0,0 +1,112 @@ +tenantId(); + + $query = DesignModel::query()->where('tenant_id', $tenantId); + + if ($q !== '') { + $query->where(function ($w) use ($q) { + $w->where('code', 'like', "%{$q}%") + ->orWhere('name', 'like', "%{$q}%") + ->orWhere('description', 'like', "%{$q}%"); + }); + } + + return $query->orderByDesc('id')->paginate($size, ['*'], 'page', $page); + } + + /** 생성 */ + public function create(array $data): DesignModel + { + $tenantId = $this->tenantId(); + + $exists = DesignModel::query() + ->where('tenant_id', $tenantId) + ->where('code', $data['code']) + ->exists(); + + if ($exists) { + throw ValidationException::withMessages(['code' => __('error.duplicate')]); + } + + return DB::transaction(function () use ($tenantId, $data) { + $payload = array_merge($data, ['tenant_id' => $tenantId]); + return DesignModel::create($payload); + }); + } + + /** 단건 */ + public function find(int $id): DesignModel + { + $tenantId = $this->tenantId(); + $model = DesignModel::query() + ->where('tenant_id', $tenantId) + ->with(['versions' => fn($q) => $q->orderBy('version_no')]) + ->find($id); + + if (!$model) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return $model; + } + + /** 수정 */ + public function update(int $id, array $data): DesignModel + { + $tenantId = $this->tenantId(); + + $model = DesignModel::query() + ->where('tenant_id', $tenantId) + ->find($id); + + if (!$model) { + throw new NotFoundHttpException(__('error.not_found')); + } + + if (isset($data['code']) && $data['code'] !== $model->code) { + $dup = DesignModel::query() + ->where('tenant_id', $tenantId) + ->where('code', $data['code']) + ->exists(); + if ($dup) { + throw ValidationException::withMessages(['code' => __('error.duplicate')]); + } + } + + $model->fill($data); + $model->save(); + + return $model; + } + + /** 삭제(soft) */ + public function delete(int $id): void + { + $tenantId = $this->tenantId(); + + $model = DesignModel::query() + ->where('tenant_id', $tenantId) + ->find($id); + + if (!$model) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $model->delete(); + } +} diff --git a/app/Services/Design/ModelVersionService.php b/app/Services/Design/ModelVersionService.php new file mode 100644 index 0000000..116f719 --- /dev/null +++ b/app/Services/Design/ModelVersionService.php @@ -0,0 +1,102 @@ +tenantId(); + + $model = DesignModel::query() + ->where('tenant_id', $tenantId) + ->find($modelId); + + if (!$model) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return ModelVersion::query() + ->where('tenant_id', $tenantId) + ->where('model_id', $modelId) + ->orderBy('version_no') + ->get(); + } + + /** DRAFT 생성 (version_no 자동/수동) */ + public function createDraft(int $modelId, array $extra = []): ModelVersion + { + $tenantId = $this->tenantId(); + + $model = DesignModel::query() + ->where('tenant_id', $tenantId) + ->find($modelId); + + if (!$model) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return DB::transaction(function () use ($tenantId, $model, $extra) { + $versionNo = $extra['version_no'] ?? null; + + if ($versionNo === null) { + $max = ModelVersion::query() + ->where('model_id', $model->id) + ->max('version_no'); + $versionNo = (int)($max ?? 0) + 1; + } else { + $exists = ModelVersion::query() + ->where('model_id', $model->id) + ->where('version_no', $versionNo) + ->exists(); + if ($exists) { + throw ValidationException::withMessages(['version_no' => __('error.duplicate')]); + } + } + + return ModelVersion::create([ + 'tenant_id' => $tenantId, + 'model_id' => $model->id, + 'version_no' => $versionNo, + 'status' => 'DRAFT', + 'is_active' => true, + 'notes' => $extra['notes'] ?? null, + 'effective_from' => $extra['effective_from'] ?? null, + 'effective_to' => $extra['effective_to'] ?? null, + ]); + }); + } + + /** RELEASED 전환 */ + public function release(int $versionId): ModelVersion + { + $tenantId = $this->tenantId(); + + $mv = ModelVersion::query() + ->where('tenant_id', $tenantId) + ->find($versionId); + + if (!$mv) { + throw new NotFoundHttpException(__('error.not_found')); + } + + if ($mv->status === 'RELEASED') { + return $mv; // 멱등 + } + + // TODO: 대표 템플릿 존재 등 사전 검증 훅 가능 + $mv->status = 'RELEASED'; + $mv->effective_from = $mv->effective_from ?? now(); + $mv->save(); + + return $mv; + } +} diff --git a/app/Swagger/v1/DesignModelApi.php b/app/Swagger/v1/DesignModelApi.php new file mode 100644 index 0000000..dec54cb --- /dev/null +++ b/app/Swagger/v1/DesignModelApi.php @@ -0,0 +1,397 @@ +bigIncrements('id'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); + $table->string('code', 100)->comment('모델코드(설계코드)'); + $table->string('name', 200)->comment('모델명'); + $table->unsignedBigInteger('category_id')->nullable()->comment('카테고리ID(참조용, FK 미설정)'); + $table->string('lifecycle', 30)->nullable()->comment('PLANNING/ACTIVE/DEPRECATED 등'); + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['tenant_id', 'code'], 'uq_models_tenant_code'); + $table->index(['tenant_id', 'is_active'], 'idx_models_tenant_active'); + $table->index(['tenant_id', 'category_id'], 'idx_models_tenant_category'); + }); + } + + public function down(): void { + Schema::dropIfExists('models'); + } +}; diff --git a/database/migrations/2025_09_05_000002_create_model_versions_table.php b/database/migrations/2025_09_05_000002_create_model_versions_table.php new file mode 100644 index 0000000..dc22ae6 --- /dev/null +++ b/database/migrations/2025_09_05_000002_create_model_versions_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); + $table->unsignedBigInteger('model_id')->comment('모델ID'); + $table->integer('version_no')->comment('버전번호(1..N)'); + $table->string('status', 30)->default('DRAFT')->comment('DRAFT/RELEASED'); + $table->dateTime('effective_from')->nullable(); + $table->dateTime('effective_to')->nullable(); + $table->text('notes')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['model_id', 'version_no'], 'uq_model_versions_model_ver'); + $table->index(['tenant_id', 'status'], 'idx_mv_tenant_status'); + $table->index(['tenant_id', 'model_id'], 'idx_mv_tenant_model'); + }); + } + + public function down(): void { + Schema::dropIfExists('model_versions'); + } +}; diff --git a/database/migrations/2025_09_05_000003_create_bom_templates_table.php b/database/migrations/2025_09_05_000003_create_bom_templates_table.php new file mode 100644 index 0000000..428f3d8 --- /dev/null +++ b/database/migrations/2025_09_05_000003_create_bom_templates_table.php @@ -0,0 +1,29 @@ +bigIncrements('id'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); + $table->unsignedBigInteger('model_version_id')->comment('모델버전ID'); + $table->string('name', 100)->default('Main')->comment('템플릿명'); + $table->boolean('is_primary')->default(true)->comment('대표 템플릿 여부'); + $table->text('notes')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['model_version_id', 'name'], 'uq_bomtpl_mv_name'); + $table->index(['tenant_id', 'model_version_id'], 'idx_bomtpl_tenant_mv'); + $table->index(['tenant_id', 'is_primary'], 'idx_bomtpl_tenant_primary'); + }); + } + + public function down(): void { + Schema::dropIfExists('bom_templates'); + } +}; diff --git a/database/migrations/2025_09_05_000004_create_bom_template_items_table.php b/database/migrations/2025_09_05_000004_create_bom_template_items_table.php new file mode 100644 index 0000000..d2ee7ee --- /dev/null +++ b/database/migrations/2025_09_05_000004_create_bom_template_items_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); + $table->unsignedBigInteger('bom_template_id')->comment('BOM템플릿ID'); + $table->string('ref_type', 20)->comment('참조타입: MATERIAL|PRODUCT'); + $table->unsignedBigInteger('ref_id')->comment('참조ID(materials.id 또는 products.id)'); + $table->decimal('qty', 18, 6)->default(1)->comment('수량'); + $table->decimal('waste_rate', 9, 6)->default(0)->comment('로스율'); + $table->unsignedBigInteger('uom_id')->nullable()->comment('단위ID(참조용)'); + $table->string('notes', 255)->nullable(); + $table->integer('sort_order')->default(0); + $table->timestamps(); + + $table->index(['tenant_id', 'bom_template_id'], 'idx_bomtpl_items_tenant_tpl'); + $table->index(['tenant_id', 'ref_type', 'ref_id'], 'idx_bomtpl_items_tenant_ref'); + $table->index(['bom_template_id', 'sort_order'], 'idx_bomtpl_items_sort'); + }); + } + + public function down(): void { + Schema::dropIfExists('bom_template_items'); + } +}; diff --git a/public/tenant/product/bom.php b/public/tenant/product/bom.php new file mode 100644 index 0000000..cdbc9a7 --- /dev/null +++ b/public/tenant/product/bom.php @@ -0,0 +1,224 @@ + + +
+
+
+

BOM 템플릿 편집기

+
모델 ID: — 모델 버전 선택 후 구성 편집
+
+
+ 목록 + 모델 수정 + +
+
+ +
+
+
+ + +
+
+ +
+ + +
+
+
+
※ 그룹 = BOM 템플릿 내 상위 분류 (예: 본체/절곡물/모터/부자재)
+
+
+
+ +
+
+
+
+

BOM 그룹 (템플릿)

+ +
+
+
    + +
+
+
+
+
+
+
+

아이템 상세

+
+ + +
+
+
+
+ + + + + + + + + + + + + + +
구분참조코드명칭수량옵션/조건삭제
+
+
+ ※ 구분: PRODUCT(서브모델) / MATERIAL(자재) / PART(부품). 옵션/조건은 견적-선택값과 매핑(예: 가이드레일=벽면형). +
+
+
+
+
+
+ + + + + diff --git a/public/tenant/product/model.php b/public/tenant/product/model.php new file mode 100644 index 0000000..fceecf2 --- /dev/null +++ b/public/tenant/product/model.php @@ -0,0 +1,321 @@ + + + + + + + + 모델 등록 | SAM Prototype (Model-only) + + + + + +
+
+
+ +
+

모델 등록

+
모델(설계 뼈대) 메타데이터만 입력합니다. ※ BOM/속성은 별도 화면
+
+ MODEL-ONLY +
+
+ 목록 +
+
+
+ +
+
+
+
+
+
+
+

기본정보

+
+ + +
+
+
+
+
+
+ + +
모델 코드를 입력하세요.
+
+
+ + +
모델명을 입력하세요.
+
+
+ + +
기본 단위를 선택하세요.
+
+
+ +
+ +
+
+
+ +
+ + +
+ +
※ 모델이 속하는 분류(예: 제품>방화셔터>KSS) — 구성(BOM)과는 별개
+
카테고리를 선택하세요.
+
+
+ + +
+
+
+
+ +
+
+

이미지

+
대표 이미지 1장
+
+
+
+
+
여기로 드래그하거나 클릭하여 선택
+ +
+
+
+ +
+
+
+
+
+ +
+ + + +
+
+ +
+
+
+

미리보기

+
+
+
+
+ +
+
+
CODE
+
모델명 미입력
+
카테고리 미선택
+
+
+
+
+
※ 이 화면은 모델 메타데이터만 다룹니다. BOM 구성은 별도의 “BOM 템플릿 편집기”에서 작성하세요.
+
+
+
+
+
+
+ + + + + +
+ +
+ + + + + + + + diff --git a/public/tenant/product/model_list.php b/public/tenant/product/model_list.php index 89b845e..ce6f2d4 100644 --- a/public/tenant/product/model_list.php +++ b/public/tenant/product/model_list.php @@ -4,307 +4,179 @@ include '../inc/header.php'; ?>
- - - - - -
-
- - -
- -
- - -
- -
- - -
- -
총 0건
- -
- - -
- - - - - - - - - - - - -
NO분류모델명로트 코드등록일수정/삭제
-
- - -
- -
-
- - -