chore(deploy): postinstall, standalone, PM2 ecosystem, README — QA-6.C2 C4 M1 M2

backend/package.json
- Add 'postinstall: prisma generate' — fresh pnpm install now produces
  a usable @prisma/client without a manual step.
- Add engines.node >= 20 and packageManager pnpm@9.12.0.

frontend/package.json
- Same engines + packageManager pin.

frontend/next.config.js
- output: 'standalone' enabled for PM2's .next/standalone/server.js
  pattern (reduces production bundle size, removes runtime node_modules
  dependency for the web process).

frontend/.env.example (new)
- Documents NEXT_PUBLIC_API_URL as a BUILD-TIME variable.

ecosystem.config.js (new)
- PM2 config for reloop-api (cwd=backend, script=dist/main.js, port
  3001) and reloop-web (cwd=frontend, script=.next/standalone/server.js,
  port 3000, HOSTNAME=0.0.0.0). max_memory_restart=512M each with
  separate error/out log files.

README.md (new)
- Root deploy guide: stack, env vars table (backend + frontend),
  local dev quickstart, prod deploy steps (rsync → pnpm install
  --frozen-lockfile → prisma migrate deploy → build → copy .next/static
  and public into standalone → pm2 start), reverse proxy mapping,
  pm2 ops. Also covers Cluster F items: 'Dev VM 에서 prisma migrate
  dev --name init' for migrations and 'pnpm install' for lockfile
  generation.

.gitignore
- Ignore .claude/state/session* + .claude/state/*.lock +
  .claude/sessions/ (ephemeral harness runtime metadata).
This commit is contained in:
reloop
2026-04-12 00:09:40 +09:00
parent e0974b8647
commit 3d7866be7e
7 changed files with 169 additions and 3 deletions

5
.gitignore vendored
View File

@@ -12,3 +12,8 @@ backend/node_modules
frontend/node_modules
frontend/.next
backend/dist
# harness runtime metadata (state is ephemeral; contracts/plans are committed)
.claude/state/session*
.claude/state/*.lock
.claude/sessions/

101
README.md Normal file
View File

@@ -0,0 +1,101 @@
# ReLoop v2
적응형 복습 스케줄링 기반 수능 학습 최적화 플랫폼.
페르소나 망각 곡선(persona-based forgetting curve)으로 개인화된 복습 간격을 산출한다.
## Stack
- **Backend**: NestJS 10 + Prisma 5 + MariaDB, JWT 인증
- **Frontend**: Next.js 14 (App Router) + React 18 + styled-components 6
- **Deploy**: PM2 on Debian VM, nginx/Caddy reverse proxy
## Environment variables
### Backend (`backend/.env`)
| Var | Required | Default | Description |
|---|---|---|---|
| `DATABASE_URL` | ✓ | — | `mysql://user:pass@host:3306/reloop` |
| `JWT_SECRET` | ✓ | — | long random string; boot fails without it |
| `JWT_EXPIRES_IN` | — | `30d` | access token TTL |
| `CORS_ORIGINS` | — | `https://reloop.nabomhalang.co.kr,http://localhost:3000` | comma-separated |
| `PORT` | — | `3001` | NestJS HTTP port |
### Frontend (`frontend/.env` at build time)
| Var | Required | Default |
|---|---|---|
| `NEXT_PUBLIC_API_URL` | — | `https://reloop-api.nabomhalang.co.kr/api` |
`NEXT_PUBLIC_*` 변수는 **빌드 시점**에 클라이언트 번들에 박힘. 배포 환경별로 다른 값을 쓰려면 환경마다 다시 빌드해야 함.
## Local development
```bash
# 1. Install
cd backend && pnpm install && cd ..
cd frontend && pnpm install && cd ..
# 2. Backend DB setup (first time)
cd backend
cp .env.example .env # edit DATABASE_URL, JWT_SECRET
pnpm prisma migrate dev --name init
pnpm prisma:seed
pnpm dev
# 3. Frontend
cd frontend
cp .env.example .env.local
pnpm dev
```
## Production deploy (Dev VM)
```bash
# Sync sources
rsync -avz --delete \
--exclude node_modules --exclude .next --exclude dist \
/path/to/reloop-v2/ dev:/home/deploy/reloop-v2/
# On Dev VM
cd /home/deploy/reloop-v2
# Backend
cd backend
pnpm install --frozen-lockfile # postinstall runs prisma generate automatically
pnpm prisma migrate deploy # apply migrations to prod DB
pnpm build # produces dist/main.js
cd ..
# Frontend
cd frontend
pnpm install --frozen-lockfile
NEXT_PUBLIC_API_URL=https://reloop-api.nabomhalang.co.kr/api pnpm build
# Copy static assets next to the standalone server
cp -r .next/static .next/standalone/.next/
cp -r public .next/standalone/ 2>/dev/null || true
cd ..
# PM2 boot
pm2 start ecosystem.config.js
pm2 save
```
## Reverse proxy (nginx/Caddy)
- `reloop.nabomhalang.co.kr``127.0.0.1:3000` (frontend)
- `reloop-api.nabomhalang.co.kr``127.0.0.1:3001` (backend)
Backend allows the frontend domain via `CORS_ORIGINS`.
## Operations
```bash
pm2 status # show reloop-api / reloop-web
pm2 logs reloop-api # tail backend
pm2 reload ecosystem.config.js # zero-downtime restart after redeploy
```
## QA
`.qa/` 아래에 Breezing QA 리포트 6개가 있어. `Plans.md``.claude/state/pending-fix-proposals.jsonl` 에 후속 fix 이력이 있음.

View File

@@ -11,6 +11,7 @@
"dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"postinstall": "prisma generate",
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"test": "vitest run",
"test:watch": "vitest",
@@ -59,5 +60,9 @@
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3",
"vitest": "^3.0.0"
}
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "pnpm@9.12.0"
}

47
ecosystem.config.js Normal file
View File

@@ -0,0 +1,47 @@
/**
* PM2 ecosystem for ReLoop v2
*
* Usage (on Dev VM after build):
* pm2 start ecosystem.config.js
* pm2 save
* pm2 reload ecosystem.config.js # zero-downtime restart
*/
module.exports = {
apps: [
{
name: 'reloop-api',
cwd: './backend',
script: 'dist/main.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'production',
PORT: '3001',
},
max_memory_restart: '512M',
error_file: './logs/reloop-api.error.log',
out_file: './logs/reloop-api.out.log',
merge_logs: true,
time: true,
},
{
name: 'reloop-web',
cwd: './frontend',
// Next.js standalone server (requires `output: 'standalone'` in next.config.js
// and manual copy of `.next/static` and `public` into `.next/standalone` after build).
script: '.next/standalone/server.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'production',
PORT: '3000',
HOSTNAME: '0.0.0.0',
},
max_memory_restart: '512M',
error_file: './logs/reloop-web.error.log',
out_file: './logs/reloop-web.out.log',
merge_logs: true,
time: true,
},
],
};

3
frontend/.env.example Normal file
View File

@@ -0,0 +1,3 @@
# Build-time env var — baked into the client bundle by Next.js.
# Must be set BEFORE running `pnpm build`, not at runtime.
NEXT_PUBLIC_API_URL=https://reloop-api.nabomhalang.co.kr/api

View File

@@ -1,9 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
reactStrictMode: true,
compiler: {
styledComponents: true,
},
reactStrictMode: true,
};
module.exports = nextConfig;

View File

@@ -28,5 +28,9 @@
"eslint-plugin-prettier": "^5.0.0",
"prettier": "^3.0.0",
"typescript": "^5"
}
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "pnpm@9.12.0"
}