Camera Archive

Project Prompt

Camera Archive 프로젝트 전체 명세

개요

개인 사진 아카이브 웹사이트. 보유한 카메라별로 촬영한 사진을 분류·관리하고, 갤러리 형태로 공개하는 포트폴리오 사이트.

Next.js 14 App RouterTypeScriptTailwind CSS (다크 테마)Prisma ORMSupabase PostgreSQLCloudflare R2Vercel 배포

기술 스택

분류기술용도
프레임워크Next.js 14 App Router + TypeScriptSSR/SSG, API Routes, Middleware
스타일Tailwind CSS 3 다크 테마반응형 UI, 커스텀 색상 토큰
폰트Playfair Display + Noto Serif KR / Inter / JetBrains Mono제목(serif) / 본문(sans) / 데이터(mono)
DBPrisma 5 + Supabase PostgreSQLORM, 스키마 관리, 마이그레이션
스토리지Cloudflare R2 (S3 호환)원본/대형/썸네일 이미지 저장
이미지 처리sharp서버사이드 리사이즈 (1920px large, 400px thumb, WebP)
EXIF 파싱exifr클라이언트 사이드 EXIF 메타데이터 추출
인증jsonwebtoken + HttpOnly Cookie관리자 JWT 인증 (7일 만료)
배포Vercel서버리스 함수, 자동 빌드/배포

디자인 시스템

색상 토큰

토큰용도
bg#0d0d0d페이지 배경 (near-black)
bg-surface#1a1a1a카드/입력 필드 배경
border#2e2e2e모든 테두리/구분선
text#e8e0d4기본 텍스트 (warm off-white)
text-muted#8a8078보조 텍스트
accent#c8a87a링크, 버튼, 강조 (warm gold)
accent-hover#d4b88aaccent 호버 상태
danger#b05a5a삭제, 에러

폰트

  • font-serif: Playfair Display, Noto Serif KR — 제목, 카메라 이름
  • font-sans: Inter — 본문 기본
  • font-mono: JetBrains Mono — EXIF 값, 날짜, 사진 수

데이터베이스 스키마

Camera (cameras 테이블)

필드타입설명
idString @id슬러그 (예: leica-m6)
nameString카메라 이름 (예: Leica M6)
makerString제조사 (예: Leica)
typeCameraTypeMIRRORLESS | DSLR | COMPACT | MEDIUM_FORMAT | SMARTPHONE | FILM
yearStartInt사용 시작 연도
monthStartInt?사용 시작 월 (1-12)
yearEndInt?사용 종료 연도 (null = 현재 사용중)
monthEndInt?사용 종료 월 (1-12)
descriptionString @db.Text카메라 설명
coverImageString커버 이미지 URL
orderInt정렬 순서
photoCountInt사진 수 (캐시)

Photo (photos 테이블)

필드타입설명
idString @id @default(uuid)UUID
filenameString원본 파일명
urlOriginal / urlLarge / urlThumbStringR2 이미지 URL (원본/1920px/400px WebP)
cameraMake / cameraModelString?EXIF 카메라 정보
lens / focalLength / aperture / shutterSpeedString?EXIF 렌즈 정보
isoInt?ISO 감도
takenAtDateTime?촬영 일시
gpsLat / gpsLngFloat?GPS 좌표
cameraIdString (FK → Camera)소속 카메라
locationTagString?장소 태그
isFeaturedBoolean홈페이지 Featured 표시 여부
isDeletedBoolean소프트 삭제 여부

페이지 구조

공개 페이지

경로설명렌더링
/홈: Featured 사진 그리드 + 카메라 목록 + 타임라인Server (force-dynamic)
/archive사진 갤러리: 무한 스크롤 Masonry + 필터 (카메라/연도/렌즈)Client
/archive/[id]사진 상세: 큰 이미지 + EXIF 메타데이터Server + generateMetadata
/dev-notes개발 기록 타임라인Server
/prompt프로젝트 명세 (이 페이지)Server

