배경 / 문제
지금 카드 조회는 셋이다 — 날짜별(GET /api/cards?date=), 월별 캘린더(GET /api/cards/monthly?yearMonth=), 단건(GET /api/cards/{cardId}). 이 중 월별은 캘린더 표시용이라 날짜와 대표 감정만 돌려주고 카드 내용이 없다.
그래서 "이번 달에 화났던 날들을 몰아 보기" 같은 화면을 만들 수 없다. 클라이언트가 억지로 하려면 월별 조회로 날짜를 받고 그 날짜마다 날짜별 조회를 다시 부르는 N+1 호출이 된다.
감정별 삭제는 이미 있다(DELETE /api/cards/emotions/{emotion}). 감정을 축으로 카드를 다루는 화면이 있는데 그 축의 조회만 없는 상태다.
목표
한 달치 카드를 감정으로 걸러 내용까지 한 번에 받는다.
제안하는 해결책
GET /api/cards/monthly/emotions/{emotion}?yearMonth=yyyy-MM
- 기존 감정별 삭제(
DELETE /api/cards/emotions/{emotion})와 경로 축을 맞춘다
- 응답은 날짜별 조회와 같은
CardResponse 목록 — 새 DTO를 만들지 않는다
- 월 경계는 KST 자정~자정, 기존 월별 조회와 동일하게
conversation_created_at 기준
- 정렬은
conversationCreatedAt desc, id desc — 한 달치를 몰아 보는 목록이라 최근 기록이 위에 오는 편이 자연스럽다. 클라이언트가 응답을 재정렬하지 않으므로 서버가 내보내는 순서가 곧 화면 순서다. 날짜별·월별 조회는 오름차순이지만 그쪽은 하루치이거나 캘린더 칸에 꽂는 데이터라 배열 순서가 화면에 드러나지 않는다 — 이 API가 처음으로 스크롤되는 목록이라 여기서 갈라진다. 타이브레이크도 함께 뒤집는다(createdAt이 DATETIME(6)이라 동률이 가능한데 desc, id asc로 두면 동률 구간만 순서가 어긋난다)
- 페이지네이션 없음. 한 달·한 감정이면 최대 31장이라 페이징이 이득보다 복잡도가 크다
- 대상이 0장이면 빈 배열 + 200
가시성 규칙은 기존 조회와 반드시 같아야 한다 — 지운 카드(c.deletedAt is null)와 삭제된 채팅방의 카드(cv.status <> DELETED)는 제외한다. CardRepository의 조회·삭제 쿼리들이 이미 이 규칙을 공유하고 있고 주석에 "항상 함께 바뀌어야 한다"고 적혀 있다. 여기서 갈리면 캘린더에는 없는 카드가 감정 탭에서는 보이는 모순이 생긴다.
리포지토리는 findAllByMemberIdAndConversationCreatedAtInRange에 감정 조건만 더한 형태다.
✅ 완료 조건 (DoD)
기술 고려사항
- API:
GET /api/cards/monthly/emotions/{emotion}?yearMonth=yyyy-MM. @Operation description에 실패 응답 표를 기존 컨트롤러 형식대로 추가
- DB: 회원당 카드 수가 작아 인덱스 없이도 문제없다. 필요해지면
(member_id, emotion, conversation_created_at)
- 감정 6종 각각의 개수를 탭 배지에 띄우려면 집계가 따로 필요하다 — 이번 범위 밖. 필요해지면 별도 이슈로
- 통합 테스트는
TestcontainersConfig. 시각 데이터를 심을 때 raw JDBC 대신 JPQL update를 쓸 것 (Hibernate의 Instant UTC 변환과 9시간 어긋난다)
우선순위
P2 - 보통
예상 규모
S - 하루 이내
배경 / 문제
지금 카드 조회는 셋이다 — 날짜별(
GET /api/cards?date=), 월별 캘린더(GET /api/cards/monthly?yearMonth=), 단건(GET /api/cards/{cardId}). 이 중 월별은 캘린더 표시용이라 날짜와 대표 감정만 돌려주고 카드 내용이 없다.그래서 "이번 달에 화났던 날들을 몰아 보기" 같은 화면을 만들 수 없다. 클라이언트가 억지로 하려면 월별 조회로 날짜를 받고 그 날짜마다 날짜별 조회를 다시 부르는 N+1 호출이 된다.
감정별 삭제는 이미 있다(
DELETE /api/cards/emotions/{emotion}). 감정을 축으로 카드를 다루는 화면이 있는데 그 축의 조회만 없는 상태다.목표
한 달치 카드를 감정으로 걸러 내용까지 한 번에 받는다.
제안하는 해결책
GET /api/cards/monthly/emotions/{emotion}?yearMonth=yyyy-MMDELETE /api/cards/emotions/{emotion})와 경로 축을 맞춘다CardResponse목록 — 새 DTO를 만들지 않는다conversation_created_at기준conversationCreatedAt desc, id desc— 한 달치를 몰아 보는 목록이라 최근 기록이 위에 오는 편이 자연스럽다. 클라이언트가 응답을 재정렬하지 않으므로 서버가 내보내는 순서가 곧 화면 순서다. 날짜별·월별 조회는 오름차순이지만 그쪽은 하루치이거나 캘린더 칸에 꽂는 데이터라 배열 순서가 화면에 드러나지 않는다 — 이 API가 처음으로 스크롤되는 목록이라 여기서 갈라진다. 타이브레이크도 함께 뒤집는다(createdAt이DATETIME(6)이라 동률이 가능한데desc, id asc로 두면 동률 구간만 순서가 어긋난다)가시성 규칙은 기존 조회와 반드시 같아야 한다 — 지운 카드(
c.deletedAt is null)와 삭제된 채팅방의 카드(cv.status <> DELETED)는 제외한다.CardRepository의 조회·삭제 쿼리들이 이미 이 규칙을 공유하고 있고 주석에 "항상 함께 바뀌어야 한다"고 적혀 있다. 여기서 갈리면 캘린더에는 없는 카드가 감정 탭에서는 보이는 모순이 생긴다.리포지토리는
findAllByMemberIdAndConversationCreatedAtInRange에 감정 조건만 더한 형태다.✅ 완료 조건 (DoD)
INVALID_INPUT(400)기술 고려사항
GET /api/cards/monthly/emotions/{emotion}?yearMonth=yyyy-MM.@Operationdescription에 실패 응답 표를 기존 컨트롤러 형식대로 추가(member_id, emotion, conversation_created_at)TestcontainersConfig. 시각 데이터를 심을 때 raw JDBC 대신 JPQL update를 쓸 것 (Hibernate의InstantUTC 변환과 9시간 어긋난다)우선순위
P2 - 보통
예상 규모
S - 하루 이내