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
+58
View File
@@ -89,6 +89,64 @@ describe('응답 계약 (e2e)', () => {
expect(body(res).data).toHaveProperty('id');
});
it('목록 응답의 pageInfo 는 공유 계약 형태를 지킨다', async () => {
const pet = {
tint: '#FF9B6A',
name: '콩이',
species: '강아지',
breed: '',
gender: 'male',
neutered: true,
birthDate: '2021-09-14',
weight: 3.2,
weightUnit: 'kg',
allergies: [],
conditions: [],
watchConditions: [],
};
for (const name of ['콩이', '두부']) {
await request(app.getHttpServer())
.post('/api/pets')
.set(auth())
.send({ ...pet, name })
.expect(201);
}
const first = await request(app.getHttpServer())
.get('/api/pets?limit=1')
.set(auth())
.expect(200);
const pageInfo = body(first).data?.pageInfo;
expect(body(first).data?.items).toHaveLength(1);
expect(Object.keys(pageInfo ?? {}).sort()).toEqual([
'hasNext',
'nextCursor',
'total',
]);
expect(pageInfo?.hasNext).toBe(true);
expect(typeof pageInfo?.nextCursor).toBe('string');
expect(pageInfo?.total).toBe(2);
const last = await request(app.getHttpServer())
.get(`/api/pets?limit=1&cursor=${pageInfo?.nextCursor ?? ''}`)
.set(auth())
.expect(200);
expect(body(last).data?.items).toHaveLength(1);
expect(body(last).data?.pageInfo?.hasNext).toBe(false);
expect(body(last).data?.pageInfo?.nextCursor).toBeNull();
});
it('정의되지 않은 쿼리 파라미터는 거부된다 (forbidNonWhitelisted)', async () => {
const res = await request(app.getHttpServer())
.get('/api/pets?정의되지않은파라미터=1')
.set(auth())
.expect(400);
expect(body(res).error?.code).toBe('VAL_001');
});
it('로그인하지 않으면 AUTH_001 과 traceId 를 반환한다', async () => {
const res = await request(app.getHttpServer())
.get('/api/users/me')