feat: 근무/출퇴근 설정 및 현장 관리 API 구현
- 근무 설정 API (GET/PUT /settings/work) - 근무유형, 소정근로시간, 연장근로시간, 근무요일, 출퇴근시간, 휴게시간 - 출퇴근 설정 API (GET/PUT /settings/attendance) - GPS 출퇴근, 허용 반경, 본사 위치 설정 - 현장 관리 API (CRUD /sites) - 현장 등록/수정/삭제, 활성화된 현장 목록(셀렉트박스용) - GPS 좌표 기반 위치 관리 마이그레이션: work_settings, attendance_settings, sites 테이블 모델: WorkSetting, AttendanceSetting, Site (BelongsToTenant, SoftDeletes) 서비스: WorkSettingService, SiteService Swagger 문서 및 i18n 메시지 키 추가
This commit is contained in:
86
app/Http/Controllers/Api/V1/SiteController.php
Normal file
86
app/Http/Controllers/Api/V1/SiteController.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\V1\Site\StoreSiteRequest;
|
||||||
|
use App\Http\Requests\V1\Site\UpdateSiteRequest;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
use App\Services\SiteService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SiteController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly SiteService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 목록
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$params = $request->only([
|
||||||
|
'search',
|
||||||
|
'is_active',
|
||||||
|
'sort_by',
|
||||||
|
'sort_dir',
|
||||||
|
'per_page',
|
||||||
|
'page',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sites = $this->service->index($params);
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.fetched'), $sites);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 등록
|
||||||
|
*/
|
||||||
|
public function store(StoreSiteRequest $request)
|
||||||
|
{
|
||||||
|
$site = $this->service->store($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.created'), $site, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 상세
|
||||||
|
*/
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
$site = $this->service->show($id);
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.fetched'), $site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 수정
|
||||||
|
*/
|
||||||
|
public function update(int $id, UpdateSiteRequest $request)
|
||||||
|
{
|
||||||
|
$site = $this->service->update($id, $request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.updated'), $site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 삭제
|
||||||
|
*/
|
||||||
|
public function destroy(int $id)
|
||||||
|
{
|
||||||
|
$this->service->destroy($id);
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성화된 현장 목록 (셀렉트박스용)
|
||||||
|
*/
|
||||||
|
public function active()
|
||||||
|
{
|
||||||
|
$sites = $this->service->getActiveSites();
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.fetched'), $sites);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Http/Controllers/Api/V1/WorkSettingController.php
Normal file
56
app/Http/Controllers/Api/V1/WorkSettingController.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\V1\WorkSetting\UpdateAttendanceSettingRequest;
|
||||||
|
use App\Http\Requests\V1\WorkSetting\UpdateWorkSettingRequest;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
use App\Services\WorkSettingService;
|
||||||
|
|
||||||
|
class WorkSettingController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WorkSettingService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무 설정 조회
|
||||||
|
*/
|
||||||
|
public function showWorkSetting()
|
||||||
|
{
|
||||||
|
$setting = $this->service->getWorkSetting();
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.fetched'), $setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무 설정 수정
|
||||||
|
*/
|
||||||
|
public function updateWorkSetting(UpdateWorkSettingRequest $request)
|
||||||
|
{
|
||||||
|
$setting = $this->service->updateWorkSetting($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.updated'), $setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출퇴근 설정 조회
|
||||||
|
*/
|
||||||
|
public function showAttendanceSetting()
|
||||||
|
{
|
||||||
|
$setting = $this->service->getAttendanceSetting();
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.fetched'), $setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출퇴근 설정 수정
|
||||||
|
*/
|
||||||
|
public function updateAttendanceSetting(UpdateAttendanceSettingRequest $request)
|
||||||
|
{
|
||||||
|
$setting = $this->service->updateAttendanceSetting($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.updated'), $setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Http/Requests/V1/Site/StoreSiteRequest.php
Normal file
34
app/Http/Requests/V1/Site/StoreSiteRequest.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\Site;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreSiteRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:100'],
|
||||||
|
'address' => ['nullable', 'string', 'max:255'],
|
||||||
|
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||||
|
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||||
|
'is_active' => ['sometimes', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => __('error.site.name_required'),
|
||||||
|
'name.max' => __('error.site.name_too_long'),
|
||||||
|
'latitude.between' => __('error.site.invalid_latitude'),
|
||||||
|
'longitude.between' => __('error.site.invalid_longitude'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Http/Requests/V1/Site/UpdateSiteRequest.php
Normal file
33
app/Http/Requests/V1/Site/UpdateSiteRequest.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\Site;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateSiteRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['sometimes', 'string', 'max:100'],
|
||||||
|
'address' => ['nullable', 'string', 'max:255'],
|
||||||
|
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||||
|
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||||
|
'is_active' => ['sometimes', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.max' => __('error.site.name_too_long'),
|
||||||
|
'latitude.between' => __('error.site.invalid_latitude'),
|
||||||
|
'longitude.between' => __('error.site.invalid_longitude'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\WorkSetting;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateAttendanceSettingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'use_gps' => ['sometimes', 'boolean'],
|
||||||
|
'allowed_radius' => ['sometimes', 'integer', 'min:10', 'max:10000'],
|
||||||
|
'hq_address' => ['nullable', 'string', 'max:255'],
|
||||||
|
'hq_latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||||
|
'hq_longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'allowed_radius.min' => __('error.attendance_setting.radius_too_small'),
|
||||||
|
'allowed_radius.max' => __('error.attendance_setting.radius_too_large'),
|
||||||
|
'hq_latitude.between' => __('error.attendance_setting.invalid_latitude'),
|
||||||
|
'hq_longitude.between' => __('error.attendance_setting.invalid_longitude'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\WorkSetting;
|
||||||
|
|
||||||
|
use App\Models\Tenants\WorkSetting;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UpdateWorkSettingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'work_type' => ['sometimes', 'string', Rule::in(WorkSetting::WORK_TYPES)],
|
||||||
|
'standard_hours' => ['sometimes', 'integer', 'min:1', 'max:168'],
|
||||||
|
'overtime_hours' => ['sometimes', 'integer', 'min:0', 'max:52'],
|
||||||
|
'overtime_limit' => ['sometimes', 'integer', 'min:0', 'max:100'],
|
||||||
|
'work_days' => ['sometimes', 'array'],
|
||||||
|
'work_days.*' => ['string', Rule::in(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])],
|
||||||
|
'start_time' => ['sometimes', 'date_format:H:i:s'],
|
||||||
|
'end_time' => ['sometimes', 'date_format:H:i:s', 'after:start_time'],
|
||||||
|
'break_minutes' => ['sometimes', 'integer', 'min:0', 'max:180'],
|
||||||
|
'break_start' => ['nullable', 'date_format:H:i:s'],
|
||||||
|
'break_end' => ['nullable', 'date_format:H:i:s', 'after:break_start'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'work_type.in' => __('error.work_setting.invalid_work_type'),
|
||||||
|
'end_time.after' => __('error.work_setting.end_time_after_start'),
|
||||||
|
'break_end.after' => __('error.work_setting.break_end_after_start'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
100
app/Models/Tenants/AttendanceSetting.php
Normal file
100
app/Models/Tenants/AttendanceSetting.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Tenants;
|
||||||
|
|
||||||
|
use App\Traits\BelongsToTenant;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출퇴근 설정 모델
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $tenant_id
|
||||||
|
* @property bool $use_gps
|
||||||
|
* @property int $allowed_radius
|
||||||
|
* @property string|null $hq_address
|
||||||
|
* @property float|null $hq_latitude
|
||||||
|
* @property float|null $hq_longitude
|
||||||
|
*/
|
||||||
|
class AttendanceSetting extends Model
|
||||||
|
{
|
||||||
|
use BelongsToTenant;
|
||||||
|
|
||||||
|
protected $table = 'attendance_settings';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'use_gps',
|
||||||
|
'allowed_radius',
|
||||||
|
'hq_address',
|
||||||
|
'hq_latitude',
|
||||||
|
'hq_longitude',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'use_gps' => 'boolean',
|
||||||
|
'allowed_radius' => 'integer',
|
||||||
|
'hq_latitude' => 'decimal:8',
|
||||||
|
'hq_longitude' => 'decimal:8',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'use_gps' => false,
|
||||||
|
'allowed_radius' => 100,
|
||||||
|
];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 헬퍼 메서드
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPS 설정 완료 여부
|
||||||
|
*/
|
||||||
|
public function isGpsConfigured(): bool
|
||||||
|
{
|
||||||
|
return $this->use_gps
|
||||||
|
&& $this->hq_latitude !== null
|
||||||
|
&& $this->hq_longitude !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 좌표가 허용 범위 내인지 확인
|
||||||
|
*/
|
||||||
|
public function isWithinRadius(float $latitude, float $longitude): bool
|
||||||
|
{
|
||||||
|
if (! $this->isGpsConfigured()) {
|
||||||
|
return true; // GPS 미설정 시 항상 허용
|
||||||
|
}
|
||||||
|
|
||||||
|
$distance = $this->calculateDistance(
|
||||||
|
$this->hq_latitude,
|
||||||
|
$this->hq_longitude,
|
||||||
|
$latitude,
|
||||||
|
$longitude
|
||||||
|
);
|
||||||
|
|
||||||
|
return $distance <= $this->allowed_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 좌표 간 거리 계산 (미터)
|
||||||
|
* Haversine 공식 사용
|
||||||
|
*/
|
||||||
|
private function calculateDistance(float $lat1, float $lon1, float $lat2, float $lon2): float
|
||||||
|
{
|
||||||
|
$earthRadius = 6371000; // 지구 반경 (미터)
|
||||||
|
|
||||||
|
$lat1Rad = deg2rad($lat1);
|
||||||
|
$lat2Rad = deg2rad($lat2);
|
||||||
|
$deltaLat = deg2rad($lat2 - $lat1);
|
||||||
|
$deltaLon = deg2rad($lon2 - $lon1);
|
||||||
|
|
||||||
|
$a = sin($deltaLat / 2) * sin($deltaLat / 2) +
|
||||||
|
cos($lat1Rad) * cos($lat2Rad) *
|
||||||
|
sin($deltaLon / 2) * sin($deltaLon / 2);
|
||||||
|
|
||||||
|
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||||
|
|
||||||
|
return $earthRadius * $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
125
app/Models/Tenants/Site.php
Normal file
125
app/Models/Tenants/Site.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Tenants;
|
||||||
|
|
||||||
|
use App\Models\Members\User;
|
||||||
|
use App\Traits\BelongsToTenant;
|
||||||
|
use App\Traits\ModelTrait;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 모델
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $tenant_id
|
||||||
|
* @property string $name
|
||||||
|
* @property string|null $address
|
||||||
|
* @property float|null $latitude
|
||||||
|
* @property float|null $longitude
|
||||||
|
* @property bool $is_active
|
||||||
|
* @property int|null $created_by
|
||||||
|
* @property int|null $updated_by
|
||||||
|
* @property int|null $deleted_by
|
||||||
|
*/
|
||||||
|
class Site extends Model
|
||||||
|
{
|
||||||
|
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'sites';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'name',
|
||||||
|
'address',
|
||||||
|
'latitude',
|
||||||
|
'longitude',
|
||||||
|
'is_active',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
'deleted_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'latitude' => 'decimal:8',
|
||||||
|
'longitude' => 'decimal:8',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'is_active' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 관계 정의
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성자
|
||||||
|
*/
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정자
|
||||||
|
*/
|
||||||
|
public function updater(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 헬퍼 메서드
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPS 좌표 설정 여부
|
||||||
|
*/
|
||||||
|
public function hasCoordinates(): bool
|
||||||
|
{
|
||||||
|
return $this->latitude !== null && $this->longitude !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 좌표가 허용 범위 내인지 확인
|
||||||
|
*/
|
||||||
|
public function isWithinRadius(float $latitude, float $longitude, int $radiusMeters = 100): bool
|
||||||
|
{
|
||||||
|
if (! $this->hasCoordinates()) {
|
||||||
|
return true; // 좌표 미설정 시 항상 허용
|
||||||
|
}
|
||||||
|
|
||||||
|
$distance = $this->calculateDistance(
|
||||||
|
$this->latitude,
|
||||||
|
$this->longitude,
|
||||||
|
$latitude,
|
||||||
|
$longitude
|
||||||
|
);
|
||||||
|
|
||||||
|
return $distance <= $radiusMeters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 좌표 간 거리 계산 (미터)
|
||||||
|
*/
|
||||||
|
private function calculateDistance(float $lat1, float $lon1, float $lat2, float $lon2): float
|
||||||
|
{
|
||||||
|
$earthRadius = 6371000;
|
||||||
|
|
||||||
|
$lat1Rad = deg2rad($lat1);
|
||||||
|
$lat2Rad = deg2rad($lat2);
|
||||||
|
$deltaLat = deg2rad($lat2 - $lat1);
|
||||||
|
$deltaLon = deg2rad($lon2 - $lon1);
|
||||||
|
|
||||||
|
$a = sin($deltaLat / 2) * sin($deltaLat / 2) +
|
||||||
|
cos($lat1Rad) * cos($lat2Rad) *
|
||||||
|
sin($deltaLon / 2) * sin($deltaLon / 2);
|
||||||
|
|
||||||
|
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||||
|
|
||||||
|
return $earthRadius * $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
app/Models/Tenants/WorkSetting.php
Normal file
110
app/Models/Tenants/WorkSetting.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Tenants;
|
||||||
|
|
||||||
|
use App\Traits\BelongsToTenant;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무 설정 모델
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $tenant_id
|
||||||
|
* @property string $work_type
|
||||||
|
* @property int $standard_hours
|
||||||
|
* @property int $overtime_hours
|
||||||
|
* @property int $overtime_limit
|
||||||
|
* @property array|null $work_days
|
||||||
|
* @property string $start_time
|
||||||
|
* @property string $end_time
|
||||||
|
* @property int $break_minutes
|
||||||
|
* @property string|null $break_start
|
||||||
|
* @property string|null $break_end
|
||||||
|
*/
|
||||||
|
class WorkSetting extends Model
|
||||||
|
{
|
||||||
|
use BelongsToTenant;
|
||||||
|
|
||||||
|
protected $table = 'work_settings';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'work_type',
|
||||||
|
'standard_hours',
|
||||||
|
'overtime_hours',
|
||||||
|
'overtime_limit',
|
||||||
|
'work_days',
|
||||||
|
'start_time',
|
||||||
|
'end_time',
|
||||||
|
'break_minutes',
|
||||||
|
'break_start',
|
||||||
|
'break_end',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'work_days' => 'array',
|
||||||
|
'standard_hours' => 'integer',
|
||||||
|
'overtime_hours' => 'integer',
|
||||||
|
'overtime_limit' => 'integer',
|
||||||
|
'break_minutes' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'work_type' => 'fixed',
|
||||||
|
'standard_hours' => 40,
|
||||||
|
'overtime_hours' => 12,
|
||||||
|
'overtime_limit' => 52,
|
||||||
|
'start_time' => '09:00:00',
|
||||||
|
'end_time' => '18:00:00',
|
||||||
|
'break_minutes' => 60,
|
||||||
|
'break_start' => '12:00:00',
|
||||||
|
'break_end' => '13:00:00',
|
||||||
|
];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 상수 정의
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public const TYPE_FIXED = 'fixed'; // 고정 근무
|
||||||
|
|
||||||
|
public const TYPE_FLEXIBLE = 'flexible'; // 유연 근무
|
||||||
|
|
||||||
|
public const TYPE_CUSTOM = 'custom'; // 커스텀 근무
|
||||||
|
|
||||||
|
public const WORK_TYPES = [
|
||||||
|
self::TYPE_FIXED,
|
||||||
|
self::TYPE_FLEXIBLE,
|
||||||
|
self::TYPE_CUSTOM,
|
||||||
|
];
|
||||||
|
|
||||||
|
public const DEFAULT_WORK_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri'];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 헬퍼 메서드
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무 유형 라벨
|
||||||
|
*/
|
||||||
|
public function getWorkTypeLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return match ($this->work_type) {
|
||||||
|
self::TYPE_FIXED => '고정근무',
|
||||||
|
self::TYPE_FLEXIBLE => '유연근무',
|
||||||
|
self::TYPE_CUSTOM => '커스텀',
|
||||||
|
default => $this->work_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일일 근무시간 계산 (분)
|
||||||
|
*/
|
||||||
|
public function getDailyWorkMinutesAttribute(): int
|
||||||
|
{
|
||||||
|
$start = strtotime($this->start_time);
|
||||||
|
$end = strtotime($this->end_time);
|
||||||
|
$totalMinutes = ($end - $start) / 60;
|
||||||
|
|
||||||
|
return (int) ($totalMinutes - $this->break_minutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
145
app/Services/SiteService.php
Normal file
145
app/Services/SiteService.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Tenants\Site;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class SiteService extends Service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 현장 목록 조회
|
||||||
|
*/
|
||||||
|
public function index(array $params): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$query = Site::query()
|
||||||
|
->where('tenant_id', $tenantId);
|
||||||
|
|
||||||
|
// 검색 필터
|
||||||
|
if (! empty($params['search'])) {
|
||||||
|
$search = $params['search'];
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('address', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성화 상태 필터
|
||||||
|
if (isset($params['is_active'])) {
|
||||||
|
$query->where('is_active', $params['is_active']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정렬
|
||||||
|
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||||
|
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||||
|
$query->orderBy($sortBy, $sortDir);
|
||||||
|
|
||||||
|
// 페이지네이션
|
||||||
|
$perPage = $params['per_page'] ?? 20;
|
||||||
|
|
||||||
|
return $query->paginate($perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 상세 조회
|
||||||
|
*/
|
||||||
|
public function show(int $id): Site
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
return Site::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 등록
|
||||||
|
*/
|
||||||
|
public function store(array $data): Site
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||||
|
$site = Site::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'name' => $data['name'],
|
||||||
|
'address' => $data['address'] ?? null,
|
||||||
|
'latitude' => $data['latitude'] ?? null,
|
||||||
|
'longitude' => $data['longitude'] ?? null,
|
||||||
|
'is_active' => $data['is_active'] ?? true,
|
||||||
|
'created_by' => $userId,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $site;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 수정
|
||||||
|
*/
|
||||||
|
public function update(int $id, array $data): Site
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||||
|
$site = Site::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
$site->fill([
|
||||||
|
'name' => $data['name'] ?? $site->name,
|
||||||
|
'address' => $data['address'] ?? $site->address,
|
||||||
|
'latitude' => $data['latitude'] ?? $site->latitude,
|
||||||
|
'longitude' => $data['longitude'] ?? $site->longitude,
|
||||||
|
'is_active' => $data['is_active'] ?? $site->is_active,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$site->save();
|
||||||
|
|
||||||
|
return $site->fresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장 삭제
|
||||||
|
*/
|
||||||
|
public function destroy(int $id): bool
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||||
|
$site = Site::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
$site->deleted_by = $userId;
|
||||||
|
$site->save();
|
||||||
|
$site->delete();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성화된 현장 목록 조회 (셀렉트박스용)
|
||||||
|
*/
|
||||||
|
public function getActiveSites(): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
return Site::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'address'])
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
92
app/Services/WorkSettingService.php
Normal file
92
app/Services/WorkSettingService.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Tenants\AttendanceSetting;
|
||||||
|
use App\Models\Tenants\WorkSetting;
|
||||||
|
|
||||||
|
class WorkSettingService extends Service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 근무 설정 조회 (없으면 생성)
|
||||||
|
*/
|
||||||
|
public function getWorkSetting(): WorkSetting
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$setting = WorkSetting::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $setting) {
|
||||||
|
$setting = WorkSetting::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'work_days' => WorkSetting::DEFAULT_WORK_DAYS,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무 설정 수정
|
||||||
|
*/
|
||||||
|
public function updateWorkSetting(array $data): WorkSetting
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$setting = WorkSetting::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $setting) {
|
||||||
|
$setting = new WorkSetting(['tenant_id' => $tenantId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting->fill($data);
|
||||||
|
$setting->save();
|
||||||
|
|
||||||
|
return $setting->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출퇴근 설정 조회 (없으면 생성)
|
||||||
|
*/
|
||||||
|
public function getAttendanceSetting(): AttendanceSetting
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$setting = AttendanceSetting::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $setting) {
|
||||||
|
$setting = AttendanceSetting::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출퇴근 설정 수정
|
||||||
|
*/
|
||||||
|
public function updateAttendanceSetting(array $data): AttendanceSetting
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$setting = AttendanceSetting::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $setting) {
|
||||||
|
$setting = new AttendanceSetting(['tenant_id' => $tenantId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting->fill($data);
|
||||||
|
$setting->save();
|
||||||
|
|
||||||
|
return $setting->fresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
270
app/Swagger/v1/SiteApi.php
Normal file
270
app/Swagger/v1/SiteApi.php
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(name="Sites", description="현장 관리")
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="Site",
|
||||||
|
* type="object",
|
||||||
|
* description="현장 정보",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1, description="현장 ID"),
|
||||||
|
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||||
|
* @OA\Property(property="name", type="string", example="강남 현장", description="현장명"),
|
||||||
|
* @OA\Property(property="address", type="string", example="서울시 강남구 테헤란로 123", nullable=true, description="현장 주소"),
|
||||||
|
* @OA\Property(property="latitude", type="number", format="float", example=37.5012, nullable=true, description="위도"),
|
||||||
|
* @OA\Property(property="longitude", type="number", format="float", example=127.0396, nullable=true, description="경도"),
|
||||||
|
* @OA\Property(property="is_active", type="boolean", example=true, description="활성화 여부"),
|
||||||
|
* @OA\Property(property="created_by", type="integer", example=1, nullable=true, description="생성자 ID"),
|
||||||
|
* @OA\Property(property="updated_by", type="integer", example=1, nullable=true, description="수정자 ID"),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="SiteCreateRequest",
|
||||||
|
* type="object",
|
||||||
|
* required={"name"},
|
||||||
|
* description="현장 등록 요청",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="name", type="string", example="강남 현장", maxLength=100, description="현장명"),
|
||||||
|
* @OA\Property(property="address", type="string", example="서울시 강남구 테헤란로 123", maxLength=255, description="현장 주소"),
|
||||||
|
* @OA\Property(property="latitude", type="number", format="float", example=37.5012, minimum=-90, maximum=90, description="위도"),
|
||||||
|
* @OA\Property(property="longitude", type="number", format="float", example=127.0396, minimum=-180, maximum=180, description="경도"),
|
||||||
|
* @OA\Property(property="is_active", type="boolean", example=true, description="활성화 여부")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="SiteUpdateRequest",
|
||||||
|
* type="object",
|
||||||
|
* description="현장 수정 요청",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="name", type="string", example="강남 현장", maxLength=100, description="현장명"),
|
||||||
|
* @OA\Property(property="address", type="string", example="서울시 강남구 테헤란로 123", maxLength=255, description="현장 주소"),
|
||||||
|
* @OA\Property(property="latitude", type="number", format="float", example=37.5012, minimum=-90, maximum=90, description="위도"),
|
||||||
|
* @OA\Property(property="longitude", type="number", format="float", example=127.0396, minimum=-180, maximum=180, description="경도"),
|
||||||
|
* @OA\Property(property="is_active", type="boolean", example=true, description="활성화 여부")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="SiteListItem",
|
||||||
|
* type="object",
|
||||||
|
* description="현장 목록 아이템 (셀렉트박스용)",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1, description="현장 ID"),
|
||||||
|
* @OA\Property(property="name", type="string", example="강남 현장", description="현장명"),
|
||||||
|
* @OA\Property(property="address", type="string", example="서울시 강남구", nullable=true, description="현장 주소")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class SiteApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/sites",
|
||||||
|
* tags={"Sites"},
|
||||||
|
* summary="현장 목록 조회",
|
||||||
|
* description="현장 목록을 조회합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="search", in="query", description="검색어 (현장명, 주소)", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="is_active", in="query", description="활성화 상태 필터", @OA\Schema(type="boolean")),
|
||||||
|
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"name","created_at"}, default="created_at")),
|
||||||
|
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(
|
||||||
|
* property="data",
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="current_page", type="integer", example=1),
|
||||||
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Site")),
|
||||||
|
* @OA\Property(property="per_page", type="integer", example=20),
|
||||||
|
* @OA\Property(property="total", type="integer", example=10)
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function index() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/sites",
|
||||||
|
* tags={"Sites"},
|
||||||
|
* summary="현장 등록",
|
||||||
|
* description="새로운 현장을 등록합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/SiteCreateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=201,
|
||||||
|
* description="등록 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/Site")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function store() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/sites/active",
|
||||||
|
* tags={"Sites"},
|
||||||
|
* summary="활성화된 현장 목록 (셀렉트박스용)",
|
||||||
|
* description="활성화된 현장 목록을 간단한 형태로 조회합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/SiteListItem"))
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function active() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/sites/{id}",
|
||||||
|
* tags={"Sites"},
|
||||||
|
* summary="현장 상세 조회",
|
||||||
|
* description="현장 상세 정보를 조회합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="현장 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/Site")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="현장 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function show() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Put(
|
||||||
|
* path="/api/v1/sites/{id}",
|
||||||
|
* tags={"Sites"},
|
||||||
|
* summary="현장 수정",
|
||||||
|
* description="현장 정보를 수정합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="현장 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/SiteUpdateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="수정 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/Site")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="현장 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function update() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Delete(
|
||||||
|
* path="/api/v1/sites/{id}",
|
||||||
|
* tags={"Sites"},
|
||||||
|
* summary="현장 삭제",
|
||||||
|
* description="현장을 삭제합니다. (Soft Delete)",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="현장 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="삭제 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ApiResponse")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="현장 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function destroy() {}
|
||||||
|
}
|
||||||
209
app/Swagger/v1/WorkSettingApi.php
Normal file
209
app/Swagger/v1/WorkSettingApi.php
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(name="WorkSettings", description="근무/출퇴근 설정")
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="WorkSetting",
|
||||||
|
* type="object",
|
||||||
|
* description="근무 설정 정보",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1, description="ID"),
|
||||||
|
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||||
|
* @OA\Property(property="work_type", type="string", enum={"fixed","flexible","custom"}, example="fixed", description="근무유형"),
|
||||||
|
* @OA\Property(property="standard_hours", type="integer", example=40, description="주당 소정근로시간"),
|
||||||
|
* @OA\Property(property="overtime_hours", type="integer", example=12, description="주당 연장근로시간"),
|
||||||
|
* @OA\Property(property="overtime_limit", type="integer", example=52, description="연장근로한도"),
|
||||||
|
* @OA\Property(property="work_days", type="array", @OA\Items(type="string"), example={"mon","tue","wed","thu","fri"}, description="근무요일"),
|
||||||
|
* @OA\Property(property="start_time", type="string", format="time", example="09:00:00", description="출근시간"),
|
||||||
|
* @OA\Property(property="end_time", type="string", format="time", example="18:00:00", description="퇴근시간"),
|
||||||
|
* @OA\Property(property="break_minutes", type="integer", example=60, description="휴게시간(분)"),
|
||||||
|
* @OA\Property(property="break_start", type="string", format="time", example="12:00:00", nullable=true, description="휴게시작"),
|
||||||
|
* @OA\Property(property="break_end", type="string", format="time", example="13:00:00", nullable=true, description="휴게종료"),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="WorkSettingUpdateRequest",
|
||||||
|
* type="object",
|
||||||
|
* description="근무 설정 수정 요청",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="work_type", type="string", enum={"fixed","flexible","custom"}, example="fixed", description="근무유형"),
|
||||||
|
* @OA\Property(property="standard_hours", type="integer", example=40, minimum=1, maximum=168, description="주당 소정근로시간"),
|
||||||
|
* @OA\Property(property="overtime_hours", type="integer", example=12, minimum=0, maximum=52, description="주당 연장근로시간"),
|
||||||
|
* @OA\Property(property="overtime_limit", type="integer", example=52, minimum=0, maximum=100, description="연장근로한도"),
|
||||||
|
* @OA\Property(property="work_days", type="array", @OA\Items(type="string", enum={"mon","tue","wed","thu","fri","sat","sun"}), description="근무요일"),
|
||||||
|
* @OA\Property(property="start_time", type="string", format="time", example="09:00:00", description="출근시간"),
|
||||||
|
* @OA\Property(property="end_time", type="string", format="time", example="18:00:00", description="퇴근시간"),
|
||||||
|
* @OA\Property(property="break_minutes", type="integer", example=60, minimum=0, maximum=180, description="휴게시간(분)"),
|
||||||
|
* @OA\Property(property="break_start", type="string", format="time", example="12:00:00", description="휴게시작"),
|
||||||
|
* @OA\Property(property="break_end", type="string", format="time", example="13:00:00", description="휴게종료")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="AttendanceSetting",
|
||||||
|
* type="object",
|
||||||
|
* description="출퇴근 설정 정보",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1, description="ID"),
|
||||||
|
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||||
|
* @OA\Property(property="use_gps", type="boolean", example=true, description="GPS 출퇴근 사용 여부"),
|
||||||
|
* @OA\Property(property="allowed_radius", type="integer", example=100, description="허용 반경(m)"),
|
||||||
|
* @OA\Property(property="hq_address", type="string", example="서울시 강남구 테헤란로 123", nullable=true, description="본사 주소"),
|
||||||
|
* @OA\Property(property="hq_latitude", type="number", format="float", example=37.5012, nullable=true, description="본사 위도"),
|
||||||
|
* @OA\Property(property="hq_longitude", type="number", format="float", example=127.0396, nullable=true, description="본사 경도"),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="AttendanceSettingUpdateRequest",
|
||||||
|
* type="object",
|
||||||
|
* description="출퇴근 설정 수정 요청",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="use_gps", type="boolean", example=true, description="GPS 출퇴근 사용 여부"),
|
||||||
|
* @OA\Property(property="allowed_radius", type="integer", example=100, minimum=10, maximum=10000, description="허용 반경(m)"),
|
||||||
|
* @OA\Property(property="hq_address", type="string", example="서울시 강남구 테헤란로 123", description="본사 주소"),
|
||||||
|
* @OA\Property(property="hq_latitude", type="number", format="float", example=37.5012, minimum=-90, maximum=90, description="본사 위도"),
|
||||||
|
* @OA\Property(property="hq_longitude", type="number", format="float", example=127.0396, minimum=-180, maximum=180, description="본사 경도")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class WorkSettingApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/settings/work",
|
||||||
|
* tags={"WorkSettings"},
|
||||||
|
* summary="근무 설정 조회",
|
||||||
|
* description="테넌트의 근무 설정을 조회합니다. 설정이 없으면 기본값으로 자동 생성됩니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkSetting")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function showWorkSetting() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Put(
|
||||||
|
* path="/api/v1/settings/work",
|
||||||
|
* tags={"WorkSettings"},
|
||||||
|
* summary="근무 설정 수정",
|
||||||
|
* description="테넌트의 근무 설정을 수정합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/WorkSettingUpdateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="수정 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/WorkSetting")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function updateWorkSetting() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/settings/attendance",
|
||||||
|
* tags={"WorkSettings"},
|
||||||
|
* summary="출퇴근 설정 조회",
|
||||||
|
* description="테넌트의 출퇴근 설정을 조회합니다. 설정이 없으면 기본값으로 자동 생성됩니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/AttendanceSetting")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function showAttendanceSetting() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Put(
|
||||||
|
* path="/api/v1/settings/attendance",
|
||||||
|
* tags={"WorkSettings"},
|
||||||
|
* summary="출퇴근 설정 수정",
|
||||||
|
* description="테넌트의 출퇴근 설정을 수정합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/AttendanceSettingUpdateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="수정 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/AttendanceSetting")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function updateAttendanceSetting() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?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('work_settings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->unique()->comment('테넌트 ID');
|
||||||
|
$table->string('work_type', 20)->default('fixed')->comment('근무유형: fixed/flexible/custom');
|
||||||
|
$table->integer('standard_hours')->default(40)->comment('주당 소정근로시간');
|
||||||
|
$table->integer('overtime_hours')->default(12)->comment('주당 연장근로시간');
|
||||||
|
$table->integer('overtime_limit')->default(52)->comment('연장근로한도');
|
||||||
|
$table->json('work_days')->nullable()->comment('근무요일 ["mon","tue","wed","thu","fri"]');
|
||||||
|
$table->time('start_time')->default('09:00:00')->comment('출근시간');
|
||||||
|
$table->time('end_time')->default('18:00:00')->comment('퇴근시간');
|
||||||
|
$table->integer('break_minutes')->default(60)->comment('휴게시간(분)');
|
||||||
|
$table->time('break_start')->nullable()->default('12:00:00')->comment('휴게시작');
|
||||||
|
$table->time('break_end')->nullable()->default('13:00:00')->comment('휴게종료');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('work_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('attendance_settings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->unique()->comment('테넌트 ID');
|
||||||
|
$table->boolean('use_gps')->default(false)->comment('GPS 출퇴근 사용 여부');
|
||||||
|
$table->integer('allowed_radius')->default(100)->comment('허용 반경(m)');
|
||||||
|
$table->string('hq_address', 255)->nullable()->comment('본사 주소');
|
||||||
|
$table->decimal('hq_latitude', 10, 8)->nullable()->comment('본사 위도');
|
||||||
|
$table->decimal('hq_longitude', 11, 8)->nullable()->comment('본사 경도');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('attendance_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
40
database/migrations/2025_12_17_110002_create_sites_table.php
Normal file
40
database/migrations/2025_12_17_110002_create_sites_table.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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('sites', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->comment('테넌트 ID');
|
||||||
|
$table->string('name', 100)->comment('현장명');
|
||||||
|
$table->string('address', 255)->nullable()->comment('현장 주소');
|
||||||
|
$table->decimal('latitude', 10, 8)->nullable()->comment('위도');
|
||||||
|
$table->decimal('longitude', 11, 8)->nullable()->comment('경도');
|
||||||
|
$table->boolean('is_active')->default(true)->comment('활성화 여부');
|
||||||
|
$table->foreignId('created_by')->nullable()->comment('생성자');
|
||||||
|
$table->foreignId('updated_by')->nullable()->comment('수정자');
|
||||||
|
$table->foreignId('deleted_by')->nullable()->comment('삭제자');
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('tenant_id');
|
||||||
|
$table->index('is_active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('sites');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -110,6 +110,8 @@
|
|||||||
'section_not_found' => '섹션을 찾을 수 없습니다.',
|
'section_not_found' => '섹션을 찾을 수 없습니다.',
|
||||||
'field_not_found' => '필드를 찾을 수 없습니다.',
|
'field_not_found' => '필드를 찾을 수 없습니다.',
|
||||||
'field_key_reserved' => '":field_key"은(는) 시스템 예약어로 사용할 수 없습니다.',
|
'field_key_reserved' => '":field_key"은(는) 시스템 예약어로 사용할 수 없습니다.',
|
||||||
|
'field_key_system_readonly' => '시스템 필드의 field_key는 변경할 수 없습니다.',
|
||||||
|
'field_key_system_cannot_delete' => '시스템 필드는 삭제할 수 없습니다.',
|
||||||
'bom_not_found' => 'BOM 항목을 찾을 수 없습니다.',
|
'bom_not_found' => 'BOM 항목을 찾을 수 없습니다.',
|
||||||
'item_type_required' => '품목 유형(item_type)은 필수입니다.',
|
'item_type_required' => '품목 유형(item_type)은 필수입니다.',
|
||||||
'item_type_or_group_required' => '품목 유형(item_type) 또는 그룹 ID(group_id)는 필수입니다.',
|
'item_type_or_group_required' => '품목 유형(item_type) 또는 그룹 ID(group_id)는 필수입니다.',
|
||||||
@@ -163,4 +165,28 @@
|
|||||||
'overlapping' => '해당 기간에 이미 신청된 휴가가 있습니다.',
|
'overlapping' => '해당 기간에 이미 신청된 휴가가 있습니다.',
|
||||||
'balance_not_found' => '휴가 잔여일수 정보를 찾을 수 없습니다.',
|
'balance_not_found' => '휴가 잔여일수 정보를 찾을 수 없습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 근무 설정 관련
|
||||||
|
'work_setting' => [
|
||||||
|
'invalid_work_type' => '유효하지 않은 근무 유형입니다.',
|
||||||
|
'invalid_work_days' => '유효하지 않은 근무요일입니다.',
|
||||||
|
'invalid_time_range' => '출근시간은 퇴근시간보다 이전이어야 합니다.',
|
||||||
|
'break_time_invalid' => '휴게시간 설정이 올바르지 않습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 출퇴근 설정 관련
|
||||||
|
'attendance_setting' => [
|
||||||
|
'invalid_radius' => '허용 반경이 유효하지 않습니다.',
|
||||||
|
'invalid_coordinates' => '좌표가 유효하지 않습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 현장 관리 관련
|
||||||
|
'site' => [
|
||||||
|
'not_found' => '현장 정보를 찾을 수 없습니다.',
|
||||||
|
'name_required' => '현장명은 필수입니다.',
|
||||||
|
'name_too_long' => '현장명은 100자를 초과할 수 없습니다.',
|
||||||
|
'invalid_latitude' => '위도는 -90 ~ 90 사이여야 합니다.',
|
||||||
|
'invalid_longitude' => '경도는 -180 ~ 180 사이여야 합니다.',
|
||||||
|
'has_dependencies' => '연관된 데이터가 있어 삭제할 수 없습니다.',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -209,4 +209,25 @@
|
|||||||
'balance_fetched' => '잔여 휴가를 조회했습니다.',
|
'balance_fetched' => '잔여 휴가를 조회했습니다.',
|
||||||
'balance_updated' => '휴가 일수가 설정되었습니다.',
|
'balance_updated' => '휴가 일수가 설정되었습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 근무 설정 관리
|
||||||
|
'work_setting' => [
|
||||||
|
'fetched' => '근무 설정을 조회했습니다.',
|
||||||
|
'updated' => '근무 설정이 수정되었습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 출퇴근 설정 관리
|
||||||
|
'attendance_setting' => [
|
||||||
|
'fetched' => '출퇴근 설정을 조회했습니다.',
|
||||||
|
'updated' => '출퇴근 설정이 수정되었습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 현장 관리
|
||||||
|
'site' => [
|
||||||
|
'fetched' => '현장을 조회했습니다.',
|
||||||
|
'created' => '현장이 등록되었습니다.',
|
||||||
|
'updated' => '현장이 수정되었습니다.',
|
||||||
|
'deleted' => '현장이 삭제되었습니다.',
|
||||||
|
'active_fetched' => '활성화된 현장 목록을 조회했습니다.',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
use App\Http\Controllers\Api\V1\EstimateController;
|
use App\Http\Controllers\Api\V1\EstimateController;
|
||||||
use App\Http\Controllers\Api\V1\FileStorageController;
|
use App\Http\Controllers\Api\V1\FileStorageController;
|
||||||
use App\Http\Controllers\Api\V1\FolderController;
|
use App\Http\Controllers\Api\V1\FolderController;
|
||||||
use App\Http\Controllers\Api\V1\LeaveController;
|
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\CustomTabController;
|
use App\Http\Controllers\Api\V1\ItemMaster\CustomTabController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\EntityRelationshipController;
|
use App\Http\Controllers\Api\V1\ItemMaster\EntityRelationshipController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemBomItemController;
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemBomItemController;
|
||||||
@@ -36,29 +35,32 @@
|
|||||||
use App\Http\Controllers\Api\V1\ItemsBomController;
|
use App\Http\Controllers\Api\V1\ItemsBomController;
|
||||||
use App\Http\Controllers\Api\V1\ItemsController;
|
use App\Http\Controllers\Api\V1\ItemsController;
|
||||||
use App\Http\Controllers\Api\V1\ItemsFileController;
|
use App\Http\Controllers\Api\V1\ItemsFileController;
|
||||||
// use App\Http\Controllers\Api\V1\MaterialController; // REMOVED: materials 테이블 삭제됨
|
use App\Http\Controllers\Api\V1\LeaveController;
|
||||||
use App\Http\Controllers\Api\V1\MenuController;
|
use App\Http\Controllers\Api\V1\MenuController;
|
||||||
use App\Http\Controllers\Api\V1\ModelSetController;
|
use App\Http\Controllers\Api\V1\ModelSetController;
|
||||||
|
// use App\Http\Controllers\Api\V1\MaterialController; // REMOVED: materials 테이블 삭제됨
|
||||||
use App\Http\Controllers\Api\V1\PermissionController;
|
use App\Http\Controllers\Api\V1\PermissionController;
|
||||||
use App\Http\Controllers\Api\V1\PostController;
|
use App\Http\Controllers\Api\V1\PostController;
|
||||||
use App\Http\Controllers\Api\V1\PricingController;
|
use App\Http\Controllers\Api\V1\PricingController;
|
||||||
// use App\Http\Controllers\Api\V1\ProductBomItemController; // REMOVED: products 테이블 삭제됨
|
|
||||||
// use App\Http\Controllers\Api\V1\ProductController; // REMOVED: products 테이블 삭제됨
|
|
||||||
use App\Http\Controllers\Api\V1\QuoteController;
|
use App\Http\Controllers\Api\V1\QuoteController;
|
||||||
use App\Http\Controllers\Api\V1\RefreshController;
|
use App\Http\Controllers\Api\V1\RefreshController;
|
||||||
|
// use App\Http\Controllers\Api\V1\ProductBomItemController; // REMOVED: products 테이블 삭제됨
|
||||||
|
// use App\Http\Controllers\Api\V1\ProductController; // REMOVED: products 테이블 삭제됨
|
||||||
use App\Http\Controllers\Api\V1\RegisterController;
|
use App\Http\Controllers\Api\V1\RegisterController;
|
||||||
use App\Http\Controllers\Api\V1\RoleController;
|
use App\Http\Controllers\Api\V1\RoleController;
|
||||||
use App\Http\Controllers\Api\V1\RolePermissionController;
|
use App\Http\Controllers\Api\V1\RolePermissionController;
|
||||||
|
use App\Http\Controllers\Api\V1\SiteController;
|
||||||
use App\Http\Controllers\Api\V1\TenantController;
|
use App\Http\Controllers\Api\V1\TenantController;
|
||||||
// 설계 전용 (디자인 네임스페이스)
|
|
||||||
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
||||||
|
// 설계 전용 (디자인 네임스페이스)
|
||||||
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
||||||
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
||||||
use App\Http\Controllers\Api\V1\TenantStatFieldController;
|
use App\Http\Controllers\Api\V1\TenantStatFieldController;
|
||||||
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
||||||
// 모델셋 관리 (견적 시스템)
|
|
||||||
use App\Http\Controllers\Api\V1\UserController;
|
use App\Http\Controllers\Api\V1\UserController;
|
||||||
|
// 모델셋 관리 (견적 시스템)
|
||||||
use App\Http\Controllers\Api\V1\UserRoleController;
|
use App\Http\Controllers\Api\V1\UserRoleController;
|
||||||
|
use App\Http\Controllers\Api\V1\WorkSettingController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
// V1 초기 개발
|
// V1 초기 개발
|
||||||
@@ -257,6 +259,16 @@
|
|||||||
Route::post('/{id}/cancel', [LeaveController::class, 'cancel'])->name('v1.leaves.cancel');
|
Route::post('/{id}/cancel', [LeaveController::class, 'cancel'])->name('v1.leaves.cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Site API (현장 관리)
|
||||||
|
Route::prefix('sites')->group(function () {
|
||||||
|
Route::get('', [SiteController::class, 'index'])->name('v1.sites.index');
|
||||||
|
Route::post('', [SiteController::class, 'store'])->name('v1.sites.store');
|
||||||
|
Route::get('/active', [SiteController::class, 'active'])->name('v1.sites.active');
|
||||||
|
Route::get('/{id}', [SiteController::class, 'show'])->whereNumber('id')->name('v1.sites.show');
|
||||||
|
Route::put('/{id}', [SiteController::class, 'update'])->whereNumber('id')->name('v1.sites.update');
|
||||||
|
Route::delete('/{id}', [SiteController::class, 'destroy'])->whereNumber('id')->name('v1.sites.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
// Permission API
|
// Permission API
|
||||||
Route::prefix('permissions')->group(function () {
|
Route::prefix('permissions')->group(function () {
|
||||||
Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스
|
Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스
|
||||||
@@ -267,6 +279,14 @@
|
|||||||
// Settings & Configuration (설정 및 환경설정 통합 관리)
|
// Settings & Configuration (설정 및 환경설정 통합 관리)
|
||||||
Route::prefix('settings')->group(function () {
|
Route::prefix('settings')->group(function () {
|
||||||
|
|
||||||
|
// 근무 설정
|
||||||
|
Route::get('/work', [WorkSettingController::class, 'showWorkSetting'])->name('v1.settings.work.show');
|
||||||
|
Route::put('/work', [WorkSettingController::class, 'updateWorkSetting'])->name('v1.settings.work.update');
|
||||||
|
|
||||||
|
// 출퇴근 설정
|
||||||
|
Route::get('/attendance', [WorkSettingController::class, 'showAttendanceSetting'])->name('v1.settings.attendance.show');
|
||||||
|
Route::put('/attendance', [WorkSettingController::class, 'updateAttendanceSetting'])->name('v1.settings.attendance.update');
|
||||||
|
|
||||||
// 테넌트 필드 설정 (기존 fields에서 이동)
|
// 테넌트 필드 설정 (기존 fields에서 이동)
|
||||||
Route::get('/fields', [TenantFieldSettingController::class, 'index'])->name('v1.settings.fields.index'); // 필드 설정 목록(전역+테넌트 병합 효과값)
|
Route::get('/fields', [TenantFieldSettingController::class, 'index'])->name('v1.settings.fields.index'); // 필드 설정 목록(전역+테넌트 병합 효과값)
|
||||||
Route::put('/fields/bulk', [TenantFieldSettingController::class, 'bulkUpsert'])->name('v1.settings.fields.bulk'); // 필드 설정 대량 저장(트랜잭션 처리)
|
Route::put('/fields/bulk', [TenantFieldSettingController::class, 'bulkUpsert'])->name('v1.settings.fields.bulk'); // 필드 설정 대량 저장(트랜잭션 처리)
|
||||||
|
|||||||
Reference in New Issue
Block a user