배포 가이드 & 아키텍처

Exam Center (exam.minsday.com) 를 새 서버에 배포하는 방법

1 전체 구성도

브라우저 요청부터 PDF 자동 채점 생성까지의 전체 경로입니다.

사용자 브라우저
exam.minsday.com
↓ HTTPS (443)
nginx
Let's Encrypt 인증서 · reverse proxy
location /
정적 파일 서빙
/var/www/exam
location /exam-api/
proxy_pass
127.0.0.1:3074
↓ (업로드 시)
PM2 process: exam-gen
Node.js / Express
server.js — multer 업로드 처리, 작업 큐
worker.py
PyMuPDF + Tesseract OCR
+ OpenCV 컴퓨터비전
generate.py
manifest.json →
퀴즈 index.html 렌더링
둘 다 격리된 venv에서 실행 · /var/www/exam-gen/venv
↓ 생성물 저장 + index.html 카드 자동 추가
/var/www/exam/<slug>/
새 퀴즈 페이지 + assets/

2 요청 흐름 — "새 시험 PDF 업로드"

/add.html 에서 PDF를 올렸을 때 내부적으로 벌어지는 일입니다.

  1. 브라우저가 POST /exam-api/generate 로 PDF(들)를 multipart/form-data로 전송
  2. nginx가 location /exam-api/ 규칙에 따라 127.0.0.1:3074의 Node 서버로 프록시
  3. Node(server.js)가 파일을 검증(PDF 매직바이트, 크기 제한)하고 임시 작업 폴더 생성
  4. venv/bin/python3 worker.py <examPdf> <outDir> [answerPdf] 를 자식 프로세스로 실행
    • PDF 페이지를 3배율 PNG로 렌더링 (PyMuPDF)
    • Tesseract OCR로 "1.1", "2.3" 같은 문항 번호를 찾아 페이지를 구간별로 분할
    • 인식된 텍스트를 지운 뒤 남은 "잉크"를 OpenCV 연결요소 분석으로 묶어 악보/그림 자동 크롭
    • TRUE/FALSE, 빈칸(…) 패턴을 감지해 입력 위젯 타입 결정
    • 정답지 PDF가 있으면 텍스트 레이어 우선 추출, 없으면(스캔본) OCR로 대체 추출
    • 결과를 manifest.json + assets/*.png 로 저장
  5. venv/bin/python3 generate.py <outDir> <slug> 로 manifest → 최종 index.html 렌더링
  6. 결과 폴더를 /var/www/exam/<slug>/ 로 복사
  7. /var/www/exam/index.html<!--AUTO-CARDS--> 마커 위치에 새 카드 삽입
  8. 브라우저에 {ok:true, url:"/<slug>/index.html"} 응답 → 새 퀴즈로 이동

3 새 서버에 배포하기

Ubuntu 22.04/24.04 기준. 아래 순서대로 진행하면 됩니다.

3.1 DNS

배포할 서브도메인의 A(및 AAAA) 레코드를 새 서버의 IP로 지정합니다. 인증서 발급 전에 전파가 완료되어야 합니다.

3.2 시스템 패키지 설치

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

3.3 TLS 인증서 발급

# 예시: exam.example.com
certbot certonly --nginx -d exam.example.com

3.4 정적 사이트 배포

mkdir -p /var/www/exam
# 로컬에서 소스 전체(index.html, add.html, publish/, 23a.html, 23b.html, assets/ 등)를 복사
scp -r ./exam_site/* root@서버IP:/var/www/exam/

3.5 생성 백엔드(exam-gen) 배포

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를 사용하세요.

3.6 설정값 확인

server.js 상단의 상수를 새 서버에 맞게 확인/수정합니다:

상수기본값의미
EXAM_ROOT/var/www/exam정적 사이트 루트 (nginx root와 동일해야 함)
PORT3074Node 서버 내부 포트. 이미 사용 중이면 다른 값으로 변경 후 nginx proxy_pass도 함께 수정

3.7 PM2로 프로세스 등록

cd /var/www/exam-gen
pm2 start server.js --name exam-gen
pm2 save
pm2 startup   # 안내되는 명령어를 그대로 한 번 더 실행하면 재부팅 후에도 자동 시작

3.8 nginx 가상호스트

/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

3.9 동작 확인

curl -I https://exam.example.com/
curl -F "examPdf=@sample.pdf" https://exam.example.com/exam-api/generate

4 디렉터리 구조

/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 임시 업로드 폴더

5 알려진 한계