From 4d161634ac004dd4a740766961a60f7eaddb9675 Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Fri, 19 Dec 2025 14:30:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=9D=91=EB=8B=B5=20=EC=A7=80?= =?UTF-8?q?=EC=97=B0=20=EC=8B=9C=EA=B0=84=20=EA=B0=9C=EC=84=A0=20->=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=20=ED=98=B8=EC=B6=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=B4=20=EC=9D=91=EB=8B=B5=20=ED=9B=84=20=EB=B0=B1=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConcertAdminController.java | 6 +- .../concerts/service/KopisApiService.java | 132 ++++++++++-------- 2 files changed, 80 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java index 3b274111..e5a135ba 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java @@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -30,8 +31,9 @@ public class ConcertAdminController { // todo : 인증 권한 추가하기 @Operation(summary = "초기 공연 정보 저장", description = "25년 12월부터 앞으로 6개월 이후까지의 전체 공연의 정보를 가져와서 저장합니다. 대략 10~12분 정도 시간이 소요됩니다.") @PostMapping("setConcertData") - public RsData setConcert() throws InterruptedException { - return RsData.success(kopisApiService.setConcertsList()); + public RsData setConcert() throws InterruptedException { + kopisApiService.setConcertsList(); + return RsData.success(HttpStatus.ACCEPTED,"저장 요청을 보냈습니다",null); } @Operation(summary = "공연 정보 갱신", description = "공연 정보를 직접 갱신합니다.") diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java index ad0a251e..6609507d 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java @@ -12,6 +12,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -28,6 +30,7 @@ @Slf4j @Service +@EnableAsync @EnableScheduling public class KopisApiService { // 공연예술통합 전산망 조회를 위한 서비스 클래스입니다. @@ -44,7 +47,7 @@ public class KopisApiService { @Value("${kopis.api-key}") private String serviceKey; private LocalDate sdate = LocalDate.of(2025, 12, 1); - private LocalDate edate = LocalDate.now().plusMonths(6); + private LocalDate edate = LocalDate.now().plusYears(1); private final RestClient restClient; @@ -60,9 +63,14 @@ public KopisApiService(ConcertRepository concertRepository, ConcertPlaceReposito .build(); } + @Async @Transactional - public SetResultResponse setConcertsList() throws InterruptedException { + public void setConcertsList() throws InterruptedException { // 최초 시작 시간 저장 + if(concertRepository.count() != 0) { + log.error("이미 최초 저장이 되었습니다!. UpdateConcert를 통해 데이터를 갱신해주십시오!"); + return; + } LocalDateTime now = LocalDateTime.now(); Long startNs = System.currentTimeMillis(); @@ -79,74 +87,86 @@ public SetResultResponse setConcertsList() throws InterruptedException { int addedConcertImages = 0; int page = 1; - while (true) { - // 콘서트 목록 받아오기 - plr = getConcertListResponse(serviceKey, sdate, edate, page); - page++; - // 더 이상 받아올 콘서트 목록이 없으면 멈춤 - if (plr.getConcertList() == null) break; - // 콘서트 요소를 콘서트 목록에서 꺼내서 더하기 - for (ConcertListElement p : plr.getConcertList()) { - totalConcertsList.add(p); + 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(200); } - log.info("Total Concert List: {}", totalConcertsList.size() + "개의 데이터 가져오는중..."); - Thread.sleep(200); + + }catch (Exception e){ + log.error("공연 목록 저장 도중 오류 발생"); + log.error("오류 내용 : " + e.getMessage()); + return; } - log.info("Total concert list size: {}", totalConcertsList.size()); - log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장"); - for (ConcertListElement performanceListElement : totalConcertsList) { - ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId()); - 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); - ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail(); - concertPlace = concertPlaceDetail.getConcertPlace(); - ConcertPlace savedConcertPlace = placeRepository.save(concertPlace); - concertPlaceMap.put(concertPlaceAPiKey, savedConcertPlace); - addedConcertPlaces++; - } + log.info("저장할 총 공연의 수: {}", totalConcertsList.size()); + log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장"); + try { + for (ConcertListElement performanceListElement : totalConcertsList) { + ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId()); + 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); + ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail(); + concertPlace = concertPlaceDetail.getConcertPlace(); + ConcertPlace savedConcertPlace = placeRepository.save(concertPlace); + concertPlaceMap.put(concertPlaceAPiKey, savedConcertPlace); + addedConcertPlaces++; + } - //콘서트 최고 금액, 최저 금액 처리. - TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice()); + //콘서트 최고 금액, 최저 금액 처리. + TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice()); - // 콘서트 저장 - Concert concert = new Concert( - concertPlace, - concertDetail.getConcertName(), - concertDetail.getConcertDescription(), - dateStringToDateTime(concertDetail.getStartDate()), - dateStringToDateTime(concertDetail.getEndDate()), - null, - ticketPrice.maxPrice, - ticketPrice.minPrice, - concertDetail.getPosterUrl(), - concertDetail.getApiConcertId() - ); + // 콘서트 저장 + Concert concert = new Concert( + concertPlace, + concertDetail.getConcertName(), + concertDetail.getConcertDescription(), + dateStringToDateTime(concertDetail.getStartDate()), + dateStringToDateTime(concertDetail.getEndDate()), + null, + ticketPrice.maxPrice, + ticketPrice.minPrice, + concertDetail.getPosterUrl(), + concertDetail.getApiConcertId() + ); - Concert savedConcert = concertRepository.save(concert); - addedConcerts++; + Concert savedConcert = concertRepository.save(concert); + addedConcerts++; - addedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert); - addedConcertImages += saveConcertImages(concertDetail, savedConcert); + addedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert); + addedConcertImages += saveConcertImages(concertDetail, savedConcert); - Thread.sleep(300); + Thread.sleep(300); + } + } catch (Exception e) { + log.error("개별 공연 세부 내용 저장 도중 오류 발생"); + log.error("오류 내용 : " + e.getMessage()); + return ; } - ConcertUpdateTime concertUpdateTime = new ConcertUpdateTime(now); concertUpdateTimeRepository.save(concertUpdateTime); log.info(now + "시 기준 " + totalConcertsList.size() + "개의 공연 데이터 저장 완료!"); long endNs = System.currentTimeMillis(); long durationSec = ((endNs - startNs) / 1000); log.info(durationSec/60 + "분, " + durationSec % 60 + "초 소요되었습니다." ); - return new SetResultResponse(addedConcerts,0,addedConcertPlaces,0,addedConcertImages,0,addedTicketOffices,0); } @@ -157,8 +177,8 @@ public SetResultResponse updateConcertData() throws InterruptedException { ConcertUpdateTime concertUpdateTime = concertUpdateTimeRepository.getReferenceById(1L); LocalDate lastUpdatedDate = concertUpdateTime.getUpdateTime().toLocalDate(); ConcertUpdateTime updatedTime = concertUpdateTime.setUpdateTime(LocalDateTime.now()); - LocalDate sdate = lastUpdatedDate.plusDays(1); - LocalDate edate = LocalDate.now().withDayOfMonth(1).plusMonths(6); + LocalDate sdate = lastUpdatedDate; + LocalDate edate = LocalDate.now().plusYears(1); int addedConcerts = 0; int updatedConcerts = 0; From 16330993fb1e58ec26dad6ce8ca4fd9534d0e02d Mon Sep 17 00:00:00 2001 From: Creamcheesepie Date: Fri, 19 Dec 2025 14:58:50 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix=20:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=B4=20=EB=A9=80=ED=8B=B0=EC=8A=A4?= =?UTF-8?q?=EB=A0=88=EB=93=9C=EC=97=90=EC=84=9C=20=EB=8B=A8=EC=9D=BC=20=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ConcertRedisRepository.java | 32 +++++++++++++++++++ .../concerts/service/KopisApiService.java | 18 +++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java new file mode 100644 index 00000000..e5456d9d --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java @@ -0,0 +1,32 @@ +package com.back.web7_9_codecrete_be.domain.concerts.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.concurrent.TimeUnit; + +@Repository +@RequiredArgsConstructor +public class ConcertRedisRepository { + private final RedisTemplate redisTemplate; + + private static final String LOCK_FLAG_PREFIX = "initLoad: "; + + public void lockSave(String key, String value) { + redisTemplate.opsForValue().set( + LOCK_FLAG_PREFIX + key, + value, + 900, + TimeUnit.SECONDS); + } + + public String lockGet(String key) { + return redisTemplate.opsForValue().get(LOCK_FLAG_PREFIX + key); + } + + public void unlockSave(String key) { + redisTemplate.delete(LOCK_FLAG_PREFIX + key); + } + +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java index 6609507d..52bbe76b 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java @@ -44,6 +44,8 @@ public class KopisApiService { private final ConcertUpdateTimeRepository concertUpdateTimeRepository; + private final ConcertRedisRepository concertRedisRepository; + @Value("${kopis.api-key}") private String serviceKey; private LocalDate sdate = LocalDate.of(2025, 12, 1); @@ -51,12 +53,13 @@ public class KopisApiService { private final RestClient restClient; - public KopisApiService(ConcertRepository concertRepository, ConcertPlaceRepository placeRepository, TicketOfficeRepository ticketOfficeRepository, ConcertImageRepository imageRepository, ConcertUpdateTimeRepository concertUpdateTimeRepository) { + public KopisApiService(ConcertRepository concertRepository, ConcertPlaceRepository placeRepository, TicketOfficeRepository ticketOfficeRepository, ConcertImageRepository imageRepository, ConcertUpdateTimeRepository concertUpdateTimeRepository,ConcertRedisRepository concertRedisRepository) { this.concertRepository = concertRepository; this.placeRepository = placeRepository; this.ticketOfficeRepository = ticketOfficeRepository; this.imageRepository = imageRepository; this.concertUpdateTimeRepository = concertUpdateTimeRepository; + this.concertRedisRepository = concertRedisRepository; this.restClient = RestClient.builder() .baseUrl("https://kopis.or.kr/openApi/restful") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -67,10 +70,20 @@ public KopisApiService(ConcertRepository concertRepository, ConcertPlaceReposito @Transactional public void setConcertsList() throws InterruptedException { // 최초 시작 시간 저장 - if(concertRepository.count() != 0) { + if(concertUpdateTimeRepository.count() != 0) { log.error("이미 최초 저장이 되었습니다!. UpdateConcert를 통해 데이터를 갱신해주십시오!"); return; } + String key = "init"; + + String value = concertRedisRepository.lockGet(key); + if(value != null) { + log.error("이미 실행중인 스레드입니다."); + return; + } else { + concertRedisRepository.lockSave(key,"running..."); + } + LocalDateTime now = LocalDateTime.now(); Long startNs = System.currentTimeMillis(); @@ -167,6 +180,7 @@ public void setConcertsList() throws InterruptedException { long endNs = System.currentTimeMillis(); long durationSec = ((endNs - startNs) / 1000); log.info(durationSec/60 + "분, " + durationSec % 60 + "초 소요되었습니다." ); + concertRedisRepository.unlockSave(key); }