Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
af8eb4f
fix: HomebaseReservation 타입에 reservationDate 필드 추가
koreahghg Jun 15, 2026
55a41da
fix: 홈베이스 조회 API에 date 쿼리 파라미터 추가
koreahghg Jun 15, 2026
21ef328
fix: homebaseQueries.list에 date 파라미터 및 queryKey 반영
koreahghg Jun 15, 2026
7c031b3
fix: useHomebaseReservationData에 reservationDate 파라미터 추가
koreahghg Jun 15, 2026
690ade9
fix: useHomebaseReservationData 호출 시 reservationDate 전달
koreahghg Jun 15, 2026
5db4eb6
fix: invalidateQueries queryKey에 reservationDate 반영
koreahghg Jun 15, 2026
60db532
Merge pull request #186 from team-incube/fix/homebase-date-query
koreahghg Jun 15, 2026
8995bc2
chore: Next.js standalone 출력 및 OG 폰트 트레이싱 설정
Dino0204 Jun 15, 2026
f5dee77
feat: refresh_token 쿠키 secure 플래그를 런타임 env로 오버라이드
Dino0204 Jun 15, 2026
ca19daf
feat: Docker 멀티스테이지 standalone 빌드 구성 추가
Dino0204 Jun 15, 2026
472940b
feat: nginx 리버스 프록시 기반 compose 배포 구성 추가
Dino0204 Jun 15, 2026
a6d7716
docs: GSM SV 배포 가이드 추가
Dino0204 Jun 15, 2026
8a544ba
chore: 포맷
Dino0204 Jun 15, 2026
1fc1815
chore: 배포 env 견본·문서 제거 (노션으로 이관)
Dino0204 Jun 16, 2026
90a27f8
refactor: nginx SSE 경로만 버퍼링 비활성으로 스코프
Dino0204 Jun 16, 2026
4d2d05c
fix: COOKIE_SECURE 빈 문자열일 때 secure 쿠키 비활성 방지
Dino0204 Jun 16, 2026
c71898c
chore: Dockerfile public 복사 시 소유권을 nextjs:nodejs로 일치
Dino0204 Jun 16, 2026
c7ec4a0
chore: GHCR 빌드·푸시 + VM 배포 GitHub Actions 워크플로 추가
Dino0204 Jun 16, 2026
523611a
chore: 배포 Environment 이름을 gsmsv-preview/gsmsv-production로 분리
Dino0204 Jun 16, 2026
5dc73d3
chore: VM 이미지 pull을 GITHUB_TOKEN으로 처리 (GHCR_PULL_TOKEN 제거)
Dino0204 Jun 16, 2026
a95b6d6
chore: 런타임 SENTRY_DSN 제거 (public DSN 폴백 사용)
Dino0204 Jun 16, 2026
39d5a88
Merge pull request #188 from team-incube/feat/gsm-sv-deploy
Dino0204 Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
node_modules
.next
.git
.github
.vercel
.env
.env.*
npm-debug.log*
Dockerfile
.dockerignore
compose.yaml
docker-compose.yml
README.md
.claude
.agents
coverage
*.log
106 changes: 106 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ─────────────────────────────────────────────────────────────────
# Deploy — Flooding-Client-V2
# develop → gsmsv-preview 환경 / main → gsmsv-production 환경
#
# 필요한 GitHub Environments: `gsmsv-preview`, `gsmsv-production`
# (Vercel 연동이 만든 `Preview`/`Production` 과 이름을 구분한다)
# - Variables(공개값): NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_DG_CLIENT_ID,
# NEXT_PUBLIC_DG_REDIRECT_URL, NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_SENTRY_DSN,
# IMAGE_TAG(develop|main), SERVER_NAME, COOKIE_SECURE
# - Secrets: SENTRY_AUTH_TOKEN, YOUTUBE_API_KEY,
# VM_HOST, VM_PORT, VM_USER, VM_SSH_KEY
# (서버 SENTRY_DSN 은 빌드에 박힌 NEXT_PUBLIC_SENTRY_DSN 으로 폴백 → 불필요)
# (GHCR pull 은 배포 시점 GITHUB_TOKEN 으로 처리 → 별도 토큰 불필요)
#
# VM 일회성 준비: deploy 공개키를 VM authorized_keys 에 등록, docker 설치
# ─────────────────────────────────────────────────────────────────
name: Deploy

