feat: [calendar] 달력 일정 관리 API 구현
- GET /api/v1/calendar-schedules — 연도별 일정 목록 조회
- GET /api/v1/calendar-schedules/stats — 통계 조회
- GET /api/v1/calendar-schedules/{id} — 단건 조회
- POST /api/v1/calendar-schedules — 등록
- PUT /api/v1/calendar-schedules/{id} — 수정
- DELETE /api/v1/calendar-schedules/{id} — 삭제
- POST /api/v1/calendar-schedules/bulk — 대량 등록
This commit is contained in:
133
app/Http/Controllers/Api/V1/CalendarScheduleController.php
Normal file
133
app/Http/Controllers/Api/V1/CalendarScheduleController.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CalendarScheduleService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CalendarScheduleController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CalendarScheduleService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 일정 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$request->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')
|
||||
);
|
||||
}
|
||||
}
|
||||
41
app/Models/Commons/Holiday.php
Normal file
41
app/Models/Commons/Holiday.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Holiday extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'holidays';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'name',
|
||||
'type',
|
||||
'is_recurring',
|
||||
'memo',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_date' => '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);
|
||||
}
|
||||
}
|
||||
180
app/Services/CalendarScheduleService.php
Normal file
180
app/Services/CalendarScheduleService.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Commons\Holiday;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class CalendarScheduleService extends Service
|
||||
{
|
||||
/**
|
||||
* 연도별 일정 목록 조회
|
||||
*/
|
||||
public function list(int $year, ?string $type = null): array
|
||||
{
|
||||
$query = Holiday::forTenant($this->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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user