generated from ttipo/project-base
feat(backend): 장소·축제·공연·전시 데이터 기반(장소 DB·동기화·조회 API) #1
@@ -5,6 +5,7 @@ import {
|
||||
kopisListItemToEvent,
|
||||
kopisTimestamp,
|
||||
kopisVenue,
|
||||
stripRegionTag,
|
||||
} from './kopis-mapper';
|
||||
|
||||
describe('kopisDate', () => {
|
||||
@@ -205,3 +206,35 @@ describe('kopisVenue', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripRegionTag', () => {
|
||||
it('제목 끝의 지역 꼬리표를 뗀다(2026-11-03 실제 제목)', () => {
|
||||
expect(stripRegionTag('굿프렌즈 [대구]')).toBe('굿프렌즈');
|
||||
expect(
|
||||
stripRegionTag('데뷔 60주년 기념공연, 남진 전국투어 콘서트 [대구 북구]'),
|
||||
).toBe('데뷔 60주년 기념공연, 남진 전국투어 콘서트');
|
||||
expect(
|
||||
stripRegionTag('V.O.S 전국투어 콘서트: 제2회 동창회 [대구 경산]'),
|
||||
).toBe('V.O.S 전국투어 콘서트: 제2회 동창회');
|
||||
});
|
||||
|
||||
it('꼬리표 안팎의 괄호 메모(앵콜·회차)는 남긴다', () => {
|
||||
expect(stripRegionTag('무명전설 전국투어 콘서트 [대구 (앵콜) ]')).toBe(
|
||||
'무명전설 전국투어 콘서트 (앵콜)',
|
||||
);
|
||||
expect(stripRegionTag('Jazz in Blue 민경인 트리오 [안동] (11월)')).toBe(
|
||||
'Jazz in Blue 민경인 트리오 (11월)',
|
||||
);
|
||||
expect(stripRegionTag('온 더 레드카펫 [대구] (11.10)')).toBe(
|
||||
'온 더 레드카펫 (11.10)',
|
||||
);
|
||||
});
|
||||
|
||||
it('지역이 아닌 꼬리표·가운데 꼬리표·꼬리표뿐인 제목은 그대로 둔다', () => {
|
||||
expect(stripRegionTag('라라라온 [롴돰 로컬다움]')).toBe(
|
||||
'라라라온 [롴돰 로컬다움]',
|
||||
);
|
||||
expect(stripRegionTag('[대구] 송년 음악회')).toBe('[대구] 송년 음악회');
|
||||
expect(stripRegionTag('[대구]')).toBe('[대구]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,35 @@ const KOPIS_ID_RE = /^[A-Za-z0-9]+$/;
|
||||
/** 끝난 공연 — 기간이 겹쳐 목록에 와도 넣지 않는다 */
|
||||
const ENDED_STATE = '공연완료';
|
||||
|
||||
/**
|
||||
* 순회 공연 제목 끝의 지역 꼬리표("굿프렌즈 [대구]")에 쓰이는 이름 — 시·도와 대구·경북 시군구.
|
||||
* 2026-11-03 대구·경북 공연 244건 중 203건에 붙어 있었다("[대구]" 141 · "[안동]" 15 · "[대구 북구]" …).
|
||||
*/
|
||||
const REGION_TAG_WORDS = new Set([
|
||||
// 시·도
|
||||
...['서울', '부산', '대구', '인천', '광주', '대전', '울산', '세종'],
|
||||
...['경기', '강원', '충북', '충남', '전북', '전남', '경북', '경남', '제주'],
|
||||
// 대구 구·군
|
||||
...[
|
||||
'중구',
|
||||
'동구',
|
||||
'서구',
|
||||
'남구',
|
||||
'북구',
|
||||
'수성',
|
||||
'수성구',
|
||||
'달서',
|
||||
'달서구',
|
||||
],
|
||||
...['달성', '달성군', '군위', '군위군'],
|
||||
// 경북 시·군
|
||||
...['포항', '경주', '김천', '안동', '구미', '영주', '영천', '상주', '문경'],
|
||||
...['경산', '의성', '청송', '영양', '영덕', '청도', '고령', '성주', '칠곡'],
|
||||
...['예천', '봉화', '울진', '울릉'],
|
||||
]);
|
||||
/** 제목 끝의 [꼬리표] 와 그 뒤에 붙는 회차 표시("(11월)", "(11.10)") */
|
||||
const TRAILING_TAG_RE = /\s*\[([^[\]]*)\](\s*\([^()]*\))?\s*$/;
|
||||
|
||||
export interface KopisListEvent {
|
||||
sourceId: string;
|
||||
name: string;
|
||||
@@ -67,12 +96,36 @@ export function kopisImageUrl(value: string | null): string | null {
|
||||
return /^https:\/\//i.test(value) ? value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 제목 끝의 지역 꼬리표를 뗀다. 꼬리표 안이 지역 이름과 괄호 메모로만 되어 있을 때만 떼고, 메모는 남긴다.
|
||||
* "굿프렌즈 [대구]" → "굿프렌즈" · "무명전설 콘서트 [대구 (앵콜) ]" → "무명전설 콘서트 (앵콜)"
|
||||
* "… 트리오 [안동] (11월)" → "… 트리오 (11월)" · "라라라온 [롴돰 로컬다움]" → 그대로
|
||||
*/
|
||||
export function stripRegionTag(name: string): string {
|
||||
const m = TRAILING_TAG_RE.exec(name);
|
||||
if (!m || m.index === 0) return name;
|
||||
const notes = m[1].match(/\([^()]*\)/g) ?? [];
|
||||
const words = m[1]
|
||||
.replace(/\([^()]*\)/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
if (words.length === 0 || !words.every((w) => REGION_TAG_WORDS.has(w))) {
|
||||
return name;
|
||||
}
|
||||
const suffix = [...notes, m[2]?.trim()]
|
||||
.filter((v): v is string => !!v)
|
||||
.map((v) => v.replace(/\(\s*/, '(').replace(/\s*\)/, ')'))
|
||||
.join(' ');
|
||||
return [name.slice(0, m.index), suffix].filter(Boolean).join(' ').trim();
|
||||
}
|
||||
|
||||
/** 목록 항목 1건 → 기본 컬럼. 쓸 수 없으면 사유를 돌려준다 */
|
||||
export function kopisListItemToEvent(
|
||||
item: KopisItem,
|
||||
): KopisListEvent | { skip: KopisSkipReason } {
|
||||
const sourceId = str(item, 'mt20id');
|
||||
const name = htmlToText(item.prfnm);
|
||||
const title = htmlToText(item.prfnm);
|
||||
const name = title ? stripRegionTag(title) : null;
|
||||
if (!sourceId || !KOPIS_ID_RE.test(sourceId) || !name) {
|
||||
return { skip: 'no_id' };
|
||||
}
|
||||
@@ -140,8 +193,9 @@ export function kopisDetail(db: KopisItem): KopisDetail {
|
||||
const endDate = kopisDate(str(db, 'prfpdto'));
|
||||
const validPeriod =
|
||||
startDate !== null && endDate !== null && endDate >= startDate;
|
||||
const title = htmlToText(db.prfnm);
|
||||
return {
|
||||
name: htmlToText(db.prfnm),
|
||||
name: title ? stripRegionTag(title) : null,
|
||||
startDate: validPeriod ? startDate : null,
|
||||
endDate: validPeriod ? endDate : null,
|
||||
genre: str(db, 'genrenm'),
|
||||
|
||||
@@ -406,6 +406,8 @@ node dist/commands/place-sync.js tour-kor
|
||||
3. **공연시설** `prfplc/{mt10id}` — 진행 중·예정 공연이 쓰는 시설 중 새 시설과 30일 지난 시설만 → `event_venue`.
|
||||
**좌표는 주소의 시·도 영역 안일 때만 쓴다**(`geo.ts` `isInSido`). 아니면 비우고 경고를 남긴다(그 공연은 근처 조회에서만 빠진다).
|
||||
4. 시설의 좌표·주소·전화를 공연에 복사한다. 조회 API 가 공연 좌표로 거리를 계산하기 때문이다. 시설 좌표를 고치면 다음 실행에 공연도 바뀐다.
|
||||
- 순회 공연 제목 끝의 지역 꼬리표("굿프렌즈 [대구]", "[대구 북구]", "[안동] (11월)")를 떼어 저장한다. 2026-11-03 244건 중 203건에 붙어 있었다.
|
||||
꼬리표 안이 지역 이름과 괄호 메모로만 되어 있을 때만 떼고, "(앵콜)"·"(11.10)" 같은 메모는 남긴다(`kopis-mapper.ts` `stripRegionTag`).
|
||||
- 포스터는 `http://www.kopis.or.kr/…` 로 온다. 앱이 http 이미지를 막으므로 `https://kopis.or.kr/…` 로 바꿔 저장한다(목록·상세 사진 모두 포스터).
|
||||
- 호출: 0.6초 간격(KOPIS 는 초당 10회에서 차단). 오퍼레이션별(`pblprfr`·`pblprfr-detail`·`prfplc-detail`) 하루 상한 980.
|
||||
공식 한도는 밝혀져 있지 않다. 이후 하루 호출은 목록 4회 + 상세 수십 회로 예상한다.
|
||||
|
||||
Reference in New Issue
Block a user