공개 명함 API 맥락

스펙 원본은 frontmatter spec:. 필드 단위 계약 전체는 as-built 통합 구현 명세가 기준 (계획 문서와 코드가 다르면 as-built 기준 커밋의 코드가 우선). 이 문서는 세 층 구조의 이유, 개인정보 경계, 연동 gotcha만.

한눈에

엔드포인트역할 한 줄
GET /card/{slug}상태별 OG 태그 + React 부트스트랩 HTML (크롤러·브라우저 공용)
GET /v1/internal/business-cards/{slug}/share-preview서버 간 최소 projection (서비스 Bearer token)
GET /v1/public/business-cards/{slug}React 화면 전체 데이터 (JWT optional → viewer)
GET /v1/public/business-cards/{slug}/og-image현재 PUBLIC 확인 후 1200×630 PNG 온디맨드 생성
GET /v1/public/og/default.png개인정보 없는 공통 PNG (30일 캐시)
PATCH /v1/me/business-card/slug사용자 지정 slug 변경 (낙관적 version + HMAC 영구 예약)

왜 이렇게 생겼나 — 세 층 분리

같은 명함 데이터가 노출 범위가 다른 세 소비자에게 나간다. 층마다 API를 분리한 이유는 데이터 최소화:

  1. OG/HTML 층 (/card/*, share-preview): 이름·직함·회사·테마만. 전화번호·이메일은 이 층의 코드 경로에 아예 존재하지 않는다 — “전체 DTO 만들고 필드 제거” 방식 금지, 허용 필드만 직접 select.
  2. React 전체 층 (/v1/public/business-cards/{slug}): 연락처 포함 전체 공개 데이터 + viewer(로그인·소유자 여부). 방문자마다 응답이 달라 항상 no-store.
  3. 이력: 원안(FE Edge가 HTML 생성)은 과거 제안(해체됨, git 이력 참조)이었으나, 구현은 BE가 /card/{slug} HTML을 직접 생성하는 것으로 바뀜(같은 프로세스에서 projection UseCase 호출). Edge 분리가 다시 필요해지면 내부 share-preview HTTP API를 쓰면 된다 — 그래서 내부 API가 남아 있다.

불변조건

  • 공개 응답에 내부 businessCardId 없음. slug + revision + 상대 shareUrl만.
  • 삭제·탈퇴·없는 slug는 외부에서 구분 불가능한 동일 404 (PUBLIC_CARD_NOT_FOUND).
  • 운영 장애(5xx)를 PRIVATE/NOT_FOUND로 바꾸지 않는다 — 재시도 가능 여부를 클라이언트가 구분해야 함.
  • /card/* HTML의 CSP nonce: 헤더와 meta[name="csp-nonce"]가 반드시 같은 값, 응답마다 새로 생성. FE는 이 값을 Emotion cache에 전달. style-src 'unsafe-inline' 없음 — 이 커플링이 깨지면 스타일 전체가 죽는다.
  • og-image는 DB에서 현재 PUBLIC 확인 후에만 개인화 캐시를 읽는다. stale 이미지 허용은 “PUBLIC 확인 성공 + 렌더러만 실패” 단 한 경우. 비공개 전환 뒤 메모리에 이미지가 남아도 다시 노출되지 않는 근거.

엣지케이스 · gotcha

  • v(revision)는 과거 스냅샷 조회 키가 아니다. 외부 플랫폼 캐시 갱신용. 오래된 v=1 URL도 항상 현재 공개 상태를 재조회 — 현재 비공개면 개인화 OG 안 나감.
  • revision은 현재 business_cards.version 재사용 → 공유와 무관한 수정에도 공유 URL의 v가 바뀐다 (알려진 제한, 별도 sharePreviewRevision 분리는 후속).
  • 브라우저는 window.location.href를 공유하지 않는다 — BE가 내려준 shareUrl을 사이트 origin과 결합. og:url·canonical에는 query 안 붙임.
  • reverse proxy 평가 순서가 계약이다: /card/* → BE를 SPA fallback보다 먼저. 순서가 바뀌면 크롤러가 정적 SPA HTML을 받아 OG가 전부 기본값이 된다.
  • slug HMAC 예약 키 회전: 이전 키를 previous-keys에서 제거하면 그 키로 예약된 과거 slug 충돌을 못 찾는다 — 이전 키는 영구 유지.
  • slug 예약어 목록에 card, nfc, login 등 라우트명 포함 — 새 최상위 라우트를 만들면 예약어에도 추가할 것.
  • OG 캐시는 인스턴스 로컬 Caffeine — 다중 인스턴스·재시작 간 공유 없음(알려진 제한).
  • 크롤러 요청은 PAGE_VIEW를 만들지 않는다(React 미실행이므로 자연 성립).

관련 문서