Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/ai/erd.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ AI_RESPONSE metadata는 `requestMessageId`, `intent`, `placeRecommendation`, `co

> **현재 구현 범위:** 장소 추가/조회/삭제, 시간 설정, 메모 수정, D&D 순서 변경, 이동 정보 조회를 제공합니다. 항목 삭제·순서 변경 시 남은 `order_index`는 0부터 연속되도록 재정렬합니다.
>
> **이동 정보 흐름:** `distance_meters`, `duration_seconds`는 Google Maps Platform 정책상 DB에 영구 저장할 수 없습니다. 서버가 Google Routes API(Compute Routes)를 프록시하여 결과를 직접 클라이언트에 반환하며, Redis에 10분 TTL로 임시 캐시합니다(`route::{origin}:{dest}:{mode}`). 동시 cache miss는 Redis single-flight lock으로 대표 요청 1개만 Google Routes API를 호출하고, 나머지 요청은 route 캐시 또는 짧은 no-route 신호를 기다립니다. 이동 수단은 서버 DB에 저장하지 않고 클라이언트가 로컬에서 관리하며, 경로 조회 시 `travelMode` 요청 파라미터로 전달합니다. 마지막 장소는 다음 장소가 없으므로 이동 정보를 반환하지 않습니다(204). Google Routes가 경로를 찾지 못한 구간도 이동 정보를 반환하지 않습니다(204).
> **이동 정보 흐름:** `distance_meters`, `duration_seconds`는 Google Maps Platform 정책상 DB에 영구 저장할 수 없습니다. 서버가 Google Routes API(Compute Routes)를 프록시하여 결과를 직접 클라이언트에 반환하며, Redis에 10분 TTL로 임시 캐시합니다(`route::{origin}:{dest}:{mode}`). 동시 cache miss는 Redis single-flight lock으로 대표 요청 1개만 Google Routes API를 호출하고, 나머지 요청은 route 캐시 또는 짧은 no-route 신호를 기다립니다. 이동 수단은 서버 DB에 저장하지 않고 클라이언트가 로컬에서 관리합니다. 단건 경로 조회는 `travelMode` 요청 파라미터로, 벌크 경로 조회는 요청 body의 항목별 `travelMode`로 이동 수단을 전달합니다. 마지막 장소는 다음 장소가 없으므로 단건 조회에서는 이동 정보를 반환하지 않습니다(204, 응답 헤더 `X-Route-Status: LAST_PLACE`). Google Routes가 경로를 찾지 못한 구간도 단건 조회에서는 이동 정보를 반환하지 않습니다(204, 응답 헤더 `X-Route-Status: NO_ROUTE`). 벌크 조회는 요청 자체가 유효하면 200을 반환하고, 마지막 항목·경로 없음·일시 실패 같은 구간별 결과는 각 route의 `status`/`errorCode`로 반환합니다.

---

Expand Down Expand Up @@ -221,7 +221,7 @@ AI_RESPONSE metadata는 `requestMessageId`, `intent`, `placeRecommendation`, `co
| `room:{roomId}:sessions:{userId}` | 접속 유저별 WebSocket/STOMP 세션 목록 (ephemeral) | 세션 종료 시 제거 | 같은 유저의 다중 탭/다중 세션 접속 상태 정합성 유지 |
| `place:preview::{googlePlaceId}` | 장소 카드 미리보기 본문 캐시 | 24시간 (1일) | 장소명, 주소, 장소 유형(`primaryType`), 장소 유형 표시명(`primaryTypeDisplayName`), 좌표를 저장. 대표 `photoName`은 저장하지 않는다. 캐시 miss 응답은 원 Google 응답의 `photos.name`을 사용하고, 캐시 hit 응답은 `photos.name` 전용 Google Place Details 요청 결과를 합친다 |
| `place:detail::{googlePlaceId}` | 장소 상세 조회 본문 캐시 | 6시간 | 사진 목록(`photoNames`)은 저장하지 않는다. 캐시 miss 응답은 원 Google 응답의 `photos.name`을 사용하고, 캐시 hit 응답은 `photos.name` 전용 Google Place Details 요청 결과를 합친다 |
| `place:photo-uri::{photoName}` | Google Place Photo Media `photoUri` 캐시 | 10분 | 짧은 수명 URL이므로 장기 저장하지 않는다 |
| `place:photo-uri::{photoName}:w400:h400` | Google Place Photo Media `photoUri` 캐시 | 24시간 (1일) | `photoName`과 기본 크기(400x400) 기준으로 저장한다. Google `photoUri`는 짧은 수명 URL이지만 운영 검증 전까지 24시간 TTL을 적용한다 |
| `route::{origin}:{dest}:{travelMode}` | Routes API 이동 정보 캐시 | 10분 | Google Maps Platform 정책상 영구 저장 불가, 임시 캐시만 허용 |
| `route:lock:{origin}:{dest}:{travelMode}` | Routes API single-flight lock | 5초 | Redis `SET NX EX` 기반 lock. 동시 cache miss 시 Google Routes API 대표 호출자 1개만 선출 |
| `route:no-route:{origin}:{dest}:{travelMode}` | Routes API 경로 없음 신호 | 5초 | 대표 호출자가 경로 없음(204)을 확인했음을 대기 요청에 전달하는 짧은 조정 신호 |
Expand All @@ -243,9 +243,9 @@ AI_RESPONSE metadata는 `requestMessageId`, `intent`, `placeRecommendation`, `co
2. **google_place_id 직접 참조:** places 중간 테이블 없이 bookmarks·schedule_items에서 google_place_id(VARCHAR)를 직접 저장한다. 단순 검색 결과를 DB에 eager insert할 필요가 없고, 북마크/일정 추가 시점에만 장소 식별자가 기록된다. 각 테이블의 google_place_id 컬럼에 인덱스를 부여해 조회 성능을 확보한다.
3. **MongoDB message `_id` cursor:** 저장된 읽음 위치 복귀와 히스토리 조회는 MongoDB `_id` 기반 `afterId`/`beforeId` cursor를 사용한다. 실시간 브로드캐스트 누락 복구는 방별 `sequence` 기반 `afterSequence`를 사용한다.
4. **room.id UUID:** 초대 URL에 노출되므로 추측 불가능한 UUID 사용.
5. **장소 검색/미리보기/상세 캐시:** 자유도가 높은 검색어는 캐시 히트율이 낮을 수 있으므로 검색 결과는 캐시하지 않는다. 검색 API는 Google Text Search(New)의 `pageSize`·`pageToken`·`nextPageToken` 기반 페이지네이션만 프록시한다. Google Place 미리보기 응답은 북마크/일정 카드 표시를 위해 `google_place_id` 기준으로 Redis에 24시간(1일) TTL로 저장하고, 상세 조회 응답은 Redis에 6시간 TTL로 저장한다.
5. **장소 검색/미리보기/상세/사진 캐시:** 자유도가 높은 검색어는 캐시 히트율이 낮을 수 있으므로 검색 결과는 캐시하지 않는다. 검색 API는 Google Text Search(New)의 `pageSize`·`pageToken`·`nextPageToken` 기반 페이지네이션만 프록시한다. Google Place 미리보기 응답은 북마크/일정 카드 표시를 위해 `google_place_id` 기준으로 Redis에 24시간(1일) TTL로 저장하고, 상세 조회 응답은 Redis에 6시간 TTL로 저장한다. Google Photo Media `photoUri`는 `photoName`과 기본 크기(400x400) 기준으로 Redis에 24시간(1일) TTL로 저장한다.
6. **schedule_items.order_index:** D&D UI를 위한 정렬 인덱스. 재정렬 시 해당 컬럼만 업데이트.
7. **이동 정보 프록시:** 이동 수단 선호는 사용자별 클라이언트 로컬 상태로 관리하고 DB에 저장하지 않는다. Google Maps Platform 정책상 `distance_meters`·`duration_seconds`는 DB에 영구 저장 불가 — 서버가 Routes API를 프록시하여 결과를 클라이언트에 직접 반환하고, Redis 10분 TTL로 임시 캐시. 동시 cache miss는 Redis single-flight lock으로 중복 Google API 호출을 줄인다.
7. **이동 정보 프록시:** 이동 수단 선호는 사용자별 클라이언트 로컬 상태로 관리하고 DB에 저장하지 않는다. 벌크 조회에서도 요청 항목별 `travelMode`를 받아 계산에만 사용한다. Google Maps Platform 정책상 `distance_meters`·`duration_seconds`는 DB에 영구 저장 불가 — 서버가 Routes API를 프록시하여 결과를 클라이언트에 직접 반환하고, Redis 10분 TTL로 임시 캐시. 동시 cache miss는 Redis single-flight lock으로 중복 Google API 호출을 줄인다.
8. **방 Hard Delete:** 모든 하위 엔티티 FK에 `@OnDelete(CASCADE)` (DB `ON DELETE CASCADE`)를 적용하여, `roomRepository.delete(room)` 한 줄로 Room과 하위 데이터를 삭제한다. 단방향 관계를 유지하면서 DB가 cascade 삭제를 처리한다.

---
Expand Down
7 changes: 5 additions & 2 deletions docs/ai/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ Caddy는 `caddy-ratelimit` 기반 IP 제한으로 엣지에서 넓고 거친 폭
|------|------|
| `POST /auth/refresh` | `10/min/refresh_token_hash` |
| Places 검색 `GET /places/search` | `10/min/user` |
| Places 상세/미리보기/사진 GET | `30/min/user` |
| Places 상세/미리보기/사진 이름 단건 조회 (`places-read-single`) | `30/min/user` |
| Places 미리보기/사진 이름 벌크 조회 (`places-read-bulk`) | `10/min/user` |
| Places 사진 URL 단건 조회 (`places-photo-single`) | `45/min/user` |
| Places 사진 URL 벌크 조회 (`places-photo-bulk`) | `15/min/user` |
| Routes 조회 `GET /rooms/{roomId}/schedules/{scheduleId}/items/{itemId}/route` | `20/min/user` + `60/min/room` |
| `POST /rooms/join` | `10/min/user` |
| 인증된 상태 변경 HTTP API | `60/min/user` |
Expand Down Expand Up @@ -133,7 +136,7 @@ Caddy는 `caddy-ratelimit` 기반 IP 제한으로 엣지에서 넓고 거친 폭
|------|------|------|----------|
| `[x]` | 일정 생성 | `dayNumber`를 생략하면 마지막 일차 뒤에 추가된다. `dayNumber`를 지정하면 해당 위치에 빈 일정을 삽입하고 기존 `dayNumber >= 지정값` 일정은 +1 shift 된다. 생성과 동시에 방의 `endDate`가 1일 늘어나고 `ROOM_SCHEDULES_RESYNCED` 이벤트가 발행된다(상한 30일, 초과 시 `SCHEDULE_LIMIT_EXCEEDED`, 허용 범위 밖 일차는 `INVALID_DAY_NUMBER`로 거부). 날짜는 저장하지 않고 `rooms.start_date + day_number - 1`로 응답마다 계산한다 | schedules, rooms |
| `[x]` | 일정 배치 생성 | 동일한 끝뒤 추가 정책으로 `commands.size()` 만큼 일괄 추가되며 방의 `endDate`가 N일 늘어나고 `ROOM_SCHEDULES_RESYNCED` 이벤트가 1회 발행된다(합계가 30을 초과하면 부분 생성 없이 전체 거부) | schedules, rooms |
| `[x]` | 일정 목록 조회 | 방의 전체 일자별 일정 조회 | schedules |
| `[x]` | 일정 목록 조회 | 방의 전체 일자별 일정 조회. `includeItems=true`이면 각 일정의 장소 항목 목록을 함께 반환해 초기 화면 조회 시 일정 항목 N+1 조회를 피한다 | schedules, schedule_items |
| `[x]` | 일정 삭제 | 특정 일자 삭제. 뒤쪽 일차들이 앞으로 -1 shift 되어 `dayNumber`가 1..N으로 연속을 유지하고, 방의 `endDate`가 1일 줄어들며 `ROOM_SCHEDULES_RESYNCED` 이벤트가 발행된다. 단, 일정이 1개만 남은 상태에서 삭제 요청이 오면 일정 row와 `endDate`는 유지하고 하위 ScheduleItem만 비운 뒤에도 같은 이벤트를 발행한다 | schedules, rooms |
| `[x]` | 일정 이동 | `PATCH /rooms/{roomId}/schedules/{scheduleId}/move`로 같은 방 안의 일정을 목표 `targetDayNumber` 위치로 재배치한다. swap이 아니라 source와 target 사이 구간을 한 칸 shift 하는 reorder이며, 허용 범위는 `1..현재 일정 개수`다. ScheduleItem은 부모 schedule row에 붙어 그대로 따라가고 Room 기간은 바뀌지 않는다. 성공 시 `ROOM_SCHEDULES_RESYNCED` 이벤트가 발행되고, source와 target 일차가 같으면 204 no-op으로 이벤트를 발행하지 않는다 | schedules |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,4 +575,3 @@ If Steps 1-4 required no file edits, do not commit. If a typo fix or test correc
git add <changed-files>
git commit -m "fix: Schedule RESYNC 이벤트 검증 보완"
```

Loading