generated from ttipo/project-base
feat(backend): 장소 조회 API와 같은 가게 중복 병합 추가
검색·지도·상세 화면이 장소 DB 를 쓰도록 조회 API 를 연다(비로그인 허용).
- GET /api/places: 이름 검색, 좌표+반경 거리순, 분류·반려동물·실내 필터,
키셋 커서 페이지네이션(정렬값+id), 전체 개수, 항목별 "지금 영업 중"(KST)
- GET /api/places/{id}: 영업시간 원문·해석 상태·출처, 반려동물 크기·조건,
이용 안내(요금·주차 등), 추천 이유(추천+확인한 곳만), 출처 표기.
병합된 id 로 들어오면 병합 대상 장소를, 숨김·폐업 추정이면 RES_001 을 준다
- 같은 가게 병합: 관광공사 쪽이 카페·식당·주점이고 100m 안이며 상호가 같거나
포함(짧은 쪽 2글자 이상)이면 상가정보 쪽을 merged_into_id 로 관광공사 쪽에 합친다.
tour-kor·store 가 끝날 때 자동으로, place:sync -- dedupe 로 따로도 돈다
- 사후면세점 브랜드 매장(SH040300)은 목록·필터에서 빼고 이름 검색에만 낸다
(반려동물 필터 결과가 더현대 입점 매장으로 덮이던 문제)
- 공공 반려동물 조건 원문의 글머리표·줄바꿈 정리
e2e(test/place.e2e-spec.ts)는 시작할 때 마이그레이션을 적용하고 동해 좌표에
테스트 장소를 만들어 노출 규칙·필터·커서·상세·오류 코드를 검증한다.
2026-11-02 대구 실측: 같은 가게 21곳 병합, 도심 반려동물 필터 7곳.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { kstStartOfDay, parseKstTimestamp } from './kst';
|
||||
import { kstClock, kstStartOfDay, parseKstTimestamp } from './kst';
|
||||
|
||||
describe('kstStartOfDay', () => {
|
||||
it('한국 날짜의 0시를 돌려준다', () => {
|
||||
@@ -36,3 +36,13 @@ describe('parseKstTimestamp', () => {
|
||||
expect(parseKstTimestamp(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('kstClock', () => {
|
||||
it('한국 시간의 요일과 자정 이후 분을 돌려준다', () => {
|
||||
// 2026-11-01(일) 15:30 UTC = 2026-11-02(월) 00:30 KST
|
||||
expect(kstClock(new Date('2026-11-01T15:30:00Z'))).toEqual({
|
||||
weekday: 1,
|
||||
minutes: 30,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,3 +34,12 @@ export function parseKstTimestamp(value: unknown): Date | null {
|
||||
const back = new Date(date.getTime() + KST_OFFSET_MS);
|
||||
return back.getUTCDate() === d ? date : null;
|
||||
}
|
||||
|
||||
/** 한국 시간 기준 요일(0=일요일 … 6=토요일)과 자정 이후 분 — "지금 영업 중" 판단에 쓴다 */
|
||||
export function kstClock(now: Date): { weekday: number; minutes: number } {
|
||||
const kst = new Date(now.getTime() + KST_OFFSET_MS);
|
||||
return {
|
||||
weekday: kst.getUTCDay(),
|
||||
minutes: kst.getUTCHours() * 60 + kst.getUTCMinutes(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { escapeLike } from './sql-like';
|
||||
|
||||
describe('escapeLike', () => {
|
||||
it('와일드카드와 이스케이프 문자를 글자 그대로 찾게 한다', () => {
|
||||
expect(escapeLike('100%_카페\\')).toBe('100\\%\\_카페\\\\');
|
||||
});
|
||||
|
||||
it('일반 글자는 그대로 둔다', () => {
|
||||
expect(escapeLike('김광석 길')).toBe('김광석 길');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* LIKE/ILIKE 패턴에 넣을 사용자 입력을 글자 그대로 찾게 한다 (순수 함수).
|
||||
* `%`·`_`(와일드카드)와 `\`(PostgreSQL 기본 이스케이프 문자)를 이스케이프한다. 값은 반드시 바인딩 파라미터로 넘긴다.
|
||||
*/
|
||||
export function escapeLike(input: string): string {
|
||||
return input.replace(/[\\%_]/g, (c) => `\\${c}`);
|
||||
}
|
||||
Reference in New Issue
Block a user