Files
sam-docs/features/hr/hr-api-analysis.md
hskwon 73b8b9325b docs: HR API 분석 문서 재구성
- features/hr/hr-api-analysis.md 추가 (상세 분석)
- features/HR_API_ANALYSIS.md 삭제 (hr 폴더로 이동)
2025-12-09 20:30:33 +09:00

35 KiB
Raw Blame History

HR 관리 API 분석 및 개발 계획

작성일: 2025-12-08 업데이트: 2025-12-08 (tenant 테이블 분석 및 정책 확정) 목적: React HR 관리 기능과 기존 API/MNG 비교 분석 및 필요 작업 도출


📋 분석 개요

React HR 컴포넌트 현황 (커밋 48dbba0)

기능 컴포넌트 위치 상태
사원관리 react/src/components/hr/EmployeeManagement/ Mock 데이터
부서관리 react/src/components/hr/DepartmentManagement/ Mock 데이터
근태관리 react/src/components/hr/AttendanceManagement/ Mock 데이터

기존 API 현황

리소스 컨트롤러 상태
Department DepartmentController 존재 (기본 CRUD + 사용자 배정 + 권한)
Employee - 없음
Attendance - 없음

MNG 현황 (참조용 구현체)

리소스 서비스 컨트롤러 상태
User (사용자) UserService.php (497줄) Api/Admin/UserController.php 완전 구현
Department (부서) DepartmentService.php (249줄) Api/Admin/DepartmentController.php 완전 구현

Note

: MNG는 관리자 패널용으로 구현되어 있으며, API 개발 시 참조할 수 있는 완성된 구현체입니다.


🔑 핵심 정책: 사용자와 사원 정보 분리

개념 정의

┌─────────────────────────────────────────┐
│     사용자 (User) - 전역, users 테이블   │
│  - 시스템 계정 정보 (로그인, 인증)        │
│  - id, user_id, name, email, phone      │
│  - password, is_active, is_super_admin  │
└─────────────────────────────────────────┘
           │
           │ 1:N (테넌트별)
           ▼
┌─────────────────────────────────────────┐
│  사원 (Employee) - 테넌트별 프로필        │
│  - tenant_user_profiles 테이블           │
│  - 사원코드, 주민번호, 성별, 주소         │
│  - 급여, 입사일, 고용형태, 직급, 상태     │
│  - 부서, 직위, 직책, 상급자              │
└─────────────────────────────────────────┘

데이터 모델 전략

JSON 필드 활용 원칙:

  • 인덱싱 필요 필드 → 컬럼으로 유지 (employee_status)
  • 확장/복잡한 필드json_extra에 JSON으로 저장
  • 추후 인덱싱 필요시 → MySQL 가상 컬럼 + 인덱스로 보완 가능
users 테이블 (전역 - 시스템 계정)
├── 계정: id, user_id, name, email, phone, password
├── 상태: is_active, is_super_admin, must_change_password
├── 인증: email_verified_at, last_login_at, two_factor_*
└── 기타: options (개인 설정), profile_photo_path

tenant_user_profiles 테이블 (테넌트별 - 사원 정보)
├── 기존 컬럼: department_id, position_key, job_title_key, employment_type_key
├── 기존 컬럼: manager_user_id, profile_photo_path, display_name
├── 추가 컬럼: employee_status (인덱싱 필요)
└── json_extra: employee_code, resident_number, gender, address, salary,
                hire_date, rank, bank_account, work_type, contract_info...

사원 형태 다양성 지원:

  • 정규직, 계약직, 파트타임, 인턴 (기존 employment_type_key)
  • 일용직, 비정규직, 임시직, 외부 업무 (json_extra.work_type으로 확장)
  • 계약 정보, 외부 파견 정보 등 (json_extra.contract_info)

상태 필드 구분 (중요!)

시스템에는 3가지 상태 개념이 존재합니다:

필드 위치 의미 레벨
users.is_active users 테이블 시스템 계정 활성화 전역 (User) boolean
user_tenants.is_active pivot 테이블 테넌트 멤버십 활성화 테넌트별 boolean
employee_status tenant_user_profiles 고용 상태 테넌트별 'active'|'leave'|'resigned'
홍길동 (user_id: 1)
├── users.is_active = true (시스템 계정 활성)
├── 테넌트 A (HQ)
│   ├── user_tenants.is_active = true (멤버십 활성)
│   └── tenant_user_profiles.employee_status = 'active' (재직)
└── 테넌트 B (협력사)
    ├── user_tenants.is_active = true (멤버십 활성)
    └── tenant_user_profiles.employee_status = 'resigned' (퇴직)

