Next.js 16, React 19, TypeScript 기반의 개인 포트폴리오 웹사이트입니다. 공개 블로그·프로젝트 페이지, 관리자 대시보드, Notion 기반 CV 렌더링, PDF 생성 파이프라인을 통합해 제공합니다.
- 대시보드형 랜딩 페이지
- Supabase 기반 블로그·프로젝트 콘텐츠 관리 및 버전 이력
- Notion 페이지를 읽어 CV 웹 페이지로 렌더링
- Gotenberg로 CV PDF를 생성하고 S3 호환 스토리지에 캐시
- 게시글·프로젝트·태그·프로필을 관리하는 관리자 대시보드
- Cloudflare Turnstile 검증을 거친 보호된 이메일 노출
- Giscus 기반 블로그 댓글
| 영역 | 사용 기술 |
|---|---|
| 프론트엔드 | Next.js 16, React 19, TypeScript, Tailwind CSS v4 |
| UI | shadcn/ui, Radix UI, Mantine, Framer Motion, BlockNote |
| 백엔드·서비스 | Supabase, Notion API, Gotenberg, S3, Cloudflare Turnstile, Giscus |
| 운영 | Docker, Docker Compose, GitHub Actions, ESLint |
app/
admin/ 관리자 대시보드, 로그인, 편집 화면
api/ blog, projects, cv, s3 관련 Route Handlers
blog/ 공개 블로그 페이지
projects/ 공개 프로젝트 페이지
cv/ 이력서 페이지 및 PDF 다운로드 UI
contact/ 보호된 이메일 / 소셜 링크 페이지
components/ 공용 UI 및 도메인 컴포넌트
contexts/ 인증 컨텍스트
database/schemas/ Supabase 스키마
hooks/ 공용 훅
lib/ 서비스 로직, Notion, Supabase, 스토리지, PDF 처리
public/ 정적 애셋
types/ 공용 타입
# 1. 의존성 설치
npm install
# 2. 환경 변수 설정
cp .env.example .env.local
# .env.local을 열어 아래 환경 변수 항목을 채웁니다
# 3. 개발 서버 실행
npm run dev
# → http://localhost:3000| 변수 | 설명 |
|---|---|
NOTION_CV_PAGE_ID |
CV로 사용할 Notion 페이지 ID |
| 변수 | 설명 |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase 프로젝트 URL |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
클라이언트용 공개 키 |
SUPABASE_SECRET_KEY |
서버 측 서비스 키 |
| 변수 | 설명 |
|---|---|
NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY |
Turnstile 사이트 키 |
CLOUDFLARE_TURNSTILE_SECRET_KEY |
Turnstile 시크릿 키 |
JWT_SECRET_KEY |
보호된 이메일 노출용 JWT 서명 키 |
PROTECTED_EMAIL |
검증 후 노출할 이메일 주소 |
| 변수 | 설명 |
|---|---|
S3_ACCESS_KEY |
스토리지 액세스 키 |
S3_SECRET_KEY |
스토리지 시크릿 키 |
NEXT_PUBLIC_S3_CDN_URL |
공개 CDN 기본 URL |
NEXT_PUBLIC_S3_BUCKET |
버킷 이름 |
NEXT_PUBLIC_S3_REGION |
리전 |
NEXT_PUBLIC_S3_ENDPOINT |
엔드포인트 |
ASSET_GC_SECRET |
애셋 GC 엔드포인트 보호용 시크릿 |
서버 코드에서는
S3_BUCKET,S3_REGION,S3_ENDPOINT,S3_CDN_URL런타임 오버라이드도 읽습니다. 기본 설정은.env.example의NEXT_PUBLIC_*값을 따릅니다.
| 변수 | 설명 |
|---|---|
NEXT_PUBLIC_SITE_URL |
프로덕션 사이트 URL |
NEXT_PUBLIC_BASE_URL |
앱 기본 URL |
PDF_RENDER_BASE_URL |
PDF 렌더링 시 사용할 CV 기준 URL |
| 변수 | 설명 |
|---|---|
GOTENBERG_URL |
PDF 변환 서비스 URL |
GOTENBERG_USERNAME |
Basic Auth 사용자명 (선택) |
GOTENBERG_PASSWORD |
Basic Auth 비밀번호 (선택) |
| 변수 | 설명 |
|---|---|
NEXT_PUBLIC_GISCUS_REPO |
GitHub 저장소 (owner/repo) |
NEXT_PUBLIC_GISCUS_REPO_ID |
저장소 ID |
NEXT_PUBLIC_GISCUS_CATEGORY |
댓글 카테고리 이름 |
NEXT_PUBLIC_GISCUS_CATEGORY_ID |
댓글 카테고리 ID |
npm run dev # 개발 서버 실행
npm run build # 프로덕션 빌드
npm start # 빌드 결과물 실행
npm run lint # ESLint 검사프로덕션 배포는 compose.production.yml을 기준으로 동작합니다. 스택은 website와 gotenberg 두 서비스로 구성됩니다.
main 브랜치에 푸시하면 워크플로가 자동으로 실행됩니다.
- 멀티아키텍처 Docker 이미지를 빌드하고 레지스트리에 푸시
- Compose 파일을 VM에 복사하고
.env.compose,.env.runtime생성 docker compose pull→docker compose up -d --remove-orphans실행
내부 네트워크에서 앱은
http://gotenberg:3000으로 PDF 생성을 요청하고, Gotenberg는http://website:3000/cv?print=true를 렌더링합니다.
두 개의 .env 파일을 준비합니다.
# .env.compose
DOCKER_IMAGE=<registry-user>/website:latest.env.runtime에는 앱 실행에 필요한 런타임 환경 변수를 입력합니다. 이후 아래 명령을 실행합니다.
docker compose \
--project-name website \
--env-file .env.compose \
-f compose.production.yml pull
docker compose \
--project-name website \
--env-file .env.compose \
-f compose.production.yml up -d --remove-orphans참고:
Dockerfile은NEXT_PUBLIC_*변수들과NOTION_CV_PAGE_ID를 빌드 타임 인자로 사용합니다. 로컬 또는 수동 빌드 시에도 이 값들이 누락되지 않도록 주의하세요.
/cv 페이지 접근
└─ Notion API로 페이지 읽기 → 웹 렌더링
/api/cv/pdf 요청
└─ Turnstile 토큰 검증
└─ CV 마지막 수정 시각 기준으로 버전 키 계산 (generated/cv/...)
├─ 캐시 히트 → 기존 공개 URL 반환
└─ 캐시 미스 → Gotenberg로 /cv?print=true 렌더링 → S3 업로드 → URL 반환
응답은 PDF 바이너리가 아닌 스토리지의 공개 객체 URL입니다.
CV PDF는 애플리케이션이 직접 삭제하지 않으므로, generated/cv/ 프리픽스에 아래 수명 주기 규칙을 설정하는 것을 권장합니다.
- Rule name:
cv-pdf-expire-7d - Prefix:
generated/cv/ - Action: 업로드 후 7일이 지난 객체 삭제
전체 스키마는 database/schemas/schema.sql을 참고하세요.
| 테이블 | 설명 |
|---|---|
profiles |
사용자 프로필 |
content_items |
공통 콘텐츠 메타데이터 (post, project, page) |
content_versions |
콘텐츠 버전 스냅샷 |
content_tags / content_item_tags |
태그 및 연결 관계 |
assets |
업로드된 애셋 |
content_version_assets |
특정 버전에 연결된 애셋 |
asset_deletion_queue |
오브젝트 스토리지 삭제 작업 큐 |
post_contents / project_contents / page_contents |
콘텐츠 타입별 확장 테이블 |
- Contact 페이지는 문의 폼이 아닌, 보호된 이메일과 소셜 링크 제공 페이지입니다.
- 댓글은 Giscus를 통해 GitHub Discussions와 연동됩니다.
- 관리자 영역 접근은 Supabase 인증 세션을 기준으로 합니다.