feat: 공통 모듈 추가 (엑셀 내보내기, 계정과목 일괄변경)
Phase 0 - 공통 모듈: - ExportService.php 생성 (Maatwebsite/Excel 기반 엑셀 내보내기) - BulkUpdateAccountCodeRequest.php 생성 (계정과목 일괄변경 유효성 검사) Phase 1 - 계정과목 일괄변경: - WithdrawalController/Service: bulkUpdateAccountCode 메서드 추가 - DepositController/Service: bulkUpdateAccountCode 메서드 추가 - POST /v1/withdrawals/bulk-update-account-code - POST /v1/deposits/bulk-update-account-code Phase 2 - 엑셀 내보내기: - AttendanceController/Service: export, getExportData 메서드 추가 - SalaryController/Service: export, getExportData 메서드 추가 - GET /v1/attendances/export - GET /v1/salaries/export Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,17 @@
|
|||||||
use App\Http\Requests\Attendance\StoreRequest;
|
use App\Http\Requests\Attendance\StoreRequest;
|
||||||
use App\Http\Requests\Attendance\UpdateRequest;
|
use App\Http\Requests\Attendance\UpdateRequest;
|
||||||
use App\Services\AttendanceService;
|
use App\Services\AttendanceService;
|
||||||
|
use App\Services\ExportService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
class AttendanceController extends Controller
|
class AttendanceController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(private AttendanceService $service) {}
|
public function __construct(
|
||||||
|
private AttendanceService $service,
|
||||||
|
private ExportService $exportService
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 근태 목록 조회
|
* 근태 목록 조회
|
||||||
@@ -121,4 +126,32 @@ public function monthlyStats(MonthlyStatsRequest $request): JsonResponse
|
|||||||
return $this->service->monthlyStats($request->validated());
|
return $this->service->monthlyStats($request->validated());
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근태 엑셀 내보내기
|
||||||
|
* GET /v1/attendances/export
|
||||||
|
*/
|
||||||
|
public function export(Request $request): BinaryFileResponse
|
||||||
|
{
|
||||||
|
$params = $request->only([
|
||||||
|
'user_id',
|
||||||
|
'date',
|
||||||
|
'date_from',
|
||||||
|
'date_to',
|
||||||
|
'status',
|
||||||
|
'department_id',
|
||||||
|
'sort_by',
|
||||||
|
'sort_dir',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$exportData = $this->service->getExportData($params);
|
||||||
|
$filename = '근태현황_'.date('Ymd_His');
|
||||||
|
|
||||||
|
return $this->exportService->download(
|
||||||
|
$exportData['data'],
|
||||||
|
$exportData['headings'],
|
||||||
|
$filename,
|
||||||
|
'근태현황'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Helpers\ApiResponse;
|
use App\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\BulkUpdateAccountCodeRequest;
|
||||||
use App\Http\Requests\V1\Deposit\StoreDepositRequest;
|
use App\Http\Requests\V1\Deposit\StoreDepositRequest;
|
||||||
use App\Http\Requests\V1\Deposit\UpdateDepositRequest;
|
use App\Http\Requests\V1\Deposit\UpdateDepositRequest;
|
||||||
use App\Services\DepositService;
|
use App\Services\DepositService;
|
||||||
@@ -94,4 +95,20 @@ public function summary(Request $request)
|
|||||||
|
|
||||||
return ApiResponse::success($summary, __('message.fetched'));
|
return ApiResponse::success($summary, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정과목 일괄 변경
|
||||||
|
*/
|
||||||
|
public function bulkUpdateAccountCode(BulkUpdateAccountCodeRequest $request)
|
||||||
|
{
|
||||||
|
$updatedCount = $this->service->bulkUpdateAccountCode(
|
||||||
|
$request->getIds(),
|
||||||
|
$request->getAccountCode()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(
|
||||||
|
['updated_count' => $updatedCount],
|
||||||
|
__('message.bulk_updated')
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
use App\Http\Requests\V1\Salary\BulkUpdateStatusRequest;
|
use App\Http\Requests\V1\Salary\BulkUpdateStatusRequest;
|
||||||
use App\Http\Requests\V1\Salary\StoreSalaryRequest;
|
use App\Http\Requests\V1\Salary\StoreSalaryRequest;
|
||||||
use App\Http\Requests\V1\Salary\UpdateSalaryRequest;
|
use App\Http\Requests\V1\Salary\UpdateSalaryRequest;
|
||||||
|
use App\Services\ExportService;
|
||||||
use App\Services\SalaryService;
|
use App\Services\SalaryService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
class SalaryController extends Controller
|
class SalaryController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SalaryService $service
|
private readonly SalaryService $service,
|
||||||
|
private readonly ExportService $exportService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,4 +126,33 @@ public function statistics(Request $request)
|
|||||||
|
|
||||||
return ApiResponse::success($stats, __('message.fetched'));
|
return ApiResponse::success($stats, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 급여 엑셀 내보내기
|
||||||
|
* GET /v1/salaries/export
|
||||||
|
*/
|
||||||
|
public function export(Request $request): BinaryFileResponse
|
||||||
|
{
|
||||||
|
$params = $request->only([
|
||||||
|
'search',
|
||||||
|
'year',
|
||||||
|
'month',
|
||||||
|
'status',
|
||||||
|
'employee_id',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'sort_by',
|
||||||
|
'sort_dir',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$exportData = $this->service->getExportData($params);
|
||||||
|
$filename = '급여현황_'.date('Ymd_His');
|
||||||
|
|
||||||
|
return $this->exportService->download(
|
||||||
|
$exportData['data'],
|
||||||
|
$exportData['headings'],
|
||||||
|
$filename,
|
||||||
|
'급여현황'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Helpers\ApiResponse;
|
use App\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\BulkUpdateAccountCodeRequest;
|
||||||
use App\Http\Requests\V1\Withdrawal\StoreWithdrawalRequest;
|
use App\Http\Requests\V1\Withdrawal\StoreWithdrawalRequest;
|
||||||
use App\Http\Requests\V1\Withdrawal\UpdateWithdrawalRequest;
|
use App\Http\Requests\V1\Withdrawal\UpdateWithdrawalRequest;
|
||||||
use App\Services\WithdrawalService;
|
use App\Services\WithdrawalService;
|
||||||
@@ -94,4 +95,20 @@ public function summary(Request $request)
|
|||||||
|
|
||||||
return ApiResponse::success($summary, __('message.fetched'));
|
return ApiResponse::success($summary, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정과목 일괄 변경
|
||||||
|
*/
|
||||||
|
public function bulkUpdateAccountCode(BulkUpdateAccountCodeRequest $request)
|
||||||
|
{
|
||||||
|
$updatedCount = $this->service->bulkUpdateAccountCode(
|
||||||
|
$request->getIds(),
|
||||||
|
$request->getAccountCode()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(
|
||||||
|
['updated_count' => $updatedCount],
|
||||||
|
__('message.bulk_updated')
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
app/Http/Requests/BulkUpdateAccountCodeRequest.php
Normal file
79
app/Http/Requests/BulkUpdateAccountCodeRequest.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정과목 일괄 변경 요청 검증
|
||||||
|
*
|
||||||
|
* 여러 모듈에서 공통으로 사용:
|
||||||
|
* - 출금관리 (WithdrawalController)
|
||||||
|
* - 입금관리 (DepositController)
|
||||||
|
* - 매출관리 (SaleController)
|
||||||
|
* - 카드거래 (CardTransactionController)
|
||||||
|
*/
|
||||||
|
class BulkUpdateAccountCodeRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 권한 확인
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효성 검사 규칙
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ids' => ['required', 'array', 'min:1'],
|
||||||
|
'ids.*' => ['required', 'integer', 'min:1'],
|
||||||
|
'account_code' => ['required', 'string', 'max:50'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효성 검사 메시지
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ids.required' => __('validation.required', ['attribute' => 'ID 목록']),
|
||||||
|
'ids.array' => __('validation.array', ['attribute' => 'ID 목록']),
|
||||||
|
'ids.min' => __('validation.min.array', ['attribute' => 'ID 목록', 'min' => 1]),
|
||||||
|
'ids.*.required' => __('validation.required', ['attribute' => 'ID']),
|
||||||
|
'ids.*.integer' => __('validation.integer', ['attribute' => 'ID']),
|
||||||
|
'ids.*.min' => __('validation.min.numeric', ['attribute' => 'ID', 'min' => 1]),
|
||||||
|
'account_code.required' => __('validation.required', ['attribute' => '계정과목']),
|
||||||
|
'account_code.string' => __('validation.string', ['attribute' => '계정과목']),
|
||||||
|
'account_code.max' => __('validation.max.string', ['attribute' => '계정과목', 'max' => 50]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검증된 ID 배열 반환
|
||||||
|
*
|
||||||
|
* @return array<int, int>
|
||||||
|
*/
|
||||||
|
public function getIds(): array
|
||||||
|
{
|
||||||
|
return $this->validated('ids');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검증된 계정과목 코드 반환
|
||||||
|
*/
|
||||||
|
public function getAccountCode(): string
|
||||||
|
{
|
||||||
|
return $this->validated('account_code');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -356,6 +356,108 @@ public function checkOut(array $data): Attendance
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 엑셀 내보내기용 데이터 조회
|
||||||
|
*
|
||||||
|
* @return array{data: array<int, array<string, mixed>>, headings: array<int, string>}
|
||||||
|
*/
|
||||||
|
public function getExportData(array $params): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$query = Attendance::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->with([
|
||||||
|
'user:id,name,email',
|
||||||
|
'user.tenantProfiles' => function ($q) use ($tenantId) {
|
||||||
|
$q->where('tenant_id', $tenantId)
|
||||||
|
->with('department:id,name');
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 사용자 필터
|
||||||
|
if (! empty($params['user_id'])) {
|
||||||
|
$query->where('user_id', $params['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜 필터 (단일)
|
||||||
|
if (! empty($params['date'])) {
|
||||||
|
$query->whereDate('base_date', $params['date']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜 범위 필터
|
||||||
|
if (! empty($params['date_from'])) {
|
||||||
|
$query->whereDate('base_date', '>=', $params['date_from']);
|
||||||
|
}
|
||||||
|
if (! empty($params['date_to'])) {
|
||||||
|
$query->whereDate('base_date', '<=', $params['date_to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 필터
|
||||||
|
if (! empty($params['status'])) {
|
||||||
|
$query->where('status', $params['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 부서 필터
|
||||||
|
if (! empty($params['department_id'])) {
|
||||||
|
$query->whereHas('user.tenantProfile', function ($q) use ($params) {
|
||||||
|
$q->where('department_id', $params['department_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정렬
|
||||||
|
$sortBy = $params['sort_by'] ?? 'base_date';
|
||||||
|
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||||
|
$query->orderBy($sortBy, $sortDir);
|
||||||
|
|
||||||
|
$attendances = $query->get();
|
||||||
|
|
||||||
|
// 상태 레이블 매핑
|
||||||
|
$statusLabels = [
|
||||||
|
'onTime' => '정상출근',
|
||||||
|
'late' => '지각',
|
||||||
|
'absent' => '결근',
|
||||||
|
'vacation' => '휴가',
|
||||||
|
'businessTrip' => '출장',
|
||||||
|
'fieldWork' => '외근',
|
||||||
|
'overtime' => '야근',
|
||||||
|
'remote' => '재택',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 엑셀 데이터 변환
|
||||||
|
$data = $attendances->map(function ($attendance) use ($statusLabels) {
|
||||||
|
$profile = $attendance->user?->tenantProfiles?->first();
|
||||||
|
$jsonDetails = $attendance->json_details ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
$attendance->base_date,
|
||||||
|
$attendance->user?->name ?? '-',
|
||||||
|
$profile?->department?->name ?? '-',
|
||||||
|
$statusLabels[$attendance->status] ?? $attendance->status,
|
||||||
|
$attendance->check_in ?? '-',
|
||||||
|
$attendance->check_out ?? '-',
|
||||||
|
isset($jsonDetails['work_minutes']) ? round($jsonDetails['work_minutes'] / 60, 1) : '-',
|
||||||
|
$attendance->remarks ?? '',
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
$headings = [
|
||||||
|
'날짜',
|
||||||
|
'직원명',
|
||||||
|
'부서',
|
||||||
|
'상태',
|
||||||
|
'출근시간',
|
||||||
|
'퇴근시간',
|
||||||
|
'근무시간(h)',
|
||||||
|
'비고',
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $data,
|
||||||
|
'headings' => $headings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 월간 통계 조회
|
* 월간 통계 조회
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -179,6 +179,27 @@ public function destroy(int $id): bool
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정과목 일괄 변경
|
||||||
|
*
|
||||||
|
* @param array<int, int> $ids 변경할 입금 ID 목록
|
||||||
|
* @param string $accountCode 새 계정과목 코드
|
||||||
|
* @return int 변경된 레코드 수
|
||||||
|
*/
|
||||||
|
public function bulkUpdateAccountCode(array $ids, string $accountCode): int
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
return Deposit::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereIn('id', $ids)
|
||||||
|
->update([
|
||||||
|
'account_code' => $accountCode,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 입금 요약 (기간별 합계)
|
* 입금 요약 (기간별 합계)
|
||||||
*/
|
*/
|
||||||
|
|||||||
129
app/Services/ExportService.php
Normal file
129
app/Services/ExportService.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromArray;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 범용 엑셀 내보내기 서비스
|
||||||
|
*
|
||||||
|
* 여러 모듈에서 공통으로 사용할 수 있는 엑셀 내보내기 기능 제공
|
||||||
|
* - 근태관리 (AttendanceController)
|
||||||
|
* - 급여관리 (SalaryController)
|
||||||
|
* - 기타 데이터 내보내기가 필요한 모듈
|
||||||
|
*/
|
||||||
|
class ExportService extends Service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 엑셀 파일 다운로드
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $data 내보낼 데이터 배열
|
||||||
|
* @param array<int, string> $headings 컬럼 헤더 배열
|
||||||
|
* @param string $filename 다운로드 파일명 (확장자 제외)
|
||||||
|
* @param string $sheetTitle 시트 제목
|
||||||
|
*/
|
||||||
|
public function download(
|
||||||
|
array $data,
|
||||||
|
array $headings,
|
||||||
|
string $filename,
|
||||||
|
string $sheetTitle = 'Sheet1'
|
||||||
|
): BinaryFileResponse {
|
||||||
|
$export = new GenericExport($data, $headings, $sheetTitle);
|
||||||
|
|
||||||
|
return Excel::download($export, $filename.'.xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 엑셀 파일 저장 (서버에 저장)
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $data 내보낼 데이터 배열
|
||||||
|
* @param array<int, string> $headings 컬럼 헤더 배열
|
||||||
|
* @param string $path 저장 경로 (storage/app 기준)
|
||||||
|
* @param string $sheetTitle 시트 제목
|
||||||
|
*/
|
||||||
|
public function store(
|
||||||
|
array $data,
|
||||||
|
array $headings,
|
||||||
|
string $path,
|
||||||
|
string $sheetTitle = 'Sheet1'
|
||||||
|
): bool {
|
||||||
|
$export = new GenericExport($data, $headings, $sheetTitle);
|
||||||
|
|
||||||
|
return Excel::store($export, $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 범용 엑셀 내보내기 클래스
|
||||||
|
*
|
||||||
|
* ExportService에서 내부적으로 사용하는 Maatwebsite Excel 구현체
|
||||||
|
*/
|
||||||
|
class GenericExport implements FromArray, ShouldAutoSize, WithHeadings, WithStyles, WithTitle
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $data 내보낼 데이터
|
||||||
|
* @param array<int, string> $headings 컬럼 헤더
|
||||||
|
* @param string $sheetTitle 시트 제목
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $data,
|
||||||
|
private readonly array $headings,
|
||||||
|
private readonly string $sheetTitle
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터 배열 반환
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function array(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 헤더 배열 반환
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return $this->headings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시트 제목 반환
|
||||||
|
*/
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return $this->sheetTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스타일 적용
|
||||||
|
*
|
||||||
|
* @return array<int|string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function styles(Worksheet $sheet): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// 헤더 행 스타일 (굵게, 배경색)
|
||||||
|
1 => [
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'E2E8F0'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -255,6 +255,113 @@ public function bulkUpdateStatus(array $ids, string $status): int
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 엑셀 내보내기용 데이터 조회
|
||||||
|
*
|
||||||
|
* @return array{data: array<int, array<string, mixed>>, headings: array<int, string>}
|
||||||
|
*/
|
||||||
|
public function getExportData(array $params): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$query = Salary::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->with([
|
||||||
|
'employee:id,name,user_id,email',
|
||||||
|
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
|
||||||
|
'employeeProfile.department:id,name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 검색 필터 (직원명)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$salaries = $query->get();
|
||||||
|
|
||||||
|
// 상태 레이블 매핑
|
||||||
|
$statusLabels = [
|
||||||
|
'scheduled' => '지급예정',
|
||||||
|
'completed' => '지급완료',
|
||||||
|
'pending' => '보류',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 엑셀 데이터 변환
|
||||||
|
$data = $salaries->map(function ($salary) use ($statusLabels) {
|
||||||
|
return [
|
||||||
|
$salary->year.'년 '.$salary->month.'월',
|
||||||
|
$salary->employee?->name ?? '-',
|
||||||
|
$salary->employeeProfile?->department?->name ?? '-',
|
||||||
|
number_format($salary->base_salary),
|
||||||
|
number_format($salary->total_allowance),
|
||||||
|
number_format($salary->total_overtime),
|
||||||
|
number_format($salary->total_bonus),
|
||||||
|
number_format($salary->total_deduction),
|
||||||
|
number_format($salary->net_payment),
|
||||||
|
$statusLabels[$salary->status] ?? $salary->status,
|
||||||
|
$salary->payment_date ?? '-',
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
$headings = [
|
||||||
|
'급여월',
|
||||||
|
'직원명',
|
||||||
|
'부서',
|
||||||
|
'기본급',
|
||||||
|
'수당',
|
||||||
|
'야근수당',
|
||||||
|
'상여금',
|
||||||
|
'공제액',
|
||||||
|
'실지급액',
|
||||||
|
'상태',
|
||||||
|
'지급일',
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $data,
|
||||||
|
'headings' => $headings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 급여 통계 조회
|
* 급여 통계 조회
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -179,6 +179,27 @@ public function destroy(int $id): bool
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정과목 일괄 변경
|
||||||
|
*
|
||||||
|
* @param array<int, int> $ids 변경할 출금 ID 목록
|
||||||
|
* @param string $accountCode 새 계정과목 코드
|
||||||
|
* @return int 변경된 레코드 수
|
||||||
|
*/
|
||||||
|
public function bulkUpdateAccountCode(array $ids, string $accountCode): int
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
return Withdrawal::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereIn('id', $ids)
|
||||||
|
->update([
|
||||||
|
'account_code' => $accountCode,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 출금 요약 (기간별 합계)
|
* 출금 요약 (기간별 합계)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -340,6 +340,7 @@
|
|||||||
Route::get('', [AttendanceController::class, 'index'])->name('v1.attendances.index');
|
Route::get('', [AttendanceController::class, 'index'])->name('v1.attendances.index');
|
||||||
Route::post('', [AttendanceController::class, 'store'])->name('v1.attendances.store');
|
Route::post('', [AttendanceController::class, 'store'])->name('v1.attendances.store');
|
||||||
Route::get('/monthly-stats', [AttendanceController::class, 'monthlyStats'])->name('v1.attendances.monthlyStats');
|
Route::get('/monthly-stats', [AttendanceController::class, 'monthlyStats'])->name('v1.attendances.monthlyStats');
|
||||||
|
Route::get('/export', [AttendanceController::class, 'export'])->name('v1.attendances.export');
|
||||||
Route::post('/check-in', [AttendanceController::class, 'checkIn'])->name('v1.attendances.checkIn');
|
Route::post('/check-in', [AttendanceController::class, 'checkIn'])->name('v1.attendances.checkIn');
|
||||||
Route::post('/check-out', [AttendanceController::class, 'checkOut'])->name('v1.attendances.checkOut');
|
Route::post('/check-out', [AttendanceController::class, 'checkOut'])->name('v1.attendances.checkOut');
|
||||||
Route::get('/{id}', [AttendanceController::class, 'show'])->name('v1.attendances.show');
|
Route::get('/{id}', [AttendanceController::class, 'show'])->name('v1.attendances.show');
|
||||||
@@ -503,6 +504,7 @@
|
|||||||
Route::get('', [DepositController::class, 'index'])->name('v1.deposits.index');
|
Route::get('', [DepositController::class, 'index'])->name('v1.deposits.index');
|
||||||
Route::post('', [DepositController::class, 'store'])->name('v1.deposits.store');
|
Route::post('', [DepositController::class, 'store'])->name('v1.deposits.store');
|
||||||
Route::get('/summary', [DepositController::class, 'summary'])->name('v1.deposits.summary');
|
Route::get('/summary', [DepositController::class, 'summary'])->name('v1.deposits.summary');
|
||||||
|
Route::post('/bulk-update-account-code', [DepositController::class, 'bulkUpdateAccountCode'])->name('v1.deposits.bulk-update-account-code');
|
||||||
Route::get('/{id}', [DepositController::class, 'show'])->whereNumber('id')->name('v1.deposits.show');
|
Route::get('/{id}', [DepositController::class, 'show'])->whereNumber('id')->name('v1.deposits.show');
|
||||||
Route::put('/{id}', [DepositController::class, 'update'])->whereNumber('id')->name('v1.deposits.update');
|
Route::put('/{id}', [DepositController::class, 'update'])->whereNumber('id')->name('v1.deposits.update');
|
||||||
Route::delete('/{id}', [DepositController::class, 'destroy'])->whereNumber('id')->name('v1.deposits.destroy');
|
Route::delete('/{id}', [DepositController::class, 'destroy'])->whereNumber('id')->name('v1.deposits.destroy');
|
||||||
@@ -513,6 +515,7 @@
|
|||||||
Route::get('', [WithdrawalController::class, 'index'])->name('v1.withdrawals.index');
|
Route::get('', [WithdrawalController::class, 'index'])->name('v1.withdrawals.index');
|
||||||
Route::post('', [WithdrawalController::class, 'store'])->name('v1.withdrawals.store');
|
Route::post('', [WithdrawalController::class, 'store'])->name('v1.withdrawals.store');
|
||||||
Route::get('/summary', [WithdrawalController::class, 'summary'])->name('v1.withdrawals.summary');
|
Route::get('/summary', [WithdrawalController::class, 'summary'])->name('v1.withdrawals.summary');
|
||||||
|
Route::post('/bulk-update-account-code', [WithdrawalController::class, 'bulkUpdateAccountCode'])->name('v1.withdrawals.bulk-update-account-code');
|
||||||
Route::get('/{id}', [WithdrawalController::class, 'show'])->whereNumber('id')->name('v1.withdrawals.show');
|
Route::get('/{id}', [WithdrawalController::class, 'show'])->whereNumber('id')->name('v1.withdrawals.show');
|
||||||
Route::put('/{id}', [WithdrawalController::class, 'update'])->whereNumber('id')->name('v1.withdrawals.update');
|
Route::put('/{id}', [WithdrawalController::class, 'update'])->whereNumber('id')->name('v1.withdrawals.update');
|
||||||
Route::delete('/{id}', [WithdrawalController::class, 'destroy'])->whereNumber('id')->name('v1.withdrawals.destroy');
|
Route::delete('/{id}', [WithdrawalController::class, 'destroy'])->whereNumber('id')->name('v1.withdrawals.destroy');
|
||||||
@@ -538,6 +541,7 @@
|
|||||||
Route::get('', [SalaryController::class, 'index'])->name('v1.salaries.index');
|
Route::get('', [SalaryController::class, 'index'])->name('v1.salaries.index');
|
||||||
Route::post('', [SalaryController::class, 'store'])->name('v1.salaries.store');
|
Route::post('', [SalaryController::class, 'store'])->name('v1.salaries.store');
|
||||||
Route::get('/statistics', [SalaryController::class, 'statistics'])->name('v1.salaries.statistics');
|
Route::get('/statistics', [SalaryController::class, 'statistics'])->name('v1.salaries.statistics');
|
||||||
|
Route::get('/export', [SalaryController::class, 'export'])->name('v1.salaries.export');
|
||||||
Route::post('/bulk-update-status', [SalaryController::class, 'bulkUpdateStatus'])->name('v1.salaries.bulk-update-status');
|
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::get('/{id}', [SalaryController::class, 'show'])->whereNumber('id')->name('v1.salaries.show');
|
||||||
Route::put('/{id}', [SalaryController::class, 'update'])->whereNumber('id')->name('v1.salaries.update');
|
Route::put('/{id}', [SalaryController::class, 'update'])->whereNumber('id')->name('v1.salaries.update');
|
||||||
|
|||||||
Reference in New Issue
Block a user