generated from ttipo/project-base
e383c2e242
한국문화정보원에 같은 전시가 제목만 다르게 두 번 등록되어 목록에 2건으로 나왔다
("문준호 전 - 남겨진 것들을 위한 풍경" / "문준호 《남겨진 것들을 위한 풍경》", 같은 갤러리·같은 기간).
같은 장소 · 같은 시작·종료일 · 제목 비교 키가 연속 6글자 이상 겹치면 같은 전시로 보고
먼저 등록된 것만 남긴다. 장소·기간만으로 합치지 않는 이유는 여러 전시실에서 같은 날
여러 전시를 여는 곳이 있어서다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 lines
5.5 KiB
TypeScript
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');
|
|
});
|
|
});
|