feat: 급여 관리 API 및 더미 시더 확장
- 급여 관리 API 추가 (SalaryController, SalaryService, Salary 모델) - 급여 목록/상세/등록/수정/삭제 - 상태 변경 (scheduled/completed) - 일괄 상태 변경 - 통계 조회 - 더미 시더 확장 - 근태, 휴가, 부서, 사용자 시더 추가 - 기존 시더 tenant_id/created_by/updated_by 필드 추가 - 부서 서비스 개선 (tree 조회 기능 추가) - 카드 서비스 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
84
.serena/project.yml
Normal file
84
.serena/project.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp csharp_omnisharp
|
||||
# dart elixir elm erlang fortran go
|
||||
# haskell java julia kotlin lua markdown
|
||||
# nix perl php python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala swift
|
||||
# terraform typescript typescript_vts yaml zig
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# - csharp: Requires the presence of a .sln file in the project folder.
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- php
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "api"
|
||||
included_optional_tools: []
|
||||
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2025-12-24 19:31:13
|
||||
> **자동 생성**: 2025-12-25 00:41:11
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -553,6 +553,12 @@ ### leave_balances
|
||||
|
||||
- **user()**: belongsTo → `users`
|
||||
|
||||
### leave_grants
|
||||
**모델**: `App\Models\Tenants\LeaveGrant`
|
||||
|
||||
- **user()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### loans
|
||||
**모델**: `App\Models\Tenants\Loan`
|
||||
|
||||
|
||||
126
app/Http/Controllers/Api/V1/SalaryController.php
Normal file
126
app/Http/Controllers/Api/V1/SalaryController.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Salary\BulkUpdateStatusRequest;
|
||||
use App\Http\Requests\V1\Salary\StoreSalaryRequest;
|
||||
use App\Http\Requests\V1\Salary\UpdateSalaryRequest;
|
||||
use App\Services\SalaryService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SalaryController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SalaryService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 급여 목록
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'search',
|
||||
'year',
|
||||
'month',
|
||||
'status',
|
||||
'employee_id',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
'per_page',
|
||||
'page',
|
||||
]);
|
||||
|
||||
$salaries = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($salaries, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 등록
|
||||
*/
|
||||
public function store(StoreSalaryRequest $request)
|
||||
{
|
||||
$salary = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($salary, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 상세
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$salary = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($salary, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 수정
|
||||
*/
|
||||
public function update(int $id, UpdateSalaryRequest $request)
|
||||
{
|
||||
$salary = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($salary, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->service->destroy($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 상태 변경 (지급완료/지급예정)
|
||||
*/
|
||||
public function updateStatus(int $id, Request $request)
|
||||
{
|
||||
$status = $request->input('status', 'completed');
|
||||
$salary = $this->service->updateStatus($id, $status);
|
||||
|
||||
return ApiResponse::success($salary, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 일괄 상태 변경
|
||||
*/
|
||||
public function bulkUpdateStatus(BulkUpdateStatusRequest $request)
|
||||
{
|
||||
$count = $this->service->bulkUpdateStatus(
|
||||
$request->input('ids'),
|
||||
$request->input('status')
|
||||
);
|
||||
|
||||
return ApiResponse::success(
|
||||
['updated_count' => $count],
|
||||
__('message.bulk_updated')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 통계 조회
|
||||
*/
|
||||
public function statistics(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'year',
|
||||
'month',
|
||||
'start_date',
|
||||
'end_date',
|
||||
]);
|
||||
|
||||
$stats = $this->service->getStatistics($params);
|
||||
|
||||
return ApiResponse::success($stats, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/V1/Salary/BulkUpdateStatusRequest.php
Normal file
32
app/Http/Requests/V1/Salary/BulkUpdateStatusRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Salary;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BulkUpdateStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => ['required', 'array', 'min:1'],
|
||||
'ids.*' => ['required', 'integer'],
|
||||
'status' => ['required', 'string', 'in:scheduled,completed'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ids.required' => __('validation.required', ['attribute' => 'ID 목록']),
|
||||
'ids.min' => __('validation.min.array', ['attribute' => 'ID 목록', 'min' => 1]),
|
||||
'status.required' => __('validation.required', ['attribute' => '상태']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Http/Requests/V1/Salary/StoreSalaryRequest.php
Normal file
44
app/Http/Requests/V1/Salary/StoreSalaryRequest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Salary;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreSalaryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'employee_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'year' => ['required', 'integer', 'min:2020', 'max:2100'],
|
||||
'month' => ['required', 'integer', 'min:1', 'max:12'],
|
||||
'base_salary' => ['required', 'numeric', 'min:0'],
|
||||
'total_allowance' => ['nullable', 'numeric', 'min:0'],
|
||||
'total_overtime' => ['nullable', 'numeric', 'min:0'],
|
||||
'total_bonus' => ['nullable', 'numeric', 'min:0'],
|
||||
'total_deduction' => ['nullable', 'numeric', 'min:0'],
|
||||
'allowance_details' => ['nullable', 'array'],
|
||||
'allowance_details.*' => ['nullable', 'array'],
|
||||
'deduction_details' => ['nullable', 'array'],
|
||||
'deduction_details.*' => ['nullable', 'array'],
|
||||
'payment_date' => ['nullable', 'date'],
|
||||
'status' => ['nullable', 'string', 'in:scheduled,completed'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'employee_id.required' => __('validation.required', ['attribute' => '직원']),
|
||||
'employee_id.exists' => __('validation.exists', ['attribute' => '직원']),
|
||||
'year.required' => __('validation.required', ['attribute' => '년도']),
|
||||
'month.required' => __('validation.required', ['attribute' => '월']),
|
||||
'base_salary.required' => __('validation.required', ['attribute' => '기본급']),
|
||||
];
|
||||
}
|
||||
}
|
||||
33
app/Http/Requests/V1/Salary/UpdateSalaryRequest.php
Normal file
33
app/Http/Requests/V1/Salary/UpdateSalaryRequest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Salary;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateSalaryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'employee_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'year' => ['nullable', 'integer', 'min:2020', 'max:2100'],
|
||||
'month' => ['nullable', 'integer', 'min:1', 'max:12'],
|
||||
'base_salary' => ['nullable', 'numeric', 'min:0'],
|
||||
'total_allowance' => ['nullable', 'numeric', 'min:0'],
|
||||
'total_overtime' => ['nullable', 'numeric', 'min:0'],
|
||||
'total_bonus' => ['nullable', 'numeric', 'min:0'],
|
||||
'total_deduction' => ['nullable', 'numeric', 'min:0'],
|
||||
'allowance_details' => ['nullable', 'array'],
|
||||
'allowance_details.*' => ['nullable', 'array'],
|
||||
'deduction_details' => ['nullable', 'array'],
|
||||
'deduction_details.*' => ['nullable', 'array'],
|
||||
'payment_date' => ['nullable', 'date'],
|
||||
'status' => ['nullable', 'string', 'in:scheduled,completed'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,18 @@ class User extends Authenticatable
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'has_account',
|
||||
];
|
||||
|
||||
/**
|
||||
* 시스템 계정(비밀번호) 보유 여부
|
||||
*/
|
||||
public function getHasAccountAttribute(): bool
|
||||
{
|
||||
return ! empty($this->password);
|
||||
}
|
||||
|
||||
public function userTenants()
|
||||
{
|
||||
return $this->hasMany(UserTenant::class);
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class Department extends Model
|
||||
{
|
||||
use HasRoles, ModelTrait; // 부서도 권한/역할을 가짐
|
||||
use HasRoles, ModelTrait, SoftDeletes; // 부서도 권한/역할을 가짐
|
||||
|
||||
protected $table = 'departments';
|
||||
|
||||
|
||||
165
app/Models/Tenants/Salary.php
Normal file
165
app/Models/Tenants/Salary.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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 int $employee_id
|
||||
* @property int $year
|
||||
* @property int $month
|
||||
* @property float $base_salary
|
||||
* @property float $total_allowance
|
||||
* @property float $total_overtime
|
||||
* @property float $total_bonus
|
||||
* @property float $total_deduction
|
||||
* @property float $net_payment
|
||||
* @property array|null $allowance_details
|
||||
* @property array|null $deduction_details
|
||||
* @property string|null $payment_date
|
||||
* @property string $status
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
*/
|
||||
class Salary extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'salaries';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'employee_id',
|
||||
'year',
|
||||
'month',
|
||||
'base_salary',
|
||||
'total_allowance',
|
||||
'total_overtime',
|
||||
'total_bonus',
|
||||
'total_deduction',
|
||||
'net_payment',
|
||||
'allowance_details',
|
||||
'deduction_details',
|
||||
'payment_date',
|
||||
'status',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'base_salary' => 'decimal:2',
|
||||
'total_allowance' => 'decimal:2',
|
||||
'total_overtime' => 'decimal:2',
|
||||
'total_bonus' => 'decimal:2',
|
||||
'total_deduction' => 'decimal:2',
|
||||
'net_payment' => 'decimal:2',
|
||||
'allowance_details' => 'array',
|
||||
'deduction_details' => 'array',
|
||||
'payment_date' => 'date',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => 'scheduled',
|
||||
'base_salary' => 0,
|
||||
'total_allowance' => 0,
|
||||
'total_overtime' => 0,
|
||||
'total_bonus' => 0,
|
||||
'total_deduction' => 0,
|
||||
'net_payment' => 0,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 직원
|
||||
*/
|
||||
public function employee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'employee_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 지급 완료 여부
|
||||
*/
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 지급 예정 여부
|
||||
*/
|
||||
public function isScheduled(): bool
|
||||
{
|
||||
return $this->status === 'scheduled';
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경
|
||||
*/
|
||||
public function markAsCompleted(): void
|
||||
{
|
||||
$this->status = 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경
|
||||
*/
|
||||
public function markAsScheduled(): void
|
||||
{
|
||||
$this->status = 'scheduled';
|
||||
}
|
||||
|
||||
/**
|
||||
* 실지급액 계산
|
||||
*/
|
||||
public function calculateNetPayment(): float
|
||||
{
|
||||
return $this->base_salary
|
||||
+ $this->total_allowance
|
||||
+ $this->total_overtime
|
||||
+ $this->total_bonus
|
||||
- $this->total_deduction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 년월 표시
|
||||
*/
|
||||
public function getPeriodLabel(): string
|
||||
{
|
||||
return sprintf('%d년 %d월', $this->year, $this->month);
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,11 @@ public function index(array $params): LengthAwarePaginator
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Card::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['assignedUser:id,name,email', 'assignedUser.department', 'assignedUser.position']);
|
||||
|
||||
// 검색 필터
|
||||
if (! empty($params['search'])) {
|
||||
if (!empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('card_name', 'like', "%{$search}%")
|
||||
@@ -29,12 +30,12 @@ public function index(array $params): LengthAwarePaginator
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
if (!empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 담당자 필터
|
||||
if (! empty($params['assigned_user_id'])) {
|
||||
if (!empty($params['assigned_user_id'])) {
|
||||
$query->where('assigned_user_id', $params['assigned_user_id']);
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ public function show(int $id): Card
|
||||
|
||||
return Card::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['assignedUser:id,name'])
|
||||
->with(['assignedUser:id,name,email', 'assignedUser.department', 'assignedUser.position'])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
@@ -82,7 +83,7 @@ public function store(array $data): Card
|
||||
$card->created_by = $userId;
|
||||
$card->updated_by = $userId;
|
||||
|
||||
if (! empty($data['card_password'])) {
|
||||
if (!empty($data['card_password'])) {
|
||||
$card->setCardPassword($data['card_password']);
|
||||
}
|
||||
|
||||
@@ -193,7 +194,7 @@ public function getActiveCards(): array
|
||||
'id' => $card->id,
|
||||
'card_name' => $card->card_name,
|
||||
'card_company' => $card->card_company,
|
||||
'display_number' => '****-'.$card->card_number_last4,
|
||||
'display_number' => '****-' . $card->card_number_last4,
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
@@ -111,6 +111,7 @@ public function store(array $params)
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$p = $this->v($params, [
|
||||
'parent_id' => 'nullable|integer|min:1',
|
||||
'code' => 'nullable|string|max:50',
|
||||
'name' => 'required|string|max:100',
|
||||
'description' => 'nullable|string|max:255',
|
||||
@@ -125,6 +126,14 @@ public function store(array $params)
|
||||
return $p;
|
||||
}
|
||||
|
||||
// parent_id 유효성 검사
|
||||
if (! empty($p['parent_id'])) {
|
||||
$parent = Department::query()->find($p['parent_id']);
|
||||
if (! $parent) {
|
||||
return ['error' => '상위 부서를 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($p['code'])) {
|
||||
$exists = Department::query()->where('code', $p['code'])->exists();
|
||||
if ($exists) {
|
||||
@@ -134,6 +143,7 @@ public function store(array $params)
|
||||
|
||||
$dept = Department::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'parent_id' => $p['parent_id'] ?? null,
|
||||
'code' => $p['code'] ?? null,
|
||||
'name' => $p['name'],
|
||||
'description' => $p['description'] ?? null,
|
||||
@@ -169,6 +179,7 @@ public function update(int $id, array $params)
|
||||
}
|
||||
|
||||
$p = $this->v($params, [
|
||||
'parent_id' => 'nullable|integer|min:0', // 0이면 최상위로 이동
|
||||
'code' => 'nullable|string|max:50',
|
||||
'name' => 'nullable|string|max:100',
|
||||
'description' => 'nullable|string|max:255',
|
||||
@@ -188,6 +199,22 @@ public function update(int $id, array $params)
|
||||
return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
// parent_id 유효성 검사
|
||||
if (array_key_exists('parent_id', $p)) {
|
||||
$parentId = $p['parent_id'];
|
||||
if ($parentId === 0) {
|
||||
$parentId = null; // 0이면 최상위로 이동
|
||||
} elseif ($parentId) {
|
||||
if ($parentId === $id) {
|
||||
return ['error' => '자기 자신을 상위 부서로 설정할 수 없습니다.', 'code' => 422];
|
||||
}
|
||||
$parent = Department::query()->find($parentId);
|
||||
if (! $parent) {
|
||||
return ['error' => '상위 부서를 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('code', $p) && ! is_null($p['code'])) {
|
||||
$exists = Department::query()
|
||||
->where('code', $p['code'])
|
||||
@@ -198,14 +225,21 @@ public function update(int $id, array $params)
|
||||
}
|
||||
}
|
||||
|
||||
$dept->fill([
|
||||
$fillData = [
|
||||
'code' => array_key_exists('code', $p) ? $p['code'] : $dept->code,
|
||||
'name' => $p['name'] ?? $dept->name,
|
||||
'description' => $p['description'] ?? $dept->description,
|
||||
'is_active' => isset($p['is_active']) ? (int) $p['is_active'] : $dept->is_active,
|
||||
'sort_order' => $p['sort_order'] ?? $dept->sort_order,
|
||||
'updated_by' => $p['updated_by'] ?? $dept->updated_by,
|
||||
])->save();
|
||||
];
|
||||
|
||||
// parent_id 업데이트
|
||||
if (array_key_exists('parent_id', $p)) {
|
||||
$fillData['parent_id'] = $p['parent_id'] === 0 ? null : $p['parent_id'];
|
||||
}
|
||||
|
||||
$dept->fill($fillData)->save();
|
||||
|
||||
return $dept->fresh();
|
||||
}
|
||||
|
||||
277
app/Services/SalaryService.php
Normal file
277
app/Services/SalaryService.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\Salary;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SalaryService extends Service
|
||||
{
|
||||
/**
|
||||
* 급여 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Salary::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['employee:id,name,employee_number', 'employee.department', 'employee.position', 'employee.rank']);
|
||||
|
||||
// 검색 필터 (직원명)
|
||||
if (!empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->whereHas('employee', function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 연도 필터
|
||||
if (!empty($params['year'])) {
|
||||
$query->where('year', $params['year']);
|
||||
}
|
||||
|
||||
// 월 필터
|
||||
if (!empty($params['month'])) {
|
||||
$query->where('month', $params['month']);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (!empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 기간 필터
|
||||
if (!empty($params['start_date']) && !empty($params['end_date'])) {
|
||||
$query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]);
|
||||
}
|
||||
|
||||
// 직원 ID 필터
|
||||
if (!empty($params['employee_id'])) {
|
||||
$query->where('employee_id', $params['employee_id']);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'year';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
|
||||
if ($sortBy === 'year') {
|
||||
$query->orderBy('year', $sortDir)
|
||||
->orderBy('month', $sortDir);
|
||||
} else {
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
}
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 상세 조회
|
||||
*/
|
||||
public function show(int $id): Salary
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Salary::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['employee:id,name,employee_number', 'employee.department', 'employee.position', 'employee.rank'])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 등록
|
||||
*/
|
||||
public function store(array $data): Salary
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
$salary = new Salary;
|
||||
$salary->tenant_id = $tenantId;
|
||||
$salary->employee_id = $data['employee_id'];
|
||||
$salary->year = $data['year'];
|
||||
$salary->month = $data['month'];
|
||||
$salary->base_salary = $data['base_salary'] ?? 0;
|
||||
$salary->total_allowance = $data['total_allowance'] ?? 0;
|
||||
$salary->total_overtime = $data['total_overtime'] ?? 0;
|
||||
$salary->total_bonus = $data['total_bonus'] ?? 0;
|
||||
$salary->total_deduction = $data['total_deduction'] ?? 0;
|
||||
$salary->allowance_details = $data['allowance_details'] ?? null;
|
||||
$salary->deduction_details = $data['deduction_details'] ?? null;
|
||||
$salary->payment_date = $data['payment_date'] ?? null;
|
||||
$salary->status = $data['status'] ?? 'scheduled';
|
||||
$salary->created_by = $userId;
|
||||
$salary->updated_by = $userId;
|
||||
|
||||
// 실지급액 계산
|
||||
$salary->net_payment = $salary->calculateNetPayment();
|
||||
|
||||
$salary->save();
|
||||
|
||||
return $salary->load('employee:id,name,email');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 수정
|
||||
*/
|
||||
public function update(int $id, array $data): Salary
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$salary = Salary::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (isset($data['employee_id'])) {
|
||||
$salary->employee_id = $data['employee_id'];
|
||||
}
|
||||
if (isset($data['year'])) {
|
||||
$salary->year = $data['year'];
|
||||
}
|
||||
if (isset($data['month'])) {
|
||||
$salary->month = $data['month'];
|
||||
}
|
||||
if (isset($data['base_salary'])) {
|
||||
$salary->base_salary = $data['base_salary'];
|
||||
}
|
||||
if (isset($data['total_allowance'])) {
|
||||
$salary->total_allowance = $data['total_allowance'];
|
||||
}
|
||||
if (isset($data['total_overtime'])) {
|
||||
$salary->total_overtime = $data['total_overtime'];
|
||||
}
|
||||
if (isset($data['total_bonus'])) {
|
||||
$salary->total_bonus = $data['total_bonus'];
|
||||
}
|
||||
if (isset($data['total_deduction'])) {
|
||||
$salary->total_deduction = $data['total_deduction'];
|
||||
}
|
||||
if (array_key_exists('allowance_details', $data)) {
|
||||
$salary->allowance_details = $data['allowance_details'];
|
||||
}
|
||||
if (array_key_exists('deduction_details', $data)) {
|
||||
$salary->deduction_details = $data['deduction_details'];
|
||||
}
|
||||
if (array_key_exists('payment_date', $data)) {
|
||||
$salary->payment_date = $data['payment_date'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$salary->status = $data['status'];
|
||||
}
|
||||
|
||||
// 실지급액 재계산
|
||||
$salary->net_payment = $salary->calculateNetPayment();
|
||||
|
||||
$salary->updated_by = $userId;
|
||||
$salary->save();
|
||||
|
||||
return $salary->fresh()->load(['employee:id,name,employee_number', 'employee.department', 'employee.position', 'employee.rank']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 삭제
|
||||
*/
|
||||
public function destroy(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$salary = Salary::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$salary->deleted_by = $userId;
|
||||
$salary->save();
|
||||
$salary->delete();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 상태 변경 (지급완료/지급예정)
|
||||
*/
|
||||
public function updateStatus(int $id, string $status): Salary
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $status, $tenantId, $userId) {
|
||||
$salary = Salary::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$salary->status = $status;
|
||||
$salary->updated_by = $userId;
|
||||
$salary->save();
|
||||
|
||||
return $salary->load(['employee:id,name,employee_number', 'employee.department', 'employee.position', 'employee.rank']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 일괄 상태 변경
|
||||
*/
|
||||
public function bulkUpdateStatus(array $ids, string $status): int
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($ids, $status, $tenantId, $userId) {
|
||||
return Salary::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->update([
|
||||
'status' => $status,
|
||||
'updated_by' => $userId,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 통계 조회
|
||||
*/
|
||||
public function getStatistics(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Salary::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 연도/월 필터
|
||||
if (!empty($params['year'])) {
|
||||
$query->where('year', $params['year']);
|
||||
}
|
||||
if (!empty($params['month'])) {
|
||||
$query->where('month', $params['month']);
|
||||
}
|
||||
|
||||
// 기간 필터
|
||||
if (!empty($params['start_date']) && !empty($params['end_date'])) {
|
||||
$query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]);
|
||||
}
|
||||
|
||||
return [
|
||||
'total_net_payment' => (float) $query->sum('net_payment'),
|
||||
'total_base_salary' => (float) $query->sum('base_salary'),
|
||||
'total_allowance' => (float) $query->sum('total_allowance'),
|
||||
'total_overtime' => (float) $query->sum('total_overtime'),
|
||||
'total_bonus' => (float) $query->sum('total_bonus'),
|
||||
'total_deduction' => (float) $query->sum('total_deduction'),
|
||||
'count' => $query->count(),
|
||||
'completed_count' => (clone $query)->where('status', 'completed')->count(),
|
||||
'scheduled_count' => (clone $query)->where('status', 'scheduled')->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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('salaries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('employee_id')->comment('직원 ID');
|
||||
$table->unsignedSmallInteger('year')->comment('년도');
|
||||
$table->unsignedTinyInteger('month')->comment('월');
|
||||
|
||||
// 급여 금액
|
||||
$table->decimal('base_salary', 15, 2)->default(0)->comment('기본급');
|
||||
$table->decimal('total_allowance', 15, 2)->default(0)->comment('총 수당');
|
||||
$table->decimal('total_overtime', 15, 2)->default(0)->comment('초과근무수당');
|
||||
$table->decimal('total_bonus', 15, 2)->default(0)->comment('상여');
|
||||
$table->decimal('total_deduction', 15, 2)->default(0)->comment('총 공제');
|
||||
$table->decimal('net_payment', 15, 2)->default(0)->comment('실지급액');
|
||||
|
||||
// 상세 내역 (JSON)
|
||||
$table->json('allowance_details')->nullable()->comment('수당 상세 내역');
|
||||
$table->json('deduction_details')->nullable()->comment('공제 상세 내역');
|
||||
|
||||
// 지급 정보
|
||||
$table->date('payment_date')->nullable()->comment('지급일');
|
||||
$table->enum('status', ['scheduled', 'completed'])->default('scheduled')->comment('상태: 지급예정/지급완료');
|
||||
|
||||
// 감사 필드
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->index('tenant_id');
|
||||
$table->index('employee_id');
|
||||
$table->index(['year', 'month']);
|
||||
$table->index('status');
|
||||
$table->unique(['tenant_id', 'employee_id', 'year', 'month'], 'unique_salary_per_employee_month');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('salaries');
|
||||
}
|
||||
};
|
||||
312
database/seeders/ApprovalTestDataSeeder.php
Normal file
312
database/seeders/ApprovalTestDataSeeder.php
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ApprovalTestDataSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 결재 시스템 테스트 데이터 시더
|
||||
* - 기안함: 15건 (draft 5건, pending 10건)
|
||||
* - 결재함: 15건 (결재 대기 상태)
|
||||
* - 참조함: 10건 (열람 대기 상태)
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = 1;
|
||||
$now = Carbon::now();
|
||||
|
||||
// 사용자 ID 가져오기
|
||||
$users = DB::table('users')->pluck('id')->toArray();
|
||||
if (count($users) < 3) {
|
||||
$this->command->error('최소 3명의 사용자가 필요합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
$mainUser = $users[0]; // 기안자 겸 참조 대상
|
||||
$approver1 = $users[1] ?? $users[0];
|
||||
$approver2 = $users[2] ?? $users[0];
|
||||
|
||||
// 1. 결재 양식 생성
|
||||
$this->command->info('결재 양식 생성 중...');
|
||||
$forms = $this->createApprovalForms($tenantId, $mainUser, $now);
|
||||
|
||||
// 2. 결재 문서 생성
|
||||
$this->command->info('결재 문서 생성 중...');
|
||||
$this->createApprovals($tenantId, $forms, $mainUser, $approver1, $approver2, $now);
|
||||
|
||||
$this->command->info('✅ 결재 테스트 데이터 생성 완료!');
|
||||
$this->command->info(' - 기안함: 15건');
|
||||
$this->command->info(' - 결재함: 15건');
|
||||
$this->command->info(' - 참조함: 10건');
|
||||
}
|
||||
|
||||
private function createApprovalForms(int $tenantId, int $userId, Carbon $now): array
|
||||
{
|
||||
$forms = [
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '품의서',
|
||||
'code' => 'proposal',
|
||||
'category' => '일반',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'title', 'type' => 'text', 'label' => '제목', 'required' => true],
|
||||
['name' => 'vendor', 'type' => 'text', 'label' => '거래처', 'required' => false],
|
||||
['name' => 'description', 'type' => 'textarea', 'label' => '내용', 'required' => true],
|
||||
['name' => 'reason', 'type' => 'textarea', 'label' => '사유', 'required' => true],
|
||||
['name' => 'estimatedCost', 'type' => 'number', 'label' => '예상비용', 'required' => false],
|
||||
]
|
||||
]),
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '지출결의서',
|
||||
'code' => 'expenseReport',
|
||||
'category' => '경비',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'requestDate', 'type' => 'date', 'label' => '신청일', 'required' => true],
|
||||
['name' => 'paymentDate', 'type' => 'date', 'label' => '지급일', 'required' => true],
|
||||
['name' => 'items', 'type' => 'array', 'label' => '지출항목', 'required' => true],
|
||||
['name' => 'totalAmount', 'type' => 'number', 'label' => '총액', 'required' => true],
|
||||
]
|
||||
]),
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '비용견적서',
|
||||
'code' => 'expenseEstimate',
|
||||
'category' => '경비',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'items', 'type' => 'array', 'label' => '비용항목', 'required' => true],
|
||||
['name' => 'totalExpense', 'type' => 'number', 'label' => '총지출', 'required' => true],
|
||||
['name' => 'accountBalance', 'type' => 'number', 'label' => '계좌잔액', 'required' => true],
|
||||
]
|
||||
]),
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
];
|
||||
|
||||
$formIds = [];
|
||||
foreach ($forms as $form) {
|
||||
// 기존 양식 확인
|
||||
$existing = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $form['code'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$formIds[$form['code']] = $existing->id;
|
||||
} else {
|
||||
$formIds[$form['code']] = DB::table('approval_forms')->insertGetId($form);
|
||||
}
|
||||
}
|
||||
|
||||
return $formIds;
|
||||
}
|
||||
|
||||
private function createApprovals(
|
||||
int $tenantId,
|
||||
array $forms,
|
||||
int $mainUser,
|
||||
int $approver1,
|
||||
int $approver2,
|
||||
Carbon $now
|
||||
): void {
|
||||
$proposalTitles = [
|
||||
'신규 장비 구매 품의', '사무용품 구매 요청', '소프트웨어 라이선스 갱신',
|
||||
'출장 경비 지원 요청', '교육 프로그램 신청', '복지시설 개선 제안',
|
||||
'마케팅 예산 증액 품의', '시스템 업그레이드 제안', '인력 충원 요청',
|
||||
'사무실 이전 품의', '연구개발 예산 신청', '고객 세미나 개최 품의',
|
||||
'협력업체 계약 갱신', '보안 시스템 도입 품의', '업무 차량 구매 요청',
|
||||
];
|
||||
|
||||
$expenseItems = [
|
||||
'교통비', '식비', '숙박비', '소모품비', '통신비', '유류비', '접대비', '회의비'
|
||||
];
|
||||
|
||||
$vendors = [
|
||||
'삼성전자', 'LG전자', 'SK하이닉스', '현대자동차', '네이버', '카카오',
|
||||
'쿠팡', '배달의민족', '토스', '당근마켓'
|
||||
];
|
||||
|
||||
$docNumber = 1;
|
||||
|
||||
// 기안함용 문서 15건 (mainUser가 기안자)
|
||||
for ($i = 0; $i < 15; $i++) {
|
||||
$status = $i < 5 ? 'draft' : 'pending';
|
||||
$formCode = ['proposal', 'expenseReport', 'expenseEstimate'][$i % 3];
|
||||
$formId = $forms[$formCode];
|
||||
|
||||
$content = $this->generateContent($formCode, $proposalTitles[$i], $vendors, $expenseItems);
|
||||
|
||||
$approvalId = DB::table('approvals')->insertGetId([
|
||||
'tenant_id' => $tenantId,
|
||||
'document_number' => sprintf('DOC-%s-%04d', $now->format('Ymd'), $docNumber++),
|
||||
'form_id' => $formId,
|
||||
'title' => $proposalTitles[$i],
|
||||
'content' => json_encode($content),
|
||||
'status' => $status,
|
||||
'drafter_id' => $mainUser,
|
||||
'drafted_at' => $status === 'pending' ? $now->copy()->subDays(rand(1, 10)) : null,
|
||||
'current_step' => $status === 'pending' ? 1 : 0,
|
||||
'created_by' => $mainUser,
|
||||
'created_at' => $now->copy()->subDays(rand(1, 15)),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 결재선 추가 (pending 상태인 경우)
|
||||
if ($status === 'pending') {
|
||||
// 결재자 1 (approver1이 결재 대기)
|
||||
DB::table('approval_steps')->insert([
|
||||
'approval_id' => $approvalId,
|
||||
'step_order' => 1,
|
||||
'step_type' => 'approval',
|
||||
'approver_id' => $approver1,
|
||||
'status' => 'pending',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 결재자 2
|
||||
DB::table('approval_steps')->insert([
|
||||
'approval_id' => $approvalId,
|
||||
'step_order' => 2,
|
||||
'step_type' => 'approval',
|
||||
'approver_id' => $approver2,
|
||||
'status' => 'pending',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 참조 (mainUser에게 참조)
|
||||
if ($i < 10) {
|
||||
DB::table('approval_steps')->insert([
|
||||
'approval_id' => $approvalId,
|
||||
'step_order' => 3,
|
||||
'step_type' => 'reference',
|
||||
'approver_id' => $mainUser,
|
||||
'status' => 'pending',
|
||||
'is_read' => false,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 결재함용 추가 문서 (approver1/approver2가 기안, mainUser가 결재자)
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$formCode = ['proposal', 'expenseReport'][$i % 2];
|
||||
$formId = $forms[$formCode];
|
||||
$drafter = $i < 3 ? $approver1 : $approver2;
|
||||
|
||||
$title = '추가 결재 요청 문서 ' . ($i + 1);
|
||||
$content = $this->generateContent($formCode, $title, $vendors, $expenseItems);
|
||||
|
||||
$approvalId = DB::table('approvals')->insertGetId([
|
||||
'tenant_id' => $tenantId,
|
||||
'document_number' => sprintf('DOC-%s-%04d', $now->format('Ymd'), $docNumber++),
|
||||
'form_id' => $formId,
|
||||
'title' => $title,
|
||||
'content' => json_encode($content),
|
||||
'status' => 'pending',
|
||||
'drafter_id' => $drafter,
|
||||
'drafted_at' => $now->copy()->subDays(rand(1, 5)),
|
||||
'current_step' => 1,
|
||||
'created_by' => $drafter,
|
||||
'created_at' => $now->copy()->subDays(rand(1, 10)),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// mainUser가 결재자
|
||||
DB::table('approval_steps')->insert([
|
||||
'approval_id' => $approvalId,
|
||||
'step_order' => 1,
|
||||
'step_type' => 'approval',
|
||||
'approver_id' => $mainUser,
|
||||
'status' => 'pending',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateContent(string $formCode, string $title, array $vendors, array $expenseItems): array
|
||||
{
|
||||
switch ($formCode) {
|
||||
case 'proposal':
|
||||
return [
|
||||
'title' => $title,
|
||||
'vendor' => $vendors[array_rand($vendors)],
|
||||
'vendorPaymentDate' => Carbon::now()->addDays(rand(7, 30))->format('Y-m-d'),
|
||||
'description' => $title . '에 대한 상세 설명입니다. 업무 효율성 향상과 비용 절감을 위해 필요합니다.',
|
||||
'reason' => '업무 효율성 향상 및 경쟁력 강화를 위해 필수적으로 진행해야 합니다.',
|
||||
'estimatedCost' => rand(100, 5000) * 10000,
|
||||
];
|
||||
|
||||
case 'expenseReport':
|
||||
$items = [];
|
||||
$total = 0;
|
||||
for ($j = 0; $j < rand(2, 5); $j++) {
|
||||
$amount = rand(10, 200) * 1000;
|
||||
$total += $amount;
|
||||
$items[] = [
|
||||
'id' => (string) ($j + 1),
|
||||
'description' => $expenseItems[array_rand($expenseItems)],
|
||||
'amount' => $amount,
|
||||
'note' => '업무 관련 지출',
|
||||
];
|
||||
}
|
||||
return [
|
||||
'requestDate' => Carbon::now()->subDays(rand(1, 7))->format('Y-m-d'),
|
||||
'paymentDate' => Carbon::now()->addDays(rand(1, 14))->format('Y-m-d'),
|
||||
'items' => $items,
|
||||
'cardId' => 'CARD-' . rand(1000, 9999),
|
||||
'totalAmount' => $total,
|
||||
];
|
||||
|
||||
case 'expenseEstimate':
|
||||
$items = [];
|
||||
$total = 0;
|
||||
for ($j = 0; $j < rand(3, 8); $j++) {
|
||||
$amount = rand(50, 500) * 10000;
|
||||
$total += $amount;
|
||||
$items[] = [
|
||||
'id' => (string) ($j + 1),
|
||||
'expectedPaymentDate' => Carbon::now()->addDays(rand(1, 60))->format('Y-m-d'),
|
||||
'category' => $expenseItems[array_rand($expenseItems)],
|
||||
'amount' => $amount,
|
||||
'vendor' => $vendors[array_rand($vendors)],
|
||||
'memo' => '예정 지출',
|
||||
'checked' => false,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'items' => $items,
|
||||
'totalExpense' => $total,
|
||||
'accountBalance' => rand(5000, 20000) * 10000,
|
||||
'finalDifference' => rand(5000, 20000) * 10000 - $total,
|
||||
];
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
191
database/seeders/Dummy/DummyAttendanceSeeder.php
Normal file
191
database/seeders/Dummy/DummyAttendanceSeeder.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Dummy;
|
||||
|
||||
use App\Models\Tenants\Attendance;
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DummyAttendanceSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 테넌트 소속 사용자 조회
|
||||
$userIds = DB::table('user_tenants')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
|
||||
if (empty($userIds)) {
|
||||
$this->command->warn(' ⚠ attendances: 사용자가 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
// 최근 30일간의 근태 데이터 생성
|
||||
$startDate = now()->subDays(30);
|
||||
$endDate = now();
|
||||
$count = 0;
|
||||
|
||||
// 상태 분포 (enum: onTime, late, absent, vacation, businessTrip, fieldWork, overtime, remote)
|
||||
$statuses = [
|
||||
'onTime' => 60, // 정시 출근 60%
|
||||
'late' => 10, // 지각 10%
|
||||
'vacation' => 10, // 휴가 10%
|
||||
'absent' => 5, // 결근 5%
|
||||
'businessTrip' => 5, // 출장 5%
|
||||
'remote' => 5, // 재택 5%
|
||||
'overtime' => 5, // 초과근무 5%
|
||||
];
|
||||
|
||||
foreach ($userIds as $uId) {
|
||||
$currentDate = clone $startDate;
|
||||
|
||||
while ($currentDate <= $endDate) {
|
||||
// 주말 제외
|
||||
if ($currentDate->isWeekend()) {
|
||||
$currentDate->addDay();
|
||||
continue;
|
||||
}
|
||||
|
||||
$baseDate = $currentDate->format('Y-m-d');
|
||||
|
||||
// 이미 존재하는지 확인
|
||||
$exists = Attendance::where('tenant_id', $tenantId)
|
||||
->where('user_id', $uId)
|
||||
->where('base_date', $baseDate)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
$currentDate->addDay();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 상태 결정 (가중치 기반 랜덤)
|
||||
$status = $this->getRandomStatus($statuses);
|
||||
|
||||
// 출퇴근 시간 생성
|
||||
$jsonDetails = $this->generateTimeDetails($status);
|
||||
|
||||
Attendance::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $uId,
|
||||
'base_date' => $baseDate,
|
||||
'status' => $status,
|
||||
'json_details' => $jsonDetails,
|
||||
'remarks' => $this->getRemarks($status),
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
$count++;
|
||||
$currentDate->addDay();
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info(' ✓ attendances: ' . $count . '건 생성');
|
||||
}
|
||||
|
||||
private function getRandomStatus(array $weights): string
|
||||
{
|
||||
$total = array_sum($weights);
|
||||
$rand = rand(1, $total);
|
||||
$cumulative = 0;
|
||||
|
||||
foreach ($weights as $status => $weight) {
|
||||
$cumulative += $weight;
|
||||
if ($rand <= $cumulative) {
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
private function generateTimeDetails(string $status): array
|
||||
{
|
||||
$checkIn = null;
|
||||
$checkOut = null;
|
||||
$workMinutes = 0;
|
||||
$lateMinutes = 0;
|
||||
$earlyLeaveMinutes = 0;
|
||||
$overtimeMinutes = 0;
|
||||
|
||||
$standardStart = 9 * 60; // 09:00 in minutes
|
||||
$standardEnd = 18 * 60; // 18:00 in minutes
|
||||
$breakMinutes = 60;
|
||||
|
||||
switch ($status) {
|
||||
case 'onTime':
|
||||
$startVariance = rand(-10, 10); // ±10분
|
||||
$endVariance = rand(-5, 30); // -5분 ~ +30분
|
||||
$checkIn = sprintf('%02d:%02d:00', 9, max(0, $startVariance));
|
||||
$checkOut = sprintf('%02d:%02d:00', 18 + intdiv($endVariance, 60), abs($endVariance) % 60);
|
||||
$workMinutes = ($standardEnd - $standardStart - $breakMinutes) + $endVariance;
|
||||
break;
|
||||
|
||||
case 'late':
|
||||
$lateMinutes = rand(10, 60); // 10분 ~ 1시간 지각
|
||||
$checkIn = sprintf('%02d:%02d:00', 9 + intdiv($lateMinutes, 60), $lateMinutes % 60);
|
||||
$checkOut = '18:00:00';
|
||||
$workMinutes = ($standardEnd - $standardStart - $breakMinutes) - $lateMinutes;
|
||||
break;
|
||||
|
||||
case 'overtime':
|
||||
$checkIn = '09:00:00';
|
||||
$overtimeMinutes = rand(60, 180); // 1시간 ~ 3시간 초과근무
|
||||
$endTime = $standardEnd + $overtimeMinutes;
|
||||
$checkOut = sprintf('%02d:%02d:00', intdiv($endTime, 60), $endTime % 60);
|
||||
$workMinutes = ($standardEnd - $standardStart - $breakMinutes) + $overtimeMinutes;
|
||||
break;
|
||||
|
||||
case 'remote':
|
||||
$checkIn = sprintf('%02d:%02d:00', 9, rand(0, 10));
|
||||
$checkOut = sprintf('%02d:%02d:00', 18, rand(0, 30));
|
||||
$workMinutes = ($standardEnd - $standardStart - $breakMinutes);
|
||||
break;
|
||||
|
||||
case 'businessTrip':
|
||||
case 'fieldWork':
|
||||
$checkIn = sprintf('%02d:%02d:00', 8, rand(0, 30)); // 출장은 일찍 시작
|
||||
$checkOut = sprintf('%02d:%02d:00', 19, rand(0, 60)); // 늦게 종료
|
||||
$workMinutes = 10 * 60 - $breakMinutes; // 10시간 근무
|
||||
break;
|
||||
|
||||
case 'vacation':
|
||||
case 'absent':
|
||||
// 출퇴근 기록 없음
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
'check_in' => $checkIn,
|
||||
'check_out' => $checkOut,
|
||||
'work_minutes' => max(0, $workMinutes),
|
||||
'late_minutes' => $lateMinutes,
|
||||
'early_leave_minutes' => $earlyLeaveMinutes,
|
||||
'overtime_minutes' => $overtimeMinutes,
|
||||
'vacation_type' => $status === 'vacation' ? 'annual' : null,
|
||||
'gps_data' => $checkIn ? [
|
||||
'check_in' => ['lat' => 37.5012 + (rand(-10, 10) / 10000), 'lng' => 127.0396 + (rand(-10, 10) / 10000)],
|
||||
'check_out' => $checkOut ? ['lat' => 37.5012 + (rand(-10, 10) / 10000), 'lng' => 127.0396 + (rand(-10, 10) / 10000)] : null,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function getRemarks(string $status): ?string
|
||||
{
|
||||
return match ($status) {
|
||||
'late' => '지각 - 교통 체증',
|
||||
'vacation' => '연차 휴가',
|
||||
'absent' => '결근',
|
||||
'businessTrip' => '출장 - 외부 미팅',
|
||||
'fieldWork' => '외근 - 현장 방문',
|
||||
'overtime' => '초과근무 - 프로젝트 마감',
|
||||
'remote' => '재택근무',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
34
database/seeders/Dummy/DummyAttendanceSettingSeeder.php
Normal file
34
database/seeders/Dummy/DummyAttendanceSettingSeeder.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Dummy;
|
||||
|
||||
use App\Models\Tenants\AttendanceSetting;
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DummyAttendanceSettingSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 설정이 있으면 스킵
|
||||
$existing = AttendanceSetting::where('tenant_id', $tenantId)->first();
|
||||
if ($existing) {
|
||||
$this->command->info(' ⚠ attendance_settings: 이미 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
AttendanceSetting::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'use_gps' => true,
|
||||
'allowed_radius' => 500, // 500m
|
||||
'hq_address' => '서울시 강남구 테헤란로 123',
|
||||
'hq_latitude' => 37.5012,
|
||||
'hq_longitude' => 127.0396,
|
||||
]);
|
||||
|
||||
$this->command->info(' ✓ attendance_settings: 1건 생성');
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = BadDebt::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ bad_debts: 이미 ' . $existing . '건 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// SALES 또는 BOTH 타입의 거래처 조회
|
||||
$clients = Client::where('tenant_id', $tenantId)
|
||||
->whereIn('client_type', ['SALES', 'BOTH'])
|
||||
|
||||
@@ -13,6 +13,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = BankAccount::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ bank_accounts: 이미 ' . $existing . '개 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
$accounts = [
|
||||
['bank_code' => '004', 'bank_name' => 'KB국민은행', 'account_number' => '123-45-6789012', 'account_holder' => '프론트테스트', 'account_name' => '운영계좌', 'is_primary' => true],
|
||||
['bank_code' => '088', 'bank_name' => '신한은행', 'account_number' => '110-123-456789', 'account_holder' => '프론트테스트', 'account_name' => '급여계좌', 'is_primary' => false],
|
||||
|
||||
@@ -16,6 +16,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Bill::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ bills: 이미 ' . $existing . '건 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 거래처 매핑
|
||||
$clients = Client::where('tenant_id', $tenantId)->get()->keyBy('name');
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = ClientGroup::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ client_groups: 이미 ' . $existing . '개 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
$groups = [
|
||||
['group_code' => 'VIP', 'group_name' => 'VIP 고객', 'price_rate' => 0.95],
|
||||
['group_code' => 'GOLD', 'group_name' => '골드 고객', 'price_rate' => 0.97],
|
||||
|
||||
@@ -14,6 +14,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Client::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ clients: 이미 ' . $existing . '개 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 그룹 ID 조회
|
||||
$groups = ClientGroup::where('tenant_id', $tenantId)
|
||||
->pluck('id', 'group_code')
|
||||
|
||||
133
database/seeders/Dummy/DummyDepartmentSeeder.php
Normal file
133
database/seeders/Dummy/DummyDepartmentSeeder.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Dummy;
|
||||
|
||||
use App\Models\Tenants\Department;
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DummyDepartmentSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 부서가 있으면 스킵
|
||||
$existing = Department::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ departments: 이미 ' . $existing . '개 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1레벨: 본부 (3개)
|
||||
$divisions = [
|
||||
['code' => 'DIV01', 'name' => '경영본부', 'description' => '경영 및 기획 업무'],
|
||||
['code' => 'DIV02', 'name' => '기술본부', 'description' => '기술 개발 및 연구'],
|
||||
['code' => 'DIV03', 'name' => '영업본부', 'description' => '영업 및 마케팅'],
|
||||
];
|
||||
|
||||
// 2레벨: 부서 (본부별 2~3개씩)
|
||||
$departments = [
|
||||
'DIV01' => [
|
||||
['code' => 'HR', 'name' => '인사팀', 'description' => '인사 및 채용 관리'],
|
||||
['code' => 'FIN', 'name' => '재무팀', 'description' => '재무 및 회계 관리'],
|
||||
['code' => 'ADM', 'name' => '총무팀', 'description' => '총무 및 시설 관리'],
|
||||
],
|
||||
'DIV02' => [
|
||||
['code' => 'DEV', 'name' => '개발팀', 'description' => '소프트웨어 개발'],
|
||||
['code' => 'QA', 'name' => 'QA팀', 'description' => '품질 보증 및 테스트'],
|
||||
['code' => 'INFRA', 'name' => '인프라팀', 'description' => '서버 및 인프라 관리'],
|
||||
],
|
||||
'DIV03' => [
|
||||
['code' => 'SALES', 'name' => '영업팀', 'description' => '영업 및 고객 관리'],
|
||||
['code' => 'MKT', 'name' => '마케팅팀', 'description' => '마케팅 및 홍보'],
|
||||
],
|
||||
];
|
||||
|
||||
$count = 0;
|
||||
$divisionIds = [];
|
||||
|
||||
// 본부 생성
|
||||
foreach ($divisions as $index => $division) {
|
||||
$dept = Department::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'parent_id' => null,
|
||||
'code' => $division['code'],
|
||||
'name' => $division['name'],
|
||||
'description' => $division['description'],
|
||||
'is_active' => true,
|
||||
'sort_order' => $index + 1,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
$divisionIds[$division['code']] = $dept->id;
|
||||
$count++;
|
||||
}
|
||||
|
||||
// 부서 생성
|
||||
foreach ($departments as $divCode => $depts) {
|
||||
$parentId = $divisionIds[$divCode] ?? null;
|
||||
foreach ($depts as $index => $dept) {
|
||||
Department::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'parent_id' => $parentId,
|
||||
'code' => $dept['code'],
|
||||
'name' => $dept['name'],
|
||||
'description' => $dept['description'],
|
||||
'is_active' => true,
|
||||
'sort_order' => $index + 1,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// 사용자-부서 연결
|
||||
$this->assignUsersToDepartments($tenantId);
|
||||
|
||||
$this->command->info(' ✓ departments: ' . $count . '개 생성 (본부 3개, 부서 8개)');
|
||||
}
|
||||
|
||||
private function assignUsersToDepartments(int $tenantId): void
|
||||
{
|
||||
// 테넌트 소속 사용자 조회
|
||||
$userIds = DB::table('user_tenants')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
|
||||
// 부서 조회 (2레벨만)
|
||||
$departments = Department::where('tenant_id', $tenantId)
|
||||
->whereNotNull('parent_id')
|
||||
->get();
|
||||
|
||||
if ($departments->isEmpty() || empty($userIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자를 부서에 분배
|
||||
foreach ($userIds as $index => $userId) {
|
||||
$dept = $departments[$index % $departments->count()];
|
||||
$isPrimary = ($index % $departments->count() === 0); // 첫 번째만 primary
|
||||
|
||||
// 이미 연결되어 있는지 확인
|
||||
$exists = DB::table('department_user')
|
||||
->where('user_id', $userId)
|
||||
->where('department_id', $dept->id)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
DB::table('department_user')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $userId,
|
||||
'department_id' => $dept->id,
|
||||
'is_primary' => $isPrimary,
|
||||
'joined_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Deposit::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ deposits: 이미 ' . $existing . '건 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 거래처 매핑 (SALES, BOTH만)
|
||||
$clients = Client::where('tenant_id', $tenantId)
|
||||
->whereIn('client_type', ['SALES', 'BOTH'])
|
||||
|
||||
95
database/seeders/Dummy/DummyLeaveGrantSeeder.php
Normal file
95
database/seeders/Dummy/DummyLeaveGrantSeeder.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Dummy;
|
||||
|
||||
use App\Models\Tenants\LeaveGrant;
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DummyLeaveGrantSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 테넌트 소속 사용자 조회
|
||||
$userIds = DB::table('user_tenants')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
|
||||
if (empty($userIds)) {
|
||||
$this->command->warn(' ⚠ leave_grants: 사용자가 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
$year = 2025;
|
||||
$count = 0;
|
||||
|
||||
// 각 사용자별 연차/월차 부여
|
||||
foreach ($userIds as $uId) {
|
||||
// 연차 부여 (1월 1일)
|
||||
$existing = LeaveGrant::where('tenant_id', $tenantId)
|
||||
->where('user_id', $uId)
|
||||
->where('grant_type', 'annual')
|
||||
->whereYear('grant_date', $year)
|
||||
->exists();
|
||||
|
||||
if (!$existing) {
|
||||
LeaveGrant::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $uId,
|
||||
'grant_type' => 'annual',
|
||||
'grant_date' => sprintf('%04d-01-01', $year),
|
||||
'grant_days' => rand(12, 20), // 12~20일
|
||||
'reason' => sprintf('%d년 연차 부여', $year),
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
// 월차 부여 (월별, 12건)
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$grantDate = sprintf('%04d-%02d-01', $year, $month);
|
||||
|
||||
$monthlyExists = LeaveGrant::where('tenant_id', $tenantId)
|
||||
->where('user_id', $uId)
|
||||
->where('grant_type', 'monthly')
|
||||
->where('grant_date', $grantDate)
|
||||
->exists();
|
||||
|
||||
if (!$monthlyExists) {
|
||||
LeaveGrant::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $uId,
|
||||
'grant_type' => 'monthly',
|
||||
'grant_date' => $grantDate,
|
||||
'grant_days' => 1,
|
||||
'reason' => sprintf('%d년 %d월 월차', $year, $month),
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// 포상 휴가 (일부 사용자에게 랜덤)
|
||||
if (rand(1, 5) === 1) { // 20% 확률
|
||||
$rewardMonth = rand(3, 11);
|
||||
LeaveGrant::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $uId,
|
||||
'grant_type' => 'reward',
|
||||
'grant_date' => sprintf('%04d-%02d-15', $year, $rewardMonth),
|
||||
'grant_days' => rand(1, 3),
|
||||
'reason' => '우수 사원 포상 휴가',
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info(' ✓ leave_grants: ' . $count . '건 생성');
|
||||
}
|
||||
}
|
||||
135
database/seeders/Dummy/DummyLeaveSeeder.php
Normal file
135
database/seeders/Dummy/DummyLeaveSeeder.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Dummy;
|
||||
|
||||
use App\Models\Tenants\Leave;
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DummyLeaveSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 테넌트 소속 사용자 조회
|
||||
$userIds = DB::table('user_tenants')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
|
||||
if (empty($userIds)) {
|
||||
$this->command->warn(' ⚠ leaves: 사용자가 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
// 휴가 유형 (가중치)
|
||||
$leaveTypes = [
|
||||
'annual' => 50, // 연차 50%
|
||||
'half_am' => 15, // 오전 반차 15%
|
||||
'half_pm' => 15, // 오후 반차 15%
|
||||
'sick' => 10, // 병가 10%
|
||||
'family' => 5, // 경조사 5%
|
||||
'other' => 5, // 기타 5%
|
||||
];
|
||||
|
||||
// 상태 (가중치)
|
||||
$statuses = [
|
||||
'approved' => 60, // 승인 60%
|
||||
'pending' => 25, // 대기 25%
|
||||
'rejected' => 10, // 반려 10%
|
||||
'cancelled' => 5, // 취소 5%
|
||||
];
|
||||
|
||||
$count = 0;
|
||||
$year = 2025;
|
||||
|
||||
// 사용자별로 1~4건의 휴가 생성
|
||||
foreach ($userIds as $uId) {
|
||||
$leaveCount = rand(1, 4);
|
||||
|
||||
for ($i = 0; $i < $leaveCount; $i++) {
|
||||
$month = rand(1, 12);
|
||||
$day = rand(1, 28);
|
||||
$startDate = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
|
||||
$leaveType = $this->getRandomWeighted($leaveTypes);
|
||||
$status = $this->getRandomWeighted($statuses);
|
||||
|
||||
// 휴가 일수 결정
|
||||
if (in_array($leaveType, ['half_am', 'half_pm'])) {
|
||||
$days = 0.5;
|
||||
$endDate = $startDate;
|
||||
} else {
|
||||
$days = rand(1, 3);
|
||||
$endDate = date('Y-m-d', strtotime($startDate . ' + ' . ($days - 1) . ' days'));
|
||||
}
|
||||
|
||||
// 승인자 정보
|
||||
$approvedBy = null;
|
||||
$approvedAt = null;
|
||||
$rejectReason = null;
|
||||
|
||||
if ($status === 'approved') {
|
||||
$approvedBy = $userId;
|
||||
$approvedAt = date('Y-m-d H:i:s', strtotime($startDate . ' - 2 days'));
|
||||
} elseif ($status === 'rejected') {
|
||||
$approvedBy = $userId;
|
||||
$approvedAt = date('Y-m-d H:i:s', strtotime($startDate . ' - 2 days'));
|
||||
$rejectReason = '업무 일정 상 불가';
|
||||
}
|
||||
|
||||
Leave::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $uId,
|
||||
'leave_type' => $leaveType,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'days' => $days,
|
||||
'reason' => $this->getLeaveReason($leaveType),
|
||||
'status' => $status,
|
||||
'approved_by' => $approvedBy,
|
||||
'approved_at' => $approvedAt,
|
||||
'reject_reason' => $rejectReason,
|
||||
'created_by' => $uId,
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info(' ✓ leaves: ' . $count . '건 생성');
|
||||
}
|
||||
|
||||
private function getRandomWeighted(array $weights): string
|
||||
{
|
||||
$total = array_sum($weights);
|
||||
$rand = rand(1, $total);
|
||||
$cumulative = 0;
|
||||
|
||||
foreach ($weights as $key => $weight) {
|
||||
$cumulative += $weight;
|
||||
if ($rand <= $cumulative) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return array_key_first($weights);
|
||||
}
|
||||
|
||||
private function getLeaveReason(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'annual' => '개인 휴가',
|
||||
'half_am' => '오전 병원 방문',
|
||||
'half_pm' => '오후 개인 일정',
|
||||
'sick' => '건강 사유',
|
||||
'family' => '가족 행사',
|
||||
'maternity' => '출산 휴가',
|
||||
'parental' => '육아 휴직',
|
||||
default => '개인 사유',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,15 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Payment::whereHas('subscription', function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId);
|
||||
})->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ payments: 이미 ' . $existing . '건 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 요금제 생성 (없으면)
|
||||
$plans = $this->createPlans($userId);
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Popup::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ popups: 이미 ' . $existing . '개 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
$popups = [
|
||||
[
|
||||
'target_type' => 'all',
|
||||
|
||||
@@ -14,6 +14,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Purchase::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ purchases: 이미 ' . $existing . '건 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 거래처 매핑 (PURCHASE, BOTH만)
|
||||
$clients = Client::where('tenant_id', $tenantId)
|
||||
->whereIn('client_type', ['PURCHASE', 'BOTH'])
|
||||
|
||||
@@ -14,6 +14,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Sale::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ sales: 이미 ' . $existing . '건 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 거래처 매핑 (SALES, BOTH만)
|
||||
$clients = Client::where('tenant_id', $tenantId)
|
||||
->whereIn('client_type', ['SALES', 'BOTH'])
|
||||
|
||||
89
database/seeders/Dummy/DummyUserSeeder.php
Normal file
89
database/seeders/Dummy/DummyUserSeeder.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Dummy;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class DummyUserSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 더미 직원 데이터 (15명)
|
||||
$employees = [
|
||||
['user_id' => 'EMP001', 'name' => '김철수', 'email' => 'chulsoo.kim@test.com', 'phone' => '010-1234-5678'],
|
||||
['user_id' => 'EMP002', 'name' => '이영희', 'email' => 'younghee.lee@test.com', 'phone' => '010-2345-6789'],
|
||||
['user_id' => 'EMP003', 'name' => '박민수', 'email' => 'minsoo.park@test.com', 'phone' => '010-3456-7890'],
|
||||
['user_id' => 'EMP004', 'name' => '정은지', 'email' => 'eunji.jung@test.com', 'phone' => '010-4567-8901'],
|
||||
['user_id' => 'EMP005', 'name' => '최준호', 'email' => 'junho.choi@test.com', 'phone' => '010-5678-9012'],
|
||||
['user_id' => 'EMP006', 'name' => '강미래', 'email' => 'mirae.kang@test.com', 'phone' => '010-6789-0123'],
|
||||
['user_id' => 'EMP007', 'name' => '임도현', 'email' => 'dohyun.lim@test.com', 'phone' => '010-7890-1234'],
|
||||
['user_id' => 'EMP008', 'name' => '윤서연', 'email' => 'seoyeon.yoon@test.com', 'phone' => '010-8901-2345'],
|
||||
['user_id' => 'EMP009', 'name' => '한지민', 'email' => 'jimin.han@test.com', 'phone' => '010-9012-3456'],
|
||||
['user_id' => 'EMP010', 'name' => '오태양', 'email' => 'taeyang.oh@test.com', 'phone' => '010-0123-4567'],
|
||||
['user_id' => 'EMP011', 'name' => '신동욱', 'email' => 'dongwook.shin@test.com', 'phone' => '010-1111-2222'],
|
||||
['user_id' => 'EMP012', 'name' => '권나래', 'email' => 'narae.kwon@test.com', 'phone' => '010-2222-3333'],
|
||||
['user_id' => 'EMP013', 'name' => '조성민', 'email' => 'sungmin.cho@test.com', 'phone' => '010-3333-4444'],
|
||||
['user_id' => 'EMP014', 'name' => '백지훈', 'email' => 'jihun.baek@test.com', 'phone' => '010-4444-5555'],
|
||||
['user_id' => 'EMP015', 'name' => '송하늘', 'email' => 'haneul.song@test.com', 'phone' => '010-5555-6666'],
|
||||
];
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($employees as $employee) {
|
||||
// 이미 존재하는지 확인
|
||||
$existing = User::where('email', $employee['email'])->first();
|
||||
if ($existing) {
|
||||
// 이미 tenant에 연결되어 있는지 확인
|
||||
$linked = DB::table('user_tenants')
|
||||
->where('user_id', $existing->id)
|
||||
->where('tenant_id', $tenantId)
|
||||
->exists();
|
||||
|
||||
if (!$linked) {
|
||||
DB::table('user_tenants')->insert([
|
||||
'user_id' => $existing->id,
|
||||
'tenant_id' => $tenantId,
|
||||
'is_active' => true,
|
||||
'is_default' => false,
|
||||
'joined_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'user_id' => $employee['user_id'],
|
||||
'name' => $employee['name'],
|
||||
'email' => $employee['email'],
|
||||
'phone' => $employee['phone'],
|
||||
'password' => Hash::make('password123'),
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 테넌트에 연결
|
||||
DB::table('user_tenants')->insert([
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenantId,
|
||||
'is_active' => true,
|
||||
'is_default' => true,
|
||||
'joined_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->command->info(' ✓ users: ' . $count . '명 생성 (테넌트 연결 완료)');
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@ public function run(): void
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 데이터 있으면 스킵
|
||||
$existing = Withdrawal::where('tenant_id', $tenantId)->count();
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ withdrawals: 이미 ' . $existing . '건 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 거래처 매핑 (PURCHASE, BOTH만)
|
||||
$clients = Client::where('tenant_id', $tenantId)
|
||||
->whereIn('client_type', ['PURCHASE', 'BOTH'])
|
||||
|
||||
39
database/seeders/Dummy/DummyWorkSettingSeeder.php
Normal file
39
database/seeders/Dummy/DummyWorkSettingSeeder.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Dummy;
|
||||
|
||||
use App\Models\Tenants\WorkSetting;
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DummyWorkSettingSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
// 기존 설정이 있으면 스킵
|
||||
$existing = WorkSetting::where('tenant_id', $tenantId)->first();
|
||||
if ($existing) {
|
||||
$this->command->info(' ⚠ work_settings: 이미 존재 (스킵)');
|
||||
return;
|
||||
}
|
||||
|
||||
WorkSetting::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'work_type' => 'fixed',
|
||||
'standard_hours' => 8,
|
||||
'overtime_hours' => 4,
|
||||
'overtime_limit' => 52,
|
||||
'work_days' => [1, 2, 3, 4, 5], // 월~금
|
||||
'start_time' => '09:00:00',
|
||||
'end_time' => '18:00:00',
|
||||
'break_minutes' => 60,
|
||||
'break_start' => '12:00:00',
|
||||
'break_end' => '13:00:00',
|
||||
]);
|
||||
|
||||
$this->command->info(' ✓ work_settings: 1건 생성');
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,44 @@ public function run(): void
|
||||
$this->command->info('🌱 더미 데이터 시딩 시작...');
|
||||
$this->command->info(' 대상 테넌트: ID '.self::TENANT_ID);
|
||||
|
||||
// 1. 기본 데이터 (순서 중요)
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 기본 데이터 생성...');
|
||||
$this->call([
|
||||
Dummy\DummyUserSeeder::class, // HR용 사용자
|
||||
Dummy\DummyDepartmentSeeder::class, // 부서
|
||||
Dummy\DummyClientGroupSeeder::class,
|
||||
Dummy\DummyBankAccountSeeder::class,
|
||||
Dummy\DummyClientSeeder::class,
|
||||
]);
|
||||
|
||||
// 2. 회계 데이터
|
||||
$this->command->info('');
|
||||
$this->command->info('💰 회계 데이터 생성...');
|
||||
$this->call([
|
||||
Dummy\DummyDepositSeeder::class,
|
||||
Dummy\DummyWithdrawalSeeder::class,
|
||||
Dummy\DummySaleSeeder::class,
|
||||
Dummy\DummyPurchaseSeeder::class,
|
||||
Dummy\DummyBadDebtSeeder::class, // 악성채권
|
||||
Dummy\DummyBillSeeder::class, // 어음
|
||||
]);
|
||||
|
||||
// 3. HR 데이터
|
||||
$this->command->info('');
|
||||
$this->command->info('👥 HR 데이터 생성...');
|
||||
$this->call([
|
||||
Dummy\DummyWorkSettingSeeder::class, // 근무 설정
|
||||
Dummy\DummyAttendanceSettingSeeder::class, // 근태 설정
|
||||
Dummy\DummyAttendanceSeeder::class, // 근태
|
||||
Dummy\DummyLeaveGrantSeeder::class, // 휴가 부여
|
||||
Dummy\DummyLeaveSeeder::class, // 휴가
|
||||
]);
|
||||
|
||||
// 4. 기타 데이터
|
||||
$this->command->info('');
|
||||
$this->command->info('📋 기타 데이터 생성...');
|
||||
$this->call([
|
||||
Dummy\DummyPopupSeeder::class,
|
||||
Dummy\DummyPaymentSeeder::class,
|
||||
]);
|
||||
@@ -34,20 +64,27 @@ public function run(): void
|
||||
$this->command->info('');
|
||||
$this->command->info('✅ 더미 데이터 시딩 완료!');
|
||||
$this->command->table(
|
||||
['테이블', '생성 수량'],
|
||||
['카테고리', '테이블', '생성 수량'],
|
||||
[
|
||||
['client_groups', '5'],
|
||||
['bank_accounts', '5'],
|
||||
['clients', '20'],
|
||||
['deposits', '60'],
|
||||
['withdrawals', '60'],
|
||||
['sales', '80'],
|
||||
['purchases', '70'],
|
||||
['popups', '8'],
|
||||
['plans', '6'],
|
||||
['subscriptions', '1'],
|
||||
['payments', '13'],
|
||||
['총계', '~328'],
|
||||
['기본', 'users', '15'],
|
||||
['기본', 'departments', '11'],
|
||||
['기본', 'client_groups', '5'],
|
||||
['기본', 'bank_accounts', '5'],
|
||||
['기본', 'clients', '20'],
|
||||
['회계', 'deposits', '60'],
|
||||
['회계', 'withdrawals', '60'],
|
||||
['회계', 'sales', '80'],
|
||||
['회계', 'purchases', '70'],
|
||||
['회계', 'bad_debts', '18'],
|
||||
['회계', 'bills', '30'],
|
||||
['HR', 'work_settings', '1'],
|
||||
['HR', 'attendance_settings', '1'],
|
||||
['HR', 'attendances', '~300'],
|
||||
['HR', 'leave_grants', '~200'],
|
||||
['HR', 'leaves', '~50'],
|
||||
['기타', 'popups', '8'],
|
||||
['기타', 'payments', '13'],
|
||||
['', '총계', '~950'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
use App\Http\Controllers\Api\V1\BarobillSettingController;
|
||||
use App\Http\Controllers\Api\V1\BoardController;
|
||||
use App\Http\Controllers\Api\V1\CardController;
|
||||
use App\Http\Controllers\Api\V1\SalaryController;
|
||||
use App\Http\Controllers\Api\V1\CategoryController;
|
||||
use App\Http\Controllers\Api\V1\CategoryFieldController;
|
||||
use App\Http\Controllers\Api\V1\CategoryLogController;
|
||||
@@ -431,6 +432,18 @@
|
||||
Route::get('/{id}/payslip', [PayrollController::class, 'payslip'])->whereNumber('id')->name('v1.payrolls.payslip');
|
||||
});
|
||||
|
||||
// Salary API (급여 관리 - React 연동)
|
||||
Route::prefix('salaries')->group(function () {
|
||||
Route::get('', [SalaryController::class, 'index'])->name('v1.salaries.index');
|
||||
Route::post('', [SalaryController::class, 'store'])->name('v1.salaries.store');
|
||||
Route::get('/statistics', [SalaryController::class, 'statistics'])->name('v1.salaries.statistics');
|
||||
Route::post('/bulk-update-status', [SalaryController::class, 'bulkUpdateStatus'])->name('v1.salaries.bulk-update-status');
|
||||
Route::get('/{id}', [SalaryController::class, 'show'])->whereNumber('id')->name('v1.salaries.show');
|
||||
Route::put('/{id}', [SalaryController::class, 'update'])->whereNumber('id')->name('v1.salaries.update');
|
||||
Route::delete('/{id}', [SalaryController::class, 'destroy'])->whereNumber('id')->name('v1.salaries.destroy');
|
||||
Route::patch('/{id}/status', [SalaryController::class, 'updateStatus'])->whereNumber('id')->name('v1.salaries.update-status');
|
||||
});
|
||||
|
||||
// Loan API (가지급금 관리)
|
||||
Route::prefix('loans')->group(function () {
|
||||
Route::get('', [LoanController::class, 'index'])->name('v1.loans.index');
|
||||
|
||||
Reference in New Issue
Block a user