시스템 동작:

// AuthService.php:42 - 로그인 시 users.is_active 체크
if (! $user->is_active) {
    Auth::logout();
    $this->loginError = '비활성화된 계정입니다.'; // 전체 시스템 차단
    return false;
}

테이블 구조

기존 테이블 분석 결과

테이블 목적 기존 컬럼
users 시스템 계정 (전역) id, user_id, name, email, phone, password, is_active, is_super_admin...
user_tenants 멤버십 관리 user_id, tenant_id, is_active, is_default, joined_at, left_at
tenant_user_profiles 사원 프로필 (테넌트별) tenant_id, user_id, department_id, position_key, job_title_key, employment_type_key, manager_user_id, json_extra...

tenant_user_profiles 확장 (json_extra 활용)

인덱싱 필요 컬럼만 추가:

// tenant_user_profiles 테이블에 추가 (마이그레이션)
$table->enum('employee_status', ['active', 'leave', 'resigned'])
      ->default('active')
      ->comment('고용상태 (인덱싱 필요)');

$table->index(['tenant_id', 'employee_status']);

나머지 사원 정보는 json_extra에 저장:

// json_extra 구조 예시
{
    "employee_code": "EMP-001",
    "resident_number": "encrypted_value",  // 암호화 저장
    "gender": "male",
    "address": {
        "zipCode": "12345",
        "address1": "서울시 강남구",
        "address2": "테헤란로 123"
    },
    "salary": 50000000,
    "hire_date": "2024-01-15",
    "rank": "대리",
    "bank_account": {
        "bankName": "신한은행",
        "accountNumber": "110-xxx-xxxxxx",
        "accountHolder": "홍길동"
    },
    "work_type": "regular",  // regular, daily, temporary, external
    "contract_info": {
        "start_date": "2024-01-15",
        "end_date": null,
        "external_company": null
    }
}

추후 인덱싱 필요시 (MySQL 가상 컬럼):

ALTER TABLE tenant_user_profiles
ADD employee_code_idx VARCHAR(50) AS (JSON_UNQUOTE(json_extra->'$.employee_code')) STORED,
ADD INDEX idx_employee_code (tenant_id, employee_code_idx);

기존 컬럼 활용:

  • employment_type_key: 고용형태 (정규직/계약직/파트타임/인턴)
  • position_key: 직위
  • job_title_key: 직책
  • department_id: 부서
  • profile_photo_path: 프로필 이미지

장점

  1. 완전한 분리: 사용자(전역) vs 사원(테넌트별) 명확한 구분
  2. 역할 분리: users(계정) / user_tenants(멤버십) / tenant_user_profiles(사원 프로필)
  3. 테넌트별 관리: 같은 사람이 테넌트별로 다른 사원코드, 급여, 직급 가능
  4. 기존 구조 활용: tenant_user_profiles 패턴 그대로 확장
  5. 유연한 확장: json_extra로 추가 속성 자유롭게 확장

🔍 상세 분석

1. 사원관리 (Employee Management)

정책: 사용자(users) = 전역 시스템 계정, 사원(tenant_user_profiles) = 테넌트별 인사 정보. 완전 분리.

React 컴포넌트 타입 정의 (types.ts)

interface Employee {
  id: string;
  name: string;
  employeeCode?: string;
  residentNumber?: string;
  phone?: string;
  email?: string;
  salary?: number;
  bankAccount?: BankAccount;
  profileImage?: string;
  gender?: Gender; // 'male' | 'female'
  address?: Address;
  hireDate?: string;
  employmentType?: EmploymentType; // 'regular' | 'contract' | 'parttime' | 'intern'
  rank?: string;
  status: EmployeeStatus; // 'active' | 'leave' | 'resigned'
  departmentPositions: DepartmentPosition[];
  userInfo?: UserInfo;
  createdAt: string;
  updatedAt: string;
}

React ↔ 테이블 매핑

전역 (users 테이블) - 시스템 계정 정보

React 필드 저장 위치 비고
id users.id PK
name users.name 이름
phone users.phone 전화번호
email users.email 이메일
userInfo.userId users.user_id 로그인 ID
userInfo.accountStatus users.is_active 계정 활성화

