diff --git a/app/Http/Controllers/Api/V1/CalendarScheduleController.php b/app/Http/Controllers/Api/V1/CalendarScheduleController.php new file mode 100644 index 0000000..2940683 --- /dev/null +++ b/app/Http/Controllers/Api/V1/CalendarScheduleController.php @@ -0,0 +1,133 @@ +validate([ + 'year' => 'required|integer|min:2000|max:2100', + 'type' => 'nullable|string', + ]); + + return ApiResponse::handle( + fn () => $this->service->list( + (int) $request->input('year'), + $request->input('type') + ), + __('message.fetched') + ); + } + + /** + * 통계 조회 + */ + public function stats(Request $request): JsonResponse + { + $request->validate([ + 'year' => 'required|integer|min:2000|max:2100', + ]); + + return ApiResponse::handle( + fn () => $this->service->stats((int) $request->input('year')), + __('message.fetched') + ); + } + + /** + * 단건 조회 + */ + public function show(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->show($id), + __('message.fetched') + ); + } + + /** + * 등록 + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'type' => 'required|string|in:public_holiday,temporary_holiday,substitute_holiday,tax_deadline,company_event', + 'is_recurring' => 'boolean', + 'memo' => 'nullable|string|max:500', + ]); + + return ApiResponse::handle( + fn () => $this->service->store($validated), + __('message.created') + ); + } + + /** + * 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'type' => 'required|string|in:public_holiday,temporary_holiday,substitute_holiday,tax_deadline,company_event', + 'is_recurring' => 'boolean', + 'memo' => 'nullable|string|max:500', + ]); + + return ApiResponse::handle( + fn () => $this->service->update($id, $validated), + __('message.updated') + ); + } + + /** + * 삭제 + */ + public function destroy(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->delete($id), + __('message.deleted') + ); + } + + /** + * 대량 등록 + */ + public function bulkStore(Request $request): JsonResponse + { + $validated = $request->validate([ + 'schedules' => 'required|array|min:1', + 'schedules.*.name' => 'required|string|max:100', + 'schedules.*.start_date' => 'required|date', + 'schedules.*.end_date' => 'required|date|after_or_equal:schedules.*.start_date', + 'schedules.*.type' => 'required|string|in:public_holiday,temporary_holiday,substitute_holiday,tax_deadline,company_event', + 'schedules.*.is_recurring' => 'boolean', + 'schedules.*.memo' => 'nullable|string|max:500', + ]); + + return ApiResponse::handle( + fn () => $this->service->bulkStore($validated['schedules']), + __('message.created') + ); + } +} diff --git a/app/Models/Commons/Holiday.php b/app/Models/Commons/Holiday.php new file mode 100644 index 0000000..216c8b7 --- /dev/null +++ b/app/Models/Commons/Holiday.php @@ -0,0 +1,41 @@ + 'date', + 'end_date' => 'date', + 'is_recurring' => 'boolean', + ]; + + public function scopeForTenant($query, int $tenantId) + { + return $query->where('tenant_id', $tenantId); + } + + public function scopeForYear($query, int $year) + { + return $query->whereYear('start_date', $year); + } +} diff --git a/app/Services/CalendarScheduleService.php b/app/Services/CalendarScheduleService.php new file mode 100644 index 0000000..a07a033 --- /dev/null +++ b/app/Services/CalendarScheduleService.php @@ -0,0 +1,180 @@ +tenantId()) + ->forYear($year) + ->orderBy('start_date'); + + if ($type) { + $query->where('type', $type); + } + + return $query->get()->map(function ($h) { + return [ + 'id' => $h->id, + 'name' => $h->name, + 'type' => $h->type, + 'start_date' => $h->start_date->format('Y-m-d'), + 'end_date' => $h->end_date->format('Y-m-d'), + 'days' => $h->start_date->diffInDays($h->end_date) + 1, + 'is_recurring' => $h->is_recurring, + 'memo' => $h->memo, + 'created_at' => $h->created_at?->toIso8601String(), + 'updated_at' => $h->updated_at?->toIso8601String(), + ]; + })->all(); + } + + /** + * 통계 조회 + */ + public function stats(int $year): array + { + $tenantId = $this->tenantId(); + $holidays = Holiday::forTenant($tenantId)->forYear($year)->get(); + + $totalDays = $holidays->sum(function ($h) { + return $h->start_date->diffInDays($h->end_date) + 1; + }); + + return [ + 'total_count' => $holidays->count(), + 'total_holiday_days' => $totalDays, + 'public_holiday_count' => $holidays->where('type', 'public_holiday')->count(), + ]; + } + + /** + * 단건 조회 + */ + public function show(int $id): array + { + $h = Holiday::forTenant($this->tenantId())->findOrFail($id); + + return [ + 'id' => $h->id, + 'name' => $h->name, + 'type' => $h->type, + 'start_date' => $h->start_date->format('Y-m-d'), + 'end_date' => $h->end_date->format('Y-m-d'), + 'days' => $h->start_date->diffInDays($h->end_date) + 1, + 'is_recurring' => $h->is_recurring, + 'memo' => $h->memo, + 'created_at' => $h->created_at?->toIso8601String(), + 'updated_at' => $h->updated_at?->toIso8601String(), + ]; + } + + /** + * 등록 + */ + public function store(array $data): array + { + $tenantId = $this->tenantId(); + + $exists = Holiday::forTenant($tenantId) + ->where('start_date', $data['start_date']) + ->where('end_date', $data['end_date']) + ->where('name', $data['name']) + ->exists(); + + if ($exists) { + throw new HttpException(422, __('error.duplicate')); + } + + $holiday = Holiday::create([ + 'tenant_id' => $tenantId, + 'start_date' => $data['start_date'], + 'end_date' => $data['end_date'], + 'name' => $data['name'], + 'type' => $data['type'] ?? 'public_holiday', + 'is_recurring' => $data['is_recurring'] ?? false, + 'memo' => $data['memo'] ?? null, + 'created_by' => $this->apiUserId(), + ]); + + return $this->show($holiday->id); + } + + /** + * 수정 + */ + public function update(int $id, array $data): array + { + $holiday = Holiday::forTenant($this->tenantId())->findOrFail($id); + + $holiday->update([ + 'start_date' => $data['start_date'], + 'end_date' => $data['end_date'], + 'name' => $data['name'], + 'type' => $data['type'], + 'is_recurring' => $data['is_recurring'] ?? false, + 'memo' => $data['memo'] ?? null, + 'updated_by' => $this->apiUserId(), + ]); + + return $this->show($id); + } + + /** + * 삭제 + */ + public function delete(int $id): void + { + $holiday = Holiday::forTenant($this->tenantId())->findOrFail($id); + $holiday->delete(); + } + + /** + * 대량 등록 + */ + public function bulkStore(array $schedules): array + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + $count = 0; + $skipped = 0; + + foreach ($schedules as $item) { + $exists = Holiday::forTenant($tenantId) + ->where('start_date', $item['start_date']) + ->where('end_date', $item['end_date']) + ->where('name', $item['name']) + ->exists(); + + if ($exists) { + $skipped++; + + continue; + } + + Holiday::create([ + 'tenant_id' => $tenantId, + 'start_date' => $item['start_date'], + 'end_date' => $item['end_date'], + 'name' => $item['name'], + 'type' => $item['type'] ?? 'public_holiday', + 'is_recurring' => $item['is_recurring'] ?? false, + 'memo' => $item['memo'] ?? null, + 'created_by' => $userId, + ]); + $count++; + } + + return [ + 'created' => $count, + 'skipped' => $skipped, + ]; + } +} diff --git a/routes/api/v1/hr.php b/routes/api/v1/hr.php index 4bcf9cf..0353a7e 100644 --- a/routes/api/v1/hr.php +++ b/routes/api/v1/hr.php @@ -16,6 +16,7 @@ use App\Http\Controllers\Api\V1\ApprovalFormController; use App\Http\Controllers\Api\V1\ApprovalLineController; use App\Http\Controllers\Api\V1\AttendanceController; +use App\Http\Controllers\Api\V1\CalendarScheduleController; use App\Http\Controllers\Api\V1\Construction\ContractController; use App\Http\Controllers\Api\V1\Construction\HandoverReportController; use App\Http\Controllers\Api\V1\Construction\StructureReviewController; @@ -213,3 +214,14 @@ Route::delete('/{id}', [StructureReviewController::class, 'destroy'])->whereNumber('id')->name('v1.construction.structure-reviews.destroy'); }); }); + +// Calendar Schedule API (달력 일정 관리) +Route::prefix('calendar-schedules')->group(function () { + Route::get('', [CalendarScheduleController::class, 'index'])->name('v1.calendar-schedules.index'); + Route::get('/stats', [CalendarScheduleController::class, 'stats'])->name('v1.calendar-schedules.stats'); + Route::post('', [CalendarScheduleController::class, 'store'])->name('v1.calendar-schedules.store'); + Route::post('/bulk', [CalendarScheduleController::class, 'bulkStore'])->name('v1.calendar-schedules.bulk'); + Route::get('/{id}', [CalendarScheduleController::class, 'show'])->whereNumber('id')->name('v1.calendar-schedules.show'); + Route::put('/{id}', [CalendarScheduleController::class, 'update'])->whereNumber('id')->name('v1.calendar-schedules.update'); + Route::delete('/{id}', [CalendarScheduleController::class, 'destroy'])->whereNumber('id')->name('v1.calendar-schedules.destroy'); +});