feat: I-8 휴가 정책 관리 API 구현
- 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 <noreply@anthropic.com>
This commit is contained in:
41
app/Http/Controllers/Api/V1/LeavePolicyController.php
Normal file
41
app/Http/Controllers/Api/V1/LeavePolicyController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\LeavePolicy\UpdateRequest;
|
||||
use App\Services\LeavePolicyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 휴가 정책 관리 컨트롤러
|
||||
*/
|
||||
class LeavePolicyController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected LeavePolicyService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 휴가 정책 조회
|
||||
* GET /v1/leave-policy
|
||||
*/
|
||||
public function show(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->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'));
|
||||
}
|
||||
}
|
||||
41
app/Http/Requests/LeavePolicy/UpdateRequest.php
Normal file
41
app/Http/Requests/LeavePolicy/UpdateRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\LeavePolicy;
|
||||
|
||||
use App\Models\Tenants\LeavePolicy;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'standard_type' => ['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]),
|
||||
];
|
||||
}
|
||||
}
|
||||
127
app/Models/Tenants/LeavePolicy.php
Normal file
127
app/Models/Tenants/LeavePolicy.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 휴가 정책 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $standard_type
|
||||
* @property int $fiscal_start_month
|
||||
* @property int $fiscal_start_day
|
||||
* @property int $default_annual_leave
|
||||
* @property int $additional_leave_per_year
|
||||
* @property int $max_annual_leave
|
||||
* @property bool $carry_over_enabled
|
||||
* @property int $carry_over_max_days
|
||||
* @property int $carry_over_expiry_months
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
*/
|
||||
class LeavePolicy extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'leave_policies';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'standard_type',
|
||||
'fiscal_start_month',
|
||||
'fiscal_start_day',
|
||||
'default_annual_leave',
|
||||
'additional_leave_per_year',
|
||||
'max_annual_leave',
|
||||
'carry_over_enabled',
|
||||
'carry_over_max_days',
|
||||
'carry_over_expiry_months',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'fiscal_start_month' => '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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
70
app/Services/LeavePolicyService.php
Normal file
70
app/Services/LeavePolicyService.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\LeavePolicy;
|
||||
|
||||
class LeavePolicyService extends Service
|
||||
{
|
||||
/**
|
||||
* 휴가 정책 조회
|
||||
* 없으면 기본값으로 생성
|
||||
*/
|
||||
public function show(): LeavePolicy
|
||||
{
|
||||
$tenantId = $this->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();
|
||||
}
|
||||
}
|
||||
109
app/Swagger/v1/LeavePolicyApi.php
Normal file
109
app/Swagger/v1/LeavePolicyApi.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(
|
||||
* name="LeavePolicy",
|
||||
* description="휴가 정책 관리 API - 테넌트별 휴가 기준 설정"
|
||||
* )
|
||||
*/
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="LeavePolicy",
|
||||
* description="휴가 정책",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", description="ID", example=1),
|
||||
* @OA\Property(property="tenant_id", type="integer", description="테넌트 ID", example=1),
|
||||
* @OA\Property(property="standard_type", type="string", enum={"fiscal", "hire"}, description="기준 유형", example="fiscal"),
|
||||
* @OA\Property(property="fiscal_start_month", type="integer", description="회계연도 시작 월", example=1),
|
||||
* @OA\Property(property="fiscal_start_day", type="integer", description="회계연도 시작 일", example=1),
|
||||
* @OA\Property(property="default_annual_leave", type="integer", description="기본 연차 일수", example=15),
|
||||
* @OA\Property(property="additional_leave_per_year", type="integer", description="근속년수당 추가 연차", example=1),
|
||||
* @OA\Property(property="max_annual_leave", type="integer", description="최대 연차 일수", example=25),
|
||||
* @OA\Property(property="carry_over_enabled", type="boolean", description="이월 허용 여부", example=true),
|
||||
* @OA\Property(property="carry_over_max_days", type="integer", description="최대 이월 일수", example=10),
|
||||
* @OA\Property(property="carry_over_expiry_months", type="integer", description="이월 연차 소멸 기간 (개월)", example=3),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time", description="생성일시"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time", description="수정일시")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="LeavePolicyUpdateRequest",
|
||||
* description="휴가 정책 수정 요청",
|
||||
*
|
||||
* @OA\Property(property="standard_type", type="string", enum={"fiscal", "hire"}, description="기준 유형", example="fiscal"),
|
||||
* @OA\Property(property="fiscal_start_month", type="integer", minimum=1, maximum=12, description="회계연도 시작 월", example=1),
|
||||
* @OA\Property(property="fiscal_start_day", type="integer", minimum=1, maximum=31, description="회계연도 시작 일", example=1),
|
||||
* @OA\Property(property="default_annual_leave", type="integer", minimum=0, maximum=100, description="기본 연차 일수", example=15),
|
||||
* @OA\Property(property="additional_leave_per_year", type="integer", minimum=0, maximum=10, description="근속년수당 추가 연차", example=1),
|
||||
* @OA\Property(property="max_annual_leave", type="integer", minimum=0, maximum=100, description="최대 연차 일수", example=25),
|
||||
* @OA\Property(property="carry_over_enabled", type="boolean", description="이월 허용 여부", example=true),
|
||||
* @OA\Property(property="carry_over_max_days", type="integer", minimum=0, maximum=100, description="최대 이월 일수", example=10),
|
||||
* @OA\Property(property="carry_over_expiry_months", type="integer", minimum=0, maximum=24, description="이월 연차 소멸 기간 (개월)", example=3)
|
||||
* )
|
||||
*/
|
||||
class LeavePolicyApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/leave-policy",
|
||||
* operationId="getLeavePolicy",
|
||||
* tags={"LeavePolicy"},
|
||||
* summary="휴가 정책 조회",
|
||||
* description="현재 테넌트의 휴가 정책을 조회합니다. 설정이 없으면 기본값으로 생성됩니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/LeavePolicy")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패"),
|
||||
* @OA\Response(response=403, description="권한 없음")
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/leave-policy",
|
||||
* operationId="updateLeavePolicy",
|
||||
* tags={"LeavePolicy"},
|
||||
* summary="휴가 정책 수정",
|
||||
* description="현재 테넌트의 휴가 정책을 수정합니다. 부분 업데이트가 가능합니다.",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/LeavePolicyUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="정보가 수정되었습니다."),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/LeavePolicy")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패"),
|
||||
* @OA\Response(response=403, description="권한 없음"),
|
||||
* @OA\Response(response=422, description="유효성 검사 실패")
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('leave_policies', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user