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
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Soundlog Server Agent Rules

- Do not merge GitHub pull requests unless the user's latest message explicitly asks to merge that exact PR.
- PR creation, CI verification, reviewer assignment, issue closing, or "finish the task" does not imply merge approval.
- If a merge request is ambiguous, ask for confirmation before using any merge UI, GitHub API, or commands such as `gh pr merge`.

## Product Platform Direction

- Soundlog is a mobile app product. We do not intend to ship or maintain a public web deployment as a product surface.
- Server API contracts should prioritize the iOS/Android app experience. Do not add web-specific API behavior unless the user explicitly asks for web support.
- Web export or browser checks from the app repository are compatibility checks, not product deployment requirements.
2 changes: 2 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ export async function seedDatabase() {
userId: user.id,
photoUrl: log.photoUrl,
createdAt: new Date(log.createdAt),
lat: log.lat,
lng: log.lng,
sessionId: log.sessionId,
placeName: log.placeName,
note: log.note,
Expand Down
8 changes: 8 additions & 0 deletions src/data/seed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export const seedMomentLogs = [
id: 'log-1',
photoUrl: 'https://tong.visitkorea.or.kr/cms/resource_photo/96/4033396_image2_1.jpg',
createdAt: '2026-05-25T00:00:00.000Z',
lat: 35.1532,
lng: 129.1187,
placeName: '광안리',
note: '바다 앞에서 처음 저장한 사운드',
trackId: 'seoul-city',
Expand All @@ -221,6 +223,8 @@ export const seedMomentLogs = [
id: 'log-2',
photoUrl: 'https://tong.visitkorea.or.kr/cms2/website/76/2012176.jpg',
createdAt: '2026-05-25T00:10:00.000Z',
lat: 37.5294,
lng: 126.9348,
placeName: '한강',
note: '강변 산책에 어울린 잔잔한 곡',
trackId: 'night-letter',
Expand All @@ -231,6 +235,8 @@ export const seedMomentLogs = [
id: 'log-3',
photoUrl: 'https://tong.visitkorea.or.kr/cms2/website/82/1870082.jpg',
createdAt: '2026-05-25T00:20:00.000Z',
lat: 35.1796,
lng: 129.0756,
placeName: '부산',
note: '여행 에너지가 올라간 순간',
trackId: 'seoul-night-track',
Expand All @@ -255,6 +261,7 @@ export const recaps = [
{
id: 'log-1',
imageUrl: 'https://tong.visitkorea.or.kr/cms/resource_photo/96/4033396_image2_1.jpg',
location: { lat: 35.1532, lng: 129.1187 },
placeName: '광안리',
recordedAt: '2026-05-25T00:00:00.000Z',
trackTitle: 'Seoul City',
Expand All @@ -277,6 +284,7 @@ export const recaps = [
{
id: 'log-1',
imageUrl: 'https://tong.visitkorea.or.kr/cms/resource_photo/96/4033396_image2_1.jpg',
location: { lat: 35.1532, lng: 129.1187 },
placeName: '광안리',
recordedAt: '2026-05-25T00:00:00.000Z',
trackTitle: 'Seoul City',
Expand Down
2 changes: 2 additions & 0 deletions src/mock/mock-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ function createMockDb() {
id: log.id,
photoUrl: log.photoUrl,
createdAt: new Date(log.createdAt),
lat: log.lat,
lng: log.lng,
sessionId: log.sessionId,
placeName: log.placeName,
note: log.note,
Expand Down
4 changes: 4 additions & 0 deletions src/services/mock-soundlog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,10 @@ export const mockSoundlogService = {
moments: moments.map((moment) => ({
id: moment.id,
imageUrl: moment.photoUrl,
location:
moment.lat !== undefined && moment.lng !== undefined
? { lat: moment.lat, lng: moment.lng }
: undefined,
placeName: moment.placeName ?? '위치 없음',
trackTitle: moment.trackSnapshot?.title ?? '저장된 순간',
artistName: moment.trackSnapshot?.artist ?? '음악 없음',
Expand Down
29 changes: 18 additions & 11 deletions src/services/soundlog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,23 @@ function recapShareToDto(recap: Recap & { representativeTrack: Track }) {
});
}

function momentLogToRecapShareMoment(moment: MomentLog) {
const momentTrack = (moment.trackSnapshot as TrackDto | null) ?? undefined;

return compact({
id: moment.id,
imageUrl: moment.photoUrl,
location:
moment.lat !== null && moment.lng !== null
? { lat: moment.lat, lng: moment.lng }
: undefined,
placeName: moment.placeName ?? '위치 없음',
trackTitle: momentTrack?.title ?? '저장된 순간',
artistName: momentTrack?.artist ?? '음악 없음',
recordedAt: moment.createdAt.toISOString(),
});
}

function travelSessionToDto(session: TravelSession) {
return compact({
id: session.id,
Expand Down Expand Up @@ -2780,17 +2797,7 @@ export const soundlogService = {
backgroundImageUrl: firstMoment?.photoUrl,
discImageUrl: firstMoment?.photoUrl,
recordedAt: firstMoment?.createdAt ?? new Date(),
moments: moments.map((moment) => {
const momentTrack = (moment.trackSnapshot as TrackDto | null) ?? undefined;
return {
id: moment.id,
imageUrl: moment.photoUrl,
placeName: moment.placeName ?? '위치 없음',
trackTitle: momentTrack?.title ?? '저장된 순간',
artistName: momentTrack?.artist ?? '음악 없음',
recordedAt: moment.createdAt.toISOString(),
};
}) as Prisma.JsonArray,
moments: moments.map(momentLogToRecapShareMoment) as Prisma.JsonArray,
},
include: { representativeTrack: true },
});
Expand Down
26 changes: 24 additions & 2 deletions tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,32 @@ function findSecretLeaks(value: unknown, secret: string, path = '$'): string[] {
async function createTestMomentLog(input: {
authHeader: string;
filename: string;
lat?: number;
lng?: number;
note?: string;
placeName: string;
sessionId?: string;
trackId?: string;
}) {
const response = await request(app)
const requestBuilder = request(app)
.post('/v1/moment-logs')
.set('Authorization', input.authHeader)
.field('createdAt', new Date().toISOString())
.field('moodTags', 'fresh,calm')
.field('note', input.note ?? '')
.field('placeName', input.placeName)
.field('sessionId', input.sessionId ?? '')
.field('trackId', input.trackId ?? 'seoul-city')
.field('trackId', input.trackId ?? 'seoul-city');

if (input.lat !== undefined) {
requestBuilder.field('lat', String(input.lat));
}

if (input.lng !== undefined) {
requestBuilder.field('lng', String(input.lng));
}

const response = await requestBuilder
.attach('photo', Buffer.from('fake-image'), {
filename: input.filename,
contentType: 'image/jpeg',
Expand Down Expand Up @@ -659,13 +671,17 @@ describe('Soundlog API', () => {
await createTestMomentLog({
authHeader,
filename: 'recap-moment-1.jpg',
lat: 37.5512,
lng: 126.9882,
placeName: '리캡 테스트 장소',
sessionId: recapSessionId,
trackId: 'seoul-night-track',
});
await createTestMomentLog({
authHeader,
filename: 'recap-moment-2.jpg',
lat: 37.552,
lng: 126.989,
placeName: '리캡 테스트 장소',
sessionId: recapSessionId,
trackId: 'seoul-night-track',
Expand Down Expand Up @@ -700,6 +716,12 @@ describe('Soundlog API', () => {
expect(share.body.data.id).toBe(createdRecapId);
expect(share.body.data.trackTitle).toBe(created.body.data.representativeTrack.title);
expect(share.body.data.moments.length).toBeGreaterThan(1);
expect(share.body.data.moments.map((moment: { location?: unknown }) => moment.location)).toEqual(
expect.arrayContaining([
{ lat: 37.5512, lng: 126.9882 },
{ lat: 37.552, lng: 126.989 },
]),
);

const shareEvent = await request(app)
.post(`/v1/recaps/${createdRecapId}/share-events`)
Expand Down
6 changes: 6 additions & 0 deletions tests/mock-soundlog.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ describe('mockSoundlogService', () => {
it('creates recaps, share payloads, share events, and validates representative tracks', async () => {
const moment = await mockSoundlogService.createMomentLog(ownerId, {
createdAt: '2026-07-09T10:00:00.000Z',
lat: 37.5444,
lng: 127.0374,
moodTags: ['감성적인'],
photoPath: '/uploads/recap.jpg',
placeName: '서울숲',
Expand Down Expand Up @@ -278,6 +280,10 @@ describe('mockSoundlogService', () => {
expect(recap.id).toBe(repeated.id);
expect(recap.title).toBe('서울숲 사운드');
expect(list.data[0].id).toBe(recapId);
expect((share.moments as Array<{ location?: { lat: number; lng: number } }>)[0].location).toEqual({
lat: 37.5444,
lng: 127.0374,
});
expect(share.moments).toHaveLength(1);
expect(mockDb.recapShareEvents).toHaveLength(1);
await expect(
Expand Down
Loading