feat: [payroll] 급여명세서 엑셀 내보내기 CSV → XLSX 변환
- 제목행 병합 + 14pt 굵게 가운데 정렬
- 남색(#1F3864) 헤더 + 흰색 글씨 + wrapText
- 금액 열(D~O) #,##0 천단위 서식 + 오른쪽 정렬
- 합계행 SUM 수식 + 회색 배경 + 굵게
- 빈 행 포함 최소 10행까지 전체 테두리
- 파일명: 급여명세서_{year}년{month}월_{Ymd}.xlsx
This commit is contained in:
@@ -3,11 +3,15 @@
|
||||
namespace App\Http\Controllers\Api\Admin\HR;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\HR\Payroll;
|
||||
use App\Services\HR\PayrollService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class PayrollController extends Controller
|
||||
@@ -425,7 +429,7 @@ public function bulkGenerate(Request $request): JsonResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* 엑셀(CSV) 내보내기
|
||||
* 엑셀(XLSX) 내보내기 — 급여명세서 서식
|
||||
*/
|
||||
public function export(Request $request): StreamedResponse|JsonResponse
|
||||
{
|
||||
@@ -437,42 +441,134 @@ public function export(Request $request): StreamedResponse|JsonResponse
|
||||
$year = $request->input('year', now()->year);
|
||||
$month = $request->input('month', now()->month);
|
||||
|
||||
$filename = "급여관리_{$year}년{$month}월_".now()->format('Ymd').'.csv';
|
||||
$filename = "급여명세서_{$year}년{$month}월_".now()->format('Ymd').'.xlsx';
|
||||
|
||||
return response()->streamDownload(function () use ($payrolls) {
|
||||
$file = fopen('php://output', 'w');
|
||||
fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM
|
||||
$spreadsheet = new Spreadsheet;
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('급여명세서');
|
||||
|
||||
fputcsv($file, ['사원명', '부서', '기본급', '고정연장근로수당', '식대(비과세)', '총지급액', '국민연금', '건강보험', '장기요양보험', '고용보험', '근로소득세', '지방소득세', '총공제액', '실수령액', '상태']);
|
||||
$lastCol = 'O';
|
||||
$headers = ['No', '사원명', '부서', '기본급', "고정연장\n근로수당", "식대\n(비과세)", '총지급액', '국민연금', '건강보험', "장기요양\n보험", '고용보험', '근로소득세', '지방소득세', '총공제액', '실수령액'];
|
||||
|
||||
foreach ($payrolls as $payroll) {
|
||||
$profile = $payroll->user?->tenantProfiles?->first();
|
||||
$displayName = $profile?->display_name ?? $payroll->user?->name ?? '-';
|
||||
$department = $profile?->department?->name ?? '-';
|
||||
$statusLabel = Payroll::STATUS_MAP[$payroll->status] ?? $payroll->status;
|
||||
// ── Row 1: 제목 ──
|
||||
$sheet->mergeCells("A1:{$lastCol}1");
|
||||
$sheet->setCellValue('A1', "< {$year}년도 {$month}월 급여명세서 >");
|
||||
$sheet->getStyle('A1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 14],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$sheet->getRowDimension(1)->setRowHeight(30);
|
||||
|
||||
fputcsv($file, [
|
||||
$displayName,
|
||||
$department,
|
||||
$payroll->base_salary,
|
||||
$payroll->overtime_pay,
|
||||
$payroll->bonus,
|
||||
$payroll->gross_salary,
|
||||
$payroll->pension,
|
||||
$payroll->health_insurance,
|
||||
$payroll->long_term_care,
|
||||
$payroll->employment_insurance,
|
||||
$payroll->income_tax,
|
||||
$payroll->resident_tax,
|
||||
$payroll->total_deductions,
|
||||
$payroll->net_salary,
|
||||
$statusLabel,
|
||||
]);
|
||||
}
|
||||
// ── Row 2: 헤더 ──
|
||||
foreach ($headers as $colIdx => $header) {
|
||||
$cell = chr(65 + $colIdx).'2';
|
||||
$sheet->setCellValue($cell, $header);
|
||||
}
|
||||
$sheet->getStyle("A2:{$lastCol}2")->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 10, 'color' => ['argb' => 'FFFFFFFF']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF1F3864']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['argb' => 'FF000000']]],
|
||||
]);
|
||||
$sheet->getRowDimension(2)->setRowHeight(36);
|
||||
|
||||
fclose($file);
|
||||
// ── Row 3~: 데이터 ──
|
||||
$dataStartRow = 3;
|
||||
$row = $dataStartRow;
|
||||
$moneyColumns = ['D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
|
||||
|
||||
foreach ($payrolls as $idx => $payroll) {
|
||||
$profile = $payroll->user?->tenantProfiles?->first();
|
||||
$displayName = $profile?->display_name ?? $payroll->user?->name ?? '-';
|
||||
$department = $profile?->department?->name ?? '-';
|
||||
|
||||
$sheet->setCellValue("A{$row}", $idx + 1);
|
||||
$sheet->setCellValue("B{$row}", $displayName);
|
||||
$sheet->setCellValue("C{$row}", $department);
|
||||
$sheet->setCellValue("D{$row}", (int) $payroll->base_salary);
|
||||
$sheet->setCellValue("E{$row}", (int) $payroll->overtime_pay);
|
||||
$sheet->setCellValue("F{$row}", (int) $payroll->bonus);
|
||||
$sheet->setCellValue("G{$row}", (int) $payroll->gross_salary);
|
||||
$sheet->setCellValue("H{$row}", (int) $payroll->pension);
|
||||
$sheet->setCellValue("I{$row}", (int) $payroll->health_insurance);
|
||||
$sheet->setCellValue("J{$row}", (int) $payroll->long_term_care);
|
||||
$sheet->setCellValue("K{$row}", (int) $payroll->employment_insurance);
|
||||
$sheet->setCellValue("L{$row}", (int) $payroll->income_tax);
|
||||
$sheet->setCellValue("M{$row}", (int) $payroll->resident_tax);
|
||||
$sheet->setCellValue("N{$row}", (int) $payroll->total_deductions);
|
||||
$sheet->setCellValue("O{$row}", (int) $payroll->net_salary);
|
||||
|
||||
$row++;
|
||||
}
|
||||
|
||||
// 빈 행 채움 (최소 10행까지 테두리)
|
||||
$minEndRow = $dataStartRow + 9;
|
||||
while ($row <= $minEndRow) {
|
||||
$row++;
|
||||
}
|
||||
$lastDataRow = $row - 1;
|
||||
|
||||
// ── 데이터 영역 스타일 ──
|
||||
$sheet->getStyle("A{$dataStartRow}:{$lastCol}{$lastDataRow}")->applyFromArray([
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['argb' => 'FF000000']]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
'font' => ['size' => 10],
|
||||
]);
|
||||
|
||||
// No 가운데 정렬
|
||||
$sheet->getStyle("A{$dataStartRow}:A{$lastDataRow}")
|
||||
->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
|
||||
// 금액 서식: #,##0 + 오른쪽 정렬
|
||||
foreach ($moneyColumns as $col) {
|
||||
$range = "{$col}{$dataStartRow}:{$col}{$lastDataRow}";
|
||||
$sheet->getStyle($range)->getNumberFormat()->setFormatCode('#,##0');
|
||||
$sheet->getStyle($range)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||
}
|
||||
|
||||
// ── 합계 행 ──
|
||||
$sumRow = $lastDataRow + 1;
|
||||
$sheet->mergeCells("A{$sumRow}:C{$sumRow}");
|
||||
$sheet->setCellValue("A{$sumRow}", '합계');
|
||||
|
||||
foreach ($moneyColumns as $col) {
|
||||
$sheet->setCellValue(
|
||||
"{$col}{$sumRow}",
|
||||
"=SUM({$col}{$dataStartRow}:{$col}{$lastDataRow})"
|
||||
);
|
||||
}
|
||||
|
||||
$sheet->getStyle("A{$sumRow}:{$lastCol}{$sumRow}")->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 10],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FFF2F2F2']],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['argb' => 'FF000000']]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$sheet->getStyle("A{$sumRow}:C{$sumRow}")
|
||||
->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
|
||||
foreach ($moneyColumns as $col) {
|
||||
$sheet->getStyle("{$col}{$sumRow}")->getNumberFormat()->setFormatCode('#,##0');
|
||||
$sheet->getStyle("{$col}{$sumRow}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||
}
|
||||
|
||||
// ── 열 너비 ──
|
||||
$widths = ['A' => 6, 'B' => 12, 'C' => 12];
|
||||
foreach (range('D', 'O') as $col) {
|
||||
$widths[$col] = 13;
|
||||
}
|
||||
foreach ($widths as $col => $width) {
|
||||
$sheet->getColumnDimension($col)->setWidth($width);
|
||||
}
|
||||
|
||||
// ── 응답 반환 ──
|
||||
return response()->streamDownload(function () use ($spreadsheet) {
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$writer->save('php://output');
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Cache-Control' => 'max-age=0',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user