mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-12 01:33:36 +01:00
feat: translate all pages, UI messages and docs and fix SEO redirects and indexing
This commit is contained in:
1 parent
5e019de5ae
commit
4edb1718cc
3344 files changed
+415643
-4759
No files matched your search
@@ -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
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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)를 참고하세요.
|
||||
@@ -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
|
||||
- 전체 체인을 직접 조립한 인증서
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 파일을 따로 업로드하세요.
|
||||
@@ -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는 정적 사이트이므로 데이터베이스나 백엔드 서버가 필요 없습니다!
|
||||
:::
|
||||
@@ -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#특정-도구-비활성화)를 참고하세요.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Netlify에 배포하기
|
||||
|
||||
[Netlify](https://netlify.com)는 넉넉한 무료 요금제와 함께 훌륭한 정적 사이트 호스팅을 제공합니다.
|
||||
|
||||
## 원클릭 배포
|
||||
|
||||
[](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에서 에셋 최적화를 활성화하세요.
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
# Vercel에 배포하기
|
||||
|
||||
[Vercel](https://vercel.com)은 정적 사이트를 가장 쉽게 배포할 수 있는 환경을 제공합니다.
|
||||
|
||||
## 원클릭 배포
|
||||
|
||||
[](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`가 반환되어야 합니다.
|
||||
Reference in new issue
Block a user