onlyTrashed(); } elseif ($filters['trashed'] === 'with') { $query->withTrashed(); } } // 검색 필터 if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('title', 'like', "%{$search}%") ->orWhere('counterparty', 'like', "%{$search}%") ->orWhere('description', 'like', "%{$search}%"); }); } // 일정 유형 필터 if (! empty($filters['schedule_type'])) { $query->where('schedule_type', $filters['schedule_type']); } // 상태 필터 if (! empty($filters['status'])) { $query->where('status', $filters['status']); } // 날짜 범위 필터 if (! empty($filters['start_date']) && ! empty($filters['end_date'])) { $query->dateBetween($filters['start_date'], $filters['end_date']); } elseif (! empty($filters['start_date'])) { $query->where('scheduled_date', '>=', $filters['start_date']); } elseif (! empty($filters['end_date'])) { $query->where('scheduled_date', '<=', $filters['end_date']); } // 월별 필터 if (! empty($filters['year']) && ! empty($filters['month'])) { $query->forMonth((int) $filters['year'], (int) $filters['month']); } // 카테고리 필터 if (! empty($filters['category'])) { $query->where('category', $filters['category']); } return $query ->with('bankAccount:id,bank_name,account_number') ->orderBy('scheduled_date') ->orderBy('id') ->paginate($perPage); } /** * 월별 일정 조회 (캘린더용) */ public function getSchedulesForMonth(int $year, int $month): Collection { return FundSchedule::forMonth($year, $month) ->with('bankAccount:id,bank_name,account_number') ->ordered() ->get(); } /** * 특정 날짜의 일정 조회 */ public function getSchedulesForDate(string $date): Collection { return FundSchedule::where('scheduled_date', $date) ->with('bankAccount:id,bank_name,account_number') ->ordered() ->get(); } /** * 예정된 일정 조회 (향후 N일) */ public function getUpcomingSchedules(int $days = 30): Collection { $startDate = now()->toDateString(); $endDate = now()->addDays($days)->toDateString(); return FundSchedule::pending() ->dateBetween($startDate, $endDate) ->with('bankAccount:id,bank_name,account_number') ->ordered() ->get(); } // ========================================================================= // 일정 CRUD // ========================================================================= /** * 일정 상세 조회 */ public function getScheduleById(int $id, bool $withTrashed = false): ?FundSchedule { $query = FundSchedule::query(); if ($withTrashed) { $query->withTrashed(); } return $query->with('bankAccount')->find($id); } /** * 일정 생성 */ public function createSchedule(array $data): FundSchedule { $data['created_by'] = auth()->id(); $data['tenant_id'] = $data['tenant_id'] ?? session('selected_tenant_id') ?? auth()->user()?->tenant_id; return FundSchedule::create($data); } /** * 일정 수정 */ public function updateSchedule(FundSchedule $schedule, array $data): FundSchedule { $data['updated_by'] = auth()->id(); $schedule->update($data); return $schedule->fresh(); } /** * 일정 삭제 (Soft Delete) */ public function deleteSchedule(FundSchedule $schedule): bool { $schedule->deleted_by = auth()->id(); $schedule->save(); return $schedule->delete(); } /** * 일정 복원 */ public function restoreSchedule(FundSchedule $schedule): bool { $schedule->deleted_by = null; return $schedule->restore(); } /** * 일정 영구 삭제 */ public function forceDeleteSchedule(FundSchedule $schedule): bool { return $schedule->forceDelete(); } // ========================================================================= // 상태 변경 // ========================================================================= /** * 완료 처리 */ public function markAsCompleted(FundSchedule $schedule, ?float $actualAmount = null, ?string $completedDate = null): FundSchedule { $schedule->markAsCompleted($actualAmount, $completedDate); return $schedule->fresh(); } /** * 취소 처리 */ public function markAsCancelled(FundSchedule $schedule): FundSchedule { $schedule->markAsCancelled(); return $schedule->fresh(); } /** * 상태 변경 */ public function updateStatus(FundSchedule $schedule, string $status): FundSchedule { $schedule->update([ 'status' => $status, 'updated_by' => auth()->id(), ]); return $schedule->fresh(); } // ========================================================================= // 월별 복사 // ========================================================================= /** * 원본 월의 일정을 대상 월로 복사 */ public function copySchedulesToMonth(int $sourceYear, int $sourceMonth, int $targetYear, int $targetMonth): int { $schedules = $this->getSchedulesForMonth($sourceYear, $sourceMonth); if ($schedules->isEmpty()) { return 0; } $targetLastDay = cal_days_in_month(CAL_GREGORIAN, $targetMonth, $targetYear); $copiedCount = 0; foreach ($schedules as $schedule) { $sourceDay = $schedule->scheduled_date->day; $targetDay = min($sourceDay, $targetLastDay); $targetDate = sprintf('%04d-%02d-%02d', $targetYear, $targetMonth, $targetDay); $this->createSchedule([ 'title' => $schedule->title, 'description' => $schedule->description, 'schedule_type' => $schedule->schedule_type, 'scheduled_date' => $targetDate, 'amount' => $schedule->amount, 'currency' => $schedule->currency, 'related_bank_account_id' => $schedule->related_bank_account_id, 'counterparty' => $schedule->counterparty, 'category' => $schedule->category, 'status' => FundSchedule::STATUS_PENDING, 'is_recurring' => $schedule->is_recurring, 'recurrence_rule' => $schedule->recurrence_rule, 'recurrence_end_date' => $schedule->recurrence_end_date, 'memo' => $schedule->memo, ]); $copiedCount++; } return $copiedCount; } // ========================================================================= // 일괄 작업 // ========================================================================= /** * 일괄 삭제 */ public function bulkDelete(array $ids): int { return FundSchedule::whereIn('id', $ids) ->update([ 'deleted_by' => auth()->id(), 'deleted_at' => now(), ]); } /** * 일괄 상태 변경 */ public function bulkUpdateStatus(array $ids, string $status): int { return FundSchedule::whereIn('id', $ids) ->update([ 'status' => $status, 'updated_by' => auth()->id(), ]); } // ========================================================================= // 요약 및 통계 // ========================================================================= /** * 월별 요약 통계 */ public function getMonthlySummary(int $year, int $month): array { $schedules = FundSchedule::forMonth($year, $month)->get(); $incomeSchedules = $schedules->where('schedule_type', FundSchedule::TYPE_INCOME); $expenseSchedules = $schedules->where('schedule_type', FundSchedule::TYPE_EXPENSE); return [ 'year' => $year, 'month' => $month, 'total_count' => $schedules->count(), 'income' => [ 'count' => $incomeSchedules->count(), 'total' => $incomeSchedules->sum('amount'), 'pending' => $incomeSchedules->where('status', FundSchedule::STATUS_PENDING)->sum('amount'), 'completed' => $incomeSchedules->where('status', FundSchedule::STATUS_COMPLETED)->sum(fn ($s) => $s->completed_amount ?: $s->amount), ], 'expense' => [ 'count' => $expenseSchedules->count(), 'total' => $expenseSchedules->sum('amount'), 'pending' => $expenseSchedules->where('status', FundSchedule::STATUS_PENDING)->sum('amount'), 'completed' => $expenseSchedules->where('status', FundSchedule::STATUS_COMPLETED)->sum(fn ($s) => $s->completed_amount ?: $s->amount), ], 'net' => $incomeSchedules->sum('amount') - $expenseSchedules->sum('amount'), ]; } /** * 캘린더용 데이터 구조화 */ public function getCalendarData(int $year, int $month): array { $schedules = $this->getSchedulesForMonth($year, $month); // 날짜별로 그룹화 $groupedByDate = $schedules->groupBy(function ($schedule) { return $schedule->scheduled_date->format('Y-m-d'); }); return $groupedByDate->toArray(); } /** * 전체 요약 통계 */ public function getSummary(): array { $pending = FundSchedule::pending()->get(); return [ 'pending_count' => $pending->count(), 'pending_income' => $pending->where('schedule_type', FundSchedule::TYPE_INCOME)->sum('amount'), 'pending_expense' => $pending->where('schedule_type', FundSchedule::TYPE_EXPENSE)->sum('amount'), 'upcoming_7days' => FundSchedule::pending() ->dateBetween(now()->toDateString(), now()->addDays(7)->toDateString()) ->count(), ]; } }