Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ SoundLog React Native/Expo 앱과 연동되는 Express + TypeScript API 서버

API 구현 기준은 `openapi/soundlog-api.yaml`이며, 현재 Express와 OpenAPI에 동기화된 62개 HTTP 연산을 제공합니다.

위치와 무드 기반 추천 모델의 구현 기준은
[`SoundLogTeam/soundlog-ml`](https://github.com/SoundLogTeam/soundlog-ml)입니다.
과거 개인 API 스냅샷이 아니라 이 저장소와 공식 ML 저장소를 함께 변경합니다.

팀에서 활성 관리하는 세 저장소와 과거 이력 보존 위치는
[저장소 구조 문서](docs/repository-structure.md)에서 확인할 수 있습니다.

리캡, 여행 로그, 여행 세션, GPS 경로를 변경할 때는 [Recap / Log 서버 도메인 계약](docs/recap-log-domain-contract.md)을 먼저 확인합니다.

## Stack
Expand Down
5 changes: 5 additions & 0 deletions docs/production-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ ALLOW_DEV_AUTH_FALLBACK=false

`ML_RECOMMENDATION_API_URL`은 API 서버가 내부적으로 호출할 HTTPS 추천 엔드포인트입니다. 앱은 이 내부 주소를 직접 호출하지 않고 `https://api.soundlog.p-e.kr/v1/recommendations/playlists`만 호출합니다. 배포 후 공개 계약 검사는 이 경로가 폴백이 아닌 `ml-recommendation` 결과와 HTTPS 커버 이미지를 반환하는지 확인합니다.

추천 서비스 코드와 `/recommend` 응답 계약은
[`SoundLogTeam/soundlog-ml`](https://github.com/SoundLogTeam/soundlog-ml)을 기준으로
관리합니다. `backgroundImageUrl` 계약을 변경할 때는 ML 저장소의 `API.md`와 이
서버의 응답 변환 테스트를 같은 작업에서 확인합니다.

## 배포 중 보호 절차

워크플로는 새 compose 파일을 복사하기 전에 기존 `.env`와 `docker-compose.prod.yml`을 `.deploy-backups/<GitHub run id>`에 보관합니다. 새 API가 시작되지 않거나 내부 계약 검사가 실패하면 이전 파일로 되돌리고 기존 컨테이너를 다시 시작합니다.
Expand Down
31 changes: 31 additions & 0 deletions docs/repository-structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# SoundLog 저장소 구조

SoundLogTeam은 다음 세 저장소만 활성 개발 대상으로 관리합니다.

| 저장소 | 책임 |
| --- | --- |
| `SoundLogTeam/SoundLogApp` | React Native와 Expo 기반 모바일 앱 |
| `SoundLogTeam/SoundLogServer` | Node.js API와 데이터베이스와 배포 |
| `SoundLogTeam/soundlog-ml` | 위치와 무드 기반 음악 및 장소 이미지 추천 |

OpenAPI의 구현 기준은 이 저장소의 `openapi/soundlog-api.yaml`입니다. API 문서만
별도 저장소에서 수정하지 않습니다. 추천 서비스의 `/recommend` 계약은
`SoundLogTeam/soundlog-ml`의 `API.md`를 함께 확인합니다.

## 과거 이력 보존

기존 `SoundLogTeam/api-docs`와 `KimJaegeol1/soundlog-api`의 Git 이력은 삭제하지
않고 이 저장소의 다음 태그로 보존했습니다.

- `archive/api-docs-final-2026-08-26`
- `archive/soundlog-api-personal-main-2026-08-26`

태그의 파일을 확인하려면 새 작업 폴더에서 다음 명령을 사용합니다.

```bash
git worktree add ../soundlog-api-docs-archive archive/api-docs-final-2026-08-26
git worktree add ../soundlog-api-snapshot-archive archive/soundlog-api-personal-main-2026-08-26
```

두 태그는 과거 이력 확인용입니다. 새 브랜치를 만들거나 운영 코드를 수정할
때의 기준으로 사용하지 않습니다.
5 changes: 3 additions & 2 deletions src/services/soundlog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { getLimit, paginateByCursor } from '../utils/pagination.js';
import { createPublicId } from '../utils/tokens.js';
import { badRequest, forbidden, notFound } from '../utils/http-error.js';
import { findRegionalPlaylistId } from '../utils/regional-playlist.js';
import { createMlRecommendationArtwork } from '../utils/ml-recommendation-artwork.js';
import { reverseGeocodeLocation } from './reverse-geocoding.service.js';
import {
assertUserTextAllowed,
Expand Down Expand Up @@ -98,6 +99,7 @@ type MlMood = '잔잔한' | '신나는' | '시원한' | '설레는' | '감성적
const RECAP_DISCOVERY_RADIUS_METERS = 300;

type MlRecommendationResponse = {
backgroundImageUrl?: unknown;
tracks?: unknown;
};

Expand Down Expand Up @@ -1013,8 +1015,7 @@ async function fetchMlRecommendationPlaylist(
regionName: state,
placeName: input.placeId,
reason: `${state} 중인 지금, ${mood} 무드에 맞춰 추천했어요`,
coverImageUrl: undefined,
backgroundImageUrl: undefined,
...createMlRecommendationArtwork(data?.backgroundImageUrl),
trackCount: tracks.length,
durationText: `${tracks.length * 4}:00분`,
context: {
Expand Down
21 changes: 21 additions & 0 deletions src/utils/ml-recommendation-artwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type MlRecommendationArtwork = {
backgroundImageUrl?: string;
coverImageUrl?: string;
};

export function createMlRecommendationArtwork(value: unknown): MlRecommendationArtwork {
if (typeof value !== 'string') {
return {};
}

const imageUrl = value.trim();

if (!imageUrl) {
return {};
}

return {
backgroundImageUrl: imageUrl,
coverImageUrl: imageUrl,
};
}
23 changes: 23 additions & 0 deletions tests/ml-recommendation-artwork.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';

import { createMlRecommendationArtwork } from '../src/utils/ml-recommendation-artwork.js';

describe('createMlRecommendationArtwork', () => {
it('uses the ML background image for both playlist artwork fields', () => {
expect(
createMlRecommendationArtwork(
' http://tong.visitkorea.or.kr/cms/resource/photo.jpg ',
),
).toEqual({
backgroundImageUrl: 'http://tong.visitkorea.or.kr/cms/resource/photo.jpg',
coverImageUrl: 'http://tong.visitkorea.or.kr/cms/resource/photo.jpg',
});
});

it.each([null, undefined, '', ' ', 123, {}])(
'omits artwork for an unusable ML value: %p',
(value) => {
expect(createMlRecommendationArtwork(value)).toEqual({});
},
);
});
Loading