feat(backend): 반려동물 API — 등록·수정·삭제·순서·추억 보관함

앱의 우리 아이 기록(PetRepository)을 서버로 옮긴다.

- GET/POST /pets, PATCH/DELETE /pets/:id, PUT /pets/order, PUT/DELETE /pets/:id/archive
- 목록은 정한 순서(sort_order) 기준 커서 페이지네이션 — 공통 keysetPaginate 로 뽑았다
- 아이를 지우거나 보관할 때 일정·다이어리·명함이 따라 정리할 수 있게 PetLifecycleListener 를 두었다
  (반려동물 모듈은 다른 도메인을 모르고, 정리는 같은 트랜잭션에서 한다)
- 사진을 바꾸면 쓰지 않는 파일만 지운다 — 다른 기록이 참조하면 FK 가 막는다
- 목록 응답 Swagger 를 실제 { items, pageInfo } 모양으로 그리는 ApiPaginatedResponse 추가
- e2e 에 목록 pageInfo 계약·미정의 쿼리 거부 검사를 되살렸다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 17:16:38 +09:00
parent 7e0e4c36b5
commit 720b2cd858
18 changed files with 1365 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
import { isValidIsoDate } from './iso-date';
describe('isValidIsoDate', () => {
it('달력에 있는 날짜면 통과한다', () => {
expect(isValidIsoDate('2026-10-11')).toBe(true);
expect(isValidIsoDate('2024-02-29')).toBe(true);
});
it('형식만 맞고 없는 날짜는 거절한다', () => {
expect(isValidIsoDate('2026-02-30')).toBe(false);
expect(isValidIsoDate('2025-02-29')).toBe(false);
expect(isValidIsoDate('2026-13-01')).toBe(false);
});
it('시각이 붙었거나 형식이 다르면 거절한다', () => {
expect(isValidIsoDate('2026-10-11T00:00:00Z')).toBe(false);
expect(isValidIsoDate('2026/10/11')).toBe(false);
expect(isValidIsoDate('26-10-11')).toBe(false);
});
});
+20
View File
@@ -0,0 +1,20 @@
// 날짜 문자열(YYYY-MM-DD) 다루기 — 부수효과 없는 순수 함수
const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
/**
* 달력에 있는 날짜인지 — `2026-02-30` 처럼 형식만 맞는 값은 거절한다.
* 시각 없는 날짜(생일·일정 날짜)는 시간대 변환 없이 문자열 그대로 주고받는다.
*/
export function isValidIsoDate(value: string): boolean {
const match = ISO_DATE_PATTERN.exec(value);
if (!match) return false;
const [year, month, day] = match.slice(1).map(Number);
const date = new Date(Date.UTC(year, month - 1, day));
return (
date.getUTCFullYear() === year &&
date.getUTCMonth() === month - 1 &&
date.getUTCDate() === day
);
}