feat(backend): 장소 목록에 관광공사·확인 장소만 받는 trusted 조건 추가
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 11m54s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m0s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 11m5s
CI / Frontend (lint · type-check · test) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
CI / Backend (lint · test · e2e · build) (push) Has been cancelled

장소 상세 '함께 가기 좋은 곳'은 확인 안 된 상가정보 매장을 빼야 한다(2026-11-02 합의).
앱이 가까운 30곳을 받아 거르면 도심처럼 상가가 빽빽한 곳은 받은 곳이 모두 상가라 칸이 비었다.
관광공사 장소 상세 860곳 중 30곳은 칸이 사라지고 186곳은 덜 보였다(봉산서원은 1.5km 안에
관광공사 장소가 53곳인데 0곳).

trusted=true 면 관광공사 장소와 운영자가 확인한 장소만 주고 '추천 안 함' 판정도 뺀다.
선택 조건이라 기존 호출은 그대로다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #4.
This commit is contained in:
2026-11-04 23:58:27 +09:00
parent 785afcc53f
commit 4bd2a9b4ad
4 changed files with 95 additions and 1 deletions
+72
View File
@@ -22,6 +22,10 @@ import { parseOpeningHours } from '@/shared/utils/opening-hours';
const C = { lat: 36.5, lng: 130.5 };
const at = (dLat: number) => ({ lat: C.lat + dLat, lng: C.lng });
const near = `lat=${C.lat}&lng=${C.lng}&radius=2000`;
/** 'trusted'(관광공사·확인 장소만) 검증용 묶음 — 위 묶음의 거리순 기대값에 섞이지 않게 5.5km 떨어뜨린다 */
const C2 = { lat: 36.55, lng: 130.5 };
const at2 = (dLat: number) => ({ lat: C2.lat + dLat, lng: C2.lng });
const near2 = `lat=${C2.lat}&lng=${C2.lng}&radius=1000`;
interface ListItem {
id: string;
@@ -186,6 +190,58 @@ describe('장소 조회 API (e2e)', () => {
verifiedMethod: 'phone',
verifiedAt: '2026-11-02',
});
// trusted 묶음: 관광공사 1 · 확인 안 된 상가 1 · 확인한 상가 1 · 추천 안 함으로 판정한 관광공사·상가 각 1
await save('R1', {
source: 'tour',
sourceId: 'e2e-R1',
category: 'attraction',
name: '이투이 바닷가 전망대',
...at2(0.001),
});
await save('R2', {
source: 'store',
sourceId: 'e2e-R2',
category: 'cafe',
name: '이투이 확인안한카페',
...at2(0.0005), // 가장 가깝다 — trusted 가 아니면 첫 자리를 차지한다
});
await save('R3', {
source: 'store',
sourceId: 'e2e-R3',
category: 'cafe',
name: '이투이 확인한카페',
...at2(0.002),
});
await save('R4', {
source: 'tour',
sourceId: 'e2e-R4',
category: 'cafe',
name: '이투이 추천안함카페',
...at2(0.0025),
});
await save('R5', {
source: 'store',
sourceId: 'e2e-R5',
category: 'bar',
name: '이투이 추천안함바',
...at2(0.003),
});
const curations = ds.getRepository(PlaceCuration);
await curations.save({
placeId: ids.R3,
verdict: 'pending',
verifiedMethod: 'official_channel',
verifiedAt: '2026-11-04',
});
for (const key of ['R4', 'R5']) {
await curations.save({
placeId: ids[key],
verdict: 'not_recommend',
verifiedMethod: 'visit',
verifiedAt: '2026-11-04',
});
}
});
afterAll(async () => {
@@ -248,6 +304,21 @@ describe('장소 조회 API (e2e)', () => {
expect(names(res)).toEqual(['이투이카페', '이투이 골목카페']);
});
it('trusted 는 관광공사·운영자 확인 장소만 주고, 추천 안 함으로 판정한 곳은 뺀다', async () => {
const all = await get(`/api/places?${near2}`).expect(200);
expect(list(all).data.pageInfo.total).toBe(5);
const res = await get(`/api/places?${near2}&trusted=true`).expect(200);
expect(names(res)).toEqual(['이투이 바닷가 전망대', '이투이 확인한카페']);
expect(list(res).data.pageInfo.total).toBe(2);
// 가장 가까운 곳이 확인 안 된 상가여도 limit 안에 관광공사·확인 장소가 채워진다
const plain = await get(`/api/places?${near2}&limit=1`).expect(200);
expect(names(plain)).toEqual(['이투이 확인안한카페']);
const one = await get(`/api/places?${near2}&trusted=true&limit=1`).expect(
200,
);
expect(names(one)).toEqual(['이투이 바닷가 전망대']);
});
it('좌표 없이 이름 검색하면 이름순으로 반경 제한 없이 준다', async () => {
const res = await get(
`/api/places?q=${encodeURIComponent('이투이')}`,
@@ -266,6 +337,7 @@ describe('장소 조회 API (e2e)', () => {
['없는 분류', 'category=pub'],
['형식이 틀린 커서', `${near}&cursor=abc`],
['true/false 가 아닌 필터', 'pet=yes'],
['true/false 가 아닌 trusted', 'trusted=1'],
])('%s 는 VAL_001(400)', async (_, query) => {
const res = await get(`/api/places?${query}`).expect(400);
expect(detail(res).error?.code).toBe('VAL_001');