Skip to content
Merged
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
25 changes: 25 additions & 0 deletions fishing/apps/api/src/catches/catches.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
16 changes: 12 additions & 4 deletions fishing/apps/api/src/catches/catches.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -148,7 +149,7 @@ export class CatchesService {

message: '자랑 기록이 올라갔습니다. 다른 낚시인의 추천을 받아보세요!',

catch: catch_,
catch: toPublicCatch(catch_),

};

Expand Down Expand Up @@ -361,7 +362,7 @@ export class CatchesService {

if (!catch_) throw new NotFoundException('기록을 찾을 수 없습니다.');

return catch_;
return toPublicCatch(catch_);

}

Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand Down
26 changes: 26 additions & 0 deletions fishing/apps/api/src/catches/public-catch.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
20 changes: 20 additions & 0 deletions fishing/apps/api/src/catches/public-catch.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object>(row: T): Omit<T, HiddenCatchKey> {
const copy = { ...row } as Record<string, unknown>;
for (const key of HIDDEN_CATCH_KEYS) delete copy[key];
return copy as Omit<T, HiddenCatchKey>;
}

export function toPublicCatches<T extends object>(rows: T[]): Array<Omit<T, HiddenCatchKey>> {
return rows.map((row) => toPublicCatch(row));
}
51 changes: 51 additions & 0 deletions fishing/apps/api/src/users/public-profile-catch.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
19 changes: 15 additions & 4 deletions fishing/apps/api/src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -52,9 +53,11 @@ export class UsersService {
},
});

return featuredCatchIds
.map((id) => catches.find((c) => c.id === id))
.filter((c): c is NonNullable<typeof c> => !!c);
return toPublicCatches(
featuredCatchIds
.map((id) => catches.find((c) => c.id === id))
.filter((c): c is NonNullable<typeof c> => !!c),
);
}

async getPublicProfile(nickname: string) {
Expand Down Expand Up @@ -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) {
Expand Down
Loading