Skip to content

Latest commit

 

History

History
413 lines (288 loc) · 14.9 KB

File metadata and controls

413 lines (288 loc) · 14.9 KB

kibble HTTP API

셀프호스트 반려동물 일지 REST API. Phase 1 기준 엔드포인트와 curl 예제만 다룬다. 특정 플랫폼(Home Assistant, iOS 단축어 등) 가이드는 포함하지 않는다.

기본 URL: 배포 환경에 맞게 BASE를 바꾼다. 아래 예제는 http://localhost:8080을 가정한다.

BASE=http://localhost:8080

인증

세션 (JWT)

로그인하면 accessToken 쿠키와 응답 본문에 JWT가 내려온다. curl에서는 Authorization: Bearer 헤더를 쓴다.

# 첫 관리자 생성 (인스턴스에 사용자가 없을 때만)
curl -sS -X POST "$BASE/api/auth/bootstrap" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Admin","email":"admin@example.com","password":"change-me-12"}'

# 로그인
curl -sS -X POST "$BASE/api/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"change-me-12"}'

# 이후 요청 (응답 JSON의 accessToken 사용)
TOKEN="<accessToken from login>"
AUTH="Authorization: Bearer $TOKEN"

API 토큰 (kbl_…)

자동 입력용. 가구 OWNER 세션으로만 발급·폐기할 수 있다. 기본적으로 대부분의 라우트는 ApiToken을 거부한다(403).

허용 라우트와 스코프:

라우트 필요한 스코프
POST /api/events event:create
GET /api/states state:read

scopes를 생략하면 ["event:create"]만 발급된다. 기존 토큰은 state:read가 없으므로 상태 조회를 하려면 새로 발급해야 한다.

# 토큰 발급 (plaintext는 이 응답에서만 한 번 노출)
curl -sS -X POST "$BASE/api/tokens" \
  -H "Content-Type: application/json" \
  -H "$AUTH" \
  -d '{"name":"feeder","presetId":"<preset-id>"}'

API_TOKEN="kbl_…"

# 이벤트 기록 (스코프에 맞는 presetId/petId만)
curl -sS -X POST "$BASE/api/events" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_TOKEN" \
  -d '{"dedupeKey":"sensor:meal:2026-08-31T08:00:00Z"}'

dedupeKey는 재시도 시 중복 생성을 막는다. 같은 키로 다시 보내면 기존 이벤트가 반환된다(소프트삭제된 경우 복구).


헬스

curl -sS "$BASE/health"
# {"status":"ok"}

curl -sS "$BASE/api/home" -H "$AUTH"
# pets, activePet, presets, todaySummary, recentEvents

특정 반려동물 기준:

curl -sS "$BASE/api/home?petId=<pet-id>" -H "$AUTH"

반려동물

# 목록
curl -sS "$BASE/api/pets" -H "$AUTH"

# 등록 (name + species만 필수)
curl -sS -X POST "$BASE/api/pets" \
  -H "Content-Type: application/json" \
  -H "$AUTH" \
  -d '{"name":"보리","species":"CAT"}'

species: CAT | DOG | OTHER


프리셋

curl -sS "$BASE/api/presets?petId=<pet-id>" -H "$AUTH"

루틴

미리 정한 값(사료 10g, 영양제 3종)을 1탭으로 저장하는 정의 (WORKPLAN §7.24). 세션 전용 — 정의의 CRUD만 있고, 저장은 항목마다 POST /api/events 로 한다 (K-4).

curl -sS "$BASE/api/routines?petId=<pet-id>" -H "$AUTH"

curl -sS -X POST "$BASE/api/routines" -H "$AUTH" -H "Content-Type: application/json" -d '{
  "petId": "<pet-id>",
  "label": "아침 밥",
  "items": [
    { "eventTypeId": "<meal-type-id>", "presetId": "<meal-preset-id>", "productId": "<product-id>", "quantity": 10, "unit": "g" },
    { "eventTypeId": "<water-type-id>", "quantity": 5.5, "unit": "ml" }
  ]
}'

# PATCH — items가 있으면 항목 전체를 바꾼다
curl -sS -X PATCH "$BASE/api/routines/<id>" -H "$AUTH" -H "Content-Type: application/json" -d '{ "label": "저녁 밥" }'
curl -sS -X DELETE "$BASE/api/routines/<id>" -H "$AUTH"

