diff --git a/app/Http/Controllers/Api/V1/SiteController.php b/app/Http/Controllers/Api/V1/SiteController.php new file mode 100644 index 0000000..29e5775 --- /dev/null +++ b/app/Http/Controllers/Api/V1/SiteController.php @@ -0,0 +1,86 @@ +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); + } +} diff --git a/app/Http/Controllers/Api/V1/WorkSettingController.php b/app/Http/Controllers/Api/V1/WorkSettingController.php new file mode 100644 index 0000000..78b86c4 --- /dev/null +++ b/app/Http/Controllers/Api/V1/WorkSettingController.php @@ -0,0 +1,56 @@ +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); + } +} diff --git a/app/Http/Requests/V1/Site/StoreSiteRequest.php b/app/Http/Requests/V1/Site/StoreSiteRequest.php new file mode 100644 index 0000000..2d51326 --- /dev/null +++ b/app/Http/Requests/V1/Site/StoreSiteRequest.php @@ -0,0 +1,34 @@ + ['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'), + ]; + } +} diff --git a/app/Http/Requests/V1/Site/UpdateSiteRequest.php b/app/Http/Requests/V1/Site/UpdateSiteRequest.php new file mode 100644 index 0000000..91b6196 --- /dev/null +++ b/app/Http/Requests/V1/Site/UpdateSiteRequest.php @@ -0,0 +1,33 @@ + ['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'), + ]; + } +} diff --git a/app/Http/Requests/V1/WorkSetting/UpdateAttendanceSettingRequest.php b/app/Http/Requests/V1/WorkSetting/UpdateAttendanceSettingRequest.php new file mode 100644 index 0000000..25e323e --- /dev/null +++ b/app/Http/Requests/V1/WorkSetting/UpdateAttendanceSettingRequest.php @@ -0,0 +1,34 @@ + ['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'), + ]; + } +} diff --git a/app/Http/Requests/V1/WorkSetting/UpdateWorkSettingRequest.php b/app/Http/Requests/V1/WorkSetting/UpdateWorkSettingRequest.php new file mode 100644 index 0000000..bcb9008 --- /dev/null +++ b/app/Http/Requests/V1/WorkSetting/UpdateWorkSettingRequest.php @@ -0,0 +1,41 @@ + ['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'), + ]; + } +} diff --git a/app/Models/Tenants/AttendanceSetting.php b/app/Models/Tenants/AttendanceSetting.php new file mode 100644 index 0000000..c7f3eda --- /dev/null +++ b/app/Models/Tenants/AttendanceSetting.php @@ -0,0 +1,100 @@ + '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; + } +} diff --git a/app/Models/Tenants/Site.php b/app/Models/Tenants/Site.php new file mode 100644 index 0000000..cfd216b --- /dev/null +++ b/app/Models/Tenants/Site.php @@ -0,0 +1,125 @@ + '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; + } +} diff --git a/app/Models/Tenants/WorkSetting.php b/app/Models/Tenants/WorkSetting.php new file mode 100644 index 0000000..5776968 --- /dev/null +++ b/app/Models/Tenants/WorkSetting.php @@ -0,0 +1,110 @@ + '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); + } +} diff --git a/app/Services/SiteService.php b/app/Services/SiteService.php new file mode 100644 index 0000000..545eeff --- /dev/null +++ b/app/Services/SiteService.php @@ -0,0 +1,145 @@ +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(); + } +} diff --git a/app/Services/WorkSettingService.php b/app/Services/WorkSettingService.php new file mode 100644 index 0000000..6a215f1 --- /dev/null +++ b/app/Services/WorkSettingService.php @@ -0,0 +1,92 @@ +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(); + } +} diff --git a/app/Swagger/v1/SiteApi.php b/app/Swagger/v1/SiteApi.php new file mode 100644 index 0000000..48360dd --- /dev/null +++ b/app/Swagger/v1/SiteApi.php @@ -0,0 +1,270 @@ +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'); + } +}; diff --git a/database/migrations/2025_12_17_110001_create_attendance_settings_table.php b/database/migrations/2025_12_17_110001_create_attendance_settings_table.php new file mode 100644 index 0000000..4a72e3e --- /dev/null +++ b/database/migrations/2025_12_17_110001_create_attendance_settings_table.php @@ -0,0 +1,35 @@ +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'); + } +}; diff --git a/database/migrations/2025_12_17_110002_create_sites_table.php b/database/migrations/2025_12_17_110002_create_sites_table.php new file mode 100644 index 0000000..48fe7da --- /dev/null +++ b/database/migrations/2025_12_17_110002_create_sites_table.php @@ -0,0 +1,40 @@ +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'); + } +}; diff --git a/lang/ko/error.php b/lang/ko/error.php index b8618e6..aeb2db4 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -110,6 +110,8 @@ 'section_not_found' => '섹션을 찾을 수 없습니다.', 'field_not_found' => '필드를 찾을 수 없습니다.', 'field_key_reserved' => '":field_key"은(는) 시스템 예약어로 사용할 수 없습니다.', + 'field_key_system_readonly' => '시스템 필드의 field_key는 변경할 수 없습니다.', + 'field_key_system_cannot_delete' => '시스템 필드는 삭제할 수 없습니다.', 'bom_not_found' => 'BOM 항목을 찾을 수 없습니다.', 'item_type_required' => '품목 유형(item_type)은 필수입니다.', 'item_type_or_group_required' => '품목 유형(item_type) 또는 그룹 ID(group_id)는 필수입니다.', @@ -163,4 +165,28 @@ 'overlapping' => '해당 기간에 이미 신청된 휴가가 있습니다.', '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' => '연관된 데이터가 있어 삭제할 수 없습니다.', + ], ]; diff --git a/lang/ko/message.php b/lang/ko/message.php index be1f77a..b7b837a 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -209,4 +209,25 @@ 'balance_fetched' => '잔여 휴가를 조회했습니다.', 'balance_updated' => '휴가 일수가 설정되었습니다.', ], + + // 근무 설정 관리 + 'work_setting' => [ + 'fetched' => '근무 설정을 조회했습니다.', + 'updated' => '근무 설정이 수정되었습니다.', + ], + + // 출퇴근 설정 관리 + 'attendance_setting' => [ + 'fetched' => '출퇴근 설정을 조회했습니다.', + 'updated' => '출퇴근 설정이 수정되었습니다.', + ], + + // 현장 관리 + 'site' => [ + 'fetched' => '현장을 조회했습니다.', + 'created' => '현장이 등록되었습니다.', + 'updated' => '현장이 수정되었습니다.', + 'deleted' => '현장이 삭제되었습니다.', + 'active_fetched' => '활성화된 현장 목록을 조회했습니다.', + ], ]; diff --git a/routes/api.php b/routes/api.php index e8b79f3..15172e8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,7 +23,6 @@ use App\Http\Controllers\Api\V1\EstimateController; use App\Http\Controllers\Api\V1\FileStorageController; 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\EntityRelationshipController; 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\ItemsController; 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\ModelSetController; +// use App\Http\Controllers\Api\V1\MaterialController; // REMOVED: materials 테이블 삭제됨 use App\Http\Controllers\Api\V1\PermissionController; use App\Http\Controllers\Api\V1\PostController; 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\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\RoleController; 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\TenantFieldSettingController; +// 설계 전용 (디자인 네임스페이스) use App\Http\Controllers\Api\V1\TenantOptionGroupController; use App\Http\Controllers\Api\V1\TenantOptionValueController; use App\Http\Controllers\Api\V1\TenantStatFieldController; use App\Http\Controllers\Api\V1\TenantUserProfileController; -// 모델셋 관리 (견적 시스템) use App\Http\Controllers\Api\V1\UserController; +// 모델셋 관리 (견적 시스템) use App\Http\Controllers\Api\V1\UserRoleController; +use App\Http\Controllers\Api\V1\WorkSettingController; use Illuminate\Support\Facades\Route; // V1 초기 개발 @@ -257,6 +259,16 @@ 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 Route::prefix('permissions')->group(function () { Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스 @@ -267,6 +279,14 @@ // Settings & Configuration (설정 및 환경설정 통합 관리) 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에서 이동) Route::get('/fields', [TenantFieldSettingController::class, 'index'])->name('v1.settings.fields.index'); // 필드 설정 목록(전역+테넌트 병합 효과값) Route::put('/fields/bulk', [TenantFieldSettingController::class, 'bulkUpsert'])->name('v1.settings.fields.bulk'); // 필드 설정 대량 저장(트랜잭션 처리)