diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..56e1ef63 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset=utf-8 +end_of_line=lf +indent_style=space +indent_size=4 +insert_final_newline=true +disabled_rules=no-wildcard-imports,import-ordering,comment-spacing + +[*.{kt,kts}] +insert_final_newline=false \ No newline at end of file diff --git a/api-repository/build.gradle b/api-repository/build.gradle new file mode 100644 index 00000000..0ea26ded --- /dev/null +++ b/api-repository/build.gradle @@ -0,0 +1,12 @@ +bootJar { + enabled = false +} + +jar { + enabled = true +} + + +dependencies { + api project(':data') +} diff --git a/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryConfig.java b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryConfig.java new file mode 100644 index 00000000..78a08d6a --- /dev/null +++ b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryConfig.java @@ -0,0 +1,15 @@ +package com.walking.api.repository.config; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackages = ApiRepositoryConfig.BASE_PACKAGE) +public class ApiRepositoryConfig { + + public static final String BASE_PACKAGE = "com.walking.api.repository"; + public static final String SERVICE_NAME = "walking"; + public static final String MODULE_NAME = "api-repository"; + public static final String BEAN_NAME_PREFIX = "apiRepository"; + public static final String PROPERTY_PREFIX = SERVICE_NAME + "." + MODULE_NAME; +} diff --git a/api/src/main/java/com/walking/api/config/ApiDataConfig.java b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryDataConfig.java similarity index 70% rename from api/src/main/java/com/walking/api/config/ApiDataConfig.java rename to api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryDataConfig.java index 47d43fff..cecf5ec9 100644 --- a/api/src/main/java/com/walking/api/config/ApiDataConfig.java +++ b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryDataConfig.java @@ -1,4 +1,4 @@ -package com.walking.api.config; +package com.walking.api.repository.config; import com.walking.data.DataConfig; import org.springframework.context.annotation.Configuration; @@ -6,4 +6,4 @@ @Configuration @Import({DataConfig.class}) -public class ApiDataConfig {} +public class ApiRepositoryDataConfig {} diff --git a/api/src/main/java/com/walking/api/config/ApiDataSourceConfig.java b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryDataSourceConfig.java similarity index 76% rename from api/src/main/java/com/walking/api/config/ApiDataSourceConfig.java rename to api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryDataSourceConfig.java index eb3ececa..74434629 100644 --- a/api/src/main/java/com/walking/api/config/ApiDataSourceConfig.java +++ b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryDataSourceConfig.java @@ -1,4 +1,4 @@ -package com.walking.api.config; +package com.walking.api.repository.config; import javax.sql.DataSource; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -15,12 +15,12 @@ DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, }) -public class ApiDataSourceConfig { +public class ApiRepositoryDataSourceConfig { - public static final String DATASOURCE_NAME = ApiAppConfig.BEAN_NAME_PREFIX + "DataSource"; + public static final String DATASOURCE_NAME = ApiRepositoryConfig.BEAN_NAME_PREFIX + "DataSource"; @Bean(name = DATASOURCE_NAME) - @ConfigurationProperties(prefix = "spring.datasource") + @ConfigurationProperties(prefix = "api.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } diff --git a/api/src/main/java/com/walking/api/config/ApiEntityConfig.java b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryEntityConfig.java similarity index 80% rename from api/src/main/java/com/walking/api/config/ApiEntityConfig.java rename to api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryEntityConfig.java index cd28c925..4ad6bee4 100644 --- a/api/src/main/java/com/walking/api/config/ApiEntityConfig.java +++ b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryEntityConfig.java @@ -1,6 +1,6 @@ -package com.walking.api.config; +package com.walking.api.repository.config; -import static com.walking.api.config.ApiDataSourceConfig.DATASOURCE_NAME; +import static com.walking.api.repository.config.ApiRepositoryDataSourceConfig.DATASOURCE_NAME; import com.walking.data.DataConfig; import com.walking.data.config.HibernatePropertyMapProvider; @@ -17,10 +17,11 @@ @Configuration @RequiredArgsConstructor -public class ApiEntityConfig { +public class ApiRepositoryEntityConfig { public static final String ENTITY_MANAGER_FACTORY_NAME = - ApiAppConfig.BEAN_NAME_PREFIX + "EntityManagerFactory"; - private static final String PERSIST_UNIT = ApiAppConfig.BEAN_NAME_PREFIX + "PersistenceUnit"; + ApiRepositoryConfig.BEAN_NAME_PREFIX + "EntityManagerFactory"; + private static final String PERSIST_UNIT = + ApiRepositoryConfig.BEAN_NAME_PREFIX + "PersistenceUnit"; private final HibernatePropertyMapProvider hibernatePropertyMapProvider; diff --git a/api/src/main/java/com/walking/api/config/ApiJpaConfig.java b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryJpaConfig.java similarity index 69% rename from api/src/main/java/com/walking/api/config/ApiJpaConfig.java rename to api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryJpaConfig.java index f7b776e2..6d29622d 100644 --- a/api/src/main/java/com/walking/api/config/ApiJpaConfig.java +++ b/api-repository/src/main/java/com/walking/api/repository/config/ApiRepositoryJpaConfig.java @@ -1,4 +1,4 @@ -package com.walking.api.config; +package com.walking.api.repository.config; import javax.persistence.EntityManagerFactory; import org.springframework.context.annotation.Bean; @@ -13,13 +13,13 @@ @EnableJpaAuditing @EnableTransactionManagement @EnableJpaRepositories( - basePackages = ApiAppConfig.BASE_PACKAGE, - transactionManagerRef = ApiJpaConfig.TRANSACTION_MANAGER_NAME, - entityManagerFactoryRef = ApiEntityConfig.ENTITY_MANAGER_FACTORY_NAME) -public class ApiJpaConfig { + basePackages = ApiRepositoryConfig.BASE_PACKAGE, + transactionManagerRef = ApiRepositoryJpaConfig.TRANSACTION_MANAGER_NAME, + entityManagerFactoryRef = ApiRepositoryEntityConfig.ENTITY_MANAGER_FACTORY_NAME) +public class ApiRepositoryJpaConfig { public static final String TRANSACTION_MANAGER_NAME = - ApiAppConfig.BEAN_NAME_PREFIX + "TransactionalManager"; + ApiRepositoryConfig.BEAN_NAME_PREFIX + "TransactionalManager"; @Bean(name = TRANSACTION_MANAGER_NAME) public PlatformTransactionManager transactionManager(EntityManagerFactory emf) { diff --git a/api-repository/src/main/java/com/walking/api/repository/dao/member/MemberRepository.java b/api-repository/src/main/java/com/walking/api/repository/dao/member/MemberRepository.java new file mode 100644 index 00000000..489f0e53 --- /dev/null +++ b/api-repository/src/main/java/com/walking/api/repository/dao/member/MemberRepository.java @@ -0,0 +1,16 @@ +package com.walking.api.repository.dao.member; + +import com.walking.data.entity.member.MemberEntity; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberRepository extends JpaRepository { + + boolean existsByIdAndDeletedFalse(Long id); + + Optional findByIdAndDeletedFalse(Long id); + + Optional findByCertificationIdAndDeletedFalse(String certificationId); +} diff --git a/api-repository/src/main/java/com/walking/api/repository/dao/member/PathFavoritesRepository.java b/api-repository/src/main/java/com/walking/api/repository/dao/member/PathFavoritesRepository.java new file mode 100644 index 00000000..d7bb495c --- /dev/null +++ b/api-repository/src/main/java/com/walking/api/repository/dao/member/PathFavoritesRepository.java @@ -0,0 +1,8 @@ +package com.walking.api.repository.dao.member; + +import com.walking.data.entity.path.PathFavoritesEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PathFavoritesRepository extends JpaRepository {} diff --git a/api/src/main/java/com/walking/api/repository/traffic/TrafficDetailRepository.java b/api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficDetailRepository.java similarity index 97% rename from api/src/main/java/com/walking/api/repository/traffic/TrafficDetailRepository.java rename to api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficDetailRepository.java index facf5661..999cbb7c 100644 --- a/api/src/main/java/com/walking/api/repository/traffic/TrafficDetailRepository.java +++ b/api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficDetailRepository.java @@ -1,4 +1,4 @@ -package com.walking.api.repository.traffic; +package com.walking.api.repository.dao.traffic; import com.walking.data.entity.traffic.TrafficDetailEntity; import java.util.List; diff --git a/api/src/main/java/com/walking/api/repository/traffic/TrafficFavoritesRepository.java b/api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficFavoritesRepository.java similarity index 92% rename from api/src/main/java/com/walking/api/repository/traffic/TrafficFavoritesRepository.java rename to api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficFavoritesRepository.java index 25a9baec..090952e9 100644 --- a/api/src/main/java/com/walking/api/repository/traffic/TrafficFavoritesRepository.java +++ b/api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficFavoritesRepository.java @@ -1,4 +1,4 @@ -package com.walking.api.repository.traffic; +package com.walking.api.repository.dao.traffic; import com.walking.data.entity.member.MemberEntity; import com.walking.data.entity.member.TrafficFavoritesEntity; diff --git a/api/src/main/java/com/walking/api/repository/traffic/TrafficRepository.java b/api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficRepository.java similarity index 98% rename from api/src/main/java/com/walking/api/repository/traffic/TrafficRepository.java rename to api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficRepository.java index 4c1e6875..582a5cea 100644 --- a/api/src/main/java/com/walking/api/repository/traffic/TrafficRepository.java +++ b/api-repository/src/main/java/com/walking/api/repository/dao/traffic/TrafficRepository.java @@ -1,4 +1,4 @@ -package com.walking.api.repository.traffic; +package com.walking.api.repository.dao.traffic; import com.walking.data.entity.traffic.TrafficEntity; import java.util.List; diff --git a/api-repository/src/main/resources/application-api-repository-dev.yml b/api-repository/src/main/resources/application-api-repository-dev.yml new file mode 100644 index 00000000..f321aa37 --- /dev/null +++ b/api-repository/src/main/resources/application-api-repository-dev.yml @@ -0,0 +1,6 @@ +api: + datasource: + jdbc-url: ${DB_HOSTNAME}/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/api-repository/src/main/resources/application-api-repository-local.yml b/api-repository/src/main/resources/application-api-repository-local.yml new file mode 100644 index 00000000..559dd252 --- /dev/null +++ b/api-repository/src/main/resources/application-api-repository-local.yml @@ -0,0 +1,6 @@ +api: + datasource: + jdbc-url: jdbc:mysql://localhost:13306/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/api-repository/src/main/resources/application-api-repository-prod.yml b/api-repository/src/main/resources/application-api-repository-prod.yml new file mode 100644 index 00000000..f321aa37 --- /dev/null +++ b/api-repository/src/main/resources/application-api-repository-prod.yml @@ -0,0 +1,6 @@ +api: + datasource: + jdbc-url: ${DB_HOSTNAME}/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/api-repository/src/main/resources/application-api-repository.yml b/api-repository/src/main/resources/application-api-repository.yml new file mode 100644 index 00000000..f321aa37 --- /dev/null +++ b/api-repository/src/main/resources/application-api-repository.yml @@ -0,0 +1,6 @@ +api: + datasource: + jdbc-url: ${DB_HOSTNAME}/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/api/build.gradle b/api/build.gradle index 8be6090a..e8f9bd29 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -8,7 +8,8 @@ dependencyManagement { } dependencies { - implementation project(':data') + implementation project(':api-repository') + implementation project(':member-api') // web implementation 'org.springframework.boot:spring-boot-starter-web' @@ -39,6 +40,8 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' implementation group: 'com.google.code.findbugs', name: 'jsr305', version: "${jsr305Version}" + implementation 'org.json:json:20200518' + // security implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/api/src/main/java/com/walking/api/config/ApiAppConfig.java b/api/src/main/java/com/walking/api/config/ApiAppConfig.java index e47e17f3..bd561e84 100644 --- a/api/src/main/java/com/walking/api/config/ApiAppConfig.java +++ b/api/src/main/java/com/walking/api/config/ApiAppConfig.java @@ -1,8 +1,11 @@ package com.walking.api.config; +import com.walking.member.api.config.MemberApiConfig; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +@Import({MemberApiConfig.class}) @Configuration @ComponentScan(basePackages = ApiAppConfig.BASE_PACKAGE) public class ApiAppConfig { diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/AddFavoriteTrafficUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/AddFavoriteTrafficUseCase.java index df0024dd..324e314d 100644 --- a/api/src/main/java/com/walking/api/domain/traffic/usecase/AddFavoriteTrafficUseCase.java +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/AddFavoriteTrafficUseCase.java @@ -1,7 +1,7 @@ package com.walking.api.domain.traffic.usecase; import com.walking.api.domain.traffic.dto.AddFavoriteTrafficUseCaseRequest; -import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.api.repository.dao.traffic.TrafficFavoritesRepository; import com.walking.data.entity.member.MemberEntity; import com.walking.data.entity.member.TrafficFavoritesEntity; import com.walking.data.entity.traffic.TrafficEntity; diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/BrowseFavoriteTrafficsUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/BrowseFavoriteTrafficsUseCase.java index f86830f5..38f43d38 100644 --- a/api/src/main/java/com/walking/api/domain/traffic/usecase/BrowseFavoriteTrafficsUseCase.java +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/BrowseFavoriteTrafficsUseCase.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.walking.api.domain.traffic.dto.BrowseFavoriteTrafficsUseCaseRequest; -import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.api.repository.dao.traffic.TrafficFavoritesRepository; import com.walking.api.web.dto.response.BrowseFavoriteTrafficsResponse; import com.walking.api.web.dto.response.detail.FavoriteTrafficDetail; import com.walking.api.web.dto.response.detail.PointDetail; diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/DeleteFavoriteTrafficUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/DeleteFavoriteTrafficUseCase.java index 8caf3dad..171f209f 100644 --- a/api/src/main/java/com/walking/api/domain/traffic/usecase/DeleteFavoriteTrafficUseCase.java +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/DeleteFavoriteTrafficUseCase.java @@ -1,7 +1,7 @@ package com.walking.api.domain.traffic.usecase; import com.walking.api.domain.traffic.dto.DeleteFavoriteTrafficUseCaseRequest; -import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.api.repository.dao.traffic.TrafficFavoritesRepository; import com.walking.data.entity.member.MemberEntity; import com.walking.data.entity.member.TrafficFavoritesEntity; import lombok.RequiredArgsConstructor; diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/UpdateFavoriteTrafficUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/UpdateFavoriteTrafficUseCase.java index ad2e9a98..7189268d 100644 --- a/api/src/main/java/com/walking/api/domain/traffic/usecase/UpdateFavoriteTrafficUseCase.java +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/UpdateFavoriteTrafficUseCase.java @@ -1,7 +1,7 @@ package com.walking.api.domain.traffic.usecase; import com.walking.api.domain.traffic.dto.UpdateFavoriteTrafficUseCaseRequest; -import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.api.repository.dao.traffic.TrafficFavoritesRepository; import com.walking.data.entity.member.TrafficFavoritesEntity; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/api/src/main/java/com/walking/api/repository/member/MemberRepository.java b/api/src/main/java/com/walking/api/repository/member/MemberRepository.java deleted file mode 100644 index b69c76d8..00000000 --- a/api/src/main/java/com/walking/api/repository/member/MemberRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.walking.api.repository.member; - -import com.walking.data.entity.member.MemberEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface MemberRepository extends JpaRepository {} diff --git a/api/src/main/java/com/walking/api/service/TrafficCurrentDetailPredictService.java b/api/src/main/java/com/walking/api/service/TrafficCurrentDetailPredictService.java index 0e9bc671..24bbcce8 100644 --- a/api/src/main/java/com/walking/api/service/TrafficCurrentDetailPredictService.java +++ b/api/src/main/java/com/walking/api/service/TrafficCurrentDetailPredictService.java @@ -1,6 +1,6 @@ package com.walking.api.service; -import com.walking.api.repository.traffic.TrafficDetailRepository; +import com.walking.api.repository.dao.traffic.TrafficDetailRepository; import com.walking.api.service.dto.ColorAndTimeLeft; import com.walking.api.service.dto.PredictedData; import com.walking.api.service.dto.request.CurrentDetailRequestDto; diff --git a/api/src/main/java/com/walking/api/service/TrafficCyclePredictServiceImpl.java b/api/src/main/java/com/walking/api/service/TrafficCyclePredictServiceImpl.java index 6941d827..ffbbf3b3 100644 --- a/api/src/main/java/com/walking/api/service/TrafficCyclePredictServiceImpl.java +++ b/api/src/main/java/com/walking/api/service/TrafficCyclePredictServiceImpl.java @@ -1,7 +1,7 @@ package com.walking.api.service; -import com.walking.api.repository.traffic.TrafficDetailRepository; -import com.walking.api.repository.traffic.TrafficRepository; +import com.walking.api.repository.dao.traffic.TrafficDetailRepository; +import com.walking.api.repository.dao.traffic.TrafficRepository; import com.walking.api.service.dto.PredictedData; import com.walking.api.service.dto.request.CyclePredictionRequestDto; import com.walking.api.util.OffsetDateTimeCalculator; diff --git a/api/src/main/java/com/walking/api/service/path/ExtractPathTrafficInfoService.java b/api/src/main/java/com/walking/api/service/path/ExtractPathTrafficInfoService.java index 8b81bfb0..39b8cb9c 100644 --- a/api/src/main/java/com/walking/api/service/path/ExtractPathTrafficInfoService.java +++ b/api/src/main/java/com/walking/api/service/path/ExtractPathTrafficInfoService.java @@ -1,6 +1,6 @@ package com.walking.api.service.path; -import com.walking.api.repository.traffic.TrafficRepository; +import com.walking.api.repository.dao.traffic.TrafficRepository; import com.walking.api.service.dto.PathTrafficData; import com.walking.api.util.JsonParser; import com.walking.data.entity.path.TrafficDirection; diff --git a/api/src/main/java/com/walking/api/service/path/ReadFavoritesPathService.java b/api/src/main/java/com/walking/api/service/path/ReadFavoritesPathService.java index 5f4a9dcf..43ca47c7 100644 --- a/api/src/main/java/com/walking/api/service/path/ReadFavoritesPathService.java +++ b/api/src/main/java/com/walking/api/service/path/ReadFavoritesPathService.java @@ -1,7 +1,7 @@ package com.walking.api.service.path; +import com.walking.api.repository.dao.member.MemberRepository; import com.walking.api.repository.dto.response.PathFavoritesVo; -import com.walking.api.repository.member.MemberRepository; import com.walking.api.repository.path.PathFavoritesRepository; import com.walking.api.service.dto.response.ReadFavoritesPathResponse; import com.walking.api.web.dto.request.OrderFilter; diff --git a/api/src/main/java/com/walking/api/service/path/RouteDetailResponseService.java b/api/src/main/java/com/walking/api/service/path/RouteDetailResponseService.java index 81684ed8..17879227 100644 --- a/api/src/main/java/com/walking/api/service/path/RouteDetailResponseService.java +++ b/api/src/main/java/com/walking/api/service/path/RouteDetailResponseService.java @@ -1,7 +1,7 @@ package com.walking.api.service.path; import com.walking.api.converter.TrafficDetailConverter; -import com.walking.api.repository.traffic.TrafficRepository; +import com.walking.api.repository.dao.traffic.TrafficRepository; import com.walking.api.service.TrafficIntegrationPredictService; import com.walking.api.service.dto.PathPrimaryData; import com.walking.api.service.dto.PathTrafficData; diff --git a/api/src/main/java/com/walking/api/service/traffic/ReadTrafficFavoritesService.java b/api/src/main/java/com/walking/api/service/traffic/ReadTrafficFavoritesService.java index 5ec2b711..4bb1ebea 100644 --- a/api/src/main/java/com/walking/api/service/traffic/ReadTrafficFavoritesService.java +++ b/api/src/main/java/com/walking/api/service/traffic/ReadTrafficFavoritesService.java @@ -1,6 +1,6 @@ package com.walking.api.service.traffic; -import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.api.repository.dao.traffic.TrafficFavoritesRepository; import com.walking.data.entity.member.MemberEntity; import com.walking.data.entity.member.TrafficFavoritesEntity; import java.util.List; diff --git a/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java b/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java index 84fb3df1..f411c06e 100644 --- a/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java +++ b/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java @@ -1,6 +1,6 @@ package com.walking.api.service.traffic; -import com.walking.api.repository.traffic.TrafficRepository; +import com.walking.api.repository.dao.traffic.TrafficRepository; import com.walking.data.entity.traffic.TrafficEntity; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/api/src/main/java/com/walking/api/web/controller/member/MemberController.java b/api/src/main/java/com/walking/api/web/controller/member/MemberController.java new file mode 100644 index 00000000..fd1c774a --- /dev/null +++ b/api/src/main/java/com/walking/api/web/controller/member/MemberController.java @@ -0,0 +1,144 @@ +package com.walking.api.web.controller.member; + +import com.walking.api.security.authentication.authority.Roles; +import com.walking.api.security.authentication.token.TokenUserDetails; +import com.walking.api.security.token.AuthToken; +import com.walking.api.security.token.TokenGenerator; +import com.walking.api.security.token.TokenResolver; +import com.walking.api.web.dto.request.member.PatchProfileBody; +import com.walking.api.web.dto.request.member.PostMemberBody; +import com.walking.api.web.dto.request.member.RefreshMemberAuthTokenBody; +import com.walking.api.web.dto.response.member.DeleteMemberResponse; +import com.walking.api.web.dto.response.member.GetMemberResponse; +import com.walking.api.web.dto.response.member.MemberTokenResponse; +import com.walking.api.web.dto.response.member.PatchProfileResponse; +import com.walking.api.web.dto.response.member.PostMemberResponse; +import com.walking.api.web.support.ApiResponse; +import com.walking.api.web.support.ApiResponseGenerator; +import com.walking.api.web.support.MessageCode; +import com.walking.member.api.usecase.DeleteMemberUseCase; +import com.walking.member.api.usecase.GetMemberDetailUseCase; +import com.walking.member.api.usecase.GetMemberTokenDetailUseCase; +import com.walking.member.api.usecase.PatchProfileImageUseCase; +import com.walking.member.api.usecase.PostMemberUseCase; +import com.walking.member.api.usecase.dto.response.DeleteMemberUseCaseResponse; +import com.walking.member.api.usecase.dto.response.GetMemberDetailUseCaseResponse; +import com.walking.member.api.usecase.dto.response.GetMemberTokenDetailUseCaseResponse; +import com.walking.member.api.usecase.dto.response.PatchProfileImageUseCaseResponse; +import com.walking.member.api.usecase.dto.response.PostMemberUseCaseResponse; +import java.io.File; +import java.io.IOException; +import java.util.List; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@Validated +@RestController +@RequestMapping("/api/v1/members") +@RequiredArgsConstructor +public class MemberController { + + private final TokenGenerator tokenGenerator; + private final TokenResolver tokenResolver; + + private final PostMemberUseCase postMemberUseCase; + private final DeleteMemberUseCase deleteMemberUseCase; + private final GetMemberDetailUseCase getMemberDetailUseCase; + private final GetMemberTokenDetailUseCase getMemberTokenDetailUseCase; + private final PatchProfileImageUseCase patchProfileImageUseCase; + + @PostMapping() + public ApiResponse> postMember( + @Valid @RequestBody PostMemberBody postMemberBody) { + PostMemberUseCaseResponse useCaseResponse = postMemberUseCase.execute(postMemberBody.getCode()); + AuthToken authToken = + tokenGenerator.generateAuthToken(useCaseResponse.getId(), List.of(Roles.ROLE_USER)); + PostMemberResponse response = + PostMemberResponse.builder() + .id(useCaseResponse.getId()) + .nickname(useCaseResponse.getNickname()) + .profile(useCaseResponse.getProfile()) + .accessToken(authToken.getAccessToken()) + .refreshToken(authToken.getRefreshToken()) + .build(); + return ApiResponseGenerator.success(response, HttpStatus.CREATED, MessageCode.RESOURCE_CREATED); + } + + @DeleteMapping() + public ApiResponse> deleteMember( + @AuthenticationPrincipal TokenUserDetails userDetails) { + Long memberId = Long.valueOf(userDetails.getUsername()); + DeleteMemberUseCaseResponse useCaseResponse = deleteMemberUseCase.execute(memberId); + DeleteMemberResponse response = + DeleteMemberResponse.builder() + .id(useCaseResponse.getId()) + .deletedAt(useCaseResponse.getDeletedAt()) + .build(); + return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.RESOURCE_DELETED); + } + + @GetMapping() + public ApiResponse> getMember( + @AuthenticationPrincipal TokenUserDetails userDetails) { + Long memberId = Long.valueOf(userDetails.getUsername()); + GetMemberDetailUseCaseResponse useCaseResponse = getMemberDetailUseCase.execute(memberId); + GetMemberResponse response = + GetMemberResponse.builder() + .id(useCaseResponse.getId()) + .nickname(useCaseResponse.getNickName()) + .profile(useCaseResponse.getProfile()) + .build(); + return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.SUCCESS); + } + + @PostMapping("/token") + public ApiResponse> refreshMemberAuthToken( + @Valid @RequestBody RefreshMemberAuthTokenBody memberAuthTokenBody) { + Long memberId = + tokenResolver + .resolveId(memberAuthTokenBody.getRefreshToken()) + .orElseThrow(() -> new IllegalArgumentException("Invalid token")); + GetMemberTokenDetailUseCaseResponse useCaseResponse = + getMemberTokenDetailUseCase.execute(memberId); + AuthToken authToken = + tokenGenerator.generateAuthToken(useCaseResponse.getId(), List.of(Roles.ROLE_USER)); + MemberTokenResponse response = + MemberTokenResponse.builder() + .accessToken(authToken.getAccessToken()) + .refreshToken(authToken.getRefreshToken()) + .build(); + return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.SUCCESS); + } + + @PatchMapping("/profile") + public ApiResponse> patchProfileImage( + @AuthenticationPrincipal TokenUserDetails userDetails, PatchProfileBody patchProfileBody) + throws IOException { + Long memberId = Long.valueOf(userDetails.getUsername()); + String suffix = patchProfileBody.getProfile().getOriginalFilename().split("\\.")[1]; + File tempFile = File.createTempFile("temp_", "." + suffix); + patchProfileBody.getProfile().transferTo(tempFile); + PatchProfileImageUseCaseResponse useCaseResponse = + patchProfileImageUseCase.execute(memberId, tempFile); + tempFile.deleteOnExit(); + PatchProfileResponse response = + PatchProfileResponse.builder() + .id(useCaseResponse.getId()) + .nickname(useCaseResponse.getNickName()) + .profile(useCaseResponse.getProfile()) + .build(); + return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.RESOURCE_MODIFIED); + } +} diff --git a/api/src/main/java/com/walking/api/web/dto/request/member/PatchProfileBody.java b/api/src/main/java/com/walking/api/web/dto/request/member/PatchProfileBody.java new file mode 100644 index 00000000..2c2a70aa --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/request/member/PatchProfileBody.java @@ -0,0 +1,23 @@ +package com.walking.api.web.dto.request.member; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import javax.validation.constraints.NotEmpty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.springframework.web.multipart.MultipartFile; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class PatchProfileBody { + + @JsonIgnore @NotEmpty private MultipartFile profile; +} diff --git a/api/src/main/java/com/walking/api/web/dto/request/member/PostMemberBody.java b/api/src/main/java/com/walking/api/web/dto/request/member/PostMemberBody.java new file mode 100644 index 00000000..d84e3aaf --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/request/member/PostMemberBody.java @@ -0,0 +1,21 @@ +package com.walking.api.web.dto.request.member; + +import javax.validation.constraints.NotEmpty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class PostMemberBody { + + @NotEmpty private String code; +} diff --git a/api/src/main/java/com/walking/api/web/dto/request/member/RefreshMemberAuthTokenBody.java b/api/src/main/java/com/walking/api/web/dto/request/member/RefreshMemberAuthTokenBody.java new file mode 100644 index 00000000..5ef803c6 --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/request/member/RefreshMemberAuthTokenBody.java @@ -0,0 +1,20 @@ +package com.walking.api.web.dto.request.member; + +import javax.validation.constraints.NotEmpty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class RefreshMemberAuthTokenBody { + @NotEmpty private String refreshToken; +} diff --git a/api/src/main/java/com/walking/api/web/dto/response/member/DeleteMemberResponse.java b/api/src/main/java/com/walking/api/web/dto/response/member/DeleteMemberResponse.java new file mode 100644 index 00000000..eafe9282 --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/response/member/DeleteMemberResponse.java @@ -0,0 +1,22 @@ +package com.walking.api.web.dto.response.member; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class DeleteMemberResponse { + + private Long id; + private LocalDateTime deletedAt; +} diff --git a/api/src/main/java/com/walking/api/web/dto/response/member/GetMemberResponse.java b/api/src/main/java/com/walking/api/web/dto/response/member/GetMemberResponse.java new file mode 100644 index 00000000..cbf29382 --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/response/member/GetMemberResponse.java @@ -0,0 +1,22 @@ +package com.walking.api.web.dto.response.member; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class GetMemberResponse { + + private Long id; + private String nickname; + private String profile; +} diff --git a/api/src/main/java/com/walking/api/web/dto/response/member/MemberTokenResponse.java b/api/src/main/java/com/walking/api/web/dto/response/member/MemberTokenResponse.java new file mode 100644 index 00000000..581346ab --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/response/member/MemberTokenResponse.java @@ -0,0 +1,21 @@ +package com.walking.api.web.dto.response.member; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class MemberTokenResponse { + + private String accessToken; + private String refreshToken; +} diff --git a/api/src/main/java/com/walking/api/web/dto/response/member/PatchProfileResponse.java b/api/src/main/java/com/walking/api/web/dto/response/member/PatchProfileResponse.java new file mode 100644 index 00000000..3ae85598 --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/response/member/PatchProfileResponse.java @@ -0,0 +1,21 @@ +package com.walking.api.web.dto.response.member; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class PatchProfileResponse { + private Long id; + private String nickname; + private String profile; +} diff --git a/api/src/main/java/com/walking/api/web/dto/response/member/PostMemberResponse.java b/api/src/main/java/com/walking/api/web/dto/response/member/PostMemberResponse.java new file mode 100644 index 00000000..2e50ea45 --- /dev/null +++ b/api/src/main/java/com/walking/api/web/dto/response/member/PostMemberResponse.java @@ -0,0 +1,24 @@ +package com.walking.api.web.dto.response.member; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class PostMemberResponse { + + private Long id; + private String nickname; + private String profile; + private String accessToken; + private String refreshToken; +} diff --git a/api/src/main/resources/application-dev.yml b/api/src/main/resources/application-dev.yml index fff2996a..081637e8 100644 --- a/api/src/main/resources/application-dev.yml +++ b/api/src/main/resources/application-dev.yml @@ -9,11 +9,10 @@ spring: # add import modules profile include: - data-dev - datasource: - jdbc-url: ${DB_HOSTNAME}/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver + - api-repository-dev + - member-api-dev + - image-store-dev + - minio # logging config logging: @@ -71,4 +70,4 @@ log: springdoc: swagger-ui: url: /docs/openapi3.yaml - path: /swagger \ No newline at end of file + path: /swagger diff --git a/api/src/main/resources/application-local.yml b/api/src/main/resources/application-local.yml index 7dda9bba..00a75669 100644 --- a/api/src/main/resources/application-local.yml +++ b/api/src/main/resources/application-local.yml @@ -9,11 +9,10 @@ spring: # add import modules profile include: - data-local - datasource: - jdbc-url: jdbc:mysql://localhost:13306/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true - username: root - password: root - driver-class-name: com.mysql.cj.jdbc.Driver + - api-repository-local + - member-api-local + - image-store-local + - minio walking: batch: @@ -79,4 +78,4 @@ log: springdoc: swagger-ui: url: /docs/openapi3.yaml - path: /swagger \ No newline at end of file + path: /swagger diff --git a/api/src/main/resources/application-prod.yml b/api/src/main/resources/application-prod.yml index e1427b1b..85fb76e0 100644 --- a/api/src/main/resources/application-prod.yml +++ b/api/src/main/resources/application-prod.yml @@ -9,11 +9,10 @@ spring: # add import modules profile include: - data-prod - datasource: - jdbc-url: ${DB_HOSTNAME}/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver + - api-repository-prod + - member-api-prod + - image-store-prod + - minio # prometheus config management: @@ -60,4 +59,4 @@ log: springdoc: swagger-ui: url: /docs/openapi3.yaml - path: /swagger \ No newline at end of file + path: /swagger diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index a3fbafb5..169a8a1c 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -9,11 +9,10 @@ spring: # add import modules profile include: - data - datasource: - jdbc-url: ${DB_HOSTNAME}/api?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver + - api-repository + - member-api + - image-store + - minio # prometheus config management: @@ -59,4 +58,4 @@ log: springdoc: swagger-ui: url: /docs/openapi3.yaml - path: /swagger \ No newline at end of file + path: /swagger diff --git a/api/src/test/java/com/walking/api/repository/RepositoryTest.java b/api/src/test/java/com/walking/api/repository/RepositoryTest.java deleted file mode 100644 index 1306d0f3..00000000 --- a/api/src/test/java/com/walking/api/repository/RepositoryTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.walking.api.repository; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.walking.api.config.ApiDataSourceConfig; -import com.walking.api.config.ApiEntityConfig; -import com.walking.api.config.ApiJpaConfig; -import com.walking.data.config.DataJpaConfig; -import com.walking.data.config.HibernatePropertyMapProvider; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; - -@Slf4j -@ActiveProfiles(profiles = {"test"}) -@DataJpaTest( - excludeAutoConfiguration = { - DataSourceAutoConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, - HibernateJpaAutoConfiguration.class, - }) -@TestPropertySource(locations = "classpath:application-test.yml") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@ContextConfiguration( - classes = { - ApiDataSourceConfig.class, - ApiEntityConfig.class, - ApiJpaConfig.class, - DataJpaConfig.class, - HibernatePropertyMapProvider.class, - ObjectMapper.class, - }) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -abstract class RepositoryTest {} diff --git a/api/src/test/java/com/walking/api/web/controller/member/MemberControllerTest.java b/api/src/test/java/com/walking/api/web/controller/member/MemberControllerTest.java new file mode 100644 index 00000000..47efaf74 --- /dev/null +++ b/api/src/test/java/com/walking/api/web/controller/member/MemberControllerTest.java @@ -0,0 +1,347 @@ +package com.walking.api.web.controller.member; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.walking.api.ApiApp; +import com.walking.api.security.token.AuthToken; +import com.walking.api.security.token.TokenGenerator; +import com.walking.api.security.token.TokenResolver; +import com.walking.api.web.controller.description.Description; +import com.walking.api.web.dto.request.member.PatchProfileBody; +import com.walking.api.web.dto.request.member.PostMemberBody; +import com.walking.api.web.dto.request.member.RefreshMemberAuthTokenBody; +import com.walking.member.api.usecase.DeleteMemberUseCase; +import com.walking.member.api.usecase.GetMemberDetailUseCase; +import com.walking.member.api.usecase.GetMemberTokenDetailUseCase; +import com.walking.member.api.usecase.PatchProfileImageUseCase; +import com.walking.member.api.usecase.PostMemberUseCase; +import com.walking.member.api.usecase.dto.response.DeleteMemberUseCaseResponse; +import com.walking.member.api.usecase.dto.response.GetMemberDetailUseCaseResponse; +import com.walking.member.api.usecase.dto.response.GetMemberTokenDetailUseCaseResponse; +import com.walking.member.api.usecase.dto.response.PatchProfileImageUseCaseResponse; +import com.walking.member.api.usecase.dto.response.PostMemberUseCaseResponse; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@ActiveProfiles(value = "test") +@AutoConfigureRestDocs +@AutoConfigureMockMvc(addFilters = false) +@SpringBootTest(classes = ApiApp.class) +class MemberControllerTest { + + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; + private static final String TAG = "MemberControllerTest"; + private static final String BASE_URL = "/api/v1/members"; + + @MockBean TokenGenerator tokenGenerator; + @MockBean TokenResolver tokenResolver; + + @MockBean PostMemberUseCase postMemberUseCase; + @MockBean DeleteMemberUseCase deleteMemberUseCase; + @MockBean GetMemberDetailUseCase getMemberDetailUseCase; + @MockBean GetMemberTokenDetailUseCase getMemberTokenDetailUseCase; + @MockBean PatchProfileImageUseCase patchProfileImageUseCase; + + @Test + @DisplayName("POST /api/v1/members 회원 가입을 한다.") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + void postMember() throws Exception { + when(postMemberUseCase.execute(any())) + .thenReturn(new PostMemberUseCaseResponse(1L, "nickname", "profile")); + when(tokenGenerator.generateAuthToken(any(), any())) + .thenReturn(new AuthToken("accessToken", "refreshToken")); + + PostMemberBody postMemberBody = PostMemberBody.builder().code("dsakfjdsakfj").build(); + + String content = objectMapper.writeValueAsString(postMemberBody); + + mockMvc + .perform( + post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + .header("Authorization", "Bearer {{ accessToken }}")) + .andExpect(status().is2xxSuccessful()) + .andDo( + document( + "postMember", + resource( + ResourceSnippetParameters.builder() + .description("회원 가입을 한다.") + .tag(TAG) + .requestSchema(Schema.schema("PostMemberRequest")) + .requestHeaders(Description.authHeader()) + .responseSchema(Schema.schema("PostMemberResponse")) + .responseFields( + Description.common( + new FieldDescriptor[] { + fieldWithPath("data") + .type(JsonFieldType.OBJECT) + .description("데이터"), + fieldWithPath("data.id") + .type(JsonFieldType.NUMBER) + .description("회원 ID"), + fieldWithPath("data.nickname") + .type(JsonFieldType.STRING) + .description("닉네임"), + fieldWithPath("data.profile") + .type(JsonFieldType.STRING) + .description("프로필 이미지"), + fieldWithPath("data.accessToken") + .type(JsonFieldType.STRING) + .description("액세스 토큰"), + fieldWithPath("data.refreshToken") + .type(JsonFieldType.STRING) + .description("리프레시 토큰") + })) + .build()))); + } + + @Test + @DisplayName("DELETE /api/v1/members 회원 탈퇴를 한다.") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + void deleteMember() throws Exception { + when(deleteMemberUseCase.execute(anyLong())) + .thenReturn(new DeleteMemberUseCaseResponse(1L, LocalDateTime.now())); + + mockMvc + .perform( + delete(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer {{ accessToken }}")) + .andExpect(status().is2xxSuccessful()) + .andDo( + document( + "deleteMember", + resource( + ResourceSnippetParameters.builder() + .description("회원 탈퇴를 한다.") + .tag(TAG) + .requestSchema(Schema.schema("DeleteMemberRequest")) + .requestHeaders(Description.authHeader()) + .responseSchema(Schema.schema("DeleteMemberResponse")) + .responseFields( + Description.common( + new FieldDescriptor[] { + fieldWithPath("data") + .type(JsonFieldType.OBJECT) + .description("데이터"), + fieldWithPath("data.id") + .type(JsonFieldType.NUMBER) + .description("회원 ID"), + fieldWithPath("data.deletedAt") + .type(JsonFieldType.STRING) + .description("삭제 시간") + })) + .build()))); + } + + @Test + @DisplayName("GET /api/v1/members 회원 정보를 조회한다.") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + void getMember() throws Exception { + when(getMemberDetailUseCase.execute(anyLong())) + .thenReturn(new GetMemberDetailUseCaseResponse(1L, "nickname", "profile", "KAKAO", "정회원")); + + mockMvc + .perform( + get(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer {{ accessToken }}")) + .andExpect(status().is2xxSuccessful()) + .andDo( + document( + "getMember", + resource( + ResourceSnippetParameters.builder() + .description("회원 정보를 조회한다.") + .tag(TAG) + .requestSchema(Schema.schema("GetMemberRequest")) + .requestHeaders(Description.authHeader()) + .responseSchema(Schema.schema("GetMemberResponse")) + .responseFields( + Description.common( + new FieldDescriptor[] { + fieldWithPath("data") + .type(JsonFieldType.OBJECT) + .description("데이터"), + fieldWithPath("data.id") + .type(JsonFieldType.NUMBER) + .description("회원 ID"), + fieldWithPath("data.nickname") + .type(JsonFieldType.STRING) + .description("닉네임"), + fieldWithPath("data.profile") + .type(JsonFieldType.STRING) + .description("프로필 이미지") + })) + .build()))); + } + + @Test + @DisplayName("POST /api/v1/members/token 회원 토큰을 갱신한다.") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + void getMemberToken() throws Exception { + when(tokenResolver.resolveId(any())).thenReturn(Optional.of(1L)); + + when(getMemberTokenDetailUseCase.execute(anyLong())) + .thenReturn(new GetMemberTokenDetailUseCaseResponse(1L)); + + when(tokenGenerator.generateAuthToken(any(), any())) + .thenReturn(new AuthToken("accessToken", "refreshToken")); + + RefreshMemberAuthTokenBody refreshToken = + RefreshMemberAuthTokenBody.builder().refreshToken("refresh").build(); + + String content = objectMapper.writeValueAsString(refreshToken); + + mockMvc + .perform(post(BASE_URL + "/token").contentType(MediaType.APPLICATION_JSON).content(content)) + .andExpect(status().is2xxSuccessful()) + .andDo( + document( + "refreshMemberAuthToken", + resource( + ResourceSnippetParameters.builder() + .description("회원 토큰을 갱신한다.") + .tag(TAG) + .requestSchema(Schema.schema("RefreshMemberAuthTokenRequest")) + .responseSchema(Schema.schema("RefreshMemberAuthTokenResponse")) + .responseFields( + Description.common( + new FieldDescriptor[] { + fieldWithPath("data") + .type(JsonFieldType.OBJECT) + .description("데이터"), + fieldWithPath("data.accessToken") + .type(JsonFieldType.STRING) + .description("액세스 토큰"), + fieldWithPath("data.refreshToken") + .type(JsonFieldType.STRING) + .description("리프레시 토큰") + })) + .build()))); + } + + @Test + @DisplayName("PATCH /api/v1/members/profile 프로필 이미지를 수정한다.") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + void patchProfile() throws Exception { + when(patchProfileImageUseCase.execute(anyLong(), any())) + .thenReturn(new PatchProfileImageUseCaseResponse(1L, "nickname", "profile")); + + File file = makeFile("src/test/resources/images", "test", "png"); + PatchProfileBody patchProfileBody = + PatchProfileBody.builder() + .profile( + new MockMultipartFile( + "profile", + file.getName(), + "image/png", + new BufferedInputStream(new FileInputStream(file)))) + .build(); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("{\n"); + stringBuilder.append(" \"profile\": \""); + stringBuilder.append(Arrays.toString(patchProfileBody.getProfile().getBytes())); + stringBuilder.append("\"\n"); + stringBuilder.append("}"); + String content = stringBuilder.toString(); + + mockMvc + .perform( + multipart(BASE_URL + "/profile") + .file((MockMultipartFile) patchProfileBody.getProfile()) + .header("Authorization", "Bearer {{ accessToken }}") + .contentType(MediaType.MULTIPART_FORM_DATA) + .content(content) + .with( + request -> { + request.setMethod("PATCH"); + return request; + })) + .andExpect(status().is2xxSuccessful()) + .andDo( + document( + "patchProfileImage", + resource( + ResourceSnippetParameters.builder() + .description("프로필 이미지를 수정한다.") + .tag(TAG) + .requestSchema(Schema.schema("PatchProfileImageRequest")) + .requestHeaders(Description.authHeader()) + .responseSchema(Schema.schema("PatchProfileImageResponse")) + .requestFields( + fieldWithPath("profile") + .type(JsonFieldType.STRING) + .description("프로필 이미지")) + .responseFields( + Description.common( + new FieldDescriptor[] { + fieldWithPath("data") + .type(JsonFieldType.OBJECT) + .description("데이터"), + fieldWithPath("data.id") + .type(JsonFieldType.NUMBER) + .description("회원 ID"), + fieldWithPath("data.nickname") + .type(JsonFieldType.STRING) + .description("닉네임"), + fieldWithPath("data.profile") + .type(JsonFieldType.STRING) + .description("프로필 이미지") + })) + .build()))); + } + + File makeFile(String path, String pictureName, String pictureExtension) { + String picture = combineDot(pictureName, pictureExtension); + File directory = new File(path); + if (!directory.exists()) { + directory.mkdirs(); + } + String testPicturePath = combinePath(path, picture); + return new File(testPicturePath); + } + + String combinePath(String s1, String s2) { + return s1 + "/" + s2; + } + + String combineDot(String s1, String s2) { + return s1 + "." + s2; + } +} diff --git a/api/src/test/java/com/walking/api/web/security/TestSecurityConfig.java b/api/src/test/java/com/walking/api/web/security/TestSecurityConfig.java new file mode 100644 index 00000000..959edd88 --- /dev/null +++ b/api/src/test/java/com/walking/api/web/security/TestSecurityConfig.java @@ -0,0 +1,13 @@ +package com.walking.api.web.security; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestSecurityConfig { + + @Bean + public TestTokenUserDetailsService testTokenUserDetailsService() { + return new TestTokenUserDetailsService(); + } +} diff --git a/api/src/test/java/com/walking/api/web/security/TestTokenUserDetailsService.java b/api/src/test/java/com/walking/api/web/security/TestTokenUserDetailsService.java new file mode 100644 index 00000000..2d98ab14 --- /dev/null +++ b/api/src/test/java/com/walking/api/web/security/TestTokenUserDetailsService.java @@ -0,0 +1,19 @@ +package com.walking.api.web.security; + +import com.walking.api.security.authentication.authority.Roles; +import com.walking.api.security.authentication.token.TokenUserDetails; +import java.util.List; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +public class TestTokenUserDetailsService implements UserDetailsService { + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return TokenUserDetails.builder() + .id("1") + .authorities(List.of(Roles.ROLE_USER.getAuthority())) + .build(); + } +} diff --git a/api/src/test/resources/images/test.png b/api/src/test/resources/images/test.png new file mode 100644 index 00000000..2af06811 Binary files /dev/null and b/api/src/test/resources/images/test.png differ diff --git a/build.gradle b/build.gradle index cff56fb5..78dd93b1 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,12 @@ buildscript { springBootVersion = '2.7.5' dependencyManagementVersion = '1.0.15.RELEASE' + // kotlin version + kotlinVersion = '1.5.31' + + // ktlint version + ktlintVersion = '11.3.2' + // jwt jsonwebtokenVersion = '0.11.5' @@ -24,6 +30,15 @@ buildscript { // spotless spotlessVersion = '6.8.0' + // HTTP client + httpclientVersion = '4.5.13' + + // Minio + minioVersion = '8.5.5' + + // aws s3 + awsS3Version = "1.12.220" + // spring cloud set('springCloudVersion', "2021.0.1") } @@ -32,6 +47,19 @@ buildscript { plugins { id 'java' + // kotlin + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" + + // plugin kotlin spring + id 'org.jetbrains.kotlin.plugin.spring' version "${kotlinVersion}" + + // plugin kotlin jpa + id 'org.jetbrains.kotlin.plugin.jpa' version "${kotlinVersion}" + + // plugin kotlin ktlint + id 'org.jlleitschuh.gradle.ktlint' version "${ktlintVersion}" + id 'org.jlleitschuh.gradle.ktlint-idea' version "${ktlintVersion}" + // spring id 'org.springframework.boot' version "${springBootVersion}" id 'io.spring.dependency-management' version "${dependencyManagementVersion}" diff --git a/data/src/main/java/com/walking/data/entity/member/MemberEntity.java b/data/src/main/java/com/walking/data/entity/member/MemberEntity.java index b1346ee9..ddadff74 100644 --- a/data/src/main/java/com/walking/data/entity/member/MemberEntity.java +++ b/data/src/main/java/com/walking/data/entity/member/MemberEntity.java @@ -51,4 +51,20 @@ public class MemberEntity extends BaseEntity { @Builder.Default @Column(nullable = false, columnDefinition = "json") private String resource = "{}"; + + public MemberEntity(String nickName, String profile, String certificationId) { + this.nickName = nickName; + this.profile = profile; + this.certificationId = certificationId; + } + + public MemberEntity withDrawn() { + this.status = MemberStatus.WITHDRAWN; + return this; + } + + public MemberEntity updateProfile(String profile) { + this.profile = profile; + return this; + } } diff --git a/image-store/build.gradle b/image-store/build.gradle new file mode 100644 index 00000000..5fb8c70c --- /dev/null +++ b/image-store/build.gradle @@ -0,0 +1,24 @@ +bootJar { + enabled = false +} + +jar { + enabled = true +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'org.jlleitschuh.gradle.ktlint-idea' +apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'org.jetbrains.kotlin.plugin.spring' + +dependencies { + // kotlin reflect + runtimeOnly "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // minio + implementation "io.minio:minio:${minioVersion}" + + // s3 + implementation "com.amazonaws:aws-java-sdk-s3:${awsS3Version}" +} diff --git a/image-store/src/main/kotlin/com/walking/image/ImageObjectArgs.kt b/image-store/src/main/kotlin/com/walking/image/ImageObjectArgs.kt new file mode 100644 index 00000000..77ce8686 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/ImageObjectArgs.kt @@ -0,0 +1,23 @@ +package com.walking.image + +import java.io.InputStream + +data class ImageGetPresignedObjectUrlArgs( + val bucket: String, + val `object`: String, + val method: String +) + +data class ImagePutObjectArgs( + val bucket: String, + val `object`: String, + val stream: InputStream, + val objectSize: Long, + val partSize: Long, + val contentType: String = "image/jpg" +) + +data class ImageRemoveObjectArgs( + val bucket: String, + val `object`: String +) \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/ImageStoreClient.kt b/image-store/src/main/kotlin/com/walking/image/ImageStoreClient.kt new file mode 100644 index 00000000..3da4f87c --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/ImageStoreClient.kt @@ -0,0 +1,10 @@ +package com.walking.image + +interface ImageStoreClient { + + fun getPresignedObjectUrl(fileName: ImageGetPresignedObjectUrlArgs): String? + + fun removeObject(fileName: ImageRemoveObjectArgs): Boolean + + fun putObject(fileName: ImagePutObjectArgs): ImageWriteResponse? +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/ImageWriteResponse.kt b/image-store/src/main/kotlin/com/walking/image/ImageWriteResponse.kt new file mode 100644 index 00000000..219cd4b0 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/ImageWriteResponse.kt @@ -0,0 +1,9 @@ +package com.walking.image + +data class ImageWriteResponse( + val bucket: String, + val region: String, + val `object`: String, + val etag: String, + val versionId: String +) \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/MinioImageStoreClient.kt b/image-store/src/main/kotlin/com/walking/image/MinioImageStoreClient.kt new file mode 100644 index 00000000..f9d1ad9a --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/MinioImageStoreClient.kt @@ -0,0 +1,73 @@ +package com.walking.image + +import io.minio.GetPresignedObjectUrlArgs +import io.minio.MinioClient +import io.minio.PutObjectArgs +import io.minio.RemoveObjectArgs +import io.minio.http.Method +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class MinioImageStoreClient( + private val minIoClient: MinioClient +) : ImageStoreClient { + + val log: Logger = LoggerFactory.getLogger(MinioImageStoreClient::class.java) + + override fun getPresignedObjectUrl(fileName: ImageGetPresignedObjectUrlArgs): String? { + GetPresignedObjectUrlArgs.builder() + .bucket(fileName.bucket) + .`object`(fileName.`object`) + .method(Method.valueOf(fileName.method)) + .build() + .let { args -> + try { + return minIoClient.getPresignedObjectUrl(args) + } catch (e: Exception) { + log.debug("Failed to get presigned url for object: ${fileName.`object`}") + return null + } + } + } + + override fun removeObject(fileName: ImageRemoveObjectArgs): Boolean { + RemoveObjectArgs.builder() + .bucket(fileName.bucket) + .`object`(fileName.`object`) + .build() + .let { args -> + try { + minIoClient.removeObject(args) + return true + } catch (e: Exception) { + log.debug("Failed to remove object: ${fileName.`object`}") + return false + } + } + } + + override fun putObject(fileName: ImagePutObjectArgs): ImageWriteResponse? { + PutObjectArgs.builder() + .bucket(fileName.bucket) + .`object`(fileName.`object`) + .stream(fileName.stream, fileName.objectSize, fileName.partSize) + .contentType(fileName.contentType) + .build() + .let { args -> + try { + minIoClient.putObject(args).let { owr -> + return ImageWriteResponse( + owr.bucket(), + owr.region(), + owr.`object`(), + owr.etag(), + owr.versionId() + ) + } + } catch (e: Exception) { + log.debug("Failed to put object: ${fileName.`object`}") + return null + } + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/S3ImageStoreClient.kt b/image-store/src/main/kotlin/com/walking/image/S3ImageStoreClient.kt new file mode 100644 index 00000000..1bad8bb3 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/S3ImageStoreClient.kt @@ -0,0 +1,77 @@ +package com.walking.image + +import com.amazonaws.HttpMethod +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.model.DeleteObjectRequest +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest +import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.PutObjectRequest +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class S3ImageStoreClient( + private val s3client: AmazonS3Client, + private val region: String +) : ImageStoreClient { + + val log: Logger = LoggerFactory.getLogger(S3ImageStoreClient::class.java) + + override fun getPresignedObjectUrl(fileName: ImageGetPresignedObjectUrlArgs): String? { + GeneratePresignedUrlRequest( + fileName.bucket, + fileName.`object`, + HttpMethod.valueOf(fileName.method) + ) + .let { args -> + try { + s3client.generatePresignedUrl(args) + .let { url -> + return url.toString() + } + } catch (e: Exception) { + log.debug("Failed to get presigned url for object: ${fileName.`object`}\n ${e.message}") + return null + } + } + } + + override fun removeObject(fileName: ImageRemoveObjectArgs): Boolean { + DeleteObjectRequest(fileName.bucket, fileName.`object`) + .let { args -> + try { + s3client.deleteObject(args) + return true + } catch (e: Exception) { + log.debug("Failed to remove object: ${fileName.`object`}\n ${e.message}") + return false + } + } + } + + override fun putObject(fileName: ImagePutObjectArgs): ImageWriteResponse? { + PutObjectRequest( + fileName.bucket, + fileName.`object`, + fileName.stream, + ObjectMetadata().apply { + contentType = fileName.contentType + } + ) + .let { args -> + try { + s3client.putObject(args).let { owr -> + return ImageWriteResponse( + fileName.bucket, + region, + fileName.`object`, + owr.eTag ?: "", + owr.versionId ?: "" + ) + } + } catch (e: Exception) { + log.debug("Failed to put object: ${fileName.`object`}\n ${e.message}") + return null + } + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/config/ImageStoreConfig.kt b/image-store/src/main/kotlin/com/walking/image/config/ImageStoreConfig.kt new file mode 100644 index 00000000..4d746ef3 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/config/ImageStoreConfig.kt @@ -0,0 +1,16 @@ +package com.walking.image.config + +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +@Configuration +@ComponentScan(basePackages = [ImageStoreConfig.BASE_PACKAGE]) +class ImageStoreConfig { + companion object { + const val BASE_PACKAGE = "com.walking.image" + const val SERVICE_NAME = "walking" + const val MODULE_NAME = "image-store" + const val BEAN_NAME_PREFIX = "imageStore" + const val PROPERTY_PREFIX = SERVICE_NAME + "." + MODULE_NAME + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/config/MinioImageStoreClientConfig.kt b/image-store/src/main/kotlin/com/walking/image/config/MinioImageStoreClientConfig.kt new file mode 100644 index 00000000..27632da1 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/config/MinioImageStoreClientConfig.kt @@ -0,0 +1,57 @@ +package com.walking.image.config + +import com.walking.image.MinioImageStoreClient +import io.minio.BucketExistsArgs +import io.minio.MakeBucketArgs +import io.minio.MinioClient +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationListener +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.context.event.ContextRefreshedEvent + +@Profile("local") +@Configuration +class MinioImageStoreClientConfig( + @Value("\${minio.url}") val url: String, + @Value("\${minio.access-key}") val accessKey: String, + @Value("\${minio.secret-key}") val secretKey: String, + @Value("\${minio.bucket-name}") val bucket: String +) : ApplicationListener { + + val log: Logger = LoggerFactory.getLogger(MinioImageStoreClientConfig::class.java) + + private var client: MinioClient? = null + + override fun onApplicationEvent(event: ContextRefreshedEvent) { + client?.bucketExists( + BucketExistsArgs.builder() + .bucket(bucket) + .build() + )?.let { exist -> + if (!exist) { + client?.makeBucket( + MakeBucketArgs.builder() + .bucket(bucket) + .build() + ) + log.info("Create bucket $bucket") + } + log.info("Bucket $bucket already exists") + } + } + + @Bean + fun minioImageStoreClient(): MinioImageStoreClient { + MinioClient.builder() + .endpoint(url) + .credentials(accessKey, secretKey) + .build().let { client -> + this.client = client + return MinioImageStoreClient(client) + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/config/S3ImageStoreConfig.kt b/image-store/src/main/kotlin/com/walking/image/config/S3ImageStoreConfig.kt new file mode 100644 index 00000000..043e2f8f --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/config/S3ImageStoreConfig.kt @@ -0,0 +1,57 @@ +package com.walking.image.config + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.services.s3.AmazonS3Client +import com.walking.image.S3ImageStoreClient +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationListener +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.context.event.ContextRefreshedEvent + +@Profile("!local") +@Configuration +class S3ImageStoreConfig( + @Value("\${s3.url}") val url: String, + @Value("\${s3.access-key}") val accessKey: String, + @Value("\${s3.secret-key}") val secretKey: String, + @Value("\${s3.bucket-name}") val bucket: String, + @Value("\${s3.region}:ap-northeast-2") val region: String +) : ApplicationListener { + + var log: Logger = LoggerFactory.getLogger(S3ImageStoreConfig::class.java) + + private var client: AmazonS3Client? = null + + override fun onApplicationEvent(event: ContextRefreshedEvent) { + client?.let { client -> + if (!client.doesBucketExistV2(bucket)) { + client.createBucket(bucket) + log.info("Create bucket $bucket") + } else { + log.info("Bucket $bucket already exists") + } + } + } + + @Bean + fun s3ImageStoreClient(): S3ImageStoreClient { + AmazonS3Client.builder() + .withRegion(region) + .withCredentials( + AWSStaticCredentialsProvider( + BasicAWSCredentials( + accessKey, + secretKey + ) + ) + ).build().let { client -> + this.client = client as AmazonS3Client + return S3ImageStoreClient(client, region) + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/GetPreSignedImageUrlService.kt b/image-store/src/main/kotlin/com/walking/image/service/GetPreSignedImageUrlService.kt new file mode 100644 index 00000000..9fda3390 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/GetPreSignedImageUrlService.kt @@ -0,0 +1,5 @@ +package com.walking.image.service + +fun interface GetPreSignedImageUrlService { + fun execute(image: String): String +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/RemoveImageService.kt b/image-store/src/main/kotlin/com/walking/image/service/RemoveImageService.kt new file mode 100644 index 00000000..c6b248b7 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/RemoveImageService.kt @@ -0,0 +1,5 @@ +package com.walking.image.service + +fun interface RemoveImageService { + fun execute(image: String): Boolean +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/UploadImageService.kt b/image-store/src/main/kotlin/com/walking/image/service/UploadImageService.kt new file mode 100644 index 00000000..5b180841 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/UploadImageService.kt @@ -0,0 +1,8 @@ +package com.walking.image.service + +import com.walking.image.ImageWriteResponse +import java.io.File + +fun interface UploadImageService { + fun execute(name: String, file: File): ImageWriteResponse +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/minio/MinioGetPreSignedImageUrlService.kt b/image-store/src/main/kotlin/com/walking/image/service/minio/MinioGetPreSignedImageUrlService.kt new file mode 100644 index 00000000..bee8b854 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/minio/MinioGetPreSignedImageUrlService.kt @@ -0,0 +1,21 @@ +package com.walking.image.service.minio + +import com.walking.image.MinioImageStoreClient +import com.walking.image.service.GetPreSignedImageUrlService +import com.walking.image.util.ImageArgsGenerator +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service + +@Profile("local") +@Service +class MinioGetPreSignedImageUrlService( + @Value("\${minio.bucket-name}") val bucket: String, + private val imageStoreClient: MinioImageStoreClient +) : GetPreSignedImageUrlService { + override fun execute(image: String): String { + ImageArgsGenerator.preSignedUrl(bucket, image).let { args -> + return imageStoreClient.getPresignedObjectUrl(args) ?: throw Exception("Failed to get pre-signed url") + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/minio/MinioRemoveImageService.kt b/image-store/src/main/kotlin/com/walking/image/service/minio/MinioRemoveImageService.kt new file mode 100644 index 00000000..14762707 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/minio/MinioRemoveImageService.kt @@ -0,0 +1,21 @@ +package com.walking.image.service.minio + +import com.walking.image.MinioImageStoreClient +import com.walking.image.service.RemoveImageService +import com.walking.image.util.ImageArgsGenerator +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service + +@Profile("local") +@Service +class MinioRemoveImageService( + @Value("\${minio.bucket-name}") val bucket: String, + private val imageStoreClient: MinioImageStoreClient +) : RemoveImageService { + override fun execute(image: String): Boolean { + ImageArgsGenerator.remove(bucket, image).let { args -> + return imageStoreClient.removeObject(args) + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/minio/MinioUploadImageService.kt b/image-store/src/main/kotlin/com/walking/image/service/minio/MinioUploadImageService.kt new file mode 100644 index 00000000..f1b8fe37 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/minio/MinioUploadImageService.kt @@ -0,0 +1,23 @@ +package com.walking.image.service.minio + +import com.walking.image.ImageWriteResponse +import com.walking.image.MinioImageStoreClient +import com.walking.image.service.UploadImageService +import com.walking.image.util.ImageArgsGenerator +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service +import java.io.File + +@Profile("local") +@Service +class MinioUploadImageService( + @Value("\${minio.bucket-name}") val bucket: String, + private val imageStoreClient: MinioImageStoreClient +) : UploadImageService { + override fun execute(name: String, file: File): ImageWriteResponse { + ImageArgsGenerator.putImage(bucket, name, file).let { args -> + return imageStoreClient.putObject(args) ?: throw Exception("Failed to upload image") + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/s3/S3GetPreSignedImageUrlService.kt b/image-store/src/main/kotlin/com/walking/image/service/s3/S3GetPreSignedImageUrlService.kt new file mode 100644 index 00000000..7d882c07 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/s3/S3GetPreSignedImageUrlService.kt @@ -0,0 +1,21 @@ +package com.walking.image.service.s3 + +import com.walking.image.S3ImageStoreClient +import com.walking.image.service.GetPreSignedImageUrlService +import com.walking.image.util.ImageArgsGenerator +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service + +@Profile("!local") +@Service +class S3GetPreSignedImageUrlService( + @Value("\${s3.bucket-name}") val bucket: String, + private val imageStoreClient: S3ImageStoreClient +) : GetPreSignedImageUrlService { + override fun execute(image: String): String { + ImageArgsGenerator.preSignedUrl(bucket, image).let { args -> + return imageStoreClient.getPresignedObjectUrl(args) ?: throw Exception("Failed to get pre-signed url") + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/s3/S3RemoveImageService.kt b/image-store/src/main/kotlin/com/walking/image/service/s3/S3RemoveImageService.kt new file mode 100644 index 00000000..45074dca --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/s3/S3RemoveImageService.kt @@ -0,0 +1,21 @@ +package com.walking.image.service.s3 + +import com.walking.image.S3ImageStoreClient +import com.walking.image.service.RemoveImageService +import com.walking.image.util.ImageArgsGenerator +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service + +@Profile("!local") +@Service +class S3RemoveImageService( + @Value("\${s3.bucket-name}") val bucket: String, + private val imageStoreClient: S3ImageStoreClient +) : RemoveImageService { + override fun execute(image: String): Boolean { + ImageArgsGenerator.remove(bucket, image).let { args -> + return imageStoreClient.removeObject(args) + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/service/s3/S3UploadImageService.kt b/image-store/src/main/kotlin/com/walking/image/service/s3/S3UploadImageService.kt new file mode 100644 index 00000000..625e35c6 --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/service/s3/S3UploadImageService.kt @@ -0,0 +1,23 @@ +package com.walking.image.service.s3 + +import com.walking.image.ImageWriteResponse +import com.walking.image.S3ImageStoreClient +import com.walking.image.service.UploadImageService +import com.walking.image.util.ImageArgsGenerator +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service +import java.io.File + +@Profile("!local") +@Service +class S3UploadImageService( + @Value("\${s3.bucket-name}") val bucket: String, + private val imageStoreClient: S3ImageStoreClient +) : UploadImageService { + override fun execute(name: String, file: File): ImageWriteResponse { + ImageArgsGenerator.putImage(bucket, name, file).let { args -> + return imageStoreClient.putObject(args) ?: throw Exception("Failed to upload image") + } + } +} \ No newline at end of file diff --git a/image-store/src/main/kotlin/com/walking/image/util/ImageArgsGenerator.kt b/image-store/src/main/kotlin/com/walking/image/util/ImageArgsGenerator.kt new file mode 100644 index 00000000..ede3e79d --- /dev/null +++ b/image-store/src/main/kotlin/com/walking/image/util/ImageArgsGenerator.kt @@ -0,0 +1,25 @@ +package com.walking.image.util + +import com.walking.image.ImageGetPresignedObjectUrlArgs +import com.walking.image.ImagePutObjectArgs +import com.walking.image.ImageRemoveObjectArgs +import io.minio.http.Method +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream + +class ImageArgsGenerator { + companion object { + fun preSignedUrl(bucket: String, image: String): ImageGetPresignedObjectUrlArgs { + return ImageGetPresignedObjectUrlArgs(bucket, image, Method.GET.toString()) + } + + fun putImage(bucket: String, name: String, image: File, contentType: String = "image/jpg"): ImagePutObjectArgs { + return ImagePutObjectArgs(bucket, name, BufferedInputStream(FileInputStream(image)), image.length(), -1, contentType) + } + + fun remove(bucket: String, image: String): ImageRemoveObjectArgs { + return ImageRemoveObjectArgs(bucket, image) + } + } +} \ No newline at end of file diff --git a/image-store/src/main/resources/application-iamge-store.yml b/image-store/src/main/resources/application-iamge-store.yml new file mode 100644 index 00000000..1b38de17 --- /dev/null +++ b/image-store/src/main/resources/application-iamge-store.yml @@ -0,0 +1,6 @@ +s3: + url: ${S3_URL} + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} + bucket-name: ${S3_BUCKET_NAME} + region: ${S3_REGION} diff --git a/image-store/src/main/resources/application-image-store-dev.yml b/image-store/src/main/resources/application-image-store-dev.yml new file mode 100644 index 00000000..1b38de17 --- /dev/null +++ b/image-store/src/main/resources/application-image-store-dev.yml @@ -0,0 +1,6 @@ +s3: + url: ${S3_URL} + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} + bucket-name: ${S3_BUCKET_NAME} + region: ${S3_REGION} diff --git a/image-store/src/main/resources/application-image-store-local.yml b/image-store/src/main/resources/application-image-store-local.yml new file mode 100644 index 00000000..b81d17c0 --- /dev/null +++ b/image-store/src/main/resources/application-image-store-local.yml @@ -0,0 +1,5 @@ +minio: + url: http://localhost:9000 + access-key: thisisroot + secret-key: thisisroot + bucket-name: picture diff --git a/image-store/src/main/resources/application-image-store-prod.yml b/image-store/src/main/resources/application-image-store-prod.yml new file mode 100644 index 00000000..1b38de17 --- /dev/null +++ b/image-store/src/main/resources/application-image-store-prod.yml @@ -0,0 +1,6 @@ +s3: + url: ${S3_URL} + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} + bucket-name: ${S3_BUCKET_NAME} + region: ${S3_REGION} diff --git a/member-api/build.gradle b/member-api/build.gradle new file mode 100644 index 00000000..eea57694 --- /dev/null +++ b/member-api/build.gradle @@ -0,0 +1,39 @@ +bootJar { + enabled = false +} + +jar { + enabled = true +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'org.jlleitschuh.gradle.ktlint-idea' +apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'org.jetbrains.kotlin.plugin.jpa' +apply plugin: 'org.jetbrains.kotlin.plugin.spring' + +dependencies { + api project(':api-repository') + implementation project(':image-store') + + // kotlin reflect + runtimeOnly "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // spring + implementation 'org.springframework.boot:spring-boot-starter-web' + + // spring cache + implementation 'org.springframework.boot:spring-boot-starter-cache' + + // jackson + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + implementation group: 'com.google.code.findbugs', name: 'jsr305', version: "${jsr305Version}" + + // retry + implementation "org.apache.httpcomponents:httpclient:${httpclientVersion}" + implementation 'org.springframework.retry:spring-retry:1.2.5.RELEASE' +} diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/config/RestTemplateConfig.kt b/member-api/src/main/kotlin/com/walking/member/api/client/config/RestTemplateConfig.kt new file mode 100644 index 00000000..45052df6 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/config/RestTemplateConfig.kt @@ -0,0 +1,47 @@ +package com.walking.member.api.client.config + +import com.walking.member.api.client.config.interceptor.LogClientHttpRequestInterceptor +import com.walking.member.api.client.config.interceptor.RetryPolicyClientHttpRequestInterceptor +import org.apache.http.client.HttpClient +import org.apache.http.impl.client.HttpClientBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.client.BufferingClientHttpRequestFactory +import org.springframework.http.client.ClientHttpRequestInterceptor +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory +import org.springframework.web.client.RestTemplate + +@Configuration +class RestTemplateConfig( + private val retryPolicyClientHttpRequestInterceptor: RetryPolicyClientHttpRequestInterceptor, + private val logClientHttpRequestInterceptor: LogClientHttpRequestInterceptor +) { + @Bean + fun restTemplate( + @Value("\${client.timeout.connect}") connectTimeout: Int, + @Value("\${client.timeout.read}") readTimeout: Int, + @Value("\${client.pool.max-connect}") maxConnectPool: Int, + @Value("\${client.pool.max-connect-per-route}") maxPerRouteConnectPool: Int + ): RestTemplate { + val factory = HttpComponentsClientHttpRequestFactory() + factory.setReadTimeout(readTimeout) + factory.setConnectTimeout(connectTimeout) + val httpClient: HttpClient = HttpClientBuilder.create() + .setMaxConnTotal(maxConnectPool) + .setMaxConnPerRoute(maxPerRouteConnectPool) + .build() + factory.setHttpClient(httpClient) + + val restTemplate = RestTemplate(BufferingClientHttpRequestFactory(factory)) + var interceptors: MutableList = + restTemplate.getInterceptors() + if (interceptors.isEmpty()) { + interceptors = ArrayList() + } + interceptors.add(retryPolicyClientHttpRequestInterceptor) + interceptors.add(logClientHttpRequestInterceptor) + restTemplate.setInterceptors(interceptors) + return restTemplate + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/config/RetryTemplateConfig.kt b/member-api/src/main/kotlin/com/walking/member/api/client/config/RetryTemplateConfig.kt new file mode 100644 index 00000000..f289107e --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/config/RetryTemplateConfig.kt @@ -0,0 +1,34 @@ +package com.walking.member.api.client.config + +import com.walking.member.api.client.config.listener.ClientListener +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.retry.annotation.EnableRetry +import org.springframework.retry.backoff.FixedBackOffPolicy +import org.springframework.retry.policy.SimpleRetryPolicy +import org.springframework.retry.support.RetryTemplate + +@Configuration +@EnableRetry +class RetryTemplateConfig( + private val clientListener: ClientListener +) { + @Bean + fun retryTemplate( + @Value("\${client.retry.maxAttempts}") maxAttempts: Int, + @Value("\${client.retry.backOffPeriod}") backOffPeriod: Int + ): RetryTemplate { + val backOffPolicy = FixedBackOffPolicy() + backOffPolicy.backOffPeriod = backOffPeriod.toLong() + + val retryPolicy = SimpleRetryPolicy() + retryPolicy.maxAttempts = maxAttempts + + val retryTemplate = RetryTemplate() + retryTemplate.setBackOffPolicy(backOffPolicy) + retryTemplate.setRetryPolicy(retryPolicy) + retryTemplate.registerListener(clientListener) + return retryTemplate + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/config/interceptor/LogClientHttpRequestInterceptor.kt b/member-api/src/main/kotlin/com/walking/member/api/client/config/interceptor/LogClientHttpRequestInterceptor.kt new file mode 100644 index 00000000..dcc12852 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/config/interceptor/LogClientHttpRequestInterceptor.kt @@ -0,0 +1,60 @@ +package com.walking.member.api.client.config.interceptor + +import org.slf4j.Logger +import org.springframework.http.HttpRequest +import org.springframework.http.client.ClientHttpRequestExecution +import org.springframework.http.client.ClientHttpRequestInterceptor +import org.springframework.http.client.ClientHttpResponse +import org.springframework.stereotype.Component +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.util.stream.Collectors + +@Component +class LogClientHttpRequestInterceptor : ClientHttpRequestInterceptor { + val log: Logger = org.slf4j.LoggerFactory.getLogger(LogClientHttpRequestInterceptor::class.java) + + @Throws(IOException::class) + override fun intercept( + request: HttpRequest, + body: ByteArray, + execution: ClientHttpRequestExecution + ): ClientHttpResponse { + val uuid = clientHttpRequestUUID() + doLogRequest(uuid, request, body) + val response = execution.execute(request, body) + doLogResponse(uuid, response) + return response + } + + private fun clientHttpRequestUUID(): String { + return (Math.random() * 1000000).toInt().toString() + } + + private fun doLogRequest(sessionNumber: String, req: HttpRequest, body: ByteArray) { + log.info( + "[{}] URI: {}, Method: {}, Headers:{}, Body:{} ", + sessionNumber, + req.uri, + req.method, + req.headers, + String(body, StandardCharsets.UTF_8) + ) + } + + @Throws(IOException::class) + private fun doLogResponse(sessionNumber: String, res: ClientHttpResponse) { + val body = BufferedReader(InputStreamReader(res.body, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")) + log.info( + "[{}] Status: {}, Headers:{}, Body:{} ", + sessionNumber, + res.statusCode, + res.headers, + body + ) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/config/interceptor/RetryPolicyClientHttpRequestInterceptor.kt b/member-api/src/main/kotlin/com/walking/member/api/client/config/interceptor/RetryPolicyClientHttpRequestInterceptor.kt new file mode 100644 index 00000000..1593249f --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/config/interceptor/RetryPolicyClientHttpRequestInterceptor.kt @@ -0,0 +1,37 @@ +package com.walking.member.api.client.config.interceptor + +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpRequest +import org.springframework.http.client.ClientHttpRequestExecution +import org.springframework.http.client.ClientHttpRequestInterceptor +import org.springframework.http.client.ClientHttpResponse +import org.springframework.retry.policy.SimpleRetryPolicy +import org.springframework.retry.support.RetryTemplate +import org.springframework.stereotype.Component +import java.io.IOException + +@Component +class RetryPolicyClientHttpRequestInterceptor( + @Value("\${client.max-attempts:3}") private val maxAttempts: Int +) : ClientHttpRequestInterceptor { + @Throws(IOException::class) + override fun intercept( + request: HttpRequest, + body: ByteArray, + execution: ClientHttpRequestExecution + ): ClientHttpResponse { + val retryTemplate = RetryTemplate() + retryTemplate.setRetryPolicy(SimpleRetryPolicy(maxAttempts)) + + return try { + retryTemplate.execute { + execution.execute( + request, + body + ) + } + } catch (e: Exception) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/config/listener/ClientListener.kt b/member-api/src/main/kotlin/com/walking/member/api/client/config/listener/ClientListener.kt new file mode 100644 index 00000000..f8a9ee91 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/config/listener/ClientListener.kt @@ -0,0 +1,37 @@ +package com.walking.member.api.client.config.listener + +import org.springframework.retry.RetryCallback +import org.springframework.retry.RetryContext +import org.springframework.retry.listener.RetryListenerSupport +import org.springframework.stereotype.Component + +@Component +class ClientListener : RetryListenerSupport() { + private val log = org.slf4j.LoggerFactory.getLogger(ClientListener::class.java) + + override fun open( + context: RetryContext, + callback: RetryCallback + ): Boolean { + log.debug("request to kakao api") + return super.open(context, callback) + } + + override fun close( + context: RetryContext, + callback: RetryCallback, + throwable: Throwable + ) { + log.debug("success get response kakao api") + super.close(context, callback, throwable) + } + + override fun onError( + context: RetryContext, + callback: RetryCallback, + throwable: Throwable + ) { + log.error("fail get response kakao api : {}", throwable.message) + super.onError(context, callback, throwable) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/config/property/KaKaoApiProperties.kt b/member-api/src/main/kotlin/com/walking/member/api/client/config/property/KaKaoApiProperties.kt new file mode 100644 index 00000000..1038b9f1 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/config/property/KaKaoApiProperties.kt @@ -0,0 +1,16 @@ +package com.walking.member.api.client.config.property + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component + +@Component +data class KaKaoApiProperties( + @Value("\${kakao.host}") val host: String, + @Value("\${kakao.adminKey}") val adminKey: String, + @Value("\${kakao.uri.token}") val uriToken: String, + @Value("\${kakao.uri.token_info}") val uriTokenInfo: String, + @Value("\${kakao.uri.me_info}") val uriMeInfo: String, + @Value("\${kakao.uri.unlink}") val unlink: String, + @Value("\${kakao.redirect_uri}") val redirectUri: String, + @Value("\${kakao.client_id}") val clientId: String +) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/config/property/KaKaoIdTokenProperties.kt b/member-api/src/main/kotlin/com/walking/member/api/client/config/property/KaKaoIdTokenProperties.kt new file mode 100644 index 00000000..51fd906f --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/config/property/KaKaoIdTokenProperties.kt @@ -0,0 +1,10 @@ +package com.walking.member.api.client.config.property + +data class +KaKaoIdTokenProperties( + val iss: String, + val aud: String, + val sub: String, + val nickname: String, + val picture: String +) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/exception/SocialClientException.kt b/member-api/src/main/kotlin/com/walking/member/api/client/exception/SocialClientException.kt new file mode 100644 index 00000000..17d554bb --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/exception/SocialClientException.kt @@ -0,0 +1,3 @@ +package com.walking.member.api.client.exception + +class SocialClientException(message: String = "클라이언트 에러가 발생하였습니다.") : RuntimeException(message) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/exception/SocialIntegrationException.kt b/member-api/src/main/kotlin/com/walking/member/api/client/exception/SocialIntegrationException.kt new file mode 100644 index 00000000..e51ba26d --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/exception/SocialIntegrationException.kt @@ -0,0 +1,4 @@ +package com.walking.member.api.client.exception + +class SocialIntegrationException(message: String = "소셜 연동 중 오류가 발생했습니다.") : + RuntimeException(message) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/member/KaKaoMemberClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/member/KaKaoMemberClient.kt new file mode 100644 index 00000000..1b240c5d --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/member/KaKaoMemberClient.kt @@ -0,0 +1,44 @@ +package com.walking.member.api.client.member + +import com.walking.member.api.client.config.property.KaKaoApiProperties +import com.walking.member.api.client.exception.SocialClientException +import com.walking.member.api.client.member.dto.KaKaoMemberData +import com.walking.member.api.client.member.dto.SocialMemberData +import com.walking.member.api.client.util.addKakaoHeader +import lombok.RequiredArgsConstructor +import lombok.extern.slf4j.Slf4j +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate + +@Slf4j +@Component +@RequiredArgsConstructor +class KaKaoMemberClient( + private val restTemplate: RestTemplate, + private val properties: KaKaoApiProperties +) : SocialMemberClient { + override fun execute(targetId: String): SocialMemberData { + val adminKey = properties.adminKey + + val headers = HttpHeaders() + headers.addKakaoHeader(adminKey) + headers.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8") + val queryParameter = "?target_id_type=user_id&target_id=$targetId" + val response: ResponseEntity = restTemplate.exchange( + properties.uriMeInfo + queryParameter, + HttpMethod.POST, + HttpEntity(null, headers), + KaKaoMemberData::class.java + ) + + val statusCode = response.statusCode + if (statusCode.is4xxClientError) { + throw SocialClientException() + } + return response.body!! + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/member/SocialMemberClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/member/SocialMemberClient.kt new file mode 100644 index 00000000..e585e7a9 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/member/SocialMemberClient.kt @@ -0,0 +1,11 @@ +package com.walking.member.api.client.member + +import com.walking.member.api.client.member.dto.SocialMemberData + +fun interface SocialMemberClient { + /** + * Get social member data by userId + * @param targetId social user id + */ + fun execute(targetId: String): SocialMemberData +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/member/dto/KaKaoMemberData.kt b/member-api/src/main/kotlin/com/walking/member/api/client/member/dto/KaKaoMemberData.kt new file mode 100644 index 00000000..f7baaef0 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/member/dto/KaKaoMemberData.kt @@ -0,0 +1,19 @@ +package com.walking.member.api.client.member.dto + +data class KaKaoMemberData(val id: String) : SocialMemberData { + val properties: Properties? = null + + data class Properties(val nickname: String, val profile_image: String) + + override fun getName(): String { + return properties?.nickname ?: "" + } + + override fun getId(): Long { + return id.toLong() + } + + fun getPicture(): String { + return properties?.profile_image ?: "" + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/member/dto/SocialMemberData.kt b/member-api/src/main/kotlin/com/walking/member/api/client/member/dto/SocialMemberData.kt new file mode 100644 index 00000000..ebe46415 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/member/dto/SocialMemberData.kt @@ -0,0 +1,6 @@ +package com.walking.member.api.client.member.dto + +interface SocialMemberData { + fun getName(): String + fun getId(): Long +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/support/KaKoIdTokenParser.kt b/member-api/src/main/kotlin/com/walking/member/api/client/support/KaKoIdTokenParser.kt new file mode 100644 index 00000000..d1194ce6 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/support/KaKoIdTokenParser.kt @@ -0,0 +1,30 @@ +package com.walking.member.api.client.support + +import com.walking.member.api.client.config.property.KaKaoApiProperties +import com.walking.member.api.client.config.property.KaKaoIdTokenProperties +import com.walking.member.api.client.exception.SocialIntegrationException +import com.walking.member.api.client.util.TokenPropertiesMapper +import org.springframework.stereotype.Component +import java.util.* + +@Component +class KaKoIdTokenParser( + private val kaKaoApiProperties: KaKaoApiProperties, + private val tokenPropertiesMapper: TokenPropertiesMapper +) { + val payloadIndex = 1 + fun parse(idToken: String): KaKaoIdTokenProperties { + val payload = idToken.split("\\.".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[payloadIndex] + val source = String(Base64.getDecoder().decode(payload)) + val idProperties: KaKaoIdTokenProperties = + tokenPropertiesMapper.read(source, KaKaoIdTokenProperties::class.java) + if (idProperties.iss != kaKaoApiProperties.host) { + throw SocialIntegrationException() + } + if (idProperties.aud != kaKaoApiProperties.clientId) { + throw SocialIntegrationException() + } + return idProperties + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/KaKaoIdTokenClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/KaKaoIdTokenClient.kt new file mode 100644 index 00000000..5aff70af --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/KaKaoIdTokenClient.kt @@ -0,0 +1,59 @@ +package com.walking.member.api.client.token + +import SocialIdTokenClient +import com.walking.member.api.client.config.property.KaKaoApiProperties +import com.walking.member.api.client.exception.SocialClientException +import com.walking.member.api.client.token.dto.KaKaoIdTokenData +import com.walking.member.api.client.token.dto.SocialIdToken +import lombok.RequiredArgsConstructor +import lombok.extern.slf4j.Slf4j +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Component +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.client.RestClientException +import org.springframework.web.client.RestTemplate + +@Slf4j +@Component +@RequiredArgsConstructor +class KaKaoIdTokenClient( + private val restTemplate: RestTemplate, + private val properties: KaKaoApiProperties +) : SocialIdTokenClient { + override fun execute(code: String): SocialIdToken { + val headers = HttpHeaders() + headers.add("Accept", "application/json") + headers.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8") + + val params: MultiValueMap = LinkedMultiValueMap() + params.add("grant_type", "authorization_code") + params.add("client_id", properties.clientId) + params.add("redirect_uri", properties.redirectUri) + params.add("code", code) + + val httpEntity = HttpEntity(params, headers) + + val response: ResponseEntity + try { + response = restTemplate.exchange( + properties.uriToken, + HttpMethod.POST, + httpEntity, + KaKaoIdTokenData::class.java + ) + } catch (e: RestClientException) { + throw SocialClientException() + } + + val statusCode = response.statusCode + if (statusCode.is4xxClientError) { + throw SocialClientException() + } + + return response.body as SocialIdToken + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/KaKaoTokenInfoClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/KaKaoTokenInfoClient.kt new file mode 100644 index 00000000..ac5648b3 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/KaKaoTokenInfoClient.kt @@ -0,0 +1,42 @@ + +import com.walking.member.api.client.config.property.KaKaoApiProperties +import com.walking.member.api.client.exception.SocialClientException +import com.walking.member.api.client.token.SocialTokenClient +import com.walking.member.api.client.token.dto.KaKaoTokenData +import com.walking.member.api.client.token.dto.SocialIdToken +import com.walking.member.api.client.token.dto.SocialToken +import com.walking.member.api.client.util.addBearerHeader +import lombok.RequiredArgsConstructor +import lombok.extern.slf4j.Slf4j +import org.springframework.http.* +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate + +@Slf4j +@Component +@RequiredArgsConstructor +class KaKaoTokenInfoClient( + private val restTemplate: RestTemplate, + private val properties: KaKaoApiProperties +) : SocialTokenClient { + override fun execute(token: SocialIdToken): SocialToken { + val accessToken: String = token.getToken() + val headers = HttpHeaders() + headers.addBearerHeader(accessToken) + headers.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8") + + val response: ResponseEntity = restTemplate.exchange( + properties.uriTokenInfo, + HttpMethod.GET, + HttpEntity(null, headers), + KaKaoTokenData::class.java + ) + + val statusCode: HttpStatus = response.statusCode + if (statusCode.is4xxClientError) { + throw SocialClientException() + } + + return response.body!! + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/RetryAbleKaKaoTokenClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/RetryAbleKaKaoTokenClient.kt new file mode 100644 index 00000000..a4a397ae --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/RetryAbleKaKaoTokenClient.kt @@ -0,0 +1,40 @@ +package com.walking.member.api.client.token + +import SocialIdTokenClient +import com.walking.member.api.client.exception.SocialClientException +import com.walking.member.api.client.token.dto.SocialIdToken +import lombok.RequiredArgsConstructor +import lombok.extern.slf4j.Slf4j +import org.slf4j.Logger +import org.springframework.retry.RecoveryCallback +import org.springframework.retry.RetryCallback +import org.springframework.retry.support.RetryTemplate +import org.springframework.stereotype.Component + +@Slf4j +@Component +@RequiredArgsConstructor +class RetryAbleKaKaoTokenClient( + private val retryTemplate: RetryTemplate, + private val kaKaoIdTokenClient: KaKaoIdTokenClient +) : SocialIdTokenClient { + val log: Logger = org.slf4j.LoggerFactory.getLogger(RetryAbleKaKaoTokenClient::class.java) + + override fun execute(code: String): SocialIdToken { + return retryTemplate.execute( + RetryCallback { retryContext -> + log.error("something wrong at KaKao api") + log.error( + "KaKao get token retry count: {}", + retryContext.retryCount + ) + kaKaoIdTokenClient.execute(code) + }, + // todo 실패 기록 + RecoveryCallback { + log.error("fail get token KaKao api") + null + } + ) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/SocialIdTokenClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/SocialIdTokenClient.kt new file mode 100644 index 00000000..724f12b4 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/SocialIdTokenClient.kt @@ -0,0 +1,10 @@ +import com.walking.member.api.client.token.dto.SocialIdToken + +fun interface SocialIdTokenClient { + + /** + * Get social id token by auth code + * @param code social auth code + */ + fun execute(code: String): SocialIdToken +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/SocialTokenClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/SocialTokenClient.kt new file mode 100644 index 00000000..52f88d12 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/SocialTokenClient.kt @@ -0,0 +1,13 @@ +package com.walking.member.api.client.token + +import com.walking.member.api.client.token.dto.SocialIdToken +import com.walking.member.api.client.token.dto.SocialToken + +fun interface SocialTokenClient { + + /** + * Get social token by social id token + * @param token social id token + */ + fun execute(token: SocialIdToken): SocialToken +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/KaKaoIdTokenData.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/KaKaoIdTokenData.kt new file mode 100644 index 00000000..db2a046e --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/KaKaoIdTokenData.kt @@ -0,0 +1,7 @@ +package com.walking.member.api.client.token.dto + +data class KaKaoIdTokenData(val id_token: String) : SocialIdToken { + override fun getToken(): String { + return id_token + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/KaKaoTokenData.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/KaKaoTokenData.kt new file mode 100644 index 00000000..c65e3c13 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/KaKaoTokenData.kt @@ -0,0 +1,7 @@ +package com.walking.member.api.client.token.dto + +data class KaKaoTokenData(val id: String) : SocialToken { + override fun getInfo(): String { + return id + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/SocialIdToken.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/SocialIdToken.kt new file mode 100644 index 00000000..45c1161d --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/SocialIdToken.kt @@ -0,0 +1,5 @@ +package com.walking.member.api.client.token.dto + +fun interface SocialIdToken { + fun getToken(): String +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/SocialToken.kt b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/SocialToken.kt new file mode 100644 index 00000000..0de875eb --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/token/dto/SocialToken.kt @@ -0,0 +1,5 @@ +package com.walking.member.api.client.token.dto + +fun interface SocialToken { + fun getInfo(): String +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/unlink/KaKaoUnlinkClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/KaKaoUnlinkClient.kt new file mode 100644 index 00000000..8caf8acd --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/KaKaoUnlinkClient.kt @@ -0,0 +1,56 @@ +package com.walking.member.api.client.unlink + +import com.walking.member.api.client.config.property.KaKaoApiProperties +import com.walking.member.api.client.exception.SocialClientException +import com.walking.member.api.client.unlink.dto.KaKaoUnlinkData +import com.walking.member.api.client.unlink.dto.SocialUnlinkData +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Component +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.client.RestClientException +import org.springframework.web.client.RestTemplate + +@Component +class KaKaoUnlinkClient( + private val restTemplate: RestTemplate, + private val properties: KaKaoApiProperties +) : SocialUnlinkClient { + override fun execute(targetId: String): SocialUnlinkData { + val adminKey: String = properties.adminKey + + val headers = HttpHeaders() + headers.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") + headers.add(HttpHeaders.AUTHORIZATION, "KakaoAK $adminKey") + + val params: MultiValueMap = LinkedMultiValueMap() + params.add("target_id_type", "user_id") + params.add("target_id", targetId.toLong()) + + val response: ResponseEntity + try { + response = restTemplate.exchange( + properties.unlink, + HttpMethod.POST, + HttpEntity(params, headers), + KaKaoUnlinkData::class.java + ) + } catch (e: RestClientException) { + throw SocialClientException() + } + + val statusCode = response.statusCode + if (statusCode.is4xxClientError) { + throw SocialClientException() + } + + return response.body!! + } + + override fun supports(type: String): Boolean { + return "KAKAO" == type.toUpperCase() + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/unlink/SocialUnlinkClient.kt b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/SocialUnlinkClient.kt new file mode 100644 index 00000000..7240c573 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/SocialUnlinkClient.kt @@ -0,0 +1,14 @@ +package com.walking.member.api.client.unlink + +import com.walking.member.api.client.unlink.dto.SocialUnlinkData + +interface SocialUnlinkClient { + + /** + * Unlink social account by targetId + * @param targetId social user id + */ + fun execute(targetId: String): SocialUnlinkData + + fun supports(type: String): Boolean +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/unlink/SocialUnlinkClientManager.kt b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/SocialUnlinkClientManager.kt new file mode 100644 index 00000000..508ce819 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/SocialUnlinkClientManager.kt @@ -0,0 +1,18 @@ +package com.walking.member.api.client.unlink + +import com.walking.member.api.client.unlink.dto.SocialUnlinkData +import org.springframework.stereotype.Component + +@Component +class SocialUnlinkClientManager( + private val socialUnlinkClients: List +) { + fun execute(socialType: String, targetId: String): SocialUnlinkData { + socialUnlinkClients.forEach { client -> + if (client.supports(socialType)) { + return client.execute(targetId) + } + } + throw IllegalArgumentException("Unsupported social type: $socialType") + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/unlink/dto/KaKaoUnlinkData.kt b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/dto/KaKaoUnlinkData.kt new file mode 100644 index 00000000..a96fbf0b --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/dto/KaKaoUnlinkData.kt @@ -0,0 +1,7 @@ +package com.walking.member.api.client.unlink.dto + +data class KaKaoUnlinkData(val id: String) : SocialUnlinkData { + override fun getUnlinkInfo(): String { + return id + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/unlink/dto/SocialUnlinkData.kt b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/dto/SocialUnlinkData.kt new file mode 100644 index 00000000..32a99343 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/unlink/dto/SocialUnlinkData.kt @@ -0,0 +1,5 @@ +package com.walking.member.api.client.unlink.dto + +fun interface SocialUnlinkData { + fun getUnlinkInfo(): String +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/util/HeadersFunction.kt b/member-api/src/main/kotlin/com/walking/member/api/client/util/HeadersFunction.kt new file mode 100644 index 00000000..f2082d28 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/util/HeadersFunction.kt @@ -0,0 +1,13 @@ +package com.walking.member.api.client.util + +import org.springframework.http.HttpHeaders + +fun HttpHeaders.addBearerHeader(token: String) { + this.add("Authorization", "Bearer $token") +} + +fun HttpHeaders.addKakaoHeader(adminKey: String) { + this.add("Authorization", "KakaoAK $adminKey") +} + +class HeadersFunction \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/client/util/TokenPropertiesMapper.kt b/member-api/src/main/kotlin/com/walking/member/api/client/util/TokenPropertiesMapper.kt new file mode 100644 index 00000000..21315ad5 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/client/util/TokenPropertiesMapper.kt @@ -0,0 +1,23 @@ +package com.walking.member.api.client.util + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class TokenPropertiesMapper( + private val objectMapper: ObjectMapper +) { + val log: Logger = LoggerFactory.getLogger(TokenPropertiesMapper::class.java) + fun read(info: String?, clazz: Class): T { + var properties: T? = null + try { + properties = objectMapper.readValue(info, clazz) + } catch (e: JsonProcessingException) { + log.error("error read token properties", e) + } + return properties!! + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/config/MemberApiCacheConfig.kt b/member-api/src/main/kotlin/com/walking/member/api/config/MemberApiCacheConfig.kt new file mode 100644 index 00000000..4a28da46 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/config/MemberApiCacheConfig.kt @@ -0,0 +1,26 @@ +package com.walking.member.api.config + +import org.springframework.cache.CacheManager +import org.springframework.cache.annotation.EnableCaching +import org.springframework.cache.concurrent.ConcurrentMapCache +import org.springframework.cache.support.SimpleCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.util.List + +@Configuration +@EnableCaching +class MemberApiCacheConfig { + + @Bean(value = [MemberApiConfig.BEAN_NAME_PREFIX + "CacheManager"]) + fun memberApiCacheManager(): CacheManager { + SimpleCacheManager().apply { + this.setCaches( + List.of( + ConcurrentMapCache("member-profile-url") + ) + ) + return this + } + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/config/MemberApiConfig.kt b/member-api/src/main/kotlin/com/walking/member/api/config/MemberApiConfig.kt new file mode 100644 index 00000000..364d5f90 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/config/MemberApiConfig.kt @@ -0,0 +1,22 @@ +package com.walking.member.api.config + +import com.walking.api.repository.config.ApiRepositoryConfig +import com.walking.image.config.ImageStoreConfig +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import + +@Import( + value = [ApiRepositoryConfig::class, ImageStoreConfig::class] +) +@Configuration +@ComponentScan(basePackages = [MemberApiConfig.BASE_PACKAGE]) +class MemberApiConfig { + companion object { + const val BASE_PACKAGE = "com.walking.member.api" + const val SERVICE_NAME = "walking" + const val MODULE_NAME = "member-api" + const val BEAN_NAME_PREFIX = "memberApi" + const val PROPERTY_PREFIX = SERVICE_NAME + "." + MODULE_NAME + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/dao/MemberDao.kt b/member-api/src/main/kotlin/com/walking/member/api/dao/MemberDao.kt new file mode 100644 index 00000000..4ad81081 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/dao/MemberDao.kt @@ -0,0 +1,37 @@ +package com.walking.member.api.dao + +import com.walking.api.repository.dao.member.MemberRepository +import com.walking.data.entity.member.MemberEntity +import org.springframework.stereotype.Repository + +@Repository +class MemberDao(private val memberRepository: MemberRepository) { + + fun save(memberEntity: MemberEntity): MemberEntity { + return memberRepository.save(memberEntity) + } + + fun findByCertificationId(certificationId: String): MemberEntity? { + return memberRepository.findByCertificationIdAndDeletedFalse(certificationId).let { + if (it.isPresent) { + it.get() + } else { + null + } + } + } + + fun findById(id: Long): MemberEntity? { + return memberRepository.findByIdAndDeletedFalse(id).let { + if (it.isPresent) { + it.get() + } else { + null + } + } + } + + fun exist(id: Long): Boolean { + return memberRepository.existsByIdAndDeletedFalse(id) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/service/CacheAbleMemberProfileUpdateDelegator.kt b/member-api/src/main/kotlin/com/walking/member/api/service/CacheAbleMemberProfileUpdateDelegator.kt new file mode 100644 index 00000000..c88183af --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/service/CacheAbleMemberProfileUpdateDelegator.kt @@ -0,0 +1,13 @@ +package com.walking.member.api.service + +import com.walking.data.entity.member.MemberEntity +import org.springframework.cache.annotation.CachePut +import org.springframework.stereotype.Service + +@Service +class CacheAbleMemberProfileUpdateDelegator { + @CachePut(key = "#entity.id", cacheManager = "memberApiCacheManager", cacheNames = ["member-profile-url"]) + fun execute(entity: MemberEntity, imageName: String): MemberEntity { + return entity.updateProfile(imageName) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/service/kakao/PostKaKaoMemberService.kt b/member-api/src/main/kotlin/com/walking/member/api/service/kakao/PostKaKaoMemberService.kt new file mode 100644 index 00000000..697c5347 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/service/kakao/PostKaKaoMemberService.kt @@ -0,0 +1,29 @@ +package com.walking.member.api.service.kakao + +import com.walking.member.api.client.support.KaKoIdTokenParser +import com.walking.member.api.client.token.KaKaoIdTokenClient +import com.walking.member.api.service.kakao.dto.SocialMemberServiceDto +import org.springframework.stereotype.Service + +@Service +class PostKaKaoMemberService(private val kaKaoIdTokenClient: KaKaoIdTokenClient, private val kaKoIdTokenParser: KaKoIdTokenParser) { + + companion object { + private const val SUBJECT = "KAKAO" + } + fun execute(code: String): SocialMemberServiceDto { + val token = kaKaoIdTokenClient.execute(code) + + val idTokenProperties = kaKoIdTokenParser.parse(token.getToken()) + val nickname = idTokenProperties.nickname + val certificationId = idTokenProperties.sub + val profile = idTokenProperties.picture + + return SocialMemberServiceDto( + nickname, + profile, + certificationId, + SUBJECT + ) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/service/kakao/dto/SocialMemberServiceDto.kt b/member-api/src/main/kotlin/com/walking/member/api/service/kakao/dto/SocialMemberServiceDto.kt new file mode 100644 index 00000000..533f8d03 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/service/kakao/dto/SocialMemberServiceDto.kt @@ -0,0 +1,8 @@ +package com.walking.member.api.service.kakao.dto + +data class SocialMemberServiceDto( + val nickName: String, + val profile: String, + val certificationId: String, + val certificationSubject: String +) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/DeleteMemberUseCase.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/DeleteMemberUseCase.kt new file mode 100644 index 00000000..f32cbf53 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/DeleteMemberUseCase.kt @@ -0,0 +1,36 @@ +package com.walking.member.api.usecase + +import com.walking.data.entity.member.MemberEntity +import com.walking.image.service.minio.MinioRemoveImageService +import com.walking.member.api.client.unlink.SocialUnlinkClientManager +import com.walking.member.api.dao.MemberDao +import com.walking.member.api.usecase.dto.response.DeleteMemberUseCaseResponse +import org.springframework.cache.annotation.CacheEvict +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class DeleteMemberUseCase( + private val memberRepository: MemberDao, + private val removeImageService: MinioRemoveImageService, + private val unlinkClientManager: SocialUnlinkClientManager +) { + @Transactional + @CacheEvict(key = "#id", cacheManager = "memberApiCacheManager", cacheNames = ["member-profile-url"]) + fun execute(id: Long): DeleteMemberUseCaseResponse { + val member = memberRepository.findById(id) ?: throw IllegalArgumentException("Member not found") + val deletedMember = withdrawMember(member) + removeImageService.execute(deletedMember.profile) + unlinkClientManager.execute( + deletedMember.certificationSubject.name, + deletedMember.certificationId + ) + + return DeleteMemberUseCaseResponse(deletedMember.id, deletedMember.updatedAt) + } + + private fun withdrawMember(member: MemberEntity): MemberEntity { + member.withDrawn() + return memberRepository.save(member) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/ExistMemberService.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/ExistMemberService.kt new file mode 100644 index 00000000..bcbdc253 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/ExistMemberService.kt @@ -0,0 +1,15 @@ +package com.walking.member.api.usecase + +import com.walking.member.api.dao.MemberDao +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ExistMemberService( + private val memberDao: MemberDao +) { + @Transactional(readOnly = true) + fun execute(id: Long): Boolean { + return memberDao.exist(id) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/GetMemberDetailUseCase.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/GetMemberDetailUseCase.kt new file mode 100644 index 00000000..355a0e3a --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/GetMemberDetailUseCase.kt @@ -0,0 +1,48 @@ +package com.walking.member.api.usecase + +import com.walking.data.entity.member.MemberEntity +import com.walking.image.service.GetPreSignedImageUrlService +import com.walking.member.api.dao.MemberDao + +import com.walking.member.api.usecase.dto.response.GetMemberDetailUseCaseResponse +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class GetMemberDetailUseCase( + private val memberRepository: MemberDao, + private val getPreSignedImageUrlService: GetPreSignedImageUrlService +) { + val log: Logger = LoggerFactory.getLogger(GetMemberDetailUseCase::class.java) + + @Transactional + @Cacheable(key = "#id", cacheManager = "memberApiCacheManager", cacheNames = ["member-profile-url"]) + fun execute(id: Long): GetMemberDetailUseCaseResponse { + val member = memberRepository.findById(id) ?: throw IllegalArgumentException("Member not found") + val id = member.id + val nickName = member.nickName + val certificationSubject = member.certificationSubject.name + val status = member.status.name + val profile = getProfile(member) + + return GetMemberDetailUseCaseResponse( + id, + nickName, + profile, + certificationSubject, + status + ) + } + + private fun getProfile(member: MemberEntity): String { + return try { + getPreSignedImageUrlService.execute(member.profile) + } catch (e: Exception) { + log.debug("Failed to get profile image: ${e.message}") + "" // todo fix 기본 이미지 + } + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/GetMemberTokenDetailUseCase.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/GetMemberTokenDetailUseCase.kt new file mode 100644 index 00000000..7a2151f5 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/GetMemberTokenDetailUseCase.kt @@ -0,0 +1,15 @@ +package com.walking.member.api.usecase + +import com.walking.member.api.dao.MemberDao +import com.walking.member.api.usecase.dto.response.GetMemberTokenDetailUseCaseResponse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class GetMemberTokenDetailUseCase(private val memberRepository: MemberDao) { + @Transactional + fun execute(id: Long): GetMemberTokenDetailUseCaseResponse { + val member = memberRepository.findById(id) ?: throw IllegalArgumentException("Member not found") + return GetMemberTokenDetailUseCaseResponse(member.id) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/PatchProfileImageUseCase.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/PatchProfileImageUseCase.kt new file mode 100644 index 00000000..8d4516c1 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/PatchProfileImageUseCase.kt @@ -0,0 +1,43 @@ +package com.walking.member.api.usecase + +import com.walking.image.service.UploadImageService +import com.walking.member.api.dao.MemberDao +import com.walking.member.api.service.CacheAbleMemberProfileUpdateDelegator +import com.walking.member.api.usecase.dto.response.PatchProfileImageUseCaseResponse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.io.File +import java.time.LocalDate +import kotlin.random.Random + +@Service +class PatchProfileImageUseCase( + private val memberDao: MemberDao, + private val uploadImageService: UploadImageService, + private val memberProfileUpdateDelegator: CacheAbleMemberProfileUpdateDelegator +) { + @Transactional + fun execute(id: Long, image: File): PatchProfileImageUseCaseResponse { + val member = memberDao.findById(id) ?: throw IllegalArgumentException("Member not found") + val imageName = generateImageName() + + uploadImageService.execute(imageName, image).let { + memberProfileUpdateDelegator.execute(member, imageName).let { member -> + memberDao.save(member) + return PatchProfileImageUseCaseResponse(member.id, member.nickName, imageName) + } + } + } + + private fun generateImageName(): String { + val now = LocalDate.now() + return "${now.year}${now.monthValue}${now.dayOfMonth}_${randomString()}" + } + + private fun randomString(): String { + val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return (1..16) + .map { Random.nextInt(0, charPool.size).let { charPool[it] } } + .joinToString("") + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/PostMemberUseCase.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/PostMemberUseCase.kt new file mode 100644 index 00000000..eb4f4e06 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/PostMemberUseCase.kt @@ -0,0 +1,38 @@ +package com.walking.member.api.usecase + +import com.walking.data.entity.member.MemberEntity +import com.walking.member.api.dao.MemberDao +import com.walking.member.api.service.kakao.dto.SocialMemberServiceDto +import com.walking.member.api.usecase.dto.response.PostMemberUseCaseResponse +import com.walking.member.api.service.kakao.PostKaKaoMemberService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class PostMemberUseCase( + private val createKaKaoMemberService: PostKaKaoMemberService, + private val memberRepository: MemberDao +) { + @Transactional + fun execute(code: String): PostMemberUseCaseResponse { + val socialMember = createKaKaoMemberService.execute(code) + + memberRepository.findByCertificationId(socialMember.certificationId) + ?.let { member -> + return PostMemberUseCaseResponse(member.id, member.nickName, member.profile) + } + + val newMember = createMemberEntity(socialMember) + memberRepository.save(newMember).let { member -> + return PostMemberUseCaseResponse(member.id, member.nickName, member.profile) + } + } + + private fun createMemberEntity(socialMember: SocialMemberServiceDto): MemberEntity { + return MemberEntity( + socialMember.nickName, + socialMember.profile, + socialMember.certificationId + ) + } +} \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/DeleteMemberUseCaseResponse.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/DeleteMemberUseCaseResponse.kt new file mode 100644 index 00000000..e8196c45 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/DeleteMemberUseCaseResponse.kt @@ -0,0 +1,8 @@ +package com.walking.member.api.usecase.dto.response + +import java.time.LocalDateTime + +data class DeleteMemberUseCaseResponse( + val id: Long, + val deletedAt: LocalDateTime +) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/GetMemberDetailUseCaseResponse.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/GetMemberDetailUseCaseResponse.kt new file mode 100644 index 00000000..1edc55fa --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/GetMemberDetailUseCaseResponse.kt @@ -0,0 +1,9 @@ +package com.walking.member.api.usecase.dto.response + +data class GetMemberDetailUseCaseResponse( + val id: Long, + val nickName: String, + val profile: String, + val certificationSubject: String, + val status: String +) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/GetMemberTokenDetailUseCaseResponse.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/GetMemberTokenDetailUseCaseResponse.kt new file mode 100644 index 00000000..85789bb3 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/GetMemberTokenDetailUseCaseResponse.kt @@ -0,0 +1,5 @@ +package com.walking.member.api.usecase.dto.response + +data class GetMemberTokenDetailUseCaseResponse( + val id: Long +) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/PatchProfileImageUseCaseResponse.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/PatchProfileImageUseCaseResponse.kt new file mode 100644 index 00000000..c62c5f52 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/PatchProfileImageUseCaseResponse.kt @@ -0,0 +1,7 @@ +package com.walking.member.api.usecase.dto.response + +data class PatchProfileImageUseCaseResponse( + val id: Long, + val nickName: String, + val profile: String +) \ No newline at end of file diff --git a/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/PostMemberUseCaseResponse.kt b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/PostMemberUseCaseResponse.kt new file mode 100644 index 00000000..f73733b7 --- /dev/null +++ b/member-api/src/main/kotlin/com/walking/member/api/usecase/dto/response/PostMemberUseCaseResponse.kt @@ -0,0 +1,7 @@ +package com.walking.member.api.usecase.dto.response + +data class PostMemberUseCaseResponse( + val id: Long, + val nickname: String, + val profile: String +) \ No newline at end of file diff --git a/member-api/src/main/resources/application-member-api-dev.yml b/member-api/src/main/resources/application-member-api-dev.yml new file mode 100644 index 00000000..c909e4f6 --- /dev/null +++ b/member-api/src/main/resources/application-member-api-dev.yml @@ -0,0 +1,23 @@ +client: + max-attempts: ${CLIENT_MAX_ATTEMPTS:3} + pool: + max-connect: ${CLIENT_MAX_CONNECT:100} + max-connect-per-route: ${CLIENT_MAX_CONNECT_PER_ROUTE:5} + timeout: + connect: ${CLIENT_CONNECT_TIMEOUT:5000} + read: ${CLIENT_READ_TIMEOUT:5000} + retry: + maxAttempts: ${MAX_ATTEMPTS} + backOffPeriod: ${BACK_OFF_PERIOD} + +kakao: + host: ${KAKO_HOST} + adminKey: ${KAKO_ADMIN_KEY} + uri: + token: ${KAKO_TOKEN_URI} + token_info: ${KAKO_TOKEN_INFO_URI} + me_info: ${KAKO_ME_INFO_URI} + unlink: ${KAKO_UNLINK_URI} + redirect_uri: ${KAKO_APP_REDIRECT_URI} + client_id: ${KAKO_APP_CLIENT_ID} + diff --git a/member-api/src/main/resources/application-member-api-local.yml b/member-api/src/main/resources/application-member-api-local.yml new file mode 100644 index 00000000..7624fa3e --- /dev/null +++ b/member-api/src/main/resources/application-member-api-local.yml @@ -0,0 +1,22 @@ +client: + max-attempts: 3 + pool: + max-connect: 100 + max-connect-per-route: 5 + timeout: + connect: 5000 + read: 5000 + retry: + maxAttempts: 3 + backOffPeriod: 2000 + +kakao: + host: https://kauth.kakao.com + adminKey: adminKey + uri: + token: https://kauth.kakao.com/oauth/token + token_info: https://kapi.kakao.com/v1/user/access_token_info + me_info: https://kapi.kakao.com/v2/user/me + unlink: https://kapi.kakao.com/v1/user/unlink + redirect_uri: http://localhost:8080/api/v1/social/kakao + client_id: thisIsKaKaoClientId diff --git a/member-api/src/main/resources/application-member-api-prod.yml b/member-api/src/main/resources/application-member-api-prod.yml new file mode 100644 index 00000000..fe646adb --- /dev/null +++ b/member-api/src/main/resources/application-member-api-prod.yml @@ -0,0 +1,22 @@ +client: + max-attempts: ${CLIENT_MAX_ATTEMPTS:3} + pool: + max-connect: ${CLIENT_MAX_CONNECT:100} + max-connect-per-route: ${CLIENT_MAX_CONNECT_PER_ROUTE:5} + timeout: + connect: ${CLIENT_CONNECT_TIMEOUT:5000} + read: ${CLIENT_READ_TIMEOUT:5000} + retry: + maxAttempts: ${MAX_ATTEMPTS} + backOffPeriod: ${BACK_OFF_PERIOD} + +kakao: + host: ${KAKO_HOST} + adminKey: ${KAKO_ADMIN_KEY} + uri: + token: ${KAKO_TOKEN_URI} + token_info: ${KAKO_TOKEN_INFO_URI} + me_info: ${KAKO_ME_INFO_URI} + unlink: ${KAKO_UNLINK_URI} + redirect_uri: ${KAKO_APP_REDIRECT_URI} + client_id: ${KAKO_APP_CLIENT_ID} diff --git a/member-api/src/main/resources/application-member-api.yml b/member-api/src/main/resources/application-member-api.yml new file mode 100644 index 00000000..fe646adb --- /dev/null +++ b/member-api/src/main/resources/application-member-api.yml @@ -0,0 +1,22 @@ +client: + max-attempts: ${CLIENT_MAX_ATTEMPTS:3} + pool: + max-connect: ${CLIENT_MAX_CONNECT:100} + max-connect-per-route: ${CLIENT_MAX_CONNECT_PER_ROUTE:5} + timeout: + connect: ${CLIENT_CONNECT_TIMEOUT:5000} + read: ${CLIENT_READ_TIMEOUT:5000} + retry: + maxAttempts: ${MAX_ATTEMPTS} + backOffPeriod: ${BACK_OFF_PERIOD} + +kakao: + host: ${KAKO_HOST} + adminKey: ${KAKO_ADMIN_KEY} + uri: + token: ${KAKO_TOKEN_URI} + token_info: ${KAKO_TOKEN_INFO_URI} + me_info: ${KAKO_ME_INFO_URI} + unlink: ${KAKO_UNLINK_URI} + redirect_uri: ${KAKO_APP_REDIRECT_URI} + client_id: ${KAKO_APP_CLIENT_ID} diff --git a/resources/local-develop-environment/docker-compose.yml b/resources/local-develop-environment/docker-compose.yml index 5d4843c7..94ce9ab1 100644 --- a/resources/local-develop-environment/docker-compose.yml +++ b/resources/local-develop-environment/docker-compose.yml @@ -22,4 +22,19 @@ services: environment: - ADMINER_DEFAULT_SERVER=walking-mysql8 - ADMINER_DESIGN=nette - - ADMINER_PLUGINS=tables-filter tinymce \ No newline at end of file + - ADMINER_PLUGINS=tables-filter tinymce + + walking-minio: + container_name: walking-minio + image: minio/minio + ports: + - "9000:9000" + - "9001:9001" + shm_size: '1gb' + environment: + - MINIO_ACCESS_KEY=thisisroot + - MINIO_SECRET_KEY=thisisroot + - MINIO_ROOT_USER=thisisroot + - MINIO_ROOT_PASSWORD=thisisroot + - MINIO_REGION_NAME=ap-northeast-2 + command: server /data --console-address ":9001" diff --git a/settings.gradle b/settings.gradle index 8b52eaa2..2102586b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,14 @@ rootProject.name = 'walking-be' // api include 'api' +include 'member-api' // data include 'data' +include 'api-repository' + +// image-store +include 'image-store' // batch include 'batch'