docs: [standards] PDF 폰트 정책 업데이트

- NanumGothic 표준 폰트 + ensureKoreanFont 자동 등록 패턴 반영
- DomPDF 미등록 폰트(Malgun Gothic 등) 단독 사용 금지 추가
- storage/fonts/ 캐시 구조 및 서버 환경 체크리스트 추가
This commit is contained in:
김보곤
2026-03-11 09:56:13 +09:00
parent 909d3e11b8
commit d1d6a56702

View File

@@ -9,7 +9,7 @@
### 1.1 목적
DomPDF로 PDF를 생성할 때 폰트 관련 문제(권한 오류, 외부 의존성, 배포 시 재발)를 방지하기 위한 정책이다.
DomPDF로 PDF를 생성할 때 폰트 관련 문제(권한 오류, 외부 의존성, 배포 시 재발, 한글 깨짐)를 방지하기 위한 정책이다.
### 1.2 배경 — 운영서버 장애 사례
@@ -25,8 +25,10 @@ Failed to open stream: Permission denied
```
❌ PDF 뷰에서 구글 폰트(@import, <link>) 사용 금지
❌ DomPDF의 isRemoteEnabled 옵션 사용 금지
시스템 기본 폰트만 사용
폰트가 필요하면 로컬 설치 후 DomPDF에 등록
❌ font-family에 'Malgun Gothic' 등 시스템 전용 폰트 단독 사용 금지 (DomPDF가 인식 못함)
NanumGothic을 DomPDF에 등록하여 사용
✅ 폰트 등록은 storage/fonts/에 캐시 (vendor/ 아님)
✅ 자동 등록 패턴(ensureKoreanFont) 적용
```
---
@@ -38,7 +40,7 @@ Failed to open stream: Permission denied
```
❌ @import url('https://fonts.googleapis.com/...');
❌ <link href="https://fonts.googleapis.com/..." rel="stylesheet">
❌ font-face src: url('https://...');
@font-face src: url('https://...');
```
### 2.2 isRemoteEnabled 옵션 금지
@@ -47,10 +49,15 @@ Failed to open stream: Permission denied
// ❌ 금지 — 외부 리소스 다운로드를 활성화하면 안 됨
$pdf = Pdf::loadView('view', $data)
->setOptions(['isRemoteEnabled' => true]);
```
// ✅ 올바른 사용
$pdf = Pdf::loadView('view', $data)
->setPaper('a4');
### 2.3 DomPDF 미등록 폰트 사용 금지
DomPDF는 시스템 폰트를 자동으로 인식하지 않는다. `Malgun Gothic`, `Apple SD Gothic Neo` 등을 `font-family`에 지정해도 DomPDF가 찾지 못해 한글이 `???`로 깨진다.
```css
/* ❌ DomPDF에서 한글 깨짐 */
body { font-family: 'Malgun Gothic', sans-serif; }
```
---
@@ -59,7 +66,7 @@ $pdf = Pdf::loadView('view', $data)
### 3.1 배포 시 권한 문제 재발
`composer install``vendor/`를 새로 생성하면 폰트 캐시 디렉토리 권한이 초기화된다. Jenkinsfile에 매번 권한 설정을 추가해야 하는데, `vendor/` 내부 특정 경로에 권한을 거는 것은 안티패턴이다.
`composer install``vendor/`를 새로 생성하면 폰트 캐시 디렉토리 권한이 초기화된다. `vendor/` 내부 특정 경로에 권한을 거는 것은 안티패턴이다.
```
배포 → composer install → vendor/ 재생성 → 폰트 캐시 권한 초기화 → PDF 생성 실패
@@ -71,45 +78,114 @@ PDF를 생성할 때마다 `fonts.googleapis.com`에 요청한다. 외부 서버
### 3.3 PDF에서 웹폰트 불필요
DomPDF는 웹 브라우저가 아니다. 구글 폰트를 다운로드 → 파싱 → 캐싱하는 과정이 불필요한 오버헤드다. 시스템 기본 폰트로 충분하다.
DomPDF는 웹 브라우저가 아니다. 구글 폰트를 다운로드 → 파싱 → 캐싱하는 과정이 불필요한 오버헤드다.
---
## 4. 올바른 폰트 사용법
### 4.1 시스템 기본 폰트 사용 (권장)
### 4.1 표준 한글 폰트: NanumGothic
SAM 프로젝트의 PDF 한글 폰트는 **NanumGothic**을 사용한다. 로컬(Docker)과 서버(Bare-metal) 모두 `fonts-nanum` 패키지가 설치되어 있다.
| 환경 | 폰트 경로 | 설치 방법 |
|------|----------|----------|
| 로컬 (Docker) | `/usr/share/fonts/truetype/nanum/` | Dockerfile에 포함 |
| 개발/운영 서버 | `/usr/share/fonts/truetype/nanum/` | `sudo apt install fonts-nanum` |
### 4.2 Blade 뷰 font-family 지정
```css
/* PDF 뷰(Blade)에서 사용할 font-family */
/* ✅ 올바른 PDF 뷰 폰트 지정 */
body {
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
font-family: 'NanumGothic', 'Malgun Gothic', sans-serif;
}
```
### 4.2 PDF 생성 코드 패턴
> `NanumGothic`이 DomPDF에서 사용되고, `Malgun Gothic`은 브라우저에서 HTML을 직접 볼 때의 fallback이다.
### 4.3 자동 폰트 등록 패턴 (ensureKoreanFont)
PDF를 생성하는 서비스에 다음 패턴을 적용한다. 최초 1회만 등록하고 이후는 캐시에서 로드한다.
```php
/**
* DomPDF에 한글(NanumGothic) 폰트 등록 (최초 1회만 실행)
*/
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'])) {
return;
}
}
// 시스템에 설치된 NanumGothic 찾기
$fontPaths = [
'normal' => '/usr/share/fonts/truetype/nanum/NanumGothic.ttf',
'bold' => '/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf',
];
if (! file_exists($fontPaths['normal'])) {
return;
}
if (! is_dir($fontDir)) {
mkdir($fontDir, 0755, true);
}
// storage/fonts/에 복사 후 DomPDF에 등록
foreach ($fontPaths as $weight => $src) {
$dst = $fontDir.'/'.basename($src);
if (! file_exists($dst)) {
copy($src, $dst);
}
}
$dompdf = app(\Barryvdh\DomPDF\PDF::class)->getDomPDF();
$fm = $dompdf->getFontMetrics();
$fm->registerFont(
['family' => 'nanumgothic', 'style' => 'normal', 'weight' => 'normal'],
$fontDir.'/NanumGothic.ttf'
);
$fm->registerFont(
['family' => 'nanumgothic', 'style' => 'normal', 'weight' => 'bold'],
$fontDir.'/NanumGothicBold.ttf'
);
$fm->saveFontFamilies();
}
```
### 4.4 PDF 생성 코드 패턴
```php
// ✅ 올바른 PDF 생성 패턴
$pdf = Pdf::loadView('emails.payslip', ['data' => $data])
$this->ensureKoreanFont();
$pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData])
->setPaper('a4');
$pdfContent = $pdf->output();
```
### 4.3 한글 폰트가 필요한 경우 — 로컬 설치
### 4.5 폰트 캐시 구조
시스템 기본 폰트로 부족하면 서버에 폰트를 직접 설치하고 DomPDF에 등록한다.
```bash
# 1. 서버에 폰트 설치 (Level 2 — 사용자 확인 후 실행)
sudo apt install fonts-noto-cjk
# 2. DomPDF 폰트 등록 (php artisan 명령)
php artisan dompdf:font "Noto Sans KR" \
/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc
```
storage/fonts/
├── installed-fonts.json ← DomPDF 폰트 레지스트리
├── NanumGothic.ttf ← 시스템에서 복사된 원본
├── NanumGothicBold.ttf
├── nanumgothic_normal_*.ufm ← DomPDF가 생성한 메트릭 캐시
├── nanumgothic_normal_*.ttf ← DomPDF가 생성한 서브셋
└── nanumgothic_bold_*.*
```
> 로컬 설치된 폰트는 `vendor/` 재생성과 무관하게 유지된다.
> `storage/fonts/`는 `.gitignore`에 포함되어 있다. 각 환경에서 첫 PDF 생성 시 자동으로 생성된다.
---
@@ -117,23 +193,22 @@ php artisan dompdf:font "Noto Sans KR" \
구글 폰트를 사용하는 기존 PDF 뷰를 발견하면 다음 절차로 수정한다.
### 5.1 Blade 뷰에서 제거
### 5.1 Blade 뷰 수정
```html
<!-- ❌ 삭제 대상 -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700;800&display=swap');
body { font-family: 'Noto Sans KR', sans-serif; }
</style>
<!-- ✅ 수정 후 -->
<style>
body {
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
body { font-family: 'NanumGothic', 'Malgun Gothic', sans-serif; }
</style>
```
### 5.2 서비스/컨트롤러에서 제거
### 5.2 서비스/컨트롤러 수정
```php
// ❌ 수정 전
@@ -142,15 +217,23 @@ $pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData])
->setPaper('a4');
// ✅ 수정 후
$this->ensureKoreanFont();
$pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData])
->setPaper('a4');
```
### 5.3 적용 사례
| 파일 | 수정 내용 |
|------|----------|
| `resources/views/emails/payslip.blade.php` | `@import` 구글 폰트 삭제, `font-family` NanumGothic 적용 |
| `app/Services/HR/PayrollService.php` | `isRemoteEnabled` 제거, `ensureKoreanFont()` 추가 |
---
## 6. 긴급 복구 — 운영서버 권한 오류 발생 시
이미 구글 폰트를 사용하는 코드가 배포되어 권한 오류가 발생한 경우의 즉시 조치이다. 근본 수정(구글 폰트 제거)을 반드시 병행한다.
이미 구글 폰트를 사용하는 코드가 배포되어 권한 오류가 발생한 경우의 즉시 조치이다. 근본 수정(구글 폰트 제거 + ensureKoreanFont 적용)을 반드시 병행한다.
```bash
# Level 2 작업 — 사용자 확인 후 실행
@@ -159,7 +242,7 @@ sudo chown -R www-data:webservice /home/webservice/mng/current/vendor/dompdf/dom
sudo chmod -R 775 /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/
```
> 이 조치는 임시이다. 재배포 시 `vendor/`가 새로 생성되면 다시 발생한다.
> 이 조치는 임시이다. 재배포 시 `vendor/`가 새로 생성되면 다시 발생한다. 반드시 `ensureKoreanFont` 패턴을 적용해야 근본 해결된다.
---
@@ -169,25 +252,34 @@ sudo chmod -R 775 /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/
- [ ] `@import url('https://fonts.googleapis.com/...')` 미포함
- [ ] `<link href="https://fonts.googleapis.com/...">` 미포함
- [ ] `font-family`구글 폰트명 미포함
- [ ] 시스템 기본 폰트 사용 (`Malgun Gothic`, `sans-serif`)
- [ ] `font-family``NanumGothic` 포함 (DomPDF 한글 지원)
- [ ] `font-family` fallback에 `Malgun Gothic`, `sans-serif` 포함 (브라우저용)
### PDF 생성 코드 작성 시
- [ ] `isRemoteEnabled` 옵션 미사용
- [ ] `ensureKoreanFont()` 호출 후 PDF 생성
- [ ] `Pdf::loadView()->setPaper()` 패턴 사용
### 코드 리뷰 시
- [ ] PDF 관련 Blade 뷰에 외부 폰트 URL 없음
- [ ] DomPDF 옵션에 `isRemoteEnabled` 없음
- [ ] 한글 PDF에 `ensureKoreanFont()` 호출 있음
### 서버 환경 확인
- [ ] `fonts-nanum` 패키지 설치 확인 (`fc-list :lang=ko | grep Nanum`)
- [ ] `storage/fonts/` 디렉토리 쓰기 권한 확인
- [ ] 미설치 시: `sudo apt install fonts-nanum`
---
## 관련 문서
- 서버 운영 매뉴얼: `dev/deploys/ops-manual/README.md`
- DomPDF 패키지: `barryvdh/laravel-dompdf`
- DomPDF 패키지: `barryvdh/laravel-dompdf` v3.1
- 구현 참조: `mng/app/Services/HR/PayrollService.php``ensureKoreanFont()`
---