관리자 페이지 (인증 필요)

경로설명
/admin관리자 대시보드 (카메라 관리 / 사진 업로드 링크)
/admin/login로그인 (ID + 비밀번호)
/admin/cameras카메라 목록 관리 (편집/삭제)
/admin/cameras/new카메라 신규 등록
/admin/cameras/[id]카메라 편집 (커버 이미지 업로드 포함)
/admin/upload사진 업로드 (드래그앤드롭, EXIF 자동 파싱, 카메라 자동 매칭)

API 라우트

공개 API

메서드경로설명
GET/api/cameras전체 카메라 목록
GET/api/cameras/[id]카메라 상세
GET/api/photos사진 목록 (커서 기반 페이지네이션, 필터 지원)
GET/api/photos/[id]사진 상세
GET/api/filters필터 옵션 (카메라, 렌즈, 연도)

관리자 API (인증 필요)

메서드경로설명
POST/api/admin/login로그인 → JWT HttpOnly Cookie 발급
POST/api/admin/logout로그아웃 → 쿠키 삭제
GET/api/admin/me인증 상태 확인
POST/api/admin/cameras카메라 생성
PUT/api/admin/cameras/[id]카메라 수정
DELETE/api/admin/cameras/[id]카메라 삭제
POST/api/admin/cameras/[id]/cover커버 이미지 presigned URL 발급
PATCH/api/admin/cameras/[id]/cover커버 이미지 URL DB 저장
PATCH/api/admin/photos/[id]사진 메타데이터 수정
DELETE/api/admin/photos/[id]사진 소프트 삭제
POST/api/admin/upload/presign사진 업로드 presigned URL 발급 (복수)
POST/api/admin/upload/complete업로드 완료 처리 (썸네일 생성, DB 저장, 카메라 매칭)

핵심 기능 상세

사진 업로드 흐름

  1. 1.클라이언트에서 이미지 선택 → exifr로 EXIF 파싱 (카메라, 렌즈, 조리개, 셔터속도, ISO, 촬영일시, GPS)
  2. 2.POST /api/admin/upload/presign → R2 presigned PUT URL 발급 (originals/{year}/{month}/{uuid}.{ext})
  3. 3.XHR PUT으로 R2에 직접 업로드 (진행률 표시)
  4. 4.POST /api/admin/upload/complete → 서버에서 sharp로 large(1920px)/thumb(400px) WebP 생성
  5. 5.카메라 자동 매칭: __auto__ 모드 시 EXIF 카메라 정보로 findOrCreateCamera() 호출
  6. 6.EXIF 데이터 타입 안전 처리 (safeStr, safeInt, safeFloat, safeDate) 후 DB 저장
  7. 7.maxDuration: 60초로 Vercel 서버리스 타임아웃 확장

카메라 자동 생성 (findOrCreateCamera)

  1. 1.EXIF cameraModel → 슬러그 생성 (예: 'LEICA Q3 43' → 'leica-q3-43')
  2. 2.EXIF cameraMake → MAKER_MAP으로 정규화 (예: 'LEICA CAMERA AG' → 'Leica')
  3. 3.키워드 기반 카메라 타입 추론 (스마트폰/중형/컴팩트/미러리스)
  4. 4.prisma.camera.upsert — 이미 존재하면 무시, 없으면 생성

커버 이미지 업로드

  1. 1.카메라 편집 페이지에서 'Choose Image' 클릭
  2. 2.POST /api/admin/cameras/[id]/cover → presigned URL (covers/{cameraId}/{uuid}.{ext})
  3. 3.XHR PUT으로 R2 직접 업로드 (진행률 바 표시)
  4. 4.PATCH /api/admin/cameras/[id]/cover → DB에 public URL 저장
  5. 5.미리보기 즉시 갱신

