feat: [approval] 경력증명서 기안/조회/PDF 기능 추가
- CareerCertService: 사원 경력정보 조회 + TCPDF PDF 생성 - 기안 작성 폼: 사원 선택, 인적/경력/발급 정보, 미리보기 - 상세 조회: 읽기전용 렌더링 + 미리보기/PDF 다운로드 - API: career-cert-info, career-cert-pdf 엔드포인트
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Boards\File;
|
||||
use App\Services\ApprovalService;
|
||||
use App\Services\CareerCertService;
|
||||
use App\Services\EmploymentCertService;
|
||||
use App\Services\GoogleCloudStorageService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -292,6 +293,43 @@ public function certPdf(int $id)
|
||||
return $service->generatePdfResponse($content);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 경력증명서
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 사원 경력증명서 정보 조회
|
||||
*/
|
||||
public function careerCertInfo(int $userId): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$service = app(CareerCertService::class);
|
||||
$data = $service->getCertInfo($userId, $tenantId);
|
||||
|
||||
return response()->json(['success' => true, 'data' => $data]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 불러올 수 없습니다.',
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 경력증명서 PDF 다운로드
|
||||
*/
|
||||
public function careerCertPdf(int $id)
|
||||
{
|
||||
$approval = \App\Models\Approvals\Approval::where('tenant_id', session('selected_tenant_id'))
|
||||
->findOrFail($id);
|
||||
|
||||
$content = $approval->content ?? [];
|
||||
$service = app(CareerCertService::class);
|
||||
|
||||
return $service->generatePdfResponse($content);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 워크플로우
|
||||
// =========================================================================
|
||||
|
||||
236
app/Services/CareerCertService.php
Normal file
236
app/Services/CareerCertService.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\HR\Employee;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantSetting;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CareerCertService
|
||||
{
|
||||
private ?string $koreanFontName = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (! defined('K_PATH_FONTS')) {
|
||||
$tcpdfFontsDir = dirname(__DIR__, 2).'/storage/fonts/tcpdf/';
|
||||
if (is_dir($tcpdfFontsDir)) {
|
||||
define('K_PATH_FONTS', $tcpdfFontsDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원의 경력증명서 정보 조회
|
||||
*/
|
||||
public function getCertInfo(int $userId, int $tenantId): array
|
||||
{
|
||||
$employee = Employee::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('user_id', $userId)
|
||||
->with(['user', 'department'])
|
||||
->firstOrFail();
|
||||
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
|
||||
$displaySetting = TenantSetting::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('setting_group', 'company')
|
||||
->where('setting_key', 'display_company_name')
|
||||
->first();
|
||||
$displayName = $displaySetting?->setting_value ?? '';
|
||||
$companyName = ! empty($displayName) ? $displayName : ($tenant->company_name ?? '');
|
||||
|
||||
// 생년월일 추출 (주민번호 앞 6자리)
|
||||
$residentNumber = $employee->resident_number ?? '';
|
||||
$birthDate = '';
|
||||
if (strlen($residentNumber) >= 6) {
|
||||
$yy = substr($residentNumber, 0, 2);
|
||||
$mm = substr($residentNumber, 2, 2);
|
||||
$dd = substr($residentNumber, 4, 2);
|
||||
// 7번째 자리로 세기 판단
|
||||
$century = '19';
|
||||
if (strlen($residentNumber) >= 7) {
|
||||
$genderDigit = substr($residentNumber, 7, 1); // 하이픈 뒤 첫째 자리
|
||||
if (in_array($genderDigit, ['3', '4'])) {
|
||||
$century = '20';
|
||||
}
|
||||
}
|
||||
$birthDate = $century.$yy.'-'.$mm.'-'.$dd;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $employee->user->name ?? $employee->display_name ?? '',
|
||||
'birth_date' => $birthDate,
|
||||
'address' => $employee->address ?? '',
|
||||
'department' => $employee->department?->name ?? '',
|
||||
'position' => $employee->position_label ?? '',
|
||||
'hire_date' => $employee->hire_date ?? '',
|
||||
'resign_date' => $employee->resign_date ?? '',
|
||||
'job_description' => '',
|
||||
'company_name' => $companyName,
|
||||
'business_num' => $tenant->business_num ?? '',
|
||||
'ceo_name' => $tenant->ceo_name ?? '',
|
||||
'phone' => $tenant->phone ?? '',
|
||||
'company_address' => $tenant->address ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* content JSON 기반 PDF Response 생성
|
||||
*/
|
||||
public function generatePdfResponse(array $content): \Illuminate\Http\Response
|
||||
{
|
||||
$pdf = new \TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
||||
$pdf->SetCreator('SAM');
|
||||
$pdf->SetAuthor($content['company_name'] ?? 'SAM');
|
||||
$pdf->SetTitle('경력증명서');
|
||||
|
||||
$pdf->setPrintHeader(false);
|
||||
$pdf->setPrintFooter(false);
|
||||
$pdf->SetMargins(20, 20, 20);
|
||||
$pdf->SetAutoPageBreak(true, 20);
|
||||
|
||||
$font = $this->getKoreanFont();
|
||||
|
||||
$pdf->AddPage();
|
||||
|
||||
// 제목
|
||||
$pdf->SetFont($font, 'B', 22);
|
||||
$pdf->Cell(0, 20, '경 력 증 명 서', 0, 1, 'C');
|
||||
$pdf->Ln(8);
|
||||
|
||||
// === 1. 인적사항 ===
|
||||
$pdf->SetFont($font, 'B', 12);
|
||||
$pdf->Cell(0, 8, '1. 인적사항', 0, 1, 'L');
|
||||
$pdf->Ln(2);
|
||||
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['성 명', $content['name'] ?? '-', 40],
|
||||
['생년월일', $content['birth_date'] ?? '-', 40],
|
||||
]);
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['주 소', $content['address'] ?? '-', 0],
|
||||
]);
|
||||
$pdf->Ln(6);
|
||||
|
||||
// === 2. 경력사항 ===
|
||||
$pdf->SetFont($font, 'B', 12);
|
||||
$pdf->Cell(0, 8, '2. 경력사항', 0, 1, 'L');
|
||||
$pdf->Ln(2);
|
||||
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['회 사 명', $content['company_name'] ?? '-', 40],
|
||||
['사업자번호', $content['business_num'] ?? '-', 40],
|
||||
]);
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['대 표 자', $content['ceo_name'] ?? '-', 40],
|
||||
['대표전화', $content['phone'] ?? '-', 40],
|
||||
]);
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['소 재 지', $content['company_address'] ?? '-', 0],
|
||||
]);
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['소속부서', $content['department'] ?? '-', 40],
|
||||
['직위/직급', $content['position'] ?? '-', 40],
|
||||
]);
|
||||
|
||||
$hireDate = $content['hire_date'] ?? '';
|
||||
$resignDate = $content['resign_date'] ?? '';
|
||||
$periodDisplay = $hireDate ? $hireDate.' ~ '.($resignDate ?: '현재') : '-';
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['근무기간', $periodDisplay, 0],
|
||||
]);
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['담당업무', $content['job_description'] ?? '-', 0],
|
||||
]);
|
||||
$this->addTableRow($pdf, $font, [
|
||||
['사용용도', $content['purpose'] ?? '-', 0],
|
||||
]);
|
||||
$pdf->Ln(12);
|
||||
|
||||
// 증명 문구
|
||||
$pdf->SetFont($font, '', 12);
|
||||
$pdf->Cell(0, 10, '위 사람은 당사에 재직(근무) 하였음을 증명합니다.', 0, 1, 'C');
|
||||
$pdf->Ln(6);
|
||||
|
||||
// 발급일
|
||||
$issueDate = $content['issue_date'] ?? date('Y-m-d');
|
||||
$issueDateFormatted = $this->formatDate($issueDate);
|
||||
$pdf->SetFont($font, 'B', 12);
|
||||
$pdf->Cell(0, 10, $issueDateFormatted, 0, 1, 'C');
|
||||
$pdf->Ln(12);
|
||||
|
||||
// 회사명 + 대표이사
|
||||
$ceoName = $content['ceo_name'] ?? '';
|
||||
$pdf->SetFont($font, 'B', 14);
|
||||
$pdf->Cell(0, 10, ($content['company_name'] ?? '').' 대표이사 '.$ceoName.' (인)', 0, 1, 'C');
|
||||
|
||||
$pdfContent = $pdf->Output('', 'S');
|
||||
$fileName = '경력증명서_'.($content['name'] ?? '').'.pdf';
|
||||
|
||||
return response($pdfContent, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="'.$fileName.'"',
|
||||
]);
|
||||
}
|
||||
|
||||
private function addTableRow(\TCPDF $pdf, string $font, array $cells): void
|
||||
{
|
||||
$pageWidth = $pdf->getPageWidth() - 40;
|
||||
$rowHeight = 8;
|
||||
$thWidth = 30;
|
||||
|
||||
if (count($cells) === 1) {
|
||||
$pdf->SetFont($font, 'B', 10);
|
||||
$pdf->SetFillColor(248, 249, 250);
|
||||
$pdf->Cell($thWidth, $rowHeight, $cells[0][0], 1, 0, 'L', true);
|
||||
$pdf->SetFont($font, '', 10);
|
||||
$pdf->Cell($pageWidth - $thWidth, $rowHeight, $cells[0][1], 1, 1, 'L');
|
||||
} else {
|
||||
$tdWidth = ($pageWidth - $thWidth * 2) / 2;
|
||||
foreach ($cells as $cell) {
|
||||
$pdf->SetFont($font, 'B', 10);
|
||||
$pdf->SetFillColor(248, 249, 250);
|
||||
$pdf->Cell($thWidth, $rowHeight, $cell[0], 1, 0, 'L', true);
|
||||
$pdf->SetFont($font, '', 10);
|
||||
$pdf->Cell($tdWidth, $rowHeight, $cell[1], 1, 0, 'L');
|
||||
}
|
||||
$pdf->Ln();
|
||||
}
|
||||
}
|
||||
|
||||
private function formatDate(string $date): string
|
||||
{
|
||||
try {
|
||||
return date('Y년 m월 d일', strtotime($date));
|
||||
} catch (\Throwable) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
private function getKoreanFont(): string
|
||||
{
|
||||
if ($this->koreanFontName) {
|
||||
return $this->koreanFontName;
|
||||
}
|
||||
|
||||
if (defined('K_PATH_FONTS') && file_exists(K_PATH_FONTS.'pretendard.php')) {
|
||||
$this->koreanFontName = 'pretendard';
|
||||
|
||||
return $this->koreanFontName;
|
||||
}
|
||||
|
||||
$fontPath = storage_path('fonts/Pretendard-Regular.ttf');
|
||||
if (file_exists($fontPath)) {
|
||||
try {
|
||||
$this->koreanFontName = \TCPDF_FONTS::addTTFfont($fontPath, 'TrueTypeUnicode', '', 96);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('TCPDF 한글 폰트 등록 실패', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->koreanFontName ?: 'helvetica';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user