Files
wandergo/backend/test/kcisa-sync.e2e-spec.ts
T
ttipo e383c2e242 feat(backend): 같은 출처에 두 번 등록된 전시를 하나로 합침
한국문화정보원에 같은 전시가 제목만 다르게 두 번 등록되어 목록에 2건으로 나왔다
("문준호 전 - 남겨진 것들을 위한 풍경" / "문준호 《남겨진 것들을 위한 풍경》", 같은 갤러리·같은 기간).

같은 장소 · 같은 시작·종료일 · 제목 비교 키가 연속 6글자 이상 겹치면 같은 전시로 보고
먼저 등록된 것만 남긴다. 장소·기간만으로 합치지 않는 이유는 여러 전시실에서 같은 날
여러 전시를 여는 곳이 있어서다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:27:59 +09:00

182 lines
5.5 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import { PlaceSyncCliModule } from '@/commands/place-sync.cli.module';
import { FestivalEvent } from '@/modules/place/entities/festival-event.entity';
import { CallBudget } from '@/modules/place/sync/call-budget';
import type {
KcisaApi,
KcisaItem,
} from '@/modules/place/sync/kcisa-api.client';
import { KcisaSync } from '@/modules/place/sync/kcisa.sync';
import { PublicDataApiFactory } from '@/modules/place/sync/public-data.factory';
import { kstDateString } from '@/shared/utils/kst';
/**
* 한국문화정보원 전시 동기화 — 가짜 응답으로 전시만 받기·다른 출처 중복 건너뛰기·상세·숨김·원본 장애 판단을 실제 DB 로 검증한다.
* 실제 데이터를 건드리지 않도록 없는 시·도 코드 '99' 와 '99000…' 일련번호만 쓴다.
*/
const REGION = '99';
const ymd = (addDays: number) =>
kstDateString(new Date(), addDays).replace(/-/g, '');
const item = (seq: string, extra: Partial<KcisaItem> = {}): KcisaItem => ({
seq,
title: `시험 전시 ${seq}`,
startDate: ymd(0),
endDate: ymd(30),
realmName: '전시',
place: '시험미술관',
gpsX: '130.6',
gpsY: '36.6',
thumbnail: 'https://www.culture.go.kr/upload/thumb.jpg',
...extra,
});
const DETAILS: Record<string, KcisaItem> = {
'99000001': {
price: '무료',
contents1: '<p>시험 전시입니다</p>',
phone: '053-000-0000',
placeAddr: '시험시 시험구 1',
url: 'https://museum.test/1',
},
'99000002': { price: '5,000원' },
};
/** 실행마다 바꿔 끼우는 가짜 목록 */
let fakeList: KcisaItem[] = [];
function fakeApi(budget: CallBudget): KcisaApi {
return {
listByArea: () => {
budget.take('area2');
return Promise.resolve({ items: fakeList, totalCount: fakeList.length });
},
getDetail: (seq) => {
budget.take('detail2');
return Promise.resolve(DETAILS[seq] ?? null);
},
};
}
describe('한국문화정보원 전시 동기화 (e2e)', () => {
let moduleRef: TestingModule;
let ds: DataSource;
let sync: KcisaSync;
const cleanup = async () => {
await ds.query(
`DELETE FROM festival_event WHERE (source = 'kcisa' AND source_id LIKE '99%') OR source_id = 'e2e-kc-tour'`,
);
await ds.query(
`DELETE FROM place_sync_run WHERE job = 'kcisa' AND scope = $1`,
[REGION],
);
};
const event = (seq: string) =>
ds.getRepository(FestivalEvent).findOneByOrFail({
source: 'kcisa',
sourceId: seq,
});
beforeAll(async () => {
moduleRef = await Test.createTestingModule({
imports: [PlaceSyncCliModule],
})
.overrideProvider(PublicDataApiFactory)
.useValue({
kcisaDailyLimit: () => 1_000,
createKcisa: (budget: CallBudget) => fakeApi(budget),
})
.compile();
await moduleRef.init();
ds = moduleRef.get(DataSource);
await ds.runMigrations();
await cleanup();
sync = moduleRef.get(KcisaSync);
// 관광공사에 이미 있는 행사 — 같은 이름의 전시는 넣지 않아야 한다
const repo = ds.getRepository(FestivalEvent);
await repo.save(
repo.create({
source: 'tour',
sourceId: 'e2e-kc-tour',
kind: 'event',
name: '시험 비엔날레',
startDate: kstDateString(new Date()),
endDate: kstDateString(new Date(), 20),
ldongRegnCd: REGION,
}),
);
});
afterAll(async () => {
await cleanup();
await moduleRef.close();
});
it('첫 실행: 전시만 넣고, 다른 출처·같은 출처 중복은 건너뛰고, 상세를 받는다(이미지는 넣지 않음)', async () => {
fakeList = [
item('99000001'),
item('99000002', { startDate: ymd(5), endDate: ymd(60) }),
item('99000003', { realmName: '음악/콘서트' }),
item('99000004', { title: '시험 비엔날레' }),
// 99000001 과 같은 장소·기간, 제목만 다르게 다시 등록된 전시
item('99000005', { title: '시험 전시 《99000001》 특별전' }),
];
const result = await sync.run({ regionCode: REGION });
expect(result).toMatchObject({
inserted: 2,
detailed: 2,
detailRemaining: 0,
skipped: {
not_exhibition: 1,
duplicate_other_source: 1,
duplicate_same_source: 1,
},
calls: { area2: 1, detail2: 2 },
});
expect(await event('99000001')).toMatchObject({
kind: 'exhibition',
genre: '전시',
eventPlace: '시험미술관',
lat: 36.6,
lng: 130.6,
feeText: '무료',
overview: '시험 전시입니다',
phone: '053-000-0000',
address: '시험시 시험구 1',
homepage: 'https://museum.test/1',
imageUrl: null,
imageThumbUrl: null,
});
});
it('목록이 통째로 비면 원본 장애로 보고 숨기지 않는다', async () => {
fakeList = [];
const result = await sync.run({ regionCode: REGION });
expect(result.hidden).toBe(0);
expect(result.errors.join(' ')).toContain('원본 장애');
expect((await event('99000001')).status).toBe('active');
});
it('목록에서 하나가 없어지면 숨기고, 상세는 7일이 지나기 전에는 다시 받지 않는다', async () => {
fakeList = [item('99000002', { startDate: ymd(5), endDate: ymd(60) })];
const result = await sync.run({ regionCode: REGION });
expect(result).toMatchObject({
hidden: 1,
updated: 1,
detailed: 0,
calls: { area2: 1 },
});
expect((await event('99000001')).status).toBe('hidden');
});
});