공개 명함과 NFC 라우팅은 어떻게 분리되는가
이 문서는 공개 명함과 근거리 무선 통신(NFC) 태그 진입 경로의 책임, 상태 분기, 분석 이벤트를 현재 프론트엔드 구현 기준으로 정리한다. NFC 경로는 물리 카드 상태를 해석하고, 공개 명함 경로는 명함 조회와 방문자별 행동을 처리한다.
이 문서가 다루는 범위
이 레퍼런스는 프론트엔드와 백엔드 개발자가 라우트별 책임과 전이 조건을 같은 기준으로 확인하도록 돕는다.
공유 링크의 Open Graph 문서와 배포 계약은 공개 명함 문서 배포에서 확인한다.
- 목표: NFC 태그부터 공개 명함과 후속 행동까지의 분기를 설명한다
- 대상: 프론트엔드, 백엔드, QA 담당자
- 포함 범위: 공개 라우트, resolve 결과, 로그인 복귀, 공개 명함 상단바, 분석 이벤트
- 제외 범위: 관리자 카드 발급과 마이페이지의 분실 모드 변경 절차
- 상태: 2026-07-22 백엔드 as-built 계약과 현재 프론트 구현 기준
라우트별 책임
각 라우트는 NFC 카드 확인, 공개 명함 조회, 인증, 편집 중 하나의 책임을 가진다.
| 경로 | 현재 책임 | 주요 입력 |
|---|---|---|
/nfc/{token} | NFC resolve, 미연결 카드 안내와 연결 처리 | 물리 카드의 cardUid |
/card/{slug} | 공개 명함 조회, 상태별 상단바, 분석 이벤트 전송 | 명함 slug |
/card-unavailable | NFC resolve가 판정한 사용 불가 안내 | 없음 |
/login | 로그인 후 저장된 NFC 경로로 복귀 | postLoginRedirect |
/auth/google/complete | 로그인 티켓 교환과 복귀 경로 결정 | OAuth ticket |
/live-studio | 명함 생성과 편집 | 로그인 세션 |
/live-preview | 편집 중인 명함 미리보기 | 로컬 draft와 내 명함 조회 |
현재 구현에는 /nfc/{token}/connect, /u/{slug}, /analytics 라우트가 없다. 미연결 카드 처리는 /nfc/{token} 안에서 수행하며, 공개 명함 상단바는 방문자 상태에 따라 로그인, 내 명함 편집, 로그아웃을 노출한다.
NFC 태그 진입 흐름
/nfc/{token}은 GET /public/nfc-tags/{token}의 resolve 결과에 따라 공개 명함, 연결 안내, 사용 불가 안내로 분기한다.
flowchart TD A[NFC 카드 태그] --> B["/nfc/{token}<br/>NFC 태그 진입"] B --> C["GET /public/nfc-tags/{token}"] C --> D{resolve 결과} D -->|CONNECTED + slug| E["/card/{slug}?source=nfc"] D -->|NEEDS_CONNECT| F["/nfc/{token}<br/>연결 안내 표시"] D -->|UNAVAILABLE| G["/card-unavailable"] C -->|네트워크 / 5xx| M[재시도 화면] F --> H{로그인 여부} H -->|비로그인| I["복귀 경로 저장 후 /login"] I --> J["/auth/google/complete"] J --> B H -->|로그인| K["POST /me/nfc-cards/connect"] K -->|성공 + slug| E K -->|NF0001 / NF0002 / NF0005 / 검증 실패| G K -->|NF0004| L["발급 계정 불일치 안내<br/>다른 계정으로 로그인"] K -->|NF0006| N["명함 데이터 비정상 안내<br/>동일 요청 재시도"] K -->|네트워크 / 5xx| M L --> I N --> K
프론트엔드는 NFC 카드의 원시 상태 대신 백엔드가 반환한 resolve 결과를 처리한다.
| resolve 결과 | 세부 사유 | 현재 처리 |
|---|---|---|
CONNECTED | 해당 없음 | slug로 공개 명함 이동 |
NEEDS_CONNECT | 해당 없음 | 같은 NFC 페이지에서 연결 안내 표시 |
UNAVAILABLE | NOT_FOUND | 사용 불가 페이지 이동 |
UNAVAILABLE | NOT_ISSUED | 발급 처리 전 카드 안내 페이지 이동 |
UNAVAILABLE | INACTIVE | 사용 불가 페이지 이동 |
UNAVAILABLE | LOST | 사용 불가 페이지 이동 |
UNAVAILABLE | REVOKED | 사용 불가 페이지 이동 |
UNAVAILABLE | NOT_PUBLISHED | 사용 불가 페이지 이동 |
| 요청 실패 | 네트워크 / 5xx | 오류 종류에 맞는 재시도 화면 표시 |
백엔드는 원시 카드와 명함 상태를 아래의 현재 계약대로 resolve 결과로 변환한다.
| 서버의 원시 상태 | 기대하는 resolve 결과 |
|---|---|
CONNECTED + 공개 명함 | CONNECTED + slug |
UNCONNECTED | NEEDS_CONNECT |
| 발급 전 재고 | UNAVAILABLE + NOT_ISSUED |
INACTIVE | UNAVAILABLE + INACTIVE |
LOST | UNAVAILABLE + LOST |
REVOKED | UNAVAILABLE + REVOKED |
| 명함 비공개 | UNAVAILABLE + NOT_PUBLISHED |
| 명함 삭제·탈퇴 소유자 | UNAVAILABLE + NOT_PUBLISHED |
| 카드 없음 | UNAVAILABLE + NOT_FOUND |
프론트엔드는 UNAVAILABLE의 허용된 세부 사유만 검증해
/card-unavailable?reason={reason}으로 전달하고, 사유별 한국어 문구를 표시한다. 알 수 없는
검색 조건은 버리고 개인정보가 없는 기본 문구를 사용한다.
로그인 전에 저장한 /nfc/{token} 경로는 신규 가입자의 /taple-introduction보다 우선한다.
로그인 완료 후 NFC resolve를 다시 호출하고 카드 연결을 이어간다. 복귀 주소는 단일 슬래시로
시작하는 서비스 내부 경로만 허용하며 외부 URL과 프로토콜 상대 URL은 폐기한다.
연결 요청에는 resolve가 반환한 token만 보낸다. 로그인 회원과 NFC 카드의 발급 대상이 같은지는
백엔드가 Access Token과 카드 발급 정보를 비교해 판단하므로 프론트는 memberId나
businessCardId를 보내지 않는다. NF0004는 구매 계정으로 다시 로그인하도록 안내하고,
회원가입 시 자동 생성되는 명함이 누락되어 NF0006이 발생하면 데이터 비정상 상태로 보고 같은
연결 요청을 다시 시도할 수 있게 한다.
공개 명함 상단바 흐름
/card/{slug}는 공개 상태를 확인한 뒤 viewer 관계에 따라 상단바 행동을 결정한다. 기존 하단 계정 CTA는 노출하지 않는다.
flowchart TD A["/card/{slug}"] --> B["GET /public/business-cards/{slug}"] B --> C{조회 결과} C -->|404| D[명함 없음 안내] C -->|네트워크 / 5xx| E[재시도 가능한 오류] C -->|visibility PRIVATE<br/>개인정보 없는 상태 응답| F[비공개 명함 안내] C -->|visibility PUBLIC| G[공개 명함 렌더링] G --> H{viewer 상태} H -->|isOwner| I["내 명함 편집 → /live-studio"] H -->|로그인 + 타인 명함| J["로그아웃 → 확인 모달"] J --> K["로그아웃 완료 → /login"] H -->|비로그인| L["로그인 → /login"]
상단바 분기는 isOwner를 먼저 확인한다. 소유자는 hasOwnCard 값과 관계없이 내 명함 편집을 보고, 로그인한 다른 방문자는 로그아웃, 비로그인 방문자는 로그인을 본다.
분석 이벤트 흐름
공개 명함은 방문과 방문자 행동을 POST /analytics/events로 전송한다. 이벤트 전송 실패는 화면 동작을 막지 않는다.
flowchart LR A[공개 명함 진입] -->|PAGE_VIEW| E["POST /analytics/events"] B[명함 저장] -->|CARD_DOWNLOAD| E C[공유] -->|SHARE_CLICK| E D[링크 클릭] -->|LINK_CLICK| E
NFC에서 공개 명함으로 이동하면 URL에 source=nfc를 붙인다. 공개 명함은 PAGE_VIEW의
sourceType을 NFC로 기록하며, 직접 진입은 DIRECT로 기록한다. 공유 URL의
source=share&v={revision}은 sourceType=SHARE, shareRevision={revision}으로 기록한다.
NFC_TAP은 NFC resolve 성공 시 백엔드가 유효한 visitId로 중복 제거해 기록한다. 프론트는
NFC_TAP을 전송하지 않고, 공개 명함 진입의 PAGE_VIEW + sourceType=NFC만 전송한다.
공개 명함 공유 문서 구조 (구현 완료)
/nfc/{token}은 React 프론트 라우트로 유지한다. /card/{slug}의 실제 화면도 기존 React가
계속 렌더링하지만, 최초 HTML은 Caddy가 백엔드 PublicCardDocument로 보내 OG 태그와 함께
반환한다. React는 실행 후 공개 전체 API만 조회하며 내부 공유 projection은 호출하지 않는다.
백엔드는 전체 공개 명함 응답을 재사용하지 않고 이름·직함·회사처럼 합의된 필드만 반환한다. 비공개 상태는 개인정보 없는 고정 응답, 삭제·탈퇴·없는 slug는 같은 404, 운영 장애는 5xx로 구분한다. 상세 API와 캐시·이미지 무효화 기준은 공개 명함 문서 배포를 따른다.
구현된 보강 항목
아래 항목은 현재 흐름의 모호한 상태와 관측 공백을 줄인다.
/card-unavailable은 비공개 명함과 다른 중립 문구를 표시한다.- NFC resolve 응답은
result별 필수 필드를 런타임 검증하며 잘못된 응답은 재시도 오류가 된다. - Google 로그인은 저장된 내부
/nfc/{token}경로로 복귀한다. - 프론트는 유효한 UUID visitId를 같은 탭·token의 재시도에서 재사용한다.
- 네트워크·5xx는 사용 불가 상태로 바꾸지 않고 재시도 화면을 표시한다.
- 세 resolve 상태, 사유별 사용 불가 상태, 로그인 복귀와 연결 오류 code를 페이지 테스트로 고정한다.
- 연결 성공 응답은
CONNECTED와 non-null 식별자를 런타임 검증하고 서버 영문 message를 노출하지 않는다. - QA 미연결 카드는 기존
POST /me/qa/nfc-cards에{}를 보내 현재 로그인 회원에게 발급한다.
코드 기준 위치
현재 동작을 변경할 때 다음 파일과 이 문서를 함께 갱신한다.
- NFC 진입 분기:
src/features/liveStudio/pages/NfcTagEntryPage.tsx - 공개 명함 분기:
src/features/liveStudio/pages/PublicBusinessCardPage.tsx - 공개 명함 상단바:
src/features/liveStudio/components/PublicCardHeader/PublicCardHeader.tsx - 로그인 복귀:
src/features/login/domain/loginComplete.ts - 라우트 생성 함수:
src/shared/constants/routes.ts - NFC와 분석 API 타입:
src/features/liveStudio/domain/api.ts