feat(API): 직책/직원/근태 관리 API 개선
- Position 모델: key 필드 추가 및 마이그레이션 - PositionSeeder: 기본 직책 시더 추가 - TenantUserProfile: 프로필 이미지 관련 필드 추가 - Employee Request: 직원 등록/수정 요청 검증 강화 - EmployeeService: 직원 관리 서비스 로직 개선 - AttendanceService: 근태 관리 서비스 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,15 @@ class Attendance extends Model
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* JSON 응답에 포함할 accessor
|
||||
*/
|
||||
protected $appends = [
|
||||
'check_in',
|
||||
'check_out',
|
||||
'break_minutes',
|
||||
];
|
||||
|
||||
/**
|
||||
* 기본값 설정
|
||||
*/
|
||||
@@ -85,18 +94,44 @@ public function updater(): BelongsTo
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 출근 시간
|
||||
* 출근 시간 (가장 빠른 출근)
|
||||
* check_ins 배열이 있으면 가장 빠른 시간 반환, 없으면 레거시 check_in 사용
|
||||
*/
|
||||
public function getCheckInAttribute(): ?string
|
||||
{
|
||||
$checkIns = $this->json_details['check_ins'] ?? [];
|
||||
|
||||
if (! empty($checkIns)) {
|
||||
$times = array_filter(array_map(fn ($entry) => $entry['time'] ?? null, $checkIns));
|
||||
if (! empty($times)) {
|
||||
sort($times);
|
||||
|
||||
return $times[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 레거시 호환: 기존 check_in 필드
|
||||
return $this->json_details['check_in'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 퇴근 시간
|
||||
* 퇴근 시간 (가장 늦은 퇴근)
|
||||
* check_outs 배열이 있으면 가장 늦은 시간 반환, 없으면 레거시 check_out 사용
|
||||
*/
|
||||
public function getCheckOutAttribute(): ?string
|
||||
{
|
||||
$checkOuts = $this->json_details['check_outs'] ?? [];
|
||||
|
||||
if (! empty($checkOuts)) {
|
||||
$times = array_filter(array_map(fn ($entry) => $entry['time'] ?? null, $checkOuts));
|
||||
if (! empty($times)) {
|
||||
rsort($times);
|
||||
|
||||
return $times[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 레거시 호환: 기존 check_out 필드
|
||||
return $this->json_details['check_out'] ?? null;
|
||||
}
|
||||
|
||||
@@ -134,6 +169,50 @@ public function getWorkMinutesAttribute(): ?int
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴게 시간 (분 단위)
|
||||
ㅣ * 1. json_details에 저장된 값이 있으면 반환
|
||||
* 2. 없으면 check_in/check_out 기준으로 WorkSetting에서 실시간 계산
|
||||
*/
|
||||
public function getBreakMinutesAttribute(): ?int
|
||||
{
|
||||
// 1. 이미 계산된 값이 있으면 사용
|
||||
if (isset($this->json_details['break_minutes'])) {
|
||||
return (int) $this->json_details['break_minutes'];
|
||||
}
|
||||
|
||||
// 2. 레거시 데이터: check_in, check_out이 있으면 실시간 계산
|
||||
$checkIn = $this->check_in;
|
||||
$checkOut = $this->check_out;
|
||||
|
||||
if (! $checkIn || ! $checkOut) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// WorkSetting에서 휴게시간 설정 조회
|
||||
$workSetting = WorkSetting::where('tenant_id', $this->tenant_id)->first();
|
||||
|
||||
if (! $workSetting || ! $workSetting->break_start || ! $workSetting->break_end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$checkInCarbon = \Carbon\Carbon::createFromFormat('H:i:s', $checkIn);
|
||||
$checkOutCarbon = \Carbon\Carbon::createFromFormat('H:i:s', $checkOut);
|
||||
$breakStart = \Carbon\Carbon::createFromFormat('H:i:s', $workSetting->break_start);
|
||||
$breakEnd = \Carbon\Carbon::createFromFormat('H:i:s', $workSetting->break_end);
|
||||
|
||||
// 출근~퇴근 시간이 휴게시간을 포함하면 휴게시간 적용
|
||||
if ($checkInCarbon->lte($breakStart) && $checkOutCarbon->gte($breakEnd)) {
|
||||
return (int) $breakStart->diffInMinutes($breakEnd);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 초과 근무 시간 (분 단위)
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $type rank(직급) | title(직책)
|
||||
* @property string|null $key 영문 키 (tenant_user_profiles 연동용)
|
||||
* @property string $name 명칭
|
||||
* @property int $sort_order 정렬 순서
|
||||
* @property bool $is_active 활성화 여부
|
||||
@@ -26,6 +27,7 @@ class Position extends Model
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'type',
|
||||
'key',
|
||||
'name',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
|
||||
@@ -65,6 +65,26 @@ public function manager(): BelongsTo
|
||||
return $this->belongsTo(User::class, 'manager_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 직급 (positions 테이블 type=rank)
|
||||
*/
|
||||
public function rankPosition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class, 'position_key', 'key')
|
||||
->where('type', Position::TYPE_RANK)
|
||||
->where('tenant_id', $this->tenant_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 직책 (positions 테이블 type=title)
|
||||
*/
|
||||
public function titlePosition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class, 'job_title_key', 'key')
|
||||
->where('type', Position::TYPE_TITLE)
|
||||
->where('tenant_id', $this->tenant_id);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// json_extra 헬퍼 메서드
|
||||
// =========================================================================
|
||||
@@ -151,36 +171,37 @@ public function getEmergencyContactAttribute(): ?string
|
||||
}
|
||||
|
||||
/**
|
||||
* 직책 레이블 (position_key → 한글)
|
||||
* 직급 레이블 (position_key → positions 테이블에서 name 조회)
|
||||
*/
|
||||
public function getPositionLabelAttribute(): ?string
|
||||
{
|
||||
$labels = [
|
||||
'EXECUTIVE' => '임원',
|
||||
'DIRECTOR' => '부장',
|
||||
'MANAGER' => '과장',
|
||||
'SENIOR' => '대리',
|
||||
'STAFF' => '사원',
|
||||
'INTERN' => '인턴',
|
||||
];
|
||||
if (! $this->position_key || ! $this->tenant_id) {
|
||||
return $this->position_key;
|
||||
}
|
||||
|
||||
return $labels[$this->position_key] ?? $this->position_key;
|
||||
$position = Position::where('tenant_id', $this->tenant_id)
|
||||
->where('type', Position::TYPE_RANK)
|
||||
->where('key', $this->position_key)
|
||||
->first();
|
||||
|
||||
return $position?->name ?? $this->position_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 직급 레이블 (job_title_key → 한글)
|
||||
* 직책 레이블 (job_title_key → positions 테이블에서 name 조회)
|
||||
*/
|
||||
public function getJobTitleLabelAttribute(): ?string
|
||||
{
|
||||
$labels = [
|
||||
'CEO' => '대표이사',
|
||||
'CTO' => '기술이사',
|
||||
'CFO' => '재무이사',
|
||||
'TEAM_LEAD' => '팀장',
|
||||
'TEAM_MEMBER' => '팀원',
|
||||
];
|
||||
if (! $this->job_title_key || ! $this->tenant_id) {
|
||||
return $this->job_title_key;
|
||||
}
|
||||
|
||||
return $labels[$this->job_title_key] ?? $this->job_title_key;
|
||||
$position = Position::where('tenant_id', $this->tenant_id)
|
||||
->where('type', Position::TYPE_TITLE)
|
||||
->where('key', $this->job_title_key)
|
||||
->first();
|
||||
|
||||
return $position?->name ?? $this->job_title_key;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user