From 0498910485ebc199e96a58a33716b6ba74591a85 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Thu, 22 Aug 2024 02:39:06 +0900 Subject: [PATCH 01/19] =?UTF-8?q?=F0=9F=94=A5=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20final=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 매개변수와 객체 생성에 final은 안붙히기로 결정 --- .../auth/apple/AppleOauthService.java | 28 ------------- .../auth/apple/ApplePublicKeyGenerator.java | 18 ++++---- .../auth/apple/AppleTokenParser.java | 13 +++--- .../auth/apple/dto/ApplePublicKey.java | 42 +++++++------------ .../auth/apple/dto/ApplePublicKeys.java | 6 ++- .../dndtravel/auth/apple/dto/AppleUser.java | 14 ------- .../auth/service/AppleOauthService.java | 30 +++++++++++++ ...pRepository.java => RegionRepository.java} | 5 ++- .../dnd/dndtravel/test/TestController.java | 12 ------ 9 files changed, 69 insertions(+), 99 deletions(-) delete mode 100644 src/main/java/com/dnd/dndtravel/auth/apple/AppleOauthService.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/apple/dto/AppleUser.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java rename src/main/java/com/dnd/dndtravel/map/repository/{MapRepository.java => RegionRepository.java} (51%) delete mode 100644 src/main/java/com/dnd/dndtravel/test/TestController.java diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/AppleOauthService.java b/src/main/java/com/dnd/dndtravel/auth/apple/AppleOauthService.java deleted file mode 100644 index 2206972..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/apple/AppleOauthService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.dnd.dndtravel.auth.apple; - -import com.dnd.dndtravel.auth.apple.dto.ApplePublicKeys; -import com.dnd.dndtravel.auth.apple.dto.AppleUser; -import io.jsonwebtoken.Claims; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.security.PublicKey; -import java.util.Map; - -@RequiredArgsConstructor -@Component -public class AppleOauthService { - private static final String DEFAULT_NAME = "apple"; - private static final String CLAIM_EMAIL = "email"; - private final AppleTokenParser appleTokenParser; - private final AppleClient appleClient; - private final ApplePublicKeyGenerator applePublicKeyGenerator; - - public AppleUser createAppleUser(final String appleToken) { - final Map appleTokenHeader = appleTokenParser.parseHeader(appleToken); - final ApplePublicKeys applePublicKeys = appleClient.getApplePublicKeys(); - final PublicKey publicKey = applePublicKeyGenerator.generate(appleTokenHeader, applePublicKeys); - final Claims claims = appleTokenParser.extractClaims(appleToken, publicKey); - return new AppleUser(DEFAULT_NAME, claims.get(CLAIM_EMAIL, String.class)); - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java b/src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java index b08519b..d1475f1 100644 --- a/src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java +++ b/src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java @@ -19,24 +19,24 @@ public class ApplePublicKeyGenerator { private static final String KEY_ID_HEADER = "kid"; private static final int POSITIVE_SIGN_NUMBER = 1; - public PublicKey generate(final Map headers, final ApplePublicKeys publicKeys) { - final ApplePublicKey applePublicKey = publicKeys.getMatchingKey( + public PublicKey generate(Map headers, ApplePublicKeys publicKeys) { + ApplePublicKey applePublicKey = publicKeys.getMatchingKey( headers.get(SIGN_ALGORITHM_HEADER), headers.get(KEY_ID_HEADER) ); return generatePublicKey(applePublicKey); } - private PublicKey generatePublicKey(final ApplePublicKey applePublicKey) { - final byte[] nBytes = Base64.getUrlDecoder().decode(applePublicKey.getN()); - final byte[] eBytes = Base64.getUrlDecoder().decode(applePublicKey.getE()); + private PublicKey generatePublicKey(ApplePublicKey applePublicKey) { + byte[] nBytes = Base64.getUrlDecoder().decode(applePublicKey.n()); + byte[] eBytes = Base64.getUrlDecoder().decode(applePublicKey.e()); - final BigInteger n = new BigInteger(POSITIVE_SIGN_NUMBER, nBytes); - final BigInteger e = new BigInteger(POSITIVE_SIGN_NUMBER, eBytes); - final RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(n, e); + BigInteger n = new BigInteger(POSITIVE_SIGN_NUMBER, nBytes); + BigInteger e = new BigInteger(POSITIVE_SIGN_NUMBER, eBytes); + RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(n, e); try { - final KeyFactory keyFactory = KeyFactory.getInstance(applePublicKey.getKty()); + KeyFactory keyFactory = KeyFactory.getInstance(applePublicKey.kty()); return keyFactory.generatePublic(rsaPublicKeySpec); } catch (NoSuchAlgorithmException | InvalidKeySpecException exception) { throw new RuntimeException("잘못된 애플 키"); diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java b/src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java index 8b071f0..3fa5eea 100644 --- a/src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java +++ b/src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java @@ -22,10 +22,9 @@ public class AppleTokenParser { private static final String IDENTITY_TOKEN_VALUE_DELIMITER = "\\."; private static final int HEADER_INDEX = 0; - private final ObjectMapper objectMapper; - public Map parseHeader(final String appleToken) { + public Map parseHeader(String appleToken) { try { final String decodedHeader = appleToken.split(IDENTITY_TOKEN_VALUE_DELIMITER)[HEADER_INDEX]; return objectMapper.readValue(decodedHeader, Map.class); @@ -36,13 +35,13 @@ public Map parseHeader(final String appleToken) { } } - public Claims extractClaims(final String appleToken, final PublicKey publicKey) { + public Claims extractClaims(String appleToken, PublicKey publicKey) { try { return Jwts.parser() - .verifyWith(publicKey) - .build() - .parseSignedClaims(appleToken) - .getPayload(); + .verifyWith(publicKey) + .build() + .parseSignedClaims(appleToken) + .getPayload(); } catch (UnsupportedJwtException e) { throw new UnsupportedJwtException("지원되지 않는 jwt 타입"); } catch (IllegalArgumentException e) { diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java b/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java index a59abdf..f51e94c 100644 --- a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java +++ b/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java @@ -1,22 +1,25 @@ package com.dnd.dndtravel.auth.apple.dto; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; -/* +/** + * @param kty Key Type(RSA or EC(Elliptic Curve)) + * @param kid key ID + * @param use 퍼블릭 키가 어떤 용도로 사용되는지 명시 ("sig"(signature) or "enc"(encryption)) + * @param alg 어떤 알고리즘을 사용하는지 + * @param n RSA modulus + * @param e RSA public exponent */ /* id_token 검증 애플 서버에서 jwk 리스트를 받아와 이를 일급 컬렉션 형태로 관리 */ -@Getter -public class ApplePublicKey { - private final String kty; //Key Type(RSA or EC(Elliptic Curve)) - private final String kid; //key ID - private final String use; //퍼블릭 키가 어떤 용도로 사용되는지 명시 ("sig"(signature) or "enc"(encryption)) - private final String alg; //어떤 알고리즘을 사용하는지 - private final String n; //RSA modulus - private final String e; //RSA public exponent - +public record ApplePublicKey( + String kty, + String kid, + String use, + String alg, + String n, + String e +) { public boolean isSameAlg(final String alg) { return this.alg.equals(alg); } @@ -24,19 +27,4 @@ public boolean isSameAlg(final String alg) { public boolean isSameKid(final String kid) { return this.kid.equals(kid); } - - @JsonCreator - public ApplePublicKey(@JsonProperty("kty") final String kty, - @JsonProperty("kid") final String kid, - @JsonProperty("use") final String use, - @JsonProperty("alg") final String alg, - @JsonProperty("n") final String n, - @JsonProperty("e") final String e) { - this.kty = kty; - this.kid = kid; - this.use = use; - this.alg = alg; - this.n = n; - this.e = e; - } } diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java b/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java index 5ef6db7..f30d1ff 100644 --- a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java +++ b/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java @@ -3,7 +3,11 @@ import java.util.List; public class ApplePublicKeys { - private List keys; + private final List keys; + + public ApplePublicKeys(List keys) { + this.keys = keys; + } public ApplePublicKey getMatchingKey(final String alg, final String kid) { return keys.stream() diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/dto/AppleUser.java b/src/main/java/com/dnd/dndtravel/auth/apple/dto/AppleUser.java deleted file mode 100644 index b2f7ba7..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/apple/dto/AppleUser.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.dnd.dndtravel.auth.apple.dto; - -import lombok.Getter; - -@Getter -public class AppleUser { - private final String name; - private final String email; - - public AppleUser(final String name, final String email) { - this.name = name; - this.email = email; - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java new file mode 100644 index 0000000..dab2367 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java @@ -0,0 +1,30 @@ +package com.dnd.dndtravel.auth.service; + +import com.dnd.dndtravel.auth.apple.AppleClient; +import com.dnd.dndtravel.auth.apple.ApplePublicKeyGenerator; +import com.dnd.dndtravel.auth.apple.AppleTokenParser; +import com.dnd.dndtravel.auth.service.dto.response.AppleUserResponse; +import com.dnd.dndtravel.member.domain.Member; +import com.dnd.dndtravel.member.service.MemberService; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.security.PublicKey; + +@RequiredArgsConstructor +@Component +public class AppleOauthService { + private static final String DEFAULT_NAME = "apple"; + private static final String CLAIM_EMAIL = "email"; + private final AppleTokenParser appleTokenParser; + private final AppleClient appleClient; + private final ApplePublicKeyGenerator applePublicKeyGenerator; + + public AppleUserResponse createAppleUser(String appleToken) { + PublicKey publicKey = applePublicKeyGenerator.generate(appleTokenParser.parseHeader(appleToken), appleClient.getApplePublicKeys()); + Claims claims = appleTokenParser.extractClaims(appleToken, publicKey); // apple에게 받은 토큰을 까서, claim을 얻는다. + return new AppleUserResponse(DEFAULT_NAME, claims.get(CLAIM_EMAIL, String.class)); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/repository/MapRepository.java b/src/main/java/com/dnd/dndtravel/map/repository/RegionRepository.java similarity index 51% rename from src/main/java/com/dnd/dndtravel/map/repository/MapRepository.java rename to src/main/java/com/dnd/dndtravel/map/repository/RegionRepository.java index 00e8fd5..4793c85 100644 --- a/src/main/java/com/dnd/dndtravel/map/repository/MapRepository.java +++ b/src/main/java/com/dnd/dndtravel/map/repository/RegionRepository.java @@ -1,8 +1,11 @@ package com.dnd.dndtravel.map.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import com.dnd.dndtravel.map.domain.Region; -public interface MapRepository extends JpaRepository { +public interface RegionRepository extends JpaRepository { + Optional findByName(String region); } diff --git a/src/main/java/com/dnd/dndtravel/test/TestController.java b/src/main/java/com/dnd/dndtravel/test/TestController.java deleted file mode 100644 index bbc01ba..0000000 --- a/src/main/java/com/dnd/dndtravel/test/TestController.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.dnd.dndtravel.test; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class TestController { - @GetMapping("test") - public String printTest() { - return "CI/CD Success!"; - } -} \ No newline at end of file From e50736052c93e7933d97f4c6e30f6a40a193eeb0 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 02:02:32 +0900 Subject: [PATCH 02/19] =?UTF-8?q?refactor:=20OAuth=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dnd/dndtravel/auth/apple/AppleClient.java | 11 --- .../auth/apple/ApplePublicKeyGenerator.java | 45 ---------- .../auth/apple/AppleTokenParser.java | 53 ----------- .../auth/apple/dto/ApplePublicKey.java | 30 ------- .../auth/apple/dto/ApplePublicKeys.java | 18 ---- .../config/AppleFeignClientConfiguration.java | 14 +++ .../auth/config/AppleProperties.java | 20 +++++ .../auth/config/JwtSecurityConfig.java | 35 -------- .../auth/controller/AuthController.java | 44 +++++++++ .../auth/controller/AuthTokenController.java | 43 --------- .../controller/request/AppleLoginRequest.java | 9 ++ .../dndtravel/auth/service/AppleClient.java | 30 +++++++ .../service/AppleFeignClientErrorDecoder.java | 39 ++++++++ .../auth/service/AppleOAuthService.java | 90 +++++++++++++++++++ .../auth/service/AppleOauthService.java | 30 ------- .../dndtravel/auth/service/TokenDecoder.java | 31 +++++++ .../auth/service/dto/AuthMember.java | 20 ----- .../dto/request/AppleLoginRequest.java | 6 -- .../dto/response/AppleIdTokenPayload.java | 14 +++ .../AppleSocialTokenInfoResponse.java | 25 ++++++ .../dnd/dndtravel/member/domain/Member.java | 16 ++-- .../member/domain/SelectedColor.java | 27 ++++++ .../member/service/MemberService.java | 7 +- src/main/resources/application.yml | 17 +++- 24 files changed, 372 insertions(+), 302 deletions(-) delete mode 100644 src/main/java/com/dnd/dndtravel/auth/apple/AppleClient.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/config/AppleFeignClientConfiguration.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/config/AppleProperties.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/config/JwtSecurityConfig.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/controller/AuthTokenController.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/AppleFeignClientErrorDecoder.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/service/dto/AuthMember.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/service/dto/request/AppleLoginRequest.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleIdTokenPayload.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleSocialTokenInfoResponse.java create mode 100644 src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/AppleClient.java b/src/main/java/com/dnd/dndtravel/auth/apple/AppleClient.java deleted file mode 100644 index 401c1e8..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/apple/AppleClient.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.dnd.dndtravel.auth.apple; - -import com.dnd.dndtravel.auth.apple.dto.ApplePublicKeys; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; - -@FeignClient(name = "apple-public-key", url = "https://appleid.apple.com") -public interface AppleClient { - @GetMapping("/auth/keys") - ApplePublicKeys getApplePublicKeys(); -} diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java b/src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java deleted file mode 100644 index d1475f1..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/apple/ApplePublicKeyGenerator.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.dnd.dndtravel.auth.apple; - -import com.dnd.dndtravel.auth.apple.dto.ApplePublicKey; -import com.dnd.dndtravel.auth.apple.dto.ApplePublicKeys; -import org.springframework.stereotype.Component; - -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.RSAPublicKeySpec; -import java.util.Base64; -import java.util.Map; - -@Component -public class ApplePublicKeyGenerator { - private static final String SIGN_ALGORITHM_HEADER = "alg"; - private static final String KEY_ID_HEADER = "kid"; - private static final int POSITIVE_SIGN_NUMBER = 1; - - public PublicKey generate(Map headers, ApplePublicKeys publicKeys) { - ApplePublicKey applePublicKey = publicKeys.getMatchingKey( - headers.get(SIGN_ALGORITHM_HEADER), - headers.get(KEY_ID_HEADER) - ); - return generatePublicKey(applePublicKey); - } - - private PublicKey generatePublicKey(ApplePublicKey applePublicKey) { - byte[] nBytes = Base64.getUrlDecoder().decode(applePublicKey.n()); - byte[] eBytes = Base64.getUrlDecoder().decode(applePublicKey.e()); - - BigInteger n = new BigInteger(POSITIVE_SIGN_NUMBER, nBytes); - BigInteger e = new BigInteger(POSITIVE_SIGN_NUMBER, eBytes); - RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(n, e); - - try { - KeyFactory keyFactory = KeyFactory.getInstance(applePublicKey.kty()); - return keyFactory.generatePublic(rsaPublicKeySpec); - } catch (NoSuchAlgorithmException | InvalidKeySpecException exception) { - throw new RuntimeException("잘못된 애플 키"); - } - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java b/src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java deleted file mode 100644 index 3fa5eea..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/apple/AppleTokenParser.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.dnd.dndtravel.auth.apple; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.UnsupportedJwtException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.security.PublicKey; -import java.util.Map; - -/* -JWK 리스트 조회 -헤더 부분을 디코딩하여 일치하는 jwk를 찾을 alg, kid 값을 얻고, id_token Claim 추출 - */ -@RequiredArgsConstructor -@Component -public class AppleTokenParser { - private static final String IDENTITY_TOKEN_VALUE_DELIMITER = "\\."; - private static final int HEADER_INDEX = 0; - private final ObjectMapper objectMapper; - - public Map parseHeader(String appleToken) { - try { - final String decodedHeader = appleToken.split(IDENTITY_TOKEN_VALUE_DELIMITER)[HEADER_INDEX]; - return objectMapper.readValue(decodedHeader, Map.class); - } catch (JsonMappingException e) { - throw new RuntimeException("appleToken 값이 jwt 형식인지, 값이 정상적인지 확인해주세요."); - } catch (JsonProcessingException e) { - throw new RuntimeException("디코드된 헤더를 Map 형태로 분류할 수 없습니다. 헤더를 확인해주세요."); - } - } - - public Claims extractClaims(String appleToken, PublicKey publicKey) { - try { - return Jwts.parser() - .verifyWith(publicKey) - .build() - .parseSignedClaims(appleToken) - .getPayload(); - } catch (UnsupportedJwtException e) { - throw new UnsupportedJwtException("지원되지 않는 jwt 타입"); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("비어있는 jwt"); - } catch (JwtException e) { - throw new JwtException("jwt 검증 or 분석 오류"); - } - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java b/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java deleted file mode 100644 index f51e94c..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKey.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.dnd.dndtravel.auth.apple.dto; - -import lombok.Getter; - -/** - * @param kty Key Type(RSA or EC(Elliptic Curve)) - * @param kid key ID - * @param use 퍼블릭 키가 어떤 용도로 사용되는지 명시 ("sig"(signature) or "enc"(encryption)) - * @param alg 어떤 알고리즘을 사용하는지 - * @param n RSA modulus - * @param e RSA public exponent */ /* -id_token 검증 -애플 서버에서 jwk 리스트를 받아와 이를 일급 컬렉션 형태로 관리 - */ -public record ApplePublicKey( - String kty, - String kid, - String use, - String alg, - String n, - String e -) { - public boolean isSameAlg(final String alg) { - return this.alg.equals(alg); - } - - public boolean isSameKid(final String kid) { - return this.kid.equals(kid); - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java b/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java deleted file mode 100644 index f30d1ff..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/apple/dto/ApplePublicKeys.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.dnd.dndtravel.auth.apple.dto; - -import java.util.List; - -public class ApplePublicKeys { - private final List keys; - - public ApplePublicKeys(List keys) { - this.keys = keys; - } - - public ApplePublicKey getMatchingKey(final String alg, final String kid) { - return keys.stream() - .filter(key -> key.isSameAlg(alg) && key.isSameKid(kid)) - .findFirst() - .orElseThrow(() -> new RuntimeException("잘못된 토큰 형태입니다.")); - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/config/AppleFeignClientConfiguration.java b/src/main/java/com/dnd/dndtravel/auth/config/AppleFeignClientConfiguration.java new file mode 100644 index 0000000..fb46af1 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/config/AppleFeignClientConfiguration.java @@ -0,0 +1,14 @@ +package com.dnd.dndtravel.auth.config; + +import org.springframework.context.annotation.Bean; + +import com.dnd.dndtravel.auth.service.AppleFeignClientErrorDecoder; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class AppleFeignClientConfiguration { + + @Bean + public AppleFeignClientErrorDecoder appleFeignClientErrorDecoder() { + return new AppleFeignClientErrorDecoder(new ObjectMapper()); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/config/AppleProperties.java b/src/main/java/com/dnd/dndtravel/auth/config/AppleProperties.java new file mode 100644 index 0000000..81d17f5 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/config/AppleProperties.java @@ -0,0 +1,20 @@ +package com.dnd.dndtravel.auth.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Getter; +import lombok.Setter; + +@Component +@ConfigurationProperties(prefix = "social-login.provider.apple") +@Getter +@Setter // 프로퍼티 주입에 필수 +public class AppleProperties { + private String grantType; + private String clientId; + private String keyId; + private String teamId; + private String audience; + private String privateKey; +} diff --git a/src/main/java/com/dnd/dndtravel/auth/config/JwtSecurityConfig.java b/src/main/java/com/dnd/dndtravel/auth/config/JwtSecurityConfig.java deleted file mode 100644 index 9f0ff62..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/config/JwtSecurityConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.dnd.dndtravel.auth.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class JwtSecurityConfig { - - private final JwtProvider jwtProvider; - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf() - .disable() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .authorizeHttpRequests() - .requestMatchers("/login/**").permitAll() - .anyRequest().authenticated() - .and() - .addFilterBefore(new JwtFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java new file mode 100644 index 0000000..c805a30 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java @@ -0,0 +1,44 @@ +package com.dnd.dndtravel.auth.controller; + +import com.dnd.dndtravel.auth.service.dto.response.AppleIdTokenPayload; +import com.dnd.dndtravel.auth.service.AppleOAuthService; +import com.dnd.dndtravel.auth.service.JwtTokenService; +import com.dnd.dndtravel.auth.controller.request.AppleLoginRequest; +import com.dnd.dndtravel.auth.service.dto.response.TokenResponse; +import com.dnd.dndtravel.member.domain.Member; +import com.dnd.dndtravel.member.service.MemberService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class AuthController { + private final AppleOAuthService appleOAuthService; + private final JwtTokenService jwtTokenService; + private final MemberService memberService; + + //todo 클라이언트에서 실제 인증코드 보내주면 테스트 진행 필요 + @PostMapping("/login/oauth2/apple") + public ResponseEntity appleOAuthLogin(@RequestBody AppleLoginRequest appleLoginRequest) { + // 클라이언트에서 준 code 값으로 apple의 IdToken Payload를 얻어온다 + AppleIdTokenPayload tokenPayload = appleOAuthService.get(appleLoginRequest.appleToken()); + + // apple에서 가져온 유저정보를 DB에 저장 + Member member = memberService.saveMember(tokenPayload.name(), tokenPayload.email(), appleLoginRequest.selectedColor()); + + // 클라이언트와 주고받을 user token(access , refresh) 생성 + TokenResponse tokenResponse = jwtTokenService.generateTokens(member.getId()); + + // refresh token 재발급 필요시 + if (tokenResponse == null) { + return ResponseEntity.noContent().build(); + } + + return ResponseEntity.ok(tokenResponse); + } +} \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/AuthTokenController.java b/src/main/java/com/dnd/dndtravel/auth/controller/AuthTokenController.java deleted file mode 100644 index c0e14dd..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/controller/AuthTokenController.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.dnd.dndtravel.auth.controller; - -import com.dnd.dndtravel.auth.apple.AppleOauthService; -import com.dnd.dndtravel.auth.apple.dto.AppleUser; -import com.dnd.dndtravel.auth.config.JwtProvider; -import com.dnd.dndtravel.auth.repository.AuthTokenRepository; -import com.dnd.dndtravel.auth.service.dto.request.AppleLoginRequest; -import com.dnd.dndtravel.auth.service.dto.response.TokenResponse; -import com.dnd.dndtravel.member.domain.Member; -import com.dnd.dndtravel.member.service.MemberService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Collections; - -@RequiredArgsConstructor -@RestController -public class AuthTokenController { - - private final MemberService memberService; - private final AppleOauthService appleOauthService; - private final JwtProvider jwtProvider; - private final AuthTokenRepository authTokenRepository; - - @PostMapping("/login/oauth2/apple") - public TokenResponse appleOauthLogin(@RequestBody AppleLoginRequest appleLoginRequest) { - AppleUser appleUser = appleOauthService.createAppleUser(appleLoginRequest.appleToken()); - Member member = memberService.saveMember(appleUser); - - Authentication authentication = new UsernamePasswordAuthenticationToken(member.getId(), null, Collections.emptyList()); - String accessToken = jwtProvider.createToken(authentication); - String refreshToken = jwtProvider.createRefreshToken(member.getId()); - authTokenRepository.saveRefreshToken(member.getId(), refreshToken); // refreshToken은 DB에 저장 - - return new TokenResponse(accessToken); - } -} \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java b/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java new file mode 100644 index 0000000..4f76346 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java @@ -0,0 +1,9 @@ +package com.dnd.dndtravel.auth.controller.request; + +//todo 필드값들 유효성 체크 +// todo 입력 색상 예시는 클라에서 String 타입들. RED, ORANGE, YELLOW, MELON, BLUE, PURPLE +public record AppleLoginRequest( + String appleToken, + String selectedColor +){ +} \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java new file mode 100644 index 0000000..fc31c33 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java @@ -0,0 +1,30 @@ +package com.dnd.dndtravel.auth.service; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import com.dnd.dndtravel.auth.service.dto.response.AppleSocialTokenInfoResponse; +import com.dnd.dndtravel.auth.config.AppleFeignClientConfiguration; + +@FeignClient( + name = "apple-auth", + url = "https://appleid.apple.com", + configuration = AppleFeignClientConfiguration.class +) +public interface AppleClient { + /** + * @param clientId(required) : 맵땅의 식별자값 + * @param clientSecret(required) : 개발자가 만든 비밀 JWT 토큰, 개발자 계정과 비밀키로 애플에 로그인할때 사용된다. + * @param grantType(required) : refresh token과 authorization code 를 검증하기위해 사용됨, 우린 현재 authorization code 사용중 + * @param code : 애플에게 받은 오직 5분만 유효한 일회용 인증코드, authorization code 검증 용도로 필요하다. + * @return + */ + @PostMapping("/auth/token") + AppleSocialTokenInfoResponse getIdToken( + @RequestParam("client_id") String clientId, + @RequestParam("client_secret") String clientSecret, + @RequestParam("grant_type") String grantType, + @RequestParam("code") String code + ); +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleFeignClientErrorDecoder.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleFeignClientErrorDecoder.java new file mode 100644 index 0000000..4972d82 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/AppleFeignClientErrorDecoder.java @@ -0,0 +1,39 @@ +package com.dnd.dndtravel.auth.service; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import feign.Response; +import feign.codec.ErrorDecoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class AppleFeignClientErrorDecoder implements ErrorDecoder { + + private final ObjectMapper objectMapper; + + /** + * 애플 소셜 로그인 Feign API 연동 시 발생되는 오류에 대해서 예외 처리를 수행. + * + * @param methodKey Feign Client 메서드 이름 + * @param response 응답 정보 + */ + @Override + public Exception decode(String methodKey, Response response) { + Object body = null; + if (response != null && response.body() != null) { + try { + body = objectMapper.readValue(response.body().asInputStream(), Object.class); + } catch (IOException e) { + log.error("Error decoding response body", e); + } + } + + log.error("애플 소셜 로그인 Feign API Feign Client 호출 중 오류가 발생되었습니다. body: {}", body); + + return new RuntimeException("애플 소셜 로그인 Feign API Feign Client 호출 오류"); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java new file mode 100644 index 0000000..db94c78 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java @@ -0,0 +1,90 @@ +package com.dnd.dndtravel.auth.service; + +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.springframework.stereotype.Component; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; + +import java.security.PrivateKey; +import java.security.Security; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Base64; +import java.util.Date; + +import com.dnd.dndtravel.auth.service.dto.response.AppleIdTokenPayload; +import com.dnd.dndtravel.auth.config.AppleProperties; + +/** + * private key와 기타 설정값들로 client secret을 생성한다 + * client_secret: 개발자가 생성한 Secret JWT 토큰, 개발자 계정과 연결된 Apple로 로그인 개인 키를 사용, authorization code와 refresh token 검증 요청에 해당 파라미터 필요 + * client_secret을 만드는 관련 docs + * - https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret + * + * clinet_secret jwt 토큰 디코딩 예시 형식 + * { + * //JWT 헤더 + * "alg": "ES256", // 토큰에 서명된 알고리즘 + * "kid": "ABC123DEFG" // 개발자 계정과 연결된 계정 + private key 로 만들어진 식별자 + * } + * { + * "iss": "DEF123GHIJ",// 팀ID + * "iat": 1437179036, // + * "exp": 1493298100, // 만료시간 + * "aud": "https://appleid.apple.com", + * "sub": "com.mytest.app" + * } + * + */ + +@RequiredArgsConstructor +@Component +@Slf4j +public class AppleOAuthService { + private final AppleClient appleClient; + private final AppleProperties appleProperties; + + public AppleIdTokenPayload get(String authorizationCode) { + String idToken = appleClient.getIdToken( + appleProperties.getClientId(), + generateClientSecret(), + appleProperties.getGrantType(), + authorizationCode + ).idToken(); + + return TokenDecoder.decodePayload(idToken, AppleIdTokenPayload.class); + } + + private String generateClientSecret() { + LocalDateTime expiration = LocalDateTime.now().plusMinutes(5); + + return Jwts.builder() + .setHeaderParam(JwsHeader.KEY_ID, appleProperties.getKeyId()) + .setIssuer(appleProperties.getTeamId()) + .setAudience(appleProperties.getAudience()) + .setSubject(appleProperties.getClientId()) + .setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant())) + .setIssuedAt(new Date()) + .signWith(getPrivateKey(), SignatureAlgorithm.ES256) + .compact(); + } + + private PrivateKey getPrivateKey() { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + + try { + byte[] privateKeyBytes = Base64.getDecoder().decode(appleProperties.getPrivateKey()); + + PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes); + return converter.getPrivateKey(privateKeyInfo); + } catch (Exception e) { + throw new RuntimeException("Error converting private key from String", e); + } + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java deleted file mode 100644 index dab2367..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/service/AppleOauthService.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.dnd.dndtravel.auth.service; - -import com.dnd.dndtravel.auth.apple.AppleClient; -import com.dnd.dndtravel.auth.apple.ApplePublicKeyGenerator; -import com.dnd.dndtravel.auth.apple.AppleTokenParser; -import com.dnd.dndtravel.auth.service.dto.response.AppleUserResponse; -import com.dnd.dndtravel.member.domain.Member; -import com.dnd.dndtravel.member.service.MemberService; - -import io.jsonwebtoken.Claims; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.security.PublicKey; - -@RequiredArgsConstructor -@Component -public class AppleOauthService { - private static final String DEFAULT_NAME = "apple"; - private static final String CLAIM_EMAIL = "email"; - private final AppleTokenParser appleTokenParser; - private final AppleClient appleClient; - private final ApplePublicKeyGenerator applePublicKeyGenerator; - - public AppleUserResponse createAppleUser(String appleToken) { - PublicKey publicKey = applePublicKeyGenerator.generate(appleTokenParser.parseHeader(appleToken), appleClient.getApplePublicKeys()); - Claims claims = appleTokenParser.extractClaims(appleToken, publicKey); // apple에게 받은 토큰을 까서, claim을 얻는다. - return new AppleUserResponse(DEFAULT_NAME, claims.get(CLAIM_EMAIL, String.class)); - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java b/src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java new file mode 100644 index 0000000..a789b2f --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java @@ -0,0 +1,31 @@ +package com.dnd.dndtravel.auth.service; + +import java.util.Base64; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TokenDecoder { + /** + * 헤더, 페이로드, 서명 세 부분을 .으로 분리해 페이로드만 가져와서, Base64디코딩 하고 얻어온 바이트배열을 + * String변환, JSON 변환을 거쳐 targetClass의 객체로 변환 + * @param token + * @param targetClass + * @return + * @param + */ + public static T decodePayload(String token, Class targetClass) { + String[] tokenParts = token.split("\\."); + String payloadJWT = tokenParts[1]; + Base64.Decoder decoder = Base64.getUrlDecoder(); + String payload = new String(decoder.decode(payloadJWT)); + ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + try { + return objectMapper.readValue(payload, targetClass); + } catch (Exception e) { + throw new RuntimeException("Error decoding token payload", e); + } + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/dto/AuthMember.java b/src/main/java/com/dnd/dndtravel/auth/service/dto/AuthMember.java deleted file mode 100644 index 1125006..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/service/dto/AuthMember.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.dnd.dndtravel.auth.service.dto; - -import com.dnd.dndtravel.member.domain.Member; -import lombok.Getter; - -@Getter -public class AuthMember { - - private final Long id; - - private final String name; - - private final String email; - - public AuthMember(final Member member) { - this.id = member.getId(); - this.name = member.getName(); - this.email = member.getEmail(); - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/dto/request/AppleLoginRequest.java b/src/main/java/com/dnd/dndtravel/auth/service/dto/request/AppleLoginRequest.java deleted file mode 100644 index 5440b50..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/service/dto/request/AppleLoginRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.dnd.dndtravel.auth.service.dto.request; - -public record AppleLoginRequest( - String appleToken -){ -} \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleIdTokenPayload.java b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleIdTokenPayload.java new file mode 100644 index 0000000..ebb8724 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleIdTokenPayload.java @@ -0,0 +1,14 @@ +package com.dnd.dndtravel.auth.service.dto.response; + +/** + * apple의 idtoken을 까서 나오는 값들 + * @param sub: 사용자 고유 식별자(subject) + * @param name + * @param email + */ +public record AppleIdTokenPayload( + String sub, + String name, + String email +) { +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleSocialTokenInfoResponse.java b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleSocialTokenInfoResponse.java new file mode 100644 index 0000000..9f4cdcd --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleSocialTokenInfoResponse.java @@ -0,0 +1,25 @@ +package com.dnd.dndtravel.auth.service.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * + * @param accessToken + * @param tokenType: 액세스 토큰은 항상 Bearer + * @param expiresIn + * @param refreshToken + * @param idToken: 사용자 신원정보가 포함된 JWT + */ +public record AppleSocialTokenInfoResponse( + @JsonProperty("access_token") + String accessToken, + @JsonProperty("token_type") + String tokenType, + @JsonProperty("expires_in") + Long expiresIn, + @JsonProperty("refresh_token") + String refreshToken, + @JsonProperty("id_token") + String idToken +) { +} diff --git a/src/main/java/com/dnd/dndtravel/member/domain/Member.java b/src/main/java/com/dnd/dndtravel/member/domain/Member.java index 8691250..df9780a 100644 --- a/src/main/java/com/dnd/dndtravel/member/domain/Member.java +++ b/src/main/java/com/dnd/dndtravel/member/domain/Member.java @@ -1,6 +1,5 @@ package com.dnd.dndtravel.member.domain; -import com.dnd.dndtravel.auth.apple.dto.AppleUser; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -22,16 +21,21 @@ public class Member { @Column(nullable = false) private String email; + @Enumerated(EnumType.STRING) + private SelectedColor selectedColor; // 유저가 선택한 컬러 + @Builder - private Member(String name, String email){ + private Member(String name, String email, SelectedColor selectedColor) { this.name = name; this.email = email; + this.selectedColor = selectedColor; } - public static Member of(AppleUser appleUser) { + public static Member of(String userName, String email, String selectedColor) { return Member.builder() - .name(appleUser.getName()) - .email(appleUser.getEmail()) - .build(); + .name(userName) + .email(email) + .selectedColor(SelectedColor.convertToEnum(selectedColor)) + .build(); } } diff --git a/src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java b/src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java new file mode 100644 index 0000000..61dc5a1 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java @@ -0,0 +1,27 @@ +package com.dnd.dndtravel.member.domain; + +import java.util.Arrays; + +import lombok.Getter; + +@Getter +public enum SelectedColor { + RED("RED"), + ORANGE("ORANGE"), + YELLOW("YELLOW"), + MELON("MELON"), + BLUE("BLUE"), + PURPLE("PURPLE"); + + private final String value; + + SelectedColor(String value) { + this.value = value; + } + + public static SelectedColor convertToEnum(String selectedColor) { + return Arrays.stream(SelectedColor.values()) + .filter(color -> color.getValue().equalsIgnoreCase(selectedColor)) + .findAny().orElseThrow(() -> new RuntimeException("존재하지 않는 색상입니다")); + } +} diff --git a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java index bd12692..973a345 100644 --- a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java +++ b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java @@ -1,6 +1,5 @@ package com.dnd.dndtravel.member.service; -import com.dnd.dndtravel.auth.apple.dto.AppleUser; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -14,8 +13,8 @@ public class MemberService { private final MemberRepository memberRepository; @Transactional - public Member saveMember(AppleUser appleUser) { - return memberRepository.findByEmail(appleUser.getEmail()) - .orElseGet(() -> memberRepository.save(Member.of(appleUser))); + public Member saveMember(String name, String email, String selectedColor) { + return memberRepository.findByEmail(email) + .orElseGet(() -> memberRepository.save(Member.of(name, email,selectedColor))); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0ca5cc4..d08fffb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,19 @@ spring: hibernate: dialect: org.hibernate.dialect.MySQLDialect jwt: - secret-key: ${JWT_SECRET_KEY} \ No newline at end of file + secret-key: ${JWT_SECRET_KEY} open-in-view: false + +social-login: + provider: + apple: + grant-type: authorization_code + client-id: ${APPLE_CLIENT_ID} + key-id: ${APPLE_KEY_ID} # 애플에서 제공하는 키의 ID + team-id: ${APPLE_TEAM_ID} # 애플 개발자 계정의 팀 ID + audience: https://appleid.apple.com + private-key: ${APPLE_PRIVATE_KEY} + +jwt: + secret-key: ${JWT_SECRET_KEY} + access-token-expired-ms: ${ACCESS_TOKEN_EXPIRE} + refresh-token-expired-ms: ${REFRESH_TOKEN_EXPIRE} From 164da29380c064b112e9e199e609dd17df7483aa Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 02:04:56 +0900 Subject: [PATCH 03/19] =?UTF-8?q?refactor:=20refresh=20token=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dnd/dndtravel/auth/config/JwtFilter.java | 50 --------- .../dndtravel/auth/config/JwtProvider.java | 102 ------------------ ...hTokenCollector.java => RefreshToken.java} | 18 +++- .../auth/repository/AuthTokenRepository.java | 15 --- .../repository/RefreshTokenRepository.java | 10 ++ .../dndtravel/auth/service/JwtProvider.java | 45 ++++++++ .../auth/service/JwtTokenService.java | 33 ++++++ .../service/dto/response/TokenResponse.java | 3 +- 8 files changed, 106 insertions(+), 170 deletions(-) delete mode 100644 src/main/java/com/dnd/dndtravel/auth/config/JwtFilter.java delete mode 100644 src/main/java/com/dnd/dndtravel/auth/config/JwtProvider.java rename src/main/java/com/dnd/dndtravel/auth/domain/{RefreshTokenCollector.java => RefreshToken.java} (53%) delete mode 100644 src/main/java/com/dnd/dndtravel/auth/repository/AuthTokenRepository.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/repository/RefreshTokenRepository.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java diff --git a/src/main/java/com/dnd/dndtravel/auth/config/JwtFilter.java b/src/main/java/com/dnd/dndtravel/auth/config/JwtFilter.java deleted file mode 100644 index 6577da2..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/config/JwtFilter.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.dnd.dndtravel.auth.config; - -import io.jsonwebtoken.io.IOException; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.GenericFilterBean; - -@RequiredArgsConstructor -public class JwtFilter extends GenericFilterBean { - - private static final String ACCESS_HEADER= "Authorization"; - private static final int BEARER_SPLIT = 7; - private final JwtProvider jwtProvider; - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, java.io.IOException { - HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - String token = resolveToken(httpServletRequest); - String requestURI = httpServletRequest.getRequestURI(); - - if (StringUtils.hasText(token) && jwtProvider.validateToken(token)) { - Authentication authentication = jwtProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(authentication); - } - else { - //예외 처리 - System.out.println("유효한 jwt 토큰이 없습니다. uri: " + requestURI); - } - - filterChain.doFilter(servletRequest, servletResponse); - } - - // Request Header 토큰 정보 추출 - private String resolveToken(HttpServletRequest request) { - String bearerToken = request.getHeader(ACCESS_HEADER); - - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(BEARER_SPLIT); - } - - return null; - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/config/JwtProvider.java b/src/main/java/com/dnd/dndtravel/auth/config/JwtProvider.java deleted file mode 100644 index b7a790e..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/config/JwtProvider.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.dnd.dndtravel.auth.config; - -import io.jsonwebtoken.*; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.SecurityException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.stereotype.Component; - -import java.security.Key; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.stream.Collectors; - -@Component -public class JwtProvider implements InitializingBean { - - private static final String AUTHORITIES_KEY = "memberId"; - private final long tokenValidityInMilliseconds; - private final String secretKey; - private Key key; - - public JwtProvider( - @Value("${JWT_SECRET_KEY}") String secretKey, - @Value("86400") long tokenValidityInSeconds) { - this.secretKey = secretKey; - this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000; - } - - // secretKey 값을 Base64 Decode 해서 key 변수에 할당 - @Override - public void afterPropertiesSet() { - byte[] keyBytes = secretKey.getBytes(); //32바이트 이상 - this.key = Keys.hmacShaKeyFor(keyBytes); - } - - // JWT Access 토큰 생성 - public String createToken(Authentication authentication) { - Long memberId = Long.parseLong(authentication.getName()); - long now = (new Date()).getTime(); - Date validity = new Date(now + this.tokenValidityInMilliseconds); //1일 - - return Jwts.builder() - .setSubject(String.valueOf(memberId)) - .claim(AUTHORITIES_KEY, memberId) - .signWith(key, SignatureAlgorithm.HS256) - .setExpiration(validity) - .compact(); - } - - // Refresh 토큰 생성 - public String createRefreshToken(Long memberId) { - long now = (new Date()).getTime(); - Date validity = new Date(now + this.tokenValidityInMilliseconds * 14); // 14일 - - return Jwts.builder() - .setSubject(String.valueOf(memberId)) - .signWith(key, SignatureAlgorithm.HS256) - .setExpiration(validity) - .compact(); - } - - // authentication 객체 생성 - public Authentication getAuthentication(String token) { - Claims claims = Jwts.parser() - .setSigningKey(key) - .build() - .parseClaimsJws(token) - .getBody(); - Collection authorities = - Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - - return new UsernamePasswordAuthenticationToken(claims.get(AUTHORITIES_KEY), token, authorities); - } - - // Token 유효성 검증 - public boolean validateToken(String token) { - try { - Jwts.parser().setSigningKey(key).build().parseClaimsJws(token); - return true; - } catch (SecurityException | MalformedJwtException e) { - // 커스텀 예외 처리 - System.out.println("잘못된 JWT Signature"); - } catch (ExpiredJwtException e) { - System.out.println("만료된 JWT 토큰"); - } catch (UnsupportedJwtException e) { - System.out.println("지원 되지 않는 JWT 토큰"); - } catch (IllegalArgumentException e) { - System.out.println("잘못된 JWT 토큰"); - } - - return false; - } -} diff --git a/src/main/java/com/dnd/dndtravel/auth/domain/RefreshTokenCollector.java b/src/main/java/com/dnd/dndtravel/auth/domain/RefreshToken.java similarity index 53% rename from src/main/java/com/dnd/dndtravel/auth/domain/RefreshTokenCollector.java rename to src/main/java/com/dnd/dndtravel/auth/domain/RefreshToken.java index 48423cf..35400c7 100644 --- a/src/main/java/com/dnd/dndtravel/auth/domain/RefreshTokenCollector.java +++ b/src/main/java/com/dnd/dndtravel/auth/domain/RefreshToken.java @@ -1,5 +1,7 @@ package com.dnd.dndtravel.auth.domain; +import java.time.LocalDateTime; + import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -8,7 +10,8 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class RefreshTokenCollector { +public class RefreshToken { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -19,8 +22,19 @@ public class RefreshTokenCollector { @Column(nullable = false) private String refreshToken; - public RefreshTokenCollector(Long memberId, String refreshToken) { + private LocalDateTime expiredTime; + + private RefreshToken(Long memberId, String refreshToken) { this.memberId = memberId; this.refreshToken = refreshToken; + this.expiredTime = LocalDateTime.now(); + } + + public static RefreshToken of(Long memberId, String refreshToken) { + return new RefreshToken(memberId, refreshToken); + } + + public boolean isExpire() { + return this.expiredTime.isBefore(LocalDateTime.now()); } } \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/repository/AuthTokenRepository.java b/src/main/java/com/dnd/dndtravel/auth/repository/AuthTokenRepository.java deleted file mode 100644 index ca7e3c0..0000000 --- a/src/main/java/com/dnd/dndtravel/auth/repository/AuthTokenRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.dnd.dndtravel.auth.repository; - -import com.dnd.dndtravel.auth.domain.RefreshTokenCollector; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -@Repository -public interface AuthTokenRepository extends JpaRepository { - @Modifying - @Query("UPDATE RefreshTokenCollector a SET a.refreshToken = :refreshToken WHERE a.id = :id") - void saveRefreshToken(@Param("id") Long id, @Param("refreshToken") String refreshToken); -} \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/repository/RefreshTokenRepository.java b/src/main/java/com/dnd/dndtravel/auth/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..05d9236 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,10 @@ +package com.dnd.dndtravel.auth.repository; + +import com.dnd.dndtravel.auth.domain.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RefreshTokenRepository extends JpaRepository { + RefreshToken findByMemberId(Long memberId); +} \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java b/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java new file mode 100644 index 0000000..99671c6 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java @@ -0,0 +1,45 @@ +package com.dnd.dndtravel.auth.service; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Base64; +import java.util.Date; + +@Component +public class JwtProvider { + private static final String CLAIM_CONTENT = "memberId"; + private final long accessTokenExpiredTime; + private final long refreshTokenExpiredTime; + private final String secretKey; + + public JwtProvider( + @Value("${jwt.secret-key}") String secretKey, + @Value("${jwt.access-token-expired-ms}") long accessTokenExpiredTime, + @Value("${jwt.refresh-token-expired-ms}") long refreshTokenExpiredTime + ) { + this.secretKey = secretKey; + this.accessTokenExpiredTime = accessTokenExpiredTime; + this.refreshTokenExpiredTime = refreshTokenExpiredTime; + } + + public String accessToken(Long memberId) { + return Jwts.builder() + .claim(CLAIM_CONTENT, memberId) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + this.accessTokenExpiredTime)) + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(this.secretKey))) + .compact(); + } + + public String refreshToken(Long memberId) { + return Jwts.builder() + .claim(CLAIM_CONTENT, memberId) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + this.refreshTokenExpiredTime)) + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(this.secretKey))) + .compact(); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java new file mode 100644 index 0000000..a52ddea --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java @@ -0,0 +1,33 @@ +package com.dnd.dndtravel.auth.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.dndtravel.auth.domain.RefreshToken; +import com.dnd.dndtravel.auth.repository.RefreshTokenRepository; +import com.dnd.dndtravel.auth.service.dto.response.TokenResponse; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class JwtTokenService { + private final JwtProvider jwtProvider; + private final RefreshTokenRepository refreshTokenRepository; + + // todo 현재 refreshtoken하나로 계속해서 발급해주는 모델인데, 이는 리프레쉬 탈취시 보안위협있음. 추후 개선 필요 + @Transactional + public TokenResponse generateTokens(Long memberId) { + RefreshToken refreshToken = refreshTokenRepository.findByMemberId(memberId); + + if (refreshToken == null) { + String newRefreshToken = jwtProvider.refreshToken(memberId); + refreshTokenRepository.save(RefreshToken.of(memberId, newRefreshToken)); // refreshToken은 DB에 저장 + return new TokenResponse(jwtProvider.accessToken(memberId), newRefreshToken); + } else if (refreshToken.isExpire()) { + return null; + } + + return new TokenResponse(jwtProvider.accessToken(memberId), null); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/dto/response/TokenResponse.java b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/TokenResponse.java index e41c3b2..e1c096b 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/dto/response/TokenResponse.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/TokenResponse.java @@ -1,6 +1,7 @@ package com.dnd.dndtravel.auth.service.dto.response; public record TokenResponse( - String accessToken + String accessToken, + String refreshToken ) { } \ No newline at end of file From 9d029c377a33241b5cbc35df9db8c2b65e15c99f Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 02:06:17 +0900 Subject: [PATCH 04/19] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20yml=ED=8C=8C=EC=9D=BC=20=EB=82=B4=EC=9A=A9=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0,=20gradle=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 10 +++++++--- src/main/resources/application.yml | 12 +----------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 63479ed..fa8533e 100644 --- a/build.gradle +++ b/build.gradle @@ -24,16 +24,20 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + //OpenFeign implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.3' + //JWT implementation 'io.jsonwebtoken:jjwt-api:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.bouncycastle:bcprov-jdk18on:1.75' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.75' + - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d08fffb..60908d5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,22 +1,12 @@ spring: application: name: dnd-travel-project - profiles: - include: - - db datasource: url: ${MYSQL_URL} username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} - mvc: - pathmatch: - matching-strategy: ant_path_matcher jpa: - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - jwt: - secret-key: ${JWT_SECRET_KEY} open-in-view: false + open-in-view: false social-login: provider: From 7841292131fb2dbb5ebfc262bd12842397114eb2 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:48:33 +0900 Subject: [PATCH 05/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20add=20s3=20and=20?= =?UTF-8?q?update=20actions=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI.yml | 36 +++++++++++++++---- build.gradle | 2 ++ .../com/dnd/dndtravel/config/S3Config.java | 34 ++++++++++++++++++ src/main/resources/application.yml | 13 +++++++ 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/dnd/dndtravel/config/S3Config.java diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4def9d1..e09f4c3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,9 +8,21 @@ on: env: AWS_REGION: ap-northeast-2 - AWS_S3_BUCKET: mapddang-s3 + AWS_S3_BUCKET: ${{ secrets.AWS_BUCKET_NAME }} AWS_CODE_DEPLOY_APPLICATION: mapddang-CD AWS_CODE_DEPLOY_GROUP: mapddang-CD-group + MYSQL_URL: ${{ secrets.MYSQL_URL }} + MYSQL_USERNAME: ${{ secrets.MYSQL_USERNAME }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + APPLE_CLIENT_ID: ${{ secrets.APPLE_CLIENT_ID }} + APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_PRIVATE_KEY: ${{ secrets.APPLE_PRIVATE_KEY }} + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + ACCESS_TOKEN_EXPIRE: ${{ secrets.ACCESS_TOKEN_EXPIRE }} + REFRESH_TOKEN_EXPIRE: ${{ secrets.REFRESH_TOKEN_EXPIRE }} + AWS_S3_ACCESS_KEY: ${{ secrets.AWS_S3_ACCESS_KEY }} + AWS_S3_SECRET_KEY: ${{ secrets.AWS_S3_SECRET_KEY }} jobs: build-with-gradle: @@ -30,19 +42,29 @@ jobs: #gradlew 파일에 실행 권한 부여 - name: Grant execute permission to gradlew run: chmod +x ./gradlew - #프로젝트 빌드(test 제외) + #프로젝트 빌드 - name: Build Project - run: ./gradlew clean build -x test + run: ./gradlew build #AWS 자격증명 설정 - name: Setup AWS credential uses: aws-actions/configure-aws-credentials@v1 with: - aws-access-key-id: ${{ secrets.EC2_ACCESS_KEY }} - aws-secret-access-key: ${{ secrets.EC2_SECRET_KEY }} + aws-access-key-id: ${{ secrets.AWS_S3_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_S3_SECRET_KEY }} aws-region: ap-northeast-2 #ZIP 파일을 S3 버킷에 업로드 - name: Upload to AWS S3 - run: aws deploy push --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} --ignore-hidden-files --s3-location s3://$AWS_S3_BUCKET/cicdtest/$GITHUB_SHA.zip --source . + run: | + aws deploy push \ + --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} \ + --ignore-hidden-files \ + --s3-location s3://$AWS_S3_BUCKET/cicdtest/$GITHUB_SHA.zip \ + --source #업로드된 ZIP 파일을 CodeDeploy로 배포 - name: AWS Code Deploy - run: aws deploy create-deployment --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name ${{ env.AWS_CODE_DEPLOY_GROUP }} --s3-location bucket=$AWS_S3_BUCKET,key=cicdtest/$GITHUB_SHA.zip,bundleType=zip + run: | + aws deploy create-deployment \ + --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} \ + --deployment-config-name CodeDeployDefault.AllAtOnce \ + --deployment-group-name ${{ env.AWS_CODE_DEPLOY_GROUP }} \ + --s3-location bucket=$AWS_S3_BUCKET,bundleType=zip,key=cicdtest/$GITHUB_SHA.zip \ No newline at end of file diff --git a/build.gradle b/build.gradle index fa8533e..2206c02 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,8 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk18on:1.75' implementation 'org.bouncycastle:bcpkix-jdk18on:1.75' + //AWS S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/dnd/dndtravel/config/S3Config.java b/src/main/java/com/dnd/dndtravel/config/S3Config.java new file mode 100644 index 0000000..ad6db25 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/config/S3Config.java @@ -0,0 +1,34 @@ +package com.dnd.dndtravel.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 60908d5..96bdde0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,3 +22,16 @@ jwt: secret-key: ${JWT_SECRET_KEY} access-token-expired-ms: ${ACCESS_TOKEN_EXPIRE} refresh-token-expired-ms: ${REFRESH_TOKEN_EXPIRE} + +cloud: + aws: + credentials: + accessKey: ${AWS_S3_ACCESS_KEY} + secretKey: ${AWS_S3_SECRET_KEY} + region: + static: ap-northeast-2 + stack: + auto: false + s3: + bucketName: ${AWS_BUCKET_NAME} + From 65f64d450891abb7beb7e3cc49ba419caad72d1b Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:03:35 +0900 Subject: [PATCH 06/19] =?UTF-8?q?=E2=9C=A8feat:=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=EC=97=90=20=EB=B0=A9?= =?UTF-8?q?=EB=AC=B8=ED=9A=9F=EC=88=98=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/controller/MapController.java | 4 +- .../dndtravel/map/domain/MemberRegion.java | 54 +++++++++++++++++++ .../com/dnd/dndtravel/map/domain/Region.java | 18 +++---- .../repository/MemberRegionRepository.java | 11 ++++ .../dnd/dndtravel/map/service/MapService.java | 31 +++++++---- .../service/dto/response/RegionResponse.java | 17 ++++-- 6 files changed, 108 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java create mode 100644 src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java diff --git a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java index 76133d3..184439a 100644 --- a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java +++ b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java @@ -16,6 +16,8 @@ public class MapController { @GetMapping("/maps") public RegionResponse map() { - return mapService.allRegions(); + Long memberId = 1L; + return mapService.allRegions(memberId); + } } } diff --git a/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java b/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java new file mode 100644 index 0000000..d1af6a2 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java @@ -0,0 +1,54 @@ +package com.dnd.dndtravel.map.domain; + +import com.dnd.dndtravel.member.domain.Member; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class MemberRegion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "region_id") + private Region region; + + private int visitCount; // 방문 횟수 + + @Builder + private MemberRegion(Member member, Region region, int visitCount) { + this.member = member; + this.region = region; + this.visitCount = visitCount; + } + + public static MemberRegion of(Member member, Region region, int visitCount) { + return MemberRegion.builder() + .member(member) + .region(region) + .visitCount(visitCount) + .build(); + } + + public boolean isVisited() { + return this.visitCount > 0; + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/domain/Region.java b/src/main/java/com/dnd/dndtravel/map/domain/Region.java index ef0d29c..dcfb438 100644 --- a/src/main/java/com/dnd/dndtravel/map/domain/Region.java +++ b/src/main/java/com/dnd/dndtravel/map/domain/Region.java @@ -1,8 +1,6 @@ package com.dnd.dndtravel.map.domain; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -21,19 +19,15 @@ public class Region { private String name; // 지역 이름 - @Enumerated(EnumType.STRING) - private VisitOpacity visitOpacity; // 방문 횟수(색의 opacity) - - public static Region of(String name, VisitOpacity visitOpacity) { - return new Region(name, visitOpacity); + public static Region of(String name) { + return new Region(name); } - public boolean isVisited() { - return visitOpacity.isNotZero(); + private Region(String name) { + this.name = name; } - private Region(String name, VisitOpacity visitOpacity) { - this.name = name; - this.visitOpacity = visitOpacity; + public boolean isEqualTo(String name) { + return this.name.equals(name); } } diff --git a/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java b/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java new file mode 100644 index 0000000..c09db39 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java @@ -0,0 +1,11 @@ +package com.dnd.dndtravel.map.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dnd.dndtravel.map.domain.MemberRegion; + +public interface MemberRegionRepository extends JpaRepository { + List findByMemberId(Long memberId); +} diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 3f70715..9ceb789 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -15,20 +15,33 @@ @RequiredArgsConstructor @Service public class MapService { + private final RegionRepository regionRepository; + private final MemberRepository memberRepository; + private final MemberRegionRepository memberRegionRepository; - private final MapRepository mapRepository; - + // 모든 지역 조회(홈) @Transactional(readOnly = true) - public RegionResponse allRegions() { - List all = mapRepository.findAll(); + public RegionResponse allRegions(Long memberId) { + //todo custom ex + Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저")); - List regions = all.stream() + // 유저의 지역 방문기록 전부 가져온다 + List memberRegions = memberRegionRepository.findByMemberId(memberId); + List regions = regionRepository.findAll().stream() .map(RegionDto::from) .toList(); - int visitCount = (int)all.stream() - .filter(Region::isVisited) - .count(); - return new RegionResponse(regions, visitCount); + if (memberRegions.isEmpty()) { + return new RegionResponse(regions, member.getSelectedColor()); + } + + return new RegionResponse( + updateRegionDto(regions, memberRegions), + (int)memberRegions.stream() + .filter(MemberRegion::isVisited) + .count(), + member.getSelectedColor() + ); + } } } diff --git a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java index 7ca098f..515f90e 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java +++ b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java @@ -3,16 +3,23 @@ import java.util.List; import com.dnd.dndtravel.map.service.dto.RegionDto; +import com.dnd.dndtravel.member.domain.SelectedColor; public record RegionResponse( - List regions, - int visitCount, - int totalCount + List regions, // 지역별 opacity 정보, 땅 이름 + int visitCount, // 방문 지도 개수 + int totalCount, // 전체 땅 개수 + SelectedColor selectedColor // 선택된 컬러 ) { private static final int TOTAL_COUNT = 16; // 전체 지역구의 개수, 변경가능성이 낮아 16이라는 상수로 고정 + private static final int DEFAULT_VISIT_COUNT = 0; - public RegionResponse(List regions, int visitCount) { - this(regions, visitCount, TOTAL_COUNT); + public RegionResponse(List regions, int visitCount, SelectedColor selectedColor) { + this(regions, visitCount, TOTAL_COUNT, selectedColor); + } + + public RegionResponse(List regions, SelectedColor selectedColor) { + this(regions, DEFAULT_VISIT_COUNT, TOTAL_COUNT, selectedColor); } } From ddb2e27cba5510d8ce9a4a185e3bdba42da7b72b Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:20:28 +0900 Subject: [PATCH 07/19] =?UTF-8?q?Revert=20"=E2=9C=A8feat:=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=EC=97=90=20?= =?UTF-8?q?=EB=B0=A9=EB=AC=B8=ED=9A=9F=EC=88=98=20=ED=91=9C=EC=8B=9C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 65f64d450891abb7beb7e3cc49ba419caad72d1b. --- .../map/controller/MapController.java | 4 +- .../dndtravel/map/domain/MemberRegion.java | 54 ------------------- .../com/dnd/dndtravel/map/domain/Region.java | 18 ++++--- .../repository/MemberRegionRepository.java | 11 ---- .../dnd/dndtravel/map/service/MapService.java | 31 ++++------- .../service/dto/response/RegionResponse.java | 17 ++---- 6 files changed, 27 insertions(+), 108 deletions(-) delete mode 100644 src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java delete mode 100644 src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java diff --git a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java index 184439a..76133d3 100644 --- a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java +++ b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java @@ -16,8 +16,6 @@ public class MapController { @GetMapping("/maps") public RegionResponse map() { - Long memberId = 1L; - return mapService.allRegions(memberId); - } + return mapService.allRegions(); } } diff --git a/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java b/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java deleted file mode 100644 index d1af6a2..0000000 --- a/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.dnd.dndtravel.map.domain; - -import com.dnd.dndtravel.member.domain.Member; - -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Entity -public class MemberRegion { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "region_id") - private Region region; - - private int visitCount; // 방문 횟수 - - @Builder - private MemberRegion(Member member, Region region, int visitCount) { - this.member = member; - this.region = region; - this.visitCount = visitCount; - } - - public static MemberRegion of(Member member, Region region, int visitCount) { - return MemberRegion.builder() - .member(member) - .region(region) - .visitCount(visitCount) - .build(); - } - - public boolean isVisited() { - return this.visitCount > 0; - } -} diff --git a/src/main/java/com/dnd/dndtravel/map/domain/Region.java b/src/main/java/com/dnd/dndtravel/map/domain/Region.java index dcfb438..ef0d29c 100644 --- a/src/main/java/com/dnd/dndtravel/map/domain/Region.java +++ b/src/main/java/com/dnd/dndtravel/map/domain/Region.java @@ -1,6 +1,8 @@ package com.dnd.dndtravel.map.domain; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -19,15 +21,19 @@ public class Region { private String name; // 지역 이름 - public static Region of(String name) { - return new Region(name); + @Enumerated(EnumType.STRING) + private VisitOpacity visitOpacity; // 방문 횟수(색의 opacity) + + public static Region of(String name, VisitOpacity visitOpacity) { + return new Region(name, visitOpacity); } - private Region(String name) { - this.name = name; + public boolean isVisited() { + return visitOpacity.isNotZero(); } - public boolean isEqualTo(String name) { - return this.name.equals(name); + private Region(String name, VisitOpacity visitOpacity) { + this.name = name; + this.visitOpacity = visitOpacity; } } diff --git a/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java b/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java deleted file mode 100644 index c09db39..0000000 --- a/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.dnd.dndtravel.map.repository; - -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.dnd.dndtravel.map.domain.MemberRegion; - -public interface MemberRegionRepository extends JpaRepository { - List findByMemberId(Long memberId); -} diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 9ceb789..3f70715 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -15,33 +15,20 @@ @RequiredArgsConstructor @Service public class MapService { - private final RegionRepository regionRepository; - private final MemberRepository memberRepository; - private final MemberRegionRepository memberRegionRepository; - // 모든 지역 조회(홈) + private final MapRepository mapRepository; + @Transactional(readOnly = true) - public RegionResponse allRegions(Long memberId) { - //todo custom ex - Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저")); + public RegionResponse allRegions() { + List all = mapRepository.findAll(); - // 유저의 지역 방문기록 전부 가져온다 - List memberRegions = memberRegionRepository.findByMemberId(memberId); - List regions = regionRepository.findAll().stream() + List regions = all.stream() .map(RegionDto::from) .toList(); + int visitCount = (int)all.stream() + .filter(Region::isVisited) + .count(); - if (memberRegions.isEmpty()) { - return new RegionResponse(regions, member.getSelectedColor()); - } - - return new RegionResponse( - updateRegionDto(regions, memberRegions), - (int)memberRegions.stream() - .filter(MemberRegion::isVisited) - .count(), - member.getSelectedColor() - ); - } + return new RegionResponse(regions, visitCount); } } diff --git a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java index 515f90e..7ca098f 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java +++ b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java @@ -3,23 +3,16 @@ import java.util.List; import com.dnd.dndtravel.map.service.dto.RegionDto; -import com.dnd.dndtravel.member.domain.SelectedColor; public record RegionResponse( - List regions, // 지역별 opacity 정보, 땅 이름 - int visitCount, // 방문 지도 개수 - int totalCount, // 전체 땅 개수 - SelectedColor selectedColor // 선택된 컬러 + List regions, + int visitCount, + int totalCount ) { private static final int TOTAL_COUNT = 16; // 전체 지역구의 개수, 변경가능성이 낮아 16이라는 상수로 고정 - private static final int DEFAULT_VISIT_COUNT = 0; - public RegionResponse(List regions, int visitCount, SelectedColor selectedColor) { - this(regions, visitCount, TOTAL_COUNT, selectedColor); - } - - public RegionResponse(List regions, SelectedColor selectedColor) { - this(regions, DEFAULT_VISIT_COUNT, TOTAL_COUNT, selectedColor); + public RegionResponse(List regions, int visitCount) { + this(regions, visitCount, TOTAL_COUNT); } } From bc4197623fb079351c96c2ba9314b135bdaad2ed Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:37:14 +0900 Subject: [PATCH 08/19] =?UTF-8?q?=E2=9C=A8feat:=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=EC=97=90=20=EB=B0=A9?= =?UTF-8?q?=EB=AC=B8=ED=9A=9F=EC=88=98=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/controller/MapController.java | 3 +- .../dndtravel/map/domain/MemberRegion.java | 54 +++++++++++++++++++ .../com/dnd/dndtravel/map/domain/Region.java | 16 +++--- .../repository/MemberRegionRepository.java | 11 ++++ .../dnd/dndtravel/map/service/MapService.java | 29 ++++++---- .../service/dto/response/RegionResponse.java | 17 ++++-- 6 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java create mode 100644 src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java diff --git a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java index 76133d3..4b420af 100644 --- a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java +++ b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java @@ -16,6 +16,7 @@ public class MapController { @GetMapping("/maps") public RegionResponse map() { - return mapService.allRegions(); + Long memberId = 1L; + return mapService.allRegions(memberId); } } diff --git a/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java b/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java new file mode 100644 index 0000000..d1af6a2 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/domain/MemberRegion.java @@ -0,0 +1,54 @@ +package com.dnd.dndtravel.map.domain; + +import com.dnd.dndtravel.member.domain.Member; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class MemberRegion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "region_id") + private Region region; + + private int visitCount; // 방문 횟수 + + @Builder + private MemberRegion(Member member, Region region, int visitCount) { + this.member = member; + this.region = region; + this.visitCount = visitCount; + } + + public static MemberRegion of(Member member, Region region, int visitCount) { + return MemberRegion.builder() + .member(member) + .region(region) + .visitCount(visitCount) + .build(); + } + + public boolean isVisited() { + return this.visitCount > 0; + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/domain/Region.java b/src/main/java/com/dnd/dndtravel/map/domain/Region.java index ef0d29c..5af1273 100644 --- a/src/main/java/com/dnd/dndtravel/map/domain/Region.java +++ b/src/main/java/com/dnd/dndtravel/map/domain/Region.java @@ -21,19 +21,15 @@ public class Region { private String name; // 지역 이름 - @Enumerated(EnumType.STRING) - private VisitOpacity visitOpacity; // 방문 횟수(색의 opacity) - - public static Region of(String name, VisitOpacity visitOpacity) { - return new Region(name, visitOpacity); + public static Region of(String name) { + return new Region(name); } - public boolean isVisited() { - return visitOpacity.isNotZero(); + private Region(String name) { + this.name = name; } - private Region(String name, VisitOpacity visitOpacity) { - this.name = name; - this.visitOpacity = visitOpacity; + public boolean isEqualTo(String name) { + return this.name.equals(name); } } diff --git a/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java b/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java new file mode 100644 index 0000000..c09db39 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/repository/MemberRegionRepository.java @@ -0,0 +1,11 @@ +package com.dnd.dndtravel.map.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dnd.dndtravel.map.domain.MemberRegion; + +public interface MemberRegionRepository extends JpaRepository { + List findByMemberId(Long memberId); +} diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 3f70715..6658d31 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -15,20 +15,31 @@ @RequiredArgsConstructor @Service public class MapService { - - private final MapRepository mapRepository; + private final RegionRepository regionRepository; + private final MemberRepository memberRepository; + private final MemberRegionRepository memberRegionRepository; @Transactional(readOnly = true) - public RegionResponse allRegions() { - List all = mapRepository.findAll(); + public RegionResponse allRegions(Long memberId) { + //todo custom ex + Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저")); - List regions = all.stream() + // 유저의 지역 방문기록 전부 가져온다 + List memberRegions = memberRegionRepository.findByMemberId(memberId); + List regions = regionRepository.findAll().stream() .map(RegionDto::from) .toList(); - int visitCount = (int)all.stream() - .filter(Region::isVisited) - .count(); - return new RegionResponse(regions, visitCount); + if (memberRegions.isEmpty()) { + return new RegionResponse(regions, member.getSelectedColor()); + } + + return new RegionResponse( + updateRegionDto(regions, memberRegions), + (int)memberRegions.stream() + .filter(MemberRegion::isVisited) + .count(), + member.getSelectedColor() + ); } } diff --git a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java index 7ca098f..515f90e 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java +++ b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java @@ -3,16 +3,23 @@ import java.util.List; import com.dnd.dndtravel.map.service.dto.RegionDto; +import com.dnd.dndtravel.member.domain.SelectedColor; public record RegionResponse( - List regions, - int visitCount, - int totalCount + List regions, // 지역별 opacity 정보, 땅 이름 + int visitCount, // 방문 지도 개수 + int totalCount, // 전체 땅 개수 + SelectedColor selectedColor // 선택된 컬러 ) { private static final int TOTAL_COUNT = 16; // 전체 지역구의 개수, 변경가능성이 낮아 16이라는 상수로 고정 + private static final int DEFAULT_VISIT_COUNT = 0; - public RegionResponse(List regions, int visitCount) { - this(regions, visitCount, TOTAL_COUNT); + public RegionResponse(List regions, int visitCount, SelectedColor selectedColor) { + this(regions, visitCount, TOTAL_COUNT, selectedColor); + } + + public RegionResponse(List regions, SelectedColor selectedColor) { + this(regions, DEFAULT_VISIT_COUNT, TOTAL_COUNT, selectedColor); } } From 3db2311223c03dba98f3c13fad53041e6b213374 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:40:44 +0900 Subject: [PATCH 09/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20ci=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/dnd/dndtravel/map/service/MapService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 6658d31..b568030 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -6,9 +6,11 @@ import org.springframework.transaction.annotation.Transactional; import com.dnd.dndtravel.map.domain.Region; -import com.dnd.dndtravel.map.repository.MapRepository; +import com.dnd.dndtravel.map.repository.MemberRegionRepository; +import com.dnd.dndtravel.map.repository.RegionRepository; import com.dnd.dndtravel.map.service.dto.RegionDto; import com.dnd.dndtravel.map.service.dto.response.RegionResponse; +import com.dnd.dndtravel.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; From 60dd985917848f10ff61d0717bd4fb0af74dd885 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:48:39 +0900 Subject: [PATCH 10/19] =?UTF-8?q?Revert=20"=F0=9F=94=A7=20chore:=20ci=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3db2311223c03dba98f3c13fad53041e6b213374. --- src/main/java/com/dnd/dndtravel/map/service/MapService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index b568030..6658d31 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -6,11 +6,9 @@ import org.springframework.transaction.annotation.Transactional; import com.dnd.dndtravel.map.domain.Region; -import com.dnd.dndtravel.map.repository.MemberRegionRepository; -import com.dnd.dndtravel.map.repository.RegionRepository; +import com.dnd.dndtravel.map.repository.MapRepository; import com.dnd.dndtravel.map.service.dto.RegionDto; import com.dnd.dndtravel.map.service.dto.response.RegionResponse; -import com.dnd.dndtravel.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; From f6fb7463ddf0d68652e81c7dabbfac255d7dacbf Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:53:12 +0900 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20ci=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dnd/dndtravel/map/service/MapService.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 6658d31..2e66a00 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -5,10 +5,16 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.dnd.dndtravel.map.domain.MemberRegion; import com.dnd.dndtravel.map.domain.Region; -import com.dnd.dndtravel.map.repository.MapRepository; import com.dnd.dndtravel.map.service.dto.RegionDto; import com.dnd.dndtravel.map.service.dto.response.RegionResponse; +import com.dnd.dndtravel.map.repository.MemberRegionRepository; +import com.dnd.dndtravel.map.repository.RegionRepository; +import com.dnd.dndtravel.map.service.dto.RegionDto; +import com.dnd.dndtravel.map.service.dto.response.RegionResponse; +import com.dnd.dndtravel.member.domain.Member; +import com.dnd.dndtravel.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -42,4 +48,18 @@ public RegionResponse allRegions(Long memberId) { member.getSelectedColor() ); } + private List updateRegionDto(List regions, List memberRegions) { + return regions.stream() + .map(regionDto -> { + String regionName = regionDto.name(); + Optional innerMemberRegion = memberRegions.stream() + .filter(memberRegion -> memberRegion.getRegion().isEqualTo(regionName)) + .findFirst(); + + if (innerMemberRegion.isPresent() && innerMemberRegion.get().isVisited()) { + return new RegionDto(regionDto.name(), innerMemberRegion.get().getVisitCount()); + } + return regionDto; + }) + .toList(); } From 7eb6bdbd4a398d7ee6faf849126e8dedfe843533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=81=EC=A7=84?= <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:15:30 +0900 Subject: [PATCH 12/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20Update=20MapServi?= =?UTF-8?q?ce.java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/dnd/dndtravel/map/service/MapService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 2e66a00..e41495b 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -1,6 +1,7 @@ package com.dnd.dndtravel.map.service; import java.util.List; +import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,8 +12,6 @@ import com.dnd.dndtravel.map.service.dto.response.RegionResponse; import com.dnd.dndtravel.map.repository.MemberRegionRepository; import com.dnd.dndtravel.map.repository.RegionRepository; -import com.dnd.dndtravel.map.service.dto.RegionDto; -import com.dnd.dndtravel.map.service.dto.response.RegionResponse; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.repository.MemberRepository; From b0b673a8f858bc411421a0fd3f71945a4a8e7e19 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:21:53 +0900 Subject: [PATCH 13/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20ci=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dnd/dndtravel/map/service/MapService.java | 4 +-- .../dndtravel/map/service/MapServiceTest.java | 27 ++++++++----------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 2e66a00..a3eb9af 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -1,18 +1,16 @@ package com.dnd.dndtravel.map.service; import java.util.List; +import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.dnd.dndtravel.map.domain.MemberRegion; -import com.dnd.dndtravel.map.domain.Region; import com.dnd.dndtravel.map.service.dto.RegionDto; import com.dnd.dndtravel.map.service.dto.response.RegionResponse; import com.dnd.dndtravel.map.repository.MemberRegionRepository; import com.dnd.dndtravel.map.repository.RegionRepository; -import com.dnd.dndtravel.map.service.dto.RegionDto; -import com.dnd.dndtravel.map.service.dto.response.RegionResponse; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.repository.MemberRepository; diff --git a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java index c5dcb3a..888b2cc 100644 --- a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java +++ b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java @@ -13,12 +13,9 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.dnd.dndtravel.map.domain.VisitOpacity; import com.dnd.dndtravel.map.domain.Region; -import com.dnd.dndtravel.map.repository.MapRepository; import com.dnd.dndtravel.map.service.dto.response.RegionResponse; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @@ -28,17 +25,15 @@ class MapServiceTest { @InjectMocks private MapService sut; - @Mock - private MapRepository mapRepository; @ParameterizedTest @MethodSource("provideRegionsForTesting") void 전체_지역정보를_조회한다(List regions, int expectedVisitCount, int expectedRegionCount) { // given - given(mapRepository.findAll()).willReturn(regions); + // given(mapRepository.findAll()).willReturn(regions); // when - RegionResponse actual = sut.allRegions(); + RegionResponse actual = sut.allRegions(1L); // then assertThat(actual.regions().size()).isEqualTo(expectedRegionCount); @@ -52,23 +47,23 @@ private static Stream provideRegionsForTesting() { // 모든 지역이 방문되지 않은 경우 Arguments.of(List.of( - Region.of("서울특별시", VisitOpacity.ZERO), - Region.of("부산", VisitOpacity.ZERO), - Region.of("충청도", VisitOpacity.ZERO) + Region.of("서울특별시"), + Region.of("부산"), + Region.of("충청도") ), 0, 3), // 모든 지역이 방문된 경우 Arguments.of(List.of( - Region.of("서울특별시", VisitOpacity.ONE), - Region.of("부산", VisitOpacity.ONE), - Region.of("충청도", VisitOpacity.ONE) + Region.of("서울특별시"), + Region.of("부산"), + Region.of("충청도") ), 3, 3), // 일부 지역만 방문된 경우 Arguments.of(List.of( - Region.of("서울특별시", VisitOpacity.ONE), - Region.of("부산", VisitOpacity.ZERO), - Region.of("충청도", VisitOpacity.ONE) + Region.of("서울특별시"), + Region.of("부산"), + Region.of("충청도") ), 2, 3) ); } From 5659953d3e460abc19ee0ded806ff44fbb8c631a Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:29:02 +0900 Subject: [PATCH 14/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20ci=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 인텔리제이 auto import때문에 계속 충돌한게 원인 --- src/main/java/com/dnd/dndtravel/map/service/MapService.java | 4 ++-- .../java/com/dnd/dndtravel/map/service/MapServiceTest.java | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index a3eb9af..a3c39e2 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -7,10 +7,10 @@ import org.springframework.transaction.annotation.Transactional; import com.dnd.dndtravel.map.domain.MemberRegion; -import com.dnd.dndtravel.map.service.dto.RegionDto; -import com.dnd.dndtravel.map.service.dto.response.RegionResponse; import com.dnd.dndtravel.map.repository.MemberRegionRepository; import com.dnd.dndtravel.map.repository.RegionRepository; +// import com.dnd.dndtravel.map.service.dto.RegionDto; +// import com.dnd.dndtravel.map.service.dto.response.RegionResponse; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.repository.MemberRepository; diff --git a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java index 888b2cc..bee5362 100644 --- a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java +++ b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java @@ -15,8 +15,9 @@ import org.mockito.InjectMocks; import org.mockito.junit.jupiter.MockitoExtension; -import com.dnd.dndtravel.map.domain.Region; -import com.dnd.dndtravel.map.service.dto.response.RegionResponse; +// import com.dnd.dndtravel.map.domain.Region; +// import com.dnd.dndtravel.map.domain.Region; +// import com.dnd.dndtravel.map.service.dto.response.RegionResponse; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @ExtendWith(MockitoExtension.class) From 97db8d140f5ab1797d12e45f7dc016bebc3f9aaa Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:38:46 +0900 Subject: [PATCH 15/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20ci=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * } 하나 빠트림 --- src/main/java/com/dnd/dndtravel/map/service/MapService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index a3c39e2..cc8f108 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -60,4 +60,5 @@ private List updateRegionDto(List regions, List Date: Fri, 23 Aug 2024 15:42:40 +0900 Subject: [PATCH 16/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20ci=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * RegionDto 변경 누락 --- src/main/java/com/dnd/dndtravel/map/service/dto/RegionDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/dnd/dndtravel/map/service/dto/RegionDto.java b/src/main/java/com/dnd/dndtravel/map/service/dto/RegionDto.java index 5434623..733a479 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/dto/RegionDto.java +++ b/src/main/java/com/dnd/dndtravel/map/service/dto/RegionDto.java @@ -8,6 +8,6 @@ public record RegionDto( ) { public static RegionDto from(Region region) { - return new RegionDto(region.getName(), region.getVisitOpacity().toInt()); + return new RegionDto(region.getName(), 0); } } From d7940990aaba2a11cba1a0c72fa0e5a925f0a92c Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:47:22 +0900 Subject: [PATCH 17/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20ci=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MapRepository 제거 --- src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java index a3987bd..2c69138 100644 --- a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java +++ b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java @@ -2,7 +2,6 @@ import com.dnd.dndtravel.map.domain.Region; import com.dnd.dndtravel.map.domain.VisitOpacity; -import com.dnd.dndtravel.map.repository.MapRepository; import com.dnd.dndtravel.map.service.dto.response.RegionResponse; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; From 483ba11cc9a4bdecee26e808af81e58c59863110 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:52:19 +0900 Subject: [PATCH 18/19] =?UTF-8?q?=F0=9F=94=A7chore:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=97=86=EC=9D=B4=20=EB=B9=8C=EB=93=9C=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 당장은 테스트코드가 중요한것이 아니므로, 나중에 테스트 코드할때 추가해서 배포한다 --- .github/workflows/CI.yml | 2 +- .../com/dnd/dndtravel/map/service/MapServiceTest.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e09f4c3..0c1d621 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -44,7 +44,7 @@ jobs: run: chmod +x ./gradlew #프로젝트 빌드 - name: Build Project - run: ./gradlew build + run: ./gradlew clean build -x test #AWS 자격증명 설정 - name: Setup AWS credential uses: aws-actions/configure-aws-credentials@v1 diff --git a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java index 2c69138..b02fc6d 100644 --- a/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java +++ b/src/test/java/com/dnd/dndtravel/map/service/MapServiceTest.java @@ -16,6 +16,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import com.dnd.dndtravel.map.domain.MemberRegion; +import com.dnd.dndtravel.map.repository.MemberRegionRepository; +import com.dnd.dndtravel.map.repository.RegionRepository; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @ExtendWith(MockitoExtension.class) @@ -24,6 +27,14 @@ class MapServiceTest { @InjectMocks private MapService sut; + @Mock + private MemberRepository memberRepository; + + @Mock + private RegionRepository regionRepository; + + @Mock + private MemberRegionRepository memberRegionRepository; @ParameterizedTest @MethodSource("provideRegionsForTesting") From f6d27cc0108c5dc543c42d0f4f16ec6750b2304e Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:57:41 +0900 Subject: [PATCH 19/19] =?UTF-8?q?=F0=9F=94=A7chore:=20update=20s3=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0c1d621..2bb2e6b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -59,7 +59,7 @@ jobs: --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} \ --ignore-hidden-files \ --s3-location s3://$AWS_S3_BUCKET/cicdtest/$GITHUB_SHA.zip \ - --source + --source . #업로드된 ZIP 파일을 CodeDeploy로 배포 - name: AWS Code Deploy run: |