tenant($tenantId) ->with(['project:id,name', 'creator:id,name', 'entries']) ->withCount('entries') ->withTrashed(); // 날짜 범위 필터 if (! empty($filters['start_date']) || ! empty($filters['end_date'])) { $query->dateRange($filters['start_date'] ?? null, $filters['end_date'] ?? null); } // 프로젝트 필터 if (! empty($filters['project_id'])) { $query->project($filters['project_id']); } // 검색 필터 (요약 내용) if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('summary', 'like', "%{$search}%") ->orWhereHas('entries', function ($eq) use ($search) { $eq->where('content', 'like', "%{$search}%") ->orWhere('assignee_name', 'like', "%{$search}%"); }); }); } // Soft Delete 필터 if (isset($filters['trashed'])) { if ($filters['trashed'] === 'only') { $query->onlyTrashed(); } elseif ($filters['trashed'] === 'with') { $query->withTrashed(); } } // 정렬 (기본: 날짜 내림차순) $sortBy = $filters['sort_by'] ?? 'log_date'; $sortDirection = $filters['sort_direction'] ?? 'desc'; $query->orderBy($sortBy, $sortDirection); return $query->paginate($perPage); } /** * 특정 일일 로그 조회 */ public function getDailyLogById(int $id, bool $withTrashed = false): ?AdminPmDailyLog { $query = AdminPmDailyLog::query() ->with(['project:id,name', 'creator:id,name', 'updater:id,name', 'entries']); if ($withTrashed) { $query->withTrashed(); } return $query->find($id); } /** * 특정 날짜의 로그 조회 (또는 생성) */ public function getOrCreateByDate(int $tenantId, string $date, ?int $projectId = null): AdminPmDailyLog { $log = AdminPmDailyLog::query() ->tenant($tenantId) ->where('log_date', $date) ->where('project_id', $projectId) ->first(); if (! $log) { $log = AdminPmDailyLog::create([ 'tenant_id' => $tenantId, 'project_id' => $projectId, 'log_date' => $date, 'created_by' => auth()->id(), ]); } return $log->load('entries'); } /** * 일일 로그 생성 */ public function createDailyLog(int $tenantId, array $data): AdminPmDailyLog { return DB::transaction(function () use ($tenantId, $data) { $log = AdminPmDailyLog::create([ 'tenant_id' => $tenantId, 'project_id' => $data['project_id'] ?? null, 'log_date' => $data['log_date'], 'summary' => $data['summary'] ?? null, 'created_by' => auth()->id(), ]); // 항목이 있으면 함께 생성 if (! empty($data['entries'])) { $this->syncEntries($log, $data['entries']); } return $log->load('entries'); }); } /** * 일일 로그 수정 */ public function updateDailyLog(int $id, array $data): AdminPmDailyLog { return DB::transaction(function () use ($id, $data) { $log = AdminPmDailyLog::findOrFail($id); $log->update([ 'log_date' => $data['log_date'] ?? $log->log_date, 'project_id' => $data['project_id'] ?? $log->project_id, 'summary' => $data['summary'] ?? $log->summary, 'updated_by' => auth()->id(), ]); // 항목 동기화 if (isset($data['entries'])) { $this->syncEntries($log, $data['entries']); } return $log->fresh('entries'); }); } /** * 항목 동기화 (bulk upsert/delete) */ protected function syncEntries(AdminPmDailyLog $log, array $entries): void { $existingIds = $log->entries()->pluck('id')->toArray(); $incomingIds = []; foreach ($entries as $index => $entryData) { if (! empty($entryData['id'])) { // 기존 항목 업데이트 $entry = AdminPmDailyLogEntry::find($entryData['id']); if ($entry && $entry->daily_log_id === $log->id) { $entry->update([ 'assignee_type' => $entryData['assignee_type'] ?? $entry->assignee_type, 'assignee_id' => $entryData['assignee_id'] ?? null, 'assignee_name' => $entryData['assignee_name'], 'content' => $entryData['content'], 'status' => $entryData['status'] ?? $entry->status, 'sort_order' => $index, ]); $incomingIds[] = $entry->id; } } else { // 새 항목 생성 $entry = $log->entries()->create([ 'assignee_type' => $entryData['assignee_type'] ?? 'user', 'assignee_id' => $entryData['assignee_id'] ?? null, 'assignee_name' => $entryData['assignee_name'], 'content' => $entryData['content'], 'status' => $entryData['status'] ?? 'todo', 'sort_order' => $index, ]); $incomingIds[] = $entry->id; } } // 제거된 항목 삭제 $toDelete = array_diff($existingIds, $incomingIds); if (! empty($toDelete)) { AdminPmDailyLogEntry::whereIn('id', $toDelete)->delete(); } } /** * 일일 로그 삭제 (Soft Delete) */ public function deleteDailyLog(int $id): bool { $log = AdminPmDailyLog::findOrFail($id); $log->deleted_by = auth()->id(); $log->save(); return $log->delete(); } /** * 일일 로그 복원 */ public function restoreDailyLog(int $id): bool { $log = AdminPmDailyLog::onlyTrashed()->findOrFail($id); $log->deleted_by = null; return $log->restore(); } /** * 일일 로그 영구 삭제 */ public function forceDeleteDailyLog(int $id): bool { $log = AdminPmDailyLog::withTrashed()->findOrFail($id); return $log->forceDelete(); } /** * 항목 수정 */ public function updateEntry(int $entryId, array $data): AdminPmDailyLogEntry { $entry = AdminPmDailyLogEntry::findOrFail($entryId); $entry->update($data); return $entry->fresh(); } /** * 항목 상태 변경 */ public function updateEntryStatus(int $entryId, string $status): AdminPmDailyLogEntry { $entry = AdminPmDailyLogEntry::findOrFail($entryId); $entry->update(['status' => $status]); return $entry; } /** * 항목 추가 */ public function addEntry(int $logId, array $data): AdminPmDailyLogEntry { $log = AdminPmDailyLog::findOrFail($logId); // 마지막 정렬 순서 가져오기 $maxOrder = $log->entries()->max('sort_order') ?? -1; return $log->entries()->create([ 'assignee_type' => $data['assignee_type'] ?? 'user', 'assignee_id' => $data['assignee_id'] ?? null, 'assignee_name' => $data['assignee_name'], 'content' => $data['content'], 'status' => $data['status'] ?? 'todo', 'sort_order' => $maxOrder + 1, ]); } /** * 항목 삭제 */ public function deleteEntry(int $entryId): bool { return AdminPmDailyLogEntry::findOrFail($entryId)->delete(); } /** * 항목 순서 변경 */ public function reorderEntries(int $logId, array $entryIds): void { foreach ($entryIds as $index => $entryId) { AdminPmDailyLogEntry::where('id', $entryId) ->where('daily_log_id', $logId) ->update(['sort_order' => $index]); } } /** * 통계 조회 */ public function getStats(int $tenantId, ?int $projectId = null): array { $query = AdminPmDailyLog::query()->tenant($tenantId); if ($projectId) { $query->project($projectId); } $totalLogs = $query->count(); // 최근 7일 로그 수 $recentLogs = (clone $query) ->where('log_date', '>=', now()->subDays(7)->format('Y-m-d')) ->count(); // 전체 항목 통계 $entryStats = AdminPmDailyLogEntry::query() ->whereHas('dailyLog', function ($q) use ($tenantId, $projectId) { $q->tenant($tenantId); if ($projectId) { $q->project($projectId); } }) ->selectRaw(" COUNT(*) as total, SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END) as todo, SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress, SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done ") ->first(); return [ 'total_logs' => $totalLogs, 'recent_logs' => $recentLogs, 'entries' => [ 'total' => (int) ($entryStats->total ?? 0), 'todo' => (int) ($entryStats->todo ?? 0), 'in_progress' => (int) ($entryStats->in_progress ?? 0), 'done' => (int) ($entryStats->done ?? 0), ], ]; } /** * 주간 타임라인 데이터 조회 (최근 7일) */ public function getWeeklyTimeline(int $tenantId, ?int $projectId = null): array { $days = []; $today = now(); // 최근 7일 날짜 목록 생성 for ($i = 6; $i >= 0; $i--) { $date = $today->copy()->subDays($i); $days[$date->format('Y-m-d')] = [ 'date' => $date->format('Y-m-d'), 'day_name' => $date->locale('ko')->isoFormat('ddd'), 'day_number' => $date->format('d'), 'is_today' => $i === 0, 'is_weekend' => $date->isWeekend(), 'log' => null, ]; } // 해당 기간의 로그 조회 $startDate = $today->copy()->subDays(6)->format('Y-m-d'); $endDate = $today->format('Y-m-d'); $query = AdminPmDailyLog::query() ->tenant($tenantId) ->whereBetween('log_date', [$startDate, $endDate]) ->with(['entries' => function ($q) { $q->select('id', 'daily_log_id', 'status'); }]); if ($projectId) { $query->project($projectId); } $logs = $query->get(); // 로그 데이터 매핑 foreach ($logs as $log) { $dateKey = $log->log_date->format('Y-m-d'); if (isset($days[$dateKey])) { $entryStats = [ 'total' => $log->entries->count(), 'done' => $log->entries->where('status', 'done')->count(), 'in_progress' => $log->entries->where('status', 'in_progress')->count(), 'todo' => $log->entries->where('status', 'todo')->count(), ]; $days[$dateKey]['log'] = [ 'id' => $log->id, 'summary' => $log->summary, 'entry_stats' => $entryStats, 'completion_rate' => $entryStats['total'] > 0 ? round(($entryStats['done'] / $entryStats['total']) * 100) : 0, ]; } } return array_values($days); } /** * 담당자 목록 조회 (자동완성용) * - 일반 사용자는 슈퍼관리자 목록을 볼 수 없음 */ public function getAssigneeList(int $tenantId): array { // 사용자 목록 $query = User::query() ->whereHas('tenants', function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId); }) ->where('is_active', true); // 슈퍼관리자가 아니면 슈퍼관리자 목록 제외 if (! auth()->user()?->is_super_admin) { $query->where('is_super_admin', false); } $users = $query ->orderBy('name') ->get(['id', 'name']) ->map(fn ($u) => [ 'type' => 'user', 'id' => $u->id, 'name' => $u->name, ]) ->toArray(); // 팀 목록 (부서 활용) $teams = DB::table('departments') ->where('tenant_id', $tenantId) ->where('is_active', true) ->orderBy('name') ->get(['id', 'name']) ->map(fn ($d) => [ 'type' => 'team', 'id' => $d->id, 'name' => $d->name, ]) ->toArray(); return [ 'users' => $users, 'teams' => $teams, ]; } /** * 미완료 항목(예정, 진행중) 조회 - 담당자별 그룹핑, 날짜 오래된 순 정렬 */ public function getPendingEntries(int $tenantId, ?int $projectId = null, int $limit = 100): array { $query = AdminPmDailyLogEntry::query() ->select('admin_pm_daily_log_entries.*') ->with(['dailyLog:id,log_date,project_id', 'dailyLog.project:id,name']) ->join('admin_pm_daily_logs', 'admin_pm_daily_log_entries.daily_log_id', '=', 'admin_pm_daily_logs.id') ->where('admin_pm_daily_logs.tenant_id', $tenantId) ->when($projectId, fn ($q) => $q->where('admin_pm_daily_logs.project_id', $projectId)) ->whereIn('admin_pm_daily_log_entries.status', ['todo', 'in_progress']) ->orderBy('admin_pm_daily_logs.log_date', 'asc') // 날짜 오래된 순 ->orderBy('admin_pm_daily_log_entries.id', 'asc') ->limit($limit); $entries = $query->get(); // 담당자별로 그룹핑 $grouped = $entries->groupBy('assignee_name')->map(function ($items, $assigneeName) { $todoItems = $items->where('status', 'todo'); $inProgressItems = $items->where('status', 'in_progress'); // 항목들을 날짜 오래된 순으로 정렬 $sortedEntries = $items->sortBy(fn ($e) => $e->dailyLog?->log_date)->values(); return [ 'assignee_name' => $assigneeName, 'total_count' => $items->count(), 'todo_count' => $todoItems->count(), 'in_progress_count' => $inProgressItems->count(), 'oldest_date' => $sortedEntries->first()?->dailyLog?->log_date, // 카드 정렬용 'entries' => $sortedEntries->map(function ($entry) { return [ 'id' => $entry->id, 'daily_log_id' => $entry->daily_log_id, 'log_date' => $entry->dailyLog?->log_date, 'project_name' => $entry->dailyLog?->project?->name, 'content' => $entry->content, 'status' => $entry->status, ]; })->values()->toArray(), ]; }); // 담당자 카드도 가장 오래된 항목 날짜 기준으로 정렬 $sorted = $grouped->sortBy('oldest_date')->values()->toArray(); return $sorted; } }