diff --git a/app/Http/Controllers/DashboardCalendarController.php b/app/Http/Controllers/DashboardCalendarController.php new file mode 100644 index 00000000..93ac94a9 --- /dev/null +++ b/app/Http/Controllers/DashboardCalendarController.php @@ -0,0 +1,171 @@ +input('year', now()->year); + $month = (int) $request->input('month', now()->month); + $tenantId = session('selected_tenant_id', 1); + + $firstDay = Carbon::create($year, $month, 1); + $lastDay = $firstDay->copy()->endOfMonth(); + $startOfWeek = $firstDay->copy()->startOfWeek(Carbon::SUNDAY); + $endOfWeek = $lastDay->copy()->endOfWeek(Carbon::SATURDAY); + + $calendarData = Schedule::forTenant($tenantId) + ->active() + ->betweenDates($startOfWeek->toDateString(), $endOfWeek->toDateString()) + ->orderBy('start_date') + ->orderBy('start_time') + ->get() + ->groupBy(fn ($s) => $s->start_date->format('Y-m-d')); + + $holidayMap = $this->getHolidayMap($tenantId, $year, $month); + + return view('dashboard.partials.calendar', compact( + 'year', 'month', 'calendarData', 'holidayMap' + )); + } + + /** + * 일정 등록 + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'start_time' => 'nullable|date_format:H:i', + 'end_time' => 'nullable|date_format:H:i', + 'is_all_day' => 'boolean', + 'type' => 'required|in:event,meeting,notice,other', + 'color' => 'nullable|string|max:20', + ]); + + $tenantId = session('selected_tenant_id', 1); + + $validated['tenant_id'] = $tenantId; + $validated['created_by'] = auth()->id(); + $validated['is_all_day'] = $validated['is_all_day'] ?? true; + + if (empty($validated['end_date'])) { + $validated['end_date'] = $validated['start_date']; + } + + $schedule = Schedule::create($validated); + + return response()->json([ + 'success' => true, + 'message' => '일정이 등록되었습니다.', + 'data' => $schedule, + ]); + } + + /** + * 일정 상세 (JSON) + */ + public function show(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + + $schedule = Schedule::forTenant($tenantId)->findOrFail($id); + + return response()->json([ + 'success' => true, + 'data' => $schedule, + ]); + } + + /** + * 일정 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'start_time' => 'nullable|date_format:H:i', + 'end_time' => 'nullable|date_format:H:i', + 'is_all_day' => 'boolean', + 'type' => 'required|in:event,meeting,notice,other', + 'color' => 'nullable|string|max:20', + ]); + + $tenantId = session('selected_tenant_id', 1); + $schedule = Schedule::forTenant($tenantId)->findOrFail($id); + + $validated['updated_by'] = auth()->id(); + $validated['is_all_day'] = $validated['is_all_day'] ?? true; + + if (empty($validated['end_date'])) { + $validated['end_date'] = $validated['start_date']; + } + + $schedule->update($validated); + + return response()->json([ + 'success' => true, + 'message' => '일정이 수정되었습니다.', + 'data' => $schedule->fresh(), + ]); + } + + /** + * 일정 삭제 + */ + public function destroy(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $schedule = Schedule::forTenant($tenantId)->findOrFail($id); + + $schedule->update(['deleted_by' => auth()->id()]); + $schedule->delete(); + + return response()->json([ + 'success' => true, + 'message' => '일정이 삭제되었습니다.', + ]); + } + + /** + * 해당 월의 휴일 맵 생성 (날짜 => 휴일명) + */ + private function getHolidayMap(int $tenantId, int $year, int $month): array + { + $startOfMonth = Carbon::create($year, $month, 1)->startOfWeek(Carbon::SUNDAY); + $endOfMonth = Carbon::create($year, $month, 1)->endOfMonth()->endOfWeek(Carbon::SATURDAY); + + $holidays = Holiday::forTenant($tenantId) + ->where('start_date', '<=', $endOfMonth->toDateString()) + ->where('end_date', '>=', $startOfMonth->toDateString()) + ->get(); + + $map = []; + foreach ($holidays as $holiday) { + $start = $holiday->start_date->copy(); + $end = $holiday->end_date->copy(); + for ($d = $start; $d->lte($end); $d->addDay()) { + $map[$d->format('Y-m-d')] = $holiday->name; + } + } + + return $map; + } +} diff --git a/app/Models/System/Schedule.php b/app/Models/System/Schedule.php new file mode 100644 index 00000000..faa0588f --- /dev/null +++ b/app/Models/System/Schedule.php @@ -0,0 +1,101 @@ + 'date', + 'end_date' => 'date', + 'is_all_day' => 'boolean', + 'is_recurring' => 'boolean', + 'is_active' => 'boolean', + ]; + + protected $attributes = [ + 'is_all_day' => true, + 'type' => 'event', + 'is_recurring' => false, + 'is_active' => true, + ]; + + public const TYPE_EVENT = 'event'; + public const TYPE_MEETING = 'meeting'; + public const TYPE_NOTICE = 'notice'; + public const TYPE_OTHER = 'other'; + + public const TYPES = [ + self::TYPE_EVENT => '일정', + self::TYPE_MEETING => '회의', + self::TYPE_NOTICE => '공지', + self::TYPE_OTHER => '기타', + ]; + + public const TYPE_COLORS = [ + self::TYPE_EVENT => ['bg' => 'bg-emerald-50', 'text' => 'text-emerald-700', 'border' => 'border-emerald-200'], + self::TYPE_MEETING => ['bg' => 'bg-blue-50', 'text' => 'text-blue-700', 'border' => 'border-blue-200'], + self::TYPE_NOTICE => ['bg' => 'bg-amber-50', 'text' => 'text-amber-700', 'border' => 'border-amber-200'], + self::TYPE_OTHER => ['bg' => 'bg-gray-50', 'text' => 'text-gray-700', 'border' => 'border-gray-200'], + ]; + + public function scopeForTenant(Builder $query, int $tenantId): Builder + { + return $query->where(function ($q) use ($tenantId) { + $q->where('tenant_id', $tenantId) + ->orWhereNull('tenant_id'); + }); + } + + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + + public function scopeBetweenDates(Builder $query, string $startDate, string $endDate): Builder + { + return $query->where(function ($q) use ($startDate, $endDate) { + $q->where('start_date', '<=', $endDate) + ->where(function ($inner) use ($startDate) { + $inner->where('end_date', '>=', $startDate) + ->orWhereNull('end_date'); + }); + }); + } + + public function getTypeLabelAttribute(): string + { + return self::TYPES[$this->type] ?? $this->type; + } + + public function getTypeColorsAttribute(): array + { + return self::TYPE_COLORS[$this->type] ?? self::TYPE_COLORS[self::TYPE_OTHER]; + } +} diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php index 079eaa3b..8ec7eac9 100644 --- a/resources/views/dashboard/index.blade.php +++ b/resources/views/dashboard/index.blade.php @@ -30,7 +30,7 @@ - +