테넌트별 (tenant_user_profiles 테이블) - 사원 정보

React 필드 저장 위치 비고
profileImage tenant_user_profiles.profile_photo_path 기존 컬럼
status tenant_user_profiles.employee_status 신규 컬럼 (인덱싱)
employmentType tenant_user_profiles.employment_type_key 기존 컬럼
departmentPositions department_id + position_key 기존 컬럼
employeeCode json_extra.employee_code JSON
residentNumber json_extra.resident_number JSON (암호화)
gender json_extra.gender JSON
address json_extra.address JSON (객체)
salary json_extra.salary JSON
hireDate json_extra.hire_date JSON
rank json_extra.rank JSON
bankAccount json_extra.bank_account JSON (객체)
workType json_extra.work_type JSON (일용직/외부 등)
contractInfo json_extra.contract_info JSON (계약정보)

필요한 API 엔드포인트

Method Endpoint 설명 우선순위
GET /v1/employees 사원 목록 (검색, 필터, 페이지네이션) 🔴 필수
GET /v1/employees/{id} 사원 상세 🔴 필수
POST /v1/employees 사원 등록 (users 테이블에 생성) 🔴 필수
PATCH /v1/employees/{id} 사원 수정 🔴 필수
DELETE /v1/employees/{id} 사원 삭제 (soft delete) 🔴 필수
POST /v1/employees/bulk-delete 일괄 삭제 🟡 권장
POST /v1/employees/csv-upload CSV 일괄 등록 🟡 권장
POST /v1/employees/{id}/create-account 시스템 계정 생성 (password 설정) 🟢 선택
GET /v1/employees/stats 통계 (재직/휴직/퇴직/평균근속) 🟢 선택

필터/검색 파라미터

GET /v1/employees:
  q: string         # 이름, 사원코드, 이메일 검색
  status: string    # active, leave, resigned (tenant_user_profiles.employee_status)
  department_id: int
  has_account: bool # 시스템 계정 보유 여부 (password IS NOT NULL)
  page: int
  per_page: int

2. 부서관리 (Department Management)

React 컴포넌트 타입 정의 (types.ts)

interface Department {
  id: number;
  name: string;
  parentId: number | null;
  depth: number;           // 깊이 (0: 최상위, 무제한)
  children?: Department[]; // 하위 부서 (재귀)
}

기존 API vs React 요구사항

항목 기존 API React 요구사항
목록 조회 플랫 리스트 트리 구조 ⚠️ 변경 필요
parentId 없음 필요 ⚠️ 추가 필요
depth 없음 필요 ⚠️ 추가 필요
children 없음 필요 ⚠️ 추가 필요

기존 API 엔드포인트 (수정 필요)

# 현재
GET /v1/departments:
  - 플랫 리스트 반환
  - code, name, description, is_active, sort_order

# 필요한 변경
GET /v1/departments:
  - 트리 구조 반환 (parent_id, depth, children 포함)
  - 또는 ?tree=true 파라미터로 트리 반환 옵션

필요한 변경사항

항목 작업 내용 우선순위
DB 스키마 departments 테이블에 parent_id, depth 컬럼 추가 🔴 필수
Model Department 모델에 parent/children 관계 추가 🔴 필수
Service 트리 구조 빌드 로직 추가 🔴 필수
Controller 트리 조회 옵션 추가 🔴 필수

3. 근태관리 (Attendance Management)

복잡한 출퇴근 시나리오 지원:

  • GPS 자동 출퇴근 (위치 기반 자동 체크인/아웃)
  • 다양한 사원 형태: 일용직, 비정규직, 임시직, 외부 업무
  • 외부 근무: 출장, 파견, 현장 업무, 재택근무
  • 복수 출퇴근: 하루 여러 번 출입 기록 가능

React 컴포넌트 타입 정의 (types.ts)

interface AttendanceRecord {
  id: string;
  employeeId: string;
  employeeName: string;
  department: string;
  position: string;
  rank: string;
  baseDate: string;
  checkIn: string | null;
  checkOut: string | null;
  breakTime: string | null;
  overtimeHours: string | null;
  reason: {
    type: ReasonType;
    label: string;
    documentId?: string;
  } | null;
  status: AttendanceStatus;
  // GPS/위치 정보 (json_details에 저장)
  gpsData?: {
    checkInLocation?: { lat: number; lng: number; address?: string };
    checkOutLocation?: { lat: number; lng: number; address?: string };
    isAutoChecked?: boolean;  // GPS 자동 체크인 여부
  };
  // 외부 근무 정보
  externalWork?: {
    type: 'dispatch' | 'fieldWork' | 'remote';
    location?: string;
    company?: string;  // 파견처
  };
  createdAt: string;
  updatedAt: string;
}

