diff --git a/build.gradle b/build.gradle index 8d45e13..0da462d 100644 --- a/build.gradle +++ b/build.gradle @@ -21,10 +21,17 @@ repositories { mavenCentral() } +ext { + set('springCloudVersion', "2022.0.4") +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-batch' + implementation 'org.springframework.cloud:spring-cloud-starter' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.json:json:20230227' + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:3.1.2' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' @@ -33,7 +40,7 @@ dependencies { dependencyManagement { imports { - mavenBom "org.springframework.cloud:spring-cloud-dependencies:2022.0.3" + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } } diff --git a/src/main/java/com/catcher/batch/BatchApplication.java b/src/main/java/com/catcher/batch/BatchApplication.java index 61bea1e..893efe6 100644 --- a/src/main/java/com/catcher/batch/BatchApplication.java +++ b/src/main/java/com/catcher/batch/BatchApplication.java @@ -1,9 +1,15 @@ package com.catcher.batch; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableBatchProcessing +@EnableScheduling +@EnableFeignClients(basePackages = "com.catcher.batch.external") public class BatchApplication { public static void main(String[] args) { diff --git a/src/main/java/com/catcher/batch/annotation/CatcherJson.java b/src/main/java/com/catcher/batch/annotation/CatcherJson.java new file mode 100644 index 0000000..f7a8e83 --- /dev/null +++ b/src/main/java/com/catcher/batch/annotation/CatcherJson.java @@ -0,0 +1,13 @@ +package com.catcher.batch.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CatcherJson { + String path(); +} diff --git a/src/main/java/com/catcher/batch/core/converter/CatcherConverter.java b/src/main/java/com/catcher/batch/core/converter/CatcherConverter.java new file mode 100644 index 0000000..4b666c9 --- /dev/null +++ b/src/main/java/com/catcher/batch/core/converter/CatcherConverter.java @@ -0,0 +1,46 @@ +package com.catcher.batch.core.converter; + +import com.catcher.batch.annotation.CatcherJson; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONObject; +import org.springframework.stereotype.Component; + +@Component +public class CatcherConverter { + private final ObjectMapper objectMapper = new ObjectMapper(); + + public T parse(String jsonMessage, Class responseType) { + String path = getPath(responseType); + JSONObject jsonObject = getJsonObject(jsonMessage, path); + + try { + return objectMapper.readValue(jsonObject.toString(), responseType); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private String getPath(Class responseType) { + + CatcherJson annotation = responseType.getAnnotation(CatcherJson.class); + if(annotation == null) { + throw new IllegalStateException(); + } + return annotation.path(); + } + + private JSONObject getJsonObject(String json, String jsonPath) { + if(jsonPath == null) { + throw new IllegalStateException(); + } + + JSONObject jsonObject = new JSONObject(json); + String[] subPath = jsonPath.split("\\."); + + for (String path : subPath) { + jsonObject = jsonObject.getJSONObject(path); + } + return jsonObject; + } +} diff --git a/src/main/java/com/catcher/batch/external/TourApiServiceExternalCallService.java b/src/main/java/com/catcher/batch/external/TourApiServiceExternalCallService.java new file mode 100644 index 0000000..c94bc5d --- /dev/null +++ b/src/main/java/com/catcher/batch/external/TourApiServiceExternalCallService.java @@ -0,0 +1,15 @@ +package com.catcher.batch.external; + +import com.catcher.batch.external.config.CatcherFeignClientCommonConfig; +import com.catcher.batch.external.vo.request.TourApiRequest; +import com.catcher.batch.external.vo.response.TourApiResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient(name = "TOUR-API-SERVICE", url = "http://apis.data.go.kr/B551011/KorService1", configuration = CatcherFeignClientCommonConfig.class) +public interface TourApiServiceExternalCallService { + + @GetMapping("/searchFestival1") + TourApiResponse callTourList(@SpringQueryMap TourApiRequest request); +} diff --git a/src/main/java/com/catcher/batch/external/config/CatcherFeignClientCommonConfig.java b/src/main/java/com/catcher/batch/external/config/CatcherFeignClientCommonConfig.java new file mode 100644 index 0000000..6440173 --- /dev/null +++ b/src/main/java/com/catcher/batch/external/config/CatcherFeignClientCommonConfig.java @@ -0,0 +1,31 @@ +package com.catcher.batch.external.config; + +import com.catcher.batch.core.converter.CatcherConverter; +import feign.RequestInterceptor; +import feign.ResponseInterceptor; +import feign.Util; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +public class CatcherFeignClientCommonConfig { + + public static final String APPLICATION_FORM_URLENCODED_UTF8_VALUE = + MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=utf-8"; + + @Bean + public RequestInterceptor requestInterceptor() { + + return requestTemplate -> requestTemplate.header(HttpHeaders.CONTENT_TYPE, APPLICATION_FORM_URLENCODED_UTF8_VALUE); + } + + @Bean + public ResponseInterceptor responseInterceptor(CatcherConverter catcherConverter) { + + return invocationContext -> { + Class responseType = (Class) invocationContext.returnType(); + String responseBody = Util.toString(invocationContext.response().body().asReader()); + return catcherConverter.parse(responseBody, responseType); + }; + } +} diff --git a/src/main/java/com/catcher/batch/external/vo/request/TourApiRequest.java b/src/main/java/com/catcher/batch/external/vo/request/TourApiRequest.java new file mode 100644 index 0000000..32176b2 --- /dev/null +++ b/src/main/java/com/catcher/batch/external/vo/request/TourApiRequest.java @@ -0,0 +1,27 @@ +package com.catcher.batch.external.vo.request; + +import lombok.Getter; + +/* TODO: 하드코딩된 값 바꾸기 */ + +@Getter +public class TourApiRequest { + + private Integer numOfRows = 100; + + private Integer pageNo = 1; + + private String MobileOS = "ETC"; + + private String MobileApp = "AppTest"; + + private String _type = "json"; + + private String listYN = "Y"; + + private String arrange = "A"; + + private String eventStartDate = "20230901"; + + private String serviceKey = "your_service_key"; +} diff --git a/src/main/java/com/catcher/batch/external/vo/response/TourApiResponse.java b/src/main/java/com/catcher/batch/external/vo/response/TourApiResponse.java new file mode 100644 index 0000000..956f698 --- /dev/null +++ b/src/main/java/com/catcher/batch/external/vo/response/TourApiResponse.java @@ -0,0 +1,70 @@ +package com.catcher.batch.external.vo.response; + +import com.catcher.batch.annotation.CatcherJson; +import lombok.Getter; + +import java.util.List; + +@Getter +@CatcherJson(path = "response.body.items") +public class TourApiResponse { + + private List item; + +// private InnerTourAPiResponse response; +// +// @Getter +// public static class InnerTourAPiResponse { +// +// private TourApiHeader header; +// +// private TourApiBody body; +// } +// +// @Getter +// public static class TourApiHeader { +// private String resultCode; +// private String resultMsg; +// } +// +// @Getter +// public static class TourApiBody { +// +// private TourApiItems items; +// private Integer numOfRows; +// private Integer pageNo; +// private Integer totalCount; +// } +// +// @Getter +// public static class TourApiItems { +// private List item; +// } + + /* TODO: JsonProperty 어노테이션 이용하여 적당한 변수명으로 바꾸기 */ + @Getter + public static class TourApiItem { + private String addr1; + private String addr2; + private String booktour; + private String cat1; + private String cat2; + private String cat3; + private String contentid; + private String contenttypeid; + private String createdtime; + private String eventstartdate; + private String eventenddate; + private String firstimage; + private String firstimage2; + private String cpyrhtDivCd; + private String mapx; + private String mapy; + private String mlevel; + private String modifiedtime; + private String areacode; + private String sigungucode; + private String tel; + private String title; + } +} diff --git a/src/main/java/com/catcher/batch/job/config/CommonJobConfig.java b/src/main/java/com/catcher/batch/job/config/CommonJobConfig.java new file mode 100644 index 0000000..005bf2e --- /dev/null +++ b/src/main/java/com/catcher/batch/job/config/CommonJobConfig.java @@ -0,0 +1,10 @@ +package com.catcher.batch.job.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.launch.JobLauncher; + +@RequiredArgsConstructor +public class CommonJobConfig { + + protected final JobLauncher jobLauncher; +} diff --git a/src/main/java/com/catcher/batch/job/config/TourApiJob.java b/src/main/java/com/catcher/batch/job/config/TourApiJob.java new file mode 100644 index 0000000..deeb66e --- /dev/null +++ b/src/main/java/com/catcher/batch/job/config/TourApiJob.java @@ -0,0 +1,22 @@ +package com.catcher.batch.job.config; + +import com.catcher.batch.external.TourApiServiceExternalCallService; +import com.catcher.batch.external.vo.request.TourApiRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TourApiJob { + + private final TourApiServiceExternalCallService tourApiServiceExternalCallService; + + /* TODO: Spring batch 이용하도록 변경? */ + @Scheduled(fixedDelay = 1000000) //TODO: 시간대 정해서 하루에 1번으로 변경 + public void tourApiJob() { + + final var response = tourApiServiceExternalCallService.callTourList(new TourApiRequest()); + } + +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties new file mode 100644 index 0000000..407403a --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -0,0 +1,27 @@ +spring.datasource.driver-class-name=org.mariadb.jdbc.Driver + +## spring datasource +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} + + +## ssh +ssh.host=${SSH_HOST} +ssh.port=${SSH_PORT} +ssh.username=${SSH_USERNAME} +ssh.password=${SSH_PASSWORD} +ssh.local-port=${SSH_LOCAL_PORT} +ssh.datasource.origin=${SSH_DATASOURCE_ORIGIN} + +#update the schema with the given values. +spring.jpa.hibernate.ddl-auto=update +#To beautify or pretty print the SQL +spring.jpa.properties.hibernate.format_sql=true +#show sql +spring.jpa.properties.hibernate.show-sql=true +#show parameter binding +logging.level.org.hibernate.type.descriptor.sql=DEBUG + +logging.level.org.hibernate.SQL=DEBUG +spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MariaDBDialect \ No newline at end of file diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 0000000..407403a --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,27 @@ +spring.datasource.driver-class-name=org.mariadb.jdbc.Driver + +## spring datasource +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} + + +## ssh +ssh.host=${SSH_HOST} +ssh.port=${SSH_PORT} +ssh.username=${SSH_USERNAME} +ssh.password=${SSH_PASSWORD} +ssh.local-port=${SSH_LOCAL_PORT} +ssh.datasource.origin=${SSH_DATASOURCE_ORIGIN} + +#update the schema with the given values. +spring.jpa.hibernate.ddl-auto=update +#To beautify or pretty print the SQL +spring.jpa.properties.hibernate.format_sql=true +#show sql +spring.jpa.properties.hibernate.show-sql=true +#show parameter binding +logging.level.org.hibernate.type.descriptor.sql=DEBUG + +logging.level.org.hibernate.SQL=DEBUG +spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MariaDBDialect \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..407403a --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,27 @@ +spring.datasource.driver-class-name=org.mariadb.jdbc.Driver + +## spring datasource +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} + + +## ssh +ssh.host=${SSH_HOST} +ssh.port=${SSH_PORT} +ssh.username=${SSH_USERNAME} +ssh.password=${SSH_PASSWORD} +ssh.local-port=${SSH_LOCAL_PORT} +ssh.datasource.origin=${SSH_DATASOURCE_ORIGIN} + +#update the schema with the given values. +spring.jpa.hibernate.ddl-auto=update +#To beautify or pretty print the SQL +spring.jpa.properties.hibernate.format_sql=true +#show sql +spring.jpa.properties.hibernate.show-sql=true +#show parameter binding +logging.level.org.hibernate.type.descriptor.sql=DEBUG + +logging.level.org.hibernate.SQL=DEBUG +spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MariaDBDialect \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..407403a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,27 @@ +spring.datasource.driver-class-name=org.mariadb.jdbc.Driver +## spring datasource +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} + + +## ssh +ssh.host=${SSH_HOST} +ssh.port=${SSH_PORT} +ssh.username=${SSH_USERNAME} +ssh.password=${SSH_PASSWORD} +ssh.local-port=${SSH_LOCAL_PORT} +ssh.datasource.origin=${SSH_DATASOURCE_ORIGIN} + +#update the schema with the given values. +spring.jpa.hibernate.ddl-auto=update +#To beautify or pretty print the SQL +spring.jpa.properties.hibernate.format_sql=true +#show sql +spring.jpa.properties.hibernate.show-sql=true +#show parameter binding +logging.level.org.hibernate.type.descriptor.sql=DEBUG + +logging.level.org.hibernate.SQL=DEBUG +spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MariaDBDialect \ No newline at end of file