diff --git a/docs/product/SOUNDLOG_TRAVEL_SOUNDTRACK_LOG_SPEC.md b/docs/product/SOUNDLOG_TRAVEL_SOUNDTRACK_LOG_SPEC.md
index 18f47b3..d42e8b7 100644
--- a/docs/product/SOUNDLOG_TRAVEL_SOUNDTRACK_LOG_SPEC.md
+++ b/docs/product/SOUNDLOG_TRAVEL_SOUNDTRACK_LOG_SPEC.md
@@ -229,7 +229,7 @@ Recap은 Soundlog의 최종 보상이므로 기능적으로만 맞는 화면이
| 와이어프레임 | 기능 및 화면 명세 |
| --- | --- |
-|
┌────────────────────┐
│ 현재 위치 │
│ 성수동 카페거리 │
│ [장소 바꾸기] │
│ │
│ 상태 [카페 v] │
│ 무드 [잔잔한 v] │
│ │
│ 오늘의 사운드트랙 │
│ ┌────────────────┐ │
│ │ 커버 / 추천 이유│ │
│ │ 12곡 · 카페 무드│ │
│ └────────────────┘ │
│ │
│ [곡 보기] [기록] │
└────────────────────┘
| **사용자 목표:** 지금 장소에 어울리는 사운드트랙을 바로 확인한다.
**핵심 CTA:** 곡 보기, 순간 기록.
**API:** `GET /v1/recommendations/playlists?x=&y=&state=&mood=` 또는 수동 장소 기반 요청.
**상태:** 위치 조회 중, 권한 거부, 수동 장소, 추천 로딩, 추천 실패, 추천 없음.
**오프라인:** 마지막 추천을 표시하고 "최근 추천" 배지를 붙인다.
**피드백:** 상태/무드 변경은 즉시 추천 재요청 또는 적용 버튼 방식 중 하나로 통일한다. MVP는 적용 버튼보다 즉시 재요청이 더 간단하다. |
+| ┌────────────────────┐
│ 현재 위치 │
│ 성수동 카페거리 │
│ [장소 바꾸기] │
│ │
│ 상태 [카페 v] │
│ 무드 [잔잔한 v] │
│ │
│ 오늘의 사운드트랙 │
│ ┌────────────────┐ │
│ │ 커버 / 추천 이유│ │
│ │ 12곡 · 카페 무드│ │
│ └────────────────┘ │
│ │
│ [곡 보기] [기록] │
└────────────────────┘
| **사용자 목표:** 지금 장소에 어울리는 사운드트랙을 바로 확인한다.
**핵심 CTA:** 곡 보기, 순간 기록.
**API:** `POST /v1/playlists/contextual`에 `location`, `travelMode`, `moodTags` 또는 수동 장소 정보를 보낸다.
**상태:** 위치 조회 중, 권한 거부, 수동 장소, 추천 로딩, 추천 실패, 추천 없음.
**오프라인:** 마지막 추천을 표시하고 "최근 추천" 배지를 붙인다.
**피드백:** 상태/무드 변경은 즉시 추천 재요청 또는 적용 버튼 방식 중 하나로 통일한다. MVP는 적용 버튼보다 즉시 재요청이 더 간단하다. |
### P-04. 추천 사운드트랙 상세
@@ -303,16 +303,45 @@ Recap은 Soundlog의 최종 보상이므로 기능적으로만 맞는 화면이
### 9.1 추천 요청
-```txt
-GET /v1/recommendations/playlists?x=126.9963&y=37.5104&state=산책&mood=잔잔한
+```http
+POST /v1/playlists/contextual
+Content-Type: application/json
+
+{
+ "location": {
+ "lat": 37.5104,
+ "lng": 126.9963
+ },
+ "travelMode": "walk",
+ "moodTags": ["calm"],
+ "preferredGenres": ["indie", "jazz"],
+ "excludeTrackIds": []
+}
```
| 필드 | 설명 |
| --- | --- |
-| `x` | 경도 |
-| `y` | 위도 |
-| `state` | 여행 상태: 바다, 드라이브, 산책, 카페, 야경 |
-| `mood` | 무드: 잔잔한, 신나는, 시원한, 설레는, 감성적인 |
+| `location.lat` | 위도 |
+| `location.lng` | 경도 |
+| `travelMode` | 여행 상태 API 값: `ocean`, `drive`, `walk`, `cafe`, `night` |
+| `moodTags` | 무드 API 값: `calm`, `active`, `fresh`, `emotional`, `local` |
+| `preferredGenres` | 사용자가 선호하는 장르 힌트 |
+| `excludeTrackIds` | 다음 추천에서 제외할 곡 ID 목록 |
+
+UI 라벨과 API 값은 다음처럼 매핑한다.
+
+| UI 라벨 | API 값 |
+| --- | --- |
+| 바다 | `ocean` |
+| 드라이브 | `drive` |
+| 산책 | `walk` |
+| 카페 | `cafe` |
+| 야경 | `night` |
+| 잔잔한 | `calm` |
+| 신나는 | `active` |
+| 시원한 | `fresh` |
+| 설레는 | `local` |
+| 감성적인 | `emotional` |
### 9.2 추천 응답
@@ -324,8 +353,8 @@ type PlaylistRecommendation = {
place: {
name: string;
address?: string;
- x?: number;
- y?: number;
+ lat?: number;
+ lng?: number;
imageUrl?: string;
};
state: '바다' | '드라이브' | '산책' | '카페' | '야경';
@@ -339,11 +368,10 @@ type TrackRecommendation = {
artist: string;
albumImageUrl?: string;
reasonTags: string[];
- externalLinks: {
- spotify?: string;
+ externalUrl?: string;
+ platformUrls?: {
+ melon?: string;
youtubeMusic?: string;
- youtube?: string;
- search?: string;
};
};
```
@@ -380,14 +408,19 @@ type RecapCreateRequest = {
### 9.5 공동 여행 / 지도
```txt
+POST /v1/travel-sessions
+PATCH /v1/travel-sessions/:sessionId
POST /v1/travel-rooms
+GET /v1/travel-rooms/:roomId
POST /v1/travel-rooms/:roomId/join
POST /v1/travel-rooms/:roomId/moments
+PATCH /v1/travel-rooms/:roomId/moments/:momentId
+POST /v1/travel-rooms/:roomId/moments/:momentId/comments
POST /v1/travel-rooms/:roomId/recaps
-GET /v1/sound-map?x=&y=&radius=&visibility=
+GET /v1/sound-map?lat=&lng=&radiusMeters=&visibility=
POST /v1/sound-map/current-track
-GET /v1/sound-map/nearby?x=&y=&radius=&state=&mood=
-GET /v1/music-matches?x=&y=&radius=&state=&mood=
+GET /v1/sound-map/nearby?lat=&lng=&radiusMeters=&state=&mood=
+GET /v1/music-matches?lat=&lng=&radiusMeters=&state=&mood=
POST /v1/travel-mate-requests
PATCH /v1/travel-mate-requests/:requestId
POST /v1/community/blocks
@@ -396,11 +429,17 @@ POST /v1/community/reports
| API | 목적 |
| --- | --- |
+| `POST /v1/travel-sessions` | 서버 기준 여행 모드를 시작한다. Live Sound Map 공개의 전제 조건이다. |
+| `PATCH /v1/travel-sessions/:sessionId` | 여행 모드를 종료하거나 상태를 변경한다. |
| `POST /v1/travel-rooms` | 같이 여행 간 사람끼리 쓸 여행방을 만든다. |
+| `GET /v1/travel-rooms/:roomId` | 공동 여행방의 멤버, 후보 순간, 댓글, Recap 상태를 조회한다. |
+| `POST /v1/travel-rooms/:roomId/join` | 초대 코드로 공동 여행방에 참여한다. 이미 참여한 사용자는 방 정보를 다시 받는다. |
| `POST /v1/travel-rooms/:roomId/moments` | 동행자가 공동 Recap 후보 순간을 추가한다. |
-| `POST /v1/travel-rooms/:roomId/recaps` | 방장이 공동 Recap을 생성한다. |
+| `PATCH /v1/travel-rooms/:roomId/moments/:momentId` | 방장이 후보 순간을 `candidate`, `accepted`, `rejected` 상태로 변경한다. |
+| `POST /v1/travel-rooms/:roomId/moments/:momentId/comments` | 방장/참여자가 후보 순간에 댓글을 남긴다. |
+| `POST /v1/travel-rooms/:roomId/recaps` | 방장이 공동 Recap을 생성한다. 승인된 후보가 있으면 승인된 순간을 우선 사용한다. |
| `GET /v1/sound-map` | 여행 모드 ON 사용자의 현재 위치와 선택 곡 핀을 조회한다. |
-| `POST /v1/sound-map/current-track` | 내 현재 위치와 Soundlog 현재 선택 곡을 지도에 표시한다. |
+| `POST /v1/sound-map/current-track` | active 여행 세션이 있는 사용자의 현재 위치와 Soundlog 현재 선택 곡을 지도에 표시한다. |
| `GET /v1/sound-map/nearby` | 주변 익명 공개 음악 핀을 조회한다. 정확 좌표 대신 대략 위치만 응답한다. |
| `GET /v1/music-matches` | 주변 여행자와의 음악 취향 일치율과 공개 프로필 카드를 조회한다. |
| `POST /v1/travel-mate-requests` | 취향이 맞는 사용자에게 동행 매칭 요청을 보낸다. |
@@ -408,6 +447,17 @@ POST /v1/community/reports
| `POST /v1/community/blocks` | 사용자 차단을 기록하고 이후 지도/매칭에서 숨긴다. |
| `POST /v1/community/reports` | 부적절한 공개 핀, 프로필, 매칭 요청을 신고한다. |
+공동 여행/지도 API의 서버 정책은 다음과 같다.
+
+| 정책 | 서버 동작 |
+| --- | --- |
+| 방 접근 권한 | 공동 여행방 조회, 후보 추가, 댓글 작성은 방장 또는 참여자만 가능하다. |
+| 초대 코드 | 신규 참여자는 `POST /v1/travel-rooms/:roomId/join` 요청에 유효한 초대 코드를 보내야 한다. |
+| 공동 Recap 권한 | 후보 상태 변경과 공동 Recap 생성은 방장만 가능하다. |
+| Live Sound Map 공개 | `POST /v1/sound-map/current-track`은 서버에 active 여행 세션이 있는 사용자만 성공한다. |
+| 위치 없는 조회 | `GET /v1/sound-map/nearby`, `GET /v1/music-matches`는 좌표가 없으면 낯선 사용자 데이터를 노출하지 않고 빈 목록을 반환한다. |
+| 동행 요청 상태 | `accept` 액션은 `accepted`, `decline` 액션은 `declined`, `cancel` 액션은 `cancelled`, `expire` 액션은 `expired`로 변경한다. |
+
---
## 10. 추천 학습 이벤트