type AttendanceStatus =
  'onTime' | 'late' | 'absent' | 'vacation' |
  'businessTrip' | 'fieldWork' | 'overtime' | 'remote';

type ReasonType =
  'businessTripRequest' | 'vacationRequest' |
  'fieldWorkRequest' | 'overtimeRequest' | 'remoteRequest';

필요한 API 엔드포인트

Method Endpoint 설명 우선순위
GET /v1/attendances 근태 목록 (날짜범위, 필터, 페이지네이션) 🔴 필수
GET /v1/attendances/{id} 근태 상세 🟡 권장
POST /v1/attendances 근태 등록 🔴 필수
PATCH /v1/attendances/{id} 근태 수정 🔴 필수
POST /v1/attendances/reason 사유 등록 (문서 연결) 🟡 권장
GET /v1/attendances/stats 통계 (정시/지각/결근/휴가) 🟢 선택
GET /v1/attendances/export 엑셀 다운로드 🟢 선택

필터/검색 파라미터

GET /v1/attendances:
  q: string           # 이름, 부서 검색
  status: string      # onTime, late, absent, vacation, etc.
  date_from: date     # 시작일
  date_to: date       # 종료일
  department_id: int
  sort_by: string     # rank, department, name
  sort_dir: string    # asc, desc
  page: int
  per_page: int

🏢 MNG 구현체 분석

1. MNG UserService (사용자관리)

MNG에 이미 구현된 사용자 관리 기능으로, Employee API 개발 시 참조할 수 있습니다.

구현된 기능

기능 메서드 설명
목록 조회 getUsers() 페이지네이션, 검색, 필터, Soft Delete 필터
상세 조회 getUserById() 단일 사용자 조회
모달 조회 getUserForModal() 역할/부서/권한 카운트 포함
생성 createUser() 비밀번호 자동생성/입력, 메일 발송, 역할/부서 동기화
수정 updateUser() 비밀번호 선택적 업데이트, 역할/부서 동기화
삭제 deleteUser() Soft Delete
복원 restoreUser() Soft Delete 복원
영구삭제 forceDeleteUser() 아카이브 저장 후 영구 삭제
비밀번호 초기화 resetPassword() 임의 비밀번호 생성 + 메일 발송
역할 동기화 syncRoles() 테넌트별 역할 관리
부서 동기화 syncDepartments() 테넌트별 부서 관리

핵심 코드 패턴

// 멀티테넌시 패턴
$tenantId = session('selected_tenant_id');

// 역할/부서 Eager Loading (테넌트별)
$query->with([
    'userRoles' => fn ($q) => $q->where('tenant_id', $tenantId)->with('role'),
    'departmentUsers' => fn ($q) => $q->where('tenant_id', $tenantId)->with('department'),
]);

// 슈퍼관리자 보호
if (! auth()->user()?->is_super_admin) {
    $query->where('is_super_admin', false);
}

React Employee vs MNG User 매핑

React Employee 필드 MNG User 필드 차이점
id id 동일
name name 동일
email email 동일
phone phone 동일
status (active/leave/resigned) is_active (boolean) ⚠️ 확장 필요
employeeCode 없음 추가 필요
residentNumber 없음 추가 필요
salary 없음 추가 필요
bankAccount 없음 추가 필요
hireDate 없음 추가 필요
employmentType 없음 추가 필요
rank 없음 추가 필요
departmentPositions[] departmentUsers ⚠️ position 추가 필요
userInfo (userId, role) 기본 필드 User 자체가 계정 정보

2. MNG DepartmentService (부서관리)

MNG에 이미 구현된 부서 관리 기능으로, 트리 구조를 지원합니다.

구현된 기능

기능 메서드 설명
목록 조회 getDepartments() 페이지네이션, 검색, 필터, parent 포함
상세 조회 getDepartmentById() parent/children 관계 포함
생성 createDepartment() parent_id 지원
수정 updateDepartment() 자기참조 방지 로직 포함
삭제 deleteDepartment() 하위 부서 보호, Soft Delete
복원 restoreDepartment() Soft Delete 복원
영구삭제 forceDeleteDepartment() 하위 부서 보호
코드 중복 체크 isCodeExists() 테넌트별 코드 유니크 검사
활성 목록 getActiveDepartments() 드롭다운용 (id, parent_id, code, name)
통계 getDepartmentStats() total, active, inactive

