diff --git a/.env.sample b/.env.sample index 3505b912..6e3b542e 100644 --- a/.env.sample +++ b/.env.sample @@ -4,4 +4,6 @@ MYSQL_DATABASE=TALKKA_DB MYSQL_URL=jdbc:mysql://localhost:3306/TALKKA_DB?createDatabaseIfNotExist=true NAVER_CLIENT_ID=CLIENT_ID -NAVER_CLINET_SECRET=CLIENT_SECRET \ No newline at end of file +NAVER_CLINET_SECRET=CLIENT_SECRET + +SERVICE_KEY_1=SERVICE_KEY_1 \ No newline at end of file diff --git a/server/build.gradle b/server/build.gradle index bbcd336d..c27ae026 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -31,6 +31,9 @@ dependencies { // Thymeleaf 의 경우 테스트 혹은 관리자에서 추후에 사용하기 위해서 유지합니다. implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + // XML Parser + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/server/src/main/java/com/talkka/server/api/core/config/ApiKeyProperty.java b/server/src/main/java/com/talkka/server/api/core/config/ApiKeyProperty.java new file mode 100644 index 00000000..114ce92d --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/core/config/ApiKeyProperty.java @@ -0,0 +1,5 @@ +package com.talkka.server.api.core.config; + +public interface ApiKeyProperty { + String getApiKey(); +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/config/BusApiKeyProperty.java b/server/src/main/java/com/talkka/server/api/datagg/config/BusApiKeyProperty.java new file mode 100644 index 00000000..d5ac166e --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/config/BusApiKeyProperty.java @@ -0,0 +1,27 @@ +package com.talkka.server.api.datagg.config; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import com.talkka.server.api.core.config.ApiKeyProperty; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Component +@ConfigurationProperties(prefix = "openapi.public.bus.service-key") +public class BusApiKeyProperty implements ApiKeyProperty { + @Setter + private List keys; + + private int rollingKeyIndex = 0; + + @Override + public String getApiKey() { + rollingKeyIndex = (rollingKeyIndex + 1) % keys.size(); + return keys.get(rollingKeyIndex); + } +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationBodyDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationBodyDto.java new file mode 100644 index 00000000..ba14ecc8 --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationBodyDto.java @@ -0,0 +1,8 @@ +package com.talkka.server.api.datagg.dto; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "busLocationList") +public record BusLocationBodyDto(String endBus, String lowPlate, String plateNo, String plateType, + Integer remainSeatCnt, Long routeId, Long stationId, Integer stationSeq) { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationRespDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationRespDto.java new file mode 100644 index 00000000..4a1b363e --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationRespDto.java @@ -0,0 +1,14 @@ +package com.talkka.server.api.datagg.dto; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "response") +public record BusLocationRespDto(@JacksonXmlProperty(localName = "comMsgHeader") Map comMsgHeader, + @JacksonXmlProperty(localName = "msgHeader") Map msgHeader, + @JacksonXmlProperty(localName = "msgBody") List msgBody) + implements PublicBusApiResp { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteInfoBodyDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteInfoBodyDto.java new file mode 100644 index 00000000..f8859548 --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteInfoBodyDto.java @@ -0,0 +1,28 @@ +package com.talkka.server.api.datagg.dto; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "busRouteInfoItem") +public record BusRouteInfoBodyDto( + String companyId, // 운수업체 아이디 + String companyName, // 운수업체명 + String companyTel, // 운수업체 전화번호 + int districtCd, // 관할 지역 코드 + String downFirstTime, // 평일 종점 첫차 시간 + String downLastTime, // 평일 종점 막차 시간 + Long endStationId, // 종점 정류소 아이디 + String endStationName, // 종점 정류소명 + int peekAlloc, // 평일 최소 배차 시간 + String regionName, // 지역명 + Long routeId, // 노선 아이디 + String routeName, // 노선 번호 + int routeTypeCd, // 노선 유형 코드 + String routeTypeName, // 노선 유형명 + String startMobileNo, // 기점 정류소 번호 + Long startStationId, // 기점 정류소 아이디 + String startStationName, // 기점 정류소명 + String upFirstTime, // 평일 기점 첫차 시간 + String upLastTime, // 평일 기점 막차 시간 + int nPeekAlloc // 평일 최대 배차 시간 +) { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteInfoRespDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteInfoRespDto.java new file mode 100644 index 00000000..86fbe315 --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteInfoRespDto.java @@ -0,0 +1,14 @@ +package com.talkka.server.api.datagg.dto; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "response") +public record BusRouteInfoRespDto(@JacksonXmlProperty(localName = "comMsgHeader") Map comMsgHeader, + @JacksonXmlProperty(localName = "msgHeader") Map msgHeader, + @JacksonXmlProperty(localName = "msgBody") List msgBody) + implements PublicBusApiResp { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteSearchBodyDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteSearchBodyDto.java new file mode 100644 index 00000000..e0b28bdf --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteSearchBodyDto.java @@ -0,0 +1,15 @@ +package com.talkka.server.api.datagg.dto; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +/** + * @param routeId 노선 아이디 + * @param routeName 노선 번호 + * @param routeTypeCd 노선 유형 코드 + * @param routeTypeName 노선 유형명 + * @param regionName 노선 운행 지역 + * @param districtCd 노선 관할 지역 코드 */ +@JacksonXmlRootElement(localName = "busRouteList") +public record BusRouteSearchBodyDto(Long routeId, String routeName, String routeTypeCd, String routeTypeName, + String regionName, int districtCd) { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteSearchRespDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteSearchRespDto.java new file mode 100644 index 00000000..aa1407bf --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteSearchRespDto.java @@ -0,0 +1,14 @@ +package com.talkka.server.api.datagg.dto; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "response") +public record BusRouteSearchRespDto(@JacksonXmlProperty(localName = "comMsgHeader") Map comMsgHeader, + @JacksonXmlProperty(localName = "msgHeader") Map msgHeader, + @JacksonXmlProperty(localName = "msgBody") List msgBody) + implements PublicBusApiResp { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteStationBodyDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteStationBodyDto.java new file mode 100644 index 00000000..3098e42c --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteStationBodyDto.java @@ -0,0 +1,20 @@ +package com.talkka.server.api.datagg.dto; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +/** + * @param stationId 정류소 아이디 + * @param stationSeq 정류소 순번 + * @param stationName 정류소 명칭 + * @param mobileNo 정류소 고유 모바일 번호 + * @param regionName 정류소 위치 지역명 + * @param districtCd 노선 관할 지역 코드 + * @param centerYn 중앙차로 여부 (N: 일반, Y: 중앙차로) + * @param turnYn 회차점 여부 (N: 일반, Y: 회차점) + * @param x 정류소 X 좌표 + * @param y 정류소 Y 좌표 */ +@JacksonXmlRootElement(localName = "busRouteStationList") +public record BusRouteStationBodyDto(Long stationId, Integer stationSeq, String stationName, String mobileNo, + String regionName, String districtCd, String centerYn, String turnYn, Double x, + Double y) { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteStationRespDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteStationRespDto.java new file mode 100644 index 00000000..39625455 --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusRouteStationRespDto.java @@ -0,0 +1,14 @@ +package com.talkka.server.api.datagg.dto; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "response") +public record BusRouteStationRespDto(@JacksonXmlProperty(localName = "comMsgHeader") Map comMsgHeader, + @JacksonXmlProperty(localName = "msgHeader") Map msgHeader, + @JacksonXmlProperty(localName = "msgBody") List msgBody) + implements PublicBusApiResp { +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/PublicBusApiResp.java b/server/src/main/java/com/talkka/server/api/datagg/dto/PublicBusApiResp.java new file mode 100644 index 00000000..3125a1e8 --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/PublicBusApiResp.java @@ -0,0 +1,12 @@ +package com.talkka.server.api.datagg.dto; + +import java.util.List; +import java.util.Map; + +public interface PublicBusApiResp { + Map comMsgHeader(); + + Map msgHeader(); + + List msgBody(); +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/service/BusApiService.java b/server/src/main/java/com/talkka/server/api/datagg/service/BusApiService.java new file mode 100644 index 00000000..19ead347 --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/service/BusApiService.java @@ -0,0 +1,20 @@ +package com.talkka.server.api.datagg.service; + +import java.util.List; + +import com.talkka.server.api.datagg.dto.BusLocationBodyDto; +import com.talkka.server.api.datagg.dto.BusRouteInfoBodyDto; +import com.talkka.server.api.datagg.dto.BusRouteSearchBodyDto; +import com.talkka.server.api.datagg.dto.BusRouteStationBodyDto; + +public interface BusApiService { + List getSearchedRouteInfo(String routeName); + + List getRouteInfo(String apiRouteId); + + List getRouteStationInfo(String apiRouteId); + + List getBusLocationInfo(String apiRouteId); + + // List getBusStationArrivalInfo(String routeId); +} diff --git a/server/src/main/java/com/talkka/server/api/datagg/service/BusApiServiceImpl.java b/server/src/main/java/com/talkka/server/api/datagg/service/BusApiServiceImpl.java new file mode 100644 index 00000000..5d722a48 --- /dev/null +++ b/server/src/main/java/com/talkka/server/api/datagg/service/BusApiServiceImpl.java @@ -0,0 +1,88 @@ +package com.talkka.server.api.datagg.service; + +import java.net.URI; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import com.talkka.server.api.datagg.config.BusApiKeyProperty; +import com.talkka.server.api.datagg.dto.BusLocationBodyDto; +import com.talkka.server.api.datagg.dto.BusLocationRespDto; +import com.talkka.server.api.datagg.dto.BusRouteInfoBodyDto; +import com.talkka.server.api.datagg.dto.BusRouteInfoRespDto; +import com.talkka.server.api.datagg.dto.BusRouteSearchBodyDto; +import com.talkka.server.api.datagg.dto.BusRouteSearchRespDto; +import com.talkka.server.api.datagg.dto.BusRouteStationBodyDto; +import com.talkka.server.api.datagg.dto.BusRouteStationRespDto; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class BusApiServiceImpl implements BusApiService { + private final BusApiKeyProperty busApiKeyProperty; + private final RestTemplate restTemplate = new RestTemplate(); + private static final String host = "apis.data.go.kr"; + + @Override + public List getSearchedRouteInfo(String keyword) { + final String path = "/6410000/busrouteservice/getBusRouteList"; + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("keyword", keyword); + URI uri = this.getOpenApiURI(path, params); + ResponseEntity resp = restTemplate.getForEntity(uri, BusRouteSearchRespDto.class); + return resp.getBody().msgBody(); + } + + @Override + public List getRouteInfo(String apiRouteId) { + final String path = "/6410000/busrouteservice/getBusRouteInfoItem"; + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("routeId", apiRouteId); + URI uri = this.getOpenApiURI(path, params); + ResponseEntity resp = restTemplate.getForEntity(uri, BusRouteInfoRespDto.class); + return resp.getBody().msgBody(); + } + + @Override + public List getRouteStationInfo(String apiRouteId) { + final String path = "/6410000/busrouteservice/getBusRouteStationList"; + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("routeId", apiRouteId); + URI uri = this.getOpenApiURI(path, params); + ResponseEntity resp = restTemplate.getForEntity(uri, BusRouteStationRespDto.class); + return resp.getBody().msgBody(); + } + + @Override + public List getBusLocationInfo(String apiRouteId) { + final String path = "/6410000/buslocationservice/getBusLocationList"; + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("routeId", apiRouteId); + URI uri = this.getOpenApiURI(path, params); + ResponseEntity resp = restTemplate.getForEntity(uri, BusLocationRespDto.class); + return resp.getBody().msgBody(); + } + + // @Override + // public List getBusStationArrivalInfo(String routeId) { + // return null; + // } + + private URI getOpenApiURI(String path, MultiValueMap params) { + final var builder = new DefaultUriBuilderFactory(); + builder.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + return builder.builder() + .scheme("https") + .host(host) + .path(path) + .queryParam("serviceKey", this.busApiKeyProperty.getApiKey()) + .queryParams(params) + .build(); + } +} diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index feb3c15d..7a64a879 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -35,4 +35,12 @@ spring: user-name-attribute: response thymeleaf: prefix: classpath:/templates/ - suffix: .html \ No newline at end of file + suffix: .html + + +openapi: + public: + bus: + service-key: + keys: + - ${SERVICE_KEY_1} \ No newline at end of file diff --git a/server/src/test/java/com/talkka/server/api/datagg/service/BusApiServiceImplTest.java b/server/src/test/java/com/talkka/server/api/datagg/service/BusApiServiceImplTest.java new file mode 100644 index 00000000..f8b78b0f --- /dev/null +++ b/server/src/test/java/com/talkka/server/api/datagg/service/BusApiServiceImplTest.java @@ -0,0 +1,92 @@ +package com.talkka.server.api.datagg.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BusApiServiceImplTest { + @Autowired + private BusApiService busApiService; + + @Nested + @DisplayName("getSearchedRouteInfo method") + public class GetSearchedRouteInfo { + @Test + void routeName을_검색하면_해당_키워드를_가진_버스_노선_정보를_리스트로_반환한다() { + // given + String routeName = "78"; + + // when + var result = busApiService.getSearchedRouteInfo(routeName); + + // then + assertNotNull(result); + assertFalse(result.isEmpty()); + for (var dto : result) { + assertThat(dto.routeName()).contains(routeName); + } + } + } + + @Nested + @DisplayName("getRouteInfo method") + public class GetRouteInfo { + @Test + void routeId를_검색하면_해당_버스_노선_정보를_리스트로_반환한다() { + // given + String routeId = "200000150"; + + // when + var result = busApiService.getRouteInfo(routeId); + + // then + assertNotNull(result); + assertFalse(result.isEmpty()); + for (var dto : result) { + assertThat(dto.routeId()).isEqualTo(Long.parseLong(routeId)); + } + } + } + + @Nested + @DisplayName("getRouteStationInfo method") + public class GetRouteStationInfo { + @Test + void routeId를_검색하면_해당_버스_노선_정류장_정보를_리스트로_반환한다() { + // given + String routeId = "200000150"; + + // when + var result = busApiService.getRouteStationInfo(routeId); + + // then + assertNotNull(result); + assertFalse(result.isEmpty()); + System.out.println(result); + } + } + + @Nested + @DisplayName("getBusLocationInfo method") + public class GetBusLocationInfo { + @Test + void routeId를_검색하면_해당_버스_위치_정보를_리스트로_반환한다() { + // given + String routeId = "200000150"; + + // when + var result = busApiService.getBusLocationInfo(routeId); + + // then + assertNotNull(result); + assertFalse(result.isEmpty()); + System.out.println(result); + } + } +} \ No newline at end of file diff --git a/server/src/test/resources/application.yaml b/server/src/test/resources/application.yaml index e3ca91ba..6ce96e64 100644 --- a/server/src/test/resources/application.yaml +++ b/server/src/test/resources/application.yaml @@ -5,9 +5,9 @@ spring: property-naming-strategy: SNAKE_CASE datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ${MYSQL_URL} - username: ${MYSQL_USERNAME} - password: ${MYSQL_PASSWORD} + url: jdbc:mysql://localhost:3306/TALKKA_DB?createDatabaseIfNotExist=true + username: root + password: 1234 jpa: hibernate: ddl-auto: create @@ -21,7 +21,7 @@ spring: client: registration: naver: - client-id: ${NAVER_CLIENT_ID} + client-id: ${NAVER_CLIENT_ID} # NOTE: 실제 테스트 시에는 env 에 대해서 값을 삽입하고 사용할 것. client-secret: ${NAVER_CLINET_SECRET} redirect_uri: http://localhost:8080/login/oauth2/code/naver client-name: Naver @@ -37,4 +37,11 @@ spring: user-name-attribute: response thymeleaf: prefix: classpath:/templates/ - suffix: .html \ No newline at end of file + suffix: .html + +openapi: + public: + bus: + service-key: + keys: + - ${SERVICE_KEY_1} \ No newline at end of file