공개 명함 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를 분리한 이유는 데이터 최소화:
- OG/HTML 층 (
/card/*, share-preview): 이름·직함·회사·테마만. 전화번호·이메일은 이 층의 코드 경로에 아예 존재하지 않는다 — “전체 DTO 만들고 필드 제거” 방식 금지, 허용 필드만 직접 select. - React 전체 층 (
/v1/public/business-cards/{slug}): 연락처 포함 전체 공개 데이터 +viewer(로그인·소유자 여부). 방문자마다 응답이 달라 항상no-store. - 이력: 원안(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=1URL도 항상 현재 공개 상태를 재조회 — 현재 비공개면 개인화 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 미실행이므로 자연 성립).
관련 문서
- 구현 심화(스포크): as-built 통합 구현 명세, 배포 계약
- FE 배포: public-card-document-deployment.md
- CSP 상세: as-built §6.3
- 유입 경로: nfc-card.md · 이벤트: analytics.md