공개 명함·NFC 시스템 전체 흐름

라우팅 소유권(Caddy)부터 상태별 HTTP 분기, 유입 분석까지 한 장. PRD의 요약 다이어그램의 상세판이고, 필드 단위 계약은 as-built가 기준.

flowchart TD
  USER["사용자 또는 공유 크롤러"] --> CADDY{"Caddy 라우팅"}

  CADDY -->|"/nfc/*, /login,<br/>/live-studio 등"| SPA["프론트 SPA"]
  CADDY -->|"/v1/*"| API["백엔드 API"]
  CADDY -->|"/card/*"| CARD_DOC["백엔드 공개 명함 문서"]
  CADDY -->|"/assets/*"| ASSETS["프론트 빌드 자산<br/>app.js · app.css · 이미지 · 폰트"]

  NFC["NFC 카드 태그"] --> NFC_PAGE["프론트<br/>/nfc/{token}"]
  NFC_PAGE --> RESOLVE["백엔드<br/>NFC 상태 조회<br/>visitId 포함"]

  RESOLVE --> NFC_STATE{"NFC 상태"}

  NFC_STATE -->|"CONNECTED"| CARD_MOVE["전체 문서 이동<br/>/card/{slug}?source=nfc"]
  NFC_STATE -->|"CLAIMABLE ·<br/>NEEDS_CONNECT"| LOGIN_STATE{"로그인 상태"}
  NFC_STATE -->|"UNAVAILABLE"| UNAVAILABLE["프론트<br/>사용할 수 없는 카드 안내"]
  NFC_STATE -->|"네트워크 · 5xx"| NFC_ERROR["프론트<br/>재시도 화면"]

  LOGIN_STATE -->|"비로그인"| LOGIN["프론트<br/>/login<br/>NFC 복귀 주소 저장"]
  LOGIN -->|"로그인 성공"| NFC_PAGE

  LOGIN_STATE -->|"로그인"| CONNECT["백엔드<br/>NFC 카드 연결<br/>(CLAIMABLE이면 선점+연결 원자 수행)"]
  CONNECT -->|"연결 성공<br/>미발행 명함 자동 발행"| CARD_MOVE
  CONNECT -->|"명함 없음 · 404"| CREATE_CARD["프론트<br/>Live Studio에서 명함 생성"]
  CREATE_CARD -->|"생성 완료"| NFC_PAGE

  CARD_MOVE --> CADDY
  CARD_DOC --> CARD_STATUS{"명함 상태"}

  CARD_STATUS -->|"PUBLIC"| PUBLIC_HTML["200<br/>개인화 OG가 포함된 HTML"]
  CARD_STATUS -->|"PRIVATE"| PRIVATE_HTML["200<br/>개인정보 없는 기본 OG"]
  CARD_STATUS -->|"NOT_FOUND"| NOT_FOUND_HTML["404<br/>개인정보 없는 기본 OG"]
  CARD_STATUS -->|"5xx"| SERVER_ERROR_HTML["503<br/>개인정보 없는 기본 OG"]

  CRAWLER{"접근 주체"} -->|"카카오 · 슬랙 등"| PREVIEW["HTML의 OG 정보로<br/>공유 미리보기 생성"]
  CRAWLER -->|"일반 브라우저"| REACT["프론트 React 실행"]

  PUBLIC_HTML --> CRAWLER
  PRIVATE_HTML --> CRAWLER
  NOT_FOUND_HTML --> CRAWLER
  SERVER_ERROR_HTML --> CRAWLER

  REACT --> ASSETS
  REACT --> PUBLIC_API["백엔드<br/>GET /v1/public/business-cards/{slug}<br/>로그인 토큰이 있으면 함께 전달"]

  PUBLIC_API --> VIEW_STATE{"조회 결과"}

  VIEW_STATE -->|"PUBLIC"| VIEWER["공개 명함 화면"]
  VIEW_STATE -->|"PRIVATE"| PRIVATE_VIEW["기존 비공개 명함 화면 유지"]
  VIEW_STATE -->|"404"| NOT_FOUND_VIEW["찾을 수 없는 명함 안내"]
  VIEW_STATE -->|"네트워크 · 5xx"| ERROR_VIEW["재시도 화면"]

  VIEWER --> VISITOR{"방문자 상태"}
  VISITOR -->|"비로그인"| LOGIN_HEADER["로그인 버튼"]
  VISITOR -->|"로그인 · 타인 명함"| LOGOUT_HEADER["로그아웃 버튼"]
  VISITOR -->|"명함 소유자"| EDIT_HEADER["내 명함 편집 버튼"]

  VIEWER --> ACTIONS["저장 · 공유 · 링크 클릭"]
  ACTIONS --> ANALYTICS["백엔드 analytics_events"]
  ACTIONS --> SHARE_URL["백엔드가 내려준 card.shareUrl"]
  SHARE_URL --> SHARE["시스템 공유 또는 링크 복사"]
  SHARE --> CADDY

  CSP["/card/* CSP nonce —<br/>헤더와 meta 동일값, unsafe-inline 없음"] -.-> CARD_DOC

읽는 법 (계약 포인트)

  • Caddy 평가 순서가 계약: /card/*가 SPA fallback보다 먼저 — 바뀌면 크롤러가 기본 OG만 받는다. 상세: public-card.md
  • NFC 4상태 분기: CLAIMABLE(self-claim 개방 재고)은 로그인 후 connect에서 선점+연결이 원자적으로 일어난다. 상세: nfc-card.md
  • 크롤러와 브라우저는 같은 HTML — User-Agent 분기 없음. 크롤러는 OG만 읽고, 브라우저만 React 실행.
  • 5xx는 어느 층에서도 PRIVATE/NOT_FOUND로 변환되지 않는다 — 재시도 가능 여부를 클라이언트가 구분해야 하므로.
  • 원본 다이어그램(구현 전 계획판)과의 차이: CLAIMABLE 분기 추가, CSP는 인라인 허용이 아니라 nonce 방식으로 구현됨.