Exam Center (exam.minsday.com) 를 새 서버에 배포하는 방법
브라우저 요청부터 PDF 자동 채점 생성까지의 전체 경로입니다.
/var/www/exam/var/www/exam-gen/venv/add.html 에서 PDF를 올렸을 때 내부적으로 벌어지는 일입니다.
POST /exam-api/generate 로 PDF(들)를 multipart/form-data로 전송location /exam-api/ 규칙에 따라 127.0.0.1:3074의 Node 서버로 프록시server.js)가 파일을 검증(PDF 매직바이트, 크기 제한)하고 임시 작업 폴더 생성venv/bin/python3 worker.py <examPdf> <outDir> [answerPdf] 를 자식 프로세스로 실행
manifest.json + assets/*.png 로 저장venv/bin/python3 generate.py <outDir> <slug> 로 manifest → 최종 index.html 렌더링/var/www/exam/<slug>/ 로 복사/var/www/exam/index.html 의 <!--AUTO-CARDS--> 마커 위치에 새 카드 삽입{ok:true, url:"/<slug>/index.html"} 응답 → 새 퀴즈로 이동Ubuntu 22.04/24.04 기준. 아래 순서대로 진행하면 됩니다.
배포할 서브도메인의 A(및 AAAA) 레코드를 새 서버의 IP로 지정합니다. 인증서 발급 전에 전파가 완료되어야 합니다.
apt-get update
apt-get install -y nginx certbot python3-certbot-nginx \
python3 python3-venv python3-pip tesseract-ocr nodejs npm
npm install -g pm2
# 예시: exam.example.com
certbot certonly --nginx -d exam.example.com
mkdir -p /var/www/exam
# 로컬에서 소스 전체(index.html, add.html, publish/, 23a.html, 23b.html, assets/ 등)를 복사
scp -r ./exam_site/* root@서버IP:/var/www/exam/
mkdir -p /var/www/exam-gen
scp -r ./exam_gen/{worker.py,generate.py,server.js,package.json} root@서버IP:/var/www/exam-gen/
cd /var/www/exam-gen
python3 -m venv venv
venv/bin/pip install pymupdf pillow pytesseract numpy opencv-python-headless
npm install --omit=dev
pip install 하지 마세요. 다른 서비스가 쓰는 numpy/scipy 등과 버전이 충돌할 수 있습니다. 반드시 위처럼 격리된 venv를 사용하세요.server.js 상단의 상수를 새 서버에 맞게 확인/수정합니다:
| 상수 | 기본값 | 의미 |
|---|---|---|
EXAM_ROOT | /var/www/exam | 정적 사이트 루트 (nginx root와 동일해야 함) |
PORT | 3074 | Node 서버 내부 포트. 이미 사용 중이면 다른 값으로 변경 후 nginx proxy_pass도 함께 수정 |
cd /var/www/exam-gen
pm2 start server.js --name exam-gen
pm2 save
pm2 startup # 안내되는 명령어를 그대로 한 번 더 실행하면 재부팅 후에도 자동 시작
/etc/nginx/conf.d/exam.example.com.conf 생성:
server {
listen 80;
listen [::]:80;
server_name exam.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name exam.example.com;
ssl_certificate /etc/letsencrypt/live/exam.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/exam.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/exam;
index index.html;
client_max_body_size 200M;
location /exam-api/ {
proxy_pass http://127.0.0.1:3074/exam-api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
client_max_body_size 60M;
}
location / {
try_files $uri $uri/ =404;
}
}
nginx -t && systemctl reload nginx
curl -I https://exam.example.com/
curl -F "examPdf=@sample.pdf" https://exam.example.com/exam-api/generate
/var/www/exam/ ← nginx root (정적 사이트)
├── index.html ← 목록 페이지 (<!--AUTO-CARDS--> 마커 포함)
├── add.html ← PDF 업로드 UI
├── publish/index.html ← 이 페이지
├── 23a.html / 23b.html ← 수동 제작 퀴즈 (기준 품질)
├── assets/23a/, assets/23b/ ← 수동 제작 퀴즈용 크롭 이미지
└── <auto-generated-slug>/ ← add.html로 생성된 퀴즈들
├── index.html
└── assets/*.png
/var/www/exam-gen/ ← 생성 백엔드 (웹에 직접 노출 안 됨)
├── server.js ← Express 업로드 API (PM2: "exam-gen")
├── worker.py ← PDF → OCR/CV → manifest.json
├── generate.py ← manifest.json → 퀴즈 index.html
├── venv/ ← 격리된 Python 환경
├── work/ ← 생성 중 임시 작업 폴더
└── uploads/ ← multer 임시 업로드 폴더