Admin NFC 카드 운영 API 명세

/admin 카드 운영 화면에서 NFC 재고를 등록하고, 회원에게 발급하고, 명함에 연결하거나 삭제하기 위한 백엔드 계약입니다.

공통 규칙

  • Base URL: {VITE_SERVER_API_URL}/admin
  • 모든 요청은 로그인과 ADMIN 권한이 필요합니다.
  • 프론트는 쿠키와 Authorization: Bearer {accessToken}을 함께 전송할 수 있습니다.
  • 날짜는 ISO 8601 문자열을 사용합니다.
  • 성공 응답은 별도 data envelope 없이 아래 타입을 그대로 반환합니다.
  • 카드 태그 횟수는 집계하거나 반환하지 않습니다.

오류 응답

{
  "code": "NF0002",
  "message": "NFC card is not in a connectable state",
  "data": null
}
HTTP코드의미
400DB0003입력값 오류
401DB0001로그인이 필요함
403DB0004관리자 권한이 없음
404US0001발급할 회원을 찾을 수 없음
404BC0001명함을 찾을 수 없음
404NF0001NFC 카드를 찾을 수 없음
409NF0002연결할 수 없는 카드 상태
409NF0003발급할 수 없는 카드 상태
422VALIDATION_FAILED필수 입력값이 없거나 잘못됨
500DB0002처리 중 서버 오류가 발생한 경우

데이터 타입

type AdminNfcCardStatus = 'IN_STOCK' | 'UNCONNECTED' | 'CONNECTED' | 'INACTIVE' | 'LOST' | 'REVOKED'
 
type AdminUserNfcCardStatus = Exclude<AdminNfcCardStatus, 'IN_STOCK'>
 
type AdminUserCardStatus = AdminUserNfcCardStatus | 'NONE'
 
type AdminNfcCard = {
  id: string
  cardUid: string
  label: string
  status: AdminNfcCardStatus
  assignedMemberId: string | null
  assignedAt: string | null
  businessCardId: string | null
  businessCardSlug: string | null
  activatedAt: string | null
  lastTaggedAt: string | null
}
 
type AdminBusinessCard = {
  id: string
  slug: string
  published: boolean
}
 
type AdminUserCardRecord = {
  memberId: string
  displayName: string
  email: string
  role: string
  joinedAt: string
  businessCard: AdminBusinessCard | null
  cardStatus: AdminUserCardStatus
  cardStatuses: AdminNfcCardStatus[]
  lastTaggedAt: string | null
  nfcCardCount: number
}

주소 생성 규칙

백엔드는 주소 전체 대신 명함 slug와 NFC 카드 cardUid를 반환합니다. 프론트는 서비스 도메인을 기준으로 다음 주소를 표시합니다.

공개 주소: {SITE_URL}/card/{businessCard.slug}
NFC 주소:  {SITE_URL}/nfc/{card.cardUid}

cardUid는 NFC 카드 발급 시 서버가 UUID v4로 자동 생성해야 합니다. 프론트 요청에는 UUID 필드가 없습니다.

API 목록

MethodPath용도
GET/admin/card-operations/summary대시보드 요약 조회
GET/admin/users사용자 목록 검색
GET/admin/members/{memberId}/nfc-cards회원에게 발급된 카드 조회
GET/admin/business-cards/{businessCardId}/nfc-cards명함에 연결된 카드 조회
GET/admin/nfc-cardsNFC 카드 목록 조회
POST/admin/nfc-cardsNFC 카드 발급
POST/admin/nfc-cards/{cardId}/assign재고 카드를 회원에게 발급
POST/admin/nfc-cards/{cardId}/connect미연결 카드 연결
DELETE/admin/nfc-cards/{cardId}NFC 카드 삭제

1. 대시보드 요약 조회

GET /admin/card-operations/summary

Response 200

{
  "totalUsers": 1250,
  "totalNfcCards": 2840,
  "connectedNfcCards": 2630,
  "unconnectedNfcCards": 120,
  "attentionNfcCards": 210
}
필드집계 기준
totalUsers삭제되지 않은 일반 사용자 수
totalNfcCards삭제되지 않은 전체 NFC 카드 수
connectedNfcCardsCONNECTED 상태 카드 수
unconnectedNfcCardsIN_STOCK, UNCONNECTED 상태 카드 수
attentionNfcCardsCONNECTED가 아닌 카드 수