핵심 코드 패턴

// 트리 구조 관계 정의 (Model)
public function parent(): BelongsTo
{
    return $this->belongsTo(self::class, 'parent_id');
}

public function children(): HasMany
{
    return $this->hasMany(self::class, 'parent_id');
}

// 하위 부서 보호 (삭제 시)
if ($department->children()->count() > 0) {
    return false;
}

// 자기 참조 방지
if (isset($data['parent_id']) && $data['parent_id'] == $id) {
    return false;
}

React Department vs MNG Department 매핑

React Department 필드 MNG Department 필드 상태
id id 동일
name name 동일
parentId parent_id 동일
depth 없음 추가 필요 (계산 가능)
children[] children() 관계 동일
- code MNG 추가 필드
- description MNG 추가 필드
- is_active MNG 추가 필드
- sort_order MNG 추가 필드

🔗 통합 개발 전략

옵션 A: API에서 새로 구현 (권장)

MNG 코드를 참조하여 API에 새로운 Employee/Attendance 리소스 구현

장점:

  • API 규칙(FormRequest, Swagger, i18n) 완전 준수
  • React 요구사항에 최적화된 설계
  • 독립적인 개발/테스트 가능

단점:

  • 개발 시간 소요
  • 일부 코드 중복

개발 방안: MNG 참조하여 API 구현

  1. DepartmentService 패턴 참조: 트리 구조 로직 (parent/children, 자기참조 방지, 하위 보호)
  2. UserService 패턴 참조: 역할/부서 동기화, 비밀번호 관리, 아카이브
  3. API 규칙 적용: FormRequest, Swagger, BelongsToTenant, 감사 로그

📊 작업 계획

Phase 1: 데이터베이스 스키마 (0.5일)

1.1 마이그레이션 파일 생성

# 부서 테이블 수정 (트리 구조)
php artisan make:migration add_tree_fields_to_departments_table

# tenant_user_profiles 테이블 수정 (사원 정보 확장)
php artisan make:migration add_employee_fields_to_tenant_user_profiles_table

# 근태 테이블 생성
php artisan make:migration create_attendances_table

1.2 tenant_user_profiles 테이블 확장 (인덱싱 필드만)

Schema::table('tenant_user_profiles', function (Blueprint $table) {
    // 인덱싱 필요한 필드만 컬럼으로 추가
    $table->enum('employee_status', ['active', 'leave', 'resigned'])
          ->default('active')
          ->comment('고용상태 (인덱싱 필요)');

    $table->index(['tenant_id', 'employee_status']);

    // 나머지 사원 정보는 기존 json_extra 컬럼 활용
    // json_extra 구조:
    // {
    //     "employee_code": "EMP-001",
    //     "resident_number": "암호화된값",
    //     "gender": "male",
    //     "address": { "zipCode": "", "address1": "", "address2": "" },
    //     "salary": 50000000,
    //     "hire_date": "2024-01-15",
    //     "rank": "대리",
    //     "bank_account": { "bankName": "", "accountNumber": "", "accountHolder": "" },
    //     "work_type": "regular",  // regular, daily, temporary, external
    //     "contract_info": { "start_date": "", "end_date": null, "external_company": null }
    // }
});

1.3 attendances 테이블 스키마 (json_details 활용)

인덱싱 필요 필드만 컬럼으로, 나머지는 JSON:

Schema::create('attendances', function (Blueprint $table) {
    $table->id();

    // 인덱싱 필요 필드 (컬럼)
    $table->unsignedBigInteger('tenant_id');
    $table->unsignedBigInteger('user_id');
    $table->date('base_date');
    $table->enum('status', [
        'onTime', 'late', 'absent', 'vacation',
        'businessTrip', 'fieldWork', 'overtime', 'remote'
    ])->default('onTime');

    // 확장 가능한 상세 정보 (JSON)
    $table->json('json_details')->nullable()->comment('출퇴근 상세 정보');

    // 감사 로그
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->timestamps();
    $table->softDeletes();

    // 인덱스
    $table->foreign('tenant_id')->references('id')->on('tenants');
    $table->foreign('user_id')->references('id')->on('users');
    $table->unique(['tenant_id', 'user_id', 'base_date']);
    $table->index(['tenant_id', 'base_date', 'status']);
});

