feat: 휴가 부여현황 API 추가
- leave_grants 테이블 마이그레이션 추가 - LeaveGrant 모델 생성 (annual/monthly/reward/condolence/other 유형) - LeaveService에 getGrants, storeGrant, destroyGrant 메서드 추가 - LeaveController에 grants, storeGrant, destroyGrant 엔드포인트 추가 - GET/POST/DELETE /api/v1/leaves/grants 라우트 추가 - 연차/월차 부여 시 LeaveBalance total_days 자동 갱신
This commit is contained in:
@@ -154,4 +154,37 @@ public function setBalance(BalanceRequest $request): JsonResponse
|
||||
);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여 이력 목록
|
||||
* GET /v1/leaves/grants
|
||||
*/
|
||||
public function grants(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->getGrants($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여
|
||||
* POST /v1/leaves/grants
|
||||
*/
|
||||
public function storeGrant(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->storeGrant($request->all());
|
||||
}, __('message.leave.granted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여 삭제
|
||||
* DELETE /v1/leaves/grants/{id}
|
||||
*/
|
||||
public function destroyGrant(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->destroyGrant($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
|
||||
144
app/Models/Tenants/LeaveGrant.php
Normal file
144
app/Models/Tenants/LeaveGrant.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 휴가 부여 이력 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $user_id
|
||||
* @property string $grant_type
|
||||
* @property string $grant_date
|
||||
* @property float $grant_days
|
||||
* @property string|null $reason
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
*/
|
||||
class LeaveGrant extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'leave_grants';
|
||||
|
||||
protected $casts = [
|
||||
'grant_date' => 'date',
|
||||
'grant_days' => 'decimal:1',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'user_id',
|
||||
'grant_type',
|
||||
'grant_date',
|
||||
'grant_days',
|
||||
'reason',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 상수 정의
|
||||
// =========================================================================
|
||||
|
||||
public const TYPE_ANNUAL = 'annual'; // 연차
|
||||
|
||||
public const TYPE_MONTHLY = 'monthly'; // 월차
|
||||
|
||||
public const TYPE_REWARD = 'reward'; // 포상휴가
|
||||
|
||||
public const TYPE_CONDOLENCE = 'condolence'; // 경조사
|
||||
|
||||
public const TYPE_OTHER = 'other'; // 기타
|
||||
|
||||
public const GRANT_TYPES = [
|
||||
self::TYPE_ANNUAL,
|
||||
self::TYPE_MONTHLY,
|
||||
self::TYPE_REWARD,
|
||||
self::TYPE_CONDOLENCE,
|
||||
self::TYPE_OTHER,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 부여 대상자
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 부여자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 특정 유형
|
||||
*/
|
||||
public function scopeOfType($query, string $type)
|
||||
{
|
||||
return $query->where('grant_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자
|
||||
*/
|
||||
public function scopeForUser($query, int $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 연도
|
||||
*/
|
||||
public function scopeForYear($query, int $year)
|
||||
{
|
||||
return $query->whereYear('grant_date', $year);
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 범위
|
||||
*/
|
||||
public function scopeBetweenDates($query, string $startDate, string $endDate)
|
||||
{
|
||||
return $query->whereBetween('grant_date', [$startDate, $endDate]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 부여 유형 라벨
|
||||
*/
|
||||
public function getGrantTypeLabelAttribute(): string
|
||||
{
|
||||
return match ($this->grant_type) {
|
||||
self::TYPE_ANNUAL => '연차',
|
||||
self::TYPE_MONTHLY => '월차',
|
||||
self::TYPE_REWARD => '포상휴가',
|
||||
self::TYPE_CONDOLENCE => '경조사',
|
||||
self::TYPE_OTHER => '기타',
|
||||
default => $this->grant_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Tenants\Leave;
|
||||
use App\Models\Tenants\LeaveBalance;
|
||||
use App\Models\Tenants\LeaveGrant;
|
||||
use App\Models\Tenants\TenantUserProfile;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -443,4 +444,142 @@ private function getOrCreateBalance(int $tenantId, int $userId, int $year): Leav
|
||||
|
||||
return $balance;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 휴가 부여 관련 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 휴가 부여 이력 목록 조회
|
||||
*/
|
||||
public function getGrants(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = LeaveGrant::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'user:id,name,email',
|
||||
'user.tenantProfile' => function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId)
|
||||
->with('department:id,name');
|
||||
},
|
||||
'creator:id,name',
|
||||
]);
|
||||
|
||||
// 사용자 필터
|
||||
if (! empty($params['user_id'])) {
|
||||
$query->where('user_id', $params['user_id']);
|
||||
}
|
||||
|
||||
// 부여 유형 필터
|
||||
if (! empty($params['grant_type'])) {
|
||||
$query->where('grant_type', $params['grant_type']);
|
||||
}
|
||||
|
||||
// 날짜 범위 필터
|
||||
if (! empty($params['date_from'])) {
|
||||
$query->where('grant_date', '>=', $params['date_from']);
|
||||
}
|
||||
if (! empty($params['date_to'])) {
|
||||
$query->where('grant_date', '<=', $params['date_to']);
|
||||
}
|
||||
|
||||
// 연도 필터
|
||||
if (! empty($params['year'])) {
|
||||
$query->whereYear('grant_date', $params['year']);
|
||||
}
|
||||
|
||||
// 부서 필터
|
||||
if (! empty($params['department_id'])) {
|
||||
$query->whereHas('user.tenantProfile', function ($q) use ($params, $tenantId) {
|
||||
$q->where('tenant_id', $tenantId)
|
||||
->where('department_id', $params['department_id']);
|
||||
});
|
||||
}
|
||||
|
||||
// 검색 (사용자명)
|
||||
if (! empty($params['search'])) {
|
||||
$query->whereHas('user', function ($q) use ($params) {
|
||||
$q->where('name', 'like', '%'.$params['search'].'%');
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'grant_date';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여
|
||||
*/
|
||||
public function storeGrant(array $data): LeaveGrant
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
$grant = LeaveGrant::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $data['user_id'],
|
||||
'grant_type' => $data['grant_type'],
|
||||
'grant_date' => $data['grant_date'],
|
||||
'grant_days' => $data['grant_days'],
|
||||
'reason' => $data['reason'] ?? null,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 연차/월차인 경우 LeaveBalance의 total_days에 추가
|
||||
if (in_array($data['grant_type'], [LeaveGrant::TYPE_ANNUAL, LeaveGrant::TYPE_MONTHLY])) {
|
||||
$year = \Carbon\Carbon::parse($data['grant_date'])->year;
|
||||
$balance = $this->getOrCreateBalance($tenantId, $data['user_id'], $year);
|
||||
$balance->total_days += $data['grant_days'];
|
||||
$balance->save();
|
||||
}
|
||||
|
||||
return $grant->fresh(['user:id,name,email', 'creator:id,name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여 삭제 (soft delete)
|
||||
*/
|
||||
public function destroyGrant(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$grant = LeaveGrant::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 연차/월차인 경우 LeaveBalance의 total_days에서 차감
|
||||
if (in_array($grant->grant_type, [LeaveGrant::TYPE_ANNUAL, LeaveGrant::TYPE_MONTHLY])) {
|
||||
$year = \Carbon\Carbon::parse($grant->grant_date)->year;
|
||||
$balance = LeaveBalance::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('user_id', $grant->user_id)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
if ($balance) {
|
||||
$balance->total_days = max(0, $balance->total_days - $grant->grant_days);
|
||||
$balance->save();
|
||||
}
|
||||
}
|
||||
|
||||
$grant->deleted_by = $userId;
|
||||
$grant->save();
|
||||
$grant->delete();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('leave_grants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('user_id')->comment('부여 대상자 ID');
|
||||
$table->string('grant_type', 20)->comment('부여유형: annual/monthly/reward/condolence/other');
|
||||
$table->date('grant_date')->comment('부여일');
|
||||
$table->decimal('grant_days', 4, 1)->comment('부여일수');
|
||||
$table->text('reason')->nullable()->comment('부여 사유');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('부여자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['tenant_id', 'user_id'], 'idx_tenant_user');
|
||||
$table->index('grant_date', 'idx_grant_date');
|
||||
$table->index('grant_type', 'idx_grant_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('leave_grants');
|
||||
}
|
||||
};
|
||||
@@ -382,4 +382,13 @@
|
||||
'token_toggled' => '토큰 상태가 변경되었습니다.',
|
||||
'token_deleted' => '토큰이 삭제되었습니다.',
|
||||
],
|
||||
|
||||
// 휴가 관리
|
||||
'leave' => [
|
||||
'created' => '휴가 신청이 완료되었습니다.',
|
||||
'approved' => '휴가가 승인되었습니다.',
|
||||
'rejected' => '휴가가 반려되었습니다.',
|
||||
'cancelled' => '휴가가 취소되었습니다.',
|
||||
'granted' => '휴가가 부여되었습니다.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -308,6 +308,9 @@
|
||||
Route::get('/balance', [LeaveController::class, 'balance'])->name('v1.leaves.balance');
|
||||
Route::get('/balance/{userId}', [LeaveController::class, 'userBalance'])->name('v1.leaves.userBalance');
|
||||
Route::put('/balance', [LeaveController::class, 'setBalance'])->name('v1.leaves.setBalance');
|
||||
Route::get('/grants', [LeaveController::class, 'grants'])->name('v1.leaves.grants');
|
||||
Route::post('/grants', [LeaveController::class, 'storeGrant'])->name('v1.leaves.grants.store');
|
||||
Route::delete('/grants/{id}', [LeaveController::class, 'destroyGrant'])->name('v1.leaves.grants.destroy');
|
||||
Route::get('/{id}', [LeaveController::class, 'show'])->name('v1.leaves.show');
|
||||
Route::patch('/{id}', [LeaveController::class, 'update'])->name('v1.leaves.update');
|
||||
Route::delete('/{id}', [LeaveController::class, 'destroy'])->name('v1.leaves.destroy');
|
||||
|
||||
Reference in New Issue
Block a user