on:
push:
branches: [develop, main]

permissions:
contents: read
packages: write

env:
IMAGE: ghcr.io/team-incube/flooding-client

jobs:
build:
name: Build & Push (GHCR)
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'main' && 'gsmsv-production' || 'gsmsv-preview' }}
steps:
- uses: actions/checkout@v4

- uses: docker/setup-buildx-action@v3

- name: Log in to GHCR (GITHUB_TOKEN, no PAT)
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build & push (amd64)
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64
tags: |
${{ env.IMAGE }}:${{ github.ref_name }}
${{ env.IMAGE }}:${{ github.ref_name }}-${{ github.sha }}
build-args: |
NEXT_PUBLIC_BASE_URL=${{ vars.NEXT_PUBLIC_BASE_URL }}
NEXT_PUBLIC_DG_CLIENT_ID=${{ vars.NEXT_PUBLIC_DG_CLIENT_ID }}
NEXT_PUBLIC_DG_REDIRECT_URL=${{ vars.NEXT_PUBLIC_DG_REDIRECT_URL }}
NEXT_PUBLIC_SITE_URL=${{ vars.NEXT_PUBLIC_SITE_URL }}
NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}

deploy:
name: Deploy to VM
needs: build
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'main' && 'gsmsv-production' || 'gsmsv-preview' }}
steps:
- uses: actions/checkout@v4

# compose.yaml + nginx/ 를 VM 으로 동기화 (레포가 source of truth)
- name: Sync deploy files to VM
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VM_HOST }}
port: ${{ secrets.VM_PORT }}
username: ${{ secrets.VM_USER }}
key: ${{ secrets.VM_SSH_KEY }}
source: "compose.yaml,nginx/"
target: "flooding"
overwrite: true

- name: Pull image & restart
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VM_HOST }}
port: ${{ secrets.VM_PORT }}
username: ${{ secrets.VM_USER }}
key: ${{ secrets.VM_SSH_KEY }}
script: |
set -e
cd ~/flooding
# 런타임 .env 를 환경 변수/시크릿에서 생성 (VM 에 수기 보관 안 함)
cat > .env <<EOF
IMAGE_TAG=${{ vars.IMAGE_TAG }}
SERVER_NAME=${{ vars.SERVER_NAME }}
COOKIE_SECURE=${{ vars.COOKIE_SECURE }}
YOUTUBE_API_KEY=${{ secrets.YOUTUBE_API_KEY }}
EOF
# GHCR 로그인: 배포 시점에만 유효한 GITHUB_TOKEN 사용 (별도 PAT 불필요)
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker compose pull
docker compose up -d
docker logout ghcr.io || true
docker image prune -f
53 changes: 53 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# syntax=docker/dockerfile:1

# ---- deps: install dependencies ----
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# ---- builder: build Next.js (standalone) ----
FROM node:22-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# NEXT_PUBLIC_* are inlined at build time, so they must be provided here.
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_DG_CLIENT_ID
ARG NEXT_PUBLIC_DG_REDIRECT_URL
ARG NEXT_PUBLIC_SITE_URL
ARG NEXT_PUBLIC_SENTRY_DSN
# Build-time only: Sentry source map upload.
ARG SENTRY_AUTH_TOKEN

ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL \
NEXT_PUBLIC_DG_CLIENT_ID=$NEXT_PUBLIC_DG_CLIENT_ID \
NEXT_PUBLIC_DG_REDIRECT_URL=$NEXT_PUBLIC_DG_REDIRECT_URL \
NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL \
NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN \
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN \
NEXT_TELEMETRY_DISABLED=1
Comment on lines +22 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

