Deployment (Render · Vercel · Branch policy)
Overview
| 컴포넌트 | 호스트 | 트리거 |
|---|---|---|
Backend (linkmusic-msa-space-was) | Render | main push → auto-deploy |
Frontend admin (apps/admin) | Vercel (별도 project · admin.space.linkmusic.io) | main push → production / 그 외 → preview |
Frontend space (apps/space) | Vercel (별도 project · space.linkmusic.io, 신설 필요) | 동일 |
Frontend docs (apps/docs) | Vercel (별도 project, 신설) | 동일 |
| PostgreSQL | Render PostgreSQL | managed |
Branch 정책 (Git Flow 단순화)
- main — production (자동 배포). 직 push 금지.
- develop — default 통합 브랜치. 모든 PR base.
- feature/* — 작업 단위. SPEC 당 1 브랜치.
머지 정책:
| 방향 | 머지 모드 |
|---|---|
feature/* → develop | squash (gh pr merge --squash) |
develop → main | merge commit (gh pr merge --merge) |
| rebase merge | 비활성 |
Branch protection (main · develop):
- Build CI 통과 필수
- 1 review approval 필수 (Copilot review 포함 자체 평가)
- 직 push 금지
main → release 자동화
develop → main 머지 후 무조건 release 생성 (memory feedback_release_on_main_merge):
gh release create vX.Y.Z --title "vX.Y.Z" --notes "포함 SPEC: #NNN · #MMM ..." --target mainSemVer:
- 사용자 기능 추가 (인증·HQ 등록 등) — minor (0.1.0 · 0.2.0 …)
- 디자인 시스템 · 인프라 — patch
- breaking change — major (1.0.0 = 베타 출시)
Vercel 설정 (apps/admin)
| 항목 | 값 |
|---|---|
| Root Directory | apps/admin |
| Framework | Next.js |
| Build Command | pnpm build |
| Install Command | pnpm install --frozen-lockfile |
| Node | 20.20.2 (.nvmrc 안에) |
| pnpm | 10.33.0 (packageManager 안에) |
Vercel monorepo + sub-app pattern:
- Root Directory 는 sub-app 폴더 (
apps/admin). - 그 폴더 안에도
.nvmrc+engines.node+packageManager([[feedback-9]]). - Vercel 이 root engines /
.nvmrc만 보고 sub-app 의 minor 가 안 맞으면engine-strictfail.
Vercel 설정 (apps/space) — 신설 필요
매장 클라이언트(본사+점장 한 앱). admin 과 다른 origin(space.linkmusic.io)이므로 별도 Vercel project.
| 항목 | 값 |
|---|---|
| Root Directory | apps/space |
| Framework | Next.js |
| Build Command | pnpm build |
| Install Command | pnpm install --frozen-lockfile |
| Node | 20.20.2 (apps/space/.nvmrc) |
| pnpm | 10.33.0 (packageManager) |
| Port | 3001 (dev) |
| 도메인 | space.linkmusic.io (후속) / 초기엔 Vercel preview URL |
env vars (server-side only):
BACKEND_BASE_URL— backend API base (prod:https://linkmusic-msa-space-was.onrender.com)SESSION_PASSWORD— iron-session 암호화 키 (≥32 bytes random).apps/admin과 별개 값으로 둘 것 (세션 cookie 네임스페이스 분리 —lm_space_session). 누락 시 부팅 거부.
Vercel 설정 (apps/docs) — 신설 필요
| 항목 | 값 |
|---|---|
| Root Directory | apps/docs |
| Framework | Next.js |
| Build Command | pnpm build |
| Install Command | pnpm install --frozen-lockfile |
| Node | 20.20.2 |
| Port | 3100 (dev) |
도메인:
- 초기 — Vercel preview URL
- 후속 —
docs.linkmusic.io또는linkmusic-docs.vercel.app
Render 설정 (backend)
- Service type: Web Service
- Runtime: Docker (또는 native Java)
- Auto-deploy:
mainpush - Health check:
/actuator/health - env vars (prod):
JWT_SECRET(≥64 chars, base64)DATABASE_URL(Render PG 자동 주입)SPRING_PROFILES_ACTIVE=renderSTORAGE_AZURE_CONNECTION_STRING(#041 — 음원 blob 스토리지. 선택: 미설정 시 Local 어댑터로 부팅·blob 미저장. production 실사용 전 필수)STORAGE_AZURE_CONTAINER(#041 — 기본music)STORAGE_AZURE_PREFIX(#041 — 기본music)UPLOAD_TICKET_SECRET(#177 — 대용량 직접 업로드 티켓 서명 시크릿, ≥256-bit 랜덤.JWT_SECRET과 분리. 미설정 시 티켓 발급/검증 불가 → 업로드 503UPLOAD_NOT_CONFIGURED(fail-lazy — 부팅·다른 기능은 정상). production 실사용 전 필수)- … (env var 추가 시 main 머지 전 사용자 안내 의무 —
[[memory: announce_new_env_vars]])
Azure Blob 스토리지 · 스토리지 추상화 (#041)
음원 파일 업로드(#041)는 backend StoragePort 추상화로 저장소를 분리한다 — Local 어댑터(기본) 와
AzureBlobStorageAdapter. Azure 어댑터는 storage.azure.connection-string(env
STORAGE_AZURE_CONNECTION_STRING)이 주입됐을 때만 @ConditionalOnProperty 로 활성화되고, 없으면
Local 어댑터로 부팅(blob 미저장)한다. 따라서 Azure 자격증명을 구축·제공하기 전에도 backend 가
부팅·동작한다(blob 저장만 비활성). production 실사용 전 connection-string 주입이 필수다.
| env | 기본 | 의미 |
|---|---|---|
STORAGE_AZURE_CONNECTION_STRING | (없음 → Local fallback) | Azure Storage 연결 문자열. 있을 때만 Azure 어댑터 활성 |
STORAGE_AZURE_CONTAINER | music | blob 컨테이너 |
STORAGE_AZURE_PREFIX | music | blob key prefix ({prefix}/{id}.mp3) |
main 머지 전 안내 의무(
[[memory: announce_new_env_vars]]) — Azure connection-string 은 production 시크릿. 사용자가 구축 후 제공한다. 미설정 시 Local 어댑터로 부팅돼 deploy 자체는 실패하지 않으나 blob 이 저장되지 않는다. 업로드 흐름·태그 재기록은 Music Upload.
음원 서빙·전송 캐시 (SPEC #179)
재생 음원·CM 은 public blob URL 직접 서빙(SAS·CDN 없음 — 서명 파라미터가 없어 URL=캐시 키 등식이 유지된다)이며, 전송 원가는 클라이언트 캐시 적중으로 줄인다. 서버·인프라 측 계약:
- 교체 = 새 UUID key 발급 (D1 — 음원·CM. BE
MusicUploadService.replace·HqCommercialService): 같은 key 덮어쓰기를 폐지해 key 가 내용 주소가 된다. URL 이 바뀌면 클라이언트 캐시(브라우저 HTTP + FE Cache Storage)가 자동 무효화된다. 옛 blob 은 삭제하지 않는다(D11 — 재생 중 클라이언트가 옛 URL 을 최대 30분(큐 refetch 주기) 들고 있으므로 즉시 삭제는 재생 실패를 만든다). TTS 는 예외 — 같은 key 덮어쓰기 유지(audioUrl 불변 계약). - Cache-Control (D2·D3): 음원·CM 업로드 =
public, max-age=31536000, immutable(key 불변 전제) · TTS =no-store명시 · 티켓 첨부 = 미지정(인증 프록시 경유라 blob 헤더 무관).StoragePort.upload()의cacheControl인자. - 인프라 후속(I1·I2 — 운영 액션): Azure Storage 계정 CORS 룰(GET · FE 캐시 계층의
fetch(cors)전제 — 미설정이어도 FE 는 원격 폴백으로 무해, 캐시만 미동작) + 기존 blobCache-Control백필 스크립트 1회(BE 배포 후 — 순서가 안전성이다: 배포 전 백필은 같은-key 교체 시절 파일에 immutable 을 고착시킨다). 새 env·시크릿 없음. - FE 캐시 계층(Cache Storage + objectURL·프리페치·
cacheHit계측)은 Store Player §음원 캐시 계층 참고.
대용량 파일 직접 업로드 · Vercel 4.5MB 우회 (#177)
음원·CM송·CS 티켓 첨부 파일 업로드는 원래 브라우저 → Vercel 서버리스 BFF proxy
(/api/backend/[...path], runtime="nodejs", 요청 본문을 arrayBuffer 로 전량 버퍼링) → 백엔드 경로를
탔다. 그런데 Vercel 서버리스 함수는 요청 본문 4.5MB 하드 리밋(FUNCTION_PAYLOAD_TOO_LARGE)이 있어
그 이상 파일이 413 으로 차단됐다(백엔드는 음원·CM 20MB · CS 첨부 10MB 허용). 이 리밋은 플랫폼 제약이라
설정으로 올릴 수 없다.
SPEC #177 — 파일 본문만 백엔드로 직접(cross-origin) 전송해 Vercel 을 우회한다. 인증은 백엔드가
발급하는 단기·용도한정 업로드 티켓(별도 시크릿 UPLOAD_TICKET_SECRET 서명 JWT, ~10분):
- FE 가
POST /api/v1/uploads/tickets(작은 JSON, proxy 경유·기존 JWT 인증)로 티켓 +uploadUrl(백엔드 공개 절대 URL)을 받는다. 백엔드가 role↔purpose 를 검증한다(OPERATOR→MUSIC·TICKET_ATTACHMENT_ADMIN/ HQ_MANAGER→COMMERCIAL·TICKET_ATTACHMENT_HQ·TICKET_ATTACHMENT_HQ_STORE/ STORE_MANAGER→TICKET_ATTACHMENT_STORE). CS 첨부는targetId(티켓 id)의 채널 격리도 함께 검증한다. - FE 가
uploadUrl(=POST /api/v1/uploads/files)로 파일 multipart 를 직접 POST(X-Upload-Ticket헤더·permitAll+티켓 검증). Vercel proxy 를 거치지 않으므로 4.5MB 리밋 무관(백엔드 상한만 적용).
음원·CM 은 크기와 무관하게 항상 이 경로를 쓰고, CS 첨부는 4MB 초과분만 이 경로로 우회한다(이하는
채널 multipart endpoint 로 요청 1회). apps/space proxy 에서 uploads/tickets 의 세션 스코프는 경로가
아니라 본문 purpose 로 갈린다 — 본사 purpose 만 impersonation-aware 이고 점장 purpose·판독 실패는
실 로그인(fail-closed)이다.
CORS 는 기존 SecurityConfig 가 /api/** 에 FE 오리진을 허용해 충족된다. UPLOAD_TICKET_SECRET
미설정 시 fail-lazy — 부팅·다른 기능은 정상이고 업로드만 503 UPLOAD_NOT_CONFIGURED 로 실패한다.
FE 신규 env 없음(uploadUrl 은 발급 응답값). 자세한 계약은
Endpoints · Uploads.
실시간 통지 채널 · 스트림 티켓 (#180)
점장 player 는 본사 즉시방송을 SSE 로 즉시 통지받는다. 연결 경로는 위 대용량 업로드(#177)와 같은 브라우저 → 백엔드 직접 + 단기 티켓 패턴이다 — BFF 스트리밍 프록시 라우트는 쓰지 않는다.
- FE 가
POST /api/v1/store/announcements/stream-ticket(작은 JSON · proxy 경유 · 기존 JWT 인증)로{ ticket, streamUrl, expiresInSeconds }를 받는다.streamUrl은 티켓까지 포함된 완전한 절대 URL 이라 FE 가 백엔드 base URL 을 조립하지 않는다. new EventSource(streamUrl)—GET /api/v1/store/announcements/stream?ticket=…(permitAll + 티켓 검증)로 직접 연결.EventSource는 커스텀 헤더를 실을 수 없어 Bearer 인증이 불가능하므로 티켓이 쿼리 파라미터로 간다(TTL 60초 · 단일 용도 · 신호만 흐르는 스트림이라 leak 가치가 낮다).
왜 BFF 프록시가 아닌가: (a) Vercel maxDuration(Hobby 300s/Pro 800s)에 스트리밍 시간이 포함돼
5~13분마다 전 기기가 강제 재연결된다. (b) in-flight 스트림이 Provisioned Memory 를 상시 과금시켜,
호출 비용 절감이 동기인 변경이 새 상시 비용을 만든다. (c) 백엔드의 30분 emitter 수명을 활용할 수 없다.
직접 연결은 재연결당 티켓 mint 1회(기기당 일 ~144회)로 끝난다.
- 시크릿·env:
UPLOAD_TICKET_SECRET을 재사용하되type=stream-ticketclaim 으로 업로드 티켓과 상호 사용을 차단한다 — 새 env 0 · DB·마이그레이션 0. 미구성 시 fail-lazy(발급 503, 방송은 폴링으로 그대로 수신). - CORS: 기존
SecurityConfig의/api/**오리진 허용을 그대로 쓴다. preview 오리진이 목록에 없으면 SSE 만 조용히 실패하고 폴링으로 폴백한다(방송 수신은 무손실 — 필요 시 오리진 추가는 env 통보 사안). 이 경우 티켓 발급은 200 인데 연결만 계속 실패하므로, FE 는connected를 한 번도 받지 못한 실패가 5회를 넘으면 재시도 상한을 5분으로 올린다 — 그대로 두면 매 분 티켓을 mint 해 기기당 하루 1,440회(폴링 완화로 아낀 호출을 상쇄)가 된다. - keepalive: 서버가 25초 주기로 SSE comment(
: ping)를 보내 중간 프록시(Render LB)의 유휴 컷을 막는다. comment 는 브라우저가 JS 로 노출하지 않으므로 클라이언트는 선제 재연결(10분) 과 연결 확정 워치독(15초) 으로 죽은 커넥션을 걷어낸다. - 단일 인스턴스 전제: 구독자 registry 가 인메모리다. 스케일아웃하면 SSE 만 동시성을 잃고 폴링으로 동작한다(기능 유지). Redis pub/sub 은 그때 별도 SPEC.
- FE 훅·폴링 완화(연결 연속 90초 유지 후 60초 / 그 외 20초 · 히스테리시스)·계측은 Store Player §실시간 통지 참고.
env var 정책
- 클라이언트 노출은
NEXT_PUBLIC_*만. .env*파일은 편집/커밋 금지.- production 시크릿 (
JWT_SECRET·SESSION_PASSWORD· API key) — 추가 시 main 머지 전 사용자에게 명시적 안내. 누락 시 deploy fail. .env.example만 commit.
CI (GitHub Actions)
.github/workflows/ci.yml:- Trigger:
pushto develop/main + PR - matrix: node 20.20.2
- steps:
pnpm install --frozen-lockfile→pnpm -r lint typecheck test build
- Trigger:
.github/workflows/e2e.yml: 풀스택 E2E harness(docker compose pg→backend→frontend + Playwright happy-path). develop 대상 PR·수동 dispatch에서만 동작하며 현재 논블로킹(required status check 미등록, 안정화 후 승격 검토). standalone 빌드는BUILD_STANDALONE=1(Dockerfile.e2e)로 E2E 도커 이미지에서만 켜진다 — 기본/Vercel 빌드 무영향.- branch protection 에 status check 등록
Pre-deploy 체크리스트 (memory local_verify_before_push)
push 전:
-
pnpm -r typecheck통과 -
pnpm -r lint통과 -
pnpm -r test통과 -
pnpm -r build통과 (실제 배포 시 발생 가능한 실수 사전 차단) - 새 env var 있다면 사용자 안내 노트 작성
- cross-cutting grep — 같은 helper 가 다른 호출처에 적용 필요한지 (
[[feedback-16]])
Roadmap
- staging 환경 분리 —
develop→ preview ·main→ production (현재 dev 가 develop 통합 역할) - env var 자동 비교 (CI 가
.env.example과 Render env 비교) - Sentry · Vercel Analytics 도입
References
- 워크스페이스
CLAUDE.md(Git 전략) .github/workflows/ci.ymllinkmusic-frontend-space/apps/admin/.nvmrc- memory
feedback_*(workflow 전반)