공개 명함·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 방식으로 구현됨.