Skip to content

Commit

Permalink
Merge pull request #509 from woowacourse-teams/feature/#493
Browse files Browse the repository at this point in the history
[feat] 지역 조회 API 구현 (#493)
  • Loading branch information
Jaeyoung22 authored Oct 19, 2023
2 parents 165d021 + bf52688 commit 2e8fc8d
Show file tree
Hide file tree
Showing 22 changed files with 792 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package dev.tripdraw.area.application;

import dev.tripdraw.area.domain.Area;
import dev.tripdraw.area.domain.AreaRepository;
import dev.tripdraw.area.dto.AreaReqeust;
import dev.tripdraw.area.dto.AreaResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Transactional
@Service
public class AreaService {

private final AreaRepository areaRepository;
private final OpenApiAreaDownloader openAPIAreaDownloader;

@Transactional(readOnly = true)
public AreaResponse read(String sido, String sigungu) {
if (sido.isBlank()) {
return readAllSidos();
}

if (sigungu.isBlank()) {
return readAllSigungusOf(sido);
}

return readAllUmdsOf(sido, sigungu);
}

private AreaResponse readAllSidos() {
List<Area> areas = areaRepository.findAll();
List<String> sidos = areas.stream()
.map(Area::sido)
.distinct()
.sorted()
.toList();

return AreaResponse.from(sidos);
}

private AreaResponse readAllSigungusOf(String sido) {
List<Area> areasOfSido = areaRepository.findBySido(sido);
List<String> sigungus = areasOfSido.stream()
.map(Area::sigungu)
.distinct()
.sorted()
.toList();

return AreaResponse.from(sigungus);
}

private AreaResponse readAllUmdsOf(String sido, String sigungu) {
List<Area> areasOfSigunguAndSido = areaRepository.findBySidoAndSigungu(sido, sigungu);
List<String> umds = areasOfSigunguAndSido.stream()
.map(Area::umd)
.distinct()
.sorted()
.toList();

return AreaResponse.from(umds);
}

public void create(List<Area> areas) {
long count = areaRepository.count();
if (count != 0) {
return;
}

areaRepository.deleteAllInBatch();
areaRepository.saveAll(areas);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.tripdraw.area.application;

import dev.tripdraw.area.domain.Area;
import dev.tripdraw.area.dto.AreaReqeust;
import dev.tripdraw.area.dto.AreaResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class AreaServiceFacade {

private final AreaService areaService;
private final OpenApiAreaDownloader openApiAreaDownloader;

public AreaResponse read(String sido, String sigungu) {
return areaService.read(sido, sigungu);
}

@Scheduled(cron = "0 0 0 1 * ?") // 매달 1일에 업데이트 한다는 CRON 표현식
public void create() {
List<Area> areas = openApiAreaDownloader.download();
areaService.create(areas);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package dev.tripdraw.area.application;

import dev.tripdraw.area.domain.Area;
import dev.tripdraw.area.dto.OpenApiAccessTokenResponse;
import dev.tripdraw.area.dto.OpenApiAreaResponse;
import dev.tripdraw.area.dto.OpenApiTotalAreaResponse;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

@RequiredArgsConstructor
@Component
public class OpenApiAreaDownloader {

private static final String AREA_API_DOMAIN = "https://sgisapi.kostat.go.kr";
private static final String AREA_CODE_NAME = "cd";
private static final String ACCESS_TOKEN_NAME = "accessToken";
private static final String SERVICE_KEY_NAME = "consumer_key";
private static final String SECRET_KEY_NAME = "consumer_secret";
private static final String AUTHENTICATION_URI = "/OpenAPI3/auth/authentication.json";
private static final String AREA_URI = "/OpenAPI3/addr/stage.json";

private final RestTemplate restTemplate;

@Value("${open.api.area.service}")
private String serviceSecret;

@Value("${open.api.area.key}")
private String keySecret;

public List<Area> download() {
String accessToken = requestAccessToken();
return downloadAreas(accessToken);
}

private String requestAccessToken() {
URI authenticationUri = UriComponentsBuilder.fromUriString(AREA_API_DOMAIN)
.path(AUTHENTICATION_URI)
.queryParam(SERVICE_KEY_NAME, serviceSecret)
.queryParam(SECRET_KEY_NAME, keySecret)
.encode()
.build()
.toUri();

OpenApiAccessTokenResponse accessTokenResponse = restTemplate.getForObject(
authenticationUri,
OpenApiAccessTokenResponse.class
);

return accessTokenResponse.result().get(ACCESS_TOKEN_NAME);
}

private List<Area> downloadAreas(String accessToken) {
List<Area> allAreas = new ArrayList<>();

List<OpenApiAreaResponse> sidoResponses = requestAreas(accessToken, null);
for (OpenApiAreaResponse sidoResponse : sidoResponses) {
fetchSigunguAndUmd(accessToken, allAreas, sidoResponse);
}
return allAreas;
}

private List<OpenApiAreaResponse> requestAreas(String accessToken, String code) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(AREA_API_DOMAIN)
.path(AREA_URI)
.queryParam(ACCESS_TOKEN_NAME, accessToken);

if (code != null) {
uriComponentsBuilder.queryParam(AREA_CODE_NAME, code);
}

URI areaUri = uriComponentsBuilder.encode()
.build()
.toUri();

OpenApiTotalAreaResponse totalAreaResponse = restTemplate.getForObject(areaUri, OpenApiTotalAreaResponse.class);
return totalAreaResponse.result();
}

private void fetchSigunguAndUmd(
String accessToken,
List<Area> allAreas,
OpenApiAreaResponse sidoResponse
) {
List<OpenApiAreaResponse> sigunguResponses = requestAreas(accessToken, sidoResponse.code());
for (OpenApiAreaResponse sigunguResponse : sigunguResponses) {
String sigungu = sigunguResponse.address();
List<OpenApiAreaResponse> umdResponses = requestAreas(accessToken, sigunguResponse.code());
addAreas(allAreas, sidoResponse.address(), sigungu, umdResponses);
}
}

private void addAreas(
List<Area> allAreas,
String sido,
String sigungu,
List<OpenApiAreaResponse> umdResponses
) {
for (OpenApiAreaResponse umdResponse : umdResponses) {
String umd = umdResponse.address();
allAreas.add(new Area(sido, sigungu, umd));
}
}
}
68 changes: 68 additions & 0 deletions backend/src/main/java/dev/tripdraw/area/domain/Area.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package dev.tripdraw.area.domain;

import static jakarta.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;

import dev.tripdraw.common.entity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.util.Objects;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Accessors(fluent = true)
@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
public class Area extends BaseEntity {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "area_id")
private Long id;

@Column(name = "sido", nullable = false)
private String sido;

@Column(name = "sigungu", nullable = false)
private String sigungu;

@Column(name = "umd", nullable = false) // 읍,면,동
private String umd;

public Area(String sido, String sigungu, String umd) {
this(null, sido, sigungu, umd);
}

public Area(Long id, String sido, String sigungu, String umd) {
this.id = id;
this.sido = sido;
this.sigungu = sigungu;
this.umd = umd;
}

public String toFullAddress() {
return sido + " " + sigungu + " " + umd;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Area area = (Area) o;
return Objects.equals(sido, area.sido) && Objects.equals(sigungu, area.sigungu)
&& Objects.equals(umd, area.umd);
}

@Override
public int hashCode() {
return Objects.hash(sido, sigungu, umd);
}
}
11 changes: 11 additions & 0 deletions backend/src/main/java/dev/tripdraw/area/domain/AreaRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.tripdraw.area.domain;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AreaRepository extends JpaRepository<Area, Long> {

List<Area> findBySido(String sido);

List<Area> findBySidoAndSigungu(String sido, String sigungu);
}
4 changes: 4 additions & 0 deletions backend/src/main/java/dev/tripdraw/area/dto/AreaReqeust.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package dev.tripdraw.area.dto;

public record AreaReqeust(String sido, String sigungu) {
}
9 changes: 9 additions & 0 deletions backend/src/main/java/dev/tripdraw/area/dto/AreaResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.tripdraw.area.dto;

import java.util.List;

public record AreaResponse(List<String> areas) {
public static AreaResponse from(List<String> areas) {
return new AreaResponse(areas);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.tripdraw.area.dto;

import java.util.Map;

public record OpenApiAccessTokenResponse(
String id,
Map<String, String> result,
String errMsg,
String errCd,
String trId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.tripdraw.area.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record OpenApiAreaResponse(
@JsonProperty("cd")
String code,

@JsonProperty("addr_name")
String address,

@JsonProperty("full_addr")
String fullAddress,

@JsonProperty("x_coor")
String longitude,

@JsonProperty("y_coor")
String latitude,

@JsonProperty("pg")
String page
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.tripdraw.area.dto;

import java.util.List;

public record OpenApiTotalAreaResponse(String id, List<OpenApiAreaResponse> result) {
}
Loading

0 comments on commit 2e8fc8d

Please sign in to comment.