2. 사용자 목록 조회

GET /admin/users?query={query}&nfcCardStatus={status}

Query

필드필수설명
query아니요이름, 이메일, 명함 slug, 카드 이름, UUID 부분 검색
nfcCardStatus아니요카드 상태 필터. 카드가 없으면 NONE

Response 200

{
  "users": [
    {
      "memberId": "member_01",
      "displayName": "김민서",
      "email": "minseo.kim@taple.team",
      "role": "USER",
      "joinedAt": "2026-07-02T03:12:00Z",
      "businessCard": {
        "id": "business_card_01",
        "slug": "minseo-kim",
        "published": true
      },
      "cardStatus": "CONNECTED",
      "cardStatuses": ["CONNECTED", "INACTIVE"],
      "lastTaggedAt": "2026-07-15T05:32:00Z",
      "nfcCardCount": 2
    }
  ]
}

사용자 목록에는 전체 카드 배열을 포함하지 않습니다. 행을 펼칠 때 회원별 카드 조회 API를 호출합니다. 카드 상태와 보유 수량은 assignedMemberId를 기준으로 집계합니다.

3. 회원별 NFC 카드 조회

GET /admin/members/{memberId}/nfc-cards

회원에게 발급된 모든 카드를 assignedMemberId 기준으로 반환합니다. 회원에게 발급됐지만 명함에는 연결되지 않은 UNCONNECTED 카드도 포함합니다.

Response 200

{
  "nfcCards": [
    {
      "id": "nfc_01",
      "cardUid": "d3357c7d-190c-4e64-9d8e-434e7aa8231e",
      "label": "메인 비즈니스 카드",
      "status": "CONNECTED",
      "assignedMemberId": "member_01",
      "assignedAt": "2026-07-03T02:10:00Z",
      "businessCardId": "business_card_01",
      "businessCardSlug": "minseo-kim",
      "activatedAt": "2026-07-03T02:10:00Z",
      "lastTaggedAt": "2026-07-15T05:32:00Z"
    }
  ]
}

기존 GET /admin/business-cards/{businessCardId}/nfc-cards는 명함에 연결된 카드만 조회하는 호환용 계약으로 유지합니다.

4. NFC 카드 목록 조회

미연결 카드 목록과 전체 카드 검색에 사용합니다.

GET /admin/nfc-cards?status=UNCONNECTED&query={query}&unassignedOnly=true

Query

필드필수설명
status아니요NFC 카드 상태
query아니요카드 이름 또는 UUID 부분 검색
unassignedOnly아니요true이면 명함에 연결되지 않은 카드만 조회

Response 200

{
  "nfcCards": [
    {
      "id": "nfc_unconnected_01",
      "cardUid": "ed3ea460-3a6f-4d11-9014-783df94aa244",
      "label": "미연결 NFC 카드",
      "status": "UNCONNECTED",
      "assignedMemberId": "member_01",
      "assignedAt": "2026-07-22T10:00:00Z",
      "businessCardId": null,
      "businessCardSlug": null,
      "activatedAt": null,
      "lastTaggedAt": null
    }
  ]
}

대량 데이터 요구사항

연결 대기 카드가 많아질 수 있으므로 운영 배포 전에는 cursor, limit을 추가하고 응답에 다음 페이지 정보를 포함해야 합니다. 기본 limit20, 최대값은 100을 권장합니다.

{
  "nfcCards": [],
  "nextCursor": "opaque-cursor",
  "hasNext": true
}

프론트에서도 이 계약을 적용할 때 더보기 또는 무한 스크롤을 연결해야 합니다.

5. NFC 카드 발급

POST /admin/nfc-cards
Content-Type: application/json

Request

사용자 명함에 바로 연결해 발급합니다.

{
  "label": "신규 NFC 카드",
  "businessCardId": "business_card_01"
}

미발급 재고로 등록하려면 businessCardIdnull로 보냅니다.

{
  "label": "미발급 재고 카드",
  "businessCardId": null
}

서버 처리 규칙

  • cardUid는 서버가 UUID v4로 생성합니다.
  • cardUid에는 unique constraint를 적용하고 재사용하지 않습니다.
  • businessCardIdnull이면 상태는 IN_STOCK입니다.
  • IN_STOCK에는 assignedMemberId, assignedAt이 없습니다.
  • 명함을 지정하면 명함 소유자에게 발급하고 즉시 CONNECTED로 전환합니다.
  • label은 공백 제거 후 1~30자로 제한합니다.

Response 201

{
  "id": "nfc_06",
  "cardUid": "e7f08a26-929f-4a23-a685-e0d356ad938a",
  "label": "신규 NFC 카드",
  "status": "CONNECTED",
  "assignedMemberId": "member_01",
  "assignedAt": "2026-07-15T05:40:00Z",
  "businessCardId": "business_card_01",
  "businessCardSlug": "minseo-kim",
  "activatedAt": "2026-07-15T05:40:00Z",
  "lastTaggedAt": null
}

6. 재고 카드를 회원에게 발급

POST /admin/nfc-cards/{cardId}/assign
Content-Type: application/json

Request

{
  "memberId": "member_01"
}

Response 200

IN_STOCK 카드를 UNCONNECTED로 전환한 AdminNfcCard를 반환합니다. 서버는 assignedMemberIdassignedAt을 기록하고, 대상 회원 행과 카드 행을 순서대로 잠급니다.

대상 회원이 없거나 탈퇴했으면 404 US0001, 재고 카드가 아니면 409 NF0003을 반환합니다.

7. 미연결 카드 연결

POST /admin/nfc-cards/{cardId}/connect
Content-Type: application/json

Request

{
  "businessCardId": "business_card_01"
}

Response 200

연결된 AdminNfcCard를 반환합니다.

서버 처리 규칙

  1. 카드 행을 잠그고 연결 가능한 상태인지 확인합니다.
  2. 대상 명함이 존재하는지 확인합니다.
  3. IN_STOCK이면 명함 소유자에게 발급한 뒤 연결합니다.
  4. UNCONNECTED이면 발급 회원과 명함 소유자가 같은지 확인합니다.
  5. 상태를 CONNECTED로 변경하고 activatedAt을 기록합니다.

이미 연결됐거나 연결할 수 없는 상태면 409 NF0002를 반환합니다.

8. NFC 카드 삭제

연결 카드와 미연결 카드 모두 삭제할 수 있습니다.

DELETE /admin/nfc-cards/{cardId}

Response 204

본문이 없습니다.

서버 처리 규칙

  • deletedAt, deletedByAdminId를 기록하는 논리 삭제를 권장합니다.
  • 삭제된 카드는 목록과 요약 집계에서 제외합니다.
  • 삭제된 cardUid의 NFC 주소는 공개 명함으로 연결하지 않습니다.
  • 같은 카드에 대한 반복 삭제는 204를 반환해 멱등성을 유지합니다.
  • 삭제된 UUID를 새 카드에 재사용하지 않습니다.

감사 로그

발급, 연결, 삭제는 관리자 작업이므로 아래 내용을 기록해야 합니다.

type AdminNfcCardAuditLog = {
  id: string
  nfcCardId: string
  action: 'ISSUED' | 'ASSIGNED' | 'CONNECTED' | 'DELETED'
  adminUserId: string
  before: Record<string, unknown> | null
  after: Record<string, unknown> | null
  requestId: string
  createdAt: string
}

구현 완료 조건

  • 관리자가 아니면 모든 /admin/* 요청이 403을 반환한다.
  • 발급 요청에 UUID 필드가 없고 서버가 UUID v4를 생성한다.
  • 사용자 목록은 전체 카드 배열을 중첩하지 않는다.
  • 재고 카드를 회원에게 발급하면 UNCONNECTED와 발급 회원 정보가 저장된다.
  • 회원별 카드 조회에는 명함이 없는 회원의 UNCONNECTED 카드도 포함된다.
  • 미연결 카드 목록은 운영 데이터 규모에 맞는 페이지네이션을 지원한다.
  • 태그 횟수 필드는 응답하지 않는다.
  • 연결과 삭제 결과가 요약 수치에 반영된다.
  • 동시 연결 요청 중 하나만 성공한다.
  • 삭제된 카드의 NFC 주소는 공개 명함으로 연결되지 않는다.
  • 발급·연결·삭제 작업에 관리자 감사 로그가 남는다.

관련 코드

  • src/features/admin/domain/api.ts
  • src/features/admin/domain/adminCards.ts
  • src/features/admin/hooks/useAdminCardOperations.ts