From 7f3d5c56a59f664970480829bb5f7c0ec45263ce Mon Sep 17 00:00:00 2001 From: rjsgud49 Date: Thu, 22 Oct 2026 00:54:34 +0000 Subject: [PATCH] =?UTF-8?q?fix(catches):=20=EA=B3=B5=EA=B0=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EC=97=90=EC=84=9C=20=EB=8C=80=ED=9A=8C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=99=80=20=EC=82=AC=EC=A7=84=20=ED=95=B4=EC=8B=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: park gun hyung --- .../api/src/catches/catches.service.spec.ts | 25 +++++++++ .../apps/api/src/catches/catches.service.ts | 16 ++++-- .../apps/api/src/catches/public-catch.spec.ts | 26 ++++++++++ fishing/apps/api/src/catches/public-catch.ts | 20 ++++++++ .../src/users/public-profile-catch.spec.ts | 51 +++++++++++++++++++ fishing/apps/api/src/users/users.service.ts | 19 +++++-- 6 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 fishing/apps/api/src/catches/public-catch.spec.ts create mode 100644 fishing/apps/api/src/catches/public-catch.ts create mode 100644 fishing/apps/api/src/users/public-profile-catch.spec.ts diff --git a/fishing/apps/api/src/catches/catches.service.spec.ts b/fishing/apps/api/src/catches/catches.service.spec.ts index 0c7ebfb..3eb7be2 100644 --- a/fishing/apps/api/src/catches/catches.service.spec.ts +++ b/fishing/apps/api/src/catches/catches.service.spec.ts @@ -132,6 +132,31 @@ describe('CatchesService 이미지 해시', () => { expect(prisma.catch.create).not.toHaveBeenCalled(); }); + it('공개 단건 조회는 대회 코드와 해시를 빼 준다', async () => { + prisma.catch.findUnique.mockResolvedValue({ + id: 'catch-1', + claimedDayCode: 'LAKE1', + imageHash: 'abc', + perceptualHash: 'ffff', + exifLat: 35.1, + exifLng: 129.1, + dayCodeStatus: 'accepted', + lengthCm: 42, + user: { id: 'user-1', nickname: 'angler', profileImage: null }, + fishSpecies: null, + certification: { grade: 'S' }, + }); + + const result = await service.findById('catch-1'); + + expect(result).toEqual(expect.objectContaining({ id: 'catch-1', dayCodeStatus: 'accepted', lengthCm: 42 })); + expect(result).not.toHaveProperty('claimedDayCode'); + expect(result).not.toHaveProperty('imageHash'); + expect(result).not.toHaveProperty('perceptualHash'); + expect(result).not.toHaveProperty('exifLat'); + expect(JSON.stringify(result)).not.toContain('LAKE1'); + }); + it('자랑 기록을 지우면 이미지 해시를 비운다', async () => { prisma.catch.findUnique.mockResolvedValue({ id: 'catch-1', diff --git a/fishing/apps/api/src/catches/catches.service.ts b/fishing/apps/api/src/catches/catches.service.ts index 1138828..86005cf 100644 --- a/fishing/apps/api/src/catches/catches.service.ts +++ b/fishing/apps/api/src/catches/catches.service.ts @@ -14,6 +14,7 @@ import { isUniqueConstraintError, softDeleteCatchData } from '../common/upload/c import { computePerceptualHash, isNearDuplicate } from '../common/upload/image-phash.util'; import { parsePageLimit } from '../common/pagination.util'; import { TournamentsService } from '../tournaments/tournaments.service'; +import { toPublicCatch, toPublicCatches } from './public-catch'; function claimedLocation(body: { locationLat?: unknown; locationLng?: unknown }) { const lat = Number(body.locationLat); @@ -148,7 +149,7 @@ export class CatchesService { message: '자랑 기록이 올라갔습니다. 다른 낚시인의 추천을 받아보세요!', - catch: catch_, + catch: toPublicCatch(catch_), }; @@ -361,7 +362,7 @@ export class CatchesService { if (!catch_) throw new NotFoundException('기록을 찾을 수 없습니다.'); - return catch_; + return toPublicCatch(catch_); } @@ -450,7 +451,13 @@ export class CatchesService { - return { items, total, page: paging.page, limit: paging.limit, recordType: recordType ?? 'all' }; + return { + items: toPublicCatches(items), + total, + page: paging.page, + limit: paging.limit, + recordType: recordType ?? 'all', + }; } async updatePersonal( @@ -473,11 +480,12 @@ export class CatchesService { if (body.locationName !== undefined) data.locationName = body.locationName.trim() || null; if (body.fishSpeciesId !== undefined) data.fishSpeciesId = body.fishSpeciesId; - return this.prisma.catch.update({ + const updated = await this.prisma.catch.update({ where: { id: catchId }, data, include: { fishSpecies: true }, }); + return toPublicCatch(updated); } async deletePersonal(userId: string, catchId: string) { diff --git a/fishing/apps/api/src/catches/public-catch.spec.ts b/fishing/apps/api/src/catches/public-catch.spec.ts new file mode 100644 index 0000000..06d7c32 --- /dev/null +++ b/fishing/apps/api/src/catches/public-catch.spec.ts @@ -0,0 +1,26 @@ +import { toPublicCatch } from './public-catch'; + +describe('공개 기록', () => { + it('대회 코드와 사진 해시, 원본 GPS를 빼지만 검수 상태는 남긴다', () => { + const row = toPublicCatch({ + id: 'c1', + claimedDayCode: 'LAKE1', + imageHash: 'abc', + perceptualHash: 'ffff', + exifLat: 37.1, + exifLng: 127.2, + dayCodeStatus: 'pending', + lengthCm: 30, + user: { nickname: 'angler' }, + }); + + expect(row).toEqual({ + id: 'c1', + dayCodeStatus: 'pending', + lengthCm: 30, + user: { nickname: 'angler' }, + }); + expect(JSON.stringify(row)).not.toContain('LAKE1'); + expect(JSON.stringify(row)).not.toContain('ffff'); + }); +}); diff --git a/fishing/apps/api/src/catches/public-catch.ts b/fishing/apps/api/src/catches/public-catch.ts new file mode 100644 index 0000000..1ba58a1 --- /dev/null +++ b/fishing/apps/api/src/catches/public-catch.ts @@ -0,0 +1,20 @@ +const HIDDEN_CATCH_KEYS = [ + 'claimedDayCode', + 'imageHash', + 'perceptualHash', + 'exifLat', + 'exifLng', +] as const; + +type HiddenCatchKey = (typeof HIDDEN_CATCH_KEYS)[number]; + +/** 공개·본인 기록 응답에서 대회 코드, 사진 해시, 원본 GPS를 뺀다. */ +export function toPublicCatch(row: T): Omit { + const copy = { ...row } as Record; + for (const key of HIDDEN_CATCH_KEYS) delete copy[key]; + return copy as Omit; +} + +export function toPublicCatches(rows: T[]): Array> { + return rows.map((row) => toPublicCatch(row)); +} diff --git a/fishing/apps/api/src/users/public-profile-catch.spec.ts b/fishing/apps/api/src/users/public-profile-catch.spec.ts new file mode 100644 index 0000000..f13cece --- /dev/null +++ b/fishing/apps/api/src/users/public-profile-catch.spec.ts @@ -0,0 +1,51 @@ +import { UsersService } from './users.service'; + +describe('공개 프로필 기록', () => { + const secretCatch = { + id: 'c1', + userId: 'user-1', + claimedDayCode: 'LAKE1', + imageHash: 'hash', + perceptualHash: 'ffff', + exifLat: 35.1, + exifLng: 129.1, + lengthCm: 40, + }; + const prisma = { + user: { findUnique: jest.fn() }, + catch: { findMany: jest.fn(), count: jest.fn() }, + post: { findMany: jest.fn(), count: jest.fn() }, + userGear: { findMany: jest.fn() }, + }; + const service = new UsersService(prisma as never); + + beforeEach(() => { + jest.clearAllMocks(); + prisma.user.findUnique.mockResolvedValue({ + id: 'user-1', + nickname: 'angler', + profileImage: null, + bio: null, + activityRegion: null, + fishingCategory: null, + featuredCatchIds: ['c1'], + createdAt: new Date('2026-10-01T00:00:00.000Z'), + }); + prisma.catch.count.mockResolvedValue(1); + prisma.post.count.mockResolvedValue(0); + prisma.catch.findMany.mockResolvedValue([secretCatch]); + prisma.post.findMany.mockResolvedValue([]); + prisma.userGear.findMany.mockResolvedValue([]); + }); + + it('대표 기록과 목록에서 대회 코드와 해시를 빼 준다', async () => { + const result = await service.getPublicProfile('angler'); + const body = JSON.stringify(result); + + expect(body).not.toContain('LAKE1'); + expect(body).not.toContain('ffff'); + expect(result.catches[0]).not.toHaveProperty('exifLat'); + expect(result.featuredCatches[0]).toEqual(expect.objectContaining({ id: 'c1', lengthCm: 40 })); + expect(result.personalCatches[0]).not.toHaveProperty('claimedDayCode'); + }); +}); diff --git a/fishing/apps/api/src/users/users.service.ts b/fishing/apps/api/src/users/users.service.ts index d88af25..b955b37 100644 --- a/fishing/apps/api/src/users/users.service.ts +++ b/fishing/apps/api/src/users/users.service.ts @@ -1,5 +1,6 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { toPublicCatches } from '../catches/public-catch'; @Injectable() export class UsersService { @@ -52,9 +53,11 @@ export class UsersService { }, }); - return featuredCatchIds - .map((id) => catches.find((c) => c.id === id)) - .filter((c): c is NonNullable => !!c); + return toPublicCatches( + featuredCatchIds + .map((id) => catches.find((c) => c.id === id)) + .filter((c): c is NonNullable => !!c), + ); } async getPublicProfile(nickname: string) { @@ -100,7 +103,15 @@ export class UsersService { this.getUserGears(user.id), ]); - return { user, stats, featuredCatches, catches, personalCatches, posts, gears }; + return { + user, + stats, + featuredCatches, + catches: toPublicCatches(catches), + personalCatches: toPublicCatches(personalCatches), + posts, + gears, + }; } async getMyStats(userId: string) {