Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ dependencies {
// 웹 클라이언트
implementation("org.springframework.boot:spring-boot-starter-webflux")

// Spring Retry( 로직 재시도 편의성 추구)
implementation ("org.springframework.boot:spring-boot-starter-aop")
implementation ("org.springframework.retry:spring-retry")

// 캐싱
implementation("org.springframework.boot:spring-boot-starter-data-redis")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;

import java.time.LocalDate;
Expand All @@ -30,6 +34,7 @@
@Slf4j
@Service
@EnableAsync
@EnableRetry
public class KopisApiService {
// 공연예술통합 전산망 조회를 위한 서비스 클래스입니다.
private final ConcertRepository concertRepository;
Expand All @@ -46,11 +51,14 @@ public class KopisApiService {

@Value("${kopis.api-key}")
private String serviceKey;

private LocalDate sdate = LocalDate.of(2025, 12, 1);
private LocalDate edate = LocalDate.now().plusYears(1);

private final RestClient restClient;

private int savedIndex;

public KopisApiService(ConcertRepository concertRepository, ConcertPlaceRepository placeRepository, TicketOfficeRepository ticketOfficeRepository, ConcertImageRepository imageRepository, ConcertUpdateTimeRepository concertUpdateTimeRepository,ConcertRedisRepository concertRedisRepository) {
this.concertRepository = concertRepository;
this.placeRepository = placeRepository;
Expand All @@ -64,6 +72,7 @@ public KopisApiService(ConcertRepository concertRepository, ConcertPlaceReposito
.build();
}

@Retryable(value = HttpClientErrorException.class, backoff = @Backoff(delay = 5000))
@Async
@Transactional
public void setConcertsList() throws InterruptedException {
Expand All @@ -83,13 +92,11 @@ public void setConcertsList() throws InterruptedException {
}

LocalDateTime now = LocalDateTime.now();
Long startNs = System.currentTimeMillis();
long startNs = System.currentTimeMillis();

// 콘서트 목록 받아올 Response 객체 선언
ConcertListResponse plr;
// 총 콘서트 요소를 저장할 배열
List<ConcertListElement> totalConcertsList = new ArrayList<>();
// 저장시 캐시로 사용할 맵(어차피 400개 정도니까 맵 쓰는게 더 효율적으로 판단)
List<ConcertListElement> totalConcertsList;
// 저장시 캐시로 사용할 맵(어차피 400개 정도니까 맵 쓰는게 더 효율적으로 판단) -> 필드로 빼야 하나?
Map<String, ConcertPlace> concertPlaceMap = new HashMap<>();

int addedConcerts = 0;
Expand All @@ -99,90 +106,59 @@ public void setConcertsList() throws InterruptedException {

int page = 1;
try{
while (true) {
// 콘서트 목록 받아오기
plr = getConcertListResponse(serviceKey, sdate, edate, page);
page++;
// 더 이상 받아올 콘서트 목록이 없으면 멈춤
if (plr.getConcertList() == null) break;
// 콘서트 요소를 콘서트 목록에서 꺼내서 더하기
for (ConcertListElement p : plr.getConcertList()) {
totalConcertsList.add(p);
}
log.info("Total Concert List: {}", totalConcertsList.size() + "개의 데이터 가져오는중...");
Thread.sleep(120);
}

// 모든 공연을 가져오기
totalConcertsList = getAllConcertsListFromKopisAPI(page);
}catch (Exception e){
log.error("공연 목록 저장 도중 오류 발생");
log.error("오류 내용 : " + e.getMessage());
return;
} finally {
concertRedisRepository.unlockSave(key);
}


concertRedisRepository.lockSave(key,"running...");
log.info("저장할 총 공연의 수: {}", totalConcertsList.size());
log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장");
try {
for (ConcertListElement performanceListElement : totalConcertsList) {
for(int i = savedIndex; i < totalConcertsList.size(); i++) {
ConcertListElement concertListElement = totalConcertsList.get(i);
// API에서 공연 상세 가져오기
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId());
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, concertListElement.getApiConcertId());
Thread.sleep(120);
ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail();

// 콘서트 위치 저장
// 콘서트 상세에서 저장할 콘서트 위치의 API ID 값 가져오기
String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id();
// 캐시로 사용하는 맵이나 DB에서 콘서트 위치가 있는지 확인하기
ConcertPlace concertPlace = concertPlaceMap.getOrDefault(concertPlaceAPiKey, placeRepository.getConcertPlaceByApiConcertPlaceId(concertPlaceAPiKey));
if (concertPlace == null) {
// 맵이나 DB에 없다면 API에서 해당 콘서트 위치를 가져와서 DB에 저장 후 캐시에 저장
ConcertPlaceDetailResponse concertPlaceDetailElement = getConcertPlaceDetailResponse(serviceKey, concertPlaceAPiKey);
Thread.sleep(120);
ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail();
concertPlace = concertPlaceDetail.getConcertPlace();
ConcertPlace savedConcertPlace = placeRepository.save(concertPlace);
concertPlaceMap.put(concertPlaceAPiKey, savedConcertPlace);
addedConcertPlaces++;
}

//콘서트 최고 금액, 최저 금액 처리.
TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice());

// 콘서트 저장
Concert concert = new Concert(
concertPlace,
concertDetail.getConcertName(),
concertDetail.getConcertDescription(),
dateStringToDateTime(concertDetail.getStartDate()),
dateStringToDateTime(concertDetail.getEndDate()),
null,
null,
ticketPrice.maxPrice,
ticketPrice.minPrice,
concertDetail.getPosterUrl(),
concertDetail.getArea(),
concertDetail.getApiConcertId()
);

Concert savedConcert = concertRepository.save(concert);
addedConcerts++;
// 캐시로 사용하는 맵이나 DB에서 콘서트 위치가 있는지 확인하기 -> 없으면 저장
ConcertPlace concertPlace = getConcertPlaceOrSaveNewConcertPlace(concertPlaceMap, concertPlaceAPiKey);

addedConcertPlaces = concertPlaceMap.size();
// 공연 저장
Concert savedConcert = saveConcert(concertPlace, concertDetail);
// 공연 예매처 저장
addedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert);
// 공연 이미지 저장
addedConcertImages += saveConcertImages(concertDetail, savedConcert);

addedConcerts++;
savedIndex++;
}
} catch (Exception e) {
log.error("개별 공연 세부 내용 저장 도중 오류 발생");
log.error("오류 내용 : " + e.getMessage());
e.printStackTrace();
return ;
} finally {
concertRedisRepository.unlockSave(key);
}
ConcertUpdateTime concertUpdateTime = new ConcertUpdateTime(now);
concertUpdateTimeRepository.save(concertUpdateTime);
savedIndex = 0;
log.info(now + "시 기준 " + totalConcertsList.size() + "개의 공연 데이터 저장 완료!");
long endNs = System.currentTimeMillis();
long durationSec = ((endNs - startNs) / 1000);
log.info(durationSec/60 + "분, " + durationSec % 60 + "초 소요되었습니다." );
concertRedisRepository.unlockSave(key);
}

@Transactional
Expand Down Expand Up @@ -225,71 +201,39 @@ public SetResultResponse updateConcertData() throws InterruptedException {
Thread.sleep(200);
}
log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장");

savedIndex = 0;
for(int i = savedIndex; i < totalConcertsList.size(); i++) {}
for (ConcertListElement performanceListElement : totalConcertsList) {
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId());
ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail();
log.info("concert detail: " + concertDetailResponse.getConcertDetail());

// 콘서트 위치 저장 -> 추후 메소드 추출하기?
// 콘서트 위치 탐색 또는 추가
String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id();
ConcertPlace concertPlace;
concertPlace = concertPlaceMap.getOrDefault(concertPlaceAPiKey, placeRepository.getConcertPlaceByApiConcertPlaceId(concertPlaceAPiKey));
if (concertPlace == null) {
// 콘서트 장소가 null일시
ConcertPlaceDetailResponse concertPlaceDetailElement = getConcertPlaceDetailResponse(serviceKey, concertPlaceAPiKey);
ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail();
concertPlace = concertPlaceDetail.getConcertPlace();
ConcertPlace savedConcertPlace = placeRepository.save(concertPlace);
concertPlaceMap.put(concertPlaceAPiKey, savedConcertPlace);
addedConcertPlaces ++;
log.info("concert place saved: " + savedConcertPlace);
}

//콘서트 최고 금액, 최저 금액 처리.
TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice());
ConcertPlace concertPlace = getConcertPlaceOrSaveNewConcertPlace(concertPlaceMap, concertPlaceAPiKey);

// 콘서트 저장
Concert concert = concertRepository.getConcertByApiConcertId(concertDetail.getApiConcertId());

if (concert == null) {
concert = new Concert(
concertPlace,
concertDetail.getConcertName(),
concertDetail.getConcertDescription(),
dateStringToDateTime(concertDetail.getStartDate()),
dateStringToDateTime(concertDetail.getEndDate()),
null,
null,
ticketPrice.maxPrice,
ticketPrice.minPrice,
concertDetail.getPosterUrl(),
concertDetail.getArea(),
concertDetail.getApiConcertId()
);

Concert savedConcert = concertRepository.save(concert);
addedConcerts ++;
// 새 공연 저장
Concert savedConcert = saveConcert(concertPlace,concertDetail);
// 공연 예매처 저장
addedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert);
// 공연 이미지 저장
addedConcertImages += saveConcertImages(concertDetail, savedConcert);
addedConcerts ++;
} else {
concert = concert.updateByAPI(
concertPlace,
concertDetail.getConcertDescription(),
dateStringToDateTime(concertDetail.getStartDate()),
dateStringToDateTime(concertDetail.getEndDate()),
ticketPrice.maxPrice,
ticketPrice.minPrice,
concertDetail.getPosterUrl()
);

Concert savedConcert = concertRepository.save(concert);
updatedConcerts ++;
// 공연 데이터 갱신 후 저장
Concert savedConcert = updateConcert(concert, concertPlace, concertDetail);
// 기존에 저장되어 있던 연관 테이블 데이터 삭제
ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId());
imageRepository.deleteByConcertId(savedConcert.getConcertId());
// 갱신된 데이터로 연관 테이블 저장
updatedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert);
updatedConcertImages += saveConcertImages(concertDetail, savedConcert);
updatedConcerts ++;
}