인증 체계

  • 관리자 ID/비밀번호를 환경변수로 관리 (ADMIN_ID, ADMIN_PASSWORD)
  • 로그인 성공 시 JWT 생성 (jsonwebtoken, 7일 만료) → HttpOnly Cookie 설정
  • 미들웨어: /admin/* 경로 접근 시 쿠키 확인, 없으면 /admin/login으로 리다이렉트
  • API 라우트: isAuthenticated(request)로 쿠키의 JWT 검증
  • 클라이언트: /api/admin/me로 인증 상태 확인 → 조건부 관리자 UI 표시

컴포넌트

컴포넌트파일설명
Headersrc/components/Header.tsx네비게이션 바 (Cameras, Archive, Dev Notes, Admin/Logout). 클라이언트에서 /api/admin/me 호출로 인증 상태 반영
Footersrc/components/Footer.tsx저작권 표시 + Dev Notes, Prompt 링크
Lightboxsrc/components/Lightbox.tsx사진 뷰어 (75vw×75vh 고정 박스). 키보드 네비게이션 (ESC/←/→), EXIF 데이터 하단 표시
PhotoEditModalsrc/components/PhotoEditModal.tsx사진 메타데이터 인라인 편집 모달 (카메라, 장소, 렌즈, EXIF 값)

환경변수

변수명설명
DATABASE_URLSupabase PostgreSQL 연결 URL (pooled)
DATABASE_URL_UNPOOLEDSupabase PostgreSQL 직접 연결 URL (마이그레이션용)
ADMIN_ID관리자 로그인 아이디
ADMIN_PASSWORD관리자 로그인 비밀번호
JWT_SECRETJWT 서명 키
R2_ACCOUNT_IDCloudflare 계정 ID
R2_ACCESS_KEY_IDR2 API 접근 키
R2_SECRET_ACCESS_KEYR2 API 시크릿 키
R2_BUCKET_NAMER2 버킷 이름
R2_PUBLIC_URLR2 퍼블릭 CDN URL

R2 스토리지 키 구조

originals/{year}/{month}/{uuid}.{ext}    # 원본 이미지
large/{year}/{month}/{uuid}.webp          # 리사이즈 (1920px, WebP 85%)
thumbs/{year}/{month}/{uuid}.webp          # 썸네일 (400px, WebP 80%)
covers/{cameraId}/{uuid}.{ext}            # 카메라 커버 이미지

디렉토리 구조

src/
├── app/
│   ├── page.tsx                    # 홈 (카메라 목록 + Featured + 타임라인)
│   ├── archive/
│   │   ├── page.tsx                # 갤러리 (Masonry + 무한스크롤 + 필터)
│   │   └── [id]/page.tsx           # 사진 상세
│   ├── dev-notes/page.tsx          # 개발 기록
│   ├── prompt/page.tsx             # 프로젝트 명세 (이 페이지)
│   ├── admin/
│   │   ├── page.tsx                # 관리자 대시보드
│   │   ├── login/page.tsx          # 로그인
│   │   ├── upload/page.tsx         # 사진 업로드
│   │   └── cameras/
│   │       ├── page.tsx            # 카메라 목록 관리
│   │       ├── new/page.tsx        # 카메라 신규 등록
│   │       └── [id]/page.tsx       # 카메라 편집
│   └── api/
│       ├── cameras/               # 공개 카메라 API
│       ├── photos/                # 공개 사진 API
│       ├── filters/               # 필터 옵션 API
│       └── admin/                 # 관리자 API (인증/카메라/사진/업로드)
├── components/
│   ├── Header.tsx                  # 네비게이션 바
│   ├── Footer.tsx                  # 푸터
│   ├── Lightbox.tsx                # 사진 뷰어
│   └── PhotoEditModal.tsx          # 사진 편집 모달
├── lib/
│   ├── auth.ts                     # JWT 인증
│   ├── prisma.ts                   # Prisma 클라이언트 싱글턴
│   ├── r2.ts                       # R2 클라이언트 + presigned URL
│   ├── camera.ts                   # 카메라 자동 생성/추론
│   └── utils.ts                    # cn() 유틸리티
└── middleware.ts                    # /admin/* 인증 미들웨어