where('year', (int) $params['year']); } if (! empty($params['quarter'])) { $query->where('quarter', (int) $params['quarter']); } if (! empty($params['type'])) { $query->where('type', $params['type']); } $query->orderByDesc('year')->orderByDesc('quarter'); $perPage = (int) ($params['per_page'] ?? 20); $paginated = $query->paginate($perPage); $items = $paginated->getCollection()->map(fn ($checklist) => $this->transformListItem($checklist)); return [ 'items' => $items, 'current_page' => $paginated->currentPage(), 'last_page' => $paginated->lastPage(), 'per_page' => $paginated->perPage(), 'total' => $paginated->total(), ]; } /** * 점검표 생성 (카테고리+항목 일괄) */ public function store(array $data): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 중복 체크 $exists = AuditChecklist::where('year', $data['year']) ->where('quarter', $data['quarter']) ->where('type', $data['type'] ?? AuditChecklist::TYPE_STANDARD_MANUAL) ->exists(); if ($exists) { throw new BadRequestHttpException(__('error.duplicate', ['attribute' => '해당 분기 점검표'])); } return DB::transaction(function () use ($data, $tenantId, $userId) { $checklist = AuditChecklist::create([ 'tenant_id' => $tenantId, 'year' => $data['year'], 'quarter' => $data['quarter'], 'type' => $data['type'] ?? AuditChecklist::TYPE_STANDARD_MANUAL, 'status' => AuditChecklist::STATUS_DRAFT, 'created_by' => $userId, 'updated_by' => $userId, ]); $this->syncCategories($checklist, $data['categories'], $tenantId); return $this->show($checklist->id); }); } /** * 점검표 상세 (카테고리→항목→문서 중첩) */ public function show(int $id): array { $checklist = AuditChecklist::with([ 'categories.items.standardDocuments.document', ])->findOrFail($id); return $this->transformDetail($checklist); } /** * 점검표 수정 */ public function update(int $id, array $data): array { $tenantId = $this->tenantId(); $checklist = AuditChecklist::findOrFail($id); if ($checklist->isCompleted()) { throw new BadRequestHttpException('완료된 점검표는 수정할 수 없습니다.'); } return DB::transaction(function () use ($checklist, $data, $tenantId) { $checklist->update([ 'updated_by' => $this->apiUserId(), ]); if (isset($data['categories'])) { $this->syncCategories($checklist, $data['categories'], $tenantId); } return $this->show($checklist->id); }); } /** * 점검표 완료 처리 */ public function complete(int $id): array { $checklist = AuditChecklist::with('categories.items')->findOrFail($id); // 미완료 항목 확인 $totalItems = 0; $completedItems = 0; foreach ($checklist->categories as $category) { foreach ($category->items as $item) { $totalItems++; if ($item->is_completed) { $completedItems++; } } } if ($completedItems < $totalItems) { throw new BadRequestHttpException("미완료 항목이 있습니다. ({$completedItems}/{$totalItems})"); } $checklist->update([ 'status' => AuditChecklist::STATUS_COMPLETED, 'updated_by' => $this->apiUserId(), ]); return $this->show($checklist->id); } /** * 항목 완료/미완료 토글 */ public function toggleItem(int $itemId): array { $item = AuditChecklistItem::findOrFail($itemId); $userId = $this->apiUserId(); DB::transaction(function () use ($item, $userId) { $item->lockForUpdate(); $newCompleted = ! $item->is_completed; $item->update([ 'is_completed' => $newCompleted, 'completed_at' => $newCompleted ? now() : null, 'completed_by' => $newCompleted ? $userId : null, ]); // 점검표 상태 자동 업데이트: draft → in_progress $category = $item->category; $checklist = $category->checklist; if ($checklist->isDraft()) { $checklist->update([ 'status' => AuditChecklist::STATUS_IN_PROGRESS, 'updated_by' => $userId, ]); } }); $item->refresh(); return [ 'id' => (string) $item->id, 'name' => $item->name, 'is_completed' => $item->is_completed, 'completed_at' => $item->completed_at?->toIso8601String(), ]; } /** * 항목별 기준 문서 조회 */ public function itemDocuments(int $itemId): array { $item = AuditChecklistItem::findOrFail($itemId); return $item->standardDocuments()->with('document')->get() ->map(fn ($doc) => $this->transformStandardDocument($doc)) ->all(); } /** * 기준 문서 연결 */ public function attachDocument(int $itemId, array $data): array { $item = AuditChecklistItem::findOrFail($itemId); $tenantId = $this->tenantId(); $doc = AuditStandardDocument::create([ 'tenant_id' => $tenantId, 'checklist_item_id' => $item->id, 'title' => $data['title'], 'version' => $data['version'] ?? null, 'date' => $data['date'] ?? null, 'document_id' => $data['document_id'] ?? null, ]); $doc->load('document'); return $this->transformStandardDocument($doc); } /** * 기준 문서 연결 해제 */ public function detachDocument(int $itemId, int $docId): void { $doc = AuditStandardDocument::where('checklist_item_id', $itemId) ->where('id', $docId) ->firstOrFail(); $doc->delete(); } // ===== Private: Sync & Transform ===== private function syncCategories(AuditChecklist $checklist, array $categoriesData, int $tenantId): void { // 기존 카테고리 ID 추적 (삭제 감지용) $existingCategoryIds = $checklist->categories()->pluck('id')->all(); $keptCategoryIds = []; foreach ($categoriesData as $catIdx => $catData) { if (! empty($catData['id'])) { // 기존 카테고리 업데이트 $category = AuditChecklistCategory::findOrFail($catData['id']); $category->update([ 'title' => $catData['title'], 'sort_order' => $catData['sort_order'] ?? $catIdx, ]); $keptCategoryIds[] = $category->id; } else { // 새 카테고리 생성 $category = AuditChecklistCategory::create([ 'tenant_id' => $tenantId, 'checklist_id' => $checklist->id, 'title' => $catData['title'], 'sort_order' => $catData['sort_order'] ?? $catIdx, ]); $keptCategoryIds[] = $category->id; } // 하위 항목 동기화 $this->syncItems($category, $catData['items'] ?? [], $tenantId); } // 삭제된 카테고리 제거 (cascade로 items도 삭제) $deletedIds = array_diff($existingCategoryIds, $keptCategoryIds); if (! empty($deletedIds)) { AuditChecklistCategory::whereIn('id', $deletedIds)->delete(); } } private function syncItems(AuditChecklistCategory $category, array $itemsData, int $tenantId): void { $existingItemIds = $category->items()->pluck('id')->all(); $keptItemIds = []; foreach ($itemsData as $itemIdx => $itemData) { if (! empty($itemData['id'])) { $item = AuditChecklistItem::findOrFail($itemData['id']); $item->update([ 'name' => $itemData['name'], 'description' => $itemData['description'] ?? null, 'sort_order' => $itemData['sort_order'] ?? $itemIdx, ]); $keptItemIds[] = $item->id; } else { $item = AuditChecklistItem::create([ 'tenant_id' => $tenantId, 'category_id' => $category->id, 'name' => $itemData['name'], 'description' => $itemData['description'] ?? null, 'sort_order' => $itemData['sort_order'] ?? $itemIdx, ]); $keptItemIds[] = $item->id; } } $deletedIds = array_diff($existingItemIds, $keptItemIds); if (! empty($deletedIds)) { AuditChecklistItem::whereIn('id', $deletedIds)->delete(); } } private function transformListItem(AuditChecklist $checklist): array { $total = 0; $completed = 0; foreach ($checklist->categories as $category) { foreach ($category->items as $item) { $total++; if ($item->is_completed) { $completed++; } } } return [ 'id' => (string) $checklist->id, 'year' => $checklist->year, 'quarter' => $checklist->quarter, 'type' => $checklist->type, 'status' => $checklist->status, 'progress' => [ 'completed' => $completed, 'total' => $total, ], ]; } private function transformDetail(AuditChecklist $checklist): array { $total = 0; $completed = 0; $categories = $checklist->categories->map(function ($category) use (&$total, &$completed) { $subItems = $category->items->map(function ($item) use (&$total, &$completed) { $total++; if ($item->is_completed) { $completed++; } return [ 'id' => (string) $item->id, 'name' => $item->name, 'description' => $item->description, 'is_completed' => $item->is_completed, 'completed_at' => $item->completed_at?->toIso8601String(), 'sort_order' => $item->sort_order, 'standard_documents' => $item->standardDocuments->map( fn ($doc) => $this->transformStandardDocument($doc) )->all(), ]; })->all(); return [ 'id' => (string) $category->id, 'title' => $category->title, 'sort_order' => $category->sort_order, 'sub_items' => $subItems, ]; })->all(); return [ 'id' => (string) $checklist->id, 'year' => $checklist->year, 'quarter' => $checklist->quarter, 'type' => $checklist->type, 'status' => $checklist->status, 'progress' => [ 'completed' => $completed, 'total' => $total, ], 'categories' => $categories, ]; } private function transformStandardDocument(AuditStandardDocument $doc): array { $file = $doc->document; return [ 'id' => (string) $doc->id, 'title' => $doc->title, 'version' => $doc->version ?? '-', 'date' => $doc->date?->toDateString() ?? '', 'file_name' => $file?->original_name ?? null, 'file_url' => $file ? "/api/v1/documents/{$file->id}/download" : null, ]; } }