From 2d85f9d6d462efff86019a598956057f5b40b867 Mon Sep 17 00:00:00 2001 From: manNomi Date: Thu, 9 Jul 2026 18:32:41 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EB=A6=AC=EC=BA=A1=20=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=20=EC=9C=84=EC=B9=98=20=EA=B3=84=EC=95=BD=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 11 ++++++++++ prisma/seed.ts | 2 ++ src/data/seed-data.ts | 8 ++++++++ src/mock/mock-db.ts | 2 ++ src/services/mock-soundlog.service.ts | 4 ++++ src/services/soundlog.service.ts | 29 +++++++++++++++++---------- tests/api.test.ts | 24 ++++++++++++++++++++-- tests/mock-soundlog.service.test.ts | 6 ++++++ 8 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f645175 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/prisma/seed.ts b/prisma/seed.ts index e5d033d..ba1604f 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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, diff --git a/src/data/seed-data.ts b/src/data/seed-data.ts index 2233124..eb4b2ca 100644 --- a/src/data/seed-data.ts +++ b/src/data/seed-data.ts @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', diff --git a/src/mock/mock-db.ts b/src/mock/mock-db.ts index 4d36140..c1b84dd 100644 --- a/src/mock/mock-db.ts +++ b/src/mock/mock-db.ts @@ -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, diff --git a/src/services/mock-soundlog.service.ts b/src/services/mock-soundlog.service.ts index 6132f1e..643071d 100644 --- a/src/services/mock-soundlog.service.ts +++ b/src/services/mock-soundlog.service.ts @@ -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 ?? '음악 없음', diff --git a/src/services/soundlog.service.ts b/src/services/soundlog.service.ts index 6970454..2df69ef 100644 --- a/src/services/soundlog.service.ts +++ b/src/services/soundlog.service.ts @@ -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, @@ -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 }, }); diff --git a/tests/api.test.ts b/tests/api.test.ts index 3338b9f..14443fc 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -74,12 +74,14 @@ 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()) @@ -87,7 +89,17 @@ async function createTestMomentLog(input: { .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', @@ -659,6 +671,8 @@ describe('Soundlog API', () => { await createTestMomentLog({ authHeader, filename: 'recap-moment-1.jpg', + lat: 37.5512, + lng: 126.9882, placeName: '리캡 테스트 장소', sessionId: recapSessionId, trackId: 'seoul-night-track', @@ -666,6 +680,8 @@ describe('Soundlog API', () => { await createTestMomentLog({ authHeader, filename: 'recap-moment-2.jpg', + lat: 37.552, + lng: 126.989, placeName: '리캡 테스트 장소', sessionId: recapSessionId, trackId: 'seoul-night-track', @@ -700,6 +716,10 @@ 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[0].location).toEqual({ + lat: 37.5512, + lng: 126.9882, + }); const shareEvent = await request(app) .post(`/v1/recaps/${createdRecapId}/share-events`) diff --git a/tests/mock-soundlog.service.test.ts b/tests/mock-soundlog.service.test.ts index 0bdaaee..8c60b37 100644 --- a/tests/mock-soundlog.service.test.ts +++ b/tests/mock-soundlog.service.test.ts @@ -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: '서울숲', @@ -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( From 4507e9c3318fab792a2219411d95dcc6f10af9db Mon Sep 17 00:00:00 2001 From: manNomi Date: Thu, 9 Jul 2026 18:35:12 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=EB=A6=AC=EC=BA=A1=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EA=B2=80=EC=A6=9D=20=EC=88=9C=EC=84=9C=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/api.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/api.test.ts b/tests/api.test.ts index 14443fc..75c5fc3 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -716,10 +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[0].location).toEqual({ - lat: 37.5512, - lng: 126.9882, - }); + 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`)