Skip to content
46 changes: 35 additions & 11 deletions src/main/java/com/opendata/domain/address/cache/AddressCache.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,61 @@
package com.opendata.domain.address.cache;

import com.opendata.domain.address.entity.Address;
import com.opendata.domain.address.entity.Area;
import com.opendata.domain.address.repository.AddressRepository;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;


import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
@RequiredArgsConstructor
public class AddressCache {

private final AddressRepository addressRepository;
private Map<String, Address> cache;

private Map<String, Address> byKorName;

private Map<String, List<Address>> byAreaId;

@PostConstruct
public void init() {
cache = new ConcurrentHashMap<>(addressRepository.findAll().stream()
.collect(Collectors.toUnmodifiableMap(Address::getAddressKorNm, Function.identity())));
// 1) 전체 Address 로드
List<Address> all = addressRepository.findAll();

// 2) 한글명 캐시
byKorName = all.stream()
.filter(a -> a.getAddressKorNm() != null)
.collect(Collectors.toConcurrentMap(
Address::getAddressKorNm,
Function.identity(),
(a, b) -> a
));

// 3) area_id 캐시 (area_id → List<Address>)
byAreaId = all.stream()
.filter(a -> a.getArea() != null)
.collect(Collectors.groupingByConcurrent(
a -> extractAreaId(a.getArea())
));
}

private String extractAreaId(Area area) {
return area.getAreaCodeId().toString();
}

public Address getByKorName(String name) {
return cache.get(name);
return byKorName.get(name);
}
public List<Address> getByAreaId(String areaId) {
return byAreaId.getOrDefault(areaId, Collections.emptyList());
}

public List<Address> getAll() {
return new ArrayList<>(cache.values());
return new ArrayList<>(byKorName.values());
}

}
}
5 changes: 5 additions & 0 deletions src/main/java/com/opendata/domain/address/entity/Address.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.opendata.domain.address.entity;

import com.opendata.domain.tourspot.entity.TourSpotRelated;
import com.opendata.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

import java.util.List;


