개발 노트
카메라 아카이브 사이트 324.ing의 개발 기록. 내가 쓴 카메라들을 정리하다가 만들기 시작한 개인 프로젝트.
2026-02-28
프로젝트 시작
- —카메라 아카이브 프로젝트 초기 스캐폴딩
- —Next.js 14 App Router + TypeScript + Tailwind CSS 기반 설정
- —Prisma ORM + Supabase PostgreSQL 데이터베이스 연결
- —Cloudflare R2 파일 스토리지 연동 (presigned URL 방식)
- —JWT 기반 관리자 인증 시스템 구현
- —핵심 스키마 설계: Camera, Photo, Project, SiteSettings 모델
- —Portfolio 시스템 구현: Work / Moment 두 가지 프로젝트 타입
- —ContentBlock 모델로 자유로운 콘텐츠 구성 (HEADING, TEXT, IMAGE, IMAGE_GRID, SLIDESHOW 등)
- —Vercel 빌드 시 prisma generate 자동 실행 설정
2026-03-11
디자인 & 테마
- —화이트 미니멀 테마 적용 — 배경 #FAFAF8, 텍스트 계층화
- —세리프 계열 헤딩 + 모노스페이스 메타데이터로 사진 아카이브 분위기 구성
- —Tailwind CSS 커스텀 컬러 토큰 정의 (text-primary, text-secondary, text-muted, accent, border 등)
- —반응형 레이아웃: 모바일 햄버거 메뉴 포함 SiteNav 구현
2026-03-11
카메라 관리
- —카메라 slug 수정 기능 추가 (ID 변경 시 관련 사진 자동 재연결)
- —사용 기간 월 단위 입력 필드 추가 (monthStart, monthEnd)
- —카메라 커버 이미지 업로드 기능 구현
- —카메라 목록 yearStart, monthStart 기준 오름차순 정렬
2026-03-11
라이트박스
- —라이트박스 오버레이 딤 조정 (50% → 75%), 외부 클릭 시 닫기
- —EXIF 값만 표시하도록 라이트박스 정보 패널 간소화
- —Work/Moment 프로젝트 상세 페이지에 라이트박스 추가
- —공유 라이트박스 컴포넌트로 테마 통일 (BlockRenderer 내부 별도 구현 제거)
- —라이트박스 하단 텍스트 색상 수정 — 어두운 배경 위 가독성 개선
- —EXIF 메타데이터 패널 가운데 정렬 + 공유 링크 하단 배치
2026-03-11
히어로 섹션
- —홈 히어로: 16/9 비율 → 전체 화면 → nav 아래 뷰포트 높이로 순차 개선
- —헤딩 오버레이 위치 좌하단 고정
- —Work/Moment 목록 페이지에 히어로 섹션 추가 (사진 + 문구)
- —관리자 설정 페이지에서 Work/Moment 히어로 이미지 및 문구 편집 기능 구현
- —히어로 문구에 Markdown 렌더링 적용 (react-markdown v10)
- —remark-breaks 플러그인 추가 — 단일 줄바꿈( )도 <br>로 변환
- —Prisma 스키마 마이그레이션: workHeroPhotoId, workHeroText, momentHeroPhotoId, momentHeroText 필드 추가
2026-03-11
내비게이션 & UX
- —모든 페이지에 Breadcrumb 내비게이션 추가
- —프로젝트 커버 이미지 픽커 구현 (Work/Moment)
- —페이지 전환 시 상단 Progress Bar 추가 (NavigationProgress 컴포넌트)
- —링크 클릭 감지 → pathname 변경 감지 → 완료 애니메이션 방식
- —진행 바 색상: 사이트 accent(#7c5c3a), glow 효과 적용
2026-03-11
분석 코드 주입
- —관리자 설정에 headerCode, bodyCode 필드 추가
- —layout.tsx에서 DB에서 코드를 읽어 <head>/<body>에 동적 삽입
- —<script> 태그 파싱하여 React 노드로 변환 (Server Component 호환)
- —1시간 캐시 + 태그 기반 무효화로 성능 최적화
2026-03-11
보안 강화
- —관리자 API 전체 인증 미들웨어 점검 및 누락 항목 수정
- —JWT 토큰 검증 강화, 쿠키 httpOnly/secure/sameSite 설정
- —SQL injection 방지 — Prisma ORM 파라미터 바인딩 일관 적용
- —XSS 방지 — dangerouslySetInnerHTML 사용 범위를 관리자 전용 코드 주입으로 제한
2026-03-11
성능 최적화
- —DB 인덱스 추가: isDeleted, takenAt, cameraId 복합 인덱스
- —이미지 사이즈 최적화 및 Next.js Image 컴포넌트 sizes 속성 정교화
- —unstable_cache 도입 — 모든 공개 페이지 DB 쿼리 캐싱
- —관리자 API에 revalidateTag 추가 — 데이터 변경 시 즉시 캐시 무효화
- —React.cache()로 getAdminSession 요청 내 중복 호출 제거
- —SiteNav를 root layout으로 이동 — 클라이언트 사이드 내비게이션 시 nav 재렌더링 방지
- —app/loading.tsx 추가 — 서버 렌더링 중 즉각적인 스켈레톤 피드백
- —ProjectCard, photoFilters 공통 모듈 추출 (코드 중복 제거)
2026-03-11
기타 개선
- —비로그인 상태에서 Admin 링크를 메인 내비게이션에서 제거
- —사이트 하단 Footer 추가 — 카피라이트 + 개발 노트 링크
- —개발 노트 페이지 작성 (현재 페이지)
2026-03-13
포토북 라이브러리
- —Sanity CMS 연동 — 포토북 데이터 관리 (projectId: emspj2jw)
- —/photobooks 목록 페이지 + /photobooks/[slug] 상세 페이지 구현
- —어드민 포토북 관리 페이지 추가 (/admin/photobooks) — Sanity CRUD
- —포토북 카드 컴포넌트, 메타데이터 뷰어, 관련 도서 섹션 구현
- —카테고리 필터 + 정렬 기능 (연도, 작가, 출판사)
- —책 상세 페이지에서 관리자 로그인 상태 시 편집 버튼 노출
- —?edit=slug 파라미터로 어드민 편집 모달 자동 열기
2026-03-13
업로드 & 스토리지 개선
- —ProjectUploadZone CORS 오류 수정 — presigned URL 방식 → 서버사이드 /api/admin/upload 방식으로 전환
- —불필요한 /api/admin/upload/presign 엔드포인트 제거
- —업로드 API 파일 크기 검증 추가 (사진 50MB, 커버 5MB)
- —EXIF JSON 파싱 try-catch 방어 로직 추가
2026-03-13
UI 개선
- —라이트박스 닫기 버튼 간소화 — 원형 버튼 → 18px SVG X 아이콘
- —Work / Moment 상세 페이지 최대 너비 max-w-3xl → 1200px로 확장
- —toSlug 유틸 lib/slug.ts로 통합 — 한글 지원 버전으로 일원화
2026-03-13
보안 강화
- —middleware.ts — JWT_SECRET 미설정 시 즉시 401 반환 방어 로직 추가
- —layout.tsx bodyCode XSS 수정 — dangerouslySetInnerHTML 제거, parseHeadScripts() 안전 처리
- —공개 API 라우트 전체 try-catch 추가 (cameras, photos, filters)
- —CLAUDE.md 작성 — 프로젝트 전체 구조 문서화 (라우트, 데이터 레이어, 업로드 플로우, 보안 정책)
2026-03-13
Google OAuth 어드민 로그인
- —어드민 로그인 페이지에 Google 로그인 버튼 추가
- —어드민 설정 페이지에 Google 계정 연동 / 해제 섹션 추가
- —/api/auth/google — OAuth 시작 라우트 (login / connect 두 가지 모드)
- —/api/auth/google/callback — 인가 코드 교환, 이메일 검증, JWT 쿠키 발급
- —SiteSettings.googleEmail 필드 추가 — 연동된 Google 이메일 저장
- —등록된 이메일만 로그인 허용, 미등록 계정 접근 차단
2026-03-13
버그 수정 — unstable_cache Date 직렬화 오류
- —증상: 5분 캐시 만료 후 Moment/Work 상세 페이지 접속 불가 (RSC 스트림 오류, digest 1830721307)
- —원인: unstable_cache는 캐시 데이터를 JSON 직렬화하여 저장 → Date 객체가 ISO 문자열로 변환됨. 캐시 히트 시 string이 반환되는데 string에는 .toISOString()이 없어 TypeError 발생
- —패턴: 첫 요청(캐시 미스) → DB에서 Date 객체 → 정상 동작 / 이후 요청(캐시 히트) → string 반환 → .toISOString() TypeError
- —수정: photo.takenAt, project.publishedAt 등 Date 필드를 모두 new Date(value).toISOString() 방식으로 교체
- —재발 방지: lib/dateUtils.ts에 safeISOString() 유틸리티 추가 — Date | string | null 모두 안전하게 처리. unstable_cache 사용 시 Date 필드는 반드시 이 함수를 통해 변환
Stack — Next.js 14 · Prisma · Supabase · Cloudflare R2 · Tailwind CSS · Vercel · Sanity CMS · Google OAuth