사용자 관리

@@ -65,4 +65,305 @@
-@endsection \ No newline at end of file + + +
+
+
+ + + + + 달력을 불러오는 중... +
+
+
+ + + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/dashboard/partials/calendar.blade.php b/resources/views/dashboard/partials/calendar.blade.php new file mode 100644 index 00000000..bc643b62 --- /dev/null +++ b/resources/views/dashboard/partials/calendar.blade.php @@ -0,0 +1,152 @@ +{{-- 대시보드 달력 그리드 (HTMX로 로드) --}} +@php + use Carbon\Carbon; + use App\Models\System\Schedule; + + $firstDay = Carbon::create($year, $month, 1); + $lastDay = $firstDay->copy()->endOfMonth(); + $startOfWeek = $firstDay->copy()->startOfWeek(Carbon::SUNDAY); + $endOfWeek = $lastDay->copy()->endOfWeek(Carbon::SATURDAY); + + $today = Carbon::today(); + $prevMonth = $firstDay->copy()->subMonth(); + $nextMonth = $firstDay->copy()->addMonth(); +@endphp + +{{-- 달력 헤더 --}} +
+
+ + +

{{ $year }}년 {{ $month }}월

+ + + + @if(!$today->isSameMonth($firstDay)) + + @endif +
+ + +
+ +{{-- 달력 그리드 --}} +
+ + + + + + + + + + + + + + @php + $currentDate = $startOfWeek->copy(); + @endphp + + @while($currentDate <= $endOfWeek) + + @for($i = 0; $i < 7; $i++) + @php + $dateKey = $currentDate->format('Y-m-d'); + $isCurrentMonth = $currentDate->month === $month; + $isToday = $currentDate->isSameDay($today); + $isSunday = $currentDate->dayOfWeek === Carbon::SUNDAY; + $isSaturday = $currentDate->dayOfWeek === Carbon::SATURDAY; + $daySchedules = $calendarData[$dateKey] ?? collect(); + $holidayName = $holidayMap[$dateKey] ?? null; + $isHoliday = $holidayName !== null; + @endphp + + + + @php + $currentDate->addDay(); + @endphp + @endfor + + @endwhile + +
+
+ {{-- 날짜 헤더 --}} +
+
+ + {{ $currentDate->day }} + + @if($isHoliday && $isCurrentMonth) + {{ $holidayName }} + @endif +
+ + @if($isCurrentMonth) + + @endif +
+ + {{-- 일정 목록 --}} +
+ @foreach($daySchedules as $schedule) + @php + $colors = $schedule->type_colors; + @endphp + + @endforeach +
+
+
+
diff --git a/routes/web.php b/routes/web.php index 1efca4a2..798f13c7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -618,9 +618,19 @@ // 대시보드 Route::get('/dashboard', function () { + if (request()->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('dashboard')); + } return view('dashboard.index'); })->name('dashboard'); + // 대시보드 달력 + Route::get('/dashboard/calendar', [\App\Http\Controllers\DashboardCalendarController::class, 'calendar'])->name('dashboard.calendar'); + Route::post('/dashboard/schedules', [\App\Http\Controllers\DashboardCalendarController::class, 'store'])->name('dashboard.schedules.store'); + Route::get('/dashboard/schedules/{id}', [\App\Http\Controllers\DashboardCalendarController::class, 'show'])->name('dashboard.schedules.show'); + Route::put('/dashboard/schedules/{id}', [\App\Http\Controllers\DashboardCalendarController::class, 'update'])->name('dashboard.schedules.update'); + Route::delete('/dashboard/schedules/{id}', [\App\Http\Controllers\DashboardCalendarController::class, 'destroy'])->name('dashboard.schedules.destroy'); + // 루트 리다이렉트 Route::get('/', function () { return redirect()->route('dashboard');