From e03541b6785e672615c70bfe83f7fb2bd96f654d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Wed, 11 Mar 2026 11:50:37 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[pdf]=20=ED=8F=B0=ED=8A=B8=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20NanumGothic=20=E2=86=92=20Pretendard=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 4 +- dev/standards/pdf-font-policy.md | 97 ++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 94dd400..74528c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -352,8 +352,8 @@ Claude 역할 Jenkins (자동) 운영 서버 ❌ 구글 폰트(@import, ) 사용 금지 ❌ isRemoteEnabled 옵션 사용 금지 ❌ font-family에 시스템 전용 폰트(Malgun Gothic 등) 단독 사용 금지 -❌ font-weight: 800 이상 사용 금지 (NanumGothic은 normal/bold만 존재) -✅ font-family: 'NanumGothic', 'Malgun Gothic', sans-serif 사용 +❌ font-weight: 800 이상 사용 금지 (Pretendard는 normal/bold만 DomPDF 등록) +✅ font-family: 'Pretendard', 'Malgun Gothic', sans-serif 사용 ✅ ensureKoreanFont() 호출 후 PDF 생성 ✅ 폰트 원본은 resources/fonts/에 프로젝트와 함께 배포 ✅ config/dompdf.php의 enable_font_subsetting: true (경량화) diff --git a/dev/standards/pdf-font-policy.md b/dev/standards/pdf-font-policy.md index f286ff4..3ead7cb 100644 --- a/dev/standards/pdf-font-policy.md +++ b/dev/standards/pdf-font-policy.md @@ -26,11 +26,11 @@ Failed to open stream: Permission denied ❌ PDF 뷰에서 구글 폰트(@import, ) 사용 금지 ❌ DomPDF의 isRemoteEnabled 옵션 사용 금지 ❌ font-family에 'Malgun Gothic' 등 시스템 전용 폰트 단독 사용 금지 (DomPDF가 인식 못함) -✅ NanumGothic을 DomPDF에 등록하여 사용 +❌ font-weight: 800 이상 사용 금지 (DomPDF 미매칭으로 한글 깨짐) +✅ Pretendard를 DomPDF에 등록하여 사용 ✅ 폰트 원본은 resources/fonts/에 프로젝트와 함께 배포 (시스템 설치 의존 금지) ✅ 폰트 캐시는 storage/fonts/에 저장 (vendor/ 아님) ✅ 자동 등록 패턴(ensureKoreanFont) 적용 -✅ font-weight는 normal/bold만 사용 (800 이상 금지 — DomPDF 미매칭으로 한글 깨짐) ✅ 폰트 서브셋팅 활성화 (config/dompdf.php) ``` @@ -56,7 +56,7 @@ $pdf = Pdf::loadView('view', $data) ### 2.3 font-weight 800 이상 사용 금지 -NanumGothic은 `normal`(400)과 `bold`(700) 두 가지 weight만 존재한다. `font-weight: 800` 이상을 지정하면 DomPDF가 매칭되는 폰트를 찾지 못해 해당 텍스트의 한글이 `?`로 깨진다. +Pretendard는 `normal`(400)과 `bold`(700)만 DomPDF에 등록되어 있다. `font-weight: 800` 이상을 지정하면 DomPDF가 매칭되는 폰트를 찾지 못해 해당 텍스트의 한글이 `?`로 깨진다. ```css /* ❌ DomPDF에서 한글 깨짐 — 800 weight에 매칭되는 폰트 없음 */ @@ -99,28 +99,28 @@ DomPDF는 웹 브라우저가 아니다. 구글 폰트를 다운로드 → 파 ## 4. 올바른 폰트 사용법 -### 4.1 표준 한글 폰트: NanumGothic +### 4.1 표준 한글 폰트: Pretendard -SAM 프로젝트의 PDF 한글 폰트는 **NanumGothic**을 사용한다. 폰트 파일은 프로젝트에 포함되어 Git으로 배포된다. 서버에 `fonts-nanum` 패키지를 별도 설치할 필요가 없다. +SAM 프로젝트의 PDF 한글 폰트는 **Pretendard**를 사용한다. 폰트 파일은 프로젝트에 포함되어 Git으로 배포된다. 서버에 별도 폰트 패키지를 설치할 필요가 없다. | 항목 | 경로 | 설명 | |------|------|------| -| 폰트 원본 (Git 관리) | `resources/fonts/NanumGothic.ttf` | 프로젝트와 함께 배포 | -| 폰트 원본 (Git 관리) | `resources/fonts/NanumGothicBold.ttf` | 프로젝트와 함께 배포 | +| 폰트 원본 (Git 관리) | `resources/fonts/Pretendard-Regular.ttf` | 프로젝트와 함께 배포 | +| 폰트 원본 (Git 관리) | `resources/fonts/Pretendard-Bold.ttf` | 프로젝트와 함께 배포 | | DomPDF 캐시 | `storage/fonts/` | 런타임에 자동 생성 (.gitignore) | -> 이전에는 시스템 폰트(`/usr/share/fonts/truetype/nanum/`)에 의존했으나, 운영서버에 `fonts-nanum` 미설치 시 한글이 깨지는 문제가 발생하여 프로젝트 번들링 방식으로 전환했다. +> 이전에는 NanumGothic을 사용했으나, 운영서버에서 폰트 메트릭 생성이 실패하는 문제가 발생하여 Pretendard로 전환했다. Pretendard는 운영서버에 이미 설치되어 있어 호환성이 높다. ### 4.2 Blade 뷰 font-family 지정 ```css /* ✅ 올바른 PDF 뷰 폰트 지정 */ body { - font-family: 'NanumGothic', 'Malgun Gothic', sans-serif; + font-family: 'Pretendard', 'Malgun Gothic', sans-serif; } ``` -> `NanumGothic`이 DomPDF에서 사용되고, `Malgun Gothic`은 브라우저에서 HTML을 직접 볼 때의 fallback이다. +> `Pretendard`가 DomPDF에서 사용되고, `Malgun Gothic`은 브라우저에서 HTML을 직접 볼 때의 fallback이다. ### 4.3 자동 폰트 등록 패턴 (ensureKoreanFont) @@ -128,28 +128,31 @@ PDF를 생성하는 서비스에 다음 패턴을 적용한다. 최초 1회만 ```php /** - * DomPDF에 한글(NanumGothic) 폰트 등록 (최초 1회만 실행) + * DomPDF에 한글(Pretendard) 폰트 등록 (최초 1회만 실행) * * 폰트 원본은 resources/fonts/에 프로젝트와 함께 배포된다. - * 시스템 폰트(fonts-nanum) 설치 여부에 의존하지 않는다. + * 시스템 폰트 설치 여부에 의존하지 않는다. */ private function ensureKoreanFont(): void { $fontDir = storage_path('fonts'); $installedFile = $fontDir.'/installed-fonts.json'; - // 이미 등록되어 있으면 스킵 + // 이미 등록되어 있고 캐시 파일이 유효하면 스킵 if (file_exists($installedFile)) { $installed = json_decode(file_get_contents($installedFile), true) ?: []; - if (isset($installed['nanumgothic'])) { + $hasFont = isset($installed['pretendard']); + $hasTtf = file_exists($fontDir.'/Pretendard-Regular.ttf'); + $hasMetric = ! empty(glob($fontDir.'/pretendard_normal_*.ufm*')); + if ($hasFont && $hasTtf && $hasMetric) { return; } } // 프로젝트에 포함된 폰트 (Git으로 배포됨) $fontSources = [ - 'normal' => resource_path('fonts/NanumGothic.ttf'), - 'bold' => resource_path('fonts/NanumGothicBold.ttf'), + 'normal' => resource_path('fonts/Pretendard-Regular.ttf'), + 'bold' => resource_path('fonts/Pretendard-Bold.ttf'), ]; if (! file_exists($fontSources['normal'])) { @@ -169,14 +172,16 @@ private function ensureKoreanFont(): void } $dompdf = app(\Barryvdh\DomPDF\PDF::class)->getDomPDF(); + $dompdf->getOptions()->set('font_dir', $fontDir); + $dompdf->getOptions()->set('font_cache', $fontDir); $fm = $dompdf->getFontMetrics(); $fm->registerFont( - ['family' => 'nanumgothic', 'style' => 'normal', 'weight' => 'normal'], - $fontDir.'/NanumGothic.ttf' + ['family' => 'pretendard', 'style' => 'normal', 'weight' => 'normal'], + $fontDir.'/Pretendard-Regular.ttf' ); $fm->registerFont( - ['family' => 'nanumgothic', 'style' => 'normal', 'weight' => 'bold'], - $fontDir.'/NanumGothicBold.ttf' + ['family' => 'pretendard', 'style' => 'normal', 'weight' => 'bold'], + $fontDir.'/Pretendard-Bold.ttf' ); $fm->saveFontFamilies(); } @@ -189,7 +194,12 @@ private function ensureKoreanFont(): void $this->ensureKoreanFont(); $pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData]) - ->setPaper('a4'); + ->setPaper('a4') + ->setOptions([ + 'font_dir' => storage_path('fonts'), + 'font_cache' => storage_path('fonts'), + 'enable_font_subsetting' => true, + ]); $pdfContent = $pdf->output(); ``` @@ -198,12 +208,12 @@ $pdfContent = $pdf->output(); ``` storage/fonts/ -├── installed-fonts.json ← DomPDF 폰트 레지스트리 -├── NanumGothic.ttf ← resources/fonts/에서 복사된 원본 -├── NanumGothicBold.ttf -├── nanumgothic_normal_*.ufm ← DomPDF가 생성한 메트릭 캐시 -├── nanumgothic_normal_*.ttf ← DomPDF가 생성한 서브셋 -└── nanumgothic_bold_*.* +├── installed-fonts.json ← DomPDF 폰트 레지스트리 +├── Pretendard-Regular.ttf ← resources/fonts/에서 복사된 원본 +├── Pretendard-Bold.ttf +├── pretendard_normal_*.ufm ← DomPDF가 생성한 메트릭 캐시 +├── pretendard_normal_*.ttf ← DomPDF가 생성한 서브셋 +└── pretendard_bold_*.* ``` > `storage/fonts/`는 `.gitignore`에 포함되어 있다. 각 환경에서 첫 PDF 생성 시 자동으로 생성된다. @@ -214,7 +224,7 @@ storage/fonts/ ### 5.1 배경 — 텍스트 PDF가 5MB -급여명세서 PDF(텍스트만, 1페이지)가 약 5MB로 생성되는 문제가 발생. 원인은 DomPDF가 NanumGothic 폰트 파일 전체(수천 개 한글 글리프 포함)를 PDF에 임베딩하기 때문이다. +급여명세서 PDF(텍스트만, 1페이지)가 약 5MB로 생성되는 문제가 발생. 원인은 DomPDF가 폰트 파일 전체(수천 개 한글 글리프 포함)를 PDF에 임베딩하기 때문이다. ### 5.2 폰트 서브셋팅 (필수) @@ -231,7 +241,7 @@ storage/fonts/ | 설정 | 변경 전 | 변경 후 | 효과 | |------|---------|---------|------| -| `enable_font_subsetting` | `false` | `true` | 폰트 전체(~4-5MB) → 사용 글자만(수십KB) | +| `enable_font_subsetting` | `false` | `true` | 폰트 전체(~2-5MB) → 사용 글자만(수십KB) | | `enable_javascript` | `true` | `false` | PDF 내 JS 코드 제거 | > 한글 폰트는 11,172개의 완성형 글자를 포함하지만, 급여명세서에 사용되는 글자는 100~200자 수준이다. 서브셋팅으로 99% 이상의 불필요한 글리프를 제거한다. @@ -249,7 +259,7 @@ storage/fonts/ 구글 폰트를 사용하는 기존 PDF 뷰를 발견하면 다음 절차로 수정한다. -### 5.1 Blade 뷰 수정 +### 6.1 Blade 뷰 수정 ```html @@ -260,11 +270,11 @@ body { font-family: 'Noto Sans KR', sans-serif; } ``` -### 5.2 서비스/컨트롤러 수정 +### 6.2 서비스/컨트롤러 수정 ```php // ❌ 수정 전 @@ -275,14 +285,19 @@ $pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData]) // ✅ 수정 후 $this->ensureKoreanFont(); $pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData]) - ->setPaper('a4'); + ->setPaper('a4') + ->setOptions([ + 'font_dir' => storage_path('fonts'), + 'font_cache' => storage_path('fonts'), + 'enable_font_subsetting' => true, + ]); ``` -### 5.3 적용 사례 +### 6.3 적용 사례 | 파일 | 수정 내용 | |------|----------| -| `resources/views/emails/payslip.blade.php` | `@import` 구글 폰트 삭제, `font-family` NanumGothic 적용 | +| `resources/views/emails/payslip.blade.php` | `@import` 구글 폰트 삭제, `font-family` Pretendard 적용 | | `app/Services/HR/PayrollService.php` | `isRemoteEnabled` 제거, `ensureKoreanFont()` 추가 | --- @@ -308,7 +323,7 @@ sudo chmod -R 775 /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/ - [ ] `@import url('https://fonts.googleapis.com/...')` 미포함 - [ ] `` 미포함 -- [ ] `font-family`에 `NanumGothic` 포함 (DomPDF 한글 지원) +- [ ] `font-family`에 `Pretendard` 포함 (DomPDF 한글 지원) - [ ] `font-family` fallback에 `Malgun Gothic`, `sans-serif` 포함 (브라우저용) - [ ] `font-weight`는 `normal`/`bold`만 사용 (800 이상 금지) @@ -316,7 +331,7 @@ sudo chmod -R 775 /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/ - [ ] `isRemoteEnabled` 옵션 미사용 - [ ] `ensureKoreanFont()` 호출 후 PDF 생성 -- [ ] `Pdf::loadView()->setPaper()` 패턴 사용 +- [ ] `Pdf::loadView()->setPaper()->setOptions()` 패턴 사용 ### 코드 리뷰 시 @@ -326,10 +341,10 @@ sudo chmod -R 775 /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/ ### 폰트 배포 확인 -- [ ] `resources/fonts/NanumGothic.ttf` 존재 확인 (Git 관리) -- [ ] `resources/fonts/NanumGothicBold.ttf` 존재 확인 (Git 관리) +- [ ] `resources/fonts/Pretendard-Regular.ttf` 존재 확인 (Git 관리) +- [ ] `resources/fonts/Pretendard-Bold.ttf` 존재 확인 (Git 관리) - [ ] `storage/fonts/` 디렉토리 쓰기 권한 확인 -- [ ] 시스템 폰트(`fonts-nanum`)에 의존하지 않음 확인 +- [ ] 시스템 폰트에 의존하지 않음 확인 --- @@ -338,7 +353,7 @@ sudo chmod -R 775 /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/ - 서버 운영 매뉴얼: `dev/deploys/ops-manual/README.md` - DomPDF 패키지: `barryvdh/laravel-dompdf` v3.1 - DomPDF 설정: `mng/config/dompdf.php` — 서브셋팅, DPI 등 -- 폰트 원본: `mng/resources/fonts/` — NanumGothic TTF (Git 관리) +- 폰트 원본: `mng/resources/fonts/` — Pretendard TTF (Git 관리) - 구현 참조: `mng/app/Services/HR/PayrollService.php` — `ensureKoreanFont()` ---