Thread.sleep(300);
Expand All @@ -303,6 +247,7 @@ public SetResultResponse updateConcertData() throws InterruptedException {
return new SetResultResponse(addedConcerts,updatedConcerts,addedConcertPlaces,updatedConcertPlaces,addedConcertImages,updatedConcertImages,addedTicketOffices,updatedTicketOffices);
}


@Transactional
public void concertUpdateByKopisApi(Long concertId){
// 해당 콘서트 ID로 콘서트 객체 찾기
Expand All @@ -323,11 +268,58 @@ public void concertUpdateByKopisApi(Long concertId){
ConcertPlace savedConcertPlace = placeRepository.save(concertPlace);
log.info("concert place saved: " + savedConcertPlace);
}
// 공연의 정보를 새로운 정보로 변경
Concert savedConcert = updateConcert(concert, concertPlace, concertDetail);
// 기존에 저장되어 있던 연관 테이블 데이터 삭제
ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId());
imageRepository.deleteByConcertId(savedConcert.getConcertId());
// 새로 받아온 예매처, 이미지 데이터 저장
saveConcertTicketOffice(concertDetail, savedConcert);
saveConcertImages(concertDetail, savedConcert);


}


private List<ConcertListElement> getAllConcertsListFromKopisAPI(int page) throws InterruptedException {
List<ConcertListElement> totalConcertsList = new ArrayList<>();
while (true) {
// 콘서트 목록 받아오기
ConcertListResponse plr = getConcertListResponse(serviceKey, sdate, edate, page);
page++;
// 더 이상 받아올 콘서트 목록이 없으면 멈춤
if (plr.getConcertList() == null) break;
// 콘서트 요소를 콘서트 목록에서 꺼내서 더하기
totalConcertsList.addAll(plr.getConcertList());
log.info("Total Concert List: {}", totalConcertsList.size() + "개의 데이터 가져오는중...");
Thread.sleep(120);
}
return totalConcertsList;
}