SENTRY_AUTH_TOKEN과 같은 민감한 자격 증명을 ARG 및 ENV를 통해 빌드 타임에 전달하면, 해당 값이 빌드 중간 레이어 및 이미지 히스토리에 고스란히 노출되어 보안상 취약할 수 있습니다 (예: docker history 명령어로 조회 가능).

이러한 보안 위험을 방지하기 위해 Docker BuildKit의 비밀 값 마운트 기능(--mount=type=secret)을 사용하거나, 최소한 최종 이미지에 노출되지 않도록 ENV 설정을 제거하고 빌드 명령어 실행 시점에만 주입하는 방식을 권장합니다.

BuildKit 비밀 값 마운트 예시:

RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \
    SENTRY_AUTH_TOKEN=$(cat /run/secrets/SENTRY_AUTH_TOKEN) npm run build


RUN npm run build

# ---- runner: minimal runtime image ----
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000 \
HOSTNAME=0.0.0.0

RUN groupadd -r nodejs && useradd -r -g nodejs nextjs

# public assets + standalone server + static chunks
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000

# standalone output entrypoint (NOT `next start`)
CMD ["node", "server.js"]
47 changes: 47 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# GSM SV VM 배포 구성 (app + nginx 리버스 프록시).
# - dev: HTTP만 사용 (nginx 80 → app:3000).
# - prod: certbot 프로파일로 인증서 발급 후 nginx/templates 를 TLS 템플릿으로 교체하고 443 활성화.
# 환경값/시크릿은 같은 디렉터리의 .env 에서 주입 (.env.example 참고).
services:
app:
image: ghcr.io/team-incube/flooding-client:${IMAGE_TAG:-develop}
pull_policy: always
env_file:
- .env
expose:
- "3000" # nginx 가 프론트, 외부로 직접 공개하지 않음
restart: unless-stopped

nginx:
image: nginx:1.27-alpine
depends_on:
- app
ports:
- "80:80"
# 프로덕션 TLS 시 주석 해제
# - "443:443"
environment:
# nginx 공식 이미지의 envsubst 는 "환경에 정의된 변수"만 치환하므로
# SERVER_NAME 만 정의 → 템플릿의 $host 등 nginx 변수는 그대로 보존됨.
- SERVER_NAME=${SERVER_NAME:-_}
volumes:
- ./nginx/templates:/etc/nginx/templates:ro
- acme-challenge:/var/www/certbot:ro
- letsencrypt:/etc/letsencrypt:ro
restart: unless-stopped

# 프로덕션 전용. 평소엔 안 뜬다. 사용: docker compose --profile prod up -d
# 최초 발급: docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d <DOMAIN>
certbot:
image: certbot/certbot
profiles:
- prod
volumes:
- acme-challenge:/var/www/certbot
- letsencrypt:/etc/letsencrypt
# 인증서 자동 갱신 루프
entrypoint: /bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

certbot 서비스의 entrypoint에서 $${!}를 사용하면 Docker Compose가 이를 ${!}로 변환합니다. 하지만 sh나 bash 쉘에서 ${!}는 올바르지 않은 치환(bad substitution) 문법이므로 구문 에러가 발생하여 컨테이너가 정상적으로 시작되지 않거나 크래시가 발생할 수 있습니다.

가장 최근에 백그라운드로 실행된 프로세스(여기서는 sleep 12h &)의 PID를 가리키는 올바른 쉘 변수는 $!입니다. Docker Compose 파일 내에서 이를 올바르게 이스케이프하려면 $$!로 작성해야 합니다.

    entrypoint: /bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $$!; done;'


volumes:
acme-challenge:
letsencrypt:
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";