json_details 구조:

{
    "check_in": "09:00:00",
    "check_out": "18:00:00",
    "break_time": "01:00",
    "overtime_hours": "02:00",
    "reason": {
        "type": "vacationRequest",
        "label": "연차",
        "document_id": "DOC-001"
    },
    "gps_data": {
        "check_in_location": { "lat": 37.5665, "lng": 126.9780, "address": "서울시 중구" },
        "check_out_location": { "lat": 37.5665, "lng": 126.9780, "address": "서울시 중구" },
        "is_auto_checked": true
    },
    "external_work": {
        "type": "dispatch",
        "location": "협력사 A",
        "company": "ABC Corp"
    },
    "multiple_entries": [
        { "in": "09:00", "out": "12:00" },
        { "in": "13:00", "out": "18:00" }
    ]
}

Phase 2: 사원관리 API (1.5일)

정책: User + TenantUserProfile 모델 활용. 사원 정보는 tenant_user_profiles 테이블에 저장.

2.1 파일 생성/수정 목록

api/app/Models/Tenants/TenantUserProfile.php (수정: 사원 필드 확장)
api/app/Services/EmployeeService.php (신규: 사원 CRUD, MNG UserService 참조)
api/app/Http/Controllers/Api/V1/EmployeeController.php (신규)
api/app/Http/Requests/Employee/IndexRequest.php
api/app/Http/Requests/Employee/StoreRequest.php
api/app/Http/Requests/Employee/UpdateRequest.php
api/app/Swagger/v1/EmployeeApi.php

2.2 TenantUserProfile 모델 확장 (json_extra 활용)

// api/app/Models/Tenants/TenantUserProfile.php에 추가

// fillable - employee_status만 신규 컬럼
protected $fillable = [
    'tenant_id', 'user_id', 'department_id',
    'position_key', 'job_title_key', 'work_location_key', 'employment_type_key',
    'manager_user_id', 'json_extra', 'profile_photo_path', 'display_name',
    'employee_status',  // 신규 컬럼 (인덱싱 필요)
];

// casts - json_extra를 array로
protected $casts = [
    'json_extra' => 'array',
];

// json_extra 헬퍼 메서드
public function getEmployeeCodeAttribute(): ?string
{
    return $this->json_extra['employee_code'] ?? null;
}

public function getSalaryAttribute(): ?float
{
    return $this->json_extra['salary'] ?? null;
}

public function getHireDateAttribute(): ?string
{
    return $this->json_extra['hire_date'] ?? null;
}

public function getRankAttribute(): ?string
{
    return $this->json_extra['rank'] ?? null;
}

public function getBankAccountAttribute(): ?array
{
    return $this->json_extra['bank_account'] ?? null;
}

public function getWorkTypeAttribute(): ?string
{
    return $this->json_extra['work_type'] ?? 'regular';
}

// json_extra 일괄 업데이트
public function updateEmployeeInfo(array $data): void
{
    $jsonExtra = $this->json_extra ?? [];
    $allowedKeys = [
        'employee_code', 'resident_number', 'gender', 'address',
        'salary', 'hire_date', 'rank', 'bank_account',
        'work_type', 'contract_info'
    ];

    foreach ($allowedKeys as $key) {
        if (isset($data[$key])) {
            $jsonExtra[$key] = $data[$key];
        }
    }

    $this->json_extra = $jsonExtra;
}

// 사용자 관계
public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}

// 시스템 계정 보유 여부
public function getHasAccountAttribute(): bool
{
    return $this->user?->password !== null;
}

2.3 라우트 추가 (routes/api.php)

Route::prefix('employees')->group(function () {
    Route::get('', [EmployeeController::class, 'index'])->name('v1.employees.index');
    Route::post('', [EmployeeController::class, 'store'])->name('v1.employees.store');
    Route::get('/stats', [EmployeeController::class, 'stats'])->name('v1.employees.stats');
    Route::get('/{id}', [EmployeeController::class, 'show'])->name('v1.employees.show');
    Route::patch('/{id}', [EmployeeController::class, 'update'])->name('v1.employees.update');
    Route::delete('/{id}', [EmployeeController::class, 'destroy'])->name('v1.employees.destroy');
    Route::post('/bulk-delete', [EmployeeController::class, 'bulkDelete'])->name('v1.employees.bulkDelete');
    Route::post('/csv-upload', [EmployeeController::class, 'csvUpload'])->name('v1.employees.csvUpload');
    Route::post('/{id}/create-account', [EmployeeController::class, 'createAccount'])->name('v1.employees.createAccount');
});