// 표 최고가, 최저가 구분
// 공연 데이터 저장
private Concert saveConcert(ConcertPlace concertPlace, ConcertDetailElement concertDetail) {
TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice());
Concert concert = new Concert(
concertPlace,
concertDetail.getConcertName(),
concertDetail.getConcertDescription(),
dateStringToDateTime(concertDetail.getStartDate()),
dateStringToDateTime(concertDetail.getEndDate()),
null,
null,
ticketPrice.maxPrice,
ticketPrice.minPrice,
concertDetail.getPosterUrl(),
concertDetail.getArea(),
concertDetail.getApiConcertId()
);
return concertRepository.save(concert);
}

// 공연의 정보를 새로운 정보로 변경
// 공연 정보를 새로운 정보로 갱신해서 DB에 저장
private Concert updateConcert(Concert concert, ConcertPlace concertPlace, ConcertDetailElement concertDetail) {
TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice());
concert = concert.updateByAPI(
concertPlace,
concertDetail.getConcertDescription(),
Expand All @@ -338,16 +330,24 @@ public void concertUpdateByKopisApi(Long concertId){
concertDetail.getPosterUrl()
);

// 공연 저장
Concert savedConcert = concertRepository.save(concert);
// 기존에 저장되어 있던 연관 테이블 데이터 삭제
ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId());
imageRepository.deleteByConcertId(savedConcert.getConcertId());
// 새로 받아온 예매처, 이미지 데이터 저장
saveConcertTicketOffice(concertDetail, savedConcert);
saveConcertImages(concertDetail, savedConcert);
return concertRepository.save(concert);
}

// 공연 장소를 주어진 map에서 찾고 없으면 DB에서 찾음, DB에서도 없으면 API에서 해당 데이터를 찾아서 저장 후 반환
private ConcertPlace getConcertPlaceOrSaveNewConcertPlace(Map<String, ConcertPlace> concertPlaceMap, String concertPlaceAPiKey) throws InterruptedException {
ConcertPlace concertPlace = concertPlaceMap.getOrDefault(concertPlaceAPiKey, placeRepository.getConcertPlaceByApiConcertPlaceId(concertPlaceAPiKey));

if (concertPlace == null) {
// 맵이나 DB에 없다면 API에서 해당 콘서트 위치를 가져와서 DB에 저장 후 캐시에 저장
ConcertPlaceDetailResponse concertPlaceDetailElement = getConcertPlaceDetailResponse(serviceKey, concertPlaceAPiKey);
Thread.sleep(120);
ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail();
concertPlace = concertPlaceDetail.getConcertPlace();
ConcertPlace savedConcertPlace = placeRepository.save(concertPlace);
concertPlaceMap.put(concertPlaceAPiKey, savedConcertPlace);
}

return concertPlace;
}

// 콘서트 예매처를 저장합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("test")
class Web79CodecreteBeApplicationTests {

@Test
Expand Down