항목의 타입·칩·제품은 같은 가구·같은 반려동물 것이어야 한다(404). medication 타입은 받지 않는다 — 처방·회차가 끼어 1탭이 안 된다.


상태 조회 (역방향)

밖에서 kibble의 현재 상태를 읽는다. 세션 또는 state:read 토큰. 읽기 전용이다 (K-7).

# 세션으로
curl -sS "$BASE/api/states?petId=<pet-id>" -H "$AUTH"

# 자동화(홈어시스턴트 등)에서 토큰으로 — 개체 스코프 토큰이면 petId 생략 가능
curl -sS "$BASE/api/states" -H "Authorization: Bearer kbl_..."

응답에 담기는 것:

필드 내용
pet 대상 반려동물
lastEvents[] 이벤트 타입별 마지막 기록 — 시각, 수량·단위, 척도값, hoursSince(경과 시간)
today[] 오늘(KST 기준) 타입별 건수와 합계 — 급여량·음수량 등
todaySince 오늘 합계의 시작 경계
medication 진행 중 과정 수, 오늘 먹인/계획된 횟수, 시각이 지난 슬롯
reminders[] 예정일과 지남 여부

이벤트 타입을 코드에 나열하지 않으므로(K-8), 프리셋·타입을 늘리면 응답이 저절로 따라온다.


이벤트

생성

세션 또는 ApiToken(event:create 스코프).

# 프리셋 1탭 기록 (웹 퀵 칩과 동일)
curl -sS -X POST "$BASE/api/events" \
  -H "Content-Type: application/json" \
  -H "$AUTH" \
  -d '{
    "petId": "<pet-id>",
    "presetId": "<preset-id>",
    "source": "WEB",
    "dedupeKey": "curl:meal:1"
  }'

빈 본문 + ApiToken(프리셋 스코프 고정)도 가능:

curl -sS -X POST "$BASE/api/events" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_TOKEN" \
  -d '{"dedupeKey":"auto:001"}'

선택 필드: occurredAt(ISO 8601), quantity, quantityOffered, unit, scaleValue, costKrw(병원비, 정수), note, rawText, eventTypeId(프리셋 없이 직접 지정 시), productId(제품 연결), productName(자유 텍스트 제품명).

단건 읽기

curl -sS "$BASE/api/events/<event-id>" -H "$AUTH"

세션 전용이다. 토큰은 개체·프리셋 스코프인데 임의 :id 읽기를 열면 가구 안의 다른 기록까지 보인다 — 밖에서 읽을 것은 GET /api/states다.

목록 (타임라인)

curl -sS "$BASE/api/events?petId=<pet-id>&limit=30" -H "$AUTH"

페이지네이션 커서: before(ISO 시각) + beforeId(이벤트 id).

수정

curl -sS -X PATCH "$BASE/api/events/<event-id>" \
  -H "Content-Type: application/json" \
  -H "$AUTH" \
  -d '{"note":"잘 먹음","quantity":40,"unit":"g","productId":"<product-id>"}'

소프트 삭제 / 복구

curl -sS -X DELETE "$BASE/api/events/<event-id>" -H "$AUTH"
# 204

curl -sS -X POST "$BASE/api/events/<event-id>/restore" -H "$AUTH"

제품 (사료·영양제·용품)

반려동물이 급여·복용하는 사료, 영양제, 간식, 위생/기기 용품을 관리합니다. 이벤트에 연결(productId)하여 어떤 제품을 급여했는지 추적하고, 개봉 경과일 및 소비기한 D-Day를 확인합니다.

카테고리(ProductCategory): MEAL(사료), SUPPLEMENT(영양제), TREAT(간식), HYGIENE(위생), DEVICE(기기), OTHER(기타).

목록

# 기본 활성 제품 목록
curl -sS "$BASE/api/products" -H "$AUTH"

# 필터 옵션 (카테고리, 펫, 활성 여부, 보관 여부)
curl -sS "$BASE/api/products?category=SUPPLEMENT&petId=<pet-id>&isActive=true&archived=false" -H "$AUTH"

등록