Phase 3: 부서관리 API 수정 (1일)

3.1 수정 파일 목록

api/database/migrations/xxxx_add_tree_fields_to_departments_table.php
api/app/Models/Tenants/Department.php (수정)
api/app/Services/DepartmentService.php (수정)
api/app/Swagger/v1/DepartmentApi.php (수정)

3.2 핵심 변경사항

// Department.php
public function parent(): BelongsTo
{
    return $this->belongsTo(self::class, 'parent_id');
}

public function children(): HasMany
{
    return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order');
}

// DepartmentService.php - 트리 빌드 메서드 추가
public function getTree(): array
{
    $departments = Department::with('children')
        ->whereNull('parent_id')
        ->orderBy('sort_order')
        ->get();

    return $this->buildTree($departments);
}

Phase 4: 근태관리 API (2일)

4.1 파일 생성 목록

api/app/Models/Members/Attendance.php
api/app/Services/AttendanceService.php
api/app/Http/Controllers/Api/V1/AttendanceController.php
api/app/Http/Requests/Attendance/IndexRequest.php
api/app/Http/Requests/Attendance/StoreRequest.php
api/app/Http/Requests/Attendance/UpdateRequest.php
api/app/Swagger/v1/AttendanceApi.php

4.2 라우트 추가 (routes/api.php)

Route::prefix('attendances')->group(function () {
    Route::get('', [AttendanceController::class, 'index'])->name('v1.attendances.index');
    Route::post('', [AttendanceController::class, 'store'])->name('v1.attendances.store');
    Route::get('/stats', [AttendanceController::class, 'stats'])->name('v1.attendances.stats');
    Route::get('/export', [AttendanceController::class, 'export'])->name('v1.attendances.export');
    Route::get('/{id}', [AttendanceController::class, 'show'])->name('v1.attendances.show');
    Route::patch('/{id}', [AttendanceController::class, 'update'])->name('v1.attendances.update');
    Route::post('/reason', [AttendanceController::class, 'storeReason'])->name('v1.attendances.storeReason');
});

📌 체크리스트

Phase 1: 데이터베이스 스키마

  • departments 테이블에 parent_id, depth 추가
  • tenant_user_profiles 테이블에 employee_status 컬럼 추가 (인덱싱 필요)
  • tenant_user_profiles 인덱스 추가: (tenant_id, employee_status)
  • attendances 테이블 생성 (json_details 포함)
  • attendances 인덱스 추가: (tenant_id, base_date, status)
  • 마이그레이션 실행 및 검증

Phase 2: 사원관리 API

  • TenantUserProfile 모델: employee_status fillable 추가
  • TenantUserProfile 모델: json_extra 헬퍼 메서드 추가
  • TenantUserProfile 모델: updateEmployeeInfo() 메서드 추가
  • EmployeeService 구현 (MNG UserService 참조)
  • EmployeeController 구현
  • FormRequest 생성 (Index, Store, Update)
  • Swagger 문서 작성
  • 라우트 등록
  • Pint 실행
  • 테스트

Phase 3: 부서관리 API 수정

  • Department 모델에 parent/children 관계 추가
  • DepartmentService에 트리 빌드 로직 추가
  • 트리 조회 엔드포인트 추가/수정
  • Swagger 문서 업데이트
  • 테스트

Phase 4: 근태관리 API

  • Attendance 모델 생성 (json_details casts 포함)
  • Attendance 모델: json_details 헬퍼 메서드 추가
  • AttendanceService 구현 (GPS 자동 체크인 로직)
  • AttendanceController 구현
  • FormRequest 생성 (Index, Store, Update)
  • Swagger 문서 작성
  • 라우트 등록
  • 엑셀 Export 기능
  • Pint 실행
  • 테스트

🔗 관련 문서

  • React 커밋: 48dbba0 - feat: 단가관리 페이지 마이그레이션 및 HR 관리 기능 추가
  • 기존 API 규칙: docs/reference/api-rules.md
  • 품질 체크리스트: docs/reference/quality-checklist.md

MNG 참조 파일

