문서 버전: v25.0 작성일: 2026-03-20 작성자: DABOM 팀 변경 이력:
v25.0 - 미션 목록 조회 응답 dto 수정 -
requestId필드 추가, 전체 문서 v25.0 Major 버전 동기화v24.3 - 응답 상태 코드 정책을
styleguide.md와 현재ApiResponse래퍼 관행에 맞춰 재정렬. 생성 API는201 Created, 본문이 없는 성공 처리도 공개 계약에서는200 OK + data: null을 기본으로 정리. 공개 API 63개 + 내부 테스트 SSE 1개 유지v24.2 - 응답 상태 코드를 "현재 구현"이 아니라 "수정 목표" 기준으로 정리. 생성 API는
201 Created, 본문 없는 성공 처리 API도 공통 래퍼 유지 원칙에 맞춰200 + data: null기준으로 재정렬v24.1 - api-core 커밋
778d64eef4de4e50f2e46a1c7a6b9ef9881ebf69반영. 엔드포인트 추가/삭제는 없지만, 정책 수정/이의제기/미션/보상 처리 시 notification outbox를 통해 알림 이벤트가 발행되는 구현을 문서화하고,OUTBOX_001내부 에러 코드를 추가v24.0 - usage-events 처리 흐름과 notification outbox 구조를 통합 반영:
usage-persist/usage-realtime제거, processor-usage의 직접 DB 정산 구조로 수정,usage_event_outbox기반 배치 서버 후행 발행 흐름 반영, notification payload를subType없는 평탄화 형태로 정리. 엔드포인트 총 64개 유지v23.3 - FAMILIES/APPEALS 동기화:
GET /families/members신규 추가(OWNER 전용, MEMBER 역할 구성원 목록 조회),POST /appeals요청/응답에policyActive반영,POST /appeals/emergency요청에서additionalBytes제거 및 서버 고정 300MB 지급 로직 반영, 긴급 승인 시CUSTOMER_QUOTA.monthly_limit_bytes와MONTHLY_LIMIT정책 할당의rules.limitBytes동기화. 엔드포인트 총 64→65개v23.1 - NOTIFICATIONS 도메인 개선:
GET /notifications페이징→커서 기반 무한스크롤 + 30일 제한 +type콤마 구분 다중 필터 +title필드 추가,GET /notifications/alert·GET /notifications/block제거,GET /notifications/unread-count신규,DELETE /notifications/{notificationId}신규, 읽음 경로 RESTful 개선 (/{id}/read,/read-all). PUSH 도메인 신설 (Section 3.11):GET /push/vapid-public-key·POST /push/subscribe·DELETE /push/subscribe·POST /push/send4개 엔드포인트, PUSH 에러 코드 3개 추가. SSE 경로GET /families/usage/sse→GET /events/stream+ Phase 2 알림 이벤트 7개 추가 (총 12개). 엔드포인트 총 60→64개v22.3 - GET
/recaps/monthly의 mission/appeal summary 집계 기준을 월 내부 full week weekly snapshot 합계 + 좌우 partial raw 보강 구조로 정리하고,appealSummary/appealHighlights의 승인 집계 기준을resolved_at월 기준으로 명시.communicationScore는 carry-in 포함 처리율/이행률 공식으로 갱신. 엔트포인트 총 60개 유지v22.2 - REWARDS 도메인:
GET /admin/rewards/templates/{id}상세 조회 엔드포인트 추가, 엔드포인트 총 59→60개v22.1 - MISSIONS 도메인 Request/Response를 Spring 구현에 맞춰 수정:
GET /missions응답에requestStatus·createdBy·페이지네이션 반영,GET /missions/logs리스트 필드명logs→missions+assignedTo추가,GET /missions/history에서assignedTo제거 +requestedAt·respondedAt추가,POST /missions요청에서rewardValue제거 + 응답에서target제거v22.0 - UPLOADS 도메인 신규 추가:
POST /uploads/images(R2 이미지 업로드), thumbnailUrl 필드 CDN 전체 URL 형식으로 일괄 변경 (16건), 에러 코드 UPLOAD_xxx 4개 추가, 엔드포인트 총 58→59개v21.6 - 스타일가이드 준수 리팩터링: Response→Responses 상태 코드별 그룹 전환 (56건), 권한 형식 통일 (12건), Path Parameters 테이블 추가 (8건), 비-SSE 엔드포인트 Headers 섹션 제거 (3건), SSE 엔드포인트 경로 수정 (
/families/{familyId}/stream→/families/usage/sse), 3.2.5/3.2.6 SSE 표기 제거 및 3.2.5 응답 ApiResponse 래퍼 추가 (일반 엔드포인트), admin 로그인/리프레시 권한admin→없음수정. 엔드포인트 총 58개 유지v21.5 - 미션 요청 이력 조회 엔드포인트 경로 변경:
GET /missions/requests→GET /missions/history, 엔드포인트 총 58개 유지v21.4 - 미션 API 역할 분리:
GET /missions/logs를 순수MissionLog기반 미션 상태 변화 타임라인으로 재정의 (응답 구조 변경),GET /missions/history신규 추가 (MissionRequest기반 요청 이력 조회, 승인/거절 결과 포함). 기존GET /missions/logs응답이MissionRequest기반이던 것을MissionLog기반으로 변경. 엔드포인트 총 57→58개v21.3 - ERD v21.3 동기화:
mission_log.action_typeENUM 재정의 —MISSION_APPROVED·MISSION_REJECTED제거 (요청 처리 결과는mission_request.status가 담당),MISSION_CANCELLED추가 (미션 삭제 로그). 미션 삭제(DELETE /missions/{missionId}) 서버 처리 흐름 추가, 엔드포인트 총 57개 유지v21.2 - ERD v21.2 동기화: 긴급 쿼터 요청 동시성 문제 해결 — 서버 처리 흐름에서 SELECT 기반 중복 체크를
emergency_grant_monthUNIQUE 제약 기반 INSERT 중복 방지로 변경, UNIQUE 위반 시APPEAL_EMERGENCY_MONTHLY_LIMIT(429) 에러 반환, 엔드포인트 총 57개 유지v21.0 - Figma 디자인 반영: reward 객체에서 defaultValue·value·unit·templateDefaultValue 필드 제거, thumbnailUrl 필드 추가 (S3/R2 이미지 경로), category ENUM 간소화 (DATA/GIFTICON만), 지급 내역 API에 phoneNumber 검색·sort 정렬·unusedOnly 필터 추가, 템플릿 수정 시 category 변경 불가 반영, 예시 데이터 Figma 기준 교체
v20.0 - ERD v20.0 동기화: REWARD_TEMPLATE에 price·isActive 필드 추가, category ENUM ERD 기준 통일 (MONEY/FOOD→삭제, DATA/GIFTICON/TIME/ETC 정규화), unit ENUM ERD 기준 통일 (원/분/회→MB/MINUTE/COUNT/NONE), REWARD 응답에 templateDefaultValue 추가, REWARD_GRANT 신규 테이블에 따른 admin 보상 지급 내역 조회 엔드포인트 추가, 예시 데이터 현실화, 엔드포인트 총 56→57개
v19.2 - ERD v19.2 동기화: CUSTOMER
profileImageUrl필드 삭제 (GET/PUT /customers/profile), GET /appeals 이의제기 목록 조회에 커서 기반 무한스크롤 적용, 엔드포인트 총 56개 유지v19.1 - ERD v19.1 동기화: 이의제기 취소 엔드포인트 추가 (
PATCH /appeals/{appealId}/cancel), 이의제기 목록 조회 status 필터에CANCELLED추가, 이의제기 상세 조회 응답에policyType필드 추가, POLICY_APPEAL.status ENUM에CANCELLED추가, 에러 코드 3개 추가 (APPEAL_CANCEL_FORBIDDEN,APPEAL_NOT_CANCELLABLE,APPEAL_EMERGENCY_CANCEL_NOT_ALLOWED), 엔드포인트 총 55→56개v19.0 - ERD v19.0 동기화: REWARD 엔티티 추가에 따른 API 응답 구조 변경, 모든 미션/보상 응답에서
rewardTemplate+rewardValue플랫 구조 →reward중첩 객체로 통합 (GET /missions, GET /missions/logs, POST /missions, POST /missions/{missionId}/request, PUT /rewards/requests/{requestId}/respond, GET /rewards/received), POST /missions 요청에서rewardCategory필드 제거 (category는 템플릿에서 스냅샷), 엔드포인트 총 55개 유지v18.3 - ERD v18.3 동기화: GET /recaps/monthly 응답에
communicationScore추가,NORMAL이의제기와 미션 완료 건수를 기반으로 월간 소통 점수 계산 규칙 명시,NORMAL이의제기 0건이면 미션 완료율로 fallback 하고 이의제기/미션 모두 0건일 때만null반환v18.2 - ERD v18.2 동기화: GET /recaps/monthly 응답의 이의제기 하이라이트 구조를
appealHighlights로 재정의 (topSuccessfulRequester,topAcceptedApprover, 최신 이력 최대 3개),approverSummary제거v18.1 - 코드-명세서 동기화: 정책 수정 PATCH→PUT (구현 기준 동기화), 관리자 가족 검색/상세 URL
/families→/admin/families(AdminFamilyController 코드 기준 동기화), ERD v18.1 동기화 (REWARD_TEMPLATE에 updated_at/deleted_at 추가), 엔드포인트 총 55개 유지v18.0 - ERD v17.0 동기화: MISSION_ITEM에 target_customer_id 추가 (GET/POST /missions, GET /missions/logs 반영), MISSION_REQUEST에 reject_reason 추가 (reason→rejectReason 변경), GET /rewards/received 신규 엔드포인트 추가, REPORTS→RECAPS 도메인 리네이밍 (GET /reports/monthly→GET /recaps/monthly), GET /recaps/monthly 응답 JSON 구조 통합 (usageByWeekday, peakUsage, missionSummary, appealSummary, approverSummary), POLICY_APPEAL.type ENUM APPEAL→NORMAL, 역할 기반 접근 패턴 문서화, 에러코드 MISSION_NOT_ASSIGNED/MISSION_TARGET_INVALID 추가, 엔드포인트 총 54→55개
- v17.0 - POLICY_APPEAL type APPEAL→NORMAL 리네이밍, 이의제기 엔드포인트 /policies prefix 제거 (/policies/appeals → /appeals), APPEALS 도메인 독립 분리, admin 보상 템플릿 엔드포인트를 REWARDS 도메인으로 문서 재분류, 엔드포인트 총 54개 유지
- v16.0 - ERD v16.0 동기화: POLICY_APPEAL_LOG 참조 제거 (긴급 쿼터 처리 흐름에서 삭제), POLICY_ASSIGNMENT reason 필드 제거 (GET /customers/policies, GET /families/policies 응답, PATCH /families/policies 요청에서 삭제), POLICY_APPEAL reason→requestReason/rejectReason 분리 (이의제기 생성/목록/상세/승인거절/긴급 요청 전체 반영), 3.3.6 이의제기 목록 조회 페이징 제거, 3.3.7 이의제기 상세 조회 댓글 커서 기반 무한스크롤 추가, 3.4.1 미션 항목 목록 조회 페이징 제거, 3.4.2 미션 승인/획득 로그 조회 페이징→커서 기반 무한스크롤 변경, 보상 승인 요청을 미션 승인 요청으로 변경하여 MISSIONS 도메인으로 이동 (POST /missions/{missionId}/request), 보상 승인/거절 처리는 REWARDS 도메인 유지 (PUT /rewards/requests/{requestId}/respond), 엔드포인트 총 54개 유지
- v15.0 - ERD v15.0 동기화: 긴급 쿼터 요청 엔드포인트 추가 (POST /policies/appeals/emergency), GET /policies/appeals에 type 필터 추가, GET /reports/monthly에 emergencyUsedCount 복원, APPEAL_EMERGENCY_xxx 에러 코드 3개 추가, 엔드포인트 총 53→54개
- v14.0 - ERD v14.0 동기화: POLICY_APPEAL 이의제기 엔드포인트 5개 추가 (목록/상세/생성/승인거절/댓글), desiredRules(nullable) 필드 반영, NEGOTIATION 에러 → APPEAL 에러로 교체, 엔드포인트 총 48→53개; POLICY_ASSIGNMENT reason 필드 추가 (GET /families/policies, GET /customers/policies 응답, PATCH /families/policies 요청), NEGOTIATION 엔드포인트 6개 완전 삭제 (협상 이력/조르기/긴급요청/승인거절/코멘트), negotiationSummary → appealSummary 변경 (emergencyUsedCount 제거), 엔드포인트 총 53개 유지 (본문 negotiation 6개 삭제, 요약 테이블 동기화)
- v11.0 - 2차기획서 Phase 2 기능 반영: ~30개 신규 엔드포인트 추가 (협상/미션/보상/리포트/알림/프로필), 에러 코드 확장
- v10.2 - ERD v10.2 동기화: POLICY 테이블 is_activate → is_active 리네이밍, 변경 이력 isActivate → isActive 수정
- v10.0 - web-core 서브도메인 분리 Major 버전 동기화
- v9.0 - api-spec 최종 동기화: 도메인 구조 변경 (7→5도메인), 엔드포인트 URL/메서드/응답 구조 변경, isActive 통일, 신규 엔드포인트 추가, 미사용 엔드포인트 제거
- v8.1 - ERD v8.1 동기화: POLICY 응답에 isActive 필드 추가, 로그인 요청 phoneNumber 숫자만 형식으로 변경
- v8.0 - 전체 문서 버전 통일 (공유 Major + 독립 Minor 체계 도입), simulator-traffic → simulator-usage 리네이밍 동기화
- v7.1 - /admin/users 응답 userId→adminId 수정 (ADMIN 테이블 네이밍 통일)
- v7.0 - api-spec 기반 통일: userId→customerId, members→customers, 응답 구조 개선, POLICY 필드 추가, rules JSON 키 통일
- v6.0 - ERD v6.0 동기화: /mypage/policies 응답 필드명 통일 (params→rules)
- v5.0 - ERD v5.0 동기화: daily→monthly 전환, DAILY_LIMIT→MONTHLY_LIMIT, CUSTOMER/ADMIN 분리 반영, POLICY.rules→POLICY_ASSIGNMENT 이동, 관리자 전용 인증 API 추가
- v4.0 - api-spec.csv 기반 전면 재구성, /api/v1 prefix 제거, 도메인별 그룹핑 (AUTH/FAMILIES/POLICIES/NOTIFICATIONS/MYPAGE/ADMIN), JWT familyId 추론, REST 알림 API 추가
| 환경 | URL |
|---|---|
| 개발/운영 | https://api.dabom.site |
JWT (JSON Web Token) 자체 구현
| 토큰 유형 | 용도 | 유효기간 |
|---|---|---|
| Access Token | API 인증 | 30분 |
| Refresh Token | Access Token 갱신 | 7일 |
헤더 형식:
Authorization: Bearer <access_token>JWT Payload에 포함되는 정보:
{
"customerId": 12345,
"familyId": 100,
"role": "OWNER",
"exp": 1705312200
}Note (v4.0):
familyId는 JWT 토큰에서 추론합니다. 대부분의 API에서 경로 파라미터로familyId를 전달하지 않습니다. Note:role은family_member.role에서 가져오며, 한 가족 그룹 내에 복수 OWNER가 존재할 수 있습니다.
Accept-Version 헤더 방식:
Accept-Version: 1.0Note (v4.0): URL 경로에서
/api/v1프리픽스가 제거되었습니다. API 버전 관리는 Accept-Version 헤더로만 수행합니다.
{
"success": true,
"data": { ... },
"timestamp": "2024-01-15T10:30:00Z"
}{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human readable message",
"details": { ... }
},
"timestamp": "2024-01-15T10:30:00Z"
}Note:
dabom-api-notification모듈의 에러 응답은 위 래퍼와 다른 형식을 사용한다:{ "status": 404, "code": "NOTIFICATION_003", "message": "알림을 찾을 수 없습니다." }
| 코드 | 설명 |
|---|---|
| 200 | 성공 |
| 201 | 생성 성공 |
| 202 | 요청 수락 (비동기 처리) |
| 400 | 잘못된 요청 |
| 401 | 인증 필요 |
| 403 | 권한 없음 |
| 404 | 리소스 없음 |
| 409 | 충돌 (중복 등) |
| 429 | 요청 제한 초과 |
| 500 | 서버 오류 |
| 503 | 서비스 이용 불가 (Fallback 모드) |
- 리소스 생성:
201 Created - 조회/수정/삭제/상태 변경:
200 OK - 본문이 실질적으로 없더라도 공통 응답 래퍼를 유지하는 공개 계약은
200 OK + data: null을 사용한다. 204 No Content는 문서상 기본 계약으로 채택하지 않는다.- 업서트 API는
200 OK를 기본으로 본다.
| 권한 | 대상 | 설명 |
|---|---|---|
| member | 모든 인증된 가족 구성원 | 본인 데이터 조회, 알림 수신 |
| owner | Owner 계정 (복수 가능) | 가족 내 정책 수정, 구성원 관리. 한 가족에 복수 OWNER 허용. 정책 충돌 시 Last Write Wins 적용 (마지막 수정이 유효, audit_log에 이력 기록) |
| admin | 백오피스 운영자 | 시스템 전체 관리, 정책 템플릿 CRUD |
familyId는 JWT 토큰에서 자동으로 추론되므로 대부분의 엔드포인트에서 경로 파라미터로 전달하지 않습니다. 예외:POST /families/{familyId}/invite,GET /families/{familyId}
아키텍처 변경 (v2.0): HTTP
POST /usage엔드포인트가 제거되었습니다. simulator-usage가 Kafka로 직접 이벤트를 발행하며, processor-usage가 Consumer로 처리합니다.
Kafka Topic: usage-events
Partition Key: familyId (가족 단위 순서 보장)
상세 스키마 및 Java Record 구현체는 Kafka 메시지 스키마 참조
{
"eventId": "evt_550e8400-e29b-41d4-a716-446655440000",
"eventType": "DATA_USAGE",
"timestamp": "2026-02-06T14:30:00Z",
"payload": {
"familyId": 100,
"customerId": 12345,
"appId": "com.youtube.app",
"bytesUsed": 5242880,
"metadata": {
"deviceId": "device_pixel_9",
"networkType": "5G"
}
}
}공통 봉투 (EventEnvelope):
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| eventId | string | ✅ | 이벤트 고유 ID (simulator-usage가 UUID v4로 생성) |
| eventType | string | ✅ | 이벤트 유형 (DATA_USAGE, POLICY_UPDATED, NOTIFICATION) |
| timestamp | string | ✅ | ISO 8601 형식 |
| payload | object | ✅ | eventType에 따라 다른 페이로드 |
UsagePayload 필드:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| familyId | integer | ✅ | 가족 그룹 ID (파티션 키) |
| customerId | integer | ✅ | 고객(사용자) ID |
| appId | string | ✅ | 앱 식별자 |
| bytesUsed | integer | ✅ | 사용 바이트 수 |
| metadata | object | ❌ | 추가 메타데이터 |
# 버스트 트래픽 처리를 위한 설정
bootstrap.servers=kafka:9092
buffer.memory=67108864 # 64MB 버퍼
batch.size=65536 # 64KB 배치
linger.ms=5 # 5ms 대기 후 발송
max.block.ms=60000 # 60초 블로킹 허용
acks=1 # Leader 응답만 대기 (처리량 우선)
compression.type=lz4 # 압축으로 네트워크 효율화
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.StringSerializer// simulator-usage에서 Kafka로 직접 발행
event := UsageEvent{
EventId: uuid.New().String(), // simulator-usage가 eventId 생성
EventType: "DATA_USAGE",
Timestamp: time.Now().UTC().Format(time.RFC3339),
FamilyId: familyId,
CustomerId: customerId,
AppId: appId,
BytesUsed: bytesUsed,
}
// familyId를 파티션 키로 사용 (가족 단위 순서 보장)
producer.Produce(&kafka.Message{
TopicPartition: kafka.TopicPartition{Topic: "usage-events"},
Key: []byte(strconv.Itoa(event.FamilyId)),
Value: eventJson,
})POST /customers/login | 권한: 없음
Request:
{
"phoneNumber": "01012345678",
"password": "password123"
}Responses:
200 OK:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"role": "OWNER"
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /customers/refresh | 권한: 없음
Request:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Responses:
200 OK:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 1800
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /customers/logout | 권한: 없음
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}POST /customers/signup | 권한: 없음
Request:
{
"phoneNumber": "01012345678",
"password": "password123",
"name": "홍길동"
}Responses:
201 Created:
{
"success": true,
"data": {
"id": 23
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /customers/me | 권한: member
customerId는 JWT 토큰에서 추론
Responses:
200 OK:
{
"success": true,
"data": {
"id": 12345,
"phoneNumber": "010-****-5678",
"name": "아빠",
"email": "dad@example.com",
"isOnboarded": true,
"termsAgreedAt": "2024-01-01T00:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /customers/mypage | 권한: member
customerId,familyId는 JWT 토큰에서 추론
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| year | integer | ✅ | 조회 연도 (예: 2024) |
| month | integer | ✅ | 조회 월 (1-12) |
Responses:
200 OK:
{
"success": true,
"data": {
"name": "홍길동",
"familyName": "김씨 가족",
"isBlocked": false,
"blockReason": null,
"monthlyLimitBytes": 2147483648,
"monthlyUsedBytes": 1073741824,
"timeBlock": null
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /admin/families | 권한: admin
Request:
{
"filters": {
"name": "김씨",
"phone": "010",
"usageRate": 50
},
"sort": "createdAt",
"page": 0,
"size": 20
}Responses:
200 OK:
{
"success": true,
"data": {
"content": [
{
"familyId": 100,
"familyName": "김씨 가족",
"customers": [
{
"customerId": 12345,
"name": "아빠",
"role": "OWNER"
}
],
"createdAt": "2024-01-01T00:00:00Z"
}
],
"page": 0,
"size": 20,
"totalElements": 250000,
"totalPages": 12500
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /families/usage/current | 권한: member
familyId는 JWT 토큰에서 추론.
Responses:
200 OK:
{
"success": true,
"data": {
"familyId": 100,
"totalUsedBytes": 53687091200,
"totalLimitBytes": 107374182400,
"remainingBytes": 53687091200
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /families/usage/customers | 권한: member
familyId는 JWT 토큰에서 추론.
Responses:
200 OK:
{
"success": true,
"data": {
"customers": [
{
"customerId": 12345,
"name": "아빠",
"monthlyUsedBytes": 1073741824,
"monthlyLimitBytes": 5368709120
},
{
"customerId": 12346,
"name": "자녀1",
"monthlyUsedBytes": 2147483648,
"monthlyLimitBytes": 2147483648
}
]
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /admin/families/{familyId} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| familyId | integer | 가족 그룹 ID |
Responses:
200 OK:
{
"success": true,
"data": {
"familyId": 100,
"familyName": "김씨 가족",
"createdById": 12345,
"customers": [
{
"customerId": 12345,
"name": "아빠",
"phoneNumber": "010-****-5678",
"role": "OWNER",
"monthlyLimitBytes": 5368709120,
"monthlyUsedBytes": 1073741824,
"isBlocked": false,
"joinedAt": "2024-01-01T00:00:00Z"
},
{
"customerId": 12346,
"name": "자녀1",
"phoneNumber": "010-****-1234",
"role": "MEMBER",
"monthlyLimitBytes": 2147483648,
"monthlyUsedBytes": 2147483648,
"isBlocked": true,
"blockReason": "MONTHLY_LIMIT_EXCEEDED",
"joinedAt": "2024-01-02T00:00:00Z"
}
],
"totalQuotaBytes": 107374182400,
"usedBytes": 53687091200,
"usedPercent": 50.0,
"currentMonth": "2024-01",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}PATCH /admin/families/{familyId} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| familyId | integer | 가족 그룹 ID |
Request:
{
"members": [
{
"customerId": 12346,
"role": "MEMBER",
"monthlyLimitBytes": 3221225472
}
]
}Responses:
200 OK:
{
"success": true,
"data": {
"familyId": 100,
"updatedCount": 1
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /families/policies | 권한: owner
familyId는 JWT 토큰에서 추론
Responses:
200 OK:
{
"success": true,
"data": {
"familyId": 100,
"customers": [
{
"customerId": 12346,
"name": "자녀1",
"phoneNumber": "010-****-8888",
"role": "MEMBER",
"usedBytes": 53687091200,
"policies": [
{
"assignmentId": 1,
"policyId": 10,
"policyName": "야간 차단",
"type": "TIME_BLOCK",
"isActive": true,
"rules": {
"start": "22:00",
"end": "07:00",
"timezone": "Asia/Seoul"
}
},
{
"assignmentId": 2,
"policyId": 11,
"policyName": "자녀1 월별 한도",
"type": "MONTHLY_LIMIT",
"isActive": true,
"rules": {
"limitBytes": 2147483648
}
}
]
}
]
},
"timestamp": "2024-01-15T10:30:00Z"
}PATCH /families/policies | 권한: owner
familyId는 JWT 토큰에서 추론. 부분 업데이트(Partial Update) 방식.
Request:
{
"updateInfo": {
"customerId": 12346,
"type": "MONTHLY_LIMIT",
"value": {
"limitBytes": 3221225472
}
}
}Responses:
200 OK:
{
"success": true,
"data": {
"result": {
"customerId": 12346,
"type": "MONTHLY_LIMIT",
"status": "APPLIED"
}
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /families/members | 권한: owner
familyId는 JWT 토큰에서 추론. OWNER가 자신의 가족에 속한 구성원 중role=MEMBER인원만 조회합니다. OWNER 계정은 응답에서 제외됩니다.
Responses:
200 OK:
{
"success": true,
"data": [
{
"customerId": 12346,
"name": "자녀1",
"role": "MEMBER"
},
{
"customerId": 12347,
"name": "자녀2",
"role": "MEMBER"
}
],
"timestamp": "2024-01-15T10:30:00Z"
}PUT /families | 권한: owner
familyId는 JWT 토큰에서 추론
Request:
{
"name": "김씨 가족(수정)"
}Responses:
200 OK:
{
"success": true,
"data": {
"familyId": 100,
"name": "김씨 가족(수정)",
"updatedAt": "2024-01-15T10:30:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /families/usage/dashboard | 권한: member
familyId는 JWT 토큰에서 추론. 현재 월 기준 통계를 제공합니다.
Responses:
200 OK:
{
"success": true,
"data": {
"familyId": 100,
"familyName": "김씨 가족",
"currentMonth": "2024-01",
"totalQuotaBytes": 107374182400,
"totalUsedBytes": 53687091200,
"totalRemainingBytes": 53687091200,
"usedPercent": 50.0,
"memberStats": [
{
"customerId": 12345,
"name": "아빠",
"role": "OWNER",
"monthlyUsedBytes": 1073741824,
"monthlyLimitBytes": 5368709120,
"usedPercent": 20.0,
"isBlocked": false
},
{
"customerId": 12346,
"name": "자녀1",
"role": "MEMBER",
"monthlyUsedBytes": 2147483648,
"monthlyLimitBytes": 2147483648,
"usedPercent": 100.0,
"isBlocked": true,
"blockReason": "MONTHLY_LIMIT_EXCEEDED"
}
]
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /rewards/templates | 권한: member
familyId는 JWT 토큰에서 추론. 가족 구성원이 조회 가능한 보상 템플릿 목록
Responses:
200 OK:
{
"success": true,
"data": [
{
"id": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"price": 3000,
"isActive": true,
"createdAt": "2024-01-01T00:00:00Z"
},
{
"id": 3,
"name": "100MB",
"category": "DATA",
"thumbnailUrl": null,
"price": 3000,
"isActive": true,
"createdAt": "2024-01-01T00:00:00Z"
}
],
"timestamp": "2024-01-15T10:30:00Z"
}GET /policies | 권한: admin
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| type | string | ❌ | 정책 유형 필터 (MONTHLY_LIMIT, TIME_BLOCK, MANUAL_BLOCK, APP_BLOCK) |
| page | integer | ❌ | 페이지 번호 (기본: 0) |
| size | integer | ❌ | 페이지 크기 (기본: 10) |
Responses:
200 OK:
{
"success": true,
"data": {
"policies": [
{
"policyId": 10,
"name": "야간 차단 기본",
"type": "TIME_BLOCK",
"defaultRules": {
"start": "22:00",
"end": "07:00",
"timezone": "Asia/Seoul"
},
"requireRole": "OWNER",
"isActive": true,
"isSystem": false,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
],
"page": 0,
"size": 10,
"totalElements": 15,
"totalPages": 2
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /policies | 권한: admin
Request:
{
"name": "학습 시간대 차단",
"type": "TIME_BLOCK"
}Responses:
201 Created:
{
"success": true,
"data": {
"policyId": 16,
"name": "학습 시간대 차단",
"type": "TIME_BLOCK",
"isSystem": false,
"createdAt": "2024-01-15T10:30:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}DELETE /policies/{policyId} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| policyId | integer | 정책 ID |
Responses:
200 OK:
{
"success": true,
"data": {
"policyId": 16,
"deletedAt": "2024-01-15T10:30:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /policies/{policyId} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| policyId | integer | 정책 ID |
Responses:
200 OK:
{
"success": true,
"data": {
"policyId": 10,
"name": "야간 차단 기본",
"description": "정책 상세 설명",
"type": "TIME_BLOCK",
"defaultRules": {
"start": "22:00",
"end": "07:00",
"timezone": "Asia/Seoul"
},
"requireRole": "OWNER",
"isActive": true,
"isSystem": false,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}PUT /policies/{policyId} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| policyId | integer | 정책 ID |
Request:
{
"description": "정책 상세 정보.....",
"requireRole": "OWNER",
"defaultRules": {
"start": "22:00",
"end": "07:00",
"timezone": "Asia/Seoul"
},
"isActive": true,
"overWrite": false
}Responses:
200 OK:
{
"success": true,
"data": {
"policyId": 16,
"updatedAt": "2024-01-15T10:30:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}자녀의 정책 이의제기 및 긴급 쿼터 요청을 관리합니다.
GET /appeals/policies | 권한: member
customerId,familyId는 JWT 토큰에서 추론. 이의제기를 생성할 수 있는 정책 목록을 조회한다.
Responses:
200 OK:
{
"success": true,
"data": [
{
"assignmentId": 2,
"policyId": 11,
"policyName": "자녀1 월별 한도",
"type": "MONTHLY_LIMIT",
"isActive": true,
"rules": {
"limitBytes": 2147483648
}
}
],
"timestamp": "2024-01-15T10:30:00Z"
}GET /appeals | 권한: member
familyId는 JWT 토큰에서 추론. MEMBER는 본인 이의제기만, OWNER는 가족 전체 이의제기 조회. 커서 기반 무한스크롤로 제공.
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| status | string | ❌ | 상태 필터 (PENDING, APPROVED, REJECTED, CANCELLED) |
| cursor | string | ❌ | 다음 페이지 커서 (이전 응답의 nextCursor 값) |
| size | integer | ❌ | 조회 크기 (기본: 20) |
Responses:
200 OK:
{
"success": true,
"data": {
"appeals": [
{
"appealId": 1,
"type": "NORMAL",
"policyAssignmentId": 55,
"requesterId": 12346,
"requesterName": "자녀1",
"requestReason": "인강을 들어야 합니다",
"desiredRules": { "limitBytes": 524288000 },
"status": "PENDING",
"createdAt": "2024-01-15T10:30:00Z"
}
],
"nextCursor": "eyJhcHBlYWxJZCI6MX0=",
"hasNext": true
},
"timestamp": "2024-01-15T10:30:00Z"
}무한스크롤 동작:
nextCursor가 null이면 더 이상 로드할 이의제기가 없음. 프론트엔드에서 스크롤 끝에 도달하면cursor파라미터로 다음 페이지를 요청.
GET /appeals/{appealId} | 권한: member
댓글은 커서 기반 무한스크롤로 제공. 최초 조회 시 최신 댓글 20개를 반환하며,
nextCursor를 사용하여 이전 댓글을 추가 로딩.policyType:policyAssignmentId로 연결된 정책의 타입을 내려줌. EMERGENCY 타입(policy_assignment_id=NULL)이면null.
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| appealId | integer | 이의제기 ID |
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| cursor | string | ❌ | 다음 페이지 커서 (이전 응답의 nextCursor 값) |
| size | integer | ❌ | 댓글 조회 크기 (기본: 20) |
Responses:
200 OK:
{
"success": true,
"data": {
"appealId": 1,
"policyAssignmentId": 55,
"policyType": "MONTHLY_LIMIT",
"requesterId": 12346,
"requesterName": "자녀1",
"requestReason": "인강을 들어야 합니다",
"rejectReason": null,
"desiredRules": { "limitBytes": 524288000 },
"status": "PENDING",
"resolvedById": null,
"resolvedAt": null,
"createdAt": "2024-01-15T10:30:00Z",
"comments": {
"content": [
{
"commentId": 10,
"authorId": 12346,
"authorName": "자녀1",
"comment": "공부할 때 필요해요",
"createdAt": "2024-01-15T10:35:00Z"
}
],
"nextCursor": "eyJjb21tZW50SWQiOjEwfQ==",
"hasNext": true
}
},
"timestamp": "2024-01-15T10:30:00Z"
}policyType 설명:
| policyType | 설명 |
|---|---|
MONTHLY_LIMIT |
월간 데이터 제한 정책 |
TIME_BLOCK |
시간대 차단 정책 |
APP_BLOCK |
앱 차단 정책 |
MANUAL_BLOCK |
수동 차단 정책 |
null |
EMERGENCY 타입 (정책 연결 없음) |
무한스크롤 동작:
nextCursor가 null이면 더 이상 로드할 댓글이 없음. 프론트엔드에서 스크롤 끝에 도달하면cursor파라미터로 다음 페이지를 요청.
POST /appeals | 권한: member (MEMBER)
Request:
{
"policyAssignmentId": 55,
"requestReason": "인강을 들어야 합니다",
"policyActive": true,
"desiredRules": { "limitBytes": 524288000 }
}
desiredRules는 optional. NULL이면 부모가 직접 정책 수정, 값이 있으면 APPROVED 시 PolicyAssignment에 자동 반영.desiredRules스키마는 해당 policy.type의 rules 스키마와 동일.policyActive는 필수입니다. 승인 시 정책 활성화 상태를 함께 변경할 수 있습니다.
Responses:
201 Created:
{
"success": true,
"data": {
"appealId": 1,
"policyAssignmentId": 55,
"status": "PENDING",
"policyActive": true,
"desiredRules": { "limitBytes": 524288000 },
"createdAt": "2024-01-15T10:30:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}PATCH /appeals/{appealId}/respond | 권한: owner
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| appealId | integer | 이의제기 ID |
Request (승인):
{
"action": "APPROVED"
}Request (거절 — 사유 포함):
{
"action": "REJECTED",
"rejectReason": "이번 달 데이터 사용량이 이미 많아서 추가 완화가 어렵습니다."
}
action:APPROVED또는REJECTED>rejectReason: 거절 시 사유 (optional). 승인 시에는 생략 가능.desiredRules가 있는 이의제기를APPROVED처리 시, PolicyAssignment.rules에desiredRules값이 자동 반영됨.desiredRules가 NULL인 경우 부모가 별도로 정책을 직접 수정해야 함.
Responses:
200 OK (승인):
{
"success": true,
"data": {
"appealId": 1,
"status": "APPROVED",
"resolvedById": 12345,
"resolvedAt": "2024-01-15T11:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}200 OK (거절):
{
"success": true,
"data": {
"appealId": 1,
"status": "REJECTED",
"rejectReason": "이번 달 데이터 사용량이 이미 많아서 추가 완화가 어렵습니다.",
"resolvedById": 12345,
"resolvedAt": "2024-01-15T11:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /appeals/{appealId}/comments | 권한: member
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| appealId | integer | 이의제기 ID |
Request:
{
"comment": "부모/자녀 모두 작성 가능한 댓글입니다."
}Responses:
201 Created:
{
"success": true,
"data": {
"commentId": 10,
"appealId": 1,
"authorId": 12345,
"comment": "부모/자녀 모두 작성 가능한 댓글입니다.",
"createdAt": "2024-01-15T11:05:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /appeals/emergency | 권한: member (MEMBER)
자녀가 월 1회, 부모 승인 없이 사유만 남기고 데이터를 받을 수 있는 긴급 쿼터 요청입니다. 서버가 고정으로 300MB(
314,572,800 bytes)를 즉시 승인하며, 승인 후CUSTOMER_QUOTA.monthly_limit_bytes와MONTHLY_LIMIT정책 할당의rules.limitBytes를 함께 동기화합니다.
제한 사항:
- 월 1회만 가능 (
emergency_grant_monthUNIQUE 제약으로 DB 레벨 동시성 안전 중복 방지) - 무제한 쿼터 사용자(
monthly_limit_bytes=NULL)는 요청 불가
Request:
{
"requestReason": "급하게 과제 제출해야 합니다"
}| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| requestReason | string | ✅ | 긴급 요청 사유 |
Responses:
201 Created:
{
"success": true,
"data": {
"appealId": 42,
"type": "EMERGENCY",
"status": "APPROVED",
"additionalBytes": 314572800,
"newMonthlyLimitBytes": 2357198848,
"requestReason": "급하게 과제 제출해야 합니다",
"createdAt": "2026-03-04T14:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}서버 처리 흐름:
- JWT에서
customerId추출, MEMBER 역할 검증 CUSTOMER_QUOTA조회 →monthly_limit_bytes가 NULL(무제한)이면 에러POLICY_APPEALINSERT (type=EMERGENCY,status=APPROVED,resolved_by_id=NULL,emergency_grant_month=현재 월 1일,desired_rules.additionalBytes=314572800)uk_appeal_emergency_monthUNIQUE 제약 위반 시 →APPEAL_EMERGENCY_MONTHLY_LIMIT(429) 에러 반환 (동시성 안전)
CUSTOMER_QUOTA.monthly_limit_bytes += 314572800- 대상 고객의
MONTHLY_LIMIT정책 할당이 있으면rules.limitBytes를 증가된 월 한도로 동기화 - NOTIFICATION 발행 → 부모(OWNER)에게
EMERGENCY_APPROVED사후 알림 - 201 응답 반환
PATCH /appeals/{appealId}/cancel | 권한: member (MEMBER)
요청자 본인이 생성한 일반(NORMAL) 이의제기만 취소 가능.
PENDING상태에서만 취소 가능.EMERGENCY타입은 취소 불가. 이미APPROVED,REJECTED,CANCELLED상태인 건 취소 불가.
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| appealId | integer | 이의제기 ID |
Responses:
200 OK:
{
"success": true,
"data": {
"appealId": 1,
"status": "CANCELLED",
"cancelledAt": "2024-01-15T10:45:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}에러 코드:
| 코드 | HTTP | 설명 |
|---|---|---|
APPEAL_NOT_FOUND |
404 | 이의제기를 찾을 수 없음 |
APPEAL_CANCEL_FORBIDDEN |
403 | 본인이 생성한 이의제기가 아님 |
APPEAL_NOT_CANCELLABLE |
409 | PENDING 상태가 아니라 취소 불가 (이미 처리/취소됨) |
APPEAL_EMERGENCY_CANCEL_NOT_ALLOWED |
400 | EMERGENCY 타입은 취소 불가 |
미션 및 보상 기능을 제공합니다. OWNER가 미션을 생성하고, MEMBER가 완료 후 보상 요청을 합니다.
GET /missions | 권한: member
familyId는 JWT 토큰에서 추론 OWNER는 가족 전체 미션, MEMBER는 본인(target) 미션만 조회.
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| cursor | string | ❌ | 다음 페이지 커서 (이전 응답의 nextCursor 값) |
| size | integer | ❌ | 조회 크기 (기본: 20) |
Responses:
200 OK:
{
"success": true,
"data": {
"missions": [
{
"missionItemId": 201,
"requestId": 55,
"missionText": "방 청소하기",
"requestStatus": null,
"target": {
"customerId": 12346,
"name": "자녀1"
},
"createdBy": {
"customerId": 12345,
"name": "아빠"
},
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"templateId": 1
},
"createdAt": "2024-01-15T09:00:00Z"
}
],
"nextCursor": "eyJtaXNzaW9uSXRlbUlkIjoyMDF9",
"hasNext": true
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /missions/logs | 권한: member
familyId는 JWT 토큰에서 추론. 커서 기반 무한스크롤로 제공. OWNER는 가족 전체 로그, MEMBER는 본인 target 미션 로그만 조회.MissionLog기반 — 미션 자체의 상태 변화 타임라인만 제공. 요청 처리 결과(승인/거절)는GET /missions/history에서 조회.
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| cursor | string | ❌ | 다음 페이지 커서 (이전 응답의 nextCursor 값) |
| size | integer | ❌ | 조회 크기 (기본: 20) |
Responses:
200 OK:
{
"success": true,
"data": {
"missions": [
{
"logId": 501,
"actionType": "MISSION_COMPLETED",
"message": "미션이 완료되었습니다",
"missionItem": {
"missionItemId": 201,
"missionText": "방 청소하기",
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"templateId": 1
}
},
"assignedTo": {
"customerId": 12346,
"name": "자녀1"
},
"actor": {
"customerId": null,
"name": null
},
"createdAt": "2024-01-15T12:05:00Z"
},
{
"logId": 500,
"actionType": "MISSION_REQUESTED",
"message": "보상 요청이 등록되었습니다",
"missionItem": {
"missionItemId": 201,
"missionText": "방 청소하기",
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"templateId": 1
}
},
"assignedTo": {
"customerId": 12346,
"name": "자녀1"
},
"actor": {
"customerId": 12346,
"name": "자녀1"
},
"createdAt": "2024-01-15T12:00:00Z"
}
],
"nextCursor": "eyJsb2dJZCI6NTAwfQ==",
"hasNext": true
},
"timestamp": "2024-01-15T10:30:00Z"
}actionType 값:
| actionType | 설명 |
|---|---|
MISSION_CREATED |
미션 생성 (부모) |
MISSION_REQUESTED |
보상 요청 (자녀) |
MISSION_COMPLETED |
미션 완료 처리 (시스템, actor=null) |
MISSION_CANCELLED |
미션 삭제/취소 (부모) |
GET /missions/history | 권한: member
familyId는 JWT 토큰에서 추론. 커서 기반 무한스크롤로 제공. OWNER는 가족 전체 요청 이력, MEMBER는 본인이 요청한 이력만 조회.MissionRequest기반 — 보상 요청의 처리 결과(승인/거절)를 조회할 때 사용.
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| status | string | ❌ | 필터: PENDING, APPROVED, REJECTED |
| cursor | string | ❌ | 다음 페이지 커서 (이전 응답의 nextCursor 값) |
| size | integer | ❌ | 조회 크기 (기본: 20) |
Responses:
200 OK:
{
"success": true,
"data": {
"requests": [
{
"requestId": 301,
"status": "APPROVED",
"missionItem": {
"missionItemId": 201,
"missionText": "방 청소하기",
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"templateId": 1
}
},
"requestedBy": {
"customerId": 12346,
"name": "자녀1"
},
"respondedBy": {
"customerId": 12345,
"name": "아빠"
},
"rejectReason": null,
"requestedAt": "2024-01-15T12:00:00Z",
"respondedAt": "2024-01-15T12:05:00Z",
"createdAt": "2024-01-15T12:00:00Z",
"updatedAt": "2024-01-15T12:05:00Z"
}
],
"nextCursor": "eyJyZXF1ZXN0SWQiOjMwMX0=",
"hasNext": true
},
"timestamp": "2024-01-15T10:30:00Z"
}무한스크롤 동작:
nextCursor가 null이면 더 이상 로드할 로그가 없음. 프론트엔드에서 스크롤 끝에 도달하면cursor파라미터로 다음 페이지를 요청.
POST /missions | 권한: owner
familyId,customerId는 JWT 토큰에서 추론
Request:
{
"targetCustomerId": 12346,
"missionText": "방 청소하기",
"rewardTemplateId": 1
}
targetCustomerId는 필수. MEMBER 역할인 가족 구성원만 대상 가능 (OWNER 지정 시MISSION_TARGET_INVALID400). 동일 미션을 여러 자녀에게 부여하려면 각 자녀별로 별도 생성.
Responses:
201 Created:
{
"success": true,
"data": {
"missionItemId": 202,
"createdAt": "2024-01-15T13:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}DELETE /missions/{missionId} | 권한: owner
familyId는 JWT 토큰에서 추론. 미션을 삭제하지 않고 status를 CANCELLED로 변경합니다.MISSION_LOG에action_type=MISSION_CANCELLED,actor_id=요청자(부모)로그를 기록합니다.
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| missionId | integer | 미션 항목 ID |
서버 처리 흐름:
- JWT에서
customerId추출, OWNER 역할 검증 MISSION_ITEM조회 →status=ACTIVE검증MISSION_ITEM.status = CANCELLED업데이트MISSION_LOGINSERT (action_type=MISSION_CANCELLED,actor_id=customerId,message=미션이 삭제되었습니다)- 200 응답 반환
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}POST /missions/{missionId}/request | 권한: member (MEMBER)
customerId,familyId는 JWT 토큰에서 추론 조건: MISSION_ITEM.status == ACTIVE 비즈니스 규칙: 요청자(JWT customerId) == MISSION_ITEM.target_customer_id 검증. 본인에게 배정된 미션만 요청 가능 (불일치 시MISSION_NOT_ASSIGNED403).
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| missionId | integer | 미션 항목 ID |
Responses:
201 Created:
{
"success": true,
"data": {
"requestId": 302,
"missionItem": {
"missionItemId": 201,
"missionText": "방 청소하기",
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"templateId": 1
}
},
"status": "PENDING",
"requestedBy": {
"customerId": 12346,
"name": "자녀1"
},
"createdAt": "2024-01-15T14:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}미션 완료 후 보상 승인/거절 처리를 담당합니다.
PUT /rewards/requests/{requestId}/respond | 권한: owner
familyId는 JWT 토큰에서 추론 부수효과: APPROVED 시 MISSION_ITEM.status가 COMPLETED로 변경됩니다. 비즈니스 규칙: status=APPROVED이면서 rejectReason이 있으면 400 에러 반환.
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| requestId | integer | 보상 요청 ID |
Request:
{
"status": "APPROVED",
"rejectReason": null
}Responses:
200 OK:
{
"success": true,
"data": {
"requestId": 302,
"status": "APPROVED",
"missionItem": {
"missionItemId": 201,
"missionText": "방 청소하기",
"status": "COMPLETED",
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"templateId": 1
}
},
"respondedBy": {
"customerId": 12345,
"name": "아빠"
},
"rejectReason": null,
"updatedAt": "2024-01-15T14:05:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /rewards/received | 권한: member (MEMBER)
customerId는 JWT 토큰에서 추론. 본인이 target인 미션의 APPROVED 보상 내역만 조회. OWNER는 사용 불가 (403). OWNER는GET /missions/logs로 전체 보상 현황 확인 가능.
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| cursor | string | ❌ | 커서 |
| size | integer | ❌ | 조회 크기 (기본: 20) |
Responses:
200 OK:
{
"success": true,
"data": {
"rewards": [
{
"requestId": 302,
"missionItem": {
"missionItemId": 201,
"missionText": "방 청소하기",
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"templateId": 1
}
},
"approvedBy": {
"customerId": 12345,
"name": "아빠"
},
"approvedAt": "2024-01-15T14:05:00Z"
}
],
"nextCursor": "eyJyZXF1ZXN0SWQiOjMwMn0=",
"hasNext": true
},
"timestamp": "2024-01-15T10:30:00Z"
}내부 쿼리: MEMBER 전용.
MISSION_REQUEST.status = 'APPROVED'ANDMISSION_ITEM.target_customer_id = JWT.customerId조건으로 조회.
GET /admin/rewards/templates | 권한: admin
Responses:
200 OK:
{
"success": true,
"data": [
{
"id": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"price": 3000,
"isSystem": true,
"isActive": true,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
],
"timestamp": "2024-01-15T10:30:00Z"
}GET /admin/rewards/templates/{id} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| id | integer | 보상 템플릿 ID |
Responses:
200 OK:
{
"success": true,
"data": {
"id": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"price": 3000,
"isSystem": true,
"isActive": true,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /admin/rewards/templates | 권한: admin
Request:
{
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"price": 3000
}Responses:
201 Created:
{
"success": true,
"data": {
"id": 3,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg",
"price": 3000,
"isSystem": false,
"isActive": true,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}PUT /admin/rewards/templates/{id} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| id | integer | 보상 템플릿 ID |
Request:
{
"name": "메가커피 아메리카노(ICE)(수정)",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee-v2.jpg",
"price": 3500,
"isActive": true
}Note (v21.0):
category는 템플릿 생성 시 결정되며, 수정 시 변경할 수 없습니다 (Figma: "유형은 변경할 수 없습니다").
Responses:
200 OK:
{
"success": true,
"data": {
"id": 1,
"name": "메가커피 아메리카노(ICE)(수정)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee-v2.jpg",
"price": 3500,
"isSystem": true,
"isActive": true,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-15T10:35:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}DELETE /admin/rewards/templates/{id} | 권한: admin
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| id | integer | 보상 템플릿 ID |
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}GET /admin/rewards/grants | 권한: admin
보상 지급 이력을 조회합니다. MISSION_REQUEST 승인 시 자동 생성된 지급 기록을 관리합니다.
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| page | integer | ❌ | 페이지 번호 (기본: 0) |
| size | integer | ❌ | 페이지 크기 (기본: 20) |
| status | string | ❌ | 상태 필터 (ISSUED, USED, EXPIRED) |
| sort | string | ❌ | 정렬 (LATEST | EXPIRING_SOON, 기본: LATEST) |
| unusedOnly | boolean | ❌ | true이면 status=ISSUED만 조회 |
| phoneNumber | string | ❌ | 전화번호 검색 |
Responses:
200 OK:
{
"success": true,
"data": {
"content": [
{
"grantId": 1,
"reward": {
"rewardId": 1,
"name": "메가커피 아메리카노(ICE)",
"category": "GIFTICON",
"thumbnailUrl": "https://cdn.dabom.site/rewards/mega-coffee.jpg"
},
"customer": {
"customerId": 12346,
"name": "자녀1",
"phoneNumber": "010-****-1234"
},
"mission": {
"missionItemId": 201,
"missionText": "방 청소하기"
},
"couponCode": "MEGA-1234-5678",
"couponUrl": "https://coupon.example.com/MEGA-1234-5678",
"status": "ISSUED",
"expiredAt": "2024-02-15T23:59:59Z",
"createdAt": "2024-01-15T14:05:00Z"
}
],
"page": 0,
"size": 20,
"totalElements": 150,
"totalPages": 8
},
"timestamp": "2024-01-15T10:30:00Z"
}월간 가족 활동 리캡을 제공합니다. ERD의 FAMILY_RECAP_MONTHLY 테이블과 1:1 매핑됩니다.
GET /recaps/monthly | 권한: member
familyId는 JWT 토큰에서 추론
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| year | integer | ✅ | 조회 연도 (예: 2024) |
| month | integer | ✅ | 조회 월 (1-12) |
Responses:
200 OK:
{
"success": true,
"data": {
"recapId": 401,
"familyId": 100,
"familyName": "김씨 가족",
"reportMonth": "2026-03-01",
"totalUsedBytes": 53687091200,
"totalQuotaBytes": 107374182400,
"usageRatePercent": 50.0,
"usageByWeekday": {
"monday": 15.2,
"tuesday": 18.5,
"wednesday": 22.1,
"thursday": 0.0,
"friday": 0.0,
"saturday": 20.3,
"sunday": 9.9
},
"peakUsage": {
"startHour": 21,
"endHour": 23,
"mostUsedWeekday": "sunday"
},
"missionSummary": {
"totalMissionCount": 10,
"completedMissionCount": 5,
"rejectedRequestCount": 3
},
"appealSummary": {
"totalAppeals": 4,
"approvedAppeals": 3,
"rejectedAppeals": 1
},
"appealHighlights": {
"topSuccessfulRequester": {
"requesterId": 12346,
"requesterName": "김민지",
"approvedAppealCount": 3,
"recentApprovedAppeals": [
{
"appealId": 91,
"approverId": 12345,
"approverName": "김철수",
"requestReason": "야간 차단 해제를 요청했어요.",
"requestedAt": "2026-03-21T14:32:00"
},
{
"appealId": 87,
"approverId": 12345,
"approverName": "김철수",
"requestReason": "주말 사용 제한 완화를 요청했어요.",
"requestedAt": "2026-03-18T20:10:00"
},
{
"appealId": 83,
"approverId": 12345,
"approverName": "김철수",
"requestReason": "인강 시청 시간 연장을 요청했어요.",
"requestedAt": "2026-03-12T19:05:00"
}
]
},
"topAcceptedApprover": {
"approverId": 12345,
"approverName": "김철수",
"approvedAppealCount": 3,
"recentAcceptedAppeals": [
{
"appealId": 91,
"requesterId": 12346,
"requesterName": "김민지",
"requestReason": "야간 차단 해제를 요청했어요.",
"resolvedAt": "2026-03-21T14:32:00"
},
{
"appealId": 87,
"requesterId": 12346,
"requesterName": "김민지",
"requestReason": "주말 사용 제한 완화를 요청했어요.",
"resolvedAt": "2026-03-18T20:10:00"
},
{
"appealId": 83,
"requesterId": 12347,
"requesterName": "김민수",
"requestReason": "인강 시청 시간 연장을 요청했어요.",
"resolvedAt": "2026-03-12T19:05:00"
}
]
}
},
"communicationScore": 82.5,
"generatedAt": "2026-03-01T00:00:00"
},
"timestamp": "2024-01-15T10:30:00Z"
}
missionSummary.totalMissionCount는 월간 생성된 미션 수입니다.completedMissionCount와rejectedRequestCount는 월간 완료 처리 또는 거절 처리된 이벤트 수입니다.appealSummary.totalAppeals는 월간 생성된type='NORMAL'이의제기 수입니다.approvedAppeals와rejectedAppeals는 월간 승인 처리 또는 거절 처리된type='NORMAL'이의제기 수입니다.appealHighlights는type='NORMAL' AND status='APPROVED'이면서resolved_at이 월 구간에 포함된 정책 이의제기만 집계합니다.EMERGENCY(긴급 요청)는 포함하지 않습니다.topSuccessfulRequester는 월간 승인 처리된 정책 이의제기를 가장 많이 성공한 구성원과 최신 승인 이력 최대 3개를 반환합니다.recentApprovedAppeals배열은 승인 처리 시각 기준 내림차순(resolved_at DESC, id DESC)이며, 각 항목에는 요청 시각requestedAt을 포함합니다.topAcceptedApprover는 월간 승인 처리된 정책 이의제기를 가장 많이 수락한 구성원과 최신 수락 이력 최대 3개를 반환합니다.recentAcceptedAppeals는resolvedAt내림차순(resolved_at DESC, id DESC)입니다. 데이터가 없으면 각 대표 인물의 ID/이름은null, 건수는0, 이력 배열은[]를 반환합니다.communicationScore는type='NORMAL'인 정책 이의제기와 mission 이벤트를 기반으로 계산한 월간 소통 점수입니다.EMERGENCY는 계산에서 제외합니다.appealCarryInCount는 월 시작 이전 생성됐고 월 시작 이전에 해결 또는 취소되지 않은type='NORMAL'이의제기 수입니다.missionCarryInCount는 월 시작 이전 생성됐고 월 시작 이전 완료 또는 취소 로그가 없는 미션 수입니다.appealBase = appealCarryInCount + totalAppeals,missionBase = missionCarryInCount + totalMissionCount로 계산합니다. 두 base가 모두 0이면communicationScore는null입니다. 한 base만 0이면 나머지 축 비율만 사용해round(rate * 100, 2)를 반환합니다. 두 base가 모두 양수이면appealResponseRate = (approvedAppeals + rejectedAppeals) / appealBase,missionCompletionRate = completedMissionCount / missionBase,communicationScore = round(((appealResponseRate * 0.55) + (missionCompletionRate * 0.45)) * 100, 2)를 반환합니다.
SSE 실시간 스트림(
GET /events/stream)과 병행하여, REST API로 알림 이력을 조회합니다. SSE는 실시간 푸시용이며, REST는 알림 이력 조회 및 읽음 처리용입니다.
GET /notifications | 권한: member
customerId는 JWT 토큰에서 추론. 서버에서sent_at >= NOW() - INTERVAL 30 DAY AND deleted_at IS NULL조건으로 필터링합니다.hasNext: false이면 프론트에서 "30일이 지난 알림은 표시되지 않습니다" 문구를 렌더링합니다.
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| cursor | string | ❌ | 다음 페이지 커서 (nextCursor 값, 미전달 시 첫 페이지) |
| size | integer | ❌ | 조회 크기 (기본: 20) |
| isRead | boolean | ❌ | 읽음 여부 필터 |
| type | string | ❌ | 콤마 구분 타입 필터 (예: CUSTOMER_BLOCKED,CUSTOMER_UNBLOCKED). 자세한 값은 아래 type Enum 값 표 참고. |
type Enum 값:
| 값 | 설명 |
|---|---|
QUOTA_UPDATED |
할당량 변경 알림 |
CUSTOMER_BLOCKED |
사용자 차단 알림 |
CUSTOMER_UNBLOCKED |
사용자 차단 해제 알림 |
THRESHOLD_ALERT |
잔여량 임계치 도달 알림 |
POLICY_CHANGED |
정책 변경 알림 |
MISSION_CREATED |
새 미션 생성 알림 |
REWARD_REQUESTED |
보상 요청 알림 |
REWARD_APPROVED |
보상 승인 알림 |
REWARD_REJECTED |
보상 거절 알림 |
APPEAL_CREATED |
이의제기 생성 알림 |
APPEAL_APPROVED |
이의제기 승인 알림 |
APPEAL_REJECTED |
이의제기 거절 알림 |
EMERGENCY_APPROVED |
긴급 요청 승인 알림 |
ADMIN_PUSH |
관리자 수동 Push 알림 |
Responses:
200 OK:
{
"success": true,
"data": {
"content": [
{
"notificationId": 1001,
"type": "THRESHOLD_ALERT",
"title": "데이터 경고",
"message": "가족 데이터가 50% 남았습니다",
"payload": {
"threshold": 50,
"remainingPercent": 48.5
},
"isRead": false,
"sentAt": "2024-01-15T10:00:00Z"
},
{
"notificationId": 1002,
"type": "CUSTOMER_BLOCKED",
"title": "데이터 차단",
"message": "데이터 한도 초과로 차단되었습니다",
"payload": {
"reason": "MONTHLY_LIMIT_EXCEEDED"
},
"isRead": true,
"sentAt": "2024-01-15T09:30:00Z"
}
],
"nextCursor": "eyJub3RpZmljYXRpb25JZCI6MTAwMn0=",
"hasNext": true,
"unreadCount": 5
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /notifications/unread-count | 권한: member
customerId는 JWT 토큰에서 추론. 30일 이내(sent_at >= NOW() - INTERVAL 30 DAY) + 소프트 삭제 미적용(deleted_at IS NULL) 조건으로 집계합니다. 앱 배지 표시 등 경량 카운트 조회에 사용합니다.
Responses:
200 OK:
{
"success": true,
"data": {
"unreadCount": 5
},
"timestamp": "2024-01-15T10:30:00Z"
}PATCH /notifications/{notificationId}/read | 권한: member
customerId는 JWT 토큰에서 추론
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| notificationId | integer | 알림 ID |
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}PATCH /notifications/read-all | 권한: member
customerId는 JWT 토큰에서 추론. 해당 사용자의 모든 미읽음 알림을 읽음으로 처리합니다.
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}DELETE /notifications/{notificationId} | 권한: member
customerId는 JWT 토큰에서 추론. 소프트 삭제(deleted_at설정)로 처리합니다. 본인 알림만 삭제 가능합니다.
Path Parameters:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| notificationId | integer | 알림 ID |
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}관리자 전용 인증 및 백오피스 관리 엔드포인트. CUSTOMER 테이블과 분리된 ADMIN 테이블에 대해 인증을 수행합니다.
POST /admin/signup | 권한: 없음
Request:
{
"email": "admin001@dabom.site",
"password": "password123"
}Responses:
201 Created:
{
"success": true,
"data": {
"id": 1
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /admin/login | 권한: 없음
Request:
{
"email": "admin001@dabom.site",
"password": "password123"
}Responses:
200 OK:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"role": "ADMIN"
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /admin/refresh | 권한: 없음
Request:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Responses:
200 OK:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 1800
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /admin/me | 권한: admin
Responses:
200 OK:
{
"success": true,
"data": {
"adminId": 1,
"email": "admin001@dabom.site",
"name": "관리자"
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /admin/logout | 권한: admin
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}GET /admin/dashboard | 권한: admin
Responses:
200 OK:
{
"success": true,
"data": {
"totalFamilies": 250000,
"activeFamilies": 248500,
"totalUsers": 1000000,
"blockedUsers": 1523,
"todayEvents": 432000000,
"currentTps": 5000,
"systemHealth": {
"redis": "UP",
"kafka": "UP",
"mysql": "UP"
},
"recentBlocks": [
{
"familyId": 100,
"customerId": 12346,
"reason": "MONTHLY_LIMIT_EXCEEDED",
"blockedAt": "2024-01-15T10:30:00Z"
}
]
},
"timestamp": "2024-01-15T10:30:00Z"
}GET /admin/audit/logs | 권한: admin
Query Parameters:
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| action | string | ❌ | 액션 타입 필터 |
| entityType | string | ❌ | 엔티티 유형 필터 (FAMILY, CUSTOMER, POLICY 등) |
| actorId | integer | ❌ | 행위자 ID 필터 |
| page | integer | ❌ | 페이지 번호 (기본: 0) |
| size | integer | ❌ | 페이지 크기 (기본: 20) |
Responses:
200 OK:
{
"success": true,
"data": {
"content": [
{
"logId": 10001,
"actorId": 99999,
"actorName": "관리자",
"action": "POLICY_CHANGED",
"entityType": "FAMILY",
"entityId": 100,
"oldValue": { "totalQuotaBytes": 107374182400 },
"newValue": { "totalQuotaBytes": 214748364800 },
"ipAddress": "192.168.1.100",
"createdAt": "2024-01-15T10:30:00Z"
}
],
"page": 0,
"size": 20,
"totalElements": 1523,
"totalPages": 77
},
"timestamp": "2024-01-15T10:30:00Z"
}클라이언트에서 업로드한 이미지를 R2에 저장하고 CDN URL을 반환합니다.
POST /uploads/images | 권한: admin
백오피스에서 보상 썸네일, 미션 이미지 등을 업로드합니다. 서버가 UUID 기반 파일명을 생성하고, type에 따라 R2 저장 경로를 결정합니다. 클라이언트는 반환된 URL을 이후 도메인 API 요청(예:
POST /admin/rewards/templates)에 사용합니다.
Headers:
Content-Type: multipart/form-dataRequest Body:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| file | file | ✅ | 업로드할 이미지 파일 |
| type | string | ✅ | 이미지 용도 (REWARD, PROFILE, MISSION) |
파일 검증 정책:
| 항목 | 값 |
|---|---|
| 허용 MIME | image/png, image/jpeg, image/webp |
| 최대 크기 | 5MB |
| 파일명 생성 | UUID v4 + Content-Type 기반 확장자 |
type ENUM → R2 경로 매핑:
| type | R2 경로 prefix | 설명 |
|---|---|---|
| REWARD | rewards/ | 보상 썸네일 |
| PROFILE | profiles/ | 사용자 프로필 |
| MISSION | missions/ | 미션 관련 이미지 |
서버 처리 흐름:
- multipart 요청 수신
- 파일 존재 여부 검증
- 파일 크기 검증 (5MB 초과 시 거부)
- MIME 타입 검증 (허용 목록 외 거부)
- UUID v4 파일명 생성
- Content-Type 기반 확장자 결정 (image/png→.png, image/jpeg→.jpg, image/webp→.webp)
- type 기반 R2 경로 생성 (
{type}/{uuid}.{ext}) - R2 업로드 (S3 호환 API)
- CDN URL 생성 (
https://cdn.dabom.site/{type}/{uuid}.{ext}) - URL 반환
Responses:
201 Created:
{
"success": true,
"data": {
"url": "https://cdn.dabom.site/rewards/550e8400-e29b-41d4-a716-446655440000.png"
},
"timestamp": "2026-03-12T10:30:00Z"
}PWA Web Push 구독 관리 및 수동 발송 엔드포인트입니다. 구현 코드(
dabom-api-notification의WebPushController) 기반입니다.
GET /push/vapid-public-key | 권한: 없음
인증 불필요. 프론트가
PushManager.subscribe()호출 전에 VAPID 공개키를 조회합니다.
Responses:
200 OK:
{
"success": true,
"data": {
"publicKey": "BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U"
},
"timestamp": "2024-01-15T10:30:00Z"
}POST /push/subscribe | 권한: member
같은 endpoint + 같은 customer → 키 갱신, 같은 endpoint + 다른 customer → 재할당, 새 endpoint → 신규 생성. endpoint URL 검증: HTTPS 필수, 내부 IP 차단 (SSRF 방어).
Request:
{
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8p8REfWRk=",
"auth": "tBHItJI5svbpC7htDNae8w=="
}
}Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}DELETE /push/subscribe | 권한: member
customerId는 JWT 토큰에서 추론. 구독 정보를 삭제합니다 (hard delete).
Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}POST /push/send | 권한: admin
운영자가 특정 사용자에게 수동으로 Push 알림을 발송합니다 (시스템 공지, 긴급 안내 등).
Request:
{
"customerId": 12345,
"title": "시스템 공지",
"message": "서비스 점검이 예정되어 있습니다."
}Responses:
200 OK:
{
"success": true,
"data": null,
"timestamp": "2024-01-15T10:30:00Z"
}아키텍처 변경 (v3.0): processor-usage는 Kafka Consumer로 동작하며, 직접적인 HTTP API는 제공하지 않습니다. 내부적으로 Redis Lua Script를 실행하여 정책 평가를 수행합니다. 결과 notification은
usage_event_outbox에 적재되며, 배치 서버가notification-events로 후행 발행합니다.
배치 서버가 usage_event_outbox를 조회해 notification-events 토픽으로 발행하는 Kafka 이벤트 (EventEnvelope 패턴):
Note (v24.0):
eventType은NOTIFICATION으로 고정하며,subType없이payload.type으로 구분합니다.
THRESHOLD_ALERT 예시:
{
"eventId": "evt_123",
"eventType": "NOTIFICATION",
"timestamp": "2026-03-16T10:15:30Z",
"payload": {
"familyId": 100,
"customerId": 1,
"type": "THRESHOLD_ALERT",
"title": "데이터 사용량 경고",
"message": "가족 데이터 잔여량이 10% 미만입니다.",
"data": {
"threshold": 10,
"triggerStatus": "WARNING_10"
}
}
}GET /events/stream | 권한: member
customerId는 JWT 토큰에서 추론합니다 (경로 파라미터 없음). SSE 실시간 스트림과 REST/notifications/*엔드포인트가 공존합니다. SSE는 실시간 푸시용이며, REST는 알림 이력 조회용입니다. SSE 스트림은 api-notification(noti.dabom.site)에서 제공합니다.text/event-stream응답.
Headers:
Accept: text/event-stream
Authorization: Bearer <access_token>실제 전송 이벤트 이름:
- 연결:
connected - 하트비트:
heartbeat - 총 사용량:
usage-updated - 구성원별 사용량:
usage-updated-by-member - 알림 이벤트:
NotificationType.name()그대로 사용
실시간 사용량 payload (usage-updated):
{
"familyId": 10,
"totalUsedBytes": 123456789,
"totalQuotaBytes": 214748364800,
"remainingBytes": 214624908011
}구성원별 사용량 payload (usage-updated-by-member):
{
"familyId": 10,
"customerId": 23,
"monthlyUsedBytes": 734003200
}event: notification
data: {"eventType":"NOTIFICATION","timestamp":"2026-03-16T10:15:30Z","payload":{"familyId":100,"customerId":1,"type":"THRESHOLD_ALERT","title":"데이터 사용량 경고","message":"가족 데이터 잔여량이 10% 미만입니다.","data":{"threshold":10,"triggerStatus":"WARNING_10"}}}
event: notification
data: {"eventType":"CUSTOMER_UNBLOCKED","familyId":100,"payload":{"customerId":12346,"reason":"TIME_BLOCK_RELEASED","unblockedAt":"2024-01-15T18:00:00Z"}}
event: policy-updated
data: {"policyKey":"LIMIT:DATA:MONTHLY","targetCustomerId":12346,"oldValue":"2147483648","newValue":"536870912"}
event: notification
data: {"eventType":"POLICY_CHANGED","familyId":100,"payload":{"customerId":12346,"policyType":"LIMIT:DATA:MONTHLY","changedAt":"2024-01-15T10:30:00Z"}}
event: notification
data: {"eventType":"MISSION_CREATED","familyId":100,"payload":{"missionId":501,"title":"독서 미션","assignedTo":12346,"createdAt":"2024-01-15T10:30:00Z"}}
event: notification
data: {"eventType":"REWARD_REQUESTED","familyId":100,"payload":{"missionId":501,"requesterId":12346,"requesterName":"김민수","requestedAt":"2024-01-15T11:00:00Z"}}
event: notification
data: {"eventType":"REWARD_APPROVED","familyId":100,"payload":{"missionId":501,"rewardId":201,"approvedAt":"2024-01-15T12:00:00Z"}}
event: notification
data: {"eventType":"REWARD_REJECTED","familyId":100,"payload":{"missionId":501,"rejectReason":"미션 조건 미충족","rejectedAt":"2024-01-15T12:00:00Z"}}
event: notification
data: {"eventType":"APPEAL_CREATED","familyId":100,"payload":{"appealId":301,"requesterId":12346,"requesterName":"김민수","createdAt":"2024-01-15T10:00:00Z"}}
event: notification
data: {"eventType":"APPEAL_APPROVED","familyId":100,"payload":{"appealId":301,"resolvedAt":"2024-01-15T13:00:00Z"}}
event: notification
data: {"eventType":"APPEAL_REJECTED","familyId":100,"payload":{"appealId":301,"rejectReason":"이의제기 사유 불충분","resolvedAt":"2024-01-15T13:00:00Z"}}
event: notification
data: {"eventType":"EMERGENCY_APPROVED","familyId":100,"payload":{"appealId":302,"grantedBytes":104857600,"approvedAt":"2024-01-15T14:00:00Z"}}
- Heartbeat: 30초마다
:heartbeat전송 - 재연결: 연결 끊김 시 클라이언트가 자동 재연결 (Last-Event-ID 헤더 활용)
GET /events/stream/test/{customerId} | 내부 테스트 전용
인증 없이 특정 고객 기준으로 SSE를 구독하는 테스트 엔드포인트. 프로덕션에서는 사용하지 않는다.
api-core 커밋 778d64e 기준으로 아래 동작은 DB 커밋 이후 usage_event_outbox를 통해 notification 이벤트를 발행한다.
PATCH /families/policiesMONTHLY_LIMIT수정 시QUOTA_UPDATEDMANUAL_BLOCK활성화 시CUSTOMER_BLOCKEDMANUAL_BLOCK비활성화 시CUSTOMER_UNBLOCKEDTIME_BLOCK,APP_BLOCK수정 시POLICY_CHANGED
POST /appeals→ 각 OWNER에게APPEAL_CREATEDPATCH /appeals/{appealId}/respond→ 요청자에게APPEAL_APPROVED또는APPEAL_REJECTEDPOST /appeals/emergency→ 각 OWNER에게EMERGENCY_APPROVEDPOST /missions→ 대상 구성원에게MISSION_CREATEDPOST /missions/{missionId}/request→ 각 OWNER에게REWARD_REQUESTEDPUT /rewards/requests/{requestId}/respond→ 요청자에게REWARD_APPROVED또는REWARD_REJECTED
| 코드 | HTTP | 설명 |
|---|---|---|
| AUTH_INVALID_CREDENTIALS | 401 | 잘못된 인증 정보 |
| AUTH_TOKEN_EXPIRED | 401 | 토큰 만료 |
| AUTH_TOKEN_INVALID | 401 | 유효하지 않은 토큰 |
| AUTH_INSUFFICIENT_PERMISSION | 403 | 권한 부족 |
| 코드 | HTTP | 설명 |
|---|---|---|
| DATA_FAMILY_NOT_FOUND | 404 | 가족 그룹 없음 |
| DATA_USER_NOT_FOUND | 404 | 사용자 없음 |
| DATA_MEMBER_LIMIT_EXCEEDED | 400 | 최대 구성원 수 초과 (10명) |
| 코드 | HTTP | 설명 |
|---|---|---|
| POLICY_USER_BLOCKED | 403 | 사용자 차단됨 |
| POLICY_QUOTA_EXCEEDED | 403 | 할당량 초과 |
| POLICY_TIME_BLOCKED | 403 | 시간대 차단 중 |
| POLICY_ALREADY_BLOCKED | 409 | 이미 차단됨 |
| POLICY_TEMPLATE_NOT_FOUND | 404 | 정책 템플릿 없음 |
| POLICY_TEMPLATE_IN_USE | 409 | 사용 중인 정책 템플릿 삭제 시도 |
| 코드 | HTTP | 설명 |
|---|---|---|
| APPEAL_NOT_FOUND | 404 | 이의제기를 찾을 수 없음 |
| APPEAL_ALREADY_RESOLVED | 409 | 이미 처리된 이의제기 |
| APPEAL_FORBIDDEN | 403 | 이의제기에 접근 권한 없음 |
| APPEAL_INVALID_DESIRED_RULES | 400 | desiredRules 스키마가 policy.type과 맞지 않음 |
| APPEAL_EMERGENCY_MONTHLY_LIMIT | 429 | 이번 달 긴급 요청을 이미 사용함 |
| APPEAL_EMERGENCY_INVALID_BYTES | 400 | 긴급 요청 바이트 입력 검증 실패 (현재 API 미사용) |
| APPEAL_EMERGENCY_UNLIMITED | 400 | 무제한 쿼터 사용자는 긴급 요청 불가 |
| APPEAL_CANCEL_FORBIDDEN | 403 | 본인이 생성한 이의제기가 아님 |
| APPEAL_NOT_CANCELLABLE | 409 | PENDING 상태가 아니라 취소 불가 |
| APPEAL_EMERGENCY_CANCEL_NOT_ALLOWED | 400 | EMERGENCY 타입은 취소 불가 |
| 코드 | HTTP | 설명 |
|---|---|---|
| MISSION_NOT_FOUND | 404 | 미션을 찾을 수 없음 |
| MISSION_NOT_ACTIVE | 409 | 미션이 활성 상태가 아님 |
| MISSION_ALREADY_COMPLETED | 409 | 이미 완료된 미션 |
| REWARD_TEMPLATE_NOT_FOUND | 404 | 보상 템플릿을 찾을 수 없음 |
| MISSION_REQUEST_NOT_FOUND | 404 | 보상 요청을 찾을 수 없음 |
| MISSION_REQUEST_ALREADY_RESOLVED | 409 | 이미 처리된 보상 요청 |
| MISSION_TARGET_INVALID | 400 | 미션 대상이 유효하지 않음 (MEMBER 역할만 대상 가능) |
| MISSION_NOT_ASSIGNED | 403 | 본인에게 배정되지 않은 미션에 대한 요청 |
| REWARD_GRANT_NOT_FOUND | 404 | 보상 지급 이력을 찾을 수 없음 |
| 코드 | HTTP | 설명 |
|---|---|---|
| RECAP_NOT_FOUND | 404 | 리캡을 찾을 수 없음 |
| 코드 | HTTP | 설명 |
|---|---|---|
| NOTIFICATION_NOT_FOUND | 404 | 알림 없음 |
| NOTIFICATION_FORBIDDEN | 403 | 해당 알림에 대한 권한 없음 |
| 코드 | HTTP | 설명 |
|---|---|---|
| UPLOAD_FILE_REQUIRED | 400 | 파일이 없음 |
| UPLOAD_INVALID_TYPE | 400 | 지원하지 않는 MIME |
| UPLOAD_FILE_TOO_LARGE | 400 | 파일 크기 초과 |
| UPLOAD_FAILED | 500 | R2 업로드 실패 |
| 코드 | HTTP | 설명 |
|---|---|---|
| SUBSCRIPTION_NOT_FOUND | 404 | 구독 정보 없음 |
| PUSH_SEND_FAILED | 500 | Push 발송 실패 |
| INVALID_ENDPOINT_URL | 400 | 유효하지 않은 구독 엔드포인트 URL |
| 코드 | HTTP | 설명 |
|---|---|---|
| SYS_INTERNAL_ERROR | 500 | 내부 서버 오류 |
| SYS_REDIS_UNAVAILABLE | 503 | Redis 장애 (DB Fallback 모드) |
| SYS_RATE_LIMIT_EXCEEDED | 429 | API 호출 제한 초과 |
| SYS_SERVICE_UNAVAILABLE | 503 | 서비스 이용 불가 |
| 코드 | HTTP | 설명 |
|---|---|---|
| OUTBOX_001 | 500 | Outbox 이벤트 발행 실패 |
총 63개 공개 엔드포인트 + 내부 테스트 SSE 1개 (v24.3 기준)
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| POST | /customers/login |
로그인 | 없음 |
| POST | /customers/refresh |
Access Token 재발급 | 없음 |
| POST | /customers/logout |
로그아웃 | 없음 |
| POST | /customers/signup |
회원가입 (✨UPDATED) | 없음 |
| GET | /customers/me |
내 계정 정보 조회 | member |
| GET | /customers/mypage |
마이페이지 조회 (🌟 New) | member |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| POST | /admin/families |
가족 그룹 검색 | admin |
| GET | /families/usage/current |
실시간 가족 사용량 | member |
| GET | /families/usage/customers |
실시간 구성원별 사용량 | member |
| GET | /admin/families/{familyId} |
가족 정보 상세 조회 | admin |
| PATCH | /admin/families/{familyId} |
관리자 가족 구성원 수정 (🌟 New) | admin |
| GET | /families/policies |
가족 구성원 정책 조회 | member |
| PATCH | /families/policies |
가족 구성원 정책 수정 | owner |
| GET | /families/members |
가족 구성원 중 자녀 목록 조회 | owner |
| PUT | /families |
가족 이름 수정 | owner |
| GET | /families/usage/dashboard |
사용량 통계 대시보드 | member |
| GET | /rewards/templates |
보상 템플릿 목록 조회 | owner |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| GET | /policies |
정책 조회 | admin |
| POST | /policies |
정책 생성 | admin |
| DELETE | /policies/{policyId} |
정책 삭제 | admin |
| GET | /policies/{policyId} |
정책 상세 조회 | admin |
| PUT | /policies/{policyId} |
정책 수정 | admin |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| GET | /appeals/policies |
이의제기 가능 정책 목록 조회 (🌟 New) | member |
| GET | /appeals |
이의제기 목록 조회 | member |
| GET | /appeals/{appealId} |
이의제기 상세 조회 | member |
| POST | /appeals |
이의제기 생성 | member (MEMBER) |
| PATCH | /appeals/{appealId}/respond |
이의제기 승인/거절 | owner |
| POST | /appeals/{appealId}/comments |
이의제기 댓글 작성 | member |
| POST | /appeals/emergency |
긴급 쿼터 요청 | member (MEMBER) |
| PATCH | /appeals/{appealId}/cancel |
이의제기 취소 | member (MEMBER) |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| GET | /missions |
미션 항목 목록 조회 | member |
| GET | /missions/logs |
미션 상태 변화 로그 | member |
| GET | /missions/history |
미션 요청 이력 조회 | member |
| POST | /missions |
미션 + 보상 생성 | owner |
| DELETE | /missions/{missionId} |
미션 삭제 | owner |
| POST | /missions/{missionId}/request |
미션 승인 요청 | member (MEMBER) |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| PUT | /rewards/requests/{requestId}/respond |
보상 승인/거절 처리 | owner |
| GET | /rewards/received |
내가 받은 보상 조회 | member (MEMBER) |
| GET | /admin/rewards/templates |
보상 템플릿 목록 조회 | admin |
| GET | /admin/rewards/templates/{id} |
보상 템플릿 상세 조회 | admin |
| POST | /admin/rewards/templates |
보상 템플릿 생성 | admin |
| PUT | /admin/rewards/templates/{id} |
보상 템플릿 수정 | admin |
| DELETE | /admin/rewards/templates/{id} |
보상 템플릿 삭제 | admin |
| GET | /admin/rewards/grants |
보상 지급 내역 조회 | admin |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| GET | /recaps/monthly |
월간 가족 리캡 조회 | member |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| GET | /notifications |
알림 목록 (커서 무한스크롤, type·isRead, 30일) | member |
| GET | /notifications/unread-count |
읽지 않은 알림 수 (30일 이내) | member |
| PATCH | /notifications/{notificationId}/read |
개별 읽음 처리 | member |
| PATCH | /notifications/read-all |
전체 읽음 처리 | member |
| DELETE | /notifications/{notificationId} |
알림 삭제 | member |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| POST | /admin/signup |
관리자 회원가입 (🌟 New) | 없음 |
| POST | /admin/login |
관리자 로그인 | 없음 |
| POST | /admin/refresh |
관리자 토큰 갱신 | 없음 |
| GET | /admin/me |
관리자 내 정보 조회 (🌟 New) | admin |
| POST | /admin/logout |
관리자 로그아웃 | 없음 |
| GET | /admin/dashboard |
관리자 대시보드 | admin |
| GET | /admin/audit/logs |
감사 로그 조회 | admin |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| POST | /uploads/images |
이미지 업로드 | admin |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| GET | /push/vapid-public-key |
VAPID 공개키 조회 | 없음 |
| POST | /push/subscribe |
Push 구독 등록 | member |
| DELETE | /push/subscribe |
Push 구독 해제 | member |
| POST | /push/send |
Push 수동 발송 (운영용) | admin |
| 메서드 | 경로 | 설명 | 권한 |
|---|---|---|---|
| GET | /events/stream |
SSE 실시간 이벤트 스트림 | member |
| GET | /events/stream/test/{customerId} |
내부 테스트 SSE (인증 없음) | 내부 테스트 |