공연잇다
흩어진 공연 데이터를
탐색 가능한 정보로.
공공 공연 데이터를 수집하고 탐색하는 서비스. 외부 데이터 변환·저장, 기존 데이터 보완, AI 요약 흐름과 보안 연동을 개선했습니다.

수집 이후의 데이터를 다루다.
KOPIS와 대전광역시 공공데이터는 응답 형식과 정보의 빈 정도가 다릅니다. 데이터를 공통 공연 정보로 변환하고, 다시 수집할 때 기존 정보를 보존하면서 부족한 값을 보완했습니다.
직접 구현·수정
외부 응답 Mapper와 수집·저장 흐름, 기존 데이터의 빈 필드 보완, 상세 AI 요약·추천 이유 흐름과 기존 보안 구성을 보완했습니다.
팀 기능과의 연결
기간 겹침 검색 조건은 팀원이 추가한 기능입니다. AI와 인증에도 여러 작성자의 이력이 있으며, 담당한 보완·연동 변경을 별도로 연결했습니다.
다시 들어온 데이터,
무조건 덮어쓰지 않기.
문제
외부 데이터를 반복 수집할 때 같은 출처의 공연이 다시 들어옵니다. 새 응답에 값이 빠져 있다고 기존에 저장한 정보까지 잃어서는 안 됩니다.
판단
동일 출처의 재수집을 식별하는 기준을 sourceName + externalId로 두었습니다. 신규 저장과 기존 데이터 보완을 분리하고, 이미 존재하는 값은 보존합니다.
출처와 외부 ID로 조회
findBySourceNameAndExternalId로 기존 공연을 찾습니다.새 공연이면 저장
생성·갱신 시점을 설정하고, 가능한 경우 요약 생성을 시도한 뒤 저장합니다.
기존 공연이면 빈 값 보완
제목·장르·포스터·상세 정보·기간 등 비어 있는 필드를 보완합니다. 변경이 있을 때 갱신 시점을 바꿉니다.
동일 출처에서 반복 수집된 항목을 처리하는 방식입니다. 서로 다른 기관의 같은 공연을 유사도 분석으로 통합하는 기능은 아닙니다.
날짜와 장르의 정규화
KOPIS 날짜는 yyyy-MM-dd, yyyy.MM.dd, yyyyMMdd 순서로 파싱합니다. 파싱 실패는 null로 처리하고, 원본 장르와 서비스용 장르를 구분해 저장합니다.
재수집 처리의 핵심 코드 보기
// PerformanceSyncService.java · 핵심 흐름 발췌
Optional<Performance> existing =
performanceRepository.findBySourceNameAndExternalId(
incoming.getSourceName(), incoming.getExternalId());
// 신규 항목: 생성·갱신 시점 설정 후 저장
// 기존 항목: enrichBlankFields(target, incoming)
// 실제 변경 시: target.setUpdatedAt(LocalDateTime.now())원본 전체는 관련 코드 링크에서 확인할 수 있습니다.
기간이 겹치는 공연을 찾는 조건.
검색 기간 이전에 시작한 공연도 검색 기간과 겹치면 포함합니다. 아래는 팀의 PerformanceSpec 구현을 읽고 정리한 조건입니다.
공연 종료일 ≥ 검색 시작일
AND
공연 시작일 ≤ 검색 종료일검색 시작일 또는 종료일이 생략되면 해당 조건을 생략합니다. 양 끝 날짜를 포함합니다.
| 공연 기간 | 판단 | 이유 |
|---|---|---|
| 9/1~9/11 | 포함 | 검색 시작일과 겹침 |
| 9/1~9/30 | 포함 | 검색 기간 전체를 포함 |
| 9/12~9/15 | 포함 | 경계 날짜 포함 |
| 9/13~9/15 | 제외 | 검색 기간 이후에 시작 |
조건을 설명하는 예시이며 실행 테스트 결과는 아닙니다. 기간 겹침 필터의 최초 추가는 다른 팀원의 변경입니다.
공개 조회와 사용자 기능의 경계.
기존 Spring Security·JWT 구성에서 공개할 조회와 인증이 필요한 기능의 경계를 보완했습니다. JWT 파싱 이후 DB의 currentToken과 비교하고, 인증 실패 응답 형식을 통일합니다.
공개 조회
공연 목록·검색GET /api/performances
숫자 ID의 공연 상세GET /api/performances/{id}
인증 후 사용
내 정보GET /api/auth/me
관심 공연 /api/favorites
추천 GET /api/recommendations
공통 인증 실패 응답
HTTP 401
{
"success": false,
"code": "UNAUTHORIZED",
"message": "인증이 필요합니다."
}코드에서 확인한 응답 형식입니다. 이번 작업에서 실서버를 호출해 캡처한 응답은 아닙니다.
저장한 요약은 재사용하고,
누락된 정보는 보완하다.
요약과 추천 이유의 생성 시점은 하나가 아닙니다. 동기화 과정과 상세 조회에서 필요한 값을 보완하고, 유효한 저장값이 있으면 재사용합니다.
| 시점 | 동작 |
|---|---|
| 데이터 동기화 | 원문 설명과 요약 유무를 확인해 요약 생성 시도. |
| 상세 조회 | 요약·추천 이유가 없거나 기본 문구일 때 생성 시도. 성공하면 저장. |
| 유효한 저장값 존재 | 저장된 값을 재사용. 목록도 저장값 사용. |
| 빈 응답·사용 불가 응답·예외 | 빈 결과 처리. 상세 DTO가 정해진 기본 문구 제공. |
추천 이유: “선택한 조건에 맞는 공연입니다.”
추천 후보와 AI 설명의 역할 분리
추천 후보는 사용자의 선호 장르와 DB 조회로 선택합니다. AI는 요약과 추천 이유 생성의 역할을 맡으며, 전체 공연의 순위를 결정하는 구조로 설명하지 않습니다.

여기서 설명한 실패 처리는 코드의 예외·대체 응답 범위입니다. 외부 AI 장애 시 전체 서비스의 정상 동작을 실행 검증했다는 의미는 아닙니다.
데이터의 출처와
빈 값의 의미를 구분하는 일.
수집 결과를 저장하는 것과 기존 데이터를 보존하는 것은 다른 문제였습니다. 데이터의 출처·식별자·빈 필드 처리 기준을 명확히 해야 반복 수집 이후의 상태를 설명할 수 있었습니다.
AI 결과 또한 생성 성공 여부만으로 끝나지 않았습니다. 생성 시점, 재사용할 값의 조건, 실패했을 때 사용자에게 표시할 문구를 함께 연결했습니다.
협업에서의 역할
5명 팀의 팀장으로 역할과 API 응답 규격을 정리하고, 기능 브랜치를 통합하며 병합 충돌을 해결했습니다. 문서를 전달하는 데서 끝내지 않고 함께 코드를 보며 막힌 부분을 확인하는 방식으로 협업을 개선했습니다.