From 5c0f92d74ab41571e9c4e377fd791b3f049f197b Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 26 Dec 2025 15:48:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20I-8=20=ED=9C=B4=EA=B0=80=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EA=B4=80=EB=A6=AC=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LeavePolicyController: 휴가 정책 CRUD API - LeavePolicyService: 정책 관리 로직 - LeavePolicy 모델: 다중 테넌트 지원 - FormRequest 검증 클래스 - Swagger 문서화 - leave_policies 테이블 마이그레이션 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Api/V1/LeavePolicyController.php | 41 ++++++ .../Requests/LeavePolicy/UpdateRequest.php | 41 ++++++ app/Models/Tenants/LeavePolicy.php | 127 ++++++++++++++++++ app/Services/LeavePolicyService.php | 70 ++++++++++ app/Swagger/v1/LeavePolicyApi.php | 109 +++++++++++++++ ..._26_100001_create_leave_policies_table.php | 47 +++++++ 6 files changed, 435 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/LeavePolicyController.php create mode 100644 app/Http/Requests/LeavePolicy/UpdateRequest.php create mode 100644 app/Models/Tenants/LeavePolicy.php create mode 100644 app/Services/LeavePolicyService.php create mode 100644 app/Swagger/v1/LeavePolicyApi.php create mode 100644 database/migrations/2025_12_26_100001_create_leave_policies_table.php diff --git a/app/Http/Controllers/Api/V1/LeavePolicyController.php b/app/Http/Controllers/Api/V1/LeavePolicyController.php new file mode 100644 index 0000000..a9be2c9 --- /dev/null +++ b/app/Http/Controllers/Api/V1/LeavePolicyController.php @@ -0,0 +1,41 @@ +service->show(); + }, __('message.fetched')); + } + + /** + * 휴가 정책 업데이트 + * PUT /v1/leave-policy + */ + public function update(UpdateRequest $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + return $this->service->update($request->validated()); + }, __('message.updated')); + } +} diff --git a/app/Http/Requests/LeavePolicy/UpdateRequest.php b/app/Http/Requests/LeavePolicy/UpdateRequest.php new file mode 100644 index 0000000..a5b6e35 --- /dev/null +++ b/app/Http/Requests/LeavePolicy/UpdateRequest.php @@ -0,0 +1,41 @@ + ['sometimes', Rule::in(LeavePolicy::STANDARD_TYPES)], + 'fiscal_start_month' => ['sometimes', 'integer', 'min:1', 'max:12'], + 'fiscal_start_day' => ['sometimes', 'integer', 'min:1', 'max:31'], + 'default_annual_leave' => ['sometimes', 'integer', 'min:0', 'max:100'], + 'additional_leave_per_year' => ['sometimes', 'integer', 'min:0', 'max:10'], + 'max_annual_leave' => ['sometimes', 'integer', 'min:0', 'max:100'], + 'carry_over_enabled' => ['sometimes', 'boolean'], + 'carry_over_max_days' => ['sometimes', 'integer', 'min:0', 'max:100'], + 'carry_over_expiry_months' => ['sometimes', 'integer', 'min:0', 'max:24'], + ]; + } + + public function messages(): array + { + return [ + 'standard_type.in' => __('validation.in', ['attribute' => '기준 유형']), + 'fiscal_start_month.min' => __('validation.min.numeric', ['attribute' => '기준월', 'min' => 1]), + 'fiscal_start_month.max' => __('validation.max.numeric', ['attribute' => '기준월', 'max' => 12]), + 'fiscal_start_day.min' => __('validation.min.numeric', ['attribute' => '기준일', 'min' => 1]), + 'fiscal_start_day.max' => __('validation.max.numeric', ['attribute' => '기준일', 'max' => 31]), + ]; + } +} diff --git a/app/Models/Tenants/LeavePolicy.php b/app/Models/Tenants/LeavePolicy.php new file mode 100644 index 0000000..f65f48d --- /dev/null +++ b/app/Models/Tenants/LeavePolicy.php @@ -0,0 +1,127 @@ + 'integer', + 'fiscal_start_day' => 'integer', + 'default_annual_leave' => 'integer', + 'additional_leave_per_year' => 'integer', + 'max_annual_leave' => 'integer', + 'carry_over_enabled' => 'boolean', + 'carry_over_max_days' => 'integer', + 'carry_over_expiry_months' => 'integer', + ]; + + // ========================================================================= + // 상수 정의 + // ========================================================================= + + public const TYPE_FISCAL = 'fiscal'; + + public const TYPE_HIRE = 'hire'; + + public const STANDARD_TYPES = [ + self::TYPE_FISCAL, + self::TYPE_HIRE, + ]; + + // ========================================================================= + // 관계 정의 + // ========================================================================= + + /** + * 테넌트 + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + // ========================================================================= + // 헬퍼 메서드 + // ========================================================================= + + /** + * 기준 유형 라벨 + */ + public function getStandardTypeLabelAttribute(): string + { + return match ($this->standard_type) { + self::TYPE_FISCAL => '회계연도', + self::TYPE_HIRE => '입사일', + default => $this->standard_type, + }; + } + + /** + * 기준일 문자열 + */ + public function getFiscalStartDateStringAttribute(): string + { + return "{$this->fiscal_start_month}월 {$this->fiscal_start_day}일"; + } + + /** + * 기본 설정 생성 + */ + public static function createDefault(int $tenantId, ?int $userId = null): self + { + return self::create([ + 'tenant_id' => $tenantId, + 'standard_type' => self::TYPE_FISCAL, + 'fiscal_start_month' => 1, + 'fiscal_start_day' => 1, + 'default_annual_leave' => 15, + 'additional_leave_per_year' => 1, + 'max_annual_leave' => 25, + 'carry_over_enabled' => true, + 'carry_over_max_days' => 10, + 'carry_over_expiry_months' => 3, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + } +} diff --git a/app/Services/LeavePolicyService.php b/app/Services/LeavePolicyService.php new file mode 100644 index 0000000..3f6dc25 --- /dev/null +++ b/app/Services/LeavePolicyService.php @@ -0,0 +1,70 @@ +tenantId(); + + $policy = LeavePolicy::where('tenant_id', $tenantId)->first(); + + if (! $policy) { + $policy = LeavePolicy::createDefault($tenantId, $this->apiUserId()); + } + + return $policy; + } + + /** + * 휴가 정책 업데이트 + * 없으면 생성 + */ + public function update(array $data): LeavePolicy + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $policy = LeavePolicy::where('tenant_id', $tenantId)->first(); + + if ($policy) { + $policy->fill([ + 'standard_type' => $data['standard_type'] ?? $policy->standard_type, + 'fiscal_start_month' => $data['fiscal_start_month'] ?? $policy->fiscal_start_month, + 'fiscal_start_day' => $data['fiscal_start_day'] ?? $policy->fiscal_start_day, + 'default_annual_leave' => $data['default_annual_leave'] ?? $policy->default_annual_leave, + 'additional_leave_per_year' => $data['additional_leave_per_year'] ?? $policy->additional_leave_per_year, + 'max_annual_leave' => $data['max_annual_leave'] ?? $policy->max_annual_leave, + 'carry_over_enabled' => $data['carry_over_enabled'] ?? $policy->carry_over_enabled, + 'carry_over_max_days' => $data['carry_over_max_days'] ?? $policy->carry_over_max_days, + 'carry_over_expiry_months' => $data['carry_over_expiry_months'] ?? $policy->carry_over_expiry_months, + 'updated_by' => $userId, + ]); + $policy->save(); + } else { + $policy = LeavePolicy::create([ + 'tenant_id' => $tenantId, + 'standard_type' => $data['standard_type'] ?? LeavePolicy::TYPE_FISCAL, + 'fiscal_start_month' => $data['fiscal_start_month'] ?? 1, + 'fiscal_start_day' => $data['fiscal_start_day'] ?? 1, + 'default_annual_leave' => $data['default_annual_leave'] ?? 15, + 'additional_leave_per_year' => $data['additional_leave_per_year'] ?? 1, + 'max_annual_leave' => $data['max_annual_leave'] ?? 25, + 'carry_over_enabled' => $data['carry_over_enabled'] ?? true, + 'carry_over_max_days' => $data['carry_over_max_days'] ?? 10, + 'carry_over_expiry_months' => $data['carry_over_expiry_months'] ?? 3, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + } + + return $policy->fresh(); + } +} diff --git a/app/Swagger/v1/LeavePolicyApi.php b/app/Swagger/v1/LeavePolicyApi.php new file mode 100644 index 0000000..126ad4b --- /dev/null +++ b/app/Swagger/v1/LeavePolicyApi.php @@ -0,0 +1,109 @@ +id(); + $table->foreignId('tenant_id')->unique()->constrained()->onDelete('cascade')->comment('테넌트 ID'); + + // 기준 설정 + $table->enum('standard_type', ['fiscal', 'hire'])->default('fiscal')->comment('기준 유형: fiscal=회계연도, hire=입사일'); + $table->tinyInteger('fiscal_start_month')->default(1)->comment('회계연도 시작 월 (1-12)'); + $table->tinyInteger('fiscal_start_day')->default(1)->comment('회계연도 시작 일 (1-31)'); + + // 연차 설정 + $table->tinyInteger('default_annual_leave')->default(15)->comment('기본 연차 일수'); + $table->tinyInteger('additional_leave_per_year')->default(1)->comment('근속년수당 추가 연차'); + $table->tinyInteger('max_annual_leave')->default(25)->comment('최대 연차 일수'); + + // 이월 설정 + $table->boolean('carry_over_enabled')->default(true)->comment('이월 허용 여부'); + $table->tinyInteger('carry_over_max_days')->default(10)->comment('최대 이월 일수'); + $table->tinyInteger('carry_over_expiry_months')->default(3)->comment('이월 연차 소멸 기간 (개월)'); + + // 감사 필드 + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete()->comment('생성자'); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete()->comment('수정자'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('leave_policies'); + } +};