@Entity
Expand Down Expand Up @@ -34,4 +36,7 @@ public class Address extends BaseEntity {

@Column(name = "longitude")
private Double longitude;

@OneToMany(mappedBy = "address", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TourSpotRelated> relatedTourSpots;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20); // 기존 10 → 30
executor.setMaxPoolSize(40); // 기존 50 → 100
executor.setCorePoolSize(10); // 기존 10 → 30
executor.setMaxPoolSize(10); // 기존 50 → 100
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Async-"); // 스레드 이름 설정
executor.setAllowCoreThreadTimeOut(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ public ResponseEntity<Void> getArea() {
return ResponseEntity.ok().build();
}


@GetMapping("/related")
public ResponseEntity<Void> getRelated() {
tourSpotService.saveRelatedTourspot();
return ResponseEntity.ok().build();
}

@GetMapping("/{tourspotId}")
public ResponseEntity<ApiResponse<TourSpotDetailResponse>> getTourSpotDetail(@PathVariable("tourspotId") Long tourspotId) throws JsonProcessingException {
return ResponseEntity.ok(ApiResponse.onSuccess(tourSpotService.combineTourSpotDetail(tourspotId)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,133 @@

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.core.JsonToken;
import lombok.Data;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class TourSpotRelatedDto
{
public class TourSpotRelatedDto {

@JsonProperty("response")
private Response response;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Response {
@JsonProperty("header")
private Header header;

@JsonProperty("body")
private Body body;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Header {
@JsonProperty("resultCode")
private String resultCode;

@JsonProperty("resultMsg")
private String resultMsg;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Body {
@JsonProperty("items")
@JsonDeserialize(using = ItemsOrEmptyStringDeserializer.class)
private Items items;

@JsonProperty("numOfRows")
private Integer numOfRows;

@JsonProperty("pageNo")
private Integer pageNo;

@JsonProperty("totalCount")
private Integer totalCount;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Items {
@JsonProperty("item")
private List<AddressItem> itemList;
private List<AddressItem> itemList = Collections.emptyList();
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class AddressItem {
@JsonProperty("baseYmd")
private String baseYmd;
// 예: 202504
@JsonProperty("baseYm")
private String baseYm;

@JsonProperty("tAtsNm")
private String tAtsNm;
// 좌표 (문자열로 두면 파싱 실패 리스크↓)
@JsonProperty("mapX")
private String mapX;

@JsonProperty("mapY")
private String mapY;

// 광역단위 (예: 11, 서울특별시)
@JsonProperty("areaCd")
private String areaCd;

@JsonProperty("areaNm")
private String areaNm;

// 자치구 (예: 11140, 중구)
@JsonProperty("signguCd")
private String signguCd;

@JsonProperty("signguNm")
private String signguNm;

@JsonProperty("rlteTatsNm")
private String rlteTatsNm;
// 거점 코드/명
@JsonProperty("hubTatsCd")
private String hubTatsCd;

@JsonProperty("hubTatsNm")
private String hubTatsNm;

// 분류
@JsonProperty("hubCtgryLclsNm")
private String hubCtgryLclsNm;

@JsonProperty("hubCtgryMclsNm")
private String hubCtgryMclsNm;

// 순위
@JsonProperty("hubRank")
private String hubRank;
}

/**
* "items": "" 처럼 빈 문자열이 내려올 때를 대비한 디시리얼라이저
* "" → 빈 Items (itemList = [])
* 객체 → 정상 파싱
*/
public static class ItemsOrEmptyStringDeserializer extends JsonDeserializer<Items> {
@Override
public Items deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonToken t = p.currentToken();
if (t == JsonToken.VALUE_STRING) {
// "", " " 모두 빈 리스트로 취급
String s = p.getText();
Items i = new Items();
i.setItemList(Collections.emptyList());
return i;
}
// 정상 객체일 경우 표준 역직렬화
return p.getCodec().readValue(p, Items.class);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ public class TourSpot extends BaseEntity {
@OneToMany(mappedBy = "tourspot", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TourSpotMonthlyCongestion> monthlyCongestions;

@OneToMany(mappedBy = "tourspot", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TourSpotRelated> relatedSpots;


protected TourSpot(Address address, String tourspotNm){
Expand All @@ -70,10 +68,6 @@ public void updateMonthlyCongestions(List<TourSpotMonthlyCongestion> newOnes) {
this.monthlyCongestions.clear();
newOnes.forEach(this::addMonthlyCongestion);
}
public void updateTourSpotRelated(List<TourSpotRelated> newOnes) {
this.relatedSpots.clear();
newOnes.forEach(this::addRelatedSpots);
}

public void addFutureCongestion(TourSpotFutureCongestion congestion) {
this.futureCongestions.add(congestion);
Expand All @@ -85,8 +79,5 @@ public void addCurrentCongestion(TourSpotCurrentCongestion congestion) {

public void addEvent(TourSpotEvent event) { this.events.add(event); }

public void addRelatedSpots(TourSpotRelated newOnes) {
this.relatedSpots.add(newOnes);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;

@MappedSuperclass
@Getter
@Setter
public abstract class TourSpotAssociated extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tourspot_id")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.opendata.domain.tourspot.entity;

import com.opendata.domain.address.entity.Address;
import jakarta.persistence.*;
import lombok.*;

Expand All @@ -9,13 +10,26 @@
@Table(name = "tourspot_related")
@NoArgsConstructor
@AllArgsConstructor
public class TourSpotRelated extends TourSpotAssociated
{
public class TourSpotRelated{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "related_tourspot_id", nullable = false)
private TourSpot relatedTourSpot;
@JoinColumn(name = "address_id")
private Address address;

private String tourSpotCode;

private String tourSpotName;

private String largeCategory;

private String middleCategory;

private double mapX;

private double mapY;


}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.opendata.domain.tourspot.mapper;

import com.opendata.domain.address.entity.Address;
import com.opendata.domain.tourspot.dto.MonthlyCongestionDto;
import com.opendata.domain.tourspot.dto.TourSpotRelatedDto;
import com.opendata.domain.tourspot.entity.TourSpot;
Expand All @@ -16,15 +17,19 @@
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface TourSpotRelatedMapper {

@Mapping(target = "relatedTourSpot", source = "relatedTourSpot")
@Mapping(target = "id", ignore = true) // PK는 자동 생성
TourSpotRelated toEntity(
@Context TourSpot currentTourSpot,
TourSpot relatedTourSpot
@Context Address address,
String tourSpotCode,
String tourSpotName,
String largeCategory,
String middleCategory,
String mapX,
String mapY
);

@AfterMapping
default void assignTourSpot(@MappingTarget TourSpotRelated entity,
@Context TourSpot currentTourSpot) {
entity.assignTourSpot(currentTourSpot);
default void assignAddress(@MappingTarget TourSpotRelated tourSpotRelated, @Context Address address){
tourSpotRelated.setAddress(address);
}
}
Loading