diff --git a/.github/workflows/ci-develop.yml b/.github/workflows/ci-develop.yml index 4550ce80..d471a92a 100644 --- a/.github/workflows/ci-develop.yml +++ b/.github/workflows/ci-develop.yml @@ -1,39 +1,50 @@ name: Build and Deploy to EC2 - on: push: branches: [ "develop" ] pull_request: branches: [ "develop" ] - workflow_dispatch: - env: BUCKET_NAME: team09-cicd-bucket PROJECT_NAME: studay DEPLOYMENT_GROUP_NAME: studay_cicd CODE_DEPLOY_APP_NAME: studay_cicd - jobs: # 작업의 이름 build_and_test: # GitHub Actions 러너의 운영 체제 runs-on: ubuntu-latest - + services: + mysql: + image: mysql:8.0.21 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: test + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=10 + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server # 순차적으로 실행될 단계들을 정의하는 섹션 steps: - name: Checkout code uses: actions/checkout@v4.1.1 - - name: Set up JDK 17 uses: actions/setup-java@v3.13.0 with: java-version: '17' distribution: 'corretto' - - name: Grant execute permission for gradlew run: chmod +x ./gradlew - + - name: Create application.yml + run: | + mkdir -p src/main/resources + echo "${{ secrets.APPLICATION_YML_CONTENT }}" > src/main/resources/application.yml + echo "${{ secrets.APPLICATION_DEV_YML_CONTENT }}" > src/main/resources/application-dev.yml + echo "${{ secrets.APPLICATION_OAUTH_YML_CONTENT }}" > src/main/resources/application-oauth.yml - name: Build with gradle - run: ./gradlew build - + run: ./gradlew build \ No newline at end of file diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index d99ddd14..ce58b802 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -44,16 +44,16 @@ jobs: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_PRIVATE_ACCESS_KEY }} - aws-region: ap-northeast-1 + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_PRIVATE_ACCESS_KEY }} + aws-region: ap-northeast-1 - name: Upload to S3 run: aws s3 cp --region ap-northeast-1 ./$GITHUB_SHA.zip s3://$BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip - name: Code Deploy To EC2 instance run: aws deploy create-deployment - --application-name $CODE_DEPLOY_APP_NAME - --deployment-config-name CodeDeployDefault.AllAtOnce - --deployment-group-name $DEPLOYMENT_GROUP_NAME - --s3-location bucket=$BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip + --application-name $CODE_DEPLOY_APP_NAME + --deployment-config-name CodeDeployDefault.AllAtOnce + --deployment-group-name $DEPLOYMENT_GROUP_NAME + --s3-location bucket=$BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip diff --git a/.gitignore b/.gitignore index 7de14826..0f2ee4f8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ HELP.md .gradle/ build/ studay_server.iml -src/main/resources/application.yaml +*.yaml ### Intellij+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientGoogle.java b/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientGoogle.java index 9b2881e0..d418f9df 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientGoogle.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientGoogle.java @@ -1,11 +1,11 @@ package org.guzzing.studayserver.domain.auth.client; -import org.guzzing.studayserver.global.error.response.ErrorCode; import org.guzzing.studayserver.domain.auth.dto.GoogleUserResponse; import org.guzzing.studayserver.domain.auth.exception.TokenValidFailedException; import org.guzzing.studayserver.domain.member.model.Member; import org.guzzing.studayserver.domain.member.model.vo.MemberProvider; import org.guzzing.studayserver.domain.member.model.vo.RoleType; +import org.guzzing.studayserver.global.error.response.ErrorCode; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @@ -26,8 +26,10 @@ public Member getUserData(String accessToken) { .uri("https://www.googleapis.com/oauth2/v3/userinfo") .headers(h -> h.set("Authorization", accessToken)) .retrieve() - .onStatus(status -> status.is4xxClientError(), response -> Mono.error(new TokenValidFailedException(ErrorCode.UNAUTHORIZED_TOKEN))) - .onStatus(status -> status.is5xxServerError(), response -> Mono.error(new TokenValidFailedException(ErrorCode.OAUTH_CLIENT_SERVER_ERROR))) + .onStatus(status -> status.is4xxClientError(), + response -> Mono.error(new TokenValidFailedException(ErrorCode.UNAUTHORIZED_TOKEN))) + .onStatus(status -> status.is5xxServerError(), + response -> Mono.error(new TokenValidFailedException(ErrorCode.OAUTH_CLIENT_SERVER_ERROR))) .bodyToMono(GoogleUserResponse.class) .block(); diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientKakao.java b/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientKakao.java index bdf1db5a..56c4934c 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientKakao.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientKakao.java @@ -1,11 +1,11 @@ package org.guzzing.studayserver.domain.auth.client; -import org.guzzing.studayserver.global.error.response.ErrorCode; import org.guzzing.studayserver.domain.auth.dto.KakaoUserResponse; import org.guzzing.studayserver.domain.auth.exception.TokenValidFailedException; import org.guzzing.studayserver.domain.member.model.Member; import org.guzzing.studayserver.domain.member.model.vo.MemberProvider; import org.guzzing.studayserver.domain.member.model.vo.RoleType; +import org.guzzing.studayserver.global.error.response.ErrorCode; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @@ -26,8 +26,10 @@ public Member getUserData(String accessToken) { .uri("https://kapi.kakao.com/v2/user/me") .headers(h -> h.set("Authorization", accessToken)) .retrieve() - .onStatus(status -> status.is4xxClientError(), response -> Mono.error(new TokenValidFailedException(ErrorCode.UNAUTHORIZED_TOKEN))) - .onStatus(status -> status.is5xxServerError(), response -> Mono.error(new TokenValidFailedException(ErrorCode.OAUTH_CLIENT_SERVER_ERROR))) + .onStatus(status -> status.is4xxClientError(), + response -> Mono.error(new TokenValidFailedException(ErrorCode.UNAUTHORIZED_TOKEN))) + .onStatus(status -> status.is5xxServerError(), + response -> Mono.error(new TokenValidFailedException(ErrorCode.OAUTH_CLIENT_SERVER_ERROR))) .bodyToMono(KakaoUserResponse.class) .block(); diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientStrategy.java b/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientStrategy.java index 1b8735d2..8cfb206a 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientStrategy.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/client/ClientStrategy.java @@ -1,12 +1,11 @@ package org.guzzing.studayserver.domain.auth.client; +import java.util.HashMap; +import java.util.Map; import org.guzzing.studayserver.domain.member.model.vo.MemberProvider; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; -import java.util.HashMap; -import java.util.Map; - @Component public class ClientStrategy { diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/config/Constants.java b/src/main/java/org/guzzing/studayserver/domain/auth/config/Constants.java index 9b418938..f9570303 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/config/Constants.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/config/Constants.java @@ -5,9 +5,10 @@ public class Constants { /** * 권한제외 대상 + * * @see SecurityConfig */ - public static final String[] permitAllArray = new String[] { + public static final String[] permitAllArray = new String[]{ "/", "/auth/**", diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/config/SecurityConfig.java b/src/main/java/org/guzzing/studayserver/domain/auth/config/SecurityConfig.java index de93755d..5da0b029 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/config/SecurityConfig.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/config/SecurityConfig.java @@ -1,5 +1,6 @@ package org.guzzing.studayserver.domain.auth.config; +import java.util.stream.Stream; import org.guzzing.studayserver.domain.auth.jwt.AuthTokenProvider; import org.guzzing.studayserver.domain.auth.jwt.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; @@ -7,12 +8,11 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; -import java.util.stream.Stream; @Configuration @EnableWebSecurity diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/config/WebClientConfig.java b/src/main/java/org/guzzing/studayserver/domain/auth/config/WebClientConfig.java index 180a26b8..ac7818f4 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/config/WebClientConfig.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/config/WebClientConfig.java @@ -2,6 +2,7 @@ import io.netty.channel.ChannelOption; import io.netty.handler.timeout.ReadTimeoutHandler; +import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,8 +15,6 @@ import reactor.netty.http.client.HttpClient; import reactor.netty.tcp.TcpClient; -import java.util.concurrent.TimeUnit; - @Configuration @Slf4j public class WebClientConfig { @@ -24,12 +23,12 @@ public class WebClientConfig { public WebClient webClient() { ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024*1024*50)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024 * 50)) .build(); exchangeStrategies .messageWriters().stream() .filter(LoggingCodecSupport.class::isInstance) - .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true)); + .forEach(writer -> ((LoggingCodecSupport) writer).setEnableLoggingRequestDetails(true)); return WebClient.builder() .clientConnector( @@ -38,24 +37,28 @@ public WebClient webClient() { TcpClient .create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) - .doOnConnected(conn -> conn.addHandler(new ReadTimeoutHandler(3000, TimeUnit.MILLISECONDS))) + .doOnConnected(conn -> conn.addHandler( + new ReadTimeoutHandler(3000, TimeUnit.MILLISECONDS))) ) )) .exchangeStrategies(exchangeStrategies) .filter(ExchangeFilterFunction.ofRequestProcessor( clientRequest -> { log.debug("Request: {} {}", clientRequest.method(), clientRequest.url()); - clientRequest.headers().forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value))); + clientRequest.headers().forEach( + (name, values) -> values.forEach(value -> log.debug("{} : {}", name, value))); return Mono.just(clientRequest); } )) .filter(ExchangeFilterFunction.ofResponseProcessor( clientResponse -> { - clientResponse.headers().asHttpHeaders().forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value))); + clientResponse.headers().asHttpHeaders().forEach( + (name, values) -> values.forEach(value -> log.debug("{} : {}", name, value))); return Mono.just(clientResponse); } )) - .defaultHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.3") + .defaultHeader("user-agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.3") .build(); } diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/controller/AuthController.java b/src/main/java/org/guzzing/studayserver/domain/auth/controller/AuthController.java index 22e7a16d..7ece70b3 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/controller/AuthController.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/controller/AuthController.java @@ -48,7 +48,7 @@ public ResponseEntity googleAuthRequest(HttpServletRequest request } @GetMapping("/refresh") - public ResponseEntity refreshToken( HttpServletRequest request) { + public ResponseEntity refreshToken(HttpServletRequest request) { String appToken = JwtHeaderUtil.getAccessToken(request); AuthToken authToken = authTokenProvider.convertAuthToken(appToken); diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/dto/KakaoUserResponse.java b/src/main/java/org/guzzing/studayserver/domain/auth/dto/KakaoUserResponse.java index cfd8c2ab..3a6103e0 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/dto/KakaoUserResponse.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/dto/KakaoUserResponse.java @@ -25,6 +25,7 @@ public KakaoUserResponse(Long id, Properties properties, KakaoAccount kakaoAccou @NoArgsConstructor @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) public static class Properties { + private NickName nickname; public Properties(NickName nickname) { @@ -36,6 +37,7 @@ public Properties(NickName nickname) { @Getter @NoArgsConstructor public static class KakaoAccount { + private String email; public KakaoAccount(String email) { diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/exception/TokenValidFailedException.java b/src/main/java/org/guzzing/studayserver/domain/auth/exception/TokenValidFailedException.java index 12c1dd93..9a5f35c1 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/exception/TokenValidFailedException.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/exception/TokenValidFailedException.java @@ -1,7 +1,7 @@ package org.guzzing.studayserver.domain.auth.exception; -import org.guzzing.studayserver.global.error.response.ErrorCode; import lombok.Getter; +import org.guzzing.studayserver.global.error.response.ErrorCode; @Getter public class TokenValidFailedException extends IllegalStateException { diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthToken.java b/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthToken.java index 1fe7c354..a10dd431 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthToken.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthToken.java @@ -1,13 +1,18 @@ package org.guzzing.studayserver.domain.auth.jwt; -import io.jsonwebtoken.*; -import lombok.Builder; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; import java.security.Key; import java.util.Date; import java.util.Optional; +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; @Slf4j public class AuthToken { @@ -61,7 +66,7 @@ public String createRefreshToken(Date expiry) { public boolean isValidTokenClaims() { Optional claims = Optional.empty(); try { - claims= Optional.ofNullable(getTokenClaims()); + claims = Optional.ofNullable(getTokenClaims()); } catch (SecurityException e) { log.info("Invalid JWT signature."); } catch (MalformedJwtException e) { diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthTokenProvider.java b/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthTokenProvider.java index e88e60d8..bdff7b77 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthTokenProvider.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/jwt/AuthTokenProvider.java @@ -2,6 +2,11 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -10,12 +15,6 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import java.security.Key; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.stream.Collectors; - @Slf4j @Component public class AuthTokenProvider { diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/jwt/CustomUser.java b/src/main/java/org/guzzing/studayserver/domain/auth/jwt/CustomUser.java index 215e6f19..8ef92e6a 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/jwt/CustomUser.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/jwt/CustomUser.java @@ -1,14 +1,13 @@ package org.guzzing.studayserver.domain.auth.jwt; -import lombok.Builder; -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - import java.io.Serial; import java.util.Collection; import java.util.List; import java.util.Objects; +import lombok.Builder; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; @Getter public class CustomUser implements UserDetails { @@ -29,8 +28,12 @@ public CustomUser(Long memberId, String socialId, List { diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/service/AuthService.java b/src/main/java/org/guzzing/studayserver/domain/auth/service/AuthService.java index 4fcdb68a..4ca20599 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/service/AuthService.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/service/AuthService.java @@ -16,7 +16,8 @@ public class AuthService { private final MemberRepository memberRepository; private final RefreshTokenService refreshTokenService; - public AuthService(AuthTokenProvider authTokenProvider, MemberRepository memberRepository, RefreshTokenService refreshTokenService) { + public AuthService(AuthTokenProvider authTokenProvider, MemberRepository memberRepository, + RefreshTokenService refreshTokenService) { this.authTokenProvider = authTokenProvider; this.memberRepository = memberRepository; this.refreshTokenService = refreshTokenService; diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/service/ClientService.java b/src/main/java/org/guzzing/studayserver/domain/auth/service/ClientService.java index 63ce6846..a357f2ba 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/service/ClientService.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/service/ClientService.java @@ -1,5 +1,6 @@ package org.guzzing.studayserver.domain.auth.service; +import java.util.Optional; import org.guzzing.studayserver.domain.auth.client.ClientProxy; import org.guzzing.studayserver.domain.auth.client.ClientStrategy; import org.guzzing.studayserver.domain.auth.dto.AuthResponse; @@ -10,8 +11,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service public class ClientService { @@ -20,7 +19,8 @@ public class ClientService { private final MemberRepository memberJpaRepository; private final RefreshTokenService refreshTokenService; - public ClientService(ClientStrategy clientStrategy, AuthTokenProvider authTokenProvider, MemberRepository memberJpaRepository, RefreshTokenService refreshTokenService) { + public ClientService(ClientStrategy clientStrategy, AuthTokenProvider authTokenProvider, + MemberRepository memberJpaRepository, RefreshTokenService refreshTokenService) { this.clientStrategy = clientStrategy; this.authTokenProvider = authTokenProvider; this.memberJpaRepository = memberJpaRepository; @@ -36,8 +36,8 @@ public AuthResponse login(String client, String accessToken) { Optional memberOptional = memberJpaRepository.findMemberIfExisted(socialId); Member savedMember = memberOptional.orElseGet(() -> memberJpaRepository.save(clientMember)); - - AuthToken newAuthToken = refreshTokenService.saveAccessTokenCache(savedMember.getId(),socialId); + + AuthToken newAuthToken = refreshTokenService.saveAccessTokenCache(savedMember.getId(), socialId); return AuthResponse.builder() .appToken(newAuthToken.getToken()) diff --git a/src/main/java/org/guzzing/studayserver/domain/auth/service/RefreshTokenService.java b/src/main/java/org/guzzing/studayserver/domain/auth/service/RefreshTokenService.java index 123c7137..85cd66da 100644 --- a/src/main/java/org/guzzing/studayserver/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/org/guzzing/studayserver/domain/auth/service/RefreshTokenService.java @@ -21,7 +21,7 @@ public RefreshTokenService(AuthTokenProvider authTokenProvider, RefreshTokenRepo public AuthToken saveNewAccessTokenInfo(Long memberId, String socialId, String accessToken) { AuthToken refreshToken = findRefreshToken(accessToken); - AuthToken newAccessToken = authTokenProvider.createAccessToken(socialId,memberId); + AuthToken newAccessToken = authTokenProvider.createAccessToken(socialId, memberId); refreshTokenRepository.save(new JwtTokenCache(memberId, refreshToken.getToken(), newAccessToken.getToken())); @@ -29,7 +29,7 @@ public AuthToken saveNewAccessTokenInfo(Long memberId, String socialId, String a } public AuthToken saveAccessTokenCache(Long memberId, String socialId) { - AuthToken newAccessToken = authTokenProvider.createAccessToken(socialId,memberId); + AuthToken newAccessToken = authTokenProvider.createAccessToken(socialId, memberId); AuthToken newRefreshToken = authTokenProvider.createRefreshToken(); refreshTokenRepository.save(new JwtTokenCache(memberId, newRefreshToken.getToken(), newAccessToken.getToken())); @@ -37,12 +37,12 @@ public AuthToken saveAccessTokenCache(Long memberId, String socialId) { return newAccessToken; } - public boolean isExpiredRefreshToken (String accessToken) { + public boolean isExpiredRefreshToken(String accessToken) { return findRefreshToken(accessToken).isValidTokenClaims(); } - @Transactional(readOnly=true) - public AuthToken findRefreshToken (String accessToken) { + @Transactional(readOnly = true) + public AuthToken findRefreshToken(String accessToken) { JwtTokenCache jwtTokenCache = refreshTokenRepository.findByAccessToken(accessToken) .orElseThrow(() -> new RuntimeException()); // to do : 예외 고치기 return authTokenProvider.convertAuthToken(jwtTokenCache.getRefreshToken()); diff --git a/src/main/java/org/guzzing/studayserver/domain/member/model/Member.java b/src/main/java/org/guzzing/studayserver/domain/member/model/Member.java index 7f2e8a1b..c1679b3e 100644 --- a/src/main/java/org/guzzing/studayserver/domain/member/model/Member.java +++ b/src/main/java/org/guzzing/studayserver/domain/member/model/Member.java @@ -1,6 +1,13 @@ package org.guzzing.studayserver.domain.member.model; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import lombok.Getter; import org.guzzing.studayserver.domain.member.model.vo.MemberProvider; import org.guzzing.studayserver.domain.member.model.vo.NickName; @@ -36,7 +43,8 @@ protected Member() { } - protected Member(Long id, NickName nickName, String email, String socialId, MemberProvider memberProvider, RoleType roleType) { + protected Member(Long id, NickName nickName, String email, String socialId, MemberProvider memberProvider, + RoleType roleType) { this.id = id; this.nickName = nickName; this.email = email; diff --git a/src/main/java/org/guzzing/studayserver/domain/member/model/vo/MemberProvider.java b/src/main/java/org/guzzing/studayserver/domain/member/model/vo/MemberProvider.java index 95d2e826..821a6b34 100644 --- a/src/main/java/org/guzzing/studayserver/domain/member/model/vo/MemberProvider.java +++ b/src/main/java/org/guzzing/studayserver/domain/member/model/vo/MemberProvider.java @@ -2,6 +2,5 @@ public enum MemberProvider { KAKAO, - GOOGLE - ; + GOOGLE; } diff --git a/src/main/java/org/guzzing/studayserver/domain/member/model/vo/NickName.java b/src/main/java/org/guzzing/studayserver/domain/member/model/vo/NickName.java index 30cc03cb..6f44dfba 100644 --- a/src/main/java/org/guzzing/studayserver/domain/member/model/vo/NickName.java +++ b/src/main/java/org/guzzing/studayserver/domain/member/model/vo/NickName.java @@ -1,11 +1,10 @@ package org.guzzing.studayserver.domain.member.model.vo; import jakarta.persistence.Embeddable; +import java.util.Objects; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import java.util.Objects; - @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) public class NickName { @@ -29,8 +28,12 @@ public String getNickName() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } NickName nickName1 = (NickName) o; return Objects.equals(nickName, nickName1.nickName); } diff --git a/src/main/java/org/guzzing/studayserver/domain/member/model/vo/RoleType.java b/src/main/java/org/guzzing/studayserver/domain/member/model/vo/RoleType.java index f03ade32..79e39858 100644 --- a/src/main/java/org/guzzing/studayserver/domain/member/model/vo/RoleType.java +++ b/src/main/java/org/guzzing/studayserver/domain/member/model/vo/RoleType.java @@ -1,10 +1,9 @@ package org.guzzing.studayserver.domain.member.model.vo; +import java.util.Arrays; import lombok.AllArgsConstructor; import lombok.Getter; -import java.util.Arrays; - @Getter @AllArgsConstructor public enum RoleType { diff --git a/src/main/java/org/guzzing/studayserver/domain/member/repository/MemberRepository.java b/src/main/java/org/guzzing/studayserver/domain/member/repository/MemberRepository.java index 6326fb55..1823e422 100644 --- a/src/main/java/org/guzzing/studayserver/domain/member/repository/MemberRepository.java +++ b/src/main/java/org/guzzing/studayserver/domain/member/repository/MemberRepository.java @@ -1,11 +1,10 @@ package org.guzzing.studayserver.domain.member.repository; +import java.util.Optional; import org.guzzing.studayserver.domain.member.model.Member; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.Optional; - public interface MemberRepository extends JpaRepository { Member findBySocialId(String socialId); diff --git a/src/main/java/org/guzzing/studayserver/domain/region/aop/RegionParamAspect.java b/src/main/java/org/guzzing/studayserver/domain/region/aop/RegionParamAspect.java new file mode 100644 index 00000000..f88f8e20 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/aop/RegionParamAspect.java @@ -0,0 +1,131 @@ +package org.guzzing.studayserver.domain.region.aop; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.guzzing.studayserver.domain.region.model.Region; +import org.guzzing.studayserver.global.exception.RegionException; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class RegionParamAspect { + + private static final String SIDO = "sido"; + private static final String SIGUNGU = "sigungu"; + private static final String UPMYEONDONG = "upmyeondong"; + + @Before(value = "@annotation(org.guzzing.studayserver.domain.region.aop.ValidSido)") + public void validateRegionSidoParameter(JoinPoint joinpoin) { + MethodSignatureInfo methodSignatureInfo = getMethodSignatureInfo(joinpoin); + + Object sido = getParameterValue(methodSignatureInfo.args(), methodSignatureInfo.parameterNames(), + methodSignatureInfo.parameterTypes(), SIDO); + + validateRegionParam(sido, SIDO); + } + + @Before(value = "@annotation(org.guzzing.studayserver.domain.region.aop.ValidSigungu)") + public void validateRegionSigunguParameter(JoinPoint joinPoint) { + MethodSignatureInfo methodSignatureInfo = getMethodSignatureInfo(joinPoint); + + Object sigungu = getParameterValue(methodSignatureInfo.args(), methodSignatureInfo.parameterNames(), + methodSignatureInfo.parameterTypes(), SIGUNGU); + + validateRegionParam(sigungu, SIGUNGU); + } + + @Before(value = "@annotation(org.guzzing.studayserver.domain.region.aop.ValidUpmyeondong)") + public void validateRegionUpmyeondongParameter(JoinPoint joinPoint) { + MethodSignatureInfo methodSignatureInfo = getMethodSignatureInfo(joinPoint); + + Object upmyeondong = getParameterValue(methodSignatureInfo.args(), methodSignatureInfo.parameterNames(), + methodSignatureInfo.parameterTypes(), UPMYEONDONG); + + validateRegionParam(upmyeondong, UPMYEONDONG); + } + + private void validateRegionParam(Object sido, String regionParamName) { + List regionPostfixList = switch (regionParamName) { + case SIDO -> Region.BASE_REGION_SIDO; + case SIGUNGU -> Region.SIGUNGU_POSTFIX; + case UPMYEONDONG -> Region.UPMYEONDONG_POSTFIX; + default -> List.of(); + }; + + if (sido != null) { + boolean isInvalidSidoParameter = regionPostfixList.stream() + .noneMatch(postfix -> String.valueOf(sido).contains(postfix)); + + if (isInvalidSidoParameter) { + throw new RegionException("올바르지 않은 시도명이거나, 제공하지 않는 지역입니다."); + } + } + } + + private MethodSignatureInfo getMethodSignatureInfo(JoinPoint joinpoin) { + MethodSignature methodSignature = (MethodSignature) joinpoin.getSignature(); + + Object[] args = joinpoin.getArgs(); + String[] parameterNames = methodSignature.getParameterNames(); + Class[] parameterTypes = methodSignature.getParameterTypes(); + + return new MethodSignatureInfo(args, parameterNames, parameterTypes); + } + + private Object getParameterValue( + Object[] args, + String[] parameterNames, + Class[] parameterTypes, + String regionUnitParamName + ) { + Object regionUnit = null; + + for (int i = 0; i < args.length; i++) { + if (Objects.equals(parameterNames[i], regionUnitParamName) + && Objects.equals(parameterTypes[i], String.class)) { + regionUnit = args[i]; + } + } + return regionUnit; + } + + private record MethodSignatureInfo(Object[] args, String[] parameterNames, Class[] parameterTypes) { + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MethodSignatureInfo that = (MethodSignatureInfo) o; + return Arrays.equals(args, that.args) && Arrays.equals(parameterNames, that.parameterNames) + && Arrays.equals(parameterTypes, that.parameterTypes); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(args); + result = 31 * result + Arrays.hashCode(parameterNames); + result = 31 * result + Arrays.hashCode(parameterTypes); + return result; + } + + @Override + public String toString() { + return new StringJoiner(", ", MethodSignatureInfo.class.getSimpleName() + "[", "]") + .add("args=" + Arrays.toString(args)) + .add("parameterNames=" + Arrays.toString(parameterNames)) + .add("parameterTypes=" + Arrays.toString(parameterTypes)) + .toString(); + } + } + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidSido.java b/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidSido.java new file mode 100644 index 00000000..be69d03f --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidSido.java @@ -0,0 +1,12 @@ +package org.guzzing.studayserver.domain.region.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidSido { + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidSigungu.java b/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidSigungu.java new file mode 100644 index 00000000..ee14d92a --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidSigungu.java @@ -0,0 +1,12 @@ +package org.guzzing.studayserver.domain.region.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidSigungu { + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidUpmyeondong.java b/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidUpmyeondong.java new file mode 100644 index 00000000..4f30f603 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/aop/ValidUpmyeondong.java @@ -0,0 +1,12 @@ +package org.guzzing.studayserver.domain.region.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidUpmyeondong { + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/controller/RegionRestController.java b/src/main/java/org/guzzing/studayserver/domain/region/controller/RegionRestController.java new file mode 100644 index 00000000..4b9dc39e --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/controller/RegionRestController.java @@ -0,0 +1,83 @@ +package org.guzzing.studayserver.domain.region.controller; + +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import org.guzzing.studayserver.domain.region.aop.ValidSido; +import org.guzzing.studayserver.domain.region.aop.ValidSigungu; +import org.guzzing.studayserver.domain.region.aop.ValidUpmyeondong; +import org.guzzing.studayserver.domain.region.controller.dto.RegionLocationResponse; +import org.guzzing.studayserver.domain.region.controller.dto.RegionResponse; +import org.guzzing.studayserver.domain.region.service.RegionService; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SidoResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SigunguResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.UpmyeondongResult; +import org.guzzing.studayserver.domain.region.service.dto.location.RegionResult; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/regions") +public class RegionRestController { + + private final RegionService regionService; + + public RegionRestController(RegionService regionService) { + this.regionService = regionService; + } + + @ValidSido + @ValidSigungu + @GetMapping(path = "/beopjungdong", produces = APPLICATION_JSON_VALUE) + public ResponseEntity getSubRegions( + @RequestParam(required = false) String sido, + @RequestParam(required = false) String sigungu + ) { + if (sido == null) { + return getSidoData(); + } else if (sigungu == null) { + return getSigunguData(sido); + } + return getUpmyeondongData(sido, sigungu); + } + + @ValidSido + @ValidSigungu + @ValidUpmyeondong + @GetMapping(path = "/location", produces = APPLICATION_JSON_VALUE) + public ResponseEntity getLocation( + @RequestParam String sido, + @RequestParam String sigungu, + @RequestParam String upmyeondong + ) { + RegionResult regionResult = regionService.findLocation(sido, sigungu, upmyeondong); + return ResponseEntity + .status(OK) + .body(RegionLocationResponse.from(regionResult)); + } + + private ResponseEntity getUpmyeondongData(String sido, String sigungu) { + UpmyeondongResult result = regionService.findUpmyeondongBySidoAndSigungu(sido, sigungu); + return ResponseEntity + .status(OK) + .body(RegionResponse.from(result)); + } + + private ResponseEntity getSigunguData(String sido) { + SigunguResult result = regionService.findSigungusBySido(sido); + return ResponseEntity + .status(OK) + .body(RegionResponse.from(result)); + } + + private ResponseEntity getSidoData() { + SidoResult result = regionService.findSido(); + return ResponseEntity + .status(OK) + .body(RegionResponse.from(result)); + } + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/controller/dto/RegionLocationResponse.java b/src/main/java/org/guzzing/studayserver/domain/region/controller/dto/RegionLocationResponse.java new file mode 100644 index 00000000..a23d0a21 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/controller/dto/RegionLocationResponse.java @@ -0,0 +1,22 @@ +package org.guzzing.studayserver.domain.region.controller.dto; + +import org.guzzing.studayserver.domain.region.service.dto.location.RegionResult; + +public record RegionLocationResponse( + String sido, + String sigungu, + String upmyeondong, + double latitude, + double longitude +) { + + public static RegionLocationResponse from(final RegionResult regionResult) { + return new RegionLocationResponse( + regionResult.sido(), + regionResult.sigungu(), + regionResult.upmyeondong(), + regionResult.latitude(), + regionResult.longtigute()); + } + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/controller/dto/RegionResponse.java b/src/main/java/org/guzzing/studayserver/domain/region/controller/dto/RegionResponse.java new file mode 100644 index 00000000..64c546c4 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/controller/dto/RegionResponse.java @@ -0,0 +1,30 @@ +package org.guzzing.studayserver.domain.region.controller.dto; + +import java.text.MessageFormat; +import java.util.List; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SidoResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SigunguResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.UpmyeondongResult; + +public record RegionResponse( + String targetRegion, + List subRegion, + int subRegionCount +) { + + public static RegionResponse from(final SigunguResult sigunguResult) { + return new RegionResponse(sigunguResult.sido(), sigunguResult.sigungus(), sigunguResult.sigunguCount()); + } + + public static RegionResponse from(final UpmyeondongResult upmyeondongResult) { + return new RegionResponse( + MessageFormat.format("{0} {1}", upmyeondongResult.sido(), upmyeondongResult.sigungu()), + upmyeondongResult.upmyeondongs(), + upmyeondongResult.upmyeondongCount()); + } + + public static RegionResponse from(final SidoResult sidoResult) { + return new RegionResponse(sidoResult.nation(), sidoResult.sidos(), sidoResult.sidoCount()); + } + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/model/Region.java b/src/main/java/org/guzzing/studayserver/domain/region/model/Region.java new file mode 100644 index 00000000..44e2f59e --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/model/Region.java @@ -0,0 +1,69 @@ +package org.guzzing.studayserver.domain.region.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import java.util.List; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "regions") +public class Region { + + @Transient + public static final List BASE_REGION_SIDO = List.of("서울특별시", "경기도"); + + @Transient + public static final List SIGUNGU_POSTFIX = List.of("시", "구", "군"); + + @Transient + public static final List UPMYEONDONG_POSTFIX = List.of("읍", "면", "동", "군", "구"); + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "sido", nullable = false) + private String sido; + + @Column(name = "sigungu", nullable = false) + private String sigungu; + + @Column(name = "upmyeondong", nullable = false) + private String upmyeondong; + + @Column(name = "latitude", nullable = false) + private double latitude; + + @Column(name = "longitude", nullable = false) + private double longitude; + + protected Region() { + } + + protected Region( + final String sido, + final String sigungu, + final String upmyeondong, + final double latitude, + final double longitude + ) { + this.sido = sido; + this.sigungu = sigungu; + this.upmyeondong = upmyeondong; + this.latitude = latitude; + this.longitude = longitude; + } + + public static Region of( + final String sido, final String sigungu, final String upmyeondong, + final double latitude, final double longitude + ) { + return new Region(sido, sigungu, upmyeondong, latitude, longitude); + } +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/repository/RegionJpaRepository.java b/src/main/java/org/guzzing/studayserver/domain/region/repository/RegionJpaRepository.java new file mode 100644 index 00000000..046b9b72 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/repository/RegionJpaRepository.java @@ -0,0 +1,18 @@ +package org.guzzing.studayserver.domain.region.repository; + +import java.util.List; +import org.guzzing.studayserver.domain.region.model.Region; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface RegionJpaRepository extends JpaRepository, RegionRepository { + + @Query("select distinct(r.sigungu) from Region r where r.sido = :sido") + List findSigunguBySido(final String sido); + + @Query("select r.upmyeondong from Region r where r.sido = :sido and r.sigungu = :sigungu") + List findUpmyeondongBySidoAndSigungu(final String sido, final String sigungu); + + Region findBySidoAndSigunguAndUpmyeondong(final String sido, final String sigungu, final String upmyeondong); + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/repository/RegionRepository.java b/src/main/java/org/guzzing/studayserver/domain/region/repository/RegionRepository.java new file mode 100644 index 00000000..2507b618 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/repository/RegionRepository.java @@ -0,0 +1,15 @@ +package org.guzzing.studayserver.domain.region.repository; + +import java.util.List; +import org.guzzing.studayserver.domain.region.model.Region; + +public interface RegionRepository { + + List findSigunguBySido(final String sido); + + List findUpmyeondongBySidoAndSigungu(final String sido, final String sigungu); + + Region findBySidoAndSigunguAndUpmyeondong(final String sido, final String sigungu, final String upmyeondong); + + Region save(Region region); +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/service/RegionService.java b/src/main/java/org/guzzing/studayserver/domain/region/service/RegionService.java new file mode 100644 index 00000000..152c726b --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/service/RegionService.java @@ -0,0 +1,52 @@ +package org.guzzing.studayserver.domain.region.service; + +import static org.guzzing.studayserver.domain.region.model.Region.BASE_REGION_SIDO; + +import java.util.List; +import org.guzzing.studayserver.domain.region.model.Region; +import org.guzzing.studayserver.domain.region.repository.RegionRepository; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SidoResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SigunguResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.UpmyeondongResult; +import org.guzzing.studayserver.domain.region.service.dto.location.RegionResult; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class RegionService { + + private final RegionRepository regionRepository; + + public RegionService(final RegionRepository regionRepository) { + this.regionRepository = regionRepository; + } + + public SigunguResult findSigungusBySido(final String sido) { + List sigungu = regionRepository.findSigunguBySido(sido); + return SigunguResult.from(sido, sigungu); + } + + public UpmyeondongResult findUpmyeondongBySidoAndSigungu(final String sido, final String sigungu) { + List upmyeondong = regionRepository.findUpmyeondongBySidoAndSigungu(sido, sigungu); + return UpmyeondongResult.from(sido, sigungu, upmyeondong); + } + + public SidoResult findSido() { + return SidoResult.from(BASE_REGION_SIDO); + } + + public RegionResult findLocation(final String sido, final String sigungu, final String upmyeondong) { + Region region = regionRepository.findBySidoAndSigunguAndUpmyeondong(sido, sigungu, upmyeondong); + return RegionResult.from(region); + } + + public RegionResult createRegion( + final String sido, final String sigungu, final String upmyeondong, + final double latitude, final double longitude + ) { + Region region = regionRepository.save(Region.of(sido, sigungu, upmyeondong, latitude, longitude)); + return RegionResult.from(region); + } + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/SidoResult.java b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/SidoResult.java new file mode 100644 index 00000000..7c3e611f --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/SidoResult.java @@ -0,0 +1,14 @@ +package org.guzzing.studayserver.domain.region.service.dto.beopjungdong; + +import java.util.List; + +public record SidoResult( + String nation, + List sidos, + int sidoCount +) { + + public static SidoResult from(final List baseRegionSido) { + return new SidoResult("전국", baseRegionSido, baseRegionSido.size()); + } +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/SigunguResult.java b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/SigunguResult.java new file mode 100644 index 00000000..3da23dea --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/SigunguResult.java @@ -0,0 +1,15 @@ +package org.guzzing.studayserver.domain.region.service.dto.beopjungdong; + +import java.util.List; + +public record SigunguResult( + String sido, + List sigungus, + int sigunguCount +) { + + public static SigunguResult from(final String sido, final List sigungus) { + return new SigunguResult(sido, sigungus, sigungus.size()); + } + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/UpmyeondongResult.java b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/UpmyeondongResult.java new file mode 100644 index 00000000..a3b8bf21 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/beopjungdong/UpmyeondongResult.java @@ -0,0 +1,16 @@ +package org.guzzing.studayserver.domain.region.service.dto.beopjungdong; + +import java.util.List; + +public record UpmyeondongResult( + String sido, + String sigungu, + List upmyeondongs, + int upmyeondongCount +) { + + public static UpmyeondongResult from(final String sido, final String sigungu, final List upmyeondong) { + return new UpmyeondongResult(sido, sigungu, upmyeondong, upmyeondong.size()); + } + +} diff --git a/src/main/java/org/guzzing/studayserver/domain/region/service/dto/location/RegionResult.java b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/location/RegionResult.java new file mode 100644 index 00000000..42982f81 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/region/service/dto/location/RegionResult.java @@ -0,0 +1,22 @@ +package org.guzzing.studayserver.domain.region.service.dto.location; + +import org.guzzing.studayserver.domain.region.model.Region; + +public record RegionResult( + String sido, + String sigungu, + String upmyeondong, + double latitude, + double longtigute +) { + + public static RegionResult from(final Region region) { + return new RegionResult( + region.getSido(), + region.getSigungu(), + region.getUpmyeondong(), + region.getLatitude(), + region.getLongitude()); + } + +} diff --git a/src/main/java/org/guzzing/studayserver/global/BaseEntity.java b/src/main/java/org/guzzing/studayserver/global/BaseEntity.java index 91884803..9860eddf 100644 --- a/src/main/java/org/guzzing/studayserver/global/BaseEntity.java +++ b/src/main/java/org/guzzing/studayserver/global/BaseEntity.java @@ -3,11 +3,10 @@ import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import java.time.LocalDate; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.LocalDate; - @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public class BaseEntity { diff --git a/src/main/java/org/guzzing/studayserver/global/config/JpaConfig.java b/src/main/java/org/guzzing/studayserver/global/config/JpaConfig.java index b3bfd7d2..3d44bf8e 100644 --- a/src/main/java/org/guzzing/studayserver/global/config/JpaConfig.java +++ b/src/main/java/org/guzzing/studayserver/global/config/JpaConfig.java @@ -5,4 +5,6 @@ @EnableJpaAuditing @Configuration -public class JpaConfig {} +public class JpaConfig { + +} diff --git a/src/main/java/org/guzzing/studayserver/global/error/handler/GlobalExceptionRestHandler.java b/src/main/java/org/guzzing/studayserver/global/error/handler/GlobalExceptionRestHandler.java index 53ad1ff6..39469882 100644 --- a/src/main/java/org/guzzing/studayserver/global/error/handler/GlobalExceptionRestHandler.java +++ b/src/main/java/org/guzzing/studayserver/global/error/handler/GlobalExceptionRestHandler.java @@ -1,15 +1,20 @@ package org.guzzing.studayserver.global.error.handler; +import static org.guzzing.studayserver.global.error.response.ErrorCode.INTERNAL_SERVER_ERROR; +import static org.guzzing.studayserver.global.error.response.ErrorCode.INVALID_INPUT_VALUE_ERROR; +import static org.guzzing.studayserver.global.error.response.ErrorCode.INVALID_METHOD_ERROR; +import static org.guzzing.studayserver.global.error.response.ErrorCode.NOT_FOUND_ENTITY; +import static org.guzzing.studayserver.global.error.response.ErrorCode.REQUEST_BODY_MISSING_ERROR; +import static org.guzzing.studayserver.global.error.response.ErrorCode.REQUEST_PARAM_MISSING_ERROR; + import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import org.guzzing.studayserver.global.error.response.ErrorResponse; import org.springframework.data.crossstore.ChangeSetPersister; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; - import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -17,13 +22,12 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import static org.guzzing.studayserver.global.error.response.ErrorCode.*; - @RequiredArgsConstructor @RestControllerAdvice @Slf4j public class GlobalExceptionRestHandler { + /** * [Exception] 객체 혹은 파라미터의 데이터 값이 유효하지 않은 경우 */ @@ -58,7 +62,8 @@ protected ResponseEntity handleMissingRequestHeaderExceptionExcep * */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) - protected ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + protected ResponseEntity handleMethodArgumentTypeMismatchException( + MethodArgumentTypeMismatchException e) { log.warn("Handle MethodArgumentTypeMismatchException", e); final ErrorResponse response = ErrorResponse.of(INVALID_INPUT_VALUE_ERROR, e.getMessage()); diff --git a/src/main/java/org/guzzing/studayserver/global/error/response/ErrorResponse.java b/src/main/java/org/guzzing/studayserver/global/error/response/ErrorResponse.java index 90bf3266..0520c590 100644 --- a/src/main/java/org/guzzing/studayserver/global/error/response/ErrorResponse.java +++ b/src/main/java/org/guzzing/studayserver/global/error/response/ErrorResponse.java @@ -1,14 +1,13 @@ package org.guzzing.studayserver.global.error.response; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.validation.BindingResult; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - /** * Global Exception Rest Handler에서 발생한 에러에 대한 응답 처리를 관리 */ @@ -16,6 +15,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ErrorResponse { + private String code; //서버 내 에러코드 private String message; //에러 메시지 private List fieldDetailErrors; //상세 에러 메시지 @@ -52,6 +52,7 @@ private ErrorResponse(final ErrorCode errorCode, final List fi } public static class FieldDetailError { + private final String field; private final String value; private final String reason; diff --git a/src/main/java/org/guzzing/studayserver/global/exception/RegionException.java b/src/main/java/org/guzzing/studayserver/global/exception/RegionException.java new file mode 100644 index 00000000..b2208fba --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/global/exception/RegionException.java @@ -0,0 +1,9 @@ +package org.guzzing.studayserver.global.exception; + +public class RegionException extends RuntimeException { + + public RegionException(String message) { + super(message); + } + +} diff --git a/src/test/java/org/guzzing/studayserver/domain/region/controller/RegionRestControllerTest.java b/src/test/java/org/guzzing/studayserver/domain/region/controller/RegionRestControllerTest.java new file mode 100644 index 00000000..c0374476 --- /dev/null +++ b/src/test/java/org/guzzing/studayserver/domain/region/controller/RegionRestControllerTest.java @@ -0,0 +1,189 @@ +package org.guzzing.studayserver.domain.region.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import jakarta.transaction.Transactional; +import org.guzzing.studayserver.domain.region.service.RegionService; +import org.guzzing.studayserver.testutil.WithMockCustomOAuth2LoginUser; +import org.junit.jupiter.api.BeforeEach; +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.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@AutoConfigureRestDocs +@AutoConfigureMockMvc +@SpringBootTest +@Transactional +@ActiveProfiles({"dev", "oauth"}) +class RegionRestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private RegionService regionService; + + final String sido = "서울특별시"; + final String sigungu = "테스트구"; + final String upmyeondong = "테스트테스트동"; + final double latitude = 37.5664; + final double longitude = 126.972925; + + @BeforeEach + void setUp() { + regionService.createRegion(sido, sigungu, upmyeondong, latitude, longitude); + } + + @Test + @DisplayName("시도를 파라미터로 요청하면 해당 시도, 시군구, 개수 데이터를 반환한다.") + @WithMockCustomOAuth2LoginUser + void getSubRegions_Sido_RegionResponse() throws Exception { + // Given & When + ResultActions perform = mockMvc.perform(get("/regions/beopjungdong") + .param("sido", sido) + .contentType(APPLICATION_JSON_VALUE) + .accept(APPLICATION_JSON_VALUE)); + + // Then + perform.andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.targetRegion").value(sido)) + .andExpect(jsonPath("$.subRegion").isNotEmpty()) + .andExpect(jsonPath("$.subRegionCount").isNumber()) + .andDo(document("get-region-beopjungdong-sigungu", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("sido").description("시도") + ), + responseFields( + fieldWithPath("targetRegion").type(STRING).description("탐색한 시도명"), + fieldWithPath("subRegion").type(ARRAY).description("탐색한 시군구 조회 결과 리스트"), + fieldWithPath("subRegionCount").type(NUMBER).description("탐색한 시군구 조회 결과 수") + ) + )); + } + + @Test + @DisplayName("시도, 시군구를 요청 파라미터로 받아 해당 시도군구의 읍면동 데이터를 응답한다.") + @WithMockCustomOAuth2LoginUser + void getSubRegions_SidoAndSigungu_RegionResponse() throws Exception { + // Given & When + ResultActions perform = mockMvc.perform(get("/regions/beopjungdong") + .param("sido", sido) + .param("sigungu", sigungu) + .contentType(APPLICATION_JSON_VALUE) + .accept(APPLICATION_JSON_VALUE)); + + // Then + perform.andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.targetRegion").value(sido + " " + sigungu)) + .andExpect(jsonPath("$.subRegion").isNotEmpty()) + .andExpect(jsonPath("$.subRegionCount").isNumber()) + .andDo(document("get-region-beopjungdong-upmyeondong", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("sido").description("시도"), + parameterWithName("sigungu").description("시군구") + ), + responseFields( + fieldWithPath("targetRegion").type(STRING).description("탐색한 시도군구명"), + fieldWithPath("subRegion").type(ARRAY).description("탐색한 읍면동 조회 결과 리스트"), + fieldWithPath("subRegionCount").type(NUMBER).description("탐색한 읍면동 조회 결과 수") + ) + )); + } + + @Test + @DisplayName("아무런 파라미터 없이 요청하면 조회 가능한 시도 데이터를 반환한다.") + @WithMockCustomOAuth2LoginUser + void getSubRegions_None_RegionResponse() throws Exception { + // Given & When + ResultActions perform = mockMvc.perform(get("/regions/beopjungdong") + .contentType(APPLICATION_JSON_VALUE) + .accept(APPLICATION_JSON_VALUE)); + + // Then + perform.andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.targetRegion").value("전국")) + .andExpect(jsonPath("$.subRegion").exists()) + .andExpect(jsonPath("$.subRegionCount").isNumber()) + .andDo(document("get-region-beopjungdong-sido", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath("targetRegion").type(STRING).description("전국"), + fieldWithPath("subRegion").type(ARRAY).description("탐색 가능한 시도 조회 결과 리스트"), + fieldWithPath("subRegionCount").type(NUMBER).description("탐색 가능한 시도 조회 결과 수") + ) + )); + } + + @Test + @DisplayName("시도, 시군구, 읍면동 데이터를 요청받아, 해당하는 위경도 데이터를 응답한다.") + @WithMockCustomOAuth2LoginUser + void getLocation_AllAddress_RegionLocationResponse() throws Exception { + // Given & When + ResultActions perform = mockMvc.perform(get("/regions/location") + .param("sido", sido) + .param("sigungu", sigungu) + .param("upmyeondong", upmyeondong) + .contentType(APPLICATION_JSON_VALUE) + .accept(APPLICATION_JSON_VALUE)); + + // Then + perform.andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.sido").value(sido)) + .andExpect(jsonPath("$.sigungu").value(sigungu)) + .andExpect(jsonPath("$.upmyeondong").value(upmyeondong)) + .andExpect(jsonPath("$.latitude").value(latitude)) + .andExpect(jsonPath("$.longitude").value(longitude)) + .andDo(document("get-region-location", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("sido").description("시도"), + parameterWithName("sigungu").description("시군구"), + parameterWithName("upmyeondong").description("읍면동") + ), + responseFields( + fieldWithPath("sido").type(STRING).description("조회된 시도"), + fieldWithPath("sigungu").type(STRING).description("조회된 시군구"), + fieldWithPath("upmyeondong").type(STRING).description("조회된 읍면동"), + fieldWithPath("latitude").type(NUMBER).description("조회된 위도"), + fieldWithPath("longitude").type(NUMBER).description("조회된 경도") + ) + )); + } + +} \ No newline at end of file diff --git a/src/test/java/org/guzzing/studayserver/domain/region/service/RegionServiceTest.java b/src/test/java/org/guzzing/studayserver/domain/region/service/RegionServiceTest.java new file mode 100644 index 00000000..79f20282 --- /dev/null +++ b/src/test/java/org/guzzing/studayserver/domain/region/service/RegionServiceTest.java @@ -0,0 +1,87 @@ +package org.guzzing.studayserver.domain.region.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SidoResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.SigunguResult; +import org.guzzing.studayserver.domain.region.service.dto.beopjungdong.UpmyeondongResult; +import org.guzzing.studayserver.domain.region.service.dto.location.RegionResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@ActiveProfiles({"dev", "oauth"}) +class RegionServiceTest { + + @Autowired + private RegionService regionService; + + private RegionResult savedRegion; + + final String sido = "서울특별시"; + final String sigungu = "테스트구"; + final String upmyeondong = "테스트테스트동"; + + @BeforeEach + void setUp() { + final double latitude = 37.5664; + final double longitude = 126.972925; + + savedRegion = regionService.createRegion(sido, sigungu, upmyeondong, latitude, longitude); + } + + @Test + @DisplayName("시도를 받아 해당 시도의 시군구를 반환한다.") + void findSigungusBySido_Sido_SigunguResult() { + // Given & When + SigunguResult result = regionService.findSigungusBySido(sido); + + // Then + assertThat(result.sido()).isEqualTo(sido); + assertThat(result.sigunguCount()).isPositive(); + } + + @Test + @DisplayName("시도, 시군구를 받아 해당 시도군구의 읍면동을 반환한다.") + void findUpmyeondongBySidoAndSigungu_SidoAndSigungu_UpmyeondongResult() { + // Given & When + UpmyeondongResult result = regionService.findUpmyeondongBySidoAndSigungu(sido, sigungu); + + // Then + assertThat(result.sido()).isEqualTo(sido); + assertThat(result.sigungu()).isEqualTo(sigungu); + assertThat(result.upmyeondongCount()).isPositive(); + } + + @Test + @DisplayName("조회 가능한 시도를 반환한다.") + void findSido_None_SidoResult() { + // Given & When + SidoResult result = regionService.findSido(); + + // Then + assertThat(result.nation()).isEqualTo("전국"); + assertThat(result.sidos()).isNotEmpty(); + assertThat(result.sidoCount()).isEqualTo(2); + } + + @Test + @DisplayName("시도, 시군구, 읍면동 데이터를 요청받아, 해당하는 위경도를 반환한다.") + void findLocation_AllAddress_RegionResult() { + // Given & When + RegionResult result = regionService.findLocation(sido, sigungu, upmyeondong); + + // Then + assertThat(result.sido()).isEqualTo(sido); + assertThat(result.upmyeondong()).isEqualTo(upmyeondong); + assertThat(result.latitude()).isLessThanOrEqualTo(40.0); + assertThat(result.longtigute()).isLessThanOrEqualTo(130.0); + } + +} \ No newline at end of file diff --git a/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUser.java b/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUser.java index 5b63c3ce..8289201b 100644 --- a/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUser.java +++ b/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUser.java @@ -1,9 +1,8 @@ package org.guzzing.studayserver.testutil; -import org.springframework.security.test.context.support.WithSecurityContext; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.springframework.security.test.context.support.WithSecurityContext; @Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithMockCustomOAuth2LoginUserSecurityContextFactory.class) diff --git a/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUserSecurityContextFactory.java b/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUserSecurityContextFactory.java index bd98443c..2095af9f 100644 --- a/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUserSecurityContextFactory.java +++ b/src/test/java/org/guzzing/studayserver/testutil/WithMockCustomOAuth2LoginUserSecurityContextFactory.java @@ -1,5 +1,8 @@ package org.guzzing.studayserver.testutil; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.guzzing.studayserver.domain.auth.jwt.CustomUser; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -8,11 +11,9 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.test.context.support.WithSecurityContextFactory; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +public class WithMockCustomOAuth2LoginUserSecurityContextFactory implements + WithSecurityContextFactory { -public class WithMockCustomOAuth2LoginUserSecurityContextFactory implements WithSecurityContextFactory { @Override public SecurityContext createSecurityContext(WithMockCustomOAuth2LoginUser oAuth2LoginUser) { final SecurityContext context = SecurityContextHolder.createEmptyContext();