const nextConfig: NextConfig = {
output: "standalone",
reactCompiler: true,
transpilePackages: ["msw"],
outputFileTracingIncludes: {
"/**": ["./src/shared/lib/og/fonts/*.ttf"],
},
images: {
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 2678400,
Expand Down
43 changes: 43 additions & 0 deletions nginx/templates/default.conf.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# HTTP 리버스 프록시 (dev/검증). envsubst 가 ${SERVER_NAME} 만 치환한다.
server {
listen 80;
server_name ${SERVER_NAME};

# 이미지 업로드(아바타·클럽 썸네일) 대비. nginx 기본 1m 초과 거부 방지.
client_max_body_size 10m;

# 공통 프록시 헤더 (server 레벨 → 하위 location 이 상속)
proxy_http_version 1.1;
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_set_header Connection "";

# Let's Encrypt HTTP-01 챌린지 경로 (TLS 준비용, dev 에선 빈 볼륨이라 무해)
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

# SSE 전용: 버퍼링 끄고 장시간 연결 유지.
# (앱도 X-Accel-Buffering: no 를 보내지만, 장시간 read/send timeout 은 여기서만 지정 가능)
location = /api/dormitory/music/subscribe {
proxy_pass http://app:3000;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/dormitory/studies/attendance {
proxy_pass http://app:3000;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}

# 일반 요청: 기본 버퍼링 유지
location / {
proxy_pass http://app:3000;
}
}
60 changes: 60 additions & 0 deletions nginx/tls/app.tls.conf.template.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 프로덕션 TLS 참고용 템플릿.
# 활성화 절차:
# 1) compose 의 nginx 443 포트와 certbot(prod 프로파일)을 켠다.
# 2) docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d <DOMAIN>
# 3) 이 파일을 nginx/templates/default.conf.template 로 교체(기존 HTTP 템플릿 대체)하고
# <DOMAIN> 을 실제 도메인(flooding.kr)으로 치환.
# 4) docker compose up -d --force-recreate nginx
# envsubst 는 ${SERVER_NAME} 만 치환한다.

server {
listen 80;
server_name ${SERVER_NAME};

location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl;
http2 on;
server_name ${SERVER_NAME};

ssl_certificate /etc/letsencrypt/live/<DOMAIN>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<DOMAIN>/privkey.pem;

client_max_body_size 10m;

# 공통 프록시 헤더 (server 레벨 → 하위 location 이 상속)
proxy_http_version 1.1;
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_set_header Connection "";

# SSE 전용: 버퍼링 끄고 장시간 연결 유지
location = /api/dormitory/music/subscribe {
proxy_pass http://app:3000;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/dormitory/studies/attendance {
proxy_pass http://app:3000;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}

# 일반 요청: 기본 버퍼링 유지
location / {
proxy_pass http://app:3000;
}
}
10 changes: 6 additions & 4 deletions src/entities/homebase/api/getHomebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ type HomebaseResponse =
| HomebaseReservation[]
| { data?: HomebaseReservation[] };

export async function getHomebaseReservations(): Promise<
HomebaseReservation[]
> {
const { data } = await instance.get<HomebaseResponse>("/homebase");
export async function getHomebaseReservations(
date: string,
): Promise<HomebaseReservation[]> {
const { data } = await instance.get<HomebaseResponse>("/homebase", {
params: { date },
});
return Array.isArray(data) ? data : (data.data ?? []);
}
6 changes: 3 additions & 3 deletions src/entities/homebase/api/homebaseQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { getHomebaseReservations } from "@/entities/homebase/api/getHomebase";
import type { HomebaseApplyRequest } from "@/entities/homebase/model/homebase";

export const homebaseQueries = {
list: () =>
list: (date: string) =>
queryOptions({
queryKey: ["homebase", "list"],
queryFn: getHomebaseReservations,
queryKey: ["homebase", "list", date],
queryFn: () => getHomebaseReservations(date),
}),
} as const;

Expand Down
1 change: 1 addition & 0 deletions src/entities/homebase/model/homebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface HomebaseMember {

export interface HomebaseReservation {
id: number;
reservationDate: string;
startPeriod: number;
endPeriod: number;
homebaseId: number;
Expand Down
1 change: 1 addition & 0 deletions src/features/homebase/model/useHomebaseReservation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function useHomebaseReservation() {
myReservationIds,
isError,
} = useHomebaseReservationData({
reservationDate,
selectedFloor,
selectedStartNum,
selectedEndNum,
Expand Down
Loading
Loading