feat: 근태관리/직원관리 API 구현
- AttendanceController, AttendanceService 추가 - EmployeeController, EmployeeService 추가 - Attendance 모델 및 마이그레이션 추가 - TenantUserProfile에 employee_status 컬럼 추가 - DepartmentService 트리 조회 기능 개선 - Swagger 문서 추가 (AttendanceApi, EmployeeApi) - API 라우트 등록
This commit is contained in:
268
app/Models/Tenants/Attendance.php
Normal file
268
app/Models/Tenants/Attendance.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 근태 기록 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $user_id
|
||||
* @property string $base_date
|
||||
* @property string $status
|
||||
* @property array|null $json_details
|
||||
* @property string|null $remarks
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
*/
|
||||
class Attendance extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'attendances';
|
||||
|
||||
protected $casts = [
|
||||
'json_details' => 'array',
|
||||
'base_date' => 'date',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'user_id',
|
||||
'base_date',
|
||||
'status',
|
||||
'json_details',
|
||||
'remarks',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* 기본값 설정
|
||||
*/
|
||||
protected $attributes = [
|
||||
'status' => 'onTime',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 사용자 관계
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자 관계
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자 관계
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// json_details 헬퍼 메서드 (Accessor)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 출근 시간
|
||||
*/
|
||||
public function getCheckInAttribute(): ?string
|
||||
{
|
||||
return $this->json_details['check_in'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 퇴근 시간
|
||||
*/
|
||||
public function getCheckOutAttribute(): ?string
|
||||
{
|
||||
return $this->json_details['check_out'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* GPS 데이터
|
||||
*/
|
||||
public function getGpsDataAttribute(): ?array
|
||||
{
|
||||
return $this->json_details['gps_data'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 외근 정보
|
||||
*/
|
||||
public function getExternalWorkAttribute(): ?array
|
||||
{
|
||||
return $this->json_details['external_work'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 다중 출퇴근 기록 (여러 번 출퇴근)
|
||||
*/
|
||||
public function getMultipleEntriesAttribute(): ?array
|
||||
{
|
||||
return $this->json_details['multiple_entries'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 근무 시간 (분 단위)
|
||||
*/
|
||||
public function getWorkMinutesAttribute(): ?int
|
||||
{
|
||||
return isset($this->json_details['work_minutes'])
|
||||
? (int) $this->json_details['work_minutes']
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 초과 근무 시간 (분 단위)
|
||||
*/
|
||||
public function getOvertimeMinutesAttribute(): ?int
|
||||
{
|
||||
return isset($this->json_details['overtime_minutes'])
|
||||
? (int) $this->json_details['overtime_minutes']
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 지각 시간 (분 단위)
|
||||
*/
|
||||
public function getLateMinutesAttribute(): ?int
|
||||
{
|
||||
return isset($this->json_details['late_minutes'])
|
||||
? (int) $this->json_details['late_minutes']
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조퇴 시간 (분 단위)
|
||||
*/
|
||||
public function getEarlyLeaveMinutesAttribute(): ?int
|
||||
{
|
||||
return isset($this->json_details['early_leave_minutes'])
|
||||
? (int) $this->json_details['early_leave_minutes']
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 유형 (vacation 상태일 때)
|
||||
*/
|
||||
public function getVacationTypeAttribute(): ?string
|
||||
{
|
||||
return $this->json_details['vacation_type'] ?? null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// json_details 업데이트 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* json_details에서 특정 키 값 설정
|
||||
*/
|
||||
public function setJsonDetailsValue(string $key, mixed $value): void
|
||||
{
|
||||
$jsonDetails = $this->json_details ?? [];
|
||||
if ($value === null) {
|
||||
unset($jsonDetails[$key]);
|
||||
} else {
|
||||
$jsonDetails[$key] = $value;
|
||||
}
|
||||
$this->json_details = $jsonDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* json_details에서 특정 키 값 가져오기
|
||||
*/
|
||||
public function getJsonDetailsValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->json_details[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 출퇴근 정보 일괄 업데이트
|
||||
*/
|
||||
public function updateAttendanceDetails(array $data): void
|
||||
{
|
||||
$jsonDetails = $this->json_details ?? [];
|
||||
$allowedKeys = [
|
||||
'check_in',
|
||||
'check_out',
|
||||
'gps_data',
|
||||
'external_work',
|
||||
'multiple_entries',
|
||||
'work_minutes',
|
||||
'overtime_minutes',
|
||||
'late_minutes',
|
||||
'early_leave_minutes',
|
||||
'vacation_type',
|
||||
];
|
||||
|
||||
foreach ($allowedKeys as $key) {
|
||||
if (array_key_exists($key, $data)) {
|
||||
if ($data[$key] === null) {
|
||||
unset($jsonDetails[$key]);
|
||||
} else {
|
||||
$jsonDetails[$key] = $data[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->json_details = $jsonDetails;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 특정 날짜의 근태 조회
|
||||
*/
|
||||
public function scopeOnDate($query, string $date)
|
||||
{
|
||||
return $query->whereDate('base_date', $date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 기간의 근태 조회
|
||||
*/
|
||||
public function scopeBetweenDates($query, string $startDate, string $endDate)
|
||||
{
|
||||
return $query->whereBetween('base_date', [$startDate, $endDate]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자의 근태 조회
|
||||
*/
|
||||
public function scopeForUser($query, int $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 상태의 근태 조회
|
||||
*/
|
||||
public function scopeWithStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,18 @@
|
||||
|
||||
use App\Models\Members\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 테넌트별 사용자 프로필 (사원 정보)
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $user_id
|
||||
* @property int|null $department_id
|
||||
* @property string $employee_status active|leave|resigned
|
||||
* @property array|null $json_extra
|
||||
*
|
||||
* @mixin IdeHelperTenantUserProfile
|
||||
*/
|
||||
class TenantUserProfile extends Model
|
||||
@@ -22,23 +32,133 @@ class TenantUserProfile extends Model
|
||||
'job_title_key',
|
||||
'work_location_key',
|
||||
'employment_type_key',
|
||||
'employee_status',
|
||||
'manager_user_id',
|
||||
'json_extra',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'profile_photo_path',
|
||||
'display_name',
|
||||
];
|
||||
|
||||
// 관계: users 테이블은 전역이라 App\Models\User 로 연결
|
||||
public function user()
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
// 조직, 직급 등은 옵션/코드 참조 가능 (필요시 추가)
|
||||
public function department()
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'department_id');
|
||||
}
|
||||
|
||||
public function manager(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'manager_user_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// json_extra 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* json_extra에서 특정 키 값 가져오기
|
||||
*/
|
||||
public function getJsonExtraValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->json_extra[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* json_extra에 특정 키 값 설정
|
||||
*/
|
||||
public function setJsonExtraValue(string $key, mixed $value): void
|
||||
{
|
||||
$extra = $this->json_extra ?? [];
|
||||
if ($value === null) {
|
||||
unset($extra[$key]);
|
||||
} else {
|
||||
$extra[$key] = $value;
|
||||
}
|
||||
$this->json_extra = $extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 정보 일괄 업데이트 (json_extra)
|
||||
*/
|
||||
public function updateEmployeeInfo(array $data): void
|
||||
{
|
||||
$allowedKeys = [
|
||||
'employee_code',
|
||||
'resident_number',
|
||||
'gender',
|
||||
'address',
|
||||
'salary',
|
||||
'hire_date',
|
||||
'rank',
|
||||
'bank_account',
|
||||
'work_type',
|
||||
'contract_info',
|
||||
'emergency_contact',
|
||||
'education',
|
||||
'certifications',
|
||||
];
|
||||
|
||||
$extra = $this->json_extra ?? [];
|
||||
foreach ($allowedKeys as $key) {
|
||||
if (array_key_exists($key, $data)) {
|
||||
if ($data[$key] === null) {
|
||||
unset($extra[$key]);
|
||||
} else {
|
||||
$extra[$key] = $data[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->json_extra = $extra;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// json_extra Accessor (자주 사용하는 필드)
|
||||
// =========================================================================
|
||||
|
||||
public function getEmployeeCodeAttribute(): ?string
|
||||
{
|
||||
return $this->json_extra['employee_code'] ?? null;
|
||||
}
|
||||
|
||||
public function getHireDateAttribute(): ?string
|
||||
{
|
||||
return $this->json_extra['hire_date'] ?? null;
|
||||
}
|
||||
|
||||
public function getAddressAttribute(): ?string
|
||||
{
|
||||
return $this->json_extra['address'] ?? null;
|
||||
}
|
||||
|
||||
public function getEmergencyContactAttribute(): ?string
|
||||
{
|
||||
return $this->json_extra['emergency_contact'] ?? null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('employee_status', 'active');
|
||||
}
|
||||
|
||||
public function scopeOnLeave($query)
|
||||
{
|
||||
return $query->where('employee_status', 'leave');
|
||||
}
|
||||
|
||||
public function scopeResigned($query)
|
||||
{
|
||||
return $query->where('employee_status', 'resigned');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user