파일 경로 용도
UserService mng/app/Services/UserService.php Employee 서비스 참조
DepartmentService mng/app/Services/DepartmentService.php 트리 구조 로직 참조
UserController (API) mng/app/Http/Controllers/Api/Admin/UserController.php API 컨트롤러 패턴
DepartmentController (API) mng/app/Http/Controllers/Api/Admin/DepartmentController.php 트리 API 패턴
User Model mng/app/Models/User.php 관계 정의 참조
Department Model mng/app/Models/Tenants/Department.php 트리 관계 정의

📝 참고사항

  1. 멀티테넌시: 모든 테이블에 tenant_id 필수, BelongsToTenant 스코프 적용
  2. 감사 로그: created_by, updated_by, deleted_by 컬럼 필수
  3. Soft Delete: 모든 테이블에 deleted_at 적용
  4. i18n: 모든 메시지는 __('message.xxx') 형식 사용
  5. Swagger: 모든 엔드포인트에 문서 작성 필수

📊 최종 요약

🔑 핵심 정책

JSON 필드 활용 원칙:

  • 인덱싱 필요 필드 → 컬럼으로 유지
  • 확장/복잡한 필드 → JSON 컬럼에 저장
  • 추후 인덱싱 필요시 → MySQL 가상 컬럼 + 인덱스
사용자(User)와 사원(Employee) 완전 분리

users 테이블 (전역 - 시스템 계정)
├── 계정: id, user_id, name, email, phone, password
├── 상태: is_active, is_super_admin, must_change_password
└── 인증: email_verified_at, last_login_at, two_factor_*

tenant_user_profiles 테이블 (테넌트별 - 사원 정보)
├── 컬럼: employee_status (인덱싱), department_id, position_key, employment_type_key
└── json_extra: employee_code, resident_number, gender, address,
                salary, hire_date, rank, bank_account, work_type, contract_info

attendances 테이블 (근태 - json_details 활용)
├── 컬럼: base_date, status (인덱싱)
└── json_details: check_in, check_out, gps_data, external_work, multiple_entries

상태 필드 구분

필드 의미 레벨
users.is_active 시스템 계정 활성화 전역
user_tenants.is_active 테넌트 멤버십 활성화 테넌트별
tenant_user_profiles.employee_status 고용 상태 (재직/휴직/퇴직) 테넌트별

프로젝트별 현황

프로젝트 사원관리 부서관리 근태관리 비고
React UI 완성 (Mock) UI 완성 (Mock) UI 완성 (Mock) API 연동 필요
API 없음 ⚠️ 플랫 구조 없음 신규 개발 필요
MNG User (참조용) 트리 구조 없음 API 개발 참조

핵심 결론

  1. Employee API: tenant_user_profiles.json_extra 활용

    • 사용자(users) = 전역 시스템 계정
    • 사원(tenant_user_profiles) = 테넌트별 인사 정보
    • employee_status만 컬럼, 나머지는 json_extra에 JSON으로 저장
  2. Department API: 기존 API + MNG 트리 구조 패턴 병합

    • 추가: parent_id, depth, children 관계
  3. Attendance API: json_details 활용

    • 인덱싱 필요 필드(base_date, status)만 컬럼
    • 나머지는 json_details에 JSON으로 저장
    • GPS 자동 출퇴근, 외부 근무, 복수 출퇴근 지원
  4. 개발 순서: Phase 1(DB) → Phase 2(Employee) → Phase 3(Department 수정) → Phase 4(Attendance)

예상 작업량

Phase 내용 예상 일수
Phase 1 DB 스키마 (마이그레이션) 0.5일
Phase 2 사원관리 API (json_extra 활용) 1.5일
Phase 3 부서관리 API 수정 1일
Phase 4 근태관리 API (json_details 활용) 2일
합계 5일

장점 요약

  1. 완전한 분리: 사용자(전역) vs 사원(테넌트별) 명확한 구분
  2. 역할 분리: users(계정) / user_tenants(멤버십) / tenant_user_profiles(사원 프로필)
  3. 테넌트별 관리: 같은 사람이 테넌트별로 다른 사원코드, 급여, 직급 가능
  4. JSON 활용: 스키마 변경 없이 필드 추가 가능, 유연한 확장
  5. 인덱싱 최적화: 검색 필요 필드만 컬럼, 나머지 JSON으로 효율적 관리
  6. 복잡한 시나리오 지원: GPS 출퇴근, 다양한 사원 형태, 복수 출퇴근 등