curl -sS -X POST "$BASE/api/products" \
  -H "Content-Type: application/json" \
  -H "$AUTH" \
  -d '{
    "name": "오메가3 오일",
    "category": "SUPPLEMENT",
    "brand": "닥터벳",
    "petId": "<pet-id>",
    "form": "LIQUID",
    "origin": "노르웨이",
    "weightG": 250,
    "dosage": "1일 1회 1펌프",
    "mainIngredients": "오메가3 1000mg, 비타민E",
    "expiryDate": "2027-12-31T00:00:00.000Z",
    "openedAt": "2026-09-01T00:00:00.000Z",
    "costKrw": 35000,
    "purchaseUrl": "https://example.com/product/omega3",
    "ingredients": "정제어유, 비타민E",
    "notes": "냉장 보관 필수"
  }'

선택 필드: brand, petId, origin, form, kibbleSize, weightG, dosage, mainIngredients, ingredients, flavor, usage, registeredIngredients, ingredientRegistrationNo, importer, manufacturedAt, storage, expiryDate, openedAt, purchaseDate, costKrw(정수), purchaseUrl(http/https URL), isActive, palatability, adverseReactions(string[]), notes.

날짜 4종(expiryDate·openedAt·purchaseDate·manufacturedAt)은 ISO 8601 datetime입니다 — 2027-12-31처럼 날짜만 주면 400입니다.

제형과 입자크기

  • form: DRY(건식) · WET(습식) · SEMI_MOIST(반습식) · GEL(겔형 — 젤리·양갱) · LICKABLE(츄르형) · CHEWY(츄잉형) · POWDER · CAPSULE · TABLET · LIQUID
  • kibbleSize: SMALL(소립) · MEDIUM(중립) · LARGE(대립)

kibbleSizeformDRY일 때만 저장됩니다. 그 외의 제형으로 보내면 서버가 null로 정리하며, 나중에 formWET으로 바꿔도 저장돼 있던 kibbleSize가 함께 지워집니다 — "습식인데 소립"이 남지 않게 하기 위해서입니다.

카테고리

MEAL(사료) · SUPPLEMENT(영양제) · MEDICATION(약·제제) · TREAT(간식) · HYGIENE(위생용품) · DEVICE(기기) · OTHER(기타).

MEDICATION은 지사제·억제제처럼 투약(MedicationCourse) 축과 짝이 맞는 물건입니다. "급성/만성"은 상태라 카테고리가 아니라 usage(용도)에 적습니다 (R111).

성분 세 칸

사료 라벨에서 실제로 다른 것들이라 나눠 받습니다 — mainIngredients(주성분, 목록 카드용 한 줄) · registeredIngredients(등록성분, 보장분석치) · ingredients(전성분, 원재료 나열).

mainIngredients는 목록 카드에 한 줄로 뜨는 주성분(200자)이고, ingredients는 상세에서 접었다 펴는 전성분(4000자)입니다. 목록에 4000자를 띄울 수 없어 컬럼을 나눴습니다.

중량

weightG그램 정수입니다. 2kg이면 2000. 웹 UI는 kg/g을 골라 입력받고 g으로 환산해 보냅니다.

상세 단건 조회

제품 기본 정보와 함께 연결된 반려동물(pet) 및 최근 급여 기록 5건(recentEvents)을 반환합니다.

curl -sS "$BASE/api/products/<product-id>" -H "$AUTH"

수정

curl -sS -X PATCH "$BASE/api/products/<product-id>" \
  -H "Content-Type: application/json" \
  -H "$AUTH" \
  -d '{
    "dosage": "1일 2회로 증량",
    "isActive": true
  }'

보관 (소프트 삭제) 및 복원

# 보관 처리 (204 No Content)
curl -sS -X DELETE "$BASE/api/products/<product-id>" -H "$AUTH"

# 보관 해제 / 복원 (복원된 Product 객체 반환)
curl -sS -X POST "$BASE/api/products/<product-id>/restore" -H "$AUTH"

사진 (여러 장 + 대표 한 장)

제품당 최대 9장입니다. 사진은 1000px WebP로 자동 변환되며 업로드는 분당 30회 rate limit이 적용됩니다.

Product.photoPath대표 한 장을 가리키는 포인터입니다. 목록 카드·기록 화면은 이 한 장만 봅니다. 첫 장은 자동으로 대표가 되고, 이후에는 사용자가 고릅니다.

# 목록 (경로는 내보내지 않는다 — 바이트는 아래 :photoId로 받는다)
curl -sS "$BASE/api/products/<product-id>/photos" -H "$AUTH"
# → [{"id":"…","sortOrder":0,"isPrimary":true}, …]

# 한 장 추가 (multipart/form-data). 9장을 넘으면 400
curl -sS -X POST "$BASE/api/products/<product-id>/photos" \
  -H "$AUTH" \
  -F "file=@front.jpg"
# → {"id":"…","sortOrder":1,"isPrimary":false}

# 특정 사진 바이트
curl -sS "$BASE/api/products/<product-id>/photos/<photo-id>" -H "$AUTH" --output photo.webp

# 대표 사진 바이트 (Product.photoPath가 가리키는 한 장)
curl -sS "$BASE/api/products/<product-id>/photo" -H "$AUTH" --output primary.webp

# 대표로 지정 (갱신된 Product 객체 반환)
curl -sS -X POST "$BASE/api/products/<product-id>/photos/<photo-id>/primary" -H "$AUTH"

# 삭제 (204). 대표를 지우면 남은 것 중 정렬이 가장 앞선 사진이 대표로 올라간다
curl -sS -X DELETE "$BASE/api/products/<product-id>/photos/<photo-id>" -H "$AUTH"

GET /api/products/<id> 상세 응답에는 photos: [{id, sortOrder, isPrimary}]가 함께 담깁니다. 목록(GET /api/products) 응답에는 없습니다 — 카드는 대표 한 장만 필요합니다.

단건 업로드 (15MB 이하 사진 등)

curl -sS -X POST "$BASE/api/attachments?eventId=<event-id>" \
  -H "$AUTH" \
  -F "file=@photo.jpg"

서버에서 이미지는 1600px·JPEG q82로 변환한다. 요청 본문 상한 20MB.

청크 업로드 (영상·대용량 — drop 이식)

총 파일 상한: FILE_SIZE_LIMIT_MB 환경 변수(기본 500MB). 백업 복원 아카이브는 별도로 BACKUP_RESTORE_LIMIT_MB(기본 2047MB)를 쓰며, 전역 multipart 20MB 제한이 아니라 이 라우트별 상한이 적용됩니다. 청크 크기 8MB (@kibble/shared UPLOAD_CHUNK_SIZE_BYTES).

상한을 넘으면 첫 요청(POST /api/attachments/uploads)이 바로 413을 돌려준다 — 바이트를 보내기 전이다. 이 숫자는 원본 크기다. 큰 영상은 업로드가 끝난 뒤 서버가 백그라운드에서 720p H.264로 줄인다. 이미 작은 파일(8MB 이하, 또는 약 2Mbps 이하, 또는 720p대이면서 2.5Mbps 이하)은 그대로 둔다. 변환 중에도 원본 재생은 되고, 실패하면 원본이 남는다.

# 1) 세션 시작
curl -sS -X POST "$BASE/api/attachments/uploads" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"eventId":"<event-id>","filename":"clip.mov","mimeType":"video/quicktime","totalSize":12345678}'

# → {"uploadId":"…"}

# 2) 청크 (index 0부터 순서대로)
curl -sS -X PUT "$BASE/api/attachments/uploads/<uploadId>/chunks/0" \
  -H "$AUTH" -H "Content-Type: application/octet-stream" \
  --data-binary @chunk0.bin

# 3) 진행 확인 (선택)
curl -sS "$BASE/api/attachments/uploads/<uploadId>" -H "$AUTH"

# 4) 완료
curl -sS -X POST "$BASE/api/attachments/uploads/<uploadId>/complete" -H "$AUTH"

웹 UI는 영상 또는 15MB 초과 파일을 자동으로 청크 경로로 올린다. 미디어 조회: GET /api/attachments/file/<path> (미디어 쿠키·Bearer).

업로드 세션은 API 프로세스 메모리에 있다(셀프호스트 단일 인스턴스 전제). 따라서:

  • API를 재시작하면 진행 중이던 업로드는 무효가 되고 404가 난다 — 클라이언트는 새 세션으로 다시 시작한다
  • 청크는 순차 전송이다. 같은 세션에 동시에 두 청크를 보내면 뒤에 온 요청이 409(expectedIndex 포함)로 거절된다

관련 문서