# ZKProofID — Node.js 백엔드 서버 (PostgreSQL 16)

> **Aleo 블록체인 ZKP 기반 연령 인증 SaaS 플랫폼**  
> Express 4 + PostgreSQL 16 + JWT + PASS 본인인증

---

## 📋 목차

1. [아키텍처 개요](#아키텍처-개요)
2. [MySQL vs PostgreSQL 변환 요약](#mysql-vs-postgresql-변환-요약)
3. [로컬 개발 환경 설정](#로컬-개발-환경-설정)
4. [PostgreSQL 호스팅 옵션](#postgresql-호스팅-옵션)
5. [API 엔드포인트 레퍼런스](#api-엔드포인트-레퍼런스)
6. [배포 가이드 (Supabase)](#배포-가이드-supabase)
7. [배포 가이드 (Neon)](#배포-가이드-neon)
8. [배포 가이드 (AWS RDS)](#배포-가이드-aws-rds)
9. [보안 체크리스트](#보안-체크리스트)
10. [트러블슈팅](#트러블슈팅)

---

## 아키텍처 개요

```
프론트엔드 (Genspark 정적 호스팅)
    www.zkpage.store
         │  HTTPS
         ▼
   Node.js 백엔드 (Express 4)
   ─────────────────────────────
   server.js            메인 서버
   ├─ routes/auth.js    PASS 본인인증, JWT
   ├─ routes/zkp.js     ★ 서버 ZKP 생성 (거짓 입력 차단)
   ├─ routes/verify.js  QR 검증 + 로그
   ├─ routes/merchant.js 가맹점 관리
   └─ routes/admin.js   KPI 대시보드
         │
         ▼
   PostgreSQL 16
   ─────────────────────────────
   users             사용자 (생년월일만)
   refresh_tokens    JWT 리프레시 토큰
   merchants         가맹점
   verify_logs       QR 검증 이력
   subscriptions     구독/결제
   pass_auth_logs    인증 감사 로그
```

---

## MySQL vs PostgreSQL 변환 요약

| 항목 | MySQL | PostgreSQL 16 |
|------|-------|---------------|
| **파라미터** | `?` | `$1, $2, $3` |
| **결과 접근** | `const [rows] = await db.execute()` | `const res = await db.query()` / `res.rows` |
| **UUID** | `CHAR(36) DEFAULT (UUID())` | `UUID DEFAULT gen_random_uuid()` |
| **Boolean** | `TINYINT(1)` / `1`/`0` | `BOOLEAN` / `TRUE`/`FALSE` |
| **날짜** | `DATETIME` | `TIMESTAMPTZ` |
| **오늘 날짜** | `CURDATE()` | `CURRENT_DATE` |
| **날짜 캐스팅** | `DATE(col)` | `col::date` |
| **연도 추출** | `YEAR(col)` | `EXTRACT(YEAR FROM col)` |
| **월 추출** | `MONTH(col)` | `EXTRACT(MONTH FROM col)` |
| **날짜 빼기** | `DATE_SUB(NOW(), INTERVAL 14 DAY)` | `NOW() - INTERVAL '14 days'` |
| **Boolean 합산** | `SUM(passed)` | `SUM(CASE WHEN passed THEN 1 ELSE 0 END)` |
| **자동증가** | `AUTO_INCREMENT` | `SERIAL` 또는 `gen_random_uuid()` |
| **중복 무시 INSERT** | `INSERT IGNORE` | `ON CONFLICT DO NOTHING` |
| **Unique 위반 코드** | `ER_DUP_ENTRY` | `'23505'` |
| **updated_at 자동** | `ON UPDATE NOW()` | 트리거 `set_updated_at()` |
| **인덱스 선언** | 테이블 내 `INDEX idx_name` | `CREATE INDEX IF NOT EXISTS` (별도) |
| **엔진** | `ENGINE=InnoDB` | 없음 (PostgreSQL 기본) |
| **드라이버** | `mysql2/promise` | `pg` (node-postgres) |
| **SSL** | `charset: utf8mb4` | `ssl: { rejectUnauthorized: false }` |

---

## 로컬 개발 환경 설정

### 1. PostgreSQL 설치 (로컬)

```bash
# macOS
brew install postgresql@16
brew services start postgresql@16

# Ubuntu / Debian
sudo apt install postgresql-16
sudo systemctl start postgresql

# Windows
# https://www.postgresql.org/download/windows/ 에서 설치 파일 다운로드
```

### 2. DB 생성

```bash
# psql 접속
psql -U postgres

# DB 및 사용자 생성
CREATE DATABASE zkproofid;
CREATE USER zkproofid WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE zkproofid TO zkproofid;
\q
```

### 3. 패키지 설치

```bash
cd server
npm install
```

> ✅ `package.json` 에서 `mysql2` 제거, `pg@8.11.3` 추가됨

### 4. 환경 변수 설정

```bash
cp .env.example .env
# .env 파일 열어 DB_PASSWORD, JWT_SECRET, PHONE_SALT 변경
```

```env
DB_HOST=localhost
DB_PORT=5432
DB_USER=zkproofid
DB_PASSWORD=your_password
DB_NAME=zkproofid
DB_SSL=false
```

### 5. 스키마 초기화

```bash
node db/init.js
# → 6개 테이블 생성 + 테스트 가맹점 STORE-001 삽입
```

### 6. 서버 실행

```bash
# 개발 (nodemon 자동재시작)
npm run dev

# 운영
npm start
```

### 7. 동작 확인

```bash
curl http://localhost:3000/api/zkp/tiers
# → {"success":true,"tiers":[...]}

curl -X POST http://localhost:3000/api/auth/merchant/login \
  -H "Content-Type: application/json" \
  -d '{"merchant_code":"STORE-001","password":"admin1234"}'
# → {"success":true,"accessToken":"eyJ..."}
```

---

## PostgreSQL 호스팅 옵션

| 서비스 | 무료 티어 | 특징 | 추천 용도 |
|--------|----------|------|----------|
| **Supabase** | 500MB, 2 프로젝트 | 관리 UI, REST API, Auth | 스타트업/개인 |
| **Neon** | 0.5GB, 분기 기능 | 서버리스, 자동 스케일 | 개발/테스트 |
| **Railway** | $5 크레딧/월 | PostgreSQL + Node.js 동시 배포 | 풀스택 배포 |
| **AWS RDS** | 750h/월 (t3.micro) | 엔터프라이즈급 | 운영 서비스 |
| **Render** | 90일 후 삭제 | 간단한 배포 | 데모 |

---

## 배포 가이드 (Supabase)

Supabase는 PostgreSQL 기반 BaaS로, 무료 500MB를 제공합니다.

### 1. 프로젝트 생성

1. [https://supabase.com](https://supabase.com) → New Project
2. **Region**: Northeast Asia (ap-northeast-1)
3. DB 비밀번호 생성 후 저장

### 2. 스키마 적용

1. Supabase 대시보드 → **SQL Editor** → New query
2. `server/db/schema.sql` 내용을 붙여넣기
3. **Run** 실행 → 6개 테이블 생성 확인

### 3. 연결 정보 확인

- **Settings** → **Database** → **Connection string** (URI 탭)
- 예: `postgresql://postgres:[PASSWORD]@db.xxxx.supabase.co:5432/postgres`

### 4. 서버 .env 설정

```env
# Supabase 연결 문자열 (방법 B)
DATABASE_URL=postgresql://postgres:PASSWORD@db.xxxx.supabase.co:5432/postgres
DB_SSL=true

# 또는 개별 변수 (방법 A)
DB_HOST=db.xxxx.supabase.co
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=YOUR_SUPABASE_DB_PASSWORD
DB_NAME=postgres
DB_SSL=true
```

### 5. Node.js 서버 배포 (Railway)

```bash
# Railway CLI
npm install -g @railway/cli
railway login
railway init
railway up

# 환경 변수 설정
railway variables set DATABASE_URL=postgresql://...
railway variables set JWT_SECRET=...
railway variables set PHONE_SALT=...
```

---

## 배포 가이드 (Neon)

Neon은 서버리스 PostgreSQL로 분기(branch) 기능이 특징입니다.

### 1. 프로젝트 생성

1. [https://neon.tech](https://neon.tech) → New Project
2. **Region**: ap-southeast-1 (Singapore) 또는 us-east-1

### 2. 연결 문자열

- 대시보드 → **Connection Details** → **Connection string**
- 예: `postgresql://zkproofid:PASSWORD@ep-xxx.ap-southeast-1.aws.neon.tech/neondb?sslmode=require`

### 3. 스키마 적용

```bash
# Neon 콘솔 SQL 에디터 또는 로컬에서:
psql "postgresql://..." -f server/db/schema.sql
```

### 4. .env 설정

```env
DATABASE_URL=postgresql://zkproofid:PASSWORD@ep-xxx.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
DB_SSL=true
```

---

## 배포 가이드 (AWS RDS)

### 1. RDS PostgreSQL 16 인스턴스 생성

```
- 엔진: PostgreSQL 16
- 인스턴스: db.t3.micro (프리 티어)
- 스토리지: 20GB gp2
- 퍼블릭 액세스: No (VPC 내부)
- 보안 그룹: EC2에서 5432 인바운드 허용
```

### 2. 스키마 적용 (EC2에서)

```bash
psql -h your-rds-endpoint.rds.amazonaws.com \
     -U zkproofid -d zkproofid \
     -f schema.sql
```

### 3. .env 설정

```env
DB_HOST=your-rds-endpoint.rds.amazonaws.com
DB_PORT=5432
DB_USER=zkproofid
DB_PASSWORD=YOUR_RDS_PASSWORD
DB_NAME=zkproofid
DB_SSL=true
```

---

## API 엔드포인트 레퍼런스

### 인증 (auth)

| 메서드 | 경로 | 설명 | 인증 |
|--------|------|------|------|
| POST | `/api/auth/pass/request` | PASS 본인인증 시작 | 없음 |
| POST | `/api/auth/pass/callback` | NICE 서버 콜백 | 없음 |
| POST | `/api/auth/pass/complete` | 인증 완료 + JWT 발급 | 없음 |
| POST | `/api/auth/merchant/login` | 가맹점 로그인 | 없음 |
| POST | `/api/auth/token/refresh` | 토큰 갱신 | 없음 |
| POST | `/api/auth/logout` | 로그아웃 | JWT |
| GET  | `/api/auth/me` | 내 정보 | JWT |

### ZKP 생성 (zkp)

| 메서드 | 경로 | 설명 | 인증 |
|--------|------|------|------|
| POST | `/api/zkp/generate` | ★ ZKP + QR 생성 (서버 생년월일 사용) | JWT |
| GET  | `/api/zkp/tiers` | 등급 목록 | 없음 |

### QR 검증 (verify)

| 메서드 | 경로 | 설명 | 인증 |
|--------|------|------|------|
| POST | `/api/verify/qr` | QR 검증 + 로그 | API Key 또는 JWT |
| GET  | `/api/verify/logs` | 검증 이력 | 가맹점 JWT |
| GET  | `/api/verify/stats` | 통계 | 가맹점 JWT |

### 가맹점 (merchant)

| 메서드 | 경로 | 설명 | 인증 |
|--------|------|------|------|
| POST | `/api/merchant/register` | 신규 등록 | 없음 |
| GET  | `/api/merchant/me` | 내 정보 | 가맹점 JWT |
| PUT  | `/api/merchant/me` | 정보 수정 | 가맹점 JWT |
| GET  | `/api/merchant/api-key` | API 키 조회 | 가맹점 JWT |
| POST | `/api/merchant/api-key/reset` | API 키 재발급 | 가맹점 JWT |
| GET  | `/api/merchant/subscription` | 구독 정보 | 가맹점 JWT |
| GET  | `/api/merchant/list` | 전체 목록 | 관리자 JWT |

### 관리자 (admin)

| 메서드 | 경로 | 설명 | 인증 |
|--------|------|------|------|
| GET | `/api/admin/stats` | KPI 통계 | 관리자 JWT |
| GET | `/api/admin/verify-logs` | 전체 검증 이력 | 관리자 JWT |
| GET | `/api/admin/merchants` | 가맹점 목록+통계 | 관리자 JWT |
| PUT | `/api/admin/merchant/:id` | 플랜/상태 변경 | 관리자 JWT |

---

## 보안 체크리스트

- [ ] `.env` 파일이 `.gitignore`에 포함되어 있는지 확인
- [ ] `JWT_SECRET` 64자 이상 랜덤 문자열로 변경
- [ ] `PHONE_SALT` 32자 이상 랜덤 문자열로 변경
- [ ] `ADMIN_PASSWORD` 강력한 비밀번호로 변경
- [ ] 테스트 가맹점 `STORE-001` 비밀번호 변경 (admin1234 → 변경)
- [ ] HTTPS (SSL/TLS) 적용 — Nginx reverse proxy + Let's Encrypt
- [ ] `DB_SSL=true` 설정 (Supabase/Neon/RDS 필수)
- [ ] Rate Limiting 운영 환경에 맞게 조정
- [ ] CORS `ALLOWED_ORIGINS` 실제 도메인만 허용
- [ ] `NODE_ENV=production` 설정
- [ ] PostgreSQL 사용자 권한 최소화 (SELECT/INSERT/UPDATE/DELETE만)

### PostgreSQL 최소 권한 설정

```sql
-- 전용 사용자 생성
CREATE USER zkproofid_app WITH PASSWORD 'strong_password';

-- 필요한 권한만 부여
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO zkproofid_app;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO zkproofid_app;

-- 슈퍼유저 권한 없음 (초기화는 postgres 계정으로)
```

---

## 트러블슈팅

### 연결 오류

```
[DB] PostgreSQL 연결 실패 ❌ connect ECONNREFUSED 127.0.0.1:5432
```
→ PostgreSQL 서버가 실행 중인지 확인: `pg_isready -h localhost -p 5432`

```
[DB] PostgreSQL 연결 실패 ❌ password authentication failed
```
→ `.env`의 `DB_PASSWORD` 확인, 사용자 비밀번호 재설정

```
SSL SYSCALL error: EOF detected
```
→ Supabase/Neon/RDS 사용 시 `DB_SSL=true` 설정 필요

### $n 파라미터 오류

```
Error: bind message supplies X parameters, but prepared statement requires Y
```
→ SQL의 `$1,$2...` 개수와 `params` 배열 길이가 일치하는지 확인

### BOOLEAN 오류

```
invalid input syntax for type boolean: "1"
```
→ PostgreSQL BOOLEAN에는 `true`/`false` 사용 (숫자 `1`/`0` 불가)

### UUID 오류

```
invalid input syntax for type uuid
```
→ `uuidv4()` 형식이 `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`인지 확인

### 날짜 함수 오류

```
function curdate() does not exist
```
→ `CURDATE()` → `CURRENT_DATE`로 변경 (이미 변환 완료)

---

## 파일 구조

```
server/
├── server.js              메인 Express 서버
├── package.json           의존성 (pg 8.11.3)
├── .env.example           환경 변수 템플릿
├── .gitignore
│
├── db/
│   ├── schema.sql         ★ PostgreSQL 16 스키마
│   ├── connection.js      ★ pg.Pool 연결
│   └── init.js            ★ 스키마 자동 실행
│
├── middleware/
│   └── auth.js            JWT 미들웨어 (변경 없음)
│
└── routes/
    ├── auth.js            ★ PASS 인증 (PostgreSQL)
    ├── zkp.js             ★ ZKP 생성 (PostgreSQL)
    ├── verify.js          ★ QR 검증 (PostgreSQL)
    ├── merchant.js        ★ 가맹점 (PostgreSQL)
    └── admin.js           ★ 관리자 (PostgreSQL)
```

---

*ZKProofID Backend — PostgreSQL 16 Edition*  
*© 2024 ZKProofID | www.zkpage.store*
