diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9553014 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Local development defaults +NEXT_PUBLIC_BASE_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://localhost:8080 + +# Production reference +# NEXT_PUBLIC_BASE_URL=https://www.git-ranker.com +# NEXT_PUBLIC_API_URL=https://www.git-ranker.com + +# Optional analytics / error tracking +# NEXT_PUBLIC_ANALYTICS_ENDPOINT= +# NEXT_PUBLIC_SENTRY_DSN= +# SENTRY_DSN= diff --git a/Dockerfile b/Dockerfile index 2451738..0ea6d09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,9 @@ ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ENV NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL} ENV NEXT_TELEMETRY_DISABLED=1 +RUN test -n "$NEXT_PUBLIC_API_URL" || (echo "NEXT_PUBLIC_API_URL build arg is required" >&2 && exit 1) +RUN test -n "$NEXT_PUBLIC_BASE_URL" || (echo "NEXT_PUBLIC_BASE_URL build arg is required" >&2 && exit 1) + RUN npm run build # Stage 3: Production runner diff --git a/README.md b/README.md index e215bc4..5a82b01 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,64 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# git-ranker-client -## Getting Started +`git-ranker-client`는 Git Ranker의 Next.js 16 프런트엔드다. 랭킹 조회, 사용자 상세, 로그인 진입, SEO 메타데이터와 배지 링크를 제공한다. -First, run the development server: +## Requirements -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +- Node.js 20+ +- npm +- `NEXT_PUBLIC_BASE_URL` +- `NEXT_PUBLIC_API_URL` + +`NEXT_PUBLIC_BASE_URL`와 `NEXT_PUBLIC_API_URL`는 build와 runtime 모두에서 필수다. 값이 없으면 `npm run build`와 런타임 초기화가 즉시 실패한다. + +## Environment + +로컬 Next.js 실행은 `.env.local`, Docker Compose 실행은 `.env`를 사용하면 된다. 시작점으로는 [.env.example](.env.example)을 복사한다. + +로컬 개발 예시: + +```env +NEXT_PUBLIC_BASE_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://localhost:8080 ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +프로덕션 예시: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```env +NEXT_PUBLIC_BASE_URL=https://www.git-ranker.com +NEXT_PUBLIC_API_URL=https://www.git-ranker.com +``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +선택 env: -## Learn More +- `NEXT_PUBLIC_ANALYTICS_ENDPOINT` +- `NEXT_PUBLIC_SENTRY_DSN` +- `SENTRY_DSN` -To learn more about Next.js, take a look at the following resources: +선택 env가 없어도 핵심 기능은 동작한다. 없으면 analytics/web vitals 전송이나 Sentry 수집만 비활성화된다. -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## Commands -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```bash +npm install +npm run dev +npm run lint +npx tsc --noEmit +npm run build +``` + +## Docker Compose + +```bash +cp .env.example .env +docker compose up --build +``` -## Deploy on Vercel +`docker-compose.yml`과 `Dockerfile`은 `NEXT_PUBLIC_BASE_URL`, `NEXT_PUBLIC_API_URL`가 비어 있으면 즉시 실패한다. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Build And Runtime Notes -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- `JetBrains Mono`는 공식 JetBrains Mono release v2.304에서 가져온 로컬 자산을 사용한다. +- 폰트 라이선스는 [src/fonts/JetBrainsMono-OFL.txt](src/fonts/JetBrainsMono-OFL.txt)에 보관한다. +- locale routing과 보안 헤더는 [src/proxy.ts](src/proxy.ts)에서 처리한다. +- public URL 정책은 [src/shared/lib/public-env.ts](src/shared/lib/public-env.ts)에서 단일 기준으로 관리한다. diff --git a/docker-compose.yml b/docker-compose.yml index 9e465d1..5bd17b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,8 @@ services: dockerfile: Dockerfile args: # Build-time args (NEXT_PUBLIC_* are inlined during build) - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} - NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:?NEXT_PUBLIC_API_URL is required} + NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL:?NEXT_PUBLIC_BASE_URL is required} restart: unless-stopped ports: - "3000:3000" @@ -16,8 +16,8 @@ services: NODE_ENV: production TZ: Asia/Seoul # Runtime vars (for middleware/server-side code) - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} - NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:?NEXT_PUBLIC_API_URL is required} + NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL:?NEXT_PUBLIC_BASE_URL is required} deploy: resources: limits: diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6014a78..a8f2710 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,5 @@ import type {Metadata, Viewport} from "next"; import localFont from "next/font/local"; -import { JetBrains_Mono } from "next/font/google"; import Script from "next/script"; import "./globals.css"; import QueryProvider from "@/shared/providers/query-provider"; @@ -12,6 +11,7 @@ import { WebVitalsReporter } from "@/shared/components/web-vitals-reporter"; import { cn } from "@/shared/lib/utils"; import { LocaleProvider } from "@/shared/providers/locale-provider"; import { getRequestLocale } from "@/shared/i18n/server-locale"; +import { publicBaseUrl } from "@/shared/lib/public-env"; const pretendard = localFont({ src: "../fonts/PretendardVariable.woff2", @@ -20,13 +20,14 @@ const pretendard = localFont({ variable: "--font-sans", }); -const jetbrainsMono = JetBrains_Mono({ - subsets: ["latin"], +const jetbrainsMono = localFont({ + src: "../fonts/JetBrainsMonoVariable.ttf", variable: "--font-mono", display: "swap", + weight: "100 800", }); -const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "https://www.git-ranker.com" +const BASE_URL = publicBaseUrl export async function generateMetadata(): Promise { const locale = await getRequestLocale() @@ -105,7 +106,6 @@ export default async function RootLayout({ {/* Preconnect to external origins for faster resource loading */} -