tenantId(); $page = (int) ($params['page'] ?? 1); $size = (int) ($params['size'] ?? 20); $q = trim((string) ($params['q'] ?? '')); $pid = $params['parent_id'] ?? null; $onlyActive = $params['only_active'] ?? null; $query = Category::query()->where('tenant_id', $tenantId); if ($q !== '') { $query->where(function ($qq) use ($q) { $qq->where('name', 'like', "%{$q}%") ->orWhere('code', 'like', "%{$q}%"); }); } if ($pid !== null) { $query->where('parent_id', (int) $pid); } if ($onlyActive !== null) { $query->where('is_active', (int) (bool) $onlyActive); } $query->orderBy('parent_id')->orderBy('sort_order')->orderBy('id'); return $query->paginate($size, ['*'], 'page', $page); } /** 단건 */ public function show(int $id) { $tenantId = $this->tenantId(); $cat = Category::where('tenant_id', $tenantId)->find($id); if (! $cat) { throw new NotFoundHttpException(__('error.not_found')); } return $cat; } /** 생성 */ public function store(array $params) { $tenantId = $this->tenantId(); $uid = $this->apiUserId(); $v = Validator::make($params, [ 'parent_id' => 'nullable|integer|min:1', 'code' => 'nullable|string|max:50', 'name' => 'required|string|max:100', 'description' => 'nullable|string|max:255', 'is_active' => 'nullable|boolean', 'sort_order' => 'nullable|integer|min:0', ]); if ($v->fails()) { throw new BadRequestHttpException($v->errors()->first()); } $data = $v->validated(); $data['tenant_id'] = $tenantId; $data['created_by'] = $uid; $data['is_active'] = (int) ($data['is_active'] ?? 1); $data['sort_order'] = $data['sort_order'] ?? 0; return Category::create($data); } /** 수정 */ public function update(int $id, array $params) { $tenantId = $this->tenantId(); $uid = $this->apiUserId(); $cat = Category::where('tenant_id', $tenantId)->find($id); if (! $cat) { throw new NotFoundHttpException(__('error.not_found')); } $v = Validator::make($params, [ 'parent_id' => 'nullable|integer|min:1', 'code' => 'nullable|string|max:50', 'name' => 'sometimes|required|string|max:100', 'description' => 'nullable|string|max:255', 'is_active' => 'nullable|boolean', 'sort_order' => 'nullable|integer|min:0', ]); if ($v->fails()) { throw new BadRequestHttpException($v->errors()->first()); } $payload = $v->validated(); $payload['updated_by'] = $uid; $cat->update($payload); return $cat->refresh(); } /** 삭제(soft) */ public function destroy(int $id) { $tenantId = $this->tenantId(); $uid = $this->apiUserId(); $cat = Category::where('tenant_id', $tenantId)->find($id); if (! $cat) { throw new NotFoundHttpException(__('error.not_found')); } // (옵션) 하위 존재 검사 $hasChild = Category::where('tenant_id', $tenantId)->where('parent_id', $id)->exists(); if ($hasChild) { throw new BadRequestHttpException(__('error.child_exists')); } $cat->deleted_by = $uid; $cat->save(); $cat->delete(); return 'success'; } /** 활성/비활성 토글 */ public function toggle(int $id) { $tenantId = $this->tenantId(); $cat = Category::where('tenant_id', $tenantId)->find($id); if (! $cat) { throw new NotFoundHttpException(__('error.not_found')); } $cat->is_active = $cat->is_active ? 0 : 1; $cat->save(); return $cat->refresh(); } /** 부모/순서 이동 */ public function move(int $id, array $params) { $tenantId = $this->tenantId(); $v = Validator::make($params, [ 'parent_id' => 'nullable|integer|min:1', 'sort_order' => 'nullable|integer|min:0', ]); if ($v->fails()) { throw new BadRequestHttpException($v->errors()->first()); } $cat = Category::where('tenant_id', $tenantId)->find($id); if (! $cat) { throw new NotFoundHttpException(__('error.not_found')); } $payload = $v->validated(); $cat->update($payload); return $cat->refresh(); } /** 정렬순서 일괄 변경: [{id, sort_order}] */ public function reorder(array $params) { $tenantId = $this->tenantId(); $items = $params['items'] ?? null; if (! is_array($items) || empty($items)) { throw new BadRequestHttpException(__('validation.required', ['attribute' => 'items'])); } foreach ($items as $row) { if (! isset($row['id'])) { continue; } Category::where('tenant_id', $tenantId) ->where('id', (int) $row['id']) ->update(['sort_order' => (int) ($row['sort_order'] ?? 0)]); } return 'success'; } /** 트리 조회 (parent_id=null 기준 전체) */ public function tree(array $params) { $tenantId = $this->tenantId(); $onlyActive = (bool) ($params['only_active'] ?? false); $q = Category::where('tenant_id', $tenantId) ->when($onlyActive, fn ($qq) => $qq->where('is_active', 1)) ->orderBy('parent_id')->orderBy('sort_order')->orderBy('id') ->get(['id', 'parent_id', 'code', 'name', 'is_active', 'sort_order']); $byParent = []; foreach ($q as $c) { $byParent[$c->parent_id ?? 0][] = $c; } $build = function ($pid) use (&$build, &$byParent) { $nodes = $byParent[$pid] ?? []; return array_map(function ($n) use ($build) { return [ 'id' => $n->id, 'code' => $n->code, 'name' => $n->name, 'is_active' => (int) $n->is_active, 'sort_order' => (int) $n->sort_order, 'children' => $build($n->id), ]; }, $nodes); }; return $build(0); } }