Admin NFC 카드 운영 API 명세
/admin 카드 운영 화면에서 NFC 재고를 등록하고, 회원에게 발급하고, 명함에 연결하거나 삭제하기 위한 백엔드 계약입니다.
공통 규칙
- Base URL:
{VITE_SERVER_API_URL}/admin - 모든 요청은 로그인과
ADMIN권한이 필요합니다. - 프론트는 쿠키와
Authorization: Bearer {accessToken}을 함께 전송할 수 있습니다. - 날짜는 ISO 8601 문자열을 사용합니다.
- 성공 응답은 별도
dataenvelope 없이 아래 타입을 그대로 반환합니다. - 카드 태그 횟수는 집계하거나 반환하지 않습니다.
오류 응답
{
"code": "NF0002",
"message": "NFC card is not in a connectable state",
"data": null
}| HTTP | 코드 | 의미 |
|---|---|---|
| 400 | DB0003 | 입력값 오류 |
| 401 | DB0001 | 로그인이 필요함 |
| 403 | DB0004 | 관리자 권한이 없음 |
| 404 | US0001 | 발급할 회원을 찾을 수 없음 |
| 404 | BC0001 | 명함을 찾을 수 없음 |
| 404 | NF0001 | NFC 카드를 찾을 수 없음 |
| 409 | NF0002 | 연결할 수 없는 카드 상태 |
| 409 | NF0003 | 발급할 수 없는 카드 상태 |
| 422 | VALIDATION_FAILED | 필수 입력값이 없거나 잘못됨 |
| 500 | DB0002 | 처리 중 서버 오류가 발생한 경우 |
데이터 타입
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 목록
| Method | Path | 용도 |
|---|---|---|
GET | /admin/card-operations/summary | 대시보드 요약 조회 |
GET | /admin/users | 사용자 목록 검색 |
GET | /admin/members/{memberId}/nfc-cards | 회원에게 발급된 카드 조회 |
GET | /admin/business-cards/{businessCardId}/nfc-cards | 명함에 연결된 카드 조회 |
GET | /admin/nfc-cards | NFC 카드 목록 조회 |
POST | /admin/nfc-cards | NFC 카드 발급 |
POST | /admin/nfc-cards/{cardId}/assign | 재고 카드를 회원에게 발급 |
POST | /admin/nfc-cards/{cardId}/connect | 미연결 카드 연결 |
DELETE | /admin/nfc-cards/{cardId} | NFC 카드 삭제 |
1. 대시보드 요약 조회
GET /admin/card-operations/summaryResponse 200
{
"totalUsers": 1250,
"totalNfcCards": 2840,
"connectedNfcCards": 2630,
"unconnectedNfcCards": 120,
"attentionNfcCards": 210
}| 필드 | 집계 기준 |
|---|---|
totalUsers | 삭제되지 않은 일반 사용자 수 |
totalNfcCards | 삭제되지 않은 전체 NFC 카드 수 |
connectedNfcCards | CONNECTED 상태 카드 수 |
unconnectedNfcCards | IN_STOCK, UNCONNECTED 상태 카드 수 |
attentionNfcCards | CONNECTED가 아닌 카드 수 |
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=trueQuery
| 필드 | 필수 | 설명 |
|---|---|---|
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을 추가하고 응답에 다음 페이지 정보를 포함해야 합니다. 기본 limit은 20, 최대값은 100을 권장합니다.
{
"nfcCards": [],
"nextCursor": "opaque-cursor",
"hasNext": true
}프론트에서도 이 계약을 적용할 때 더보기 또는 무한 스크롤을 연결해야 합니다.
5. NFC 카드 발급
POST /admin/nfc-cards
Content-Type: application/jsonRequest
사용자 명함에 바로 연결해 발급합니다.
{
"label": "신규 NFC 카드",
"businessCardId": "business_card_01"
}미발급 재고로 등록하려면 businessCardId를 null로 보냅니다.
{
"label": "미발급 재고 카드",
"businessCardId": null
}서버 처리 규칙
cardUid는 서버가 UUID v4로 생성합니다.cardUid에는 unique constraint를 적용하고 재사용하지 않습니다.businessCardId가null이면 상태는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/jsonRequest
{
"memberId": "member_01"
}Response 200
IN_STOCK 카드를 UNCONNECTED로 전환한 AdminNfcCard를 반환합니다. 서버는 assignedMemberId와 assignedAt을 기록하고, 대상 회원 행과 카드 행을 순서대로 잠급니다.
대상 회원이 없거나 탈퇴했으면 404 US0001, 재고 카드가 아니면 409 NF0003을 반환합니다.
7. 미연결 카드 연결
POST /admin/nfc-cards/{cardId}/connect
Content-Type: application/jsonRequest
{
"businessCardId": "business_card_01"
}Response 200
연결된 AdminNfcCard를 반환합니다.
서버 처리 규칙
- 카드 행을 잠그고 연결 가능한 상태인지 확인합니다.
- 대상 명함이 존재하는지 확인합니다.
IN_STOCK이면 명함 소유자에게 발급한 뒤 연결합니다.UNCONNECTED이면 발급 회원과 명함 소유자가 같은지 확인합니다.- 상태를
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.tssrc/features/admin/domain/adminCards.tssrc/features/admin/hooks/useAdminCardOperations.ts