feat: translate all pages, UI messages and docs and fix SEO redirects and indexing

This commit is contained in:
alam00000 committed 2026-10-25 12:55:21 +05:30
1 parent 5e019de5ae
commit 4edb1718cc
3344 files changed
+415643 -4759

No files matched your search

+275
View File
@@ -0,0 +1,275 @@
# Apache로 배포하기
Apache HTTP Server로 BentoPDF를 호스팅하세요.
## 사전 요구 사항
- Apache 2.4+
- mod_rewrite 활성화
- SSL 인증서 (권장)
## 1단계: 프로젝트 빌드
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
브랜딩을 변경하려면 빌드하기 전에 환경 변수를 설정하세요:
```bash
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
```
## 2단계: 파일 복사
```bash
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
```
## 3단계: Apache 설정
`/etc/apache2/sites-available/bentopdf.conf`를 만드세요:
```apache
<VirtualHost *:80>
ServerName your-domain.com
Redirect permanent / https://your-domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName your-domain.com
DocumentRoot /var/www/bentopdf
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
<Directory /var/www/bentopdf>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# WASM MIME type
AddType application/wasm .wasm
# Prevent double-compression of pre-compressed files
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json application/wasm
</IfModule>
# Cache headers
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>
# Required headers for SharedArrayBuffer (LibreOffice WASM)
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "cross-origin"
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
# Pre-compressed LibreOffice WASM files
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
</VirtualHost>
```
## 4단계: 라우팅용 .htaccess
`/var/www/bentopdf/.htaccess`를 만드세요:
```apache
RewriteEngine On
RewriteBase /
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# LANGUAGE ROUTES
# ============================================
# Supported languages: de, es, zh, zh-TW, vi, it, id, tr, fr, pt
# English has no prefix - served from root
# English prefix redirects to root
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# ADD .HTML EXTENSION (ROOT LEVEL)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /404.html
```
## 5단계: 필요한 모듈 활성화
```bash
sudo a2enmod rewrite
sudo a2enmod ssl
sudo a2enmod headers
sudo a2enmod expires
sudo a2enmod deflate
```
## 6단계: 사이트 활성화
```bash
sudo a2ensite bentopdf.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
```
## 하위 디렉터리 배포
`/pdf/`에 호스팅하려면:
1. 기본 URL을 지정하여 빌드하세요:
```bash
BASE_URL=/pdf/ npm run build
```
2. `.htaccess`를 업데이트하세요:
```apache
RewriteEngine On
RewriteBase /pdf/
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Language routes
RewriteRule ^en/?$ /pdf/ [R=301,L]
RewriteRule ^en/(.+)$ /pdf/$1 [R=301,L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /pdf/$1/index.html [L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /pdf/$1/$2.html [L]
# Root level .html extension
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /pdf/404.html
```
## 문제 해결
### WASM 404 오류
MIME 타입이 설정되어 있는지 확인하세요:
```apache
AddType application/wasm .wasm
```
### PDF 서명 또는 Form Filler에 빈 뷰어가 표시됨 (`.mjs` MIME 오류)
브라우저 콘솔에 다음과 같은 메시지가 표시되는 경우:
```
Failed to load module script: The server responded with a non-JavaScript
MIME type of "application/octet-stream". Strict MIME type checking is
enforced for module scripts per HTML spec.
```
…그리고 실패한 요청이 `.mjs`로 끝난다면, Apache 설정에 `.mjs → application/javascript` 매핑이 없는 것입니다. 번들된 PDF 뷰어는 `.mjs` 확장자의 ES 모듈 파일을 포함하며, Apache 기본 `mime.types`에는 이 확장자가 들어 있지 않습니다.
VirtualHost 또는 `.htaccess`에 다음을 추가하세요:
```apache
AddType application/javascript .mjs
```
그런 다음 Apache를 다시 로드하세요:
```bash
sudo apache2ctl configtest && sudo systemctl reload apache2
```
확인하려면 브라우저 DevTools의 Network 탭에서 실패한 `.mjs` 요청을 찾아 `Content-Type` 응답 헤더를 확인하세요. 이제 `application/octet-stream`이 아니라 `application/javascript`여야 합니다.
### Rewrite가 작동하지 않음
mod_rewrite가 활성화되어 있는지 확인하세요:
```bash
sudo a2enmod rewrite
```
### Word/ODT/Excel을 PDF로 변환이 작동하지 않음
LibreOffice WASM에는 `SharedArrayBuffer`가 필요하며, 이를 위해 다음 헤더가 필요합니다:
```apache
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
```
보안 컨텍스트도 필요합니다. `http://localhost`는 로컬 테스트에서 작동하지만, `http://192.168.x.x` 같은 LAN IP는 대체로 HTTPS가 필요합니다. 헤더가 있는데도 `window.crossOriginIsolated`가 계속 `false`라면, 페이지가 루프백이 아닌 origin에서 일반 HTTP로 열려 있는지 확인하세요.
미리 압축된 `.wasm.gz` 및 `.data.gz` 파일에도 올바른 `Content-Encoding`이 필요합니다:
```apache
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
```
`mod_headers`가 활성화되어 있는지 확인하세요: `sudo a2enmod headers`
### 권한 거부
```bash
sudo chown -R www-data:www-data /var/www/bentopdf
sudo chmod -R 755 /var/www/bentopdf
```
+200
View File
@@ -0,0 +1,200 @@
# AWS S3 + CloudFront에 배포하기
최대한의 제어와 확장성을 위해 AWS에서 BentoPDF를 호스팅하세요.
## 아키텍처
```
User → CloudFront (CDN) → S3 (Static Files)
```
## 1단계: S3 버킷 만들기
```bash
# Create bucket
aws s3 mb s3://your-bentopdf-bucket --region us-east-1
# Enable static website hosting
aws s3 website s3://your-bentopdf-bucket \
--index-document index.html \
--error-document index.html
```
## 2단계: 빌드 및 업로드
```bash
# Build the project (optionally with custom branding)
# VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
npm run build
# Sync to S3
aws s3 sync dist/ s3://your-bentopdf-bucket \
--delete \
--cache-control "max-age=31536000"
# Set correct MIME types for WASM
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.wasm" \
--content-type "application/wasm" \
--metadata-directive REPLACE
# Set correct MIME type for ES module files (.mjs)
# S3 defaults .mjs to application/octet-stream, which browsers refuse to
# execute as ES modules. The bundled PDF viewer (Sign PDF, Form Filler)
# ships ES modules with .mjs extensions and will fail without this step.
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.mjs" \
--content-type "application/javascript" \
--metadata-directive REPLACE
```
## 3단계: CloudFront 배포 만들기
```bash
aws cloudfront create-distribution \
--origin-domain-name your-bentopdf-bucket.s3.amazonaws.com \
--default-root-object index.html
```
또는 AWS 콘솔을 사용하세요:
1. CloudFront → Create distribution으로 이동합니다
2. Origin domain: S3 버킷을 선택합니다
3. "Origin Access Control"을 활성화합니다
4. Default root object: `index.html`
5. 배포를 만듭니다
## 3b단계: 응답 헤더 정책 (LibreOffice WASM에 필수)
LibreOffice 기반 변환(Word, Excel, PowerPoint를 PDF로)에는 `SharedArrayBuffer`가 필요하며, 이를 위해 특정 응답 헤더가 필요합니다. CloudFront 응답 헤더 정책을 만드세요:
1. CloudFront → Policies → Response headers로 이동합니다
2. 다음 헤더로 사용자 지정 정책을 만듭니다:
| 헤더 | 값 |
| ------------------------------ | -------------- |
| `Cross-Origin-Embedder-Policy` | `require-corp` |
| `Cross-Origin-Opener-Policy` | `same-origin` |
| `Cross-Origin-Resource-Policy` | `cross-origin` |
3. 정책을 배포의 기본 캐시 동작에 연결합니다
또는 CLI로:
```bash
aws cloudfront create-response-headers-policy \
--response-headers-policy-config '{
"Name": "BentoPDF-COEP-COOP",
"CustomHeadersConfig": {
"Quantity": 3,
"Items": [
{"Header": "Cross-Origin-Embedder-Policy", "Value": "require-corp", "Override": true},
{"Header": "Cross-Origin-Opener-Policy", "Value": "same-origin", "Override": true},
{"Header": "Cross-Origin-Resource-Policy", "Value": "cross-origin", "Override": true}
]
}
}'
```
## 3c단계: 미리 압축된 WASM 파일의 S3 메타데이터
LibreOffice WASM 파일은 미리 압축되어 있습니다 (`.wasm.gz`, `.data.gz`). 브라우저가 압축을 풀 수 있도록 올바른 Content-Type과 Content-Encoding을 설정하세요:
```bash
# Set correct headers for soffice.wasm.gz
aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \
s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \
--content-type "application/wasm" \
--content-encoding "gzip" \
--metadata-directive REPLACE
# Set correct headers for soffice.data.gz
aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \
s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \
--content-type "application/octet-stream" \
--content-encoding "gzip" \
--metadata-directive REPLACE
```
::: warning 중요
응답 헤더 정책이 없으면 `SharedArrayBuffer`를 사용할 수 없고 LibreOffice WASM 변환이 약 55%에서 멈춥니다. `.gz` 파일에 올바른 Content-Encoding이 없으면 브라우저가 원시 gzip 바이트를 받게 되어 WASM 컴파일이 실패합니다.
:::
## 4단계: S3 버킷 정책
CloudFront가 버킷에 접근할 수 있도록 허용합니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bentopdf-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}
```
## 5단계: 사용자 지정 오류 페이지
SPA 라우팅을 위해 404가 `index.html`을 반환하도록 설정하세요:
1. CloudFront → Error pages
2. 사용자 지정 오류 응답을 만듭니다:
- HTTP error code: 404
- Response page path: `/index.html`
- HTTP response code: 200
## 비용 예상
| 리소스 | 예상 비용 |
| ---------------------- | --------- |
| S3 스토리지 (~500MB) | ~$0.01/월 |
| CloudFront (1TB 전송) | ~$85/월 |
| CloudFront (10GB 전송) | ~$0.85/월 |
::: tip
자주 접근하지 않는 파일의 비용을 최적화하려면 S3 Intelligent Tiering을 사용하세요.
:::
## Terraform으로 자동화
```hcl
# main.tf
resource "aws_s3_bucket" "bentopdf" {
bucket = "your-bentopdf-bucket"
}
resource "aws_cloudfront_distribution" "bentopdf" {
origin {
domain_name = aws_s3_bucket.bentopdf.bucket_regional_domain_name
origin_id = "S3Origin"
}
enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
}
}
```
+154
View File
@@ -0,0 +1,154 @@
# Cloudflare Pages에 배포하기
[Cloudflare Pages](https://pages.cloudflare.com)는 대역폭 제한 없이 빠른 글로벌 정적 사이트 호스팅을 제공합니다.
## 빠른 배포
1. [Cloudflare Pages](https://dash.cloudflare.com/?to=/:account/pages)로 이동합니다
2. "Create a project"를 클릭합니다
3. GitHub 저장소를 연결합니다
## 빌드 설정
| 설정 | 값 |
| ------------------ | --------------- |
| 프레임워크 프리셋 | None |
| 빌드 명령어 | `npm run build` |
| 빌드 출력 디렉터리 | `dist` |
| 루트 디렉터리 | `/` |
## 환경 변수
Settings → Environment variables에서 다음을 추가하세요:
| 변수 | 값 |
| ----------------------- | ------------------------------------- |
| `NODE_VERSION` | `18` |
| `SIMPLE_MODE` | `false` (선택) |
| `VITE_BRAND_NAME` | 사용자 지정 브랜드 이름 (선택) |
| `VITE_BRAND_LOGO` | `public/` 기준 로고 경로 (선택) |
| `VITE_FOOTER_TEXT` | 사용자 지정 푸터/저작권 텍스트 (선택) |
| `VITE_DEFAULT_LANGUAGE` | 기본 UI 언어, 예: `fr` (선택) |
## 설정 파일
`public` 폴더에 `_headers`를 만드세요:
```
# Required security headers for SharedArrayBuffer (used by LibreOffice WASM)
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: cross-origin
# Pre-compressed LibreOffice WASM binary
/libreoffice-wasm/soffice.wasm.gz
Content-Type: application/wasm
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
# Pre-compressed LibreOffice WASM data
/libreoffice-wasm/soffice.data.gz
Content-Type: application/octet-stream
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
# Cache WASM files aggressively
/*.wasm
Cache-Control: public, max-age=31536000, immutable
Content-Type: application/wasm
# Service worker
/sw.js
Cache-Control: no-cache
```
::: warning 중요
Word/ODT/Excel/PowerPoint를 PDF로 변환하려면 `Cross-Origin-Embedder-Policy` 및 `Cross-Origin-Opener-Policy` 헤더가 필요합니다. 이 헤더가 없으면 `SharedArrayBuffer`를 사용할 수 없고 LibreOffice WASM 엔진이 초기화되지 못합니다.
:::
SPA 라우팅을 위해 `_redirects`를 만드세요:
```
/* /index.html 200
```
## 사용자 지정 도메인
1. Pages 프로젝트로 이동합니다
2. "Custom domains"를 클릭합니다
3. 도메인을 추가합니다
4. 도메인이 Cloudflare에 있으면 Cloudflare가 DNS를 자동으로 설정합니다
## 장점
- **무료 무제한 대역폭**
- 300개 이상의 엣지 로케이션을 갖춘 **글로벌 CDN**
- **자동 HTTPS**
- 풀 리퀘스트용 **미리보기 배포**
- **빠른 빌드**
## 문제 해결
### 대용량 파일 업로드
Cloudflare Pages는 최대 25MB 파일을 지원합니다. WASM 모듈은 문제없겠지만 제한에 걸린다면 다음을 고려하세요:
```bash
# Split large files during build
npm run build
```
### Worker 크기 제한
고급 라우팅에 Cloudflare Workers를 사용하는 경우, 무료 플랜에는 1MB 제한이 있다는 점에 유의하세요.
## CORS 프록시 Worker (디지털 서명용)
디지털 서명 도구는 인증서 체인을 가져오기 위해 CORS 프록시가 필요합니다. 포함된 worker를 배포하세요:
```bash
cd cloudflare
npx wrangler login
npx wrangler deploy
```
### 보안 기능
| 기능 | 설명 |
| ------------------ | ------------------------ |
| **URL 제한** | 인증서 URL만 허용 |
| **파일 크기 제한** | 요청당 최대 10MB |
| **속도 제한** | IP당 분당 60회 (KV 필요) |
| **사설 IP 차단** | localhost, 내부 IP 차단 |
### 속도 제한 활성화
```bash
# Create KV namespace
npx wrangler kv namespace create "RATE_LIMIT_KV"
# Add to wrangler.toml with returned ID:
# [[kv_namespaces]]
# binding = "RATE_LIMIT_KV"
# id = "YOUR_ID"
npx wrangler deploy
```
### 프록시 URL로 빌드
```bash
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
```
또는 Docker로:
```bash
export VITE_CORS_PROXY_URL="https://your-worker.workers.dev"
DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
-t your-bentopdf .
```
> **참고:** HMAC 서명 설정은 [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required)를 참고하세요.
+180
View File
@@ -0,0 +1,180 @@
# 인증서 가져오기용 CORS 프록시
디지털 서명 도구는 CORS 프록시를 사용해 외부 인증 기관(CA)에서 발급자 인증서를 가져옵니다. 많은 CA 서버가 응답에 CORS 헤더를 포함하지 않아 브라우저에서 직접 가져올 수 없기 때문입니다.
또한 많은 CA 서버가 일반 HTTP로 인증서를 제공합니다. BentoPDF 인스턴스가 HTTPS로 호스팅되면 브라우저가 이러한 HTTP 요청을 차단합니다(혼합 콘텐츠 정책). CORS 프록시는 적절한 헤더를 붙인 HTTPS 엔드포인트를 거쳐 요청을 전달하여 두 문제를 모두 해결합니다.
## 작동 방식
인증서로 PDF에 서명할 때:
1. `zgapdfsigner` 라이브러리가 전체 인증서 체인을 구성하려고 시도합니다
2. 인증서의 AIA(Authority Information Access) 확장에 포함된 URL에서 발급자 인증서를 가져옵니다
3. 이 요청은 필요한 `Access-Control-Allow-Origin` 헤더를 추가하는 CORS 프록시를 거칩니다
4. 프록시가 인증서 데이터를 브라우저에 반환합니다
## CORS 프록시 직접 호스팅하기
BentoPDF를 직접 호스팅한다면, 체인 가져오기가 필요한 인증서로 디지털 서명이 작동하도록 자체 CORS 프록시를 배포해야 합니다.
### 옵션 1: Cloudflare Workers (권장)
1. **Wrangler CLI 설치**:
```bash
npm install -g wrangler
```
2. **Cloudflare에 로그인**:
```bash
wrangler login
```
3. **BentoPDF를 클론하고 허용 origin 업데이트**:
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf/cloudflare
```
`cors-proxy-worker.js`를 열고 `ALLOWED_ORIGINS` 배열을 사용자의 도메인으로 변경하세요:
```js
const ALLOWED_ORIGINS = [
'https://your-domain.com',
'https://www.your-domain.com',
];
```
::: warning 중요
이 변경이 없으면 프록시가 사이트의 모든 요청을 **403 Forbidden** 오류로 거부합니다. 기본값은 `bentopdf.com`의 요청만 허용합니다.
:::
4. **프록시 배포**:
```bash
wrangler deploy
```
worker URL을 메모해 두세요 (예: `https://bentopdf-cors-proxy.your-subdomain.workers.dev`).
5. **프록시 URL로 BentoPDF 다시 빌드**:
Docker를 사용하는 경우:
```bash
export VITE_CORS_PROXY_URL="https://your-worker.workers.dev"
DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
-t your-bentopdf .
```
소스에서 빌드하는 경우:
```bash
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
```
### 옵션 2: 사용자 지정 백엔드 프록시
자체 프록시 엔드포인트를 만들 수도 있습니다. 요구 사항은 다음과 같습니다:
1. `url` 쿼리 매개변수가 있는 GET 요청을 받습니다
2. 서버에서 해당 URL을 가져옵니다 (서버 측에는 CORS 제한이 없습니다)
3. 다음 헤더와 함께 응답을 반환합니다:
- `Access-Control-Allow-Origin: https://your-domain.com`
- `Access-Control-Allow-Methods: GET, OPTIONS`
- `X-Content-Type-Options: nosniff`
Express.js 구현 예시:
```javascript
app.get('/api/cert-proxy', async (req, res) => {
const targetUrl = req.query.url;
// Validate it's a certificate URL
if (!isValidCertUrl(targetUrl)) {
return res.status(400).json({ error: 'Invalid URL' });
}
try {
const response = await fetch(targetUrl);
const data = await response.arrayBuffer();
res.set('Access-Control-Allow-Origin', 'https://your-domain.com');
res.set('Content-Type', 'application/octet-stream');
res.set('X-Content-Type-Options', 'nosniff');
res.send(Buffer.from(data));
} catch (error) {
res.status(500).json({ error: 'Proxy error' });
}
});
```
## 보안 고려 사항
포함된 Cloudflare Worker에는 여러 보안 장치가 있습니다:
| 기능 | 설명 |
| ----------------------- | --------------------------------------------------------------------------- |
| **Origin 검증** | `ALLOWED_ORIGINS`에 나열된 도메인의 요청만 허용 |
| **URL 제한** | 인증서 URL만 허용 (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`, `/crl`) |
| **사설 IP 차단** | IPv4/IPv6 사설 대역, 링크 로컬, 루프백, 십진수 IP, 클라우드 메타데이터 차단 |
| **Content-Type 안전성** | 안전한 인증서 MIME 타입만 반환하고, 업스트림 content-type 주입 차단 |
| **파일 크기 제한** | 응답을 10MB 제한으로 스트리밍하며, 초과하면 다운로드 도중 중단 |
| **속도 제한** | IP 또는 IPv6 /64당 분당 60회 (KV 필요, KV 오류 시 fail open) |
| **HMAC 서명** | 선택적 클라이언트 측 서명 (단순한 악용 억제) |
### DNS 리바인딩 (Cloudflare 외부에서 직접 호스팅하는 경우)
worker는 사설 또는 내부 IP로 확인되는 모든 URL을 거부합니다. 인증서 체인 URL은 인증서를 발급한 CA(FNMT, 기업 CA 등)가 가리키는 곳 어디든 될 수 있어 미리 정해진 호스트 허용 목록을 쓸 수 없고, 목적지를 사전에 알 수 없습니다. 그래서 작은 리바인딩 틈이 남습니다. 자기 도메인의 DNS를 운영하는 누군가가 안전 검사에는 공개 IP를, 잠시 후의 실제 요청에는 사설 IP를 돌려줄 수 있습니다.
Cloudflare에서는 악용할 수 없습니다. Workers는 사설 IP나 `169.254.169.254` 메타데이터 엔드포인트에 접근할 수 없으므로 리바인딩해도 아무것도 닿지 않습니다. 다만 worker에 진입 경로를 만들지 마세요. Tunnel, Service, 사설 네트워크 바인딩을 연결하지 마세요 (필요한 것은 `RATE_LIMIT_KV`뿐입니다).
VPC 안에서 Cloudflare 외부(workerd, Node 등)로 실행하면 런타임이 내부 IP에 _접근할 수 있습니다_. 앞단에 송신 방화벽을 두고 사설 대역(`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16` 및 해당 IPv6 대역)을 차단하세요. Cloudflare를 벗어나면 앱 코드로는 이를 강제할 수 없습니다.
## 프록시 비활성화
CORS 프록시를 사용하고 싶지 않다면 환경 변수를 빈 문자열로 설정하세요:
```
VITE_CORS_PROXY_URL=
```
**참고**: 프록시가 없으면 외부 체인 가져오기가 필요한 인증서(FNMT 또는 일부 기업 CA 등)로 서명할 때 "Failed to fetch" 오류가 발생합니다.
## 문제 해결
### "Signing error: TypeError: Failed to fetch"
보통 다음 중 하나를 의미합니다:
1. **CORS 프록시가 설정되지 않음**: `VITE_CORS_PROXY_URL`을 설정하고 다시 빌드하세요
2. **혼합 콘텐츠 차단됨**: 사이트는 HTTPS인데 인증서의 발급자 URL은 HTTP입니다. CORS 프록시가 이를 해결합니다.
3. **CORS 프록시가 origin을 거부함**: `cors-proxy-worker.js`의 `ALLOWED_ORIGINS` 배열에 도메인이 있는지 확인하세요
### 프록시에서 "403 Forbidden"
도메인이 `ALLOWED_ORIGINS` 목록에 없습니다. `cors-proxy-worker.js`를 수정하세요:
```js
const ALLOWED_ORIGINS = ['https://your-domain.com'];
```
그런 다음 다시 배포하세요: `npx wrangler deploy`
### 프록시 테스트
```bash
curl -H "Origin: https://your-domain.com" \
"https://your-proxy.workers.dev?url=http://www.cert.fnmt.es/certs/ACUSU.crt"
```
### 프록시 없이 작동하는 인증서
일부 인증서는 P12/PFX 파일에 전체 체인이 포함되어 있어 외부 가져오기가 필요하지 않습니다:
- 자체 서명 인증서
- 중간 인증서를 번들하는 일부 상용 CA
- 전체 체인을 직접 조립한 인증서
+622
View File
@@ -0,0 +1,622 @@
# Docker / Podman으로 배포하기
프로덕션 환경에서 BentoPDF를 직접 호스팅하는 가장 쉬운 방법입니다.
> [!IMPORTANT]
> **Office 파일 변환에 필요한 헤더**
>
> LibreOffice 기반 도구(Word, Excel, PowerPoint 변환)는 `SharedArrayBuffer` 지원을 위해 다음 HTTP 헤더가 필요합니다:
>
> - `Cross-Origin-Opener-Policy: same-origin`
> - `Cross-Origin-Embedder-Policy: require-corp`
>
> 페이지는 보안 컨텍스트에서 제공되어야 합니다. `http://localhost`는 로컬 테스트에서 작동하지만, `http://192.168.x.x` 같은 LAN IP는 대체로 해당되지 않으므로 헤더가 있어도 일반 HTTP로는 Office 변환이 실패합니다.
>
> 공식 컨테이너 이미지에는 이 헤더가 포함되어 있습니다. 리버스 프록시(Traefik, Caddy 등)를 사용한다면 이 헤더가 유지되거나 추가되도록 하고, 루프백이 아닌 접근에는 HTTPS를 사용하세요.
> [!TIP]
> **Podman 사용자:** 모든 `docker` 명령은 `docker`를 `podman`으로, `docker-compose`를 `podman-compose`로 바꾸면 Podman에서도 작동합니다.
## 빠른 시작
> [!TIP]
> BentoPDF는 두 가지 빌드로 제공됩니다:
>
> - **셀프 호스팅 빌드**: `ghcr.io/alam00000/bentopdf-simple:latest`. 마케팅 요소(히어로, FAQ, 후기, 푸터)를 **제외한** 모든 PDF 도구를 제공합니다. 내부/팀/조직 배포에 사용하세요. 기능이 줄어든 "라이트" 버전이 **아닙니다**.
> - **상용 빌드**: `ghcr.io/alam00000/bentopdf:latest`. 전체 마케팅 사이트이며, bentopdf.com 자체와 공개 서비스를 운영하는 상용 라이선스 보유자가 사용합니다.
>
> 잘 모르겠다면 셀프 호스팅 빌드를 받으세요.
```bash
# Docker
docker run -d \
--name bentopdf \
-p 3000:8080 \
--restart unless-stopped \
ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman run -d \
--name bentopdf \
-p 3000:8080 \
ghcr.io/alam00000/bentopdf-simple:latest
```
## Docker Compose / Podman Compose
`docker-compose.yml`을 만드세요:
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
container_name: bentopdf
ports:
- '3000:8080'
restart: unless-stopped
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:8080']
interval: 30s
timeout: 10s
retries: 3
```
실행:
```bash
# Docker Compose
docker compose up -d
# Podman Compose
podman-compose up -d
```
## 직접 이미지 빌드하기
```dockerfile
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginxinc/nginx-unprivileged:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
```
빌드 및 실행:
```bash
docker build -t bentopdf:custom .
docker run -d -p 3000:8080 bentopdf:custom
```
## 환경 변수
| 변수 | 설명 | 기본값 |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `SIMPLE_MODE` | 셀프 호스팅 변형을 빌드합니다 (bentopdf.com 마케팅 요소인 히어로, FAQ, 후기, 푸터를 숨깁니다). 모든 PDF 도구는 유지됩니다. | `false` |
| `BASE_URL` | 하위 디렉터리에 배포 | `/` |
| `VITE_WASM_PYMUPDF_URL` | PyMuPDF WASM 모듈 URL | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` |
| `VITE_WASM_GS_URL` | Ghostscript WASM 모듈 URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` |
| `VITE_WASM_CPDF_URL` | CoherentPDF WASM 모듈 URL | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` |
| `VITE_TESSERACT_WORKER_URL` | OCR 워커 스크립트 URL | _(비어 있음, Tesseract.js 기본 CDN 사용)_ |
| `VITE_TESSERACT_CORE_URL` | OCR 코어 런타임 디렉터리 | _(비어 있음, Tesseract.js 기본 CDN 사용)_ |
| `VITE_TESSERACT_LANG_URL` | OCR traineddata 디렉터리 | _(비어 있음, Tesseract.js 기본 CDN 사용)_ |
| `VITE_TESSERACT_AVAILABLE_LANGUAGES` | UI에 표시할 OCR 언어 (쉼표로 구분) | _(비어 있음, 전체 목록 표시)_ |
| `VITE_OCR_FONT_BASE_URL` | OCR 텍스트 레이어 글꼴 디렉터리 | _(비어 있음, 원격 Noto 글꼴 URL 사용)_ |
| `VITE_DEFAULT_LANGUAGE` | 기본 UI 언어 | `en` |
| `VITE_BRAND_NAME` | 사용자 지정 브랜드 이름 | `BentoPDF` |
| `VITE_BRAND_LOGO` | `public/` 기준 로고 경로 | `images/favicon-no-bg.svg` |
| `VITE_FOOTER_TEXT` | 사용자 지정 푸터/저작권 텍스트 | `© 2026 BentoPDF. All rights reserved.` |
| `DISABLE_TOOLS` | 숨길 도구 ID (쉼표로 구분) | _(비어 있음, 모든 도구 활성화)_ |
| `DISABLE_GITHUB_STARS` | 페이지 로드 시 GitHub 스타 수 가져오기를 건너뛰고 CSP에서 `api.github.com`을 제거합니다. Simple Mode 빌드는 항상 건너뜁니다. | `false` |
| `SITE_URL` | canonical, hreflang, 사이트맵 링크에 기록되는 공개 URL입니다. 검색 엔진이 사용자의 배포를 독립된 사이트로 취급하길 원하지 않는다면 기본값으로 두세요. | `https://www.bentopdf.com` |
| `ALLOW_INDEXING` | 사용자 지정 `SITE_URL`을 사용하면 모든 페이지가 `noindex`로 표시되어 사용자의 사본이 검색 결과에서 bentopdf.com과 경쟁하지 않습니다. 검색 엔진이 색인하도록 하려면 `true`로 설정하세요. | `false` |
WASM 모듈 URL에는 CDN 기본값이 미리 설정되어 있어 모든 고급 기능이 별도 설정 없이 작동합니다. 폐쇄망이나 직접 호스팅하는 배포에서는 이 값을 덮어쓰세요.
### 검색 엔진
기본적으로 모든 페이지는 검색 엔진이 bentopdf.com을 원본으로 인식하도록 하므로 사용자의 배포가 중복으로 표시되지 않습니다. 비공개 또는 내부 인스턴스라면 아무것도 바꿀 필요가 없습니다.
- **인스턴스를 검색 결과에서 완전히 제외하기:** `-e ROBOTS_NOINDEX=true`로 컨테이너를 실행하세요. 재시작만 하면 되며 다시 빌드할 필요는 없습니다.
- **인스턴스를 검색 결과에서 독립된 사이트로 만들기:** `--build-arg SITE_URL=https://your-domain.example --build-arg ALLOW_INDEXING=true`로 빌드하세요. `ALLOW_INDEXING=true`가 없으면 사용자 지정 `SITE_URL`은 모든 페이지를 `noindex`로 표시합니다.
### Content-Security-Policy
nginx 이미지에는 적용(enforcing) 모드의 `Content-Security-Policy` 헤더가 포함되어 있습니다. CSP의 `script-src`, `connect-src`, `font-src` 지시어는 위의 `VITE_*` URL 변수로부터 **빌드 시점에 생성**됩니다. `--build-arg`로 전달하는 호스트는 자동으로 정책에 추가됩니다.
그 결과:
- 빌드 시 `VITE_CORS_PROXY_URL` 또는 `VITE_WASM_*_URL`을 덮어쓰면 CSP가 해당 origin을 자동으로 허용합니다. 별도 설정이 필요 없습니다.
- `cdn.jsdelivr.net`의 URL에 대해 `script-src`는 전체 CDN이 아니라 버전이 명시된 정확한 패키지 폴더만 허용합니다. 예: `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/`. jsDelivr는 모든 공개 npm 패키지와 GitHub 저장소를 제공하므로 호스트 전체를 허용하면 그 어떤 것도 사용자의 인스턴스에서 실행될 수 있기 때문입니다. 다른 호스트의 URL은 origin 단위로 허용됩니다.
- 앱 내 고급 설정 페이지에서 _런타임에_ 사용자 지정 WASM URL을 설정하면 해당 origin은 CSP에 **포함되지 않으며** 브라우저가 해당 가져오기를 차단합니다. `cdn.jsdelivr.net`의 같은 패키지라도 다른 버전이면 마찬가지인데, 빌드 시 설정한 버전만 허용되기 때문입니다. 런타임 설정은 실험용입니다. 영구적인 사용자 지정 URL은 해당 `VITE_*` 빌드 인수를 설정하세요.
- 세 가지 `VITE_WASM_*_URL` 값을 모두 덮어쓰는 폐쇄망 배포는 해당 `cdn.jsdelivr.net` 패키지 폴더도 CSP에서 제거됩니다 (각 기본값은 추가되는 것이 아니라 대체됩니다). `cdn.jsdelivr.net`의 Tesseract OCR 기본값은 `VITE_TESSERACT_*` 세 값도 설정하기 전까지 정책에 남습니다. 마찬가지로 `VITE_CORS_PROXY_URL`을 설정하면 공개 `bentopdf-cors-proxy.bentopdf.workers.dev` 기본값이 대체됩니다.
CSP의 `script-src`에는 `'unsafe-eval'`이 포함되어 있습니다. LibreOffice WASM 런타임(Word/Excel/PowerPoint 변환 도구에서 사용)과 wasm-vips 이미지 라이브러리(PDF를 TIFF로 도구에서 사용)가 `new Function()`으로 내부 디스패치 코드를 컴파일하기 때문입니다. 이를 제거하면 해당 도구가 작동하지 않습니다. `SIMPLE_MODE`(LibreOffice 없음)로 빌드하고 PDF를 TIFF로 도구가 필요 없다면, 생성된 `security-headers.conf`에서 `'unsafe-eval'`을 직접 제거하여 더 엄격한 정책을 적용할 수 있습니다.
OCR의 경우 `VITE_TESSERACT_*` 변수를 비워 두면 기본 온라인 에셋을 사용하고, 직접 호스팅하거나 오프라인 OCR을 쓰려면 세 가지를 함께 설정하세요. 워커, 코어 런타임, traineddata 디렉터리가 서로 일치해야 하므로 일부만 덮어쓰는 것은 거부됩니다. 완전히 오프라인으로 검색 가능한 PDF를 만들려면 `VITE_OCR_FONT_BASE_URL`도 설정하여 OCR 텍스트 레이어 글꼴을 공개 Noto 글꼴 URL 대신 내부 서버에서 불러오도록 하세요.
`VITE_DEFAULT_LANGUAGE`는 처음 방문하는 사용자의 UI 언어를 설정합니다. 지원되는 값: `en`, `ar`, `be`, `fr`, `de`, `es`, `zh`, `zh-TW`, `vi`, `tr`, `id`, `it`, `pt`, `nl`, `da`. 사용자는 언제든 언어를 바꿀 수 있으며, 이 값은 기본값만 바꿉니다.
예시:
```bash
# Build with French as the default language
docker build --build-arg VITE_DEFAULT_LANGUAGE=fr -t bentopdf .
docker run -d -p 3000:8080 bentopdf
```
### 사용자 지정 브랜딩
기본 BentoPDF 로고, 이름, 푸터 텍스트를 직접 만든 것으로 바꾸세요. 로고 파일을 `public/` 폴더에 두거나(또는 기존 이미지를 사용하고) 빌드 시 브랜딩 변수를 전달하세요:
```bash
docker build \
--build-arg VITE_BRAND_NAME="AcmePDF" \
--build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \
--build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
-t acmepdf .
```
브랜딩은 전체 모드와 Simple Mode 모두에서 작동하며 다른 모든 빌드 시점 옵션과 함께 사용할 수 있습니다.
### 특정 도구 비활성화
규정 준수나 보안 요구 사항에 따라 UI에서 도구를 숨길 수 있습니다. 비활성화된 도구는 홈페이지, 검색 결과, 키보드 단축키, 워크플로 빌더에서 제거됩니다. URL로 직접 접근하면 "도구를 사용할 수 없음" 페이지가 표시됩니다.
도구 ID는 `.html`을 뺀 페이지 URL입니다. 예를 들어 도구가 `bentopdf.com/edit-pdf.html`에 있다면 ID는 `edit-pdf`입니다.
#### 도구 ID 찾기
가장 쉬운 방법: BentoPDF에서 아무 도구나 열고 URL을 보세요. 경로의 마지막 부분(`.html` 제외)이 도구 ID입니다.
<details>
<summary>전체 도구 ID 목록</summary>
**편집 및 주석:** `edit-pdf`, `bookmark`, `table-of-contents`, `page-numbers`, `add-page-labels`, `bates-numbering`, `add-watermark`, `header-footer`, `invert-colors`, `scanner-effect`, `adjust-colors`, `background-color`, `text-color`, `sign-pdf`, `add-stamps`, `remove-annotations`, `crop-pdf`, `form-filler`, `form-creator`, `remove-blank-pages`
**PDF로 변환:** `image-to-pdf`, `jpg-to-pdf`, `png-to-pdf`, `webp-to-pdf`, `svg-to-pdf`, `bmp-to-pdf`, `heic-to-pdf`, `tiff-to-pdf`, `txt-to-pdf`, `markdown-to-pdf`, `json-to-pdf`, `csv-to-pdf`, `rtf-to-pdf`, `odt-to-pdf`, `word-to-pdf`, `excel-to-pdf`, `powerpoint-to-pdf`, `xps-to-pdf`, `mobi-to-pdf`, `epub-to-pdf`, `fb2-to-pdf`, `cbz-to-pdf`, `wpd-to-pdf`, `wps-to-pdf`, `xml-to-pdf`, `pages-to-pdf`, `odg-to-pdf`, `ods-to-pdf`, `odp-to-pdf`, `pub-to-pdf`, `vsd-to-pdf`, `psd-to-pdf`, `email-to-pdf`
**PDF에서 변환:** `pdf-to-jpg`, `pdf-to-png`, `pdf-to-webp`, `pdf-to-bmp`, `pdf-to-tiff`, `pdf-to-cbz`, `pdf-to-svg`, `pdf-to-csv`, `pdf-to-excel`, `pdf-to-greyscale`, `pdf-to-json`, `pdf-to-docx`, `extract-images`, `pdf-to-markdown`, `prepare-pdf-for-ai`, `pdf-to-text`
**정리 및 관리:** `ocr-pdf`, `merge-pdf`, `alternate-merge`, `organize-pdf`, `add-attachments`, `extract-attachments`, `edit-attachments`, `pdf-multi-tool`, `pdf-layers`, `extract-tables`, `split-pdf`, `divide-pages`, `extract-pages`, `delete-pages`, `add-blank-page`, `reverse-pages`, `rotate-pdf`, `rotate-custom`, `n-up-pdf`, `pdf-booklet`, `combine-single-page`, `view-metadata`, `edit-metadata`, `pdf-to-zip`, `compare-pdfs`, `posterize-pdf`, `page-dimensions`
**최적화 및 복구:** `compress-pdf`, `pdf-to-pdfa`, `fix-page-size`, `linearize-pdf`, `remove-restrictions`, `repair-pdf`, `rasterize-pdf`, `deskew-pdf`, `font-to-outline`
**보안:** `encrypt-pdf`, `sanitize-pdf`, `decrypt-pdf`, `flatten-pdf`, `remove-metadata`, `change-permissions`, `digital-sign-pdf`, `validate-signature-pdf`, `timestamp-pdf`
</details>
#### 옵션 1: 빌드 시점 (Docker 빌드 인수)
```bash
docker build \
--build-arg DISABLE_TOOLS="edit-pdf,sign-pdf,encrypt-pdf" \
-t bentopdf .
```
비활성화 목록이 JavaScript 번들에 포함됩니다. 변경하려면 다시 빌드해야 합니다.
#### 옵션 2: 런타임 (config.json)
제공되는 디렉터리에 `config.json` 파일을 마운트하세요. 다시 빌드할 필요가 없습니다:
```json
{
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
}
```
```bash
docker run -d \
-p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf-simple:latest
```
또는 Docker Compose로:
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
ports:
- '3000:8080'
volumes:
- ./config.json:/usr/share/nginx/html/config.json:ro
```
두 방법을 함께 사용할 수 있으며, 목록은 병합됩니다. 둘 중 하나에 있는 도구는 비활성화됩니다.
#### 에디터 기능 비활성화
전체 에디터 도구를 비활성화하지 않고 PDF 에디터 내부의 특정 기능(예: 교정, 주석, 양식)만 비활성화할 수도 있습니다. `config.json`에 `editorDisabledCategories`를 추가하세요:
```json
{
"editorDisabledCategories": ["redaction", "annotation-stamp"]
}
```
<details>
<summary>전체 에디터 카테고리 목록</summary>
**확대/축소:** `zoom`, `zoom-in`, `zoom-out`, `zoom-fit-page`, `zoom-fit-width`, `zoom-marquee`, `zoom-level`
**주석:** `annotation`, `annotation-markup`, `annotation-highlight`, `annotation-underline`, `annotation-strikeout`, `annotation-squiggly`, `annotation-ink`, `annotation-text`, `annotation-stamp`
**도형:** `annotation-shape`, `annotation-rectangle`, `annotation-circle`, `annotation-line`, `annotation-arrow`, `annotation-polygon`, `annotation-polyline`
**양식:** `form`, `form-textfield`, `form-checkbox`, `form-radio`, `form-select`, `form-listbox`, `form-fill-mode`
**교정(Redaction):** `redaction`, `redaction-area`, `redaction-text`, `redaction-apply`, `redaction-clear`
**문서:** `document`, `document-open`, `document-close`, `document-print`, `document-capture`, `document-export`, `document-fullscreen`, `document-protect`
**페이지:** `page`, `spread`, `rotate`, `scroll`, `navigation`
**패널:** `panel`, `panel-sidebar`, `panel-search`, `panel-comment`
**도구:** `tools`, `pan`, `pointer`, `capture`
**선택:** `selection`, `selection-copy`
**기록:** `history`, `history-undo`, `history-redo`
</details>
카테고리는 계층 구조입니다. 상위 항목(예: `annotation`)을 비활성화하면 모든 하위 항목도 비활성화됩니다.
### GitHub 스타 카운터 비활성화
내비게이션 바에는 페이지 로드 시 `api.github.com`에서 가져오는 실시간 GitHub 스타 수가 표시됩니다. PDF 기능과 무관한 유일한 외부 요청입니다. 빌드 시 비활성화하면 번들에서 가져오기 코드가 제거되고 Content-Security-Policy에서 `api.github.com`이 제거됩니다:
```bash
docker build --build-arg DISABLE_GITHUB_STARS=true -t bentopdf .
```
Simple Mode 빌드(`bentopdf-simple`)는 항상 스타 카운터를 건너뜁니다. 이 플래그는 상용 빌드나 사용자 지정 전체 빌드에서만 필요합니다.
### 사용자 지정 WASM URL (폐쇄망 / 직접 호스팅)
> [!IMPORTANT]
> WASM URL은 **빌드 시점**에 JavaScript에 포함됩니다. WASM 파일은 런타임에 **사용자의 브라우저**가 다운로드하며, 빌드 중에 Docker가 다운로드하지 않습니다. 폐쇄망에서는 브라우저가 접근할 수 있는 내부 서버에 WASM 파일을 호스팅해야 합니다.
**전체 폐쇄망 워크플로:**
```bash
# 1. On a machine WITH internet — download WASM packages
bash scripts/prepare-airgap.sh --list-ocr-languages
bash scripts/prepare-airgap.sh --search-ocr-language german
# 2. Download WASM/OCR packages
npm pack @bentopdf/pymupdf-wasm@0.11.14
npm pack @bentopdf/gs-wasm
npm pack coherentpdf
npm pack tesseract.js@7.0.0
npm pack tesseract.js-core@7.0.0
mkdir -p tesseract-langdata
curl -fsSL https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz -o tesseract-langdata/eng.traineddata.gz
mkdir -p ocr-fonts
curl -fsSL https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf -o ocr-fonts/NotoSans-Regular.ttf
# 3. Build the image with your internal server URLs
docker build \
--build-arg VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/ \
--build-arg VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/ \
--build-arg VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data \
--build-arg VITE_TESSERACT_AVAILABLE_LANGUAGES=eng,deu \
--build-arg VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts \
-t bentopdf .
# 4. Export the image
docker save bentopdf -o bentopdf.tar
# 5. Transfer bentopdf.tar + the .tgz packages + tesseract-langdata/ + ocr-fonts/ into the air-gapped network
# 6. Inside the air-gapped network — load and run
docker load -i bentopdf.tar
# Extract WASM packages to your internal web server
mkdir -p /var/www/wasm/pymupdf /var/www/wasm/gs /var/www/wasm/cpdf /var/www/wasm/ocr/core /var/www/wasm/ocr/lang-data /var/www/wasm/ocr/fonts
tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C /var/www/wasm/pymupdf --strip-components=1
tar xzf bentopdf-gs-wasm-*.tgz -C /var/www/wasm/gs --strip-components=1
tar xzf coherentpdf-*.tgz -C /var/www/wasm/cpdf --strip-components=1
TEMP_TESS=$(mktemp -d)
tar xzf tesseract.js-7.0.0.tgz -C "$TEMP_TESS"
cp "$TEMP_TESS/package/dist/worker.min.js" /var/www/wasm/ocr/worker.min.js
rm -rf "$TEMP_TESS"
tar xzf tesseract.js-core-7.0.0.tgz -C /var/www/wasm/ocr/core --strip-components=1
cp ./tesseract-langdata/*.traineddata.gz /var/www/wasm/ocr/lang-data/
cp ./ocr-fonts/* /var/www/wasm/ocr/fonts/
# Run BentoPDF
docker run -d -p 3000:8080 --restart unless-stopped bentopdf
```
`--ocr-languages`에는 `bash scripts/prepare-airgap.sh --list-ocr-languages`가 출력하는 코드를 사용하거나, `bash scripts/prepare-airgap.sh --search-ocr-language <term>`으로 이름을 검색하세요. 제한된 OCR 언어 하위 집합으로 빌드할 때는 같은 코드를 `VITE_TESSERACT_AVAILABLE_LANGUAGES`에 전달하여 앱이 번들된 언어만 표시하도록 하세요. 완전한 오프라인 OCR 출력을 위해서는 번들된 `ocr-fonts/` 디렉터리도 호스팅하고 `VITE_OCR_FONT_BASE_URL`이 이를 가리키게 하세요.
변수를 빈 문자열로 설정하면 해당 모듈이 비활성화됩니다 (사용자가 고급 설정에서 직접 구성해야 합니다).
## 사용자 지정 포트
기본적으로 BentoPDF는 컨테이너 내부에서 `8080` 포트로 수신합니다. 변경하려면 런타임에 `PORT` 환경 변수를 설정하세요:
```bash
docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf-simple:latest
```
| 변수 | 설명 | 기본값 |
| ------ | --------------------------- | ------ |
| `PORT` | 컨테이너 내 Nginx 수신 포트 | `8080` |
표준 Dockerfile과 nonroot Dockerfile 모두에서 작동합니다.
## 사용자 지정 사용자 ID (PUID/PGID)
특정 비루트 사용자로 실행해야 하는 환경(NAS 기기, 보안 컨텍스트가 있는 Kubernetes, 조직 정책 등)을 위해 BentoPDF는 LSIO 방식의 PUID/PGID를 지원하는 별도의 Dockerfile을 제공합니다.
### 빌드 및 실행
```bash
# Build the non-root image
docker build -f Dockerfile.nonroot -t bentopdf-nonroot .
# Run with custom UID/GID
docker run -d \
--name bentopdf \
-p 3000:8080 \
-e PUID=1000 \
-e PGID=1000 \
--restart unless-stopped \
bentopdf-nonroot
```
### 환경 변수
| 변수 | 설명 | 기본값 |
| -------------- | -------------------- | ------- |
| `PUID` | 실행할 사용자 ID | `1000` |
| `PGID` | 실행할 그룹 ID | `1000` |
| `PORT` | Nginx 수신 포트 | `8080` |
| `DISABLE_IPV6` | IPv6 리스너 비활성화 | `false` |
### Docker Compose
```yaml
services:
bentopdf:
build:
context: .
dockerfile: Dockerfile.nonroot
container_name: bentopdf
ports:
- '3000:8080'
environment:
- PUID=1000
- PGID=1000
restart: unless-stopped
```
### 작동 방식
컨테이너는 root로 시작하여 지정된 PUID/PGID로 사용자를 만들고, 쓰기 가능한 모든 디렉터리의 소유권을 조정한 다음 `su-exec`로 권한을 낮춥니다. nginx 프로세스는 전적으로 지정한 사용자로 실행됩니다.
> [!NOTE]
> 표준 `Dockerfile`은 `nginx-unprivileged`(UID 101)를 사용하며 대부분의 배포에 권장됩니다. 특정 UID/GID가 필요할 때만 `Dockerfile.nonroot`를 사용하세요.
> [!WARNING]
> PUID/PGID는 `0`(root)일 수 없습니다. 엔트리포인트가 입력을 검증하고 잘못된 값이면 오류와 함께 종료합니다.
## Traefik과 함께 사용 (리버스 프록시)
```yaml
services:
traefik:
image: traefik:v2.10
command:
- '--providers.docker=true'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--certificatesresolvers.letsencrypt.acme.email=you@example.com'
- '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
- '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
ports:
- '80:80'
- '443:443'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.bentopdf.rule=Host(`pdf.example.com`)'
- 'traefik.http.routers.bentopdf.entrypoints=websecure'
- 'traefik.http.routers.bentopdf.tls.certresolver=letsencrypt'
- 'traefik.http.services.bentopdf.loadbalancer.server.port=8080'
# Required headers for SharedArrayBuffer (LibreOffice WASM)
- 'traefik.http.routers.bentopdf.middlewares=bentopdf-headers'
- 'traefik.http.middlewares.bentopdf-headers.headers.customresponseheaders.Cross-Origin-Opener-Policy=same-origin'
- 'traefik.http.middlewares.bentopdf-headers.headers.customresponseheaders.Cross-Origin-Embedder-Policy=require-corp'
restart: unless-stopped
```
## Caddy와 함께 사용 (리버스 프록시)
```yaml
services:
caddy:
image: caddy:2
ports:
- '80:80'
- '443:443'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
restart: unless-stopped
volumes:
caddy_data:
```
Caddyfile:
```
pdf.example.com {
reverse_proxy bentopdf:8080
header Cross-Origin-Opener-Policy "same-origin"
header Cross-Origin-Embedder-Policy "require-corp"
}
```
## 리소스 제한
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
```
## Podman Quadlet (Systemd 통합)
[Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)을 사용하면 Podman 컨테이너를 systemd 서비스로 실행할 수 있습니다. Linux 시스템의 프로덕션 배포에 이상적입니다.
### 기본 Quadlet 설정
`~/.config/containers/systemd/bentopdf.container`(사용자) 또는 `/etc/containers/systemd/bentopdf.container`(시스템)에 컨테이너 유닛 파일을 만드세요:
```ini
[Unit]
Description=BentoPDF - Privacy-first PDF toolkit
After=network-online.target
Wants=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
```
### 활성화 및 시작
```bash
# Reload systemd to detect new unit
systemctl --user daemon-reload
# Start the service
systemctl --user start bentopdf
# Enable on boot
systemctl --user enable bentopdf
# Check status
systemctl --user status bentopdf
# View logs
journalctl --user -u bentopdf -f
```
> [!TIP]
> 시스템 전체 배포에는 `--user` 플래그 없이 `systemctl`을 사용하고 파일을 `/etc/containers/systemd/`에 두세요.
### 헬스 체크가 있는 Quadlet
```ini
[Unit]
Description=BentoPDF with health monitoring
After=network-online.target
Wants=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
HealthCmd=wget --spider -q http://localhost:8080 || exit 1
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
```
### Quadlet 자동 업데이트
Podman은 새 이미지가 있으면 컨테이너를 자동으로 업데이트할 수 있습니다:
```bash
# Enable auto-update timer
systemctl --user enable --now podman-auto-update.timer
# Check for updates manually
podman auto-update
# Dry run (check without updating)
podman auto-update --dry-run
```
### Quadlet 네트워크 설정
사용자 지정 네트워크 설정을 위해 네트워크 파일 `bentopdf.network`를 만드세요:
```ini
[Network]
Subnet=10.89.0.0/24
Gateway=10.89.0.1
```
그런 다음 컨테이너 파일에서 참조하세요:
```ini
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
Network=bentopdf.network
```
## 업데이트
```bash
# Pull latest image
docker compose pull
# Recreate container
docker compose up -d
```
+266
View File
@@ -0,0 +1,266 @@
# Hostinger에 배포하기
[Hostinger](https://hostinger.com)는 인기 있는 공유 호스팅 제공업체입니다. 이 가이드는 Hostinger 공유 호스팅에 BentoPDF를 배포하는 방법을 다룹니다.
## 사전 요구 사항
- 파일 관리자에 접근할 수 있는 Hostinger 호스팅 플랜
- 빌드를 위해 로컬에 설치된 Node.js
## 1단계: 프로젝트 빌드
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
빌드된 파일은 `dist` 폴더에 있습니다.
## 2단계: Hostinger에 업로드
### 루트 도메인 배포
1. Hostinger 계정에 로그인합니다
2. **File Manager** → **public_html**로 이동합니다
3. 기존 파일을 모두 **삭제**합니다 (필요하면 백업하세요)
4. 로컬 `dist` 폴더의 모든 내용을 `public_html`에 **업로드**합니다
### 하위 디렉터리 배포
하위 디렉터리(예: `yourdomain.com/pdf-tools/`)에 배포하는 경우:
1. 올바른 기본 URL로 빌드합니다:
```bash
BASE_URL=/pdf-tools/ npm run build
```
2. Hostinger에서 폴더를 만듭니다:
- **File Manager** → **public_html**로 이동합니다
- 새 폴더를 만듭니다: `pdf-tools`
3. `dist`의 모든 내용을 `public_html/pdf-tools/`에 업로드합니다
## 3단계: .htaccess 파일 만들기
배포 폴더의 루트(`public_html` 또는 `public_html/pdf-tools/`)에 `.htaccess` 파일을 만드세요:
::: warning 중요
하위 디렉터리에 배포하는 경우 `RewriteBase /`를 `RewriteBase /pdf-tools/`(또는 사용하는 폴더 이름)로 변경하세요.
:::
```apache
RewriteEngine On
RewriteBase /
# ============================================
# 1. SECURITY HEADERS (CRITICAL FOR WASM)
# ============================================
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# REQUIRED for soffice.js (SharedArrayBuffer)
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
</IfModule>
# ============================================
# 2. BROWSER CACHING
# ============================================
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType application/gzip "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
# ============================================
# 3. COMPRESSION (STANDARD)
# ============================================
# Prevent server from double-compressing files
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
SetEnvIfNoCase Request_URI "\.br$" no-gzip
SetEnvIfNoCase Request_URI "\.wasm$" no-gzip
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE font/woff
AddOutputFilterByType DEFLATE font/ttf
</IfModule>
# ============================================
# 4. MIME TYPES & SPECIAL FILE HANDLING
# ============================================
AddType application/javascript .js .mjs
AddType application/wasm .wasm
AddType font/woff2 .woff2
AddType font/woff .woff
AddType image/webp .webp
# Handle soffice.wasm.gz correctly
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
Header set Cross-Origin-Resource-Policy "cross-origin"
Header append Vary Accept-Encoding
</FilesMatch>
# Handle data.gz
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
Header append Vary Accept-Encoding
</FilesMatch>
# ============================================
# 5. REDIRECTS & ROUTING
# ============================================
# Canonical WWW (update domain as needed)
RewriteCond %{HTTP_HOST} ^bentopdf\.com [NC]
RewriteRule ^(.*)$ https://www.bentopdf.com/$1 [L,R=301]
# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Remove trailing slash (except for language root directories)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/$
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# 5.1. LANGUAGE ROUTES
# ============================================
# Supported languages: de, es, zh, zh-TW, vi, it, id, tr, fr, pt
# English has no prefix - served from root
# English prefix redirects to root (for SEO consistency)
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# 5.5. DOCS ROUTING (VitePress)
# ============================================
RewriteCond %{REQUEST_URI} ^/docs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.*)$ $1.html [L]
# ============================================
# 6. ADD .HTML EXTENSION IF FILE EXISTS (ROOT LEVEL ONLY)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
# ============================================
# 7. ERROR PAGES
# ============================================
ErrorDocument 404 /404.html
```
## 하위 디렉터리 .htaccess 예시
`yourdomain.com/pdf-tools/`의 경우 다음 줄을 업데이트하세요:
```apache
RewriteBase /pdf-tools/
# ... (same content as above, but update paths) ...
# Language prefix root
RewriteCond %{DOCUMENT_ROOT}/pdf-tools/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /pdf-tools/$1/index.html [L]
# Language prefix with path
RewriteCond %{DOCUMENT_ROOT}/pdf-tools/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /pdf-tools/$1/$2.html [L]
# Root level .html extension
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /pdf-tools/404.html
```
## 문제 해결
### WASM 파일이 로드되지 않음
`.htaccess`에 MIME 타입이 올바르게 설정되어 있는지 확인하세요:
```apache
AddType application/wasm .wasm
```
### LibreOffice 도구가 작동하지 않음
보안 헤더는 SharedArrayBuffer에 필수입니다:
```apache
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
```
헤더가 적용되지 않으면 Hostinger 지원팀에 `mod_headers` 활성화를 요청하세요.
### 페이지 새로고침 시 404 오류
`.html` 확장자 규칙과 언어 라우트가 올바르게 설정되어 있는지 확인하세요. BentoPDF는 SPA 라우팅이 아니라 정적 HTML 파일을 사용합니다:
```apache
# Language routes serve actual files from language directories
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# Root level pages
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
```
### 파일 업로드 제한
Hostinger에는 파일 크기 제한이 있을 수 있습니다. 일괄 업로드가 실패하면 WASM 파일을 따로 업로드하세요.
+538
View File
@@ -0,0 +1,538 @@
# 셀프 호스팅 가이드
BentoPDF는 자체 인프라에서 셀프 호스팅할 수 있습니다. 이 가이드는 다양한 배포 옵션을 다룹니다.
## Docker / Podman으로 빠르게 시작하기
BentoPDF를 셀프 호스팅하는 가장 빠른 방법입니다.
> [!TIP]
> BentoPDF는 두 가지 빌드로 제공됩니다.
>
> - **셀프 호스팅 빌드**: `ghcr.io/alam00000/bentopdf-simple:latest`. 마케팅 요소(히어로, FAQ, 후기, 푸터)를 **제외한** 모든 PDF 도구를 제공합니다. 내부/팀/조직 배포에 사용하세요. 기능이 줄어든 "라이트" 버전이 **아닙니다**.
> - **상업용 빌드**: `ghcr.io/alam00000/bentopdf:latest`. 전체 마케팅 사이트를 포함하며 bentopdf.com 자체와, 공개 서비스를 운영하는 상업용 라이선스 보유자가 사용합니다.
>
> 잘 모르겠다면 셀프 호스팅 빌드를 받으세요.
> [!IMPORTANT]
> Office 파일 변환에는 `SharedArrayBuffer`가 필요하며, 이를 위해 앱이 교차 출처 격리되어 있고 보안 컨텍스트에서 제공되어야 합니다. 공식 이미지는 필요한 COOP/COEP 헤더를 이미 전송하지만, `http://192.168.x.x` 같은 일반 HTTP 로컬 네트워크 출처에서는 브라우저가 여전히 `SharedArrayBuffer`를 비활성화합니다.
>
> `http://localhost`는 같은 기기에서 테스트할 때만 사용하세요. 사용자가 LAN IP나 호스트 이름으로 BentoPDF에 접속한다면 HTTPS로 종단 처리하세요.
```bash
# Docker
docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
```
또는 Docker Compose / Podman Compose로:
```yaml
# docker-compose.yml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
ports:
- '3000:8080'
restart: unless-stopped
```
```bash
# Docker Compose
docker compose up -d
# Podman Compose
podman-compose up -d
```
## Podman Quadlet (Linux Systemd)
BentoPDF를 systemd 서비스로 실행합니다. `~/.config/containers/systemd/bentopdf.container`를 만듭니다.
```ini
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=default.target
```
```bash
systemctl --user daemon-reload
systemctl --user enable --now bentopdf
```
Quadlet 전체 문서는 [Docker 배포 가이드](/ko/self-hosting/docker)를 참고하세요.
## 소스에서 빌드하기
```bash
# Clone and build
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
# The built files are in the `dist` folder
```
## 구성 옵션
### 셀프 호스팅 빌드 (Simple Mode)
셀프 호스팅 빌드(`bentopdf-simple` 이미지, Simple Mode라고도 함)는 상업용 빌드와 **기능이 동일**합니다. 모든 PDF 도구가 들어 있고 똑같이 작동합니다. bentopdf.com 자체나 공개 서비스를 운영하는 상업용 배포에서만 의미가 있는 마케팅 요소만 숨깁니다. **기능이 줄어든 "라이트" 버전이 아닙니다.**
**셀프 호스팅 빌드가 숨기는 것** (외관상 변경일 뿐이며 PDF 기능은 제거되지 않습니다):
- 탐색 모음, 히어로 섹션, 기능 섹션, FAQ, 후기, 푸터
- 페이지 제목을 "PDF 도구"로 변경
**셀프 호스팅 빌드가 유지하는 것** (실제 PDF 작업을 수행하는 모든 것):
- 모든 PDF 도구 (병합, 분할, 편집, 서명, OCR, Office 변환 등)
- 사용자 지정 브랜딩 지원, 모든 빌드 시점 및 런타임 구성
상업용 빌드(`ghcr.io/alam00000/bentopdf:latest`)는 bentopdf.com 자체를 구동하며 공개 서비스를 운영하는 상업용 라이선스 보유자가 사용합니다. 내부 도구에는 어울리지 않는 히어로, FAQ, 후기, 푸터가 추가됩니다.
팀, 조직 또는 내부 도구로 BentoPDF를 셀프 호스팅한다면 셀프 호스팅 빌드를 받으세요.
```bash
# Use the pre-built image (recommended)
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Or build it yourself
SIMPLE_MODE=true npm run build
```
자세한 내용은 [SIMPLE_MODE.md](https://github.com/alam00000/bentopdf/blob/main/SIMPLE_MODE.md)를 참고하세요.
### 상업용 빌드 (자체 브랜드의 공개 배포)
상업용 빌드(`bentopdf` 이미지, `-simple` 접미사 없음)는 bentopdf.com 자체를 구동합니다. 모든 PDF 도구에 더해 전체 마케팅 사이트(히어로, 기능, FAQ, 후기, 푸터)를 포함합니다. **자체 브랜드로 BentoPDF를 공개 PDF 서비스로 배포**하려는 경우에 이 빌드를 사용하세요. 예를 들면 다음과 같습니다.
- 자체 도메인에서 최종 사용자를 위한 호스팅 SaaS로 BentoPDF를 운영하는 경우
- 도구 화면만이 아니라 랜딩 페이지 경험(마케팅 섹션 + 도구)을 원하는 경우
- BentoPDF를 상용 제품에 포함하는 상업용 라이선스 보유자인 경우
**그대로 실행** (BentoPDF 브랜딩, 빌드를 미리 확인할 때 유용):
```bash
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
```
**자체 브랜드로 빌드** (일반적인 상업용 워크플로):
```bash
docker build \
--build-arg VITE_BRAND_NAME="AcmePDF" \
--build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \
--build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. All rights reserved." \
-t acmepdf .
docker run -p 3000:8080 acmepdf
```
다른 모든 빌드 시점 옵션(`BASE_URL`, `VITE_DEFAULT_LANGUAGE`, `DISABLE_TOOLS`, WASM URL 재정의 등)은 셀프 호스팅 빌드와 같은 방식으로 작동합니다.
::: warning 라이선스
상업용 빌드는 BentoPDF의 두 가지 라이선스 옵션 중 어느 쪽으로든 실행할 수 있습니다.
- **AGPL-3.0** (무료): 브랜딩 수정과 주변 비즈니스 로직을 포함한 전체 소스 코드를 AGPL로 공개하는 배포에서 허용됩니다.
- **상업용 라이선스** ($79 평생): 브랜딩 포크나 비즈니스 코드를 오픈소스로 공개하지 않는 비공개/독점 배포에 필요합니다.
전체 비교는 [라이선스 페이지](https://www.bentopdf.com/licensing)를 참고하세요. AGPL 라이선스의 WASM 모듈(PyMuPDF, Ghostscript, CoherentPDF)은 런타임에 CDN에서 로드되므로 이미지에 포함되지 않으며 라이선스 상태에 영향을 주지 않습니다.
:::
### Base URL
하위 디렉터리에 배포합니다.
```bash
BASE_URL=/pdf-tools/ npm run build
```
### 사용자 지정 브랜딩
기본 BentoPDF 로고, 이름, 푸터 텍스트를 빌드 시점에 직접 만든 것으로 바꿉니다.
| 변수 | 설명 | 기본값 |
| ------------------ | ---------------------------------- | --------------------------------------- |
| `VITE_BRAND_NAME` | 헤더와 푸터에 표시되는 브랜드 이름 | `BentoPDF` |
| `VITE_BRAND_LOGO` | `public/` 기준 로고 경로 | `images/favicon-no-bg.svg` |
| `VITE_FOOTER_TEXT` | 사용자 지정 푸터/저작권 텍스트 | `© 2026 BentoPDF. All rights reserved.` |
```bash
# Place your logo in public/, then build
VITE_BRAND_NAME="AcmePDF" \
VITE_BRAND_LOGO="images/acme-logo.svg" \
VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
npm run build
```
또는 Docker로:
```bash
docker build \
--build-arg VITE_BRAND_NAME="AcmePDF" \
--build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \
--build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
-t acmepdf .
```
브랜딩은 전체 모드와 Simple Mode 모두에서 작동하며, 다른 모든 빌드 시점 옵션(`BASE_URL`, `SIMPLE_MODE`, `VITE_DEFAULT_LANGUAGE`)과 함께 사용할 수 있습니다.
### 특정 도구 비활성화
규정 준수나 보안을 위해 개별 도구를 숨깁니다. 비활성화된 도구는 홈페이지, 검색, 단축키, 워크플로 빌더, 직접 URL 접근에서 제거됩니다.
도구 ID는 `.html`을 뺀 페이지 URL입니다. 아무 도구나 열어 URL을 확인하세요(예: `edit-pdf`, `sign-pdf`, `encrypt-pdf`).
**빌드 시점** (번들에 포함됨):
```bash
DISABLE_TOOLS="edit-pdf,sign-pdf" npm run build
```
**런타임** (재빌드 불필요, `config.json`을 마운트):
```json
{
"disabledTools": ["edit-pdf", "sign-pdf"]
}
```
```bash
docker run -d -p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf-simple:latest
```
두 방법을 함께 사용할 수 있으며 목록이 병합됩니다.
`config.json`에 `editorDisabledCategories`를 추가하면 도구 전체를 비활성화하지 않고 PDF 편집기 내부의 특정 기능(예: 교정)만 비활성화할 수도 있습니다. 전체 카테고리 목록은 [Docker 가이드](/ko/self-hosting/docker#에디터-기능-비활성화)를 참고하세요.
## 배포 가이드
플랫폼을 선택하세요.
- [Vercel](/ko/self-hosting/vercel)
- [Netlify](/ko/self-hosting/netlify)
- [Cloudflare Pages](/ko/self-hosting/cloudflare)
- [AWS S3 + CloudFront](/ko/self-hosting/aws)
- [Hostinger](/ko/self-hosting/hostinger)
- [Nginx](/ko/self-hosting/nginx)
- [Apache](/ko/self-hosting/apache)
- [Docker](/ko/self-hosting/docker)
- [Kubernetes](/ko/self-hosting/kubernetes)
- [CORS 프록시](/ko/self-hosting/cors-proxy) - 디지털 서명에 필요
## 자주 발생하는 문제
### Word / ODT / Excel / PowerPoint를 PDF로 변환이 멈춤 (SharedArrayBuffer 사용 불가)
**증상**: LibreOffice 기반 문서 변환(Word, ODT, Excel, PowerPoint를 PDF로)이 약 55%에서 멈추거나 시작되지 않습니다. 브라우저 콘솔에 다음이 표시될 수 있습니다.
> `ReferenceError: SharedArrayBuffer is not defined`
...또는 `window.crossOriginIsolated`가 `false`로 보고되거나, WASM 컴파일이 `expected magic word 00 61 73 6d, found 1f 8b 08 08`을 보고합니다.
**원인**: LibreOffice WASM에는 `SharedArrayBuffer`가 필요하며, 브라우저는 페이지가 **교차 출처 격리**되어 있고 **보안 컨텍스트**에서 제공될 때만 이를 활성화합니다. 즉 두 가지가 모두 충족되어야 합니다.
1. 모든 응답에 다음 두 헤더가 포함되어야 합니다.
- `Cross-Origin-Embedder-Policy: require-corp`
- `Cross-Origin-Opener-Policy: same-origin`
2. 페이지가 `https://...` 또는 `http://localhost`에서 제공되어야 합니다. LAN IP의 일반 HTTP(예: `http://192.168.x.x`)는 보안 컨텍스트로 인정되지 않으며, 브라우저가 `SharedArrayBuffer`를 비활성화합니다.
`00 61 73 6d / 1f 8b 08 08` 불일치는 별도의 하위 증상입니다. 미리 압축된 `.wasm.gz` / `.data.gz` 파일에 `Content-Encoding: gzip` 응답 헤더가 없어서 브라우저가 압축 해제된 WASM이 아닌 원시 gzip 바이트를 받는 것입니다.
**해결**: 정확한 설정은 플랫폼별 배포 가이드를 참고하세요.
- [Nginx →](/ko/self-hosting/nginx#word-odt-excel을-pdf로-변환이-작동하지-않음)
- [Apache →](/ko/self-hosting/apache#word-odt-excel을-pdf로-변환이-작동하지-않음)
- [AWS S3 + CloudFront →](/ko/self-hosting/aws#_3b단계-응답-헤더-정책-libreoffice-wasm에-필수)
- [Cloudflare Pages →](/ko/self-hosting/cloudflare#설정-파일) (`_headers` 파일)
- [Netlify →](/ko/self-hosting/netlify#word-odt-excel을-pdf로-변환이-55-에서-멈춤) (`netlify.toml`)
- [Vercel →](/ko/self-hosting/vercel#word-odt-excel을-pdf로-변환이-작동하지-않음) (`vercel.json`)
- [Hostinger →](/ko/self-hosting/hostinger#libreoffice-도구가-작동하지-않음) (`.htaccess`)
- [Kubernetes →](/ko/self-hosting/kubernetes#sharedarraybuffer-헤더가-계속-작동하는지-확인-ingress-gateway)
- **Docker**: 번들된 nginx 설정이 자동으로 처리하므로 별도 조치가 필요 없습니다.
**확인**: 아무 BentoPDF 페이지에서 DevTools 콘솔을 열고 다음을 실행하세요.
```js
console.log(window.crossOriginIsolated); // should be true
console.log(typeof SharedArrayBuffer); // should be "function"
```
페이지가 HTTPS 또는 `http://localhost`이고 모든 응답에 COEP/COOP 헤더가 모두 있으면 두 검사 모두 통과합니다. `http://192.168.x.x` 같은 루프백이 아닌 HTTP 출처라면 HTTPS로 종단 처리하세요. 헤더만으로 해결할 방법은 없습니다.
### `.mjs` 파일이 `application/octet-stream`으로 제공됨
**증상**: PDF 서명 / 양식 작성기 / 일부 다른 도구에서 뷰어가 비어 있거나 로드에 실패합니다. 브라우저 콘솔에 다음이 보고됩니다.
> `Failed to load module script: The server responded with a non-JavaScript MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.`
**원인**: 웹 서버나 리버스 프록시에 `.mjs` 파일의 MIME 타입 매핑이 없습니다(내장 PDF 뷰어는 이 확장자의 ES 모듈을 포함합니다). 많은 기본 서버 설정은 인식하지 못하는 확장자를 `application/octet-stream`으로 처리하며, 브라우저는 이를 ES 모듈로 실행하지 않습니다.
**해결**: 정확한 스니펫은 플랫폼별 배포 가이드를 참고하세요.
- [Nginx →](/ko/self-hosting/nginx#pdf-서명-또는-양식-작성기에-빈-뷰어가-표시됨-mjs-mime-오류)
- [Apache →](/ko/self-hosting/apache#pdf-서명-또는-form-filler에-빈-뷰어가-표시됨-mjs-mime-오류)
- [AWS S3 + CloudFront →](/ko/self-hosting/aws#_2단계-빌드-및-업로드) (`aws s3 cp ... --include "*.mjs"` 참고)
- [Kubernetes →](/ko/self-hosting/kubernetes#mjs-mime-타입-오류-pdf-서명-양식-작성기-iframe이-비어-있음)
- **Docker, Vercel, Netlify, Cloudflare Pages, Hostinger**: 자동으로 처리되므로 별도 조치가 필요 없습니다.
**확인**: DevTools → Network 탭을 열어 실패한 `.mjs` 요청을 찾고 `Content-Type` 응답 헤더를 확인하세요. `application/javascript`여야 합니다. 여전히 `application/octet-stream`이라면 바깥쪽 리버스 프록시나 CDN이 타입을 다시 판별하고 있을 수 있으니, 서비스 경로의 각 계층을 확인하세요.
## WASM 구성 (AGPL 구성 요소)
BentoPDF는 AGPL 라이선스 처리 라이브러리를 소스 코드에 **번들하지 않지만**, 모든 기능이 바로 작동하도록 **CDN URL을 미리 구성**해 둡니다. 수동 설정이 필요 없습니다.
::: tip 기본 제로 설정
v2.0.0부터 WASM 모듈은 환경 변수를 통해 jsDelivr CDN에서 로드되도록 미리 구성되어 있습니다. 모든 고급 기능이 사용자 설정 없이 바로 작동합니다.
:::
| 구성 요소 | 라이선스 | 기능 |
| --------------- | -------- | -------------------------------------------- |
| **PyMuPDF** | AGPL-3.0 | EPUB/MOBI/FB2/XPS 변환, 이미지 추출, 표 추출 |
| **Ghostscript** | AGPL-3.0 | PDF/A 변환, 압축, 기울기 보정, 래스터화 |
| **CoherentPDF** | AGPL-3.0 | 목차, 첨부파일, 북마크를 유지한 PDF 병합 |
### 기본 환경 변수
이 값은 `.env.production`에 설정되어 빌드에 포함됩니다.
```bash
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/
VITE_TESSERACT_WORKER_URL=
VITE_TESSERACT_CORE_URL=
VITE_TESSERACT_LANG_URL=
VITE_TESSERACT_AVAILABLE_LANGUAGES=
VITE_OCR_FONT_BASE_URL=
```
### WASM URL 재정의
사용자 지정 배포를 위해 빌드 시점에 기본값을 재정의할 수 있습니다.
```bash
# Via Docker build args
docker build \
--build-arg VITE_WASM_PYMUPDF_URL=https://your-server.com/pymupdf/ \
--build-arg VITE_WASM_GS_URL=https://your-server.com/gs/ \
--build-arg VITE_WASM_CPDF_URL=https://your-server.com/cpdf/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://your-server.com/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://your-server.com/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://your-server.com/ocr/lang-data \
--build-arg VITE_TESSERACT_AVAILABLE_LANGUAGES=eng,deu \
--build-arg VITE_OCR_FONT_BASE_URL=https://your-server.com/ocr/fonts \
-t bentopdf .
# Or via .env.production before building from source
VITE_WASM_PYMUPDF_URL=https://your-server.com/pymupdf/ npm run build
```
모듈을 완전히 비활성화하려면(고급 설정에서 사용자가 직접 구성해야 함) 해당 변수를 빈 문자열로 설정하세요.
OCR의 경우 모든 `VITE_TESSERACT_*` 변수를 비워 두고 기본 온라인 에셋을 사용하거나, 셀프 호스팅/오프라인 OCR을 위해 worker/core/lang URL을 함께 설정하세요. 특정 OCR 언어만 번들한다면 `VITE_TESSERACT_AVAILABLE_LANGUAGES`에도 같은 쉼표 구분 코드를 설정하세요. 그러면 UI가 설치된 언어만 제공하고, 지원되지 않는 선택은 설명이 포함된 오류로 실패합니다. 완전 오프라인으로 검색 가능한 PDF를 출력하려면 번들된 OCR 글꼴을 제공하는 내부 디렉터리를 `VITE_OCR_FONT_BASE_URL`로 설정하세요.
사용자는 UI의 **고급 설정**에서 언제든지 이 기본값을 재정의할 수도 있습니다. 브라우저에 저장된 사용자 재정의가 환경 기본값보다 우선합니다.
### 폐쇄망 / 오프라인 배포
인터넷에 접속할 수 없는 네트워크(정부, 의료, 금융 등)를 위한 방법입니다. WASM URL은 **빌드 시점**에 JavaScript에 포함되고, 실제 WASM 파일은 런타임에 **사용자의 브라우저**가 다운로드합니다. 따라서 인터넷이 되는 컴퓨터에서 모든 것을 준비한 뒤 격리된 네트워크로 옮겨야 합니다.
#### 자동화 스크립트 (권장)
포함된 `prepare-airgap.sh` 스크립트가 WASM 패키지 다운로드, Docker 이미지 빌드, 설정 스크립트가 포함된 독립 실행형 번들 생성까지 전체 과정을 자동화합니다.
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
# Show supported OCR language codes (for --ocr-languages)
bash scripts/prepare-airgap.sh --list-ocr-languages
# Search OCR language codes by name or abbreviation
bash scripts/prepare-airgap.sh --search-ocr-language german
# Interactive mode — prompts for all options
bash scripts/prepare-airgap.sh
# Or fully automated
bash scripts/prepare-airgap.sh --wasm-base-url https://internal.example.com/wasm
```
다음과 같은 번들 디렉터리가 만들어집니다.
```
bentopdf-airgap-bundle/
bentopdf.tar # Docker image
*.tgz # WASM packages (PyMuPDF, Ghostscript, CoherentPDF, Tesseract)
tesseract-langdata/ # OCR traineddata files
ocr-fonts/ # OCR text-layer font files
setup.sh # Setup script for the air-gapped side
README.md # Instructions
```
USB, 내부 아티팩트 저장소 또는 승인된 방법으로 번들을 폐쇄망에 옮깁니다. 그런 다음 포함된 설정 스크립트를 실행합니다.
```bash
cd bentopdf-airgap-bundle
bash setup.sh
```
설정 스크립트는 Docker 이미지를 로드하고 WASM 파일을 추출하며, 선택적으로 컨테이너를 시작합니다.
**스크립트 옵션:**
| 플래그 | 설명 | 기본값 |
| ------------------------------ | ------------------------------------------------ | -------------------------- |
| `--wasm-base-url <url>` | WASM을 내부적으로 호스팅할 위치 | _(필수, 없으면 입력 요청)_ |
| `--image-name <name>` | Docker 이미지 태그 | `bentopdf` |
| `--output-dir <path>` | 출력 번들 디렉터리 | `./bentopdf-airgap-bundle` |
| `--simple-mode` | Simple Mode 활성화 | off |
| `--base-url <path>` | 하위 디렉터리 기본 URL (예: `/pdf/`) | `/` |
| `--language <code>` | 기본 UI 언어 (예: `fr`, `de`) | _(없음)_ |
| `--brand-name <name>` | 사용자 지정 브랜드 이름 | _(없음)_ |
| `--brand-logo <path>` | `public/` 기준 로고 경로 | _(없음)_ |
| `--footer-text <text>` | 사용자 지정 푸터 텍스트 | _(없음)_ |
| `--ocr-languages <list>` | 번들할 OCR 언어(쉼표로 구분) | `eng` |
| `--list-ocr-languages` | 지원되는 OCR 코드와 이름을 출력하고 종료 | off |
| `--search-ocr-language <term>` | 이름이나 약어로 OCR 코드 검색 | off |
| `--dockerfile <path>` | 사용할 Dockerfile | `Dockerfile` |
| `--skip-docker` | Docker 빌드와 내보내기 건너뛰기 | off |
| `--skip-wasm` | WASM 다운로드 건너뛰기 (기존 `.tgz` 파일 재사용) | off |
대화형 프롬프트에서도 `list`를 입력하면 지원되는 전체 Tesseract 코드 목록을 출력하고, `search <term>`을 입력하면 `search german`이나 `search chi` 같은 일치 항목을 찾을 수 있습니다.
::: warning 동일 출처 요구 사항
WASM 파일은 BentoPDF 앱과 **같은 출처**에서 제공되어야 합니다. 웹 워커는 다른 출처의 스크립트를 로드할 수 없는 `importScripts()`를 사용합니다. 예를 들어 BentoPDF가 `https://internal.example.com`에서 실행된다면 WASM 기본 URL도 `https://internal.example.com/wasm`이어야 합니다.
:::
#### 수동 단계
<details>
<summary>스크립트 없이 직접 진행하려는 경우</summary>
**1단계: WASM 및 OCR 패키지 다운로드** (인터넷이 되는 컴퓨터에서)
```bash
npm pack @bentopdf/pymupdf-wasm@0.11.14
npm pack @bentopdf/gs-wasm
npm pack coherentpdf
npm pack tesseract.js@7.0.0
npm pack tesseract.js-core@7.0.0
mkdir -p tesseract-langdata
curl -fsSL https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz -o tesseract-langdata/eng.traineddata.gz
mkdir -p ocr-fonts
curl -fsSL https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf -o ocr-fonts/NotoSans-Regular.ttf
```
**2단계: 내부 URL로 Docker 이미지 빌드**
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
docker build \
--build-arg VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/ \
--build-arg VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/ \
--build-arg VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data \
--build-arg VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts \
-t bentopdf .
```
**3단계: Docker 이미지 내보내기**
```bash
docker save bentopdf -o bentopdf.tar
```
**4단계: 폐쇄망으로 전송**
USB, 내부 아티팩트 저장소 또는 승인된 전송 방법으로 다음을 복사합니다.
- `bentopdf.tar`: Docker 이미지
- 1단계의 `.tgz` WASM/OCR 패키지 다섯 개
- 1단계의 `tesseract-langdata/` 디렉터리
- 1단계의 `ocr-fonts/` 디렉터리
**5단계: 폐쇄망 안에서 설정**
```bash
# Load the Docker image
docker load -i bentopdf.tar
# Extract WASM packages
mkdir -p ./wasm/pymupdf ./wasm/gs ./wasm/cpdf ./wasm/ocr/core ./wasm/ocr/lang-data ./wasm/ocr/fonts
tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C ./wasm/pymupdf --strip-components=1
tar xzf bentopdf-gs-wasm-*.tgz -C ./wasm/gs --strip-components=1
tar xzf coherentpdf-*.tgz -C ./wasm/cpdf --strip-components=1
TEMP_TESS=$(mktemp -d)
tar xzf tesseract.js-7.0.0.tgz -C "$TEMP_TESS"
cp "$TEMP_TESS/package/dist/worker.min.js" ./wasm/ocr/worker.min.js
rm -rf "$TEMP_TESS"
tar xzf tesseract.js-core-7.0.0.tgz -C ./wasm/ocr/core --strip-components=1
cp ./tesseract-langdata/*.traineddata.gz ./wasm/ocr/lang-data/
cp ./ocr-fonts/* ./wasm/ocr/fonts/
# Run BentoPDF
docker run -d -p 3000:8080 --restart unless-stopped bentopdf
```
`.../ocr/worker.min.js`, `.../ocr/core`, `.../ocr/lang-data`, `.../ocr/fonts`를 포함해 2단계에서 구성한 URL로 파일에 접근할 수 있는지 확인하세요.
</details>
::: info Docker 대신 소스에서 빌드하나요?
`npm run build`를 실행하기 전에 `.env.production`에 변수를 설정하세요.
```bash
VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/
VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/
VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/
VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js
VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core
VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data
VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts
```
:::
### 자체 WASM 프록시 호스팅
적절한 CORS 헤더와 함께 AGPL WASM 파일을 제공해야 한다면 간단한 프록시를 배포할 수 있습니다. 구현 예시는 [Cloudflare WASM Proxy 가이드](https://github.com/alam00000/bentopdf/blob/main/cloudflare/WASM-PROXY.md)를 참고하세요.
::: tip 왜 분리했나요?
이렇게 분리하면 다음이 보장됩니다.
- 상업용 사용자를 위한 명확한 법적 준수
- BentoPDF 코어가 듀얼 라이선스(AGPL-3.0 / 상업용)를 유지
- WASM 파일은 소스에 번들되지 않고 런타임에 로드됨
:::
## 시스템 요구 사항
| 요구 사항 | 최소 사양 |
| --------- | ------------------------------- |
| 저장 공간 | 약 100 MB (AGPL 모듈 제외 코어) |
| RAM | 512 MB |
| CPU | 최신 프로세서 아무거나 |
::: tip
BentoPDF는 정적 사이트이므로 데이터베이스나 백엔드 서버가 필요 없습니다!
:::
+211
View File
@@ -0,0 +1,211 @@
# Kubernetes로 배포하기
정적 사이트에 Kubernetes는 과할 수 있지만, 이미 Helm + GitOps를 표준으로 사용하고 있다면 잘 맞을 수 있습니다.
> [!IMPORTANT]
> **Office 파일 변환에 필요한 헤더**
>
> LibreOffice 기반 도구(Word, Excel, PowerPoint 변환)에는 `SharedArrayBuffer` 지원을 위해 다음 HTTP 헤더가 필요합니다.
>
> - `Cross-Origin-Opener-Policy: same-origin`
> - `Cross-Origin-Embedder-Policy: require-corp`
>
> 공식 BentoPDF nginx 이미지에는 이 헤더가 포함되어 있습니다. Kubernetes에서는 **Ingress/Gateway 컨트롤러도 리버스 프록시**이므로 이 헤더가 유지되는지 확인하거나 엣지에서 추가하세요.
## 사전 준비
- Kubernetes 클러스터
- Helm v3
- **8080 포트**로 서비스하는 BentoPDF nginx 이미지 (예: `ghcr.io/alam00000/bentopdf:<tag>`)
## Helm으로 배포
### 이 저장소에서 설치 (로컬 차트)
```bash
kubectl create namespace bentopdf
helm upgrade --install bentopdf /path/to/bentopdf/chart \
--namespace bentopdf \
--set image.repository=ghcr.io/alam00000/bentopdf \
--set image.tag=latest
```
### GHCR에서 설치 (OCI 차트)
차트가 OCI 아티팩트로 GHCR에 게시된 경우:
```bash
export GHCR_USERNAME="<github-org-or-user>"
helm upgrade --install bentopdf oci://ghcr.io/$GHCR_USERNAME/charts/bentopdf \
--namespace bentopdf \
--create-namespace \
--version 0.1.0 \
--set image.repository=ghcr.io/alam00000/bentopdf \
--set image.tag=latest
```
## 외부에 노출하기
### 포트 포워딩 (빠른 테스트)
```bash
kubectl -n bentopdf port-forward deploy/bentopdf 8080:8080
```
### Ingress (선택 사항)
Ingress를 활성화합니다 (nginx-ingress 예시).
```yaml
ingress:
enabled: true
className: nginx
hosts:
- host: pdf.example.com
paths:
- path: /
pathType: Prefix
```
### Gateway API (선택 사항)
이 차트는 Gateway API의 `Gateway`와 `HTTPRoute`를 지원합니다.
예시 (Cloudflare Gateway API 오퍼레이터):
```yaml
gateway:
enabled: true
name: bento-tunnel
namespace: bentopdf
gatewayClassName: cloudflare
httpRoute:
enabled: true
parentRefs:
- name: bento-tunnel
namespace: bentopdf
sectionName: http
hostnames:
- pdfs.example.com
```
## SharedArrayBuffer 헤더가 계속 작동하는지 확인 (Ingress/Gateway)
### 정상적으로 일어나야 하는 일
BentoPDF의 nginx 설정이 필요한 응답 헤더를 설정합니다. 대부분의 Ingress/Gateway 컨트롤러는 **업스트림 응답 헤더를 그대로 전달**합니다.
### 문제가 생길 수 있는 경우
- 응답 헤더를 **덮어쓰거나** **제거**하는 컨트롤러/엣지 정책
- 다른 COOP/COEP 값을 설정하는 "보안 헤더" 미들웨어
### 확인 방법
공개 엔드포인트에 대해 다음을 실행합니다.
```bash
curl -I https://pdf.example.com/ | egrep -i 'cross-origin-opener-policy|cross-origin-embedder-policy'
```
다음이 표시되어야 합니다.
- `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp`
### Ingress 컨트롤러가 헤더를 유지하지 않는 경우
엣지에서 헤더를 추가합니다(컨트롤러마다 다름). **nginx-ingress** 예시:
```yaml
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
```
## `.mjs` MIME 타입 오류 (PDF 서명 / 양식 작성기 iframe이 비어 있음)
브라우저 콘솔에 `.mjs` 요청에 대해 `Failed to load module script: ... non-JavaScript MIME type "application/octet-stream"`이 표시되면, BentoPDF nginx 앞단의 Ingress/Gateway 컨트롤러가 업스트림 `Content-Type` 헤더를 `application/octet-stream`으로 바꾸고 있는 것입니다.
BentoPDF 이미지의 nginx는 이미 `.mjs`를 `application/javascript`로 제공합니다. 해결 방법은 컨트롤러가 이를 제거하거나 바꾸지 못하게 하는 것입니다. nginx-ingress의 경우:
```yaml
ingress:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
location ~* \.mjs$ {
types {} default_type application/javascript;
}
```
또는 Gateway API의 `ResponseHeaderModifier` 필터를 사용해 `.mjs` 경로에 `Content-Type: application/javascript`를 설정할 수 있습니다. DevTools → Network 탭에서 확인하세요. 실패했던 `.mjs` 요청에 `content-type: application/javascript`가 표시되어야 합니다.
### Gateway API를 사용하며 헤더를 강제로 추가하려는 경우
Gateway API는 `ResponseHeaderModifier` 필터를 지원합니다. `httpRoute.rules[*].filters`에 연결할 수 있습니다.
```yaml
httpRoute:
enabled: true
hostnames: [pdf.example.com]
parentRefs:
- name: bento-tunnel
namespace: misc
sectionName: http
rules:
- matches:
- path: { type: PathPrefix, value: / }
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: Cross-Origin-Opener-Policy
value: same-origin
- name: Cross-Origin-Embedder-Policy
value: require-corp
```
특정 필터의 지원 여부는 Gateway 컨트롤러에 따라 다릅니다. 필터가 무시되면 엣지/컨트롤러 계층에서 헤더를 추가하세요.
## 특정 도구 비활성화
이미지를 다시 빌드하지 않고 ConfigMap으로 런타임에 도구를 비활성화합니다.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bentopdf-config
namespace: bentopdf
data:
config.json: |
{
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
}
```
서비스되는 디렉터리에 마운트합니다.
```yaml
spec:
containers:
- name: bentopdf
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/config.json
subPath: config.json
readOnly: true
volumes:
- name: config
configMap:
name: bentopdf-config
```
도구 ID는 `.html`을 뺀 페이지 URL입니다. 아무 도구나 열어 URL을 확인하세요(예: `edit-pdf`, `merge-pdf`, `compress-pdf`). 비활성화된 도구는 홈페이지, 검색, 단축키, 워크플로 빌더, 직접 URL 접근에서 숨겨집니다. 전체 옵션 목록은 [Docker 가이드](/ko/self-hosting/docker#특정-도구-비활성화)를 참고하세요.
+139
View File
@@ -0,0 +1,139 @@
# Netlify에 배포하기
[Netlify](https://netlify.com)는 넉넉한 무료 요금제와 함께 훌륭한 정적 사이트 호스팅을 제공합니다.
## 원클릭 배포
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/alam00000/bentopdf)
## 수동 배포
### 1단계: 저장소 연결
1. [Netlify](https://app.netlify.com)에 로그인합니다
2. "Add new site" → "Import an existing project"를 클릭합니다
3. GitHub 계정을 연결합니다
4. BentoPDF 포크를 선택합니다
### 2단계: 빌드 설정 구성
| 설정 | 값 |
| ----------------- | --------------- |
| Build command | `npm run build` |
| Publish directory | `dist` |
| Node version | 18+ |
### 3단계: 배포
"Deploy site"를 클릭하고 빌드가 끝날 때까지 기다립니다.
## 설정 파일
프로젝트 루트에 `netlify.toml`을 만듭니다.
```toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "18"
# Required security headers for SharedArrayBuffer (used by LibreOffice WASM)
[[headers]]
for = "/*"
[headers.values]
Cross-Origin-Embedder-Policy = "require-corp"
Cross-Origin-Opener-Policy = "same-origin"
Cross-Origin-Resource-Policy = "cross-origin"
# Pre-compressed LibreOffice WASM binary - must be served with Content-Encoding
[[headers]]
for = "/libreoffice-wasm/soffice.wasm.gz"
[headers.values]
Content-Type = "application/wasm"
Content-Encoding = "gzip"
Cache-Control = "public, max-age=31536000, immutable"
# Pre-compressed LibreOffice WASM data - must be served with Content-Encoding
[[headers]]
for = "/libreoffice-wasm/soffice.data.gz"
[headers.values]
Content-Type = "application/octet-stream"
Content-Encoding = "gzip"
Cache-Control = "public, max-age=31536000, immutable"
# Cache other WASM files
[[headers]]
for = "*.wasm"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
Content-Type = "application/wasm"
# SPA routing
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
```
::: warning 중요
Word/ODT/Excel/PowerPoint를 PDF로 변환하려면 `Cross-Origin-Embedder-Policy` 및 `Cross-Origin-Opener-Policy` 헤더가 필요합니다. 이 헤더가 없으면 `SharedArrayBuffer`를 사용할 수 없어 LibreOffice WASM 엔진이 초기화되지 못합니다.
`.wasm.gz` 및 `.data.gz` 파일의 `Content-Encoding: gzip` 헤더는 브라우저가 자동으로 압축을 풀도록 알려 줍니다. 이 헤더가 없으면 브라우저가 원시 gzip 바이트를 받아 WASM 컴파일이 실패합니다.
:::
## 환경 변수
Site settings → Environment variables에서 설정합니다.
| 변수 | 설명 |
| ----------------------- | --------------------------------------------------- |
| `SIMPLE_MODE` | 최소 빌드를 사용하려면 `true`로 설정 |
| `VITE_BRAND_NAME` | 사용자 지정 브랜드 이름 ("BentoPDF" 대체) |
| `VITE_BRAND_LOGO` | `public/` 기준 로고 경로 (예: `images/my-logo.svg`) |
| `VITE_FOOTER_TEXT` | 사용자 지정 바닥글/저작권 텍스트 |
| `VITE_DEFAULT_LANGUAGE` | 기본 UI 언어 (예: `fr`, `de`, `es`) |
## 사용자 지정 도메인
1. Site settings → Domain management로 이동합니다
2. "Add custom domain"을 클릭합니다
3. DNS 구성 안내를 따릅니다
## 대용량 미디어
큰 WASM 파일에는 [Netlify Large Media](https://docs.netlify.com/large-media/overview/) 사용을 고려해 보세요.
```bash
netlify lm:setup
git lfs track "*.wasm"
```
## 문제 해결
### Word/ODT/Excel을 PDF로 변환이 55%에서 멈춤
문서 변환이 55%에서 멈추면 DevTools 콘솔을 열고 다음을 확인하세요.
```js
console.log(window.crossOriginIsolated); // should be true
console.log(typeof SharedArrayBuffer); // should be "function"
```
`crossOriginIsolated`가 `false`이면 `netlify.toml`의 COEP/COOP 헤더가 적용되지 않은 것입니다. 파일이 프로젝트 루트에 있는지 확인하고 다시 배포하세요.
콘솔에 `expected magic word 00 61 73 6d, found 1f 8b 08 08`이 표시되면 `.wasm.gz` 파일에 `Content-Encoding: gzip` 헤더가 빠진 것입니다. `soffice.wasm.gz`와 `soffice.data.gz`용 `[[headers]]` 블록이 `netlify.toml`에 있는지 확인하세요.
### 빌드 실패
Node 버전 호환성을 확인하세요.
```toml
[build.environment]
NODE_VERSION = "20"
```
### 첫 로딩이 느림
Site settings → Build & deploy → Asset optimization에서 에셋 최적화를 활성화하세요.
+232
View File
@@ -0,0 +1,232 @@
# Nginx로 배포하기
Nginx를 사용해 자체 서버에서 BentoPDF를 호스팅합니다.
## 사전 요구 사항
- Ubuntu/Debian 서버
- Nginx 설치
- SSL 인증서 (권장: Let's Encrypt)
## 1단계: 프로젝트 빌드
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
브랜딩을 사용자 지정하려면 빌드하기 전에 환경 변수를 설정합니다.
```bash
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
```
## 2단계: 파일 복사
```bash
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
```
## 3단계: Nginx 설정
`/etc/nginx/sites-available/bentopdf`를 만듭니다.
```nginx
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
root /var/www/bentopdf;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/wasm;
gzip_min_length 1000;
# WASM MIME type
types {
application/wasm wasm;
}
# Required headers for SharedArrayBuffer (LibreOffice WASM)
# These must be on every response - especially HTML pages
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Pre-compressed LibreOffice WASM binary
location ~* /libreoffice-wasm/soffice\.wasm\.gz$ {
gzip off;
types {} default_type application/wasm;
add_header Content-Encoding gzip;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# Pre-compressed LibreOffice WASM data
location ~* /libreoffice-wasm/soffice\.data\.gz$ {
gzip off;
types {} default_type application/octet-stream;
add_header Content-Encoding gzip;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|wasm)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
}
```
## 4단계: 사이트 활성화
```bash
sudo ln -s /etc/nginx/sites-available/bentopdf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
## 5단계: Let's Encrypt로 SSL 설정
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
```
## 하위 디렉터리 배포
`/pdf/`에 호스팅하려면:
```nginx
location /pdf/ {
alias /var/www/bentopdf/;
try_files $uri $uri/ /pdf/index.html;
}
```
다음과 같이 빌드합니다.
```bash
BASE_URL=/pdf/ npm run build
```
## 성능 튜닝
`nginx.conf`에 추가합니다.
```nginx
http {
# Enable sendfile
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Increase buffer sizes
client_max_body_size 100M;
# Worker connections
worker_connections 2048;
}
```
## 문제 해결
### WASM이 로드되지 않음
MIME 타입이 설정되어 있는지 확인합니다.
```nginx
types {
application/wasm wasm;
}
```
### PDF 서명 또는 양식 작성기에 빈 뷰어가 표시됨 (`.mjs` MIME 오류)
브라우저 콘솔에 다음이 표시되는 경우:
```
Failed to load module script: The server responded with a non-JavaScript
MIME type of "application/octet-stream". Strict MIME type checking is
enforced for module scripts per HTML spec.
```
...그리고 실패한 요청이 `.mjs`로 끝난다면, nginx에 `.mjs → application/javascript` 매핑이 빠져 있는 것입니다. 내장 PDF 뷰어(PDF 서명, 양식 작성기 및 일부 다른 도구에서 사용)는 `.mjs` 확장자의 ES 모듈 파일을 포함하는데, nginx의 기본 `mime.types`에는 이 확장자가 들어 있지 않습니다.
`/etc/nginx/mime.types`의 기존 `application/javascript` 줄에 `mjs`를 추가해 해결합니다.
```nginx
application/javascript js mjs;
```
> `application/javascript` 줄을 _두 번째로_ 추가하지 마세요. nginx는 `mime.types`를 한 줄씩 파싱하며 타입과 확장자의 매핑은 하나만 적용되므로, 일부 버전에서는 두 번째 줄이 조용히 무시됩니다. 기존 줄을 수정해 `js` 옆에 `mjs`를 추가하세요.
또는 server 블록 안에서 로컬로 재정의할 수 있습니다(전역 파일 수정 불필요).
```nginx
types {
application/javascript js mjs;
application/wasm wasm;
}
```
변경 후 nginx를 다시 로드합니다.
```bash
sudo nginx -t && sudo systemctl reload nginx
```
확인하려면 브라우저 DevTools → Network 탭에서 실패했던 `.mjs` 요청을 찾아 `Content-Type` 응답 헤더를 확인하세요. 이제 `application/octet-stream`이 아니라 `application/javascript`여야 합니다.
### Word/ODT/Excel을 PDF로 변환이 작동하지 않음
LibreOffice WASM에는 `SharedArrayBuffer`가 필요하며, 이를 위해 `Cross-Origin-Embedder-Policy` 및 `Cross-Origin-Opener-Policy` 헤더가 필요합니다. 보안 컨텍스트도 필요하므로 `http://localhost`는 로컬 테스트에서 작동하지만 `http://192.168.x.x` 같은 LAN IP는 보통 HTTPS가 필요합니다. `location` 블록의 nginx `add_header` 지시문은 server 수준 `add_header` 지시문을 병합하지 않고 **덮어씁니다**. 자체 `add_header`가 있는 모든 `location` 블록에는 COEP/COOP 헤더가 포함되어야 합니다.
다음으로 확인합니다.
```bash
curl -I https://your-domain.com/ | grep -i cross-origin
```
nginx 앞에 리버스 프록시를 사용하는 경우 이 헤더가 유지되는지 확인하세요.
### 502 Bad Gateway
Nginx 오류 로그를 확인합니다.
```bash
sudo tail -f /var/log/nginx/error.log
```
+122
View File
@@ -0,0 +1,122 @@
# Vercel에 배포하기
[Vercel](https://vercel.com)은 정적 사이트를 가장 쉽게 배포할 수 있는 환경을 제공합니다.
## 원클릭 배포
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/alam00000/bentopdf)
## 수동 배포
### 1단계: 저장소 포크
[bentopdf/bentopdf](https://github.com/alam00000/bentopdf)를 GitHub 계정으로 포크합니다.
### 2단계: Vercel로 가져오기
1. [vercel.com/new](https://vercel.com/new)로 이동합니다
2. 포크한 저장소를 선택합니다
3. 프로젝트를 구성합니다.
| 설정 | 값 |
| ---------------- | --------------- |
| Framework Preset | Vite |
| Build Command | `npm run build` |
| Output Directory | `dist` |
| Install Command | `npm install` |
### 3단계: 환경 변수 (선택 사항)
필요한 경우 다음을 추가합니다.
| 변수 | 설명 |
| ----------------------- | --------------------------------------------------- |
| `SIMPLE_MODE` | 최소 UI를 사용하려면 `true`로 설정 |
| `VITE_BRAND_NAME` | 사용자 지정 브랜드 이름 ("BentoPDF" 대체) |
| `VITE_BRAND_LOGO` | `public/` 기준 로고 경로 (예: `images/my-logo.svg`) |
| `VITE_FOOTER_TEXT` | 사용자 지정 바닥글/저작권 텍스트 |
| `VITE_DEFAULT_LANGUAGE` | 기본 UI 언어 (예: `fr`, `de`, `es`) |
### 4단계: 배포
"Deploy"를 클릭하고 빌드가 끝날 때까지 기다립니다.
## 사용자 지정 도메인
1. 프로젝트 설정으로 이동합니다
2. "Domains"로 이동합니다
3. 사용자 지정 도메인을 추가합니다
4. 안내에 따라 DNS를 구성합니다
## 제한 사항
::: warning 대용량 파일
Vercel의 서버리스 함수에는 50MB 제한이 있습니다. BentoPDF는 정적 사이트이므로 영향을 받지 않아야 하지만, WASM 모듈은 용량이 큽니다(총 약 100MB). `/public` 폴더에서 제공되도록 하세요.
:::
## 문제 해결
### 빌드 시간 초과
빌드 시간이 초과되면 다음을 시도해 보세요.
```json
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": "dist"
}
```
### 새로고침 시 404
SPA 라우팅을 위해 `vercel.json`을 추가합니다.
```json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
```
### Word/ODT/Excel을 PDF로 변환이 작동하지 않음
LibreOffice WASM 변환에는 `SharedArrayBuffer`가 필요하며, 이를 위해 모든 페이지에 다음 응답 헤더가 있어야 합니다.
- `Cross-Origin-Embedder-Policy: require-corp`
- `Cross-Origin-Opener-Policy: same-origin`
미리 압축된 `.wasm.gz` 및 `.data.gz` 파일도 브라우저가 압축을 풀 수 있도록 `Content-Encoding: gzip`이 필요합니다.
`vercel.json`에 다음을 추가합니다.
```json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" }
]
},
{
"source": "/libreoffice-wasm/soffice.wasm.gz",
"headers": [
{ "key": "Content-Type", "value": "application/wasm" },
{ "key": "Content-Encoding", "value": "gzip" }
]
},
{
"source": "/libreoffice-wasm/soffice.data.gz",
"headers": [
{ "key": "Content-Type", "value": "application/octet-stream" },
{ "key": "Content-Encoding", "value": "gzip" }
]
}
]
}
```
확인하려면 DevTools 콘솔을 열고 `console.log(window.crossOriginIsolated)`를 실행하세요. `true`가 반환되어야 합니다.