NFC 카드 API 맥락

스펙 원본은 frontmatter spec:. 구현 전체 계약은 as-built 통합 구현 명세 §4와 self-claim 명세self-claim(2026-07-23, V9)이 as-built(07-22)보다 최신이므로 resolve 상태는 self-claim 명세가 기준. 이 문서는 왜 그렇게 정했는지와 연동 시 밟기 쉬운 것만.

한눈에

엔드포인트역할 한 줄
GET /v1/public/nfc-tags/{token}태그 → 4상태(CONNECTED/CLAIMABLE/NEEDS_CONNECT/UNAVAILABLE) 판정
POST /v1/me/nfc-cards/connect연결. 미배정 개방 카드면 선점+연결을 원자적으로 수행
POST /v1/me/nfc-cards/{cardId}/report-lost · /recover분실 모드 ON/OFF
POST /v1/admin/nfc-cards · /{cardId}/assign · /self-claim발급(선점 개방 플래그), 사전 배정, 발급 후 개방 전환

왜 이렇게 생겼나

  • 원시 상태를 소수 상태로 접는다. 판정은 BE NfcTagResolutionPolicy 한 곳 — FE는 result별 분기만. 카드 없음·비활성·분실·폐기·명함 비공개는 전부 UNAVAILABLE + reason.
  • 소유(claim)와 연결(connect)은 다른 개념. claim = assigned_member_id(소유권), connect = business_card_id(명함 링크). 관리자 assign은 claim만 해서 NEEDS_CONNECT의 원천이 되고, self-claim 경로는 “회원은 명함을 정확히 1개 갖는다” 불변식 덕에 claim+connect가 한 요청에서 원자적으로 함께 일어난다.
  • CLAIMABLE은 opt-in. 미배정 재고가 무조건 선점 가능하면 법인용 예약 재고를 제3자가 가로챈다 → 카드별 self_claim_enabled(기본 false)로 공개 배포용만 개방. 예약 재고는 계속 UNAVAILABLE + NOT_ISSUED.
  • NFC 상태와 명함 공개 상태는 분리. 카드 분실·폐기여도 명함 PUBLIC이면 /card/{slug} 직접 URL은 열린다. NFC는 유입 경로지 접근 권한이 아니다.

불변조건

  • result별 필수 필드: CONNECTED.slug / CLAIMABLE.token / NEEDS_CONNECT.token / UNAVAILABLE.reason — FE가 런타임 검증, 위반은 재시도 오류 처리.
  • reasonNOT_FOUND | NOT_ISSUED | INACTIVE | LOST | REVOKED | NOT_PUBLISHED.
  • 저장소 timeout·내부 오류를 UNAVAILABLE로 변환하지 않는다 — 실제 5xx를 반환해야 FE가 재시도 화면과 사용 불가 안내를 구분.
  • first-tap-wins는 조건부 UPDATE로만: UPDATE ... WHERE assigned_member_id IS NULL AND self_claim_enabled = true, 영향 행 0 = 경합 패배. read-then-write 분기 금지(경합 창 생김). 같은 회원 재시도는 멱등(같은 CONNECTED 응답).
  • 소유권 경합·타인 소유는 전부 NF0004로 수렴(403 타인 소유, 409 경합 패배 — code 동일, status만 다름).
  • connect 성공 시 미발행 명함 자동 발행 — 셀프 선점한 신규 회원이 방문자에게 NOT_PUBLISHED로 막히지 않게. 이미 발행된 명함의 발행 시각은 유지(의도적 비공개 존중).
  • NFC token은 로그에 원문으로 남기지 않는다(마스킹).

엣지케이스 · gotcha

  • visitId 멱등성: React Query 재시도가 NFC_TAP을 중복 집계하던 문제 → FE는 sessionStorageanalyticsNfcVisitId:{token} 값을 모든 재시도에 동일하게 전송, BE는 nfc:{visitId} 키로 중복 제거. 기록 조건 3개 전부 필요: CONNECTED + businessCardId 확인 + visitId가 UUID/ULID 형식.
  • visitId 형식이 잘못돼도 resolve는 정상 수행 — 이벤트만 안 남는다. 분석 수치 누락이면 이것부터 의심.
  • NF0006(명함 데이터 비정상) 경로에서 선점은 이미 확정된 채 남는다 — 연결만 실패. 운영에서 발견 시 데이터 정합 확인 필요.
  • 발급 후 개방 전환(POST /v1/admin/nfc-cards/{cardId}/self-claim)은 IN_STOCK에만 허용 — 배정·연결·불능 상태는 409 NF0007. cardId는 admin 식별자지 token(cardUid)이 아니다.
  • 비로그인 NEEDS_CONNECT/CLAIMABLE 흐름: 복귀 경로 저장 → /login → 같은 /nfc/{token} 재진입.

관련 문서