From 3d44031ea3762216015aa92d10e981410e5e2e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 17:43:21 +0900 Subject: [PATCH 01/48] refactor : Move file's directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth 디렉토리를 도메인에 추가합니다. - common/jwt/dto를 auth 도메인에 포함되도록합니다. --- .../java/com/tasksprints/auction/common/jwt/JwtProvider.java | 2 +- .../{common/jwt => domain/auth}/dto/response/JwtResponse.java | 2 +- .../com/tasksprints/auction/common/jwt/JwtProviderTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/com/tasksprints/auction/{common/jwt => domain/auth}/dto/response/JwtResponse.java (86%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 033e8ead..08c2f728 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -2,7 +2,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; -import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java similarity index 86% rename from src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java rename to src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java index 1d209eca..c53b08fa 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.common.jwt.dto.response; +package com.tasksprints.auction.domain.auth.dto.response; import lombok.*; diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index ea1bafc5..ab76b217 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -1,6 +1,6 @@ package com.tasksprints.auction.common.jwt; -import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; import io.jsonwebtoken.ExpiredJwtException; import java.time.Clock; import java.time.Instant; From 522328ff5bcd6bd11e9a237176343a4335884029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 17:46:36 +0900 Subject: [PATCH 02/48] refactor : Refactor JwtConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이름은 변경한다 - header와 prefix는 클래스에서 지정하도록 한다. --- .../jwt/{JwtProperties.java => JwtConfig.java} | 7 +------ .../auction/common/jwt/JwtProvider.java | 16 ++++++++-------- .../auction/common/jwt/JwtProviderTest.java | 10 +++++----- 3 files changed, 14 insertions(+), 19 deletions(-) rename src/main/java/com/tasksprints/auction/common/jwt/{JwtProperties.java => JwtConfig.java} (75%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java similarity index 75% rename from src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java rename to src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java index e87ce1d1..676468b9 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java @@ -6,12 +6,7 @@ @Component @Getter -public class JwtProperties { - @Value("${jwt.header}") - private String header; - - @Value("${jwt.prefix}") - private String prefix; +public class JwtConfig { @Value("${jwt.expire-ms}") private Long expireMs; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 08c2f728..d645bd37 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -17,7 +17,7 @@ @RequiredArgsConstructor public class JwtProvider { - private final JwtProperties jwtProperties; + private final JwtConfig jwtConfig; private final Clock clock; public JwtResponse generateToken(Long userId, String userRole) { @@ -28,18 +28,18 @@ public String createAccessToken(Long userId, String userRole) { Date now = localDateTimeToDate(LocalDateTime.now(clock)); - return Jwts.builder().setIssuer(jwtProperties.getIssuer()).claim("userId", userId).claim("userRole", userRole) - .setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtProperties.getExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); + return Jwts.builder().setIssuer(jwtConfig.getIssuer()).claim("userId", userId).claim("userRole", userRole) + .setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtConfig.getExpireMs())) + .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); } public String createRefreshToken() { Date now = localDateTimeToDate(LocalDateTime.now(clock)); - return Jwts.builder().setIssuer(jwtProperties.getIssuer()).setIssuedAt(now) - .setExpiration(new Date(now.getTime() + jwtProperties.getRefreshExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); + return Jwts.builder().setIssuer(jwtConfig.getIssuer()).setIssuedAt(now) + .setExpiration(new Date(now.getTime() + jwtConfig.getRefreshExpireMs())) + .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); } public boolean verifyToken(String token) { @@ -52,7 +52,7 @@ public boolean verifyToken(String token) { } public Claims getClaims(String token) { - return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())) + return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())) .parseClaimsJws(token) .getBody(); } diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index ab76b217..f1c6a24b 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -21,7 +21,7 @@ @ExtendWith(MockitoExtension.class) class JwtProviderTest { @Mock - private JwtProperties jwtProperties; + private JwtConfig jwtConfig; @Mock private Clock clock; @InjectMocks @@ -38,16 +38,16 @@ class JwtProviderTest { public void setUp() { when(clock.instant()).thenReturn(Instant.now()); when(clock.getZone()).thenReturn(ZONE_ID); - when(jwtProperties.getIssuer()).thenReturn(ISSUER); - when(jwtProperties.getSecretKey()).thenReturn(SECRET_KEY); + when(jwtConfig.getIssuer()).thenReturn(ISSUER); + when(jwtConfig.getSecretKey()).thenReturn(SECRET_KEY); } private void stubAccessTokenExpiration(Long expireMs) { - when(jwtProperties.getExpireMs()).thenReturn(expireMs); + when(jwtConfig.getExpireMs()).thenReturn(expireMs); } private void stubRefreshTokenExpiration(Long expireMs) { - when(jwtProperties.getRefreshExpireMs()).thenReturn(expireMs); + when(jwtConfig.getRefreshExpireMs()).thenReturn(expireMs); } @Test From 110f022d6ed1a47dbcc819fe5d182fbe31d3b1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 17:52:03 +0900 Subject: [PATCH 03/48] refactor : Rename JwtResponse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwtResponse -> UserTokens 변경한다. - Data 대신 Getter을 대신해서 필요한 기능만 사용하게 한다. - NoArgsConstructor을 삭제해서, 필요한 어노테이션만 있게하도록 한다. --- .../com/tasksprints/auction/common/jwt/JwtProvider.java | 7 +++---- .../dto/response/{JwtResponse.java => UserTokens.java} | 9 ++++----- .../tasksprints/auction/common/jwt/JwtProviderTest.java | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) rename src/main/java/com/tasksprints/auction/domain/auth/dto/response/{JwtResponse.java => UserTokens.java} (62%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index d645bd37..2e2fdff4 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -2,7 +2,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; -import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -16,12 +16,11 @@ @Component @RequiredArgsConstructor public class JwtProvider { - private final JwtConfig jwtConfig; private final Clock clock; - public JwtResponse generateToken(Long userId, String userRole) { - return JwtResponse.of(createAccessToken(userId, userRole), createRefreshToken()); + public UserTokens generateToken(Long userId, String userRole) { + return UserTokens.of(createAccessToken(userId, userRole), createRefreshToken()); } public String createAccessToken(Long userId, String userRole) { diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java similarity index 62% rename from src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java rename to src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java index c53b08fa..ad91a135 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java @@ -2,16 +2,15 @@ import lombok.*; -@Data +@Getter @Builder -@NoArgsConstructor @AllArgsConstructor -public class JwtResponse { +public class UserTokens { private String refreshToken; private String accessToken; - public static JwtResponse of(String accessToken, String refreshToken) { - return JwtResponse.builder() + public static UserTokens of(String accessToken, String refreshToken) { + return UserTokens.builder() .accessToken(accessToken) .refreshToken(refreshToken) .build(); diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index f1c6a24b..69ec9588 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -1,6 +1,6 @@ package com.tasksprints.auction.common.jwt; -import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.ExpiredJwtException; import java.time.Clock; import java.time.Instant; @@ -53,7 +53,7 @@ private void stubRefreshTokenExpiration(Long expireMs) { @Test @DisplayName("token generator 을 통한 access token, refresh token 발급 테스트") void generateToken() { - JwtResponse jwtResponse = jwtProvider.generateToken(1L, "admin"); + UserTokens jwtResponse = jwtProvider.generateToken(1L, "admin"); assertNotNull(jwtResponse.getAccessToken(), "access token 이 발급되어야 합니다."); assertNotNull(jwtResponse.getRefreshToken(), "refresh token 이 발급되어야 합니다."); From 2a5a279fb2d7ae981eedec181f7e0441ead31bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 18:09:34 +0900 Subject: [PATCH 04/48] refactor : Refactor JwtProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰 검증 과정이 겹친다. 따라서 parseToken에서 토큰을 검증하는 기능을 맡게한다. - getSubject는 파싱한 토큰에서 정보를 빼는 메서드의 역할을 부여한다. --- .../auction/common/jwt/JwtConfig.java | 2 +- .../auction/common/jwt/JwtProvider.java | 57 ++++++++------- .../auction/common/jwt/JwtProviderTest.java | 72 ++++++++++--------- 3 files changed, 69 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java index 676468b9..29ef63cd 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java @@ -9,7 +9,7 @@ public class JwtConfig { @Value("${jwt.expire-ms}") - private Long expireMs; + private Long accessExpireMs; @Value("${jwt.expire-ms}") private Long refreshExpireMs; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 2e2fdff4..0bd26023 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -4,6 +4,8 @@ import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.time.Clock; @@ -16,43 +18,46 @@ @Component @RequiredArgsConstructor public class JwtProvider { + private static final String EMPTY_SUBJECT = ""; private final JwtConfig jwtConfig; private final Clock clock; - public UserTokens generateToken(Long userId, String userRole) { - return UserTokens.of(createAccessToken(userId, userRole), createRefreshToken()); + public UserTokens generateToken(String subject) { + return UserTokens.of( + createToken(subject, jwtConfig.getAccessExpireMs()), + createToken(EMPTY_SUBJECT, jwtConfig.getRefreshExpireMs()) + ); } - public String createAccessToken(Long userId, String userRole) { - + private String createToken(String subject, Long expiredMs) { + byte[] secretKey = JwtUtil.encodeSecretKey(jwtConfig.getSecretKey()); Date now = localDateTimeToDate(LocalDateTime.now(clock)); - - return Jwts.builder().setIssuer(jwtConfig.getIssuer()).claim("userId", userId).claim("userRole", userRole) - .setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtConfig.getExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); + Date expirationTime = new Date(now.getTime() + expiredMs); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer(jwtConfig.getIssuer()) + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(expirationTime) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); } - public String createRefreshToken() { - - Date now = localDateTimeToDate(LocalDateTime.now(clock)); - - return Jwts.builder().setIssuer(jwtConfig.getIssuer()).setIssuedAt(now) - .setExpiration(new Date(now.getTime() + jwtConfig.getRefreshExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); + public void validateToken(String token) { + parseToken(token); } - public boolean verifyToken(String token) { - - Date now = localDateTimeToDate(LocalDateTime.now(clock)); - - Claims claims = getClaims(token); - - return !claims.getExpiration().before(now); + public String getSubject(String token) { + return parseToken(token) + .getBody() + .getSubject(); } - public Claims getClaims(String token) { - return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())) - .parseClaimsJws(token) - .getBody(); + private Jws parseToken(String token) { + byte[] secretKey = JwtUtil.encodeSecretKey(jwtConfig.getSecretKey()); + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token); } } diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index 69ec9588..09b9e9d4 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -43,7 +43,7 @@ public void setUp() { } private void stubAccessTokenExpiration(Long expireMs) { - when(jwtConfig.getExpireMs()).thenReturn(expireMs); + when(jwtConfig.getAccessExpireMs()).thenReturn(expireMs); } private void stubRefreshTokenExpiration(Long expireMs) { @@ -51,64 +51,66 @@ private void stubRefreshTokenExpiration(Long expireMs) { } @Test - @DisplayName("token generator 을 통한 access token, refresh token 발급 테스트") + @DisplayName("accessToken과 refreshToken을 발급해야한다.") void generateToken() { - UserTokens jwtResponse = jwtProvider.generateToken(1L, "admin"); + // when + UserTokens userTokens = jwtProvider.generateToken("1L"); - assertNotNull(jwtResponse.getAccessToken(), "access token 이 발급되어야 합니다."); - assertNotNull(jwtResponse.getRefreshToken(), "refresh token 이 발급되어야 합니다."); + // then + assertNotNull(userTokens.getAccessToken(), "access token 이 발급되어야 합니다."); + assertNotNull(userTokens.getRefreshToken(), "refresh token 이 발급되어야 합니다."); } @Test - @DisplayName("access token 발급 테스트") - void createAccessToken() { - stubAccessTokenExpiration(VALID_EXPIRE_MS); - - String token = jwtProvider.createAccessToken(1L, "admin"); - - assertNotNull(token, "access token 이 발급되어야 합니다."); - } - - @Test - @DisplayName("refresh token 발급 테스트") - void createRefreshToken() { - stubRefreshTokenExpiration(REFRESH_EXPIRE_MS); - String token = jwtProvider.createRefreshToken(); - assertNotNull(token, "refresh token 이 발급되어야 합니다."); - } - - @Test - @DisplayName("유효한 토큰 테스트") + @DisplayName("유효기간에는 토큰이 유효해야한다.") void verifyToken_valid() { + // given stubAccessTokenExpiration(VALID_EXPIRE_MS); + stubRefreshTokenExpiration(REFRESH_EXPIRE_MS); - String token = jwtProvider.createAccessToken(1L, "admin"); + // when + UserTokens userTokens = jwtProvider.generateToken("1L"); - Assertions.assertTrue(jwtProvider.verifyToken(token)); + // then + Assertions.assertDoesNotThrow(() -> { + jwtProvider.validateToken(userTokens.getAccessToken()); + }); + Assertions.assertDoesNotThrow(() -> { + jwtProvider.validateToken(userTokens.getRefreshToken()); + }); } @Test - @DisplayName("만료된 토큰 테스트") + @DisplayName("유효기간이 지나면, 토큰 만료 예외를 반환해야한다") void verifyToken_expired() { + // given stubAccessTokenExpiration(EXPIRED_EXPIRE_MS); + stubRefreshTokenExpiration(EXPIRED_EXPIRE_MS); + + // when + UserTokens userTokens = jwtProvider.generateToken("1L"); - String token = jwtProvider.createAccessToken(1L, "admin"); + // then + Assertions.assertThrows(ExpiredJwtException.class, () -> { + jwtProvider.validateToken(userTokens.getRefreshToken()); + }, "리프레시토큰이 즉시 만료되어야 합니다."); Assertions.assertThrows(ExpiredJwtException.class, () -> { - jwtProvider.verifyToken(token); - }, "토큰이 즉시 만료되어야 합니다."); + jwtProvider.validateToken(userTokens.getAccessToken()); + }, "액세스토큰이 즉시 만료되어야 합니다."); } @Test @DisplayName("디코딩 된 페이로드 정확성 테스트") void getClaims() { + // given stubAccessTokenExpiration(VALID_EXPIRE_MS); + UserTokens userTokens = jwtProvider.generateToken("1L"); - String token = jwtProvider.createAccessToken(1L, "admin"); - Long decodedUserId = jwtProvider.getClaims(token).get("userId", Long.class); - String decodedUserRole = jwtProvider.getClaims(token).get("userRole", String.class); + // when + String decodedUserId = jwtProvider.getSubject(userTokens.getAccessToken()); - assertThat(decodedUserId).isEqualTo(1L); - assertThat(decodedUserRole).isEqualTo("admin"); + // then + assertThat(decodedUserId).isEqualTo("1L"); } } From 62b57dec8c396a222386a3d528e057f7c8771297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 18:11:44 +0900 Subject: [PATCH 05/48] refactor : Move JwtConfig's directory path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common/config에 포함되도록 합니다. --- .../tasksprints/auction/common/{jwt => config}/JwtConfig.java | 2 +- .../java/com/tasksprints/auction/common/jwt/JwtProvider.java | 1 + .../com/tasksprints/auction/common/jwt/JwtProviderTest.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) rename src/main/java/com/tasksprints/auction/common/{jwt => config}/JwtConfig.java (90%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java similarity index 90% rename from src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java rename to src/main/java/com/tasksprints/auction/common/config/JwtConfig.java index 29ef63cd..5abef1db 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.common.jwt; +package com.tasksprints.auction.common.config; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 0bd26023..e6ab226e 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -2,6 +2,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; +import com.tasksprints.auction.common.config.JwtConfig; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index 09b9e9d4..afbfa45d 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -1,5 +1,6 @@ package com.tasksprints.auction.common.jwt; +import com.tasksprints.auction.common.config.JwtConfig; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.ExpiredJwtException; import java.time.Clock; From c902cd73f46cde37e5ea64606213ab4ec4b77af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:11:40 +0900 Subject: [PATCH 06/48] feat : Add refresh token model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리프레시 토큰 모델을 생성합니다. - id로 refreshtoken 값을 갖습니다. - 유저의 id를 열로 갖습니다. --- .../domain/auth/model/RefreshToken.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java new file mode 100644 index 00000000..a0395f9d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -0,0 +1,23 @@ +package com.tasksprints.auction.domain.auth.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + @Id + private String id; + + @Column + private Long memberId; +} From fbee8cd05cd1362e4458596286f6e6355581de31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:18:10 +0900 Subject: [PATCH 07/48] feat : Add refresh token repsitory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리프레시 토큰 레파지토리를 생성합니다. - refreshToken 레파지토리에 refreshToken을 저장하고, findById와 existById가 실행되는지 확인합니다. --- .../repository/RefreshTokenRepository.java | 7 +++ .../RefreshTokenRepositoryTest.java | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java b/src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..7259764b --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.repository; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java new file mode 100644 index 00000000..f4f491da --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java @@ -0,0 +1,60 @@ +package com.tasksprints.auction.domain.auth.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.tasksprints.auction.common.config.QueryDslConfig; +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + + +@DataJpaTest +@Import(QueryDslConfig.class) +class RefreshTokenRepositoryTest { + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + private RefreshToken refreshToken; + + @BeforeEach + void setUp() { + refreshToken = RefreshToken.builder() + .id("testId") + .memberId(1L) + .build(); + } + + @Test + @DisplayName("refresh token ID로 refresh token 조회") + void testFindById() { + // given + refreshTokenRepository.save(refreshToken); + + // when + Optional resultRefreshToken = refreshTokenRepository.findById(refreshToken.getId()); + + // then + assertTrue(resultRefreshToken.isPresent()); + assertThat(refreshToken.getId()).isEqualTo(resultRefreshToken.get().getId()); + } + + @Test + @DisplayName("refresh token ID로 refresh token이 존재하는지 확인합니다.") + void testExistById() { + // given + refreshTokenRepository.save(refreshToken); + + // when + boolean resultRefreshToken = refreshTokenRepository.existsById(refreshToken.getId()); + + // then + assertTrue(resultRefreshToken); + } +} From ea7968efd31bc48136227829add7428bb3363a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:19:12 +0900 Subject: [PATCH 08/48] feat : Add Role enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자의 권한을 다양하게 부여하기 위해, role enum을 추가합니다. --- .../com/tasksprints/auction/domain/auth/model/Role.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/model/Role.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Role.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Role.java new file mode 100644 index 00000000..5591e894 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Role.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.model; + +public enum Role { + USER, + ADMIN, + GUEST +} From a810d0affd21971afe62c3ff4047bba86ee87c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:20:43 +0900 Subject: [PATCH 09/48] refactor : Delete userRole class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자의 권한 부여에 대한 것을 auth 도메인에서 관리합니다. --- .../auction/domain/user/model/UserRole.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java diff --git a/src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java b/src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java deleted file mode 100644 index 13d8e420..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tasksprints.auction.domain.user.model; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum UserRole { - ADMIN("admin"), - USER("user"); - - private final String userRole; -} From dc488ddd4ae956df6e1cc4f23fec4709614f77fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:23:39 +0900 Subject: [PATCH 10/48] refactor : Add Accessor domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자가 접속했을 때, 권한을 부여해 줄 모델을 생성합니다. --- .../auction/domain/auth/model/Accessor.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java new file mode 100644 index 00000000..d9ad8150 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java @@ -0,0 +1,34 @@ +package com.tasksprints.auction.domain.auth.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Builder +@Getter +@RequiredArgsConstructor +public class Accessor { + + private static final Long GUEST_USER_ID = 0L; + private final Long userId; + private final Role role; + + public static Accessor guest() { + return Accessor.of(GUEST_USER_ID, Role.GUEST); + } + public static Accessor user(Long userId) { + return Accessor.of(userId, Role.USER); + } + public static Accessor admin(Long userId) { + return Accessor.of(userId, Role.ADMIN); + } + + private static Accessor of(Long userId, Role role) { + return Accessor.builder() + .userId(userId) + .role(role) + .build(); + } +} + + From d90a93fe3aa179813e436b079fa5c776e31eaa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:26:15 +0900 Subject: [PATCH 11/48] refactor : Add Auth annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증인가를 필요로 하는 엔드포인트를 식별하기 위한 어노테이션을 생성합니다. --- .../java/com/tasksprints/auction/common/jwt/Auth.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/Auth.java diff --git a/src/main/java/com/tasksprints/auction/common/jwt/Auth.java b/src/main/java/com/tasksprints/auction/common/jwt/Auth.java new file mode 100644 index 00000000..2061c8ab --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/Auth.java @@ -0,0 +1,11 @@ +package com.tasksprints.auction.common.jwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Auth { +} From a9ee79fa5d6f15d7c58e65141ba627cf378401dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:29:16 +0900 Subject: [PATCH 12/48] refactor : Add AuthException class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증인가시 발생하는 예외를 처리하기 위한 클래스를 생성합니다. --- .../domain/auth/exception/AccessTokenException.java | 7 +++++++ .../auction/domain/auth/exception/AuthException.java | 7 +++++++ .../domain/auth/exception/RefreshTokenException.java | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java b/src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java new file mode 100644 index 00000000..c4acac9f --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.exception; + +public class AccessTokenException extends AuthException { + public AccessTokenException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java b/src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java new file mode 100644 index 00000000..ce842a7e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.exception; + +public class AuthException extends RuntimeException { + public AuthException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java b/src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java new file mode 100644 index 00000000..c0c1d255 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.exception; + +public class RefreshTokenException extends AuthException { + public RefreshTokenException(String message) { + super(message); + } +} From b92dda38ac76bbaffa1eac305221ee8db36ef3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:52:08 +0900 Subject: [PATCH 13/48] refactor : Add TokenExtractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰을 추출하는 인터페이스를 생성합니다. - accessToken을 추출하는 클래스를, 인터페이스를 통해 구현합니다. --- .../common/constant/ApiResponseMessages.java | 3 ++ .../domain/auth/AccessTokenExtractor.java | 18 ++++++++ .../auction/domain/auth/TokenExtractor.java | 5 +++ .../domain/auth/AccessTokenExtractorTest.java | 43 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java diff --git a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java index a8963f05..eb2d9770 100644 --- a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java +++ b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java @@ -30,5 +30,8 @@ public class ApiResponseMessages { public static final String REVIEWS_RETRIEVED = "Reviews successfully retrieved"; public static final String REVIEW_RETRIEVED = "Review successfully retrieved"; + // AUTH + public static final String ACCESS_TOKEN_NOT_FOUND = "Access token Not found"; + // Additional messages can be defined as needed } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java new file mode 100644 index 00000000..3d56d7e4 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java @@ -0,0 +1,18 @@ +package com.tasksprints.auction.domain.auth; + +import static com.tasksprints.auction.common.constant.ApiResponseMessages.ACCESS_TOKEN_NOT_FOUND; + +import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import org.springframework.stereotype.Component; + +@Component +public class AccessTokenExtractor implements TokenExtractor { + private static final String TYPE = "Bearer "; + + public String extractToken(String header) { + if(header != null && header.startsWith(TYPE)) { + return header.substring(TYPE.length()); + } + throw new AccessTokenException(ACCESS_TOKEN_NOT_FOUND); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java new file mode 100644 index 00000000..824a961d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java @@ -0,0 +1,5 @@ +package com.tasksprints.auction.domain.auth; + +public interface TokenExtractor { + String extractToken(String value); +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java new file mode 100644 index 00000000..35b38058 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java @@ -0,0 +1,43 @@ +package com.tasksprints.auction.domain.auth; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AccessTokenExtractorTest { + private TokenExtractor tokenExtractor; + + @BeforeEach + void setUp() { + tokenExtractor = new AccessTokenExtractor(); + } + + @Test + @DisplayName("헤더에서 access token 을 꺼낸다") + void testExtractToken_success() { + // given + String header = "Bearer token"; + + // when + String accessToken = tokenExtractor.extractToken(header); + + // then + assertThat(accessToken).isEqualTo("token"); + } + + @Test + @DisplayName("access token 이 존재하지 않으면 예외를 반환한다") + void testExtractToken_fail() { + // given + String header = "no token"; + + // when, then + Assertions.assertThrows(AccessTokenException.class, () -> { + tokenExtractor.extractToken(header); + }); + } +} From cc4468e08ea57b54c4bfb8315d862935b32aaa66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 20:04:20 +0900 Subject: [PATCH 14/48] refactor : Refactor TokenExtractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰을 추출하는 인터페이스의 매개변수를 HttpServletRequest로 변경합니다. --- .../auction/domain/auth/AccessTokenExtractor.java | 6 +++++- .../auction/domain/auth/TokenExtractor.java | 4 +++- .../domain/auth/AccessTokenExtractorTest.java | 13 +++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java index 3d56d7e4..2b36ea99 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java @@ -3,13 +3,17 @@ import static com.tasksprints.auction.common.constant.ApiResponseMessages.ACCESS_TOKEN_NOT_FOUND; import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; @Component + public class AccessTokenExtractor implements TokenExtractor { private static final String TYPE = "Bearer "; + private static final String HEADER = "Authorization"; - public String extractToken(String header) { + public String extractToken(HttpServletRequest request) { + String header = request.getHeader(HEADER); if(header != null && header.startsWith(TYPE)) { return header.substring(TYPE.length()); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java index 824a961d..6dc30cfb 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java @@ -1,5 +1,7 @@ package com.tasksprints.auction.domain.auth; +import jakarta.servlet.http.HttpServletRequest; + public interface TokenExtractor { - String extractToken(String value); + String extractToken(HttpServletRequest request); } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java index 35b38058..3efe9fc7 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java @@ -1,8 +1,11 @@ package com.tasksprints.auction.domain.auth; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,10 +23,11 @@ void setUp() { @DisplayName("헤더에서 access token 을 꺼낸다") void testExtractToken_success() { // given - String header = "Bearer token"; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("Bearer token"); // when - String accessToken = tokenExtractor.extractToken(header); + String accessToken = tokenExtractor.extractToken(request); // then assertThat(accessToken).isEqualTo("token"); @@ -33,11 +37,12 @@ void testExtractToken_success() { @DisplayName("access token 이 존재하지 않으면 예외를 반환한다") void testExtractToken_fail() { // given - String header = "no token"; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("notoken"); // when, then Assertions.assertThrows(AccessTokenException.class, () -> { - tokenExtractor.extractToken(header); + tokenExtractor.extractToken(request); }); } } From dcc0fe04996aae8cdb0f42efd998855e4a0e0bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 20:19:55 +0900 Subject: [PATCH 15/48] feat : Add RefreshTokenExtractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쿠키에서 토큰을 추출하고 반환합니다. --- .../common/constant/ApiResponseMessages.java | 1 + .../domain/auth/RefreshTokenExtractor.java | 33 ++++++++++ .../auth/RefreshTokenExtractorTest.java | 65 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java diff --git a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java index eb2d9770..9b097fea 100644 --- a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java +++ b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java @@ -32,6 +32,7 @@ public class ApiResponseMessages { // AUTH public static final String ACCESS_TOKEN_NOT_FOUND = "Access token Not found"; + public static final String REFRESH_TOKEN_NOT_FOUND = "Refresh token not found"; // Additional messages can be defined as needed } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java new file mode 100644 index 00000000..4ca8ef98 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java @@ -0,0 +1,33 @@ +package com.tasksprints.auction.domain.auth; + +import static com.tasksprints.auction.common.constant.ApiResponseMessages.REFRESH_TOKEN_NOT_FOUND; + +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RefreshTokenExtractor implements TokenExtractor { + private static final String COOKIE_NAME = "refresh-token"; + + private final RefreshTokenRepository refreshTokenRepository; + @Override + public String extractToken(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + + if (cookies == null) { + throw new RefreshTokenException(REFRESH_TOKEN_NOT_FOUND); + } + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals(COOKIE_NAME)) + .filter(cookie -> refreshTokenRepository.existsById(cookie.getValue())) + .findFirst() + .orElseThrow(() -> new RefreshTokenException(REFRESH_TOKEN_NOT_FOUND)) + .getValue(); + } +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java new file mode 100644 index 00000000..e267f204 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java @@ -0,0 +1,65 @@ +package com.tasksprints.auction.domain.auth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class RefreshTokenExtractorTest { + @Mock + private RefreshTokenRepository refreshTokenRepository; + + @InjectMocks + private RefreshTokenExtractor tokenExtractor; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("쿠키에서 refresh token 을 꺼내서 반환한다.") + void testExtractToken_success() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + new Cookie("refresh-token", "tokenName"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + when(refreshTokenRepository.existsById("tokenName")).thenReturn(true); + + // when + String resultValue = tokenExtractor.extractToken(request); + + // then + assertThat(resultValue).isEqualTo("tokenName"); + } + + @Test + @DisplayName("쿠키에 refresh token이 존재하지 않으면 예외를 반환한다.") + void testExtractToken_fail() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + + // when, then + Assertions.assertThrows(RefreshTokenException.class, ()-> { + tokenExtractor.extractToken(request); + }, "리프레시토큰이 쿠키에 존재해야 합니다."); + } +} From 5ae66f275db75ac1facb87d867225a9e1e38ed4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Sat, 16 Nov 2024 23:42:13 +0900 Subject: [PATCH 16/48] feat : Add AuthenticationResolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth 어노테이션을 가진 파라미터 대상으로 resolver가 작동합니다 - RefreshToken 관련 오류가 발생하지 않는경우 member 권한으로 접근합니다 - 그렇지 않는 경우 guest 권한으로 접근합니다 - 컨트롤러에서 의존성 문제로 발생하는 오류를 목킹을 해결합니다 --- .../auction/api/auth/AuthController.java | 18 +++++++ .../auction/common/config/JwtConfig.java | 10 ++-- .../auction/common/config/WebConfig.java | 7 ++- .../resolver/AuthenticationResolver.java | 53 +++++++++++++++++++ .../domain/auth/AccessTokenExtractor.java | 3 +- .../domain/auth/RefreshTokenExtractor.java | 2 + .../auction/domain/auth/model/Accessor.java | 2 - .../auction/api/AuctionControllerTest.java | 5 +- .../auction/api/BaseControllerTest.java | 10 ++++ .../auction/api/ProductControllerTest.java | 2 +- .../auction/api/UserControllerTest.java | 2 +- .../auction/common/config/TestAuthConfig.java | 25 +++++++++ 12 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/api/auth/AuthController.java create mode 100644 src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java create mode 100644 src/test/java/com/tasksprints/auction/api/BaseControllerTest.java create mode 100644 src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java new file mode 100644 index 00000000..e4a50046 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -0,0 +1,18 @@ +package com.tasksprints.auction.api.auth; + +import com.tasksprints.auction.common.jwt.Auth; +import com.tasksprints.auction.domain.auth.model.Accessor; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class AuthController { + @GetMapping("/access") + public Boolean access(@Auth Accessor accessor) { + return true; + } +} diff --git a/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java index 5abef1db..f272e578 100644 --- a/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java @@ -1,22 +1,24 @@ package com.tasksprints.auction.common.config; import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @Getter +@RequiredArgsConstructor public class JwtConfig { @Value("${jwt.expire-ms}") - private Long accessExpireMs; + private final Long accessExpireMs; @Value("${jwt.expire-ms}") - private Long refreshExpireMs; + private final Long refreshExpireMs; @Value("${jwt.issuer}") - private String issuer; + private final String issuer; @Value("${jwt.secret}") - private String secretKey; + private final String secretKey; } diff --git a/src/main/java/com/tasksprints/auction/common/config/WebConfig.java b/src/main/java/com/tasksprints/auction/common/config/WebConfig.java index 0f7bb22a..b895df7c 100644 --- a/src/main/java/com/tasksprints/auction/common/config/WebConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/WebConfig.java @@ -1,7 +1,7 @@ package com.tasksprints.auction.common.config; +import com.tasksprints.auction.common.resolver.AuthenticationResolver; import com.tasksprints.auction.common.resolver.SearchConditionResolver; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; @@ -12,14 +12,17 @@ @Configuration public class WebConfig implements WebMvcConfigurer { private final SearchConditionResolver searchConditionResolver; + private final AuthenticationResolver authenticationResolver; - public WebConfig(SearchConditionResolver searchConditionResolver) { + public WebConfig(SearchConditionResolver searchConditionResolver, AuthenticationResolver authenticationResolver) { this.searchConditionResolver = searchConditionResolver; + this.authenticationResolver = authenticationResolver; } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(searchConditionResolver); + resolvers.add(authenticationResolver); } @Override diff --git a/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java new file mode 100644 index 00000000..56064687 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java @@ -0,0 +1,53 @@ +package com.tasksprints.auction.common.resolver; + +import com.tasksprints.auction.common.jwt.Auth; +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.TokenExtractor; +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.model.Accessor; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthenticationResolver implements HandlerMethodArgumentResolver { + private final JwtProvider jwtProvider; + private final TokenExtractor refreshTokenExtractor; + private final TokenExtractor accessTokenExtractor; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter + .hasParameterAnnotation(Auth.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + + if (request == null) { + throw new IllegalArgumentException(); + } + try { + String refreshToken = refreshTokenExtractor.extractToken(request); + String accessToken = accessTokenExtractor.extractToken(request); + + jwtProvider.validateToken(accessToken); + jwtProvider.validateToken(refreshToken); + + Long userId = Long.valueOf(jwtProvider.getSubject(refreshToken)); + return Accessor.user(userId); + } catch (RefreshTokenException e) { + return Accessor.guest(); + } + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java index 2b36ea99..cd2e0179 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java @@ -4,10 +4,11 @@ import com.tasksprints.auction.domain.auth.exception.AccessTokenException; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component - +@Qualifier("accessTokenExtractor") public class AccessTokenExtractor implements TokenExtractor { private static final String TYPE = "Bearer "; private static final String HEADER = "Authorization"; diff --git a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java index 4ca8ef98..f512be34 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java @@ -8,9 +8,11 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component +@Qualifier("refreshTokenExtractor") @RequiredArgsConstructor public class RefreshTokenExtractor implements TokenExtractor { private static final String COOKIE_NAME = "refresh-token"; diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java index d9ad8150..03060d48 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java @@ -30,5 +30,3 @@ private static Accessor of(Long userId, Role role) { .build(); } } - - diff --git a/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java b/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java index e21acad2..6d9cb5ee 100644 --- a/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java @@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -40,10 +41,10 @@ @WebMvcTest(AuctionController.class) @MockBean(JpaMetamodelMappingContext.class) -public class AuctionControllerTest { +public class AuctionControllerTest extends BaseControllerTest { @Autowired - private MockMvc mockMvc; + protected MockMvc mockMvc; @MockBean private AuctionService auctionService; diff --git a/src/test/java/com/tasksprints/auction/api/BaseControllerTest.java b/src/test/java/com/tasksprints/auction/api/BaseControllerTest.java new file mode 100644 index 00000000..58a4aba1 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/api/BaseControllerTest.java @@ -0,0 +1,10 @@ +package com.tasksprints.auction.api; + +import com.tasksprints.auction.common.config.TestAuthConfig; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; + +@WebMvcTest +@Import(TestAuthConfig.class) +public abstract class BaseControllerTest { +} diff --git a/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java b/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java index 54212bc5..6d56ebc6 100644 --- a/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java @@ -28,7 +28,7 @@ @WebMvcTest(ProductController.class) @MockBean(JpaMetamodelMappingContext.class) -public class ProductControllerTest { +public class ProductControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java b/src/test/java/com/tasksprints/auction/api/UserControllerTest.java index 091985ab..5cb0c775 100644 --- a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/UserControllerTest.java @@ -30,7 +30,7 @@ @WebMvcTest(UserController.class) @MockBean(JpaMetamodelMappingContext.class) -class UserControllerTest { +class UserControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java new file mode 100644 index 00000000..ef4c6be9 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java @@ -0,0 +1,25 @@ +package com.tasksprints.auction.common.config; + +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.TokenExtractor; +import org.mockito.Mockito; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestAuthConfig { + @Bean + public JwtProvider jwtProvider() { + return Mockito.mock(JwtProvider.class); + } + + @Bean + public TokenExtractor refreshTokenExtractor() { + return Mockito.mock(TokenExtractor.class); + } + + @Bean + public TokenExtractor accessTokenExtractor() { + return Mockito.mock(TokenExtractor.class); + } +} From 1197c69650ece144457797a60580ff3e95576b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 13:26:18 +0900 Subject: [PATCH 17/48] feat : Add find user by email method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 레파지토리에서 user를 email로 찾는다 --- .../user/repository/UserRepository.java | 2 ++ .../domain/user/UserRepositoryTest.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java b/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java index 007cf110..ff883b64 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java +++ b/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java @@ -1,7 +1,9 @@ package com.tasksprints.auction.domain.user.repository; import com.tasksprints.auction.domain.user.model.User; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java index 45f64ea9..960400d1 100644 --- a/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java @@ -95,4 +95,25 @@ void createUser() { log.info("Created User: {}", createdUser); } } + + + @DisplayName("find User By Email") + @Test + void findUserByEmail() { + // given + User createdUser = userRepository.save(user); + String findEmail = "test@example.com"; + + // when + User foundUser = userRepository.findByEmail(findEmail).orElse(null); + + // then + Assertions.assertNotNull(foundUser); + Assertions.assertEquals(createdUser.getId(), foundUser.getId()); + Assertions.assertEquals("testUser", foundUser.getName()); + Assertions.assertEquals("testNick", foundUser.getNickName()); + Assertions.assertEquals("test@example.com", foundUser.getEmail()); + + log.info("Found User: {}", foundUser); + } } From 09874d95c966dc3b391b2ef2cbc6f699e1c9dc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 13:28:35 +0900 Subject: [PATCH 18/48] feat : Add get user detail method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserService와 UserServiceImpl에 메서드를 추가하고 테스트 코드를 작성한다 - 유저를 찾을 경우 유저의 상세 정보를 return 한다 - 유저를 찾지 못하는 경우, 예외를 반환한다 --- .../domain/user/service/UserService.java | 2 + .../domain/user/service/UserServiceImpl.java | 6 +++ .../domain/user/UserServiceImplTest.java | 39 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java index 63755627..73646ad5 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java @@ -16,4 +16,6 @@ public interface UserService { UserDetailResponse updateUser(Long id, UserRequest.Update user); void deleteUser(Long id); + + UserDetailResponse getUserDetailByEmail(String email); } diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java index ad9d0b6b..dc4e8779 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java @@ -59,4 +59,10 @@ public void deleteUser(Long id) { userRepository.save(user); // 상태 업데이트를 저장 } + @Override + public UserDetailResponse getUserDetailByEmail(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException("User not found with email " + email)); + return UserDetailResponse.of(user); + } } diff --git a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java index 95c78348..a4aacf07 100644 --- a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java @@ -171,4 +171,43 @@ void shouldThrowExceptionWhenUserNotFound() { verify(userRepository, never()).delete(any(User.class)); } } + + @Nested + @DisplayName("Get User By Email") + class GetUserByEmailTests { + + @Test + @DisplayName("Get user's detail information by email") + void shouldReturnUserWhenFound() { + // given + String findEmail = "test@example.com"; + when(userRepository.findByEmail(any())).thenReturn(Optional.ofNullable(existingUser)); + + // when + UserDetailResponse user = userService.getUserDetailByEmail(findEmail); + + // then + Assertions.assertNotNull(user); + Assertions.assertEquals(existingUser.getId(), user.getId()); + Assertions.assertEquals(existingUser.getName(), user.getName()); + verify(userRepository, times(1)).findByEmail(findEmail); + } + + @Test + @DisplayName("Should throw an exception if the user cannot be found by email") + void shouldThrowExceptionWhenUserNotFound() { + // given + String findEmail = "different@example.com"; + when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); + + // when + UserNotFoundException exception = Assertions.assertThrows(UserNotFoundException.class, () -> { + userService.getUserDetailByEmail(findEmail); + }); + + // then + Assertions.assertEquals("User not found with email " + findEmail, exception.getMessage()); + verify(userRepository, times(1)).findByEmail(findEmail); + } + } } From 72eaf1fcf0ed0b10ae3c4d34a21554ecd59f0311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 13:38:05 +0900 Subject: [PATCH 19/48] feat : Add login method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 정보가 올바를 경우 토큰을 반환한다 - 로그인 정보가 올바르지 않을 경우 예외를 반환한다 --- .../domain/auth/model/RefreshToken.java | 7 ++ .../domain/auth/service/AuthService.java | 7 ++ .../domain/auth/service/AuthServiceImpl.java | 37 ++++++++ .../auth/service/AuthServiceImplTest.java | 91 +++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java index a0395f9d..0a3e22f9 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -20,4 +20,11 @@ public class RefreshToken { @Column private Long memberId; + + public static RefreshToken create(String id, Long memberId) { + return RefreshToken.builder() + .id(id) + .memberId(memberId) + .build(); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java new file mode 100644 index 00000000..3ecebf36 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; + +public interface AuthService { + UserTokens login(String email, String password); +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java new file mode 100644 index 00000000..a4c88056 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -0,0 +1,37 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; +import com.tasksprints.auction.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class AuthServiceImpl implements AuthService { + private final UserService userService; + private final JwtProvider jwtProvider; + private final RefreshTokenRepository refreshTokenRepository; + + @Transactional + @Override + public UserTokens login(String email, String password) { + UserDetailResponse userDetail = userService.getUserDetailByEmail(email); + + if (!password.equals(userDetail.getPassword())) { + throw new AuthException("password is not correct"); + } + + UserTokens userTokens = jwtProvider.generateToken(userDetail.getId().toString()); + RefreshToken refreshToken = RefreshToken.create(userTokens.getRefreshToken(), userDetail.getId()); + refreshTokenRepository.save(refreshToken); + return userTokens; + } +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java new file mode 100644 index 00000000..72e263a7 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -0,0 +1,91 @@ +package com.tasksprints.auction.domain.auth.service; + + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; +import com.tasksprints.auction.domain.user.model.User; +import com.tasksprints.auction.domain.user.service.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + + +@ExtendWith(MockitoExtension.class) +class AuthServiceImplTest { + + @Mock + UserService userService; + + @Mock + JwtProvider jwtProvider; + @Mock + RefreshTokenRepository refreshTokenRepository; + @InjectMocks + private AuthServiceImpl authService; + + private UserDetailResponse userDetail; + + @BeforeEach + void setUp() { + User existingUser = User.builder() + .id(1L) + .email("user@exapmle.com") + .password("password") + .nickName("testUser") + .name("realName") + .build(); + + userDetail = UserDetailResponse.of(existingUser); + } + + @Nested + @DisplayName("Login success when password is correct") + class testLogin { + @Test + @DisplayName("Return tokens when password is correct") + void ReturnTokensWhenPasswordIsCorrect() { + // given + String email = "user@exapmle.com"; + String password = "password"; + UserTokens expected = UserTokens.of("accessToken", "refreshToken"); + when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); + when(jwtProvider.generateToken(any())).thenReturn(expected); + when(refreshTokenRepository.save(any())).thenReturn(any()); + + // when + UserTokens actual = authService.login(email, password); + + // then + assertEquals(expected.getRefreshToken(), actual.getRefreshToken()); + assertEquals(expected.getAccessToken(), actual.getAccessToken()); + } + + @Test + @DisplayName("should throw exception when password is different") + void shouldReturnExceptionWhenPasswordIsDifferent() { + // given + String email = "user@exapmle.com"; + String password = "differentPassword"; + when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); + + // when + AuthException exception = assertThrows(AuthException.class, () -> { + authService.login(email, password); + }); + + // then + assertEquals("password is not correct", exception.getMessage()); + } + } +} From 2558555785f98f33ffde83a434029dad04c26a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 16:01:04 +0900 Subject: [PATCH 20/48] feat : Add login controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 정보가 올바를 경우 쿠키로 refresh token을 반환한다 - 로그인 정보가 올바를 경우 body에 access token을 반환한다 - 로그인 정보가 올바르지 않을 경우 예외를 반환한다 --- .../auction/api/auth/AuthController.java | 38 +++++- .../common/constant/ApiResponseMessages.java | 1 + .../handler/GlobalExceptionHandler.java | 6 + .../domain/auth/dto/request/LoginRequest.java | 16 +++ .../domain/auth/dto/response/AccessToken.java | 7 ++ .../auction/api/auth/AuthControllerTest.java | 114 ++++++++++++++++++ 6 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java create mode 100644 src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index e4a50046..9a29e866 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -1,18 +1,44 @@ package com.tasksprints.auction.api.auth; -import com.tasksprints.auction.common.jwt.Auth; -import com.tasksprints.auction.domain.auth.model.Accessor; +import static org.springframework.http.HttpHeaders.SET_COOKIE; + +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; +import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.service.AuthService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; -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.RequestMapping; @Controller @RequiredArgsConstructor @RequestMapping("/api/v1/auth") public class AuthController { - @GetMapping("/access") - public Boolean access(@Auth Accessor accessor) { - return true; + + private final AuthService authService; + private static final Integer COOKIE_AGE_SECONDS = 1209600; + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody LoginRequest.Login login) { + UserTokens tokens = authService.login(login.email(), login.password()); + AccessToken accessToken = AccessToken.of(tokens.getAccessToken()); + + ResponseCookie cookie = ResponseCookie.from("refresh-token", tokens.getRefreshToken()) + .maxAge(COOKIE_AGE_SECONDS) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + + return ResponseEntity.ok() + .header(SET_COOKIE, cookie.toString()) + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, accessToken)); } } diff --git a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java index 9b097fea..244215c3 100644 --- a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java +++ b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java @@ -33,6 +33,7 @@ public class ApiResponseMessages { // AUTH public static final String ACCESS_TOKEN_NOT_FOUND = "Access token Not found"; public static final String REFRESH_TOKEN_NOT_FOUND = "Refresh token not found"; + public static final String LOGIN_SUCCESS = "Login Success"; // Additional messages can be defined as needed } diff --git a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java index 4b8a90aa..423cccbf 100644 --- a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import com.tasksprints.auction.domain.auction.exception.AuctionEndedException; import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException; +import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.bid.exception.BidNotFoundException; import com.tasksprints.auction.domain.bid.exception.InvalidBidAmountException; import com.tasksprints.auction.domain.product.exception.ProductNotFoundException; @@ -75,4 +76,9 @@ public ResponseEntity> handleIllegalStateException(IllegalStat public ResponseEntity> handleRuntimeException(RuntimeException ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResult.failure(ex.getMessage())); } + + @ExceptionHandler(AuthException.class) + public ResponseEntity> handleAuthException(AuthException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResult.failure(ApiResponseMessages.USER_NOT_FOUND)); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java new file mode 100644 index 00000000..8f87b031 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java @@ -0,0 +1,16 @@ +package com.tasksprints.auction.domain.auth.dto.request; + +import lombok.Builder; + + +public class LoginRequest { + @Builder + public record Login(String email, String password) { + public static Login of(String email, String password) { + return Login.builder() + .email(email) + .password(password) + .build(); + } + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java new file mode 100644 index 00000000..9f0dc450 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.dto.response; + +public record AccessToken(String accessToken) { + public static AccessToken of(String accessToken) { + return new AccessToken(accessToken); + } +} diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java new file mode 100644 index 00000000..4177844e --- /dev/null +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -0,0 +1,114 @@ +package com.tasksprints.auction.api.auth; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.api.BaseControllerTest; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; +import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.service.AuthService; +import com.tasksprints.auction.domain.user.exception.UserNotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + + +@WebMvcTest(AuthController.class) +@MockBean(JpaMetamodelMappingContext.class) +class AuthControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AuthService authService; + + @Autowired + private ObjectMapper objectMapper; + + + @Nested + @DisplayName("test login") + class LoginTest { + @Test + @DisplayName("Return refresh and access token, when login success") + void login_success() throws Exception { + // given + UserTokens tokens = UserTokens.of("accessTokenValue", "refreshTokenValue"); + LoginRequest.Login request = new Login("example@email.com", "password"); + when(authService.login(any(), any())).thenReturn(tokens); + + // when + ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accessToken").value("accessTokenValue")) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.LOGIN_SUCCESS)) + .andExpect(header().string("set-cookie", containsString("refresh-token=refreshTokenValue"))) + .andExpect(header().string("set-cookie", containsString("Max-Age=1209600"))) + .andExpect(header().string("set-cookie", containsString("Secure"))) + .andExpect(header().string("set-cookie", containsString("HttpOnly"))) + .andExpect(header().string("set-cookie", containsString("SameSite=None"))); + } + + @Test + @DisplayName("Throw Exception, when password is different") + void loginFailWhenPasswordIsDifferent() throws Exception { + // given + LoginRequest.Login request = new Login("example@email.com", "password"); + when(authService.login(any(), any())).thenThrow(AuthException.class); + + // when + ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_NOT_FOUND)); + } + + @Test + @DisplayName("Throw Exception, when email is different") + void loginFailWhenEmailIsDifferent() throws Exception { + // given + LoginRequest.Login request = new Login("example@email.com", "password"); + when(authService.login(any(), any())).thenThrow(UserNotFoundException.class); + + // when + ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_NOT_FOUND)); + } + } +} + From f13cc9eeca504b125a70813b55bf951ca97cf373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Tue, 19 Nov 2024 01:58:31 +0900 Subject: [PATCH 21/48] refactor : Refactor auth domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthServiceImpl에 치중된 책임을 RefreshTokenServiceImpl을 통해 덜어낸다 - cookie 생성 로직을 여러번 사용하기 위해, service 레이어에 작성한다 - UserTokens라는 DTO가 AccessToken DTO를 가질 수 있게한다 --- .../auction/api/auth/AuthController.java | 14 +----- .../auction/common/jwt/JwtProvider.java | 10 ++++- .../domain/auth/dto/response/UserTokens.java | 4 +- .../domain/auth/model/RefreshToken.java | 2 +- .../domain/auth/service/AuthService.java | 3 ++ .../domain/auth/service/AuthServiceImpl.java | 25 ++++++++--- .../auth/service/RefreshTokenService.java | 7 +++ .../auth/service/RefreshTokenServiceImpl.java | 21 +++++++++ .../auction/api/auth/AuthControllerTest.java | 21 ++++++--- .../auction/common/jwt/JwtProviderTest.java | 6 +-- .../auth/service/AuthServiceImplTest.java | 43 ++++++++++++++---- .../service/RefreshTokenServiceImplTest.java | 45 +++++++++++++++++++ 12 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index 9a29e866..a58f6c39 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -20,25 +20,15 @@ @RequiredArgsConstructor @RequestMapping("/api/v1/auth") public class AuthController { - private final AuthService authService; - private static final Integer COOKIE_AGE_SECONDS = 1209600; @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest.Login login) { UserTokens tokens = authService.login(login.email(), login.password()); - AccessToken accessToken = AccessToken.of(tokens.getAccessToken()); - - ResponseCookie cookie = ResponseCookie.from("refresh-token", tokens.getRefreshToken()) - .maxAge(COOKIE_AGE_SECONDS) - .secure(true) - .httpOnly(true) - .sameSite("None") - .path("/") - .build(); + ResponseCookie cookie = authService.getResponseCookie(tokens.getRefreshToken()); return ResponseEntity.ok() .header(SET_COOKIE, cookie.toString()) - .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, accessToken)); + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, tokens.getAccessToken())); } } diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index e6ab226e..aaf55948 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -3,6 +3,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; import com.tasksprints.auction.common.config.JwtConfig; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; @@ -24,9 +25,14 @@ public class JwtProvider { private final Clock clock; public UserTokens generateToken(String subject) { + String accessTokenValue = createToken(subject, jwtConfig.getAccessExpireMs()); + AccessToken accessToken = AccessToken.of(accessTokenValue); + + String refreshToken = createToken(EMPTY_SUBJECT, jwtConfig.getRefreshExpireMs()); + return UserTokens.of( - createToken(subject, jwtConfig.getAccessExpireMs()), - createToken(EMPTY_SUBJECT, jwtConfig.getRefreshExpireMs()) + accessToken, + refreshToken ); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java index ad91a135..985065af 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java @@ -7,9 +7,9 @@ @AllArgsConstructor public class UserTokens { private String refreshToken; - private String accessToken; + private AccessToken accessToken; - public static UserTokens of(String accessToken, String refreshToken) { + public static UserTokens of(AccessToken accessToken, String refreshToken) { return UserTokens.builder() .accessToken(accessToken) .refreshToken(refreshToken) diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java index 0a3e22f9..a68c0911 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -21,7 +21,7 @@ public class RefreshToken { @Column private Long memberId; - public static RefreshToken create(String id, Long memberId) { + public static RefreshToken of(String id, Long memberId) { return RefreshToken.builder() .id(id) .memberId(memberId) diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index 3ecebf36..59673e99 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -1,7 +1,10 @@ package com.tasksprints.auction.domain.auth.service; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import org.springframework.http.ResponseCookie; public interface AuthService { UserTokens login(String email, String password); + + ResponseCookie getResponseCookie(String refreshToken); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index a4c88056..51754048 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -3,22 +3,22 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; -import com.tasksprints.auction.domain.auth.model.RefreshToken; -import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; import com.tasksprints.auction.domain.user.service.UserService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor @Service +@RequiredArgsConstructor @Transactional(readOnly = true) public class AuthServiceImpl implements AuthService { private final UserService userService; private final JwtProvider jwtProvider; - private final RefreshTokenRepository refreshTokenRepository; + private final RefreshTokenService refreshTokenService; + + private static final Integer COOKIE_AGE_SECONDS = 1209600; @Transactional @Override @@ -30,8 +30,19 @@ public UserTokens login(String email, String password) { } UserTokens userTokens = jwtProvider.generateToken(userDetail.getId().toString()); - RefreshToken refreshToken = RefreshToken.create(userTokens.getRefreshToken(), userDetail.getId()); - refreshTokenRepository.save(refreshToken); + refreshTokenService.saveRefreshToken(userTokens.getRefreshToken(), userDetail.getId()); + return userTokens; } + + @Override + public ResponseCookie getResponseCookie(String refreshToken) { + return ResponseCookie.from("refresh-token", refreshToken) + .maxAge(COOKIE_AGE_SECONDS) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java new file mode 100644 index 00000000..ce648085 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; + +public interface RefreshTokenService { + RefreshToken saveRefreshToken(String refreshTokenValue, Long userId); +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java new file mode 100644 index 00000000..d5893f3e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java @@ -0,0 +1,21 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class RefreshTokenServiceImpl implements RefreshTokenService { + private final RefreshTokenRepository refreshTokenRepository; + + @Override + @Transactional + public RefreshToken saveRefreshToken(String refreshTokenValue, Long userId) { + RefreshToken refreshToken = RefreshToken.of(refreshTokenValue, userId); + return refreshTokenRepository.save(refreshToken); + } +} diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 4177844e..09cf76ad 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -10,6 +10,7 @@ import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.auth.service.AuthService; @@ -22,16 +23,14 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @WebMvcTest(AuthController.class) @MockBean(JpaMetamodelMappingContext.class) class AuthControllerTest extends BaseControllerTest { - @Autowired private MockMvc mockMvc; @@ -45,13 +44,25 @@ class AuthControllerTest extends BaseControllerTest { @Nested @DisplayName("test login") class LoginTest { + + private final ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "refreshTokenValue") + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + + @Test @DisplayName("Return refresh and access token, when login success") void login_success() throws Exception { // given - UserTokens tokens = UserTokens.of("accessTokenValue", "refreshTokenValue"); + AccessToken accessToken = AccessToken.of("accessTokenValue"); + UserTokens tokens = UserTokens.of(accessToken, "refreshTokenValue"); LoginRequest.Login request = new Login("example@email.com", "password"); when(authService.login(any(), any())).thenReturn(tokens); + when(authService.getResponseCookie(any())).thenReturn(responseCookie); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") @@ -66,7 +77,7 @@ void login_success() throws Exception { .andExpect(jsonPath("$.data.accessToken").value("accessTokenValue")) .andExpect(jsonPath("$.message").value(ApiResponseMessages.LOGIN_SUCCESS)) .andExpect(header().string("set-cookie", containsString("refresh-token=refreshTokenValue"))) - .andExpect(header().string("set-cookie", containsString("Max-Age=1209600"))) + .andExpect(header().string("set-cookie", containsString("Max-Age=3600"))) .andExpect(header().string("set-cookie", containsString("Secure"))) .andExpect(header().string("set-cookie", containsString("HttpOnly"))) .andExpect(header().string("set-cookie", containsString("SameSite=None"))); diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index afbfa45d..96860980 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -74,7 +74,7 @@ void verifyToken_valid() { // then Assertions.assertDoesNotThrow(() -> { - jwtProvider.validateToken(userTokens.getAccessToken()); + jwtProvider.validateToken(userTokens.getAccessToken().accessToken()); }); Assertions.assertDoesNotThrow(() -> { jwtProvider.validateToken(userTokens.getRefreshToken()); @@ -97,7 +97,7 @@ void verifyToken_expired() { }, "리프레시토큰이 즉시 만료되어야 합니다."); Assertions.assertThrows(ExpiredJwtException.class, () -> { - jwtProvider.validateToken(userTokens.getAccessToken()); + jwtProvider.validateToken(userTokens.getAccessToken().accessToken()); }, "액세스토큰이 즉시 만료되어야 합니다."); } @@ -109,7 +109,7 @@ void getClaims() { UserTokens userTokens = jwtProvider.generateToken("1L"); // when - String decodedUserId = jwtProvider.getSubject(userTokens.getAccessToken()); + String decodedUserId = jwtProvider.getSubject(userTokens.getAccessToken().accessToken()); // then assertThat(decodedUserId).isEqualTo("1L"); diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index 72e263a7..66f47521 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -5,12 +5,14 @@ import static org.mockito.Mockito.*; import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; -import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.service.UserService; +import java.time.Duration; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -19,6 +21,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseCookie; @ExtendWith(MockitoExtension.class) @@ -26,11 +29,10 @@ class AuthServiceImplTest { @Mock UserService userService; - @Mock JwtProvider jwtProvider; @Mock - RefreshTokenRepository refreshTokenRepository; + RefreshTokenService refreshTokenService; @InjectMocks private AuthServiceImpl authService; @@ -50,18 +52,19 @@ void setUp() { } @Nested - @DisplayName("Login success when password is correct") - class testLogin { + @DisplayName("Login success test") + class TestLogin { @Test @DisplayName("Return tokens when password is correct") - void ReturnTokensWhenPasswordIsCorrect() { + void returnTokensWhenPasswordIsCorrect() { // given String email = "user@exapmle.com"; String password = "password"; - UserTokens expected = UserTokens.of("accessToken", "refreshToken"); + AccessToken accessToken = AccessToken.of("accessToken"); + UserTokens expected = UserTokens.of(accessToken, "refreshToken"); when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); when(jwtProvider.generateToken(any())).thenReturn(expected); - when(refreshTokenRepository.save(any())).thenReturn(any()); + when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); // when UserTokens actual = authService.login(email, password); @@ -72,7 +75,7 @@ void ReturnTokensWhenPasswordIsCorrect() { } @Test - @DisplayName("should throw exception when password is different") + @DisplayName("Should throw exception when password is different") void shouldReturnExceptionWhenPasswordIsDifferent() { // given String email = "user@exapmle.com"; @@ -88,4 +91,26 @@ void shouldReturnExceptionWhenPasswordIsDifferent() { assertEquals("password is not correct", exception.getMessage()); } } + + @Nested + @DisplayName("Get response cookie test") + class TestResponseCookie { + + @Test + @DisplayName("Return ResponseCookie, when creating the cookie successfully") + void returnResponseCookie_success() { + // given + String refreshToken = "refreshTokenValue"; + + // when + ResponseCookie responseCookie = authService.getResponseCookie(refreshToken); + + // then + Assertions.assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); + Assertions.assertTrue(responseCookie.isSecure()); + Assertions.assertTrue(responseCookie.isHttpOnly()); + Assertions.assertEquals("None", responseCookie.getSameSite()); + Assertions.assertEquals("/", responseCookie.getPath()); + } + } } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java new file mode 100644 index 00000000..5aa3abb0 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java @@ -0,0 +1,45 @@ +package com.tasksprints.auction.domain.auth.service; + +import static org.mockito.Mockito.*; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class RefreshTokenServiceImplTest { + + @Mock + RefreshTokenRepository refreshTokenRepository; + + @InjectMocks + RefreshTokenServiceImpl refreshTokenService; + + @Nested + @DisplayName("Save RefreshToken test") + class saveRefreshTokenTest { + @Test + @DisplayName("should return RefreshToken, when the test is success") + void testSaveRefreshToken_success() { + // given + String refreshTokenValue = "refreshToken"; + Long userId = 1L; + RefreshToken expectedRefreshToken = RefreshToken.of(refreshTokenValue, userId); + when(refreshTokenRepository.save(any())).thenReturn(expectedRefreshToken); + + // when + RefreshToken actualRefreshToken = refreshTokenService.saveRefreshToken(refreshTokenValue, userId); + + // then + Assertions.assertEquals(expectedRefreshToken.getMemberId(), actualRefreshToken.getMemberId()); + Assertions.assertEquals(expectedRefreshToken.getId(), actualRefreshToken.getId()); + } + } +} From b6ee0b33c51a992e1dd36bcb3bb68830edea8955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Tue, 19 Nov 2024 02:52:25 +0900 Subject: [PATCH 22/48] refactor : Refactor auth service layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthServiceImpl의 login 메서드 하나에 존재했던 책임들을 여러 메서드로 분리한다 --- .../auction/api/auth/AuthController.java | 3 +- .../domain/auth/service/AuthService.java | 4 +- .../domain/auth/service/AuthServiceImpl.java | 15 +++-- .../auction/api/auth/AuthControllerTest.java | 10 ++-- .../auth/service/AuthServiceImplTest.java | 60 ++++++++++++------- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index a58f6c39..016e471b 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -24,7 +24,8 @@ public class AuthController { @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest.Login login) { - UserTokens tokens = authService.login(login.email(), login.password()); + Long userId = authService.validateLogin(login.email(), login.password()); + UserTokens tokens = authService.issueTokens(userId); ResponseCookie cookie = authService.getResponseCookie(tokens.getRefreshToken()); return ResponseEntity.ok() diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index 59673e99..edc08d46 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -4,7 +4,9 @@ import org.springframework.http.ResponseCookie; public interface AuthService { - UserTokens login(String email, String password); + Long validateLogin(String email, String password); ResponseCookie getResponseCookie(String refreshToken); + + UserTokens issueTokens(Long userId); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index 51754048..8dd37d19 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -20,19 +20,22 @@ public class AuthServiceImpl implements AuthService { private static final Integer COOKIE_AGE_SECONDS = 1209600; - @Transactional @Override - public UserTokens login(String email, String password) { + public Long validateLogin(String email, String password) { UserDetailResponse userDetail = userService.getUserDetailByEmail(email); if (!password.equals(userDetail.getPassword())) { throw new AuthException("password is not correct"); } + return userDetail.getId(); + } - UserTokens userTokens = jwtProvider.generateToken(userDetail.getId().toString()); - refreshTokenService.saveRefreshToken(userTokens.getRefreshToken(), userDetail.getId()); - - return userTokens; + @Transactional + @Override + public UserTokens issueTokens(Long userId) { + UserTokens tokens = jwtProvider.generateToken(userId.toString()); + refreshTokenService.saveRefreshToken(tokens.getRefreshToken(), userId); + return tokens; } @Override diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 09cf76ad..0ae55fd5 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -42,9 +42,8 @@ class AuthControllerTest extends BaseControllerTest { @Nested - @DisplayName("test login") + @DisplayName("Test login") class LoginTest { - private final ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "refreshTokenValue") .maxAge(3600) .secure(true) @@ -61,7 +60,8 @@ void login_success() throws Exception { AccessToken accessToken = AccessToken.of("accessTokenValue"); UserTokens tokens = UserTokens.of(accessToken, "refreshTokenValue"); LoginRequest.Login request = new Login("example@email.com", "password"); - when(authService.login(any(), any())).thenReturn(tokens); + when(authService.validateLogin(any(), any())).thenReturn(1L); + when(authService.issueTokens(any())).thenReturn(tokens); when(authService.getResponseCookie(any())).thenReturn(responseCookie); // when @@ -88,7 +88,7 @@ void login_success() throws Exception { void loginFailWhenPasswordIsDifferent() throws Exception { // given LoginRequest.Login request = new Login("example@email.com", "password"); - when(authService.login(any(), any())).thenThrow(AuthException.class); + when(authService.validateLogin(any(), any())).thenThrow(AuthException.class); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") @@ -107,7 +107,7 @@ void loginFailWhenPasswordIsDifferent() throws Exception { void loginFailWhenEmailIsDifferent() throws Exception { // given LoginRequest.Login request = new Login("example@email.com", "password"); - when(authService.login(any(), any())).thenThrow(UserNotFoundException.class); + when(authService.validateLogin(any(), any())).thenThrow(UserNotFoundException.class); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index 66f47521..9e9f514b 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -9,9 +9,9 @@ import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; +import com.tasksprints.auction.domain.user.exception.UserNotFoundException; import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.service.UserService; -import java.time.Duration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -38,8 +38,11 @@ class AuthServiceImplTest { private UserDetailResponse userDetail; + private String loginEmail; + @BeforeEach void setUp() { + loginEmail = "user@exapmle.com"; User existingUser = User.builder() .id(1L) .email("user@exapmle.com") @@ -52,39 +55,32 @@ void setUp() { } @Nested - @DisplayName("Login success test") + @DisplayName("Validate login information test") class TestLogin { @Test - @DisplayName("Return tokens when password is correct") - void returnTokensWhenPasswordIsCorrect() { + @DisplayName("Return userDetails, when password same") + void validateLogin_success() { // given - String email = "user@exapmle.com"; String password = "password"; - AccessToken accessToken = AccessToken.of("accessToken"); - UserTokens expected = UserTokens.of(accessToken, "refreshToken"); when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); - when(jwtProvider.generateToken(any())).thenReturn(expected); - when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); // when - UserTokens actual = authService.login(email, password); + Long actualUserId = authService.validateLogin(loginEmail, password); // then - assertEquals(expected.getRefreshToken(), actual.getRefreshToken()); - assertEquals(expected.getAccessToken(), actual.getAccessToken()); + assertEquals(1L, actualUserId); } @Test @DisplayName("Should throw exception when password is different") - void shouldReturnExceptionWhenPasswordIsDifferent() { + void validateLoginDifferentPassword() { // given - String email = "user@exapmle.com"; String password = "differentPassword"; when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); // when AuthException exception = assertThrows(AuthException.class, () -> { - authService.login(email, password); + authService.validateLogin(loginEmail, password); }); // then @@ -92,6 +88,30 @@ void shouldReturnExceptionWhenPasswordIsDifferent() { } } + @Nested + @DisplayName("Issue tokens test") + class TestIssueTokens { + + @Test + @DisplayName("Return tokens, when issue tokens successfully") + void returnTokens_success() { + // given + Long userId = 1L; + AccessToken accessToken = AccessToken.of("accessTokenValue"); + String refreshToken = "refreshTokenValue"; + UserTokens generated = UserTokens.of(accessToken, refreshToken); + when(jwtProvider.generateToken(any())).thenReturn(generated); + when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); + + // when + UserTokens issued = authService.issueTokens(userId); + + // then + assertEquals(generated.getAccessToken().accessToken(), issued.getAccessToken().accessToken()); + assertEquals(refreshToken, issued.getRefreshToken()); + } + } + @Nested @DisplayName("Get response cookie test") class TestResponseCookie { @@ -106,11 +126,11 @@ void returnResponseCookie_success() { ResponseCookie responseCookie = authService.getResponseCookie(refreshToken); // then - Assertions.assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); - Assertions.assertTrue(responseCookie.isSecure()); - Assertions.assertTrue(responseCookie.isHttpOnly()); - Assertions.assertEquals("None", responseCookie.getSameSite()); - Assertions.assertEquals("/", responseCookie.getPath()); + assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); + assertTrue(responseCookie.isSecure()); + assertTrue(responseCookie.isHttpOnly()); + assertEquals("None", responseCookie.getSameSite()); + assertEquals("/", responseCookie.getPath()); } } } From 9d1bfaa94157fb1a351d158799ac265714593c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Wed, 20 Nov 2024 02:14:53 +0900 Subject: [PATCH 23/48] refactor : Refactor refresh token from cookie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refreshToken을 쿠키에서 추출하고, 쿠키 설정까지의 과정을 하나의 클래스로 묶는다 - 그로인해 변경되는 코드를 변경한다 - 후에, 토큰들을 한번에 발급하고 반환하는 과정이 중복 될 것이기 때문에, 그 부분을 issueResponseTokens 메서드로 통합하여 사용성을 높인다 --- .../auction/api/auth/AuthController.java | 10 +- .../resolver/AuthenticationResolver.java | 5 +- .../auth/dto/response/ResponseTokens.java | 14 +++ .../domain/auth/service/AuthService.java | 7 +- .../domain/auth/service/AuthServiceImpl.java | 18 +--- .../RefreshTokenCookieManager.java} | 22 +++-- .../auth/service/RefreshTokenService.java | 3 + .../auth/service/RefreshTokenServiceImpl.java | 7 ++ .../auction/api/auth/AuthControllerTest.java | 5 +- .../auction/common/config/TestAuthConfig.java | 5 +- .../auth/RefreshTokenExtractorTest.java | 65 ------------- .../auth/service/AuthServiceImplTest.java | 63 ++++++------ .../RefreshTokenCookieManagerTest.java | 97 +++++++++++++++++++ 13 files changed, 188 insertions(+), 133 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java rename src/main/java/com/tasksprints/auction/domain/auth/{RefreshTokenExtractor.java => service/RefreshTokenCookieManager.java} (65%) delete mode 100644 src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index 016e471b..1446913e 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -6,10 +6,9 @@ import com.tasksprints.auction.common.response.ApiResult; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.response.AccessToken; -import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.service.AuthService; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; @@ -25,11 +24,10 @@ public class AuthController { @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest.Login login) { Long userId = authService.validateLogin(login.email(), login.password()); - UserTokens tokens = authService.issueTokens(userId); - ResponseCookie cookie = authService.getResponseCookie(tokens.getRefreshToken()); + ResponseTokens responseTokens = authService.issueResponseTokens(userId); return ResponseEntity.ok() - .header(SET_COOKIE, cookie.toString()) - .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, tokens.getAccessToken())); + .header(SET_COOKIE, responseTokens.refreshToken().toString()) + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken())); } } diff --git a/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java index 56064687..6715a407 100644 --- a/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java +++ b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java @@ -5,6 +5,7 @@ import com.tasksprints.auction.domain.auth.TokenExtractor; import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; import com.tasksprints.auction.domain.auth.model.Accessor; +import com.tasksprints.auction.domain.auth.service.RefreshTokenCookieManager; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; @@ -18,7 +19,7 @@ @RequiredArgsConstructor public class AuthenticationResolver implements HandlerMethodArgumentResolver { private final JwtProvider jwtProvider; - private final TokenExtractor refreshTokenExtractor; + private final RefreshTokenCookieManager refreshTokenCookieManager; private final TokenExtractor accessTokenExtractor; @Override @@ -38,7 +39,7 @@ public Object resolveArgument(MethodParameter parameter, throw new IllegalArgumentException(); } try { - String refreshToken = refreshTokenExtractor.extractToken(request); + String refreshToken = refreshTokenCookieManager.extractRefreshToken(request); String accessToken = accessTokenExtractor.extractToken(request); jwtProvider.validateToken(accessToken); diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java new file mode 100644 index 00000000..919123b1 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java @@ -0,0 +1,14 @@ +package com.tasksprints.auction.domain.auth.dto.response; + +import lombok.Builder; +import org.springframework.http.ResponseCookie; + +@Builder +public record ResponseTokens(AccessToken accessToken, ResponseCookie refreshToken) { + public static ResponseTokens of(AccessToken accessToken, ResponseCookie refreshToken) { + return ResponseTokens.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index edc08d46..cd371509 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -1,12 +1,9 @@ package com.tasksprints.auction.domain.auth.service; -import com.tasksprints.auction.domain.auth.dto.response.UserTokens; -import org.springframework.http.ResponseCookie; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; public interface AuthService { Long validateLogin(String email, String password); - ResponseCookie getResponseCookie(String refreshToken); - - UserTokens issueTokens(Long userId); + ResponseTokens issueResponseTokens(Long userId); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index 8dd37d19..0204334b 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -1,6 +1,7 @@ package com.tasksprints.auction.domain.auth.service; import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; @@ -18,8 +19,6 @@ public class AuthServiceImpl implements AuthService { private final JwtProvider jwtProvider; private final RefreshTokenService refreshTokenService; - private static final Integer COOKIE_AGE_SECONDS = 1209600; - @Override public Long validateLogin(String email, String password) { UserDetailResponse userDetail = userService.getUserDetailByEmail(email); @@ -32,20 +31,11 @@ public Long validateLogin(String email, String password) { @Transactional @Override - public UserTokens issueTokens(Long userId) { + public ResponseTokens issueResponseTokens(Long userId) { UserTokens tokens = jwtProvider.generateToken(userId.toString()); refreshTokenService.saveRefreshToken(tokens.getRefreshToken(), userId); - return tokens; - } + ResponseCookie refreshToken = refreshTokenService.getResponseRefreshToken(tokens.getRefreshToken()); - @Override - public ResponseCookie getResponseCookie(String refreshToken) { - return ResponseCookie.from("refresh-token", refreshToken) - .maxAge(COOKIE_AGE_SECONDS) - .secure(true) - .httpOnly(true) - .sameSite("None") - .path("/") - .build(); + return ResponseTokens.of(tokens.getAccessToken(), refreshToken); } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManager.java similarity index 65% rename from src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java rename to src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManager.java index f512be34..d84f41f9 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManager.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.auth; +package com.tasksprints.auction.domain.auth.service; import static com.tasksprints.auction.common.constant.ApiResponseMessages.REFRESH_TOKEN_NOT_FOUND; @@ -8,18 +8,18 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @Component -@Qualifier("refreshTokenExtractor") @RequiredArgsConstructor -public class RefreshTokenExtractor implements TokenExtractor { +public class RefreshTokenCookieManager { + private static final Integer COOKIE_AGE_SECONDS = 1209600; private static final String COOKIE_NAME = "refresh-token"; private final RefreshTokenRepository refreshTokenRepository; - @Override - public String extractToken(HttpServletRequest request) { + + public String extractRefreshToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { @@ -32,4 +32,14 @@ public String extractToken(HttpServletRequest request) { .orElseThrow(() -> new RefreshTokenException(REFRESH_TOKEN_NOT_FOUND)) .getValue(); } + + public ResponseCookie createResponseCookie(String refreshToken) { + return ResponseCookie.from(COOKIE_NAME, refreshToken) + .maxAge(COOKIE_AGE_SECONDS) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java index ce648085..e9d1ebc0 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java @@ -1,7 +1,10 @@ package com.tasksprints.auction.domain.auth.service; import com.tasksprints.auction.domain.auth.model.RefreshToken; +import org.springframework.http.ResponseCookie; public interface RefreshTokenService { RefreshToken saveRefreshToken(String refreshTokenValue, Long userId); + + ResponseCookie getResponseRefreshToken(String refresh); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java index d5893f3e..e5657a99 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java @@ -3,6 +3,7 @@ import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,6 +12,7 @@ @Transactional(readOnly = true) public class RefreshTokenServiceImpl implements RefreshTokenService { private final RefreshTokenRepository refreshTokenRepository; + private final RefreshTokenCookieManager cookieManager; @Override @Transactional @@ -18,4 +20,9 @@ public RefreshToken saveRefreshToken(String refreshTokenValue, Long userId) { RefreshToken refreshToken = RefreshToken.of(refreshTokenValue, userId); return refreshTokenRepository.save(refreshToken); } + + @Override + public ResponseCookie getResponseRefreshToken(String refreshToken) { + return cookieManager.createResponseCookie(refreshToken); + } } diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 0ae55fd5..7c910141 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -11,6 +11,7 @@ import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; import com.tasksprints.auction.domain.auth.dto.response.AccessToken; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.auth.service.AuthService; @@ -60,9 +61,9 @@ void login_success() throws Exception { AccessToken accessToken = AccessToken.of("accessTokenValue"); UserTokens tokens = UserTokens.of(accessToken, "refreshTokenValue"); LoginRequest.Login request = new Login("example@email.com", "password"); + ResponseTokens responseTokens = ResponseTokens.of(accessToken, responseCookie); when(authService.validateLogin(any(), any())).thenReturn(1L); - when(authService.issueTokens(any())).thenReturn(tokens); - when(authService.getResponseCookie(any())).thenReturn(responseCookie); + when(authService.issueResponseTokens(any())).thenReturn(responseTokens); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") diff --git a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java index ef4c6be9..b2741959 100644 --- a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java +++ b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java @@ -2,6 +2,7 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.TokenExtractor; +import com.tasksprints.auction.domain.auth.service.RefreshTokenCookieManager; import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -14,8 +15,8 @@ public JwtProvider jwtProvider() { } @Bean - public TokenExtractor refreshTokenExtractor() { - return Mockito.mock(TokenExtractor.class); + public RefreshTokenCookieManager refreshTokenCookieManager() { + return Mockito.mock(RefreshTokenCookieManager.class); } @Bean diff --git a/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java deleted file mode 100644 index e267f204..00000000 --- a/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.tasksprints.auction.domain.auth; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; -import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -class RefreshTokenExtractorTest { - @Mock - private RefreshTokenRepository refreshTokenRepository; - - @InjectMocks - private RefreshTokenExtractor tokenExtractor; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - @DisplayName("쿠키에서 refresh token 을 꺼내서 반환한다.") - void testExtractToken_success() { - // given - Cookie[] cookies = { - new Cookie("nothing", "token"), - new Cookie("refresh-token", "tokenName"), - }; - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getCookies()).thenReturn(cookies); - when(refreshTokenRepository.existsById("tokenName")).thenReturn(true); - - // when - String resultValue = tokenExtractor.extractToken(request); - - // then - assertThat(resultValue).isEqualTo("tokenName"); - } - - @Test - @DisplayName("쿠키에 refresh token이 존재하지 않으면 예외를 반환한다.") - void testExtractToken_fail() { - // given - Cookie[] cookies = { - new Cookie("nothing", "token"), - }; - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getCookies()).thenReturn(cookies); - - // when, then - Assertions.assertThrows(RefreshTokenException.class, ()-> { - tokenExtractor.extractToken(request); - }, "리프레시토큰이 쿠키에 존재해야 합니다."); - } -} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index 9e9f514b..b06ec2f2 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -6,10 +6,11 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.dto.response.AccessToken; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.service.UserService; import org.junit.jupiter.api.Assertions; @@ -89,48 +90,48 @@ void validateLoginDifferentPassword() { } @Nested - @DisplayName("Issue tokens test") + @DisplayName("Issue response tokens test") class TestIssueTokens { - @Test - @DisplayName("Return tokens, when issue tokens successfully") - void returnTokens_success() { - // given - Long userId = 1L; - AccessToken accessToken = AccessToken.of("accessTokenValue"); - String refreshToken = "refreshTokenValue"; - UserTokens generated = UserTokens.of(accessToken, refreshToken); - when(jwtProvider.generateToken(any())).thenReturn(generated); - when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); - - // when - UserTokens issued = authService.issueTokens(userId); + public static ResponseCookie createResponseCookie(String value) { + return ResponseCookie.from("refresh-token", value) + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } - // then - assertEquals(generated.getAccessToken().accessToken(), issued.getAccessToken().accessToken()); - assertEquals(refreshToken, issued.getRefreshToken()); + public static AccessToken createAccessToken(String value) { + return new AccessToken(value); } - } - @Nested - @DisplayName("Get response cookie test") - class TestResponseCookie { + public static UserTokens createUserTokens(String accessTokenValue, String refreshTokenValue) { + return UserTokens.of(createAccessToken(accessTokenValue), refreshTokenValue); + } @Test - @DisplayName("Return ResponseCookie, when creating the cookie successfully") - void returnResponseCookie_success() { + @DisplayName("Return tokens, when issue tokens successfully") + void returnResponseTokens_success() { // given - String refreshToken = "refreshTokenValue"; + String refreshTokenValue = "refreshTokenValue"; + String accessTokenValue = "accessTokenValue"; + UserTokens generatedTokens = createUserTokens(accessTokenValue, refreshTokenValue); + Long userId = 1L; + RefreshToken refreshToken = new RefreshToken(refreshTokenValue, userId); + + when(jwtProvider.generateToken(any())).thenReturn(generatedTokens); + when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(refreshToken); + when(refreshTokenService.getResponseRefreshToken(any())).thenReturn(createResponseCookie(refreshTokenValue)); // when - ResponseCookie responseCookie = authService.getResponseCookie(refreshToken); + ResponseTokens responseTokens = authService.issueResponseTokens(userId); // then - assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); - assertTrue(responseCookie.isSecure()); - assertTrue(responseCookie.isHttpOnly()); - assertEquals("None", responseCookie.getSameSite()); - assertEquals("/", responseCookie.getPath()); + assertNotNull(responseTokens); + assertNotNull(responseTokens.accessToken()); + assertNotNull(responseTokens.refreshToken()); } } } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java new file mode 100644 index 00000000..20710fe6 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java @@ -0,0 +1,97 @@ +package com.tasksprints.auction.domain.auth.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseCookie; + +class RefreshTokenCookieManagerTest { + @Mock + private RefreshTokenRepository refreshTokenRepository; + + @InjectMocks + private RefreshTokenCookieManager cookieManager; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Nested + @DisplayName("") + class TestExtractToken { + @Test + @DisplayName("Return refresh token, after extracting refresh token") + void extractRefreshToken_success() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + new Cookie("refresh-token", "tokenName"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + when(refreshTokenRepository.existsById("tokenName")).thenReturn(true); + + // when + String resultValue = cookieManager.extractRefreshToken(request); + + // then + assertThat(resultValue).isEqualTo("tokenName"); + } + + @Test + @DisplayName("Should throw exception, when refresh token doesn't exist in cookie") + void extractRefreshToken_fail() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + + // when, then + Assertions.assertThrows(RefreshTokenException.class, () -> { + cookieManager.extractRefreshToken(request); + }, "리프레시토큰이 쿠키에 존재해야 합니다."); + } + + } + + + @Nested + @DisplayName("Get response cookie test") + class TestResponseCookie { + + @Test + @DisplayName("Return ResponseCookie, when creating the cookie successfully") + void returnResponseCookie_success() { + // given + String refreshToken = "refreshTokenValue"; + + // when + ResponseCookie responseCookie = cookieManager.createResponseCookie(refreshToken); + + // then + assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); + assertTrue(responseCookie.isSecure()); + assertTrue(responseCookie.isHttpOnly()); + assertEquals("None", responseCookie.getSameSite()); + assertEquals("/", responseCookie.getPath()); + } + } +} From 6bb43a31e938798d8b02e3575b05876f9a42e06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Wed, 20 Nov 2024 22:26:58 +0900 Subject: [PATCH 24/48] feat : Add reissueResponseTokens api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - access token이 만료될 경우, 클라이언트는, 해당 api로 토큰 재발급 요청을 보낸다 - 토큰이 repository에 존재할 경우, accesstoken과 refreshtoken을 재발급한다 - refreshtoken을 검증하고, DB에서 지우는 로직을 추가로 구현할 예정이다 --- .../auction/api/auth/AuthController.java | 11 +++++ .../domain/auth/model/RefreshToken.java | 6 +-- .../domain/auth/service/AuthService.java | 2 + .../domain/auth/service/AuthServiceImpl.java | 7 +++ .../auth/service/RefreshTokenService.java | 2 + .../auth/service/RefreshTokenServiceImpl.java | 7 +++ .../auction/api/auth/AuthControllerTest.java | 46 +++++++++++++++++++ .../auction/common/config/TestAuthConfig.java | 21 +++------ .../RefreshTokenRepositoryTest.java | 2 +- .../auth/service/AuthServiceImplTest.java | 21 ++++----- .../service/RefreshTokenServiceImplTest.java | 39 ++++++++++++++-- 11 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index 1446913e..b2714d2b 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CookieValue; +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.RequestMapping; @@ -30,4 +32,13 @@ public ResponseEntity> login(@RequestBody LoginRequest.Lo .header(SET_COOKIE, responseTokens.refreshToken().toString()) .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken())); } + + @GetMapping("/reissue") + public ResponseEntity> reissueTokens(@CookieValue("refresh-token") String refreshToken) { + ResponseTokens responseTokens = authService.reissueResponseTokens(refreshToken); + + return ResponseEntity.ok() + .header(SET_COOKIE, responseTokens.refreshToken().toString()) + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken())); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java index a68c0911..edac44b6 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -19,12 +19,12 @@ public class RefreshToken { private String id; @Column - private Long memberId; + private Long userId; - public static RefreshToken of(String id, Long memberId) { + public static RefreshToken of(String id, Long userId) { return RefreshToken.builder() .id(id) - .memberId(memberId) + .userId(userId) .build(); } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index cd371509..30533708 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -6,4 +6,6 @@ public interface AuthService { Long validateLogin(String email, String password); ResponseTokens issueResponseTokens(Long userId); + + ResponseTokens reissueResponseTokens(String refreshToken); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index 0204334b..f4fced71 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -38,4 +38,11 @@ public ResponseTokens issueResponseTokens(Long userId) { return ResponseTokens.of(tokens.getAccessToken(), refreshToken); } + + @Transactional + @Override + public ResponseTokens reissueResponseTokens(String refreshToken) { + Long userId = refreshTokenService.findRefreshTokenById(refreshToken).getUserId(); + return issueResponseTokens(userId); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java index e9d1ebc0..30e8e1c8 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java @@ -7,4 +7,6 @@ public interface RefreshTokenService { RefreshToken saveRefreshToken(String refreshTokenValue, Long userId); ResponseCookie getResponseRefreshToken(String refresh); + + RefreshToken findRefreshTokenById(String refreshToken); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java index e5657a99..b23fb021 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java @@ -1,5 +1,6 @@ package com.tasksprints.auction.domain.auth.service; +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; @@ -25,4 +26,10 @@ public RefreshToken saveRefreshToken(String refreshTokenValue, Long userId) { public ResponseCookie getResponseRefreshToken(String refreshToken) { return cookieManager.createResponseCookie(refreshToken); } + + @Override + public RefreshToken findRefreshTokenById(String refreshToken) { + return refreshTokenRepository.findById(refreshToken) + .orElseThrow(() -> new RefreshTokenException("Invalid refresh token")); + } } diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 7c910141..c414961a 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -14,8 +14,10 @@ import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.Accessor; import com.tasksprints.auction.domain.auth.service.AuthService; import com.tasksprints.auction.domain.user.exception.UserNotFoundException; +import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -41,6 +43,16 @@ class AuthControllerTest extends BaseControllerTest { @Autowired private ObjectMapper objectMapper; + static ResponseCookie createResponseCookie() { + return ResponseCookie.from("refresh-token", "refreshTokenValue") + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } + @Nested @DisplayName("Test login") @@ -122,5 +134,39 @@ void loginFailWhenEmailIsDifferent() throws Exception { .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_NOT_FOUND)); } } + + @Nested + @DisplayName("Test reissueToken") + class ReissueTest { + // given + Long userId = 1L; + AccessToken accessToken = AccessToken.of("accessTokenValue"); + ResponseTokens responseTokens = ResponseTokens.of(accessToken, createResponseCookie()); + Cookie cookie = new Cookie("refresh-token", "refreshToken"); + @Test + @DisplayName("If refresh token exist in repository, return refresh token") + void testReissue_success() throws Exception { + // given + when(authService.reissueResponseTokens(any())).thenReturn(responseTokens); + + // when + ResultActions resultActions = mockMvc.perform(get("/api/v1/auth/reissue") + .contentType(MediaType.APPLICATION_JSON) + .cookie(cookie) + ); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accessToken").value("accessTokenValue")) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.LOGIN_SUCCESS)) + .andExpect(header().string("set-cookie", containsString("refresh-token=refreshTokenValue"))) + .andExpect(header().string("set-cookie", containsString("Max-Age=3600"))) + .andExpect(header().string("set-cookie", containsString("Secure"))) + .andExpect(header().string("set-cookie", containsString("HttpOnly"))) + .andExpect(header().string("set-cookie", containsString("SameSite=None"))); + } + } } diff --git a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java index b2741959..e1261044 100644 --- a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java +++ b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java @@ -3,24 +3,17 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.TokenExtractor; import com.tasksprints.auction.domain.auth.service.RefreshTokenCookieManager; -import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.test.mock.mockito.MockBean; @TestConfiguration public class TestAuthConfig { - @Bean - public JwtProvider jwtProvider() { - return Mockito.mock(JwtProvider.class); - } + @MockBean + public JwtProvider jwtProvider; - @Bean - public RefreshTokenCookieManager refreshTokenCookieManager() { - return Mockito.mock(RefreshTokenCookieManager.class); - } + @MockBean + public RefreshTokenCookieManager refreshTokenCookieManager; - @Bean - public TokenExtractor accessTokenExtractor() { - return Mockito.mock(TokenExtractor.class); - } + @MockBean + public TokenExtractor accessTokenExtractor; } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java index f4f491da..261eae51 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java @@ -27,7 +27,7 @@ class RefreshTokenRepositoryTest { void setUp() { refreshToken = RefreshToken.builder() .id("testId") - .memberId(1L) + .userId(1L) .build(); } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index b06ec2f2..b7347c23 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -55,6 +55,16 @@ void setUp() { userDetail = UserDetailResponse.of(existingUser); } + public static ResponseCookie createResponseCookie(String value) { + return ResponseCookie.from("refresh-token", value) + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } + @Nested @DisplayName("Validate login information test") class TestLogin { @@ -92,17 +102,6 @@ void validateLoginDifferentPassword() { @Nested @DisplayName("Issue response tokens test") class TestIssueTokens { - - public static ResponseCookie createResponseCookie(String value) { - return ResponseCookie.from("refresh-token", value) - .maxAge(3600) - .secure(true) - .httpOnly(true) - .sameSite("None") - .path("/") - .build(); - } - public static AccessToken createAccessToken(String value) { return new AccessToken(value); } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java index 5aa3abb0..64de817d 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java @@ -1,9 +1,12 @@ package com.tasksprints.auction.domain.auth.service; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -24,7 +27,7 @@ class RefreshTokenServiceImplTest { @Nested @DisplayName("Save RefreshToken test") - class saveRefreshTokenTest { + class TestSaveRefreshToken { @Test @DisplayName("should return RefreshToken, when the test is success") void testSaveRefreshToken_success() { @@ -38,8 +41,38 @@ void testSaveRefreshToken_success() { RefreshToken actualRefreshToken = refreshTokenService.saveRefreshToken(refreshTokenValue, userId); // then - Assertions.assertEquals(expectedRefreshToken.getMemberId(), actualRefreshToken.getMemberId()); - Assertions.assertEquals(expectedRefreshToken.getId(), actualRefreshToken.getId()); + assertEquals(expectedRefreshToken.getUserId(), actualRefreshToken.getUserId()); + assertEquals(expectedRefreshToken.getId(), actualRefreshToken.getId()); + } + } + + @Nested + @DisplayName("Find refresh Token test") + class TestFindRefreshToken { + + void testFindRefreshToken_success() { + // given + String refreshTokenValue = "refreshToken"; + RefreshToken existedRefreshToken = RefreshToken.of("refreshToken", 1L); + when(refreshTokenRepository.findById(any())).thenReturn(Optional.ofNullable(existedRefreshToken)); + + // when + RefreshToken foundRefreshToken = refreshTokenService.findRefreshTokenById(refreshTokenValue); + + // then + assertEquals(1L, foundRefreshToken.getUserId()); + assertEquals(refreshTokenValue, foundRefreshToken.getId()); + } + + void testFindRefreshToken_fail() { + // given + String refreshTokenValue = "refreshToken"; + when(refreshTokenRepository.findById(any())).thenReturn(Optional.empty()); + + // when, then + Assertions.assertThrows(RefreshTokenException.class, () -> { + refreshTokenService.findRefreshTokenById(refreshTokenValue); + }); } } } From 3eafa4619e4921cde2bfcbc8569bb0078e98efc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Sun, 1 Dec 2024 20:14:06 +0900 Subject: [PATCH 25/48] feat : Add Permission check AOP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 유저의 권한을 체크하는 AOP를 추가한다 - 엔드포인트에서, 해당 AOP를 통해 유저가 올바른 접근을 하고 있는지 확인한다 - 올바른 접근일 경우, controller를 실행하고 아닐 경우 예외를 던진다 --- .../auction/common/jwt/UserCheck.java | 25 +++++++++++++++++++ .../auction/common/jwt/UserOnly.java | 11 ++++++++ .../auction/domain/auth/model/Accessor.java | 14 +++++------ 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java diff --git a/src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java b/src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java new file mode 100644 index 00000000..1e8e8c01 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java @@ -0,0 +1,25 @@ +package com.tasksprints.auction.common.jwt; + +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.Accessor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class UserCheck { + @Before("@annotation(UserOnly)") + public void userCheck(JoinPoint joinPoint) { + Object[] args = joinPoint.getArgs(); + + for (Object arg : args) { + if (arg instanceof Accessor accessor) { + if (!accessor.isUser()) { + throw new AuthException("Invalid Access"); + } + } + } + } +} diff --git a/src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java b/src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java new file mode 100644 index 00000000..bc794653 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java @@ -0,0 +1,11 @@ +package com.tasksprints.auction.common.jwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserOnly { +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java index 03060d48..14e00fda 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java @@ -1,28 +1,28 @@ package com.tasksprints.auction.domain.auth.model; import lombok.Builder; -import lombok.Getter; -import lombok.RequiredArgsConstructor; @Builder -@Getter -@RequiredArgsConstructor -public class Accessor { +public record Accessor(Long userId, Role role) { private static final Long GUEST_USER_ID = 0L; - private final Long userId; - private final Role role; public static Accessor guest() { return Accessor.of(GUEST_USER_ID, Role.GUEST); } + public static Accessor user(Long userId) { return Accessor.of(userId, Role.USER); } + public static Accessor admin(Long userId) { return Accessor.of(userId, Role.ADMIN); } + public boolean isUser() { + return Role.USER.equals(role); + } + private static Accessor of(Long userId, Role role) { return Accessor.builder() .userId(userId) From 7fdb53bbef1a7d0c28c02aa066cb3fb399e72fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Sun, 1 Dec 2024 20:27:20 +0900 Subject: [PATCH 26/48] refactor : Delete test directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일관성을 위해 테스트 디렉토리에 존재하는 auth 디렉토리를 삭제합니다 --- .../tasksprints/auction/api/{auth => }/AuthControllerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename src/test/java/com/tasksprints/auction/api/{auth => }/AuthControllerTest.java (98%) diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/AuthControllerTest.java similarity index 98% rename from src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java rename to src/test/java/com/tasksprints/auction/api/AuthControllerTest.java index c414961a..1348acea 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/AuthControllerTest.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.api.auth; +package com.tasksprints.auction.api; import static org.hamcrest.Matchers.containsString; import static org.mockito.Mockito.*; @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.tasksprints.auction.api.BaseControllerTest; +import com.tasksprints.auction.api.auth.AuthController; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; From 1aeab1c1d998a9119532506bc84ca98c56329b81 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:18:02 +0900 Subject: [PATCH 27/48] =?UTF-8?q?refactor=20:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=B4=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/common/entity/QBaseEntity.java | 37 -- .../common/entity/QBaseEntityWithUpdate.java | 42 -- .../domain/auction/model/QAuction.java | 73 ---- .../auction/domain/bid/model/QBid.java | 63 --- .../domain/product/model/QProduct.java | 67 --- .../domain/product/model/QProductImage.java | 39 -- .../auction/domain/review/model/QReview.java | 58 --- .../domain/socket/model/QChatRoom.java | 44 -- .../auction/domain/socket/model/QChatter.java | 39 -- .../auction/domain/user/model/QUser.java | 58 --- .../api/auction/AuctionController.java | 150 ------- .../auction/api/bid/BidController.java | 42 -- .../api/product/ProductController.java | 51 --- .../auction/api/socket/ChatController.java | 26 -- .../api/socket/ChatRoomController.java | 46 -- .../auction/api/user/UserController.java | 89 ---- .../resolver/SearchConditionResolver.java | 60 --- .../auction/dto/request/AuctionRequest.java | 40 -- .../auction/dto/response/AuctionResponse.java | 84 ---- .../dto/response/AuctionStatusResponse.java | 8 - .../AuctionAlreadyClosedException.java | 7 - .../exception/AuctionEndedException.java | 7 - .../exception/AuctionNotFoundException.java | 7 - .../InvalidAuctionTimeException.java | 7 - .../auction/domain/auction/model/Auction.java | 98 ----- .../domain/auction/model/AuctionCategory.java | 18 - .../domain/auction/model/AuctionStatus.java | 17 - .../auction/repository/AuctionRepository.java | 22 - .../support/AuctionCriteriaRepository.java | 18 - .../AuctionCriteriaRepositoryImpl.java | 124 ------ .../auction/service/AuctionService.java | 31 -- .../auction/service/AuctionServiceImpl.java | 125 ------ .../auction/domain/bid/dto/BidRequest.java | 13 - .../auction/domain/bid/dto/BidResponse.java | 31 -- .../bid/exception/BidNotFoundException.java | 7 - .../exception/InvalidBidAmountException.java | 7 - .../auction/domain/bid/model/Bid.java | 69 --- .../domain/bid/repository/BidRepository.java | 20 - .../domain/bid/service/BidService.java | 19 - .../domain/bid/service/BidServiceImpl.java | 98 ----- .../product/dto/request/ProductRequest.java | 26 -- .../product/dto/response/ProductResponse.java | 45 -- .../ProductImageUploadException.java | 13 - .../exception/ProductNotFoundException.java | 7 - .../auction/domain/product/model/Product.java | 78 ---- .../domain/product/model/ProductCategory.java | 66 --- .../domain/product/model/ProductImage.java | 32 -- .../repository/ProductImageRepository.java | 7 - .../product/repository/ProductRepository.java | 14 - .../product/service/ProductService.java | 25 -- .../product/service/ProductServiceImpl.java | 130 ------ .../review/dto/request/ReviewRequest.java | 11 - .../review/dto/response/ReviewResponse.java | 23 - .../auction/domain/review/model/Review.java | 57 --- .../review/repository/ReviewRepository.java | 16 - .../domain/review/service/ReviewService.java | 19 - .../review/service/ReviewServiceImpl.java | 49 --- .../domain/socket/dto/AddChatRoomDto.java | 20 - .../auction/domain/socket/dto/MessageDto.java | 24 -- .../auction/domain/socket/model/ChatRoom.java | 39 -- .../auction/domain/socket/model/Chatter.java | 28 -- .../socket/repository/ChatRoomRepository.java | 9 - .../socket/repository/ChatterRepository.java | 9 - .../domain/socket/service/ChatService.java | 46 -- .../domain/user/dto/request/UserRequest.java | 29 -- .../user/dto/response/UserDetailResponse.java | 28 -- .../user/dto/response/UserResponse.java | 4 - .../dto/response/UserSummaryResponse.java | 21 - .../user/exception/UserNotFoundException.java | 7 - .../auction/domain/user/model/User.java | 82 ---- .../user/repository/UserRepository.java | 7 - .../domain/user/service/UserService.java | 19 - .../domain/user/service/UserServiceImpl.java | 62 --- src/main/resources/templates/chat/enter.html | 105 ----- src/main/resources/templates/chat/room.html | 98 ----- .../auction/api/AuctionControllerTest.java | 215 ---------- .../auction/api/ProductControllerTest.java | 127 ------ .../auction/api/UserControllerTest.java | 203 --------- .../repository/AuctionRepositoryTest.java | 186 -------- .../service/AuctionServiceImplTest.java | 405 ------------------ .../domain/product/ProductRepositoryTest.java | 221 ---------- .../product/ProductServiceImplTest.java | 220 ---------- .../domain/user/UserRepositoryTest.java | 98 ----- .../domain/user/UserServiceImplTest.java | 174 -------- 84 files changed, 4965 deletions(-) delete mode 100644 src/main/generated/com/tasksprints/auction/common/entity/QBaseEntity.java delete mode 100644 src/main/generated/com/tasksprints/auction/common/entity/QBaseEntityWithUpdate.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/auction/model/QAuction.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/bid/model/QBid.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/product/model/QProduct.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/product/model/QProductImage.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/review/model/QReview.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/socket/model/QChatRoom.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/socket/model/QChatter.java delete mode 100644 src/main/generated/com/tasksprints/auction/domain/user/model/QUser.java delete mode 100644 src/main/java/com/tasksprints/auction/api/auction/AuctionController.java delete mode 100644 src/main/java/com/tasksprints/auction/api/bid/BidController.java delete mode 100644 src/main/java/com/tasksprints/auction/api/product/ProductController.java delete mode 100644 src/main/java/com/tasksprints/auction/api/socket/ChatController.java delete mode 100644 src/main/java/com/tasksprints/auction/api/socket/ChatRoomController.java delete mode 100644 src/main/java/com/tasksprints/auction/api/user/UserController.java delete mode 100644 src/main/java/com/tasksprints/auction/common/resolver/SearchConditionResolver.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/dto/request/AuctionRequest.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionStatusResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionAlreadyClosedException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionEndedException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionNotFoundException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/exception/InvalidAuctionTimeException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/model/AuctionCategory.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/model/AuctionStatus.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/repository/AuctionRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/service/AuctionService.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImpl.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/dto/BidRequest.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/dto/BidResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/exception/BidNotFoundException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/exception/InvalidBidAmountException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/model/Bid.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/repository/BidRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/service/BidService.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/bid/service/BidServiceImpl.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/dto/request/ProductRequest.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/dto/response/ProductResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/exception/ProductImageUploadException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/exception/ProductNotFoundException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/model/Product.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/model/ProductCategory.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/model/ProductImage.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/repository/ProductImageRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/repository/ProductRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/service/ProductService.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/product/service/ProductServiceImpl.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/review/dto/request/ReviewRequest.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/review/dto/response/ReviewResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/review/model/Review.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/review/repository/ReviewRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/review/service/ReviewService.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/review/service/ReviewServiceImpl.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/socket/dto/AddChatRoomDto.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/socket/dto/MessageDto.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/socket/model/ChatRoom.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/socket/model/Chatter.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/socket/repository/ChatRoomRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/socket/repository/ChatterRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/socket/service/ChatService.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/dto/request/UserRequest.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/dto/response/UserResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/dto/response/UserSummaryResponse.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/exception/UserNotFoundException.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/model/User.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/service/UserService.java delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java delete mode 100644 src/main/resources/templates/chat/enter.html delete mode 100644 src/main/resources/templates/chat/room.html delete mode 100644 src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java delete mode 100644 src/test/java/com/tasksprints/auction/api/ProductControllerTest.java delete mode 100644 src/test/java/com/tasksprints/auction/api/UserControllerTest.java delete mode 100644 src/test/java/com/tasksprints/auction/domain/auction/repository/AuctionRepositoryTest.java delete mode 100644 src/test/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImplTest.java delete mode 100644 src/test/java/com/tasksprints/auction/domain/product/ProductRepositoryTest.java delete mode 100644 src/test/java/com/tasksprints/auction/domain/product/ProductServiceImplTest.java delete mode 100644 src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java delete mode 100644 src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java diff --git a/src/main/generated/com/tasksprints/auction/common/entity/QBaseEntity.java b/src/main/generated/com/tasksprints/auction/common/entity/QBaseEntity.java deleted file mode 100644 index 71a60b9e..00000000 --- a/src/main/generated/com/tasksprints/auction/common/entity/QBaseEntity.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.tasksprints.auction.common.entity; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QBaseEntity is a Querydsl query type for BaseEntity - */ -@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") -public class QBaseEntity extends EntityPathBase { - - private static final long serialVersionUID = -501798226L; - - public static final QBaseEntity baseEntity = new QBaseEntity("baseEntity"); - - public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); - - public QBaseEntity(String variable) { - super(BaseEntity.class, forVariable(variable)); - } - - public QBaseEntity(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QBaseEntity(PathMetadata metadata) { - super(BaseEntity.class, metadata); - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/common/entity/QBaseEntityWithUpdate.java b/src/main/generated/com/tasksprints/auction/common/entity/QBaseEntityWithUpdate.java deleted file mode 100644 index 75dbafed..00000000 --- a/src/main/generated/com/tasksprints/auction/common/entity/QBaseEntityWithUpdate.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.tasksprints.auction.common.entity; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QBaseEntityWithUpdate is a Querydsl query type for BaseEntityWithUpdate - */ -@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") -public class QBaseEntityWithUpdate extends EntityPathBase { - - private static final long serialVersionUID = 639151741L; - - public static final QBaseEntityWithUpdate baseEntityWithUpdate = new QBaseEntityWithUpdate("baseEntityWithUpdate"); - - public final QBaseEntity _super = new QBaseEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath updatedAt = createDateTime("updatedAt", java.time.LocalDateTime.class); - - public QBaseEntityWithUpdate(String variable) { - super(BaseEntityWithUpdate.class, forVariable(variable)); - } - - public QBaseEntityWithUpdate(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QBaseEntityWithUpdate(PathMetadata metadata) { - super(BaseEntityWithUpdate.class, metadata); - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/auction/model/QAuction.java b/src/main/generated/com/tasksprints/auction/domain/auction/model/QAuction.java deleted file mode 100644 index d8d0c59a..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/auction/model/QAuction.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.tasksprints.auction.domain.auction.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAuction is a Querydsl query type for Auction - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAuction extends EntityPathBase { - - private static final long serialVersionUID = -698942437L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAuction auction = new QAuction("auction"); - - public final com.tasksprints.auction.common.entity.QBaseEntity _super = new com.tasksprints.auction.common.entity.QBaseEntity(this); - - public final EnumPath auctionCategory = createEnum("auctionCategory", AuctionCategory.class); - - public final EnumPath auctionStatus = createEnum("auctionStatus", AuctionStatus.class); - - public final ListPath bids = this.createList("bids", com.tasksprints.auction.domain.bid.model.Bid.class, com.tasksprints.auction.domain.bid.model.QBid.class, PathInits.DIRECT2); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath endTime = createDateTime("endTime", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final com.tasksprints.auction.domain.product.model.QProduct product; - - public final com.tasksprints.auction.domain.user.model.QUser seller; - - public final NumberPath startingBid = createNumber("startingBid", java.math.BigDecimal.class); - - public final DateTimePath startTime = createDateTime("startTime", java.time.LocalDateTime.class); - - public final NumberPath viewCount = createNumber("viewCount", Long.class); - - public QAuction(String variable) { - this(Auction.class, forVariable(variable), INITS); - } - - public QAuction(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAuction(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAuction(PathMetadata metadata, PathInits inits) { - this(Auction.class, metadata, inits); - } - - public QAuction(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.product = inits.isInitialized("product") ? new com.tasksprints.auction.domain.product.model.QProduct(forProperty("product"), inits.get("product")) : null; - this.seller = inits.isInitialized("seller") ? new com.tasksprints.auction.domain.user.model.QUser(forProperty("seller")) : null; - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/bid/model/QBid.java b/src/main/generated/com/tasksprints/auction/domain/bid/model/QBid.java deleted file mode 100644 index ba479754..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/bid/model/QBid.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.tasksprints.auction.domain.bid.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QBid is a Querydsl query type for Bid - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QBid extends EntityPathBase { - - private static final long serialVersionUID = -531916529L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QBid bid = new QBid("bid"); - - public final com.tasksprints.auction.common.entity.QBaseEntity _super = new com.tasksprints.auction.common.entity.QBaseEntity(this); - - public final NumberPath amount = createNumber("amount", java.math.BigDecimal.class); - - public final com.tasksprints.auction.domain.auction.model.QAuction auction; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final com.tasksprints.auction.domain.user.model.QUser user; - - public final StringPath uuid = createString("uuid"); - - public QBid(String variable) { - this(Bid.class, forVariable(variable), INITS); - } - - public QBid(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QBid(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QBid(PathMetadata metadata, PathInits inits) { - this(Bid.class, metadata, inits); - } - - public QBid(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.auction = inits.isInitialized("auction") ? new com.tasksprints.auction.domain.auction.model.QAuction(forProperty("auction"), inits.get("auction")) : null; - this.user = inits.isInitialized("user") ? new com.tasksprints.auction.domain.user.model.QUser(forProperty("user")) : null; - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/product/model/QProduct.java b/src/main/generated/com/tasksprints/auction/domain/product/model/QProduct.java deleted file mode 100644 index d6bc8ea6..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/product/model/QProduct.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.tasksprints.auction.domain.product.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QProduct is a Querydsl query type for Product - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QProduct extends EntityPathBase { - - private static final long serialVersionUID = -1668563853L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QProduct product = new QProduct("product"); - - public final com.tasksprints.auction.common.entity.QBaseEntity _super = new com.tasksprints.auction.common.entity.QBaseEntity(this); - - public final com.tasksprints.auction.domain.auction.model.QAuction auction; - - public final EnumPath category = createEnum("category", ProductCategory.class); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final StringPath description = createString("description"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public final com.tasksprints.auction.domain.user.model.QUser owner; - - public final ListPath productImageList = this.createList("productImageList", ProductImage.class, QProductImage.class, PathInits.DIRECT2); - - public QProduct(String variable) { - this(Product.class, forVariable(variable), INITS); - } - - public QProduct(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QProduct(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QProduct(PathMetadata metadata, PathInits inits) { - this(Product.class, metadata, inits); - } - - public QProduct(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.auction = inits.isInitialized("auction") ? new com.tasksprints.auction.domain.auction.model.QAuction(forProperty("auction"), inits.get("auction")) : null; - this.owner = inits.isInitialized("owner") ? new com.tasksprints.auction.domain.user.model.QUser(forProperty("owner")) : null; - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/product/model/QProductImage.java b/src/main/generated/com/tasksprints/auction/domain/product/model/QProductImage.java deleted file mode 100644 index d39909c6..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/product/model/QProductImage.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.tasksprints.auction.domain.product.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QProductImage is a Querydsl query type for ProductImage - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QProductImage extends EntityPathBase { - - private static final long serialVersionUID = 434031784L; - - public static final QProductImage productImage = new QProductImage("productImage"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath imageUrl = createString("imageUrl"); - - public QProductImage(String variable) { - super(ProductImage.class, forVariable(variable)); - } - - public QProductImage(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QProductImage(PathMetadata metadata) { - super(ProductImage.class, metadata); - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/review/model/QReview.java b/src/main/generated/com/tasksprints/auction/domain/review/model/QReview.java deleted file mode 100644 index 248565be..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/review/model/QReview.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.tasksprints.auction.domain.review.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QReview is a Querydsl query type for Review - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QReview extends EntityPathBase { - - private static final long serialVersionUID = -1171556645L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QReview review = new QReview("review"); - - public final com.tasksprints.auction.domain.auction.model.QAuction auction; - - public final StringPath content = createString("content"); - - public final NumberPath id = createNumber("id", Long.class); - - public final NumberPath rating = createNumber("rating", Integer.class); - - public final com.tasksprints.auction.domain.user.model.QUser writer; - - public QReview(String variable) { - this(Review.class, forVariable(variable), INITS); - } - - public QReview(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QReview(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QReview(PathMetadata metadata, PathInits inits) { - this(Review.class, metadata, inits); - } - - public QReview(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.auction = inits.isInitialized("auction") ? new com.tasksprints.auction.domain.auction.model.QAuction(forProperty("auction"), inits.get("auction")) : null; - this.writer = inits.isInitialized("writer") ? new com.tasksprints.auction.domain.user.model.QUser(forProperty("writer")) : null; - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/socket/model/QChatRoom.java b/src/main/generated/com/tasksprints/auction/domain/socket/model/QChatRoom.java deleted file mode 100644 index c437dad8..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/socket/model/QChatRoom.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.tasksprints.auction.domain.socket.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QChatRoom is a Querydsl query type for ChatRoom - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QChatRoom extends EntityPathBase { - - private static final long serialVersionUID = -65337957L; - - public static final QChatRoom chatRoom = new QChatRoom("chatRoom"); - - public final StringPath chatRoomId = createString("chatRoomId"); - - public final ListPath chatters = this.createList("chatters", Chatter.class, QChatter.class, PathInits.DIRECT2); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public QChatRoom(String variable) { - super(ChatRoom.class, forVariable(variable)); - } - - public QChatRoom(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QChatRoom(PathMetadata metadata) { - super(ChatRoom.class, metadata); - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/socket/model/QChatter.java b/src/main/generated/com/tasksprints/auction/domain/socket/model/QChatter.java deleted file mode 100644 index 0113e8f6..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/socket/model/QChatter.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.tasksprints.auction.domain.socket.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QChatter is a Querydsl query type for Chatter - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QChatter extends EntityPathBase { - - private static final long serialVersionUID = 1660492673L; - - public static final QChatter chatter = new QChatter("chatter"); - - public final StringPath chatterId = createString("chatterId"); - - public final StringPath sessionId = createString("sessionId"); - - public QChatter(String variable) { - super(Chatter.class, forVariable(variable)); - } - - public QChatter(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QChatter(PathMetadata metadata) { - super(Chatter.class, metadata); - } - -} - diff --git a/src/main/generated/com/tasksprints/auction/domain/user/model/QUser.java b/src/main/generated/com/tasksprints/auction/domain/user/model/QUser.java deleted file mode 100644 index 1520cddd..00000000 --- a/src/main/generated/com/tasksprints/auction/domain/user/model/QUser.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.tasksprints.auction.domain.user.model; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QUser is a Querydsl query type for User - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QUser extends EntityPathBase { - - private static final long serialVersionUID = -738054213L; - - public static final QUser user = new QUser("user"); - - public final com.tasksprints.auction.common.entity.QBaseEntityWithUpdate _super = new com.tasksprints.auction.common.entity.QBaseEntityWithUpdate(this); - - public final ListPath auctions = this.createList("auctions", com.tasksprints.auction.domain.auction.model.Auction.class, com.tasksprints.auction.domain.auction.model.QAuction.class, PathInits.DIRECT2); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final StringPath email = createString("email"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public final StringPath nickName = createString("nickName"); - - public final StringPath password = createString("password"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QUser(String variable) { - super(User.class, forVariable(variable)); - } - - public QUser(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QUser(PathMetadata metadata) { - super(User.class, metadata); - } - -} - diff --git a/src/main/java/com/tasksprints/auction/api/auction/AuctionController.java b/src/main/java/com/tasksprints/auction/api/auction/AuctionController.java deleted file mode 100644 index 49881281..00000000 --- a/src/main/java/com/tasksprints/auction/api/auction/AuctionController.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.tasksprints.auction.api.auction; - -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; -import com.tasksprints.auction.domain.auction.service.AuctionService; -import com.tasksprints.auction.domain.bid.dto.BidResponse; -import com.tasksprints.auction.domain.bid.service.BidService; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import com.tasksprints.auction.domain.review.dto.request.ReviewRequest; -import com.tasksprints.auction.domain.review.dto.response.ReviewResponse; -import com.tasksprints.auction.domain.review.service.ReviewService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.math.BigDecimal; -import java.util.List; - -@RestController -@RequestMapping("/api/v1/auction") -@RequiredArgsConstructor -@Tag(name = "Auction", description = "Operations related to auctions") -public class AuctionController { - private final AuctionService auctionService; - private final BidService bidService; - private final ReviewService reviewService; - - @PostMapping - @Operation(summary = "Create an auction", description = "Creates a new auction for a user.") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Auction created successfully")}) - public ResponseEntity> createAuction(@Parameter(description = "ID of the user creating the auction") @RequestParam Long userId, @RequestBody AuctionRequest.Create auctionRequest) { - AuctionResponse createdAuction = auctionService.createAuction(userId, auctionRequest); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_CREATED_SUCCESS, createdAuction)); - } - - @PostMapping("/{auctionId}/close") - @Operation(summary = "Close an auction", description = "Closes the auction by its ID.") - @ApiResponse(responseCode = "200", description = "Auction closed successfully") - public ResponseEntity> closeAuction(@PathVariable Long auctionId) { - auctionService.closeAuction(auctionId); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_CLOSED_SUCCESS)); - } - - @GetMapping("/{auctionId}/status") - @Operation(summary = "Get auction status", description = "Retrieves the status of the auction.") - @ApiResponse(responseCode = "200", description = "Auction status retrieved successfully") - public ResponseEntity> getAuctionStatus(@PathVariable Long auctionId) { - String status = auctionService.getAuctionStatus(auctionId); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_STATUS_RETRIEVED, status)); - } -// 필터로 추가 - - -// @GetMapping("/") -// @Operation(summary = "Get auctions by user", description = "Retrieves all auctions created by a specific user.") -// @ApiResponse(responseCode = "200", description = "User auctions retrieved successfully") -// public ResponseEntity>> getAuctionsByUser(@RequestParam Long userId) { -// List userAuctions = auctionService.getAuctionsByUser(userId); -// return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_BY_USER_RETRIEVED, userAuctions)); -// } - - // @GetMapping -// @Operation(summary = "Get all auctions", description = "Retrieves all auctions.") -// @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") -// public ResponseEntity>> getAllAuctions(@RequestParam(required = false) AuctionRequest.AuctionCategoryParam auctionCategory) { -// List allAuctions; -// if (auctionCategory != null) { -// allAuctions = auctionService.getAuctionsByAuctionCategory(auctionCategory); -// } else { -// allAuctions = auctionService.getAllAuctions(); -// } -// return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED, allAuctions)); -// } - @GetMapping - @Operation(summary = "Get all auctions", description = "Retrieves all auctions.") - @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") - public ResponseEntity>> getAllAuctions(Pageable pageable, AuctionRequest.SearchCondition searchCondition) { - Page auctions = auctionService.getAuctionsByFilter(pageable, searchCondition); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED, auctions)); - } - - @GetMapping("/{auctionId}") - @Operation(summary = "Get auction by ID", description = "Retrieves auction details by its ID.") - @ApiResponse(responseCode = "200", description = "Auction retrieved successfully") - public ResponseEntity> getAuctionById(@PathVariable Long auctionId) { - AuctionResponse auction = auctionService.getAuctionById(auctionId); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_RETRIEVED, auction)); - } - - @Deprecated - @GetMapping("/category/{category}") - @Operation(summary = "Get auctions by ProductCategory", description = "Retrieve all auction by its ProductCategory.") - @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") - public ResponseEntity>> getAuctionByProductCategory(Pageable pageable, @PathVariable String category, AuctionRequest.SearchCondition searchCondition) { - Page auctions = auctionService.getAuctionsByProductCategory(pageable, searchCondition, ProductCategory.fromDisplayName(category)); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_RETRIEVED, auctions)); - } - - // Bid Endpoints - @PostMapping("/{auctionId}/bid") - @Operation(summary = "Submit a bid", description = "Submits a bid for the specified auction.") - @ApiResponse(responseCode = "200", description = "Bid submitted successfully") - public ResponseEntity> submitBid(@Parameter(description = "ID of the user submitting the bid") @RequestParam Long userId, @PathVariable Long auctionId, @Parameter(description = "Bid amount") @RequestParam BigDecimal amount) { - BidResponse bid = bidService.submitBid(userId, auctionId, amount); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_SUBMITTED_SUCCESS, bid)); - } - - @PutMapping("/{auctionId}/bid") - @Operation(summary = "Update a bid", description = "Updates the amount of an existing bid.") - @ApiResponse(responseCode = "200", description = "Bid updated successfully") - public ResponseEntity> updateBid(@Parameter(description = "ID of the user updating the bid") @RequestParam Long userId, @PathVariable Long auctionId, @Parameter(description = "New bid amount") @RequestParam BigDecimal amount) { - BidResponse updatedBid = bidService.updateBidAmount(userId, auctionId, amount); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_UPDATED_SUCCESS, updatedBid)); - } - - @GetMapping("/{auctionId}/bid/status") - @Operation(summary = "Check user bid status", description = "Checks if the user has already placed a bid on the auction.") - @ApiResponse(responseCode = "200", description = "Bid status checked successfully") - public ResponseEntity> checkUserBidStatus(@PathVariable Long auctionId, @Parameter(description = "ID of the user") @RequestParam Long userId) { - Boolean hasBidded = bidService.hasUserAlreadyBid(auctionId); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_STATUS_CHECKED, hasBidded)); - } - - // Review Endpoints - @PostMapping("/{auctionId}/review") - @Operation(summary = "Create a review", description = "Creates a review for a specific auction.") - @ApiResponse(responseCode = "200", description = "Review created successfully") - public ResponseEntity> createReview(@Parameter(description = "ID of the user creating the review") @RequestParam Long userId, @PathVariable Long auctionId, @RequestBody ReviewRequest.Create reviewRequest) { - ReviewResponse createdReview = reviewService.createReview(userId, auctionId, reviewRequest); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEW_CREATED_SUCCESS, createdReview)); - } - - - @GetMapping("/{auctionId}/review") - @Operation(summary = "Get review by auction ID", description = "Retrieves the review for a specific auction.") - @ApiResponse(responseCode = "200", description = "Review retrieved successfully") - public ResponseEntity> getReviewByAuctionId(@PathVariable Long auctionId) { - ReviewResponse review = reviewService.getReviewByAuctionId(auctionId); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEW_RETRIEVED, review)); - } -} diff --git a/src/main/java/com/tasksprints/auction/api/bid/BidController.java b/src/main/java/com/tasksprints/auction/api/bid/BidController.java deleted file mode 100644 index 0527a094..00000000 --- a/src/main/java/com/tasksprints/auction/api/bid/BidController.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.tasksprints.auction.api.bid; - -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.domain.bid.dto.BidRequest; -import com.tasksprints.auction.domain.bid.dto.BidResponse; -import com.tasksprints.auction.domain.bid.service.BidService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.simp.SimpMessageSendingOperations; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequiredArgsConstructor -@RequestMapping("/api/v1/bid") -public class BidController { - private final BidService bidService; - private final SimpMessageSendingOperations simpMessageSendingOperations; - - @MessageMapping("/bid") - public void handleBid(BidRequest bidRequest) { - /** - * 입찰하는거 여기다가 추가하면 좋을 듯 합니다g. - */ - BidResponse bidResponse = bidService.submitBid(bidRequest.getUserId(), bidRequest.getAuctionId(), bidRequest.getAmount()); - simpMessageSendingOperations.convertAndSend("/bid/"+bidResponse.getUuid(), bidResponse); - } - - @GetMapping("/{uuid}") - @Operation(summary = "Get a bid", description = "Get a bid by bid uuid") - @ApiResponse(responseCode = "200", description = "Bid status retrieved successfully") - public ResponseEntity> getBidByUuid(@PathVariable(value = "uuid") String uuid) { - BidResponse bid = bidService.getBidByUuid(uuid); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_RETRIEVED, bid)); - } -} diff --git a/src/main/java/com/tasksprints/auction/api/product/ProductController.java b/src/main/java/com/tasksprints/auction/api/product/ProductController.java deleted file mode 100644 index 5d0c2d55..00000000 --- a/src/main/java/com/tasksprints/auction/api/product/ProductController.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.tasksprints.auction.api.product; - -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.domain.product.dto.request.ProductRequest; -import com.tasksprints.auction.domain.product.dto.response.ProductResponse; -import com.tasksprints.auction.domain.product.service.ProductService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/product") -@RequiredArgsConstructor -public class ProductController { - private final ProductService productService; - - @Operation(summary = "Register Product", description = "Register a new product for an auction by user ID and auction ID.") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Product registered successfully."), @ApiResponse(responseCode = "404", description = "Auction not found.")}) - @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) - public ResponseEntity> registerProduct(@RequestParam Long userId, @RequestParam Long auctionId, @RequestPart("productRequest") ProductRequest.Register productRequest, @RequestPart("images") List images) { - ProductResponse ProductResponse = productService.register(userId, auctionId, productRequest, images); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.PRODUCT_FOUND_SUCCESS, ProductResponse)); - } - - @Operation(summary = "Get Products by Auction ID", description = "Retrieve products based on user ID or auction ID.") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Product(s) retrieved successfully."), @ApiResponse(responseCode = "404", description = "Product(s) not found for the given user ID or auction ID.")}) - @GetMapping - public ResponseEntity> getProducts(@RequestParam Long auctionId) { - - ProductResponse ProductResponse = productService.getProductByAuctionId(auctionId); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.PRODUCT_FOUND_SUCCESS, ProductResponse)); - } - - @Operation(summary = "Update Product", description = "Update an existing product with new information.") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Product updated successfully."), @ApiResponse(responseCode = "404", description = "Product not found.")}) - @PutMapping - public ResponseEntity> updateProduct(@RequestBody ProductRequest.Update productRequest) { - ProductResponse updatedProduct = productService.update(productRequest); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEW_RETRIEVED, updatedProduct)); - } - -} - diff --git a/src/main/java/com/tasksprints/auction/api/socket/ChatController.java b/src/main/java/com/tasksprints/auction/api/socket/ChatController.java deleted file mode 100644 index 9dcb7324..00000000 --- a/src/main/java/com/tasksprints/auction/api/socket/ChatController.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.tasksprints.auction.api.socket; - -import com.tasksprints.auction.domain.socket.dto.MessageDto; -import lombok.RequiredArgsConstructor; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.simp.SimpMessageSendingOperations; -import org.springframework.stereotype.Controller; - -@RequiredArgsConstructor -@Controller -public class ChatController { - - private final SimpMessageSendingOperations simpMessageSendingOperations; - - @MessageMapping("/chat/message") - public void message(MessageDto messageDto) { - if (MessageDto.MessageType.ENTER.equals(messageDto.getType())) { - messageDto.setMessage(messageDto.getSender() + "님이 입장하셨습니다."); - } - - if (MessageDto.MessageType.LEAVE.equals(messageDto.getType())) { - messageDto.setMessage(messageDto.getSender() + "님이 퇴장하셨습니다."); - } - simpMessageSendingOperations.convertAndSend("/sub/chat/room/" + messageDto.getRoomId(), messageDto); - } -} diff --git a/src/main/java/com/tasksprints/auction/api/socket/ChatRoomController.java b/src/main/java/com/tasksprints/auction/api/socket/ChatRoomController.java deleted file mode 100644 index a8a88285..00000000 --- a/src/main/java/com/tasksprints/auction/api/socket/ChatRoomController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.tasksprints.auction.api.socket; - -import com.tasksprints.auction.domain.socket.model.ChatRoom; -import com.tasksprints.auction.domain.socket.service.ChatService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RequiredArgsConstructor -@Controller -public class ChatRoomController { - - private final ChatService chatService; - - @GetMapping("/chat/room") - public String chatRoomList(Model model) { - return "/chat/room"; - } //채팅방 목록 화면 - - @GetMapping("/chat/room/all") - @ResponseBody - public List chatRoomAll() { - return chatService.findAllRoom(); - } //채팅방 목록 조회 - - @PostMapping("/chat/room") - @ResponseBody - public ChatRoom createChatRoom(@RequestParam("name") String name) { - return chatService.createRoom(name); - } //채팅방 생성 - - @GetMapping("/chat/room/enter/{chatRoomId}") - public String enterChatRoom(Model model, @PathVariable(value = "chatRoomId") String chatRoomId) { - model.addAttribute("chatRoomId", chatRoomId); - return "/chat/enter"; - } //채팅방 입장 화면 - - @GetMapping("/chat/room/{chatRoomId}") - @ResponseBody - public ChatRoom chatRoom(@PathVariable(value = "chatRoomId") String chatRoomId) { - return chatService.findRoomById(chatRoomId); - } //채팅방 조회 -} diff --git a/src/main/java/com/tasksprints/auction/api/user/UserController.java b/src/main/java/com/tasksprints/auction/api/user/UserController.java deleted file mode 100644 index 1771ec0d..00000000 --- a/src/main/java/com/tasksprints/auction/api/user/UserController.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.tasksprints.auction.api.user; - -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.dto.response.UserSummaryResponse; -import com.tasksprints.auction.domain.user.service.UserService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/user") -@RequiredArgsConstructor -public class UserController { - private final UserService userService; - - @Operation(summary = "Register User", description = "Register a new user with the provided user information.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User created successfully."), - @ApiResponse(responseCode = "400", description = "Invalid user data.") - }) - @PostMapping() - public ResponseEntity> register(@RequestBody UserRequest.Register user) { - UserDetailResponse createdUser = userService.createUser(user); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.USER_CREATED_SUCCESS, createdUser)); - } - - @Operation(summary = "Get User by ID", description = "Retrieve user details based on the user ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User details retrieved successfully."), - @ApiResponse(responseCode = "404", description = "User not found.") - }) - @GetMapping("/{id}") - public ResponseEntity> getUserById(@PathVariable Long id) { - UserDetailResponse user = userService.getUserDetailsById(id); - return ResponseEntity.ok(ApiResult.success(null, user)); - } - - @Operation(summary = "Get All Users", description = "Retrieve a summary list of all users.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User summary list retrieved successfully."), - @ApiResponse(responseCode = "204", description = "No users found.") - }) - @GetMapping() - public ResponseEntity>> getAllUsers() { - List users = userService.getUsersSummary(); - return ResponseEntity.ok(ApiResult.success(null, users)); - } - - @Operation(summary = "Update User", description = "Update user information for a specific user ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User updated successfully."), - @ApiResponse(responseCode = "404", description = "User not found.") - }) - @PutMapping() - public ResponseEntity> updateUser(@RequestParam Long id, @RequestBody UserRequest.Update user) { - UserDetailResponse updatedUser = userService.updateUser(id, user); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.USER_UPDATED_SUCCESS, updatedUser)); - } - - @Operation(summary = "Delete User", description = "Delete a user by user ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User deleted successfully."), - @ApiResponse(responseCode = "404", description = "User not found.") - }) - @DeleteMapping() - public ResponseEntity> deleteUser(@RequestParam Long id) { - userService.deleteUser(id); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.USER_DELETED_SUCCESS)); - } -// @GetMapping("/reviews") -// @Operation(summary = "Get reviews by user", description = "Retrieves all reviews created by a specific user.") -// @ApiResponses(value = { -// @ApiResponse(responseCode = "200", description = "User reviews retrieved successfully"), -// @ApiResponse(responseCode = "404", description = "User not found") -// }) -// public ResponseEntity>> getReviewsByUser(@RequestParam Long userId) { -// List reviews = reviewService.getReviewsByUserId(userId); -// return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEWS_RETRIEVED, reviews)); -// } - -} diff --git a/src/main/java/com/tasksprints/auction/common/resolver/SearchConditionResolver.java b/src/main/java/com/tasksprints/auction/common/resolver/SearchConditionResolver.java deleted file mode 100644 index ab9dacb6..00000000 --- a/src/main/java/com/tasksprints/auction/common/resolver/SearchConditionResolver.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.tasksprints.auction.common.resolver; - -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -@Component -public class SearchConditionResolver implements HandlerMethodArgumentResolver { - @Override - public boolean supportsParameter(MethodParameter parameter) { - // 메서드 파라미터 타입이 AuctionRequest.SearchCondition일 때 처리 - return parameter.getParameterType().equals(AuctionRequest.SearchCondition.class); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - - // QueryString에서 값을 추출 - String auctionCategory = webRequest.getParameter("auctionCategory"); - String productCategory = webRequest.getParameter("productCategory"); - String startTime = webRequest.getParameter("startTime"); - String endTime = webRequest.getParameter("endTime"); - String minPrice = webRequest.getParameter("minPrice"); - String maxPrice = webRequest.getParameter("maxPrice"); - String auctionStatus = webRequest.getParameter("auctionStatus"); - String sortBy = webRequest.getParameter("sortBy"); - // 파싱 및 변환 - AuctionCategory parsedAuctionCategory = auctionCategory != null ? AuctionCategory.fromDisplayName(auctionCategory) : null; - ProductCategory parsedProductCategory = productCategory != null ? ProductCategory.fromDisplayName(productCategory) : null; - LocalDateTime parsedStartTime = startTime != null ? LocalDateTime.parse(startTime, DateTimeFormatter.ISO_DATE_TIME) : null; - LocalDateTime parsedEndTime = endTime != null ? LocalDateTime.parse(endTime, DateTimeFormatter.ISO_DATE_TIME) : null; - BigDecimal parsedMinPrice = minPrice != null ? new BigDecimal(minPrice) : null; - BigDecimal parsedMaxPrice = maxPrice != null ? new BigDecimal(maxPrice) : null; - AuctionStatus parsedAuctionStatus = auctionStatus != null ? AuctionStatus.fromDisplayName(auctionStatus) : null; - - // SearchCondition 객체 생성 및 반환 - return new AuctionRequest.SearchCondition( - parsedAuctionCategory, - parsedProductCategory, - parsedStartTime, - parsedEndTime, - parsedMinPrice, - parsedMaxPrice, - parsedAuctionStatus, - sortBy - ); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/dto/request/AuctionRequest.java b/src/main/java/com/tasksprints/auction/domain/auction/dto/request/AuctionRequest.java deleted file mode 100644 index 927f297e..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/dto/request/AuctionRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.tasksprints.auction.domain.auction.dto.request; - -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -public class AuctionRequest { - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class Create { - private LocalDateTime startTime; - private LocalDateTime endTime; - private BigDecimal startingBid; - private AuctionCategory auctionCategory; - private AuctionStatus auctionStatus; - } - - @Getter - @AllArgsConstructor - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class SearchCondition { - private AuctionCategory auctionCategory; - private ProductCategory productCategory; - private LocalDateTime startTime; - private LocalDateTime endTime; - private BigDecimal minPrice; - private BigDecimal maxPrice; - private AuctionStatus auctionStatus; - private String sortBy; - - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionResponse.java b/src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionResponse.java deleted file mode 100644 index c0ff0dd4..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionResponse.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.tasksprints.auction.domain.auction.dto.response; - -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.product.model.ProductImage; -import lombok.*; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; - -import static java.util.stream.Collectors.toList; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class AuctionResponse { - private Long id; - private LocalDateTime startTime; - private LocalDateTime endTime; - private String category; - private String status; - private BigDecimal startingBid; - private Long viewCount; - private Long sellerId; - private String sellerNickName; - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Details { - private Long id; - private LocalDateTime startTime; - private LocalDateTime endTime; - private String category; - private String status; - private BigDecimal startingBid; - private Long viewCount; - private Long sellerId; - private String sellerNickName; - private Long productId; - private String productCategory; - @Setter - private List productImageUrls; - - public static AuctionResponse.Details of(Auction auction) { - return AuctionResponse.Details.builder() - .id(auction.getId()) - .startTime(auction.getStartTime()) - .endTime(auction.getEndTime()) - .category(auction.getAuctionCategory().name()) - .status(auction.getAuctionStatus().name()) - .startingBid(auction.getStartingBid()) - .viewCount(auction.getViewCount()) - .sellerId(auction.getSeller() != null ? auction.getSeller().getId() : null) - .sellerNickName(auction.getSeller() != null ? auction.getSeller().getNickName() : null) - .productId(auction.getProduct() != null ? auction.getProduct().getId() : null) - .productCategory(auction.getProduct() != null && auction.getProduct().getCategory() != null ? auction.getProduct().getCategory().name() : null) - .productImageUrls(auction.getProduct() != null && auction.getProduct().getProductImageList() != null - ? auction.getProduct().getProductImageList().stream() - .map(ProductImage::getImageUrl) - .collect(toList()) - : Collections.emptyList()) - .build(); - } - } - public static AuctionResponse of(Auction auction) { - return AuctionResponse.builder() - .id(auction.getId()) - .startTime(auction.getStartTime()) - .endTime(auction.getEndTime()) - .category(auction.getAuctionCategory().name()) - .status(auction.getAuctionStatus().name()) - .startingBid(auction.getStartingBid()) - .viewCount(auction.getViewCount()) - .sellerId(auction.getSeller().getId()) - .sellerNickName(auction.getSeller().getNickName()) - .build(); - } - - -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionStatusResponse.java b/src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionStatusResponse.java deleted file mode 100644 index ae608607..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/dto/response/AuctionStatusResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.tasksprints.auction.domain.auction.dto.response; - -import lombok.Data; - -@Data -public class AuctionStatusResponse { - private String status; -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionAlreadyClosedException.java b/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionAlreadyClosedException.java deleted file mode 100644 index 33ed6523..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionAlreadyClosedException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.auction.exception; - -public class AuctionAlreadyClosedException extends RuntimeException { - public AuctionAlreadyClosedException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionEndedException.java b/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionEndedException.java deleted file mode 100644 index e145943d..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionEndedException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.auction.exception; - -public class AuctionEndedException extends RuntimeException { - public AuctionEndedException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionNotFoundException.java b/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionNotFoundException.java deleted file mode 100644 index e0d5f63d..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/exception/AuctionNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.auction.exception; - -public class AuctionNotFoundException extends RuntimeException { - public AuctionNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/exception/InvalidAuctionTimeException.java b/src/main/java/com/tasksprints/auction/domain/auction/exception/InvalidAuctionTimeException.java deleted file mode 100644 index 34f5c3b8..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/exception/InvalidAuctionTimeException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.auction.exception; - -public class InvalidAuctionTimeException extends RuntimeException { - public InvalidAuctionTimeException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java b/src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java deleted file mode 100644 index c141b035..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.tasksprints.auction.domain.auction.model; - -import com.tasksprints.auction.common.entity.BaseEntity; -import com.tasksprints.auction.domain.bid.model.Bid; -import com.tasksprints.auction.domain.product.model.Product; -import com.tasksprints.auction.domain.user.model.User; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.ColumnDefault; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Getter -@ToString -@Entity(name = "auction") -public class Auction extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private LocalDateTime startTime; - - @Column(nullable = false) - private LocalDateTime endTime; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private AuctionCategory auctionCategory; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - @Setter - private AuctionStatus auctionStatus; - - @Column(nullable = false) - private BigDecimal startingBid; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - @ToString.Exclude - private User seller; - - @OneToOne - @Builder.Default - private Product product = null; - - @OneToMany - @Builder.Default - private List bids = new ArrayList<>(); - - @Column(nullable = false) - private Long viewCount; - - @PrePersist - protected void onCreate() { - if (viewCount == null) { - viewCount = 0L; // 기본값 설정 - } - } - - public static Auction create(LocalDateTime startTime, LocalDateTime endTime, BigDecimal startingBid, AuctionCategory auctionCategory, AuctionStatus auctionStatus, User seller) { - Auction newAuction = Auction.builder() - .startTime(startTime) - .endTime(endTime) - .startingBid(startingBid) - .auctionCategory(auctionCategory) - .auctionStatus(auctionStatus) - .build(); - newAuction.addUser(seller); - return newAuction; - } - - public void addProduct(Product product) { - //product.addAuction(this); product에서 auction을 추가하고 있어서 중복 - this.product = product; - } - - public void addUser(User seller) { - seller.addAuction(this); - this.seller = seller; - } - - public void incrementViewCount() { - if (viewCount == null) { - viewCount = 0L; - } - this.viewCount += 1; - } - - -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/model/AuctionCategory.java b/src/main/java/com/tasksprints/auction/domain/auction/model/AuctionCategory.java deleted file mode 100644 index 7c362815..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/model/AuctionCategory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.tasksprints.auction.domain.auction.model; - -public enum AuctionCategory { - PRIVATE_FREE, - PUBLIC_FREE, - PRIVATE_PAID, - PUBLIC_PAID; - - - public static AuctionCategory fromDisplayName(String auctionCategory) { - for (AuctionCategory category : values()) { - if (category.name().equalsIgnoreCase(auctionCategory)) { - return category; - } - } - throw new IllegalArgumentException("Unknown auctionCategory: " + auctionCategory); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/model/AuctionStatus.java b/src/main/java/com/tasksprints/auction/domain/auction/model/AuctionStatus.java deleted file mode 100644 index 71355903..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/model/AuctionStatus.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.tasksprints.auction.domain.auction.model; - -public enum AuctionStatus { - PENDING, - ACTIVE, - CLOSED, - CANCELED; - - public static AuctionStatus fromDisplayName(String auctionStatus) { - for (AuctionStatus status : values() ) { - if (status.name().equalsIgnoreCase(auctionStatus)) { - return status; - } - } - throw new IllegalArgumentException("Unknown auctionStatus: " + auctionStatus); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/repository/AuctionRepository.java b/src/main/java/com/tasksprints/auction/domain/auction/repository/AuctionRepository.java deleted file mode 100644 index 24f95a20..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/repository/AuctionRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.tasksprints.auction.domain.auction.repository; - - -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.repository.support.AuctionCriteriaRepository; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; -import java.util.Optional; - -public interface AuctionRepository extends JpaRepository, AuctionCriteriaRepository { - - @Query("SELECT a FROM auction a WHERE a.seller.id = :userId") - List findAuctionsByUserId(@Param("userId") Long userId); - - @Query("SELECT a FROM auction a WHERE a.id = :auctionId") - Optional findAuctionById(@Param("auctionId") Long auctionId); - -} - diff --git a/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepository.java b/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepository.java deleted file mode 100644 index 47fc40ee..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.tasksprints.auction.domain.auction.repository.support; - -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; - -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - - -public interface AuctionCriteriaRepository { - Page getAuctionsByFilters(Pageable pageable, AuctionRequest.SearchCondition searchCondition); - @Deprecated - Page getAuctionsByCategory(Pageable pageable, - AuctionRequest.SearchCondition searchCondition, - ProductCategory category); -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java b/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java deleted file mode 100644 index 18e085be..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.tasksprints.auction.domain.auction.repository.support; - -import com.querydsl.core.BooleanBuilder; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.jpa.impl.JPAQueryFactory; -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; - -import java.util.List; - -import static com.tasksprints.auction.domain.auction.model.QAuction.auction; -import static com.tasksprints.auction.domain.product.model.QProduct.product; -import static com.tasksprints.auction.domain.user.model.QUser.user; - -@RequiredArgsConstructor -@Repository -public class AuctionCriteriaRepositoryImpl implements AuctionCriteriaRepository { - private final JPAQueryFactory queryFactory; - - public Page getAuctionsByFilters(Pageable pageable, AuctionRequest.SearchCondition condition) { - BooleanBuilder builder = buildSearchCondition(condition); - OrderSpecifier sortOrder = getSortOrder(condition); - List result = buildQueryWithPaginationAndSorting(builder, pageable, sortOrder); - - // int 오버플로 주의 -// int total = queryFactory -// .selectFrom(auction) -// .where(builder) -// .fetch().size(); - - long total = result.size(); - - return new PageImpl<>(result, pageable, total); - } - - private List buildQueryWithPaginationAndSorting(BooleanBuilder builder, Pageable pageable, OrderSpecifier sortOrder) { - var mainQuery = queryFactory - .selectFrom(auction) - .leftJoin(auction.product, product) - .fetchJoin() - .leftJoin(auction.seller, user) - .fetchJoin() - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()); - - if (sortOrder != null) { - mainQuery.orderBy(sortOrder); - } - - return mainQuery.fetch(); - } - - @Deprecated - public Page getAuctionsByCategory(Pageable pageable, - AuctionRequest.SearchCondition condition, - ProductCategory category) { - - BooleanBuilder builder = buildSearchCondition(condition); - filterByCategory(category, builder); - OrderSpecifier sortOrder = getSortOrder(condition); - List result = buildQueryWithPaginationAndSorting(builder, pageable, sortOrder); - - int total = queryFactory - .selectFrom(auction) - .where(builder) - .fetch().size(); - - return new PageImpl<>(result, pageable, total); - - } - @Deprecated - private void filterByCategory(ProductCategory category, BooleanBuilder builder) { - if (category != null) { - builder.and(product.category.eq(category)); - } - } - - - private BooleanBuilder buildSearchCondition(AuctionRequest.SearchCondition condition) { - BooleanBuilder builder = new BooleanBuilder(); - - if (condition.getAuctionCategory() != null) { - builder.and(auction.auctionCategory.eq(condition.getAuctionCategory())); - } - if (condition.getProductCategory() != null) { - builder.and(product.category.eq(condition.getProductCategory())); - } - if (condition.getMinPrice() != null && condition.getMaxPrice() != null) { - builder.and(auction.startingBid.between(condition.getMinPrice(), condition.getMaxPrice())); - } - if (condition.getStartTime() != null && condition.getEndTime() != null) { - builder.and(auction.endTime.between(condition.getStartTime(), condition.getEndTime())); - } - if (condition.getAuctionStatus() != null) { - builder.and(auction.auctionStatus.eq(condition.getAuctionStatus())); - } - - return builder; - } - - private OrderSpecifier getSortOrder(AuctionRequest.SearchCondition condition) { - if (condition.getSortBy() != null) { - return switch (condition.getSortBy()) { - case "bidsAsc" -> auction.bids.size().asc(); - case "bidsDesc" -> auction.bids.size().desc(); - case "endTimeASC" -> auction.endTime.asc(); - case "startTimeASC" -> auction.startTime.asc(); - case "viewCountDESC" -> auction.viewCount.desc(); - - default -> null; - }; - } - return null; - } - - -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/service/AuctionService.java b/src/main/java/com/tasksprints/auction/domain/auction/service/AuctionService.java deleted file mode 100644 index 69606124..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/service/AuctionService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.tasksprints.auction.domain.auction.service; - -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.List; - -/** - * 사용자가 맞는지 판단도 해야함. - */ -public interface AuctionService { - AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionRequest); - - void closeAuction(Long auctionId); - - String getAuctionStatus(Long auctionId); - - List getAuctionsByUser(Long userId); - - List getAllAuctions(); - - AuctionResponse getAuctionById(Long auctionId); - - Page getAuctionsByFilter(Pageable pageable, AuctionRequest.SearchCondition searchCondition); - - @Deprecated - Page getAuctionsByProductCategory(Pageable pageable, AuctionRequest.SearchCondition searchCondition, ProductCategory category); -} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImpl.java deleted file mode 100644 index f14522cf..00000000 --- a/src/main/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImpl.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.tasksprints.auction.domain.auction.service; - -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; -import com.tasksprints.auction.domain.auction.exception.AuctionAlreadyClosedException; -import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; -import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@Slf4j -@RequiredArgsConstructor -public class AuctionServiceImpl implements AuctionService { - private final UserRepository userRepository; - private final AuctionRepository auctionRepository; - - @Override - public AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionRequest) { - User seller = userRepository.findById(userId) - .orElseThrow(() -> new UserNotFoundException("User not found")); - if (auctionRequest.getStartTime().isAfter(auctionRequest.getEndTime())) { - throw new InvalidAuctionTimeException("End time must be after start time"); - } - - Auction newAuction = Auction.create( - auctionRequest.getStartTime(), - auctionRequest.getEndTime(), - auctionRequest.getStartingBid(), - auctionRequest.getAuctionCategory(), - auctionRequest.getAuctionStatus(), - seller - ); - - Auction savedAuction = auctionRepository.save(newAuction); - /** - * Product 생성에 대한 부분 고려 필요 - * STEP 1 - * - S3 버킷에 올리는 api 따로 구성 ( 독립적 시행 ) url 반환 - * - 해당 url 을 토대로 Auction 생성 시, Product 도 같이 생성 - * STEP 2 - * - 각각의 기능을 완전 분리 - */ - return AuctionResponse.of(savedAuction); - } - - @Override - public void closeAuction(Long auctionId) { - Auction auction = auctionRepository.findById(auctionId) - .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); - - if (auction.getAuctionStatus() == AuctionStatus.CLOSED) { - throw new AuctionAlreadyClosedException("Auction is already closed"); - } - auction.setAuctionStatus(AuctionStatus.CLOSED); - } - - @Override - public String getAuctionStatus(Long auctionId) { - Auction auction = auctionRepository.findById(auctionId) - .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); - - return auction.getAuctionStatus().name(); - } - - @Override - public List getAuctionsByUser(Long userId) { - User seller = userRepository.findById(userId) - .orElseThrow(() -> new UserNotFoundException("User not found")); - - List foundAuctions = auctionRepository.findAuctionsByUserId(seller.getId()); - - return foundAuctions.stream() - .map(AuctionResponse::of) - .toList(); - } - - - @Override - public List getAllAuctions() { - List foundAuctions = auctionRepository.findAll(); - return foundAuctions.stream() - .map(AuctionResponse::of) - .toList(); - } - - @Override - public AuctionResponse getAuctionById(Long auctionId) { - Auction foundAuction = auctionRepository.findAuctionById(auctionId) - .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); - foundAuction.incrementViewCount(); - auctionRepository.save(foundAuction); - return AuctionResponse.of(foundAuction); - } - - /** - * NULL POINTER EXCEPTION 발생 - * NULL 안정성 보장을 해줬음 - **/ - @Override - public Page getAuctionsByFilter(Pageable pageable, AuctionRequest.SearchCondition searchCondition) { - Page auctions = auctionRepository.getAuctionsByFilters(pageable, searchCondition); - return auctions.map(AuctionResponse.Details::of); - } - - @Deprecated - @Override - public Page getAuctionsByProductCategory(Pageable pageable, AuctionRequest.SearchCondition searchCondition, ProductCategory category) { - Page auctions = auctionRepository.getAuctionsByFilters(pageable, searchCondition); - return auctions.map(AuctionResponse.Details::of); - } - -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/dto/BidRequest.java b/src/main/java/com/tasksprints/auction/domain/bid/dto/BidRequest.java deleted file mode 100644 index aeb60234..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/dto/BidRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tasksprints.auction.domain.bid.dto; - -import lombok.Data; - -import java.math.BigDecimal; - - -@Data -public class BidRequest { - private Long userId; - private Long auctionId; - private BigDecimal amount; -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/dto/BidResponse.java b/src/main/java/com/tasksprints/auction/domain/bid/dto/BidResponse.java deleted file mode 100644 index ff75f9e1..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/dto/BidResponse.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.tasksprints.auction.domain.bid.dto; - -import com.tasksprints.auction.domain.bid.model.Bid; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class BidResponse { - Long userId; - String name; - Long auctionId; - BigDecimal amount; - String uuid; - - public static BidResponse of(Bid bid) { - return BidResponse.builder() - .userId(bid.getUser().getId()) - .name(bid.getUser().getName()) - .auctionId(bid.getAuction().getId()) - .amount(bid.getAmount()) - .build(); - /** 아이템 목록 추가**/ - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/exception/BidNotFoundException.java b/src/main/java/com/tasksprints/auction/domain/bid/exception/BidNotFoundException.java deleted file mode 100644 index 79067b6a..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/exception/BidNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.bid.exception; - -public class BidNotFoundException extends RuntimeException { - public BidNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/exception/InvalidBidAmountException.java b/src/main/java/com/tasksprints/auction/domain/bid/exception/InvalidBidAmountException.java deleted file mode 100644 index 148a73bf..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/exception/InvalidBidAmountException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.bid.exception; - -public class InvalidBidAmountException extends RuntimeException { - public InvalidBidAmountException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/model/Bid.java b/src/main/java/com/tasksprints/auction/domain/bid/model/Bid.java deleted file mode 100644 index 8df974cd..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/model/Bid.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.tasksprints.auction.domain.bid.model; - -import com.tasksprints.auction.common.entity.BaseEntity; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.user.model.User; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.util.UUID; - -@Entity(name = "bids") -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Getter -public class Bid extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - //UUID를 ID로 사용하게 되면 성능 저하 -> 기본 키는 Id를 유지하되, 외부에 공개할 키는 UUID로 설정 - private String uuid; - - private BigDecimal amount; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "auction_id") - @Builder.Default - private Auction auction = null; - - @ManyToOne(fetch = FetchType.LAZY) //지연 로딩 설정 이유 ? -> N + 1 문제 발생 가능성 있으므로 fetch join 으로 개선 필요 - @JoinColumn(name = "user_id") - @Builder.Default - private User user = null; //null 설정한 이유가 무엇인지? - - public static Bid create(BigDecimal amount, User user, Auction auction) { - Bid newBid = Bid.builder() - .amount(amount) - .build(); - - newBid.addUserAndAuction(user, auction); - return newBid; - } - - /** - * addUser 와 addAuction 을 양방향으로 묶을지 단방향으로 묶을지에 대한 고민 - */ - public void addUser(User user) { - this.user = user; - - } - - public void addAuction(Auction auction) { - this.auction = auction; - } - - public void addUserAndAuction(User user, Auction auction) { - addUser(user); - addAuction(auction); - } - - public void update(BigDecimal amount) { - this.amount = amount; - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/repository/BidRepository.java b/src/main/java/com/tasksprints/auction/domain/bid/repository/BidRepository.java deleted file mode 100644 index 6fbd03ab..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/repository/BidRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.tasksprints.auction.domain.bid.repository; - -import com.tasksprints.auction.domain.bid.model.Bid; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import java.util.List; -import java.util.Optional; - -public interface BidRepository extends JpaRepository { - - @Query("select b from bids b where b.auction.id = :auctionId") - List findByAuctionId(Long auctionId); - - @Query("select b from bids b where b.auction.id = :auctionId and b.user.id = :userId") - Optional findByUserIdAndAuctionId(Long userId, Long auctionId); - - @Query("select b from bids b where b.uuid = :uuid") - Optional findByUuid(String uuid); -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/service/BidService.java b/src/main/java/com/tasksprints/auction/domain/bid/service/BidService.java deleted file mode 100644 index bde62713..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/service/BidService.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.tasksprints.auction.domain.bid.service; - -import com.tasksprints.auction.domain.bid.dto.BidResponse; - -import java.math.BigDecimal; - -public interface BidService { - /** - * 입찰 - * 입찰금액 변경 - */ - BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount); - - BidResponse updateBidAmount(Long userId, Long auctionId, BigDecimal amount); - - Boolean hasUserAlreadyBid(Long auctionId); - - BidResponse getBidByUuid(String uuid); -} diff --git a/src/main/java/com/tasksprints/auction/domain/bid/service/BidServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/bid/service/BidServiceImpl.java deleted file mode 100644 index 8a6b436f..00000000 --- a/src/main/java/com/tasksprints/auction/domain/bid/service/BidServiceImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.tasksprints.auction.domain.bid.service; - -import com.tasksprints.auction.domain.auction.exception.AuctionEndedException; -import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.bid.dto.BidResponse; -import com.tasksprints.auction.domain.bid.exception.BidNotFoundException; -import com.tasksprints.auction.domain.bid.exception.InvalidBidAmountException; -import com.tasksprints.auction.domain.bid.model.Bid; -import com.tasksprints.auction.domain.bid.repository.BidRepository; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; - -/** - * 실시간성으로 인해서 socket으로 대체할지에 대한 여부 고민 필요 - */ -@Service -@RequiredArgsConstructor -public class BidServiceImpl implements BidService { - - private final BidRepository bidRepository; - private final UserRepository userRepository; - private final AuctionRepository auctionRepository; - - @Override - public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { - // 입찰 시 유효성 검사 - User foundUser = userRepository.findById(userId) - .orElseThrow(() -> new UserNotFoundException("User not found")); - Auction foundAuction = auctionRepository.findById(auctionId) - .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); - - // 경매가 종료되었는지 확인 - if (foundAuction.getEndTime().isBefore(LocalDateTime.now())) { - throw new AuctionEndedException("This auction has already ended."); - } - - // 최소 입찰 금액 충족 여부 확인 - if (amount.compareTo(foundAuction.getStartingBid()) < 0) { - throw new InvalidBidAmountException("Bid amount is less than the minimum required bid amount."); - } - - // 입찰 생성 및 저장 - Bid createdBid = Bid.create(amount, foundUser, foundAuction); - Bid savedBid = bidRepository.save(createdBid); - return BidResponse.of(savedBid); - } - - @Override - public BidResponse updateBidAmount(Long userId, Long auctionId, BigDecimal newAmount) { - // 기존 입찰을 찾습니다. - Bid foundBid = bidRepository.findByUserIdAndAuctionId(userId, auctionId) - .orElseThrow(() -> new BidNotFoundException("Bid not found")); - - Auction foundAuction = foundBid.getAuction(); - - // 경매가 종료되었는지 확인합니다. - if (foundAuction.getEndTime().isBefore(LocalDateTime.now())) { - throw new AuctionEndedException("This auction has already ended."); - } - - // 새로운 입찰 금액이 기존 금액보다 큰지 확인합니다. - if (newAmount.compareTo(foundBid.getAmount()) <= 0) { - throw new InvalidBidAmountException("New bid amount must be greater than the previous bid amount."); - } - - // 새로운 입찰 금액이 최소 입찰 금액을 충족하는지 확인합니다. - if (newAmount.compareTo(foundAuction.getStartingBid()) < 0) { - throw new InvalidBidAmountException("Bid amount is less than the minimum required bid amount."); - } - - // 입찰 금액 업데이트 - foundBid.update(newAmount); - Bid updatedBid = bidRepository.save(foundBid); - return BidResponse.of(updatedBid); - } - - @Override - public Boolean hasUserAlreadyBid(Long auctionId) { - List bids = bidRepository.findByAuctionId(auctionId); - return !bids.isEmpty(); - } - - @Override - public BidResponse getBidByUuid(String uuid) { - Bid bid = bidRepository.findByUuid(uuid) - .orElseThrow(() -> new BidNotFoundException("Bid not found")); - return BidResponse.of(bid); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/dto/request/ProductRequest.java b/src/main/java/com/tasksprints/auction/domain/product/dto/request/ProductRequest.java deleted file mode 100644 index 0c57dc62..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/dto/request/ProductRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.tasksprints.auction.domain.product.dto.request; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -public class ProductRequest { - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class Register { - private String name; - private String description; - private String category; - } - - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class Update { - private Long productId; - private String name; - private String description; - } - -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/dto/response/ProductResponse.java b/src/main/java/com/tasksprints/auction/domain/product/dto/response/ProductResponse.java deleted file mode 100644 index 22359ee7..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/dto/response/ProductResponse.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.tasksprints.auction.domain.product.dto.response; - -import com.tasksprints.auction.domain.product.model.Product; -import com.tasksprints.auction.domain.product.model.ProductImage; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Getter -public class ProductResponse { - private Long productId; - private String name; - private String description; - private String category; - private Long ownerId; - private String ownerNickName; - private Long auctionId; - private List productImageList; - - private static List extractProductImageList(Product product) { - return product.getProductImageList() - .stream() - .map(ProductImage::getImageUrl) - .toList(); - } - - public static ProductResponse of(Product product) { - return ProductResponse.builder() - .productId(product.getId()) - .name(product.getName()) - .description(product.getDescription()) - .ownerId(product.getOwner().getId()) - .ownerNickName(product.getOwner().getNickName()) - .category(product.getCategory().getDisplayName()) - .auctionId(product.getAuction().getId()) - .productImageList(extractProductImageList(product)) - .build(); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/exception/ProductImageUploadException.java b/src/main/java/com/tasksprints/auction/domain/product/exception/ProductImageUploadException.java deleted file mode 100644 index 993fcee8..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/exception/ProductImageUploadException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tasksprints.auction.domain.product.exception; - - -public class ProductImageUploadException extends RuntimeException { - - public ProductImageUploadException(String message) { - super(message); - } - - public ProductImageUploadException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/exception/ProductNotFoundException.java b/src/main/java/com/tasksprints/auction/domain/product/exception/ProductNotFoundException.java deleted file mode 100644 index 6fb2881f..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/exception/ProductNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.product.exception; - -public class ProductNotFoundException extends RuntimeException { - public ProductNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/model/Product.java b/src/main/java/com/tasksprints/auction/domain/product/model/Product.java deleted file mode 100644 index ff72c7c7..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/model/Product.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.tasksprints.auction.domain.product.model; - -import com.tasksprints.auction.common.entity.BaseEntity; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.user.model.User; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Entity(name = "products") -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class Product extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String description; - - @Enumerated(EnumType.STRING) - private ProductCategory category; //제품의 Category - - @ManyToOne - @JoinColumn(name = "owner_id") - private User owner; - - @OneToOne - //mappedby - @JoinColumn(name = "auction_id") - private Auction auction; - - @OneToMany - @Builder.Default - private List productImageList = new ArrayList<>(); - - public static Product create(String name, String description, User owner, Auction auction, String productCategory, List productImageList) { - Product product = Product.builder().name(name).description(description).category(ProductCategory.fromDisplayName(productCategory)).build(); - product.addOwnerAndAuction(owner, auction); - product.initProductImageList(productImageList); - return product; - } - - public void addOwner(User owner) { - this.owner = owner; - } - - public void addAuction(Auction auction) { - //양방향 매핑 - this.auction = auction; - auction.addProduct(this); - } - - public void initProductImageList(List productImageList) { - this.productImageList = productImageList; - } - - public void addOwnerAndAuction(User owner, Auction auction) { - addOwner(owner); - addAuction(auction); - } - - public void update(String name, String description) { - this.name = name; - this.description = description; - } - -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/model/ProductCategory.java b/src/main/java/com/tasksprints/auction/domain/product/model/ProductCategory.java deleted file mode 100644 index db4d2de4..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/model/ProductCategory.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.tasksprints.auction.domain.product.model; - -public enum ProductCategory { - WOMENS_CLOTHING("여성의류"), - UNDERWEAR_SLEEPWEAR("언더웨어/잠옷"), - PLUS_SIZE("빅사이즈"), - SENIOR_CLOTHING("시니어의류"), - MENS_CLOTHING("남성의류"), - - OVERSEAS_COSMETICS("해외화장품"), - HAIR("헤어"), - DOMESTIC_COSMETICS("국내화장품"), - BODY("바디"), - PERFUME("향수"), - BEAUTY("미용"), - - CURTAINS("커튼"), - FURNITURE("가구"), - LIVING("생활"), - DECOR("장식소품"), - STORAGE("수납용품"), - KITCHEN("주방"), - INTERIOR("인테리어"), - BEDDING("침구"), - - TV("TV"), - REFRIGERATOR("냉장고"), - WASHING_MACHINE("세탁기"), - KITCHEN_APPLIANCES("주방/생활"), - BEAUTY_APPLIANCES("이미용가전"), - AIR_PURIFIER("청정/제습"), - FAN_AIRCON("선풍기/에어컨"), - - MIRRORLESS_CAMERA("미러리스"), - DSLR("DSLR"), - DIGITAL_CAMERA("디카"), - MP3_PMP_DICTIONARY("MP3/PMP/사전"), - GAMING_CONSOLE("게임기/타이틀"), - SMARTPHONE("휴대폰/스마트폰"), - LAPTOP("노트북"), - DESKTOP("데스크탑"), - MONITOR("모니터"), - PRINTER("프린터"), - ETC("기타제품"); - private final String displayName; - - ProductCategory(String displayName) { - this.displayName = displayName; - } - - /** - * 한정된 갯수의 Category 이기에 따로 Over-head는 발생하지 않을 것으로 판단 - ***/ - public static ProductCategory fromDisplayName(String displayName) { - for (ProductCategory category : values()) { - if (category.displayName.equals(displayName)) { - return category; - } - } - throw new IllegalArgumentException("Unknown category: " + displayName); - } - - public String getDisplayName() { - return displayName; - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/model/ProductImage.java b/src/main/java/com/tasksprints/auction/domain/product/model/ProductImage.java deleted file mode 100644 index 6f3eb357..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/model/ProductImage.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.tasksprints.auction.domain.product.model; - -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity(name = "product_images") -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ProductImage { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(unique = false) - @Getter - private String imageUrl; - - -// @ColumnDefault("false") -// private Boolean isPrime; - - public static ProductImage create(String imageUrl) { - return ProductImage.builder() - .imageUrl(imageUrl) - .build(); - } - -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/repository/ProductImageRepository.java b/src/main/java/com/tasksprints/auction/domain/product/repository/ProductImageRepository.java deleted file mode 100644 index 75f89859..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/repository/ProductImageRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.product.repository; - -import com.tasksprints.auction.domain.product.model.ProductImage; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductImageRepository extends JpaRepository { -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/repository/ProductRepository.java b/src/main/java/com/tasksprints/auction/domain/product/repository/ProductRepository.java deleted file mode 100644 index 2231e7e1..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/repository/ProductRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.tasksprints.auction.domain.product.repository; - -import com.tasksprints.auction.domain.product.model.Product; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; -import java.util.Optional; - -public interface ProductRepository extends JpaRepository { - Optional findByAuctionId(Long auctionId); - - // 쿼리를 메서드 이름으로 표현 - List findByOwnerId(Long ownerId); -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/service/ProductService.java b/src/main/java/com/tasksprints/auction/domain/product/service/ProductService.java deleted file mode 100644 index 475fde1d..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/service/ProductService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.tasksprints.auction.domain.product.service; - -import com.tasksprints.auction.domain.product.dto.request.ProductRequest; -import com.tasksprints.auction.domain.product.dto.response.ProductResponse; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.List; - -/** - * item으로 Auction 검색 - */ -public interface ProductService { - String uploadImage(MultipartFile image) throws IOException; - - List uploadImageBulk(List images); - - List getProductsByUserId(Long userId); - - ProductResponse getProductByAuctionId(Long auctionId); - - ProductResponse register(Long userId, Long auctionId, ProductRequest.Register product, List images); - - ProductResponse update(ProductRequest.Update product); -} diff --git a/src/main/java/com/tasksprints/auction/domain/product/service/ProductServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/product/service/ProductServiceImpl.java deleted file mode 100644 index 26ba29c2..00000000 --- a/src/main/java/com/tasksprints/auction/domain/product/service/ProductServiceImpl.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.tasksprints.auction.domain.product.service; - -import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.product.dto.request.ProductRequest; -import com.tasksprints.auction.domain.product.dto.response.ProductResponse; -import com.tasksprints.auction.domain.product.exception.ProductImageUploadException; -import com.tasksprints.auction.domain.product.exception.ProductNotFoundException; -import com.tasksprints.auction.domain.product.model.Product; -import com.tasksprints.auction.domain.product.model.ProductImage; -import com.tasksprints.auction.domain.product.repository.ProductImageRepository; -import com.tasksprints.auction.domain.product.repository.ProductRepository; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class ProductServiceImpl implements ProductService { - private static final String UPLOADS_DIR = "src/main/resources/static/uploads/thumbnails/"; - private final ProductRepository productRepository; - private final UserRepository userRepository; - private final AuctionRepository auctionRepository; - private final ProductImageRepository productImageRepository; - - @Override - public String uploadImage(MultipartFile image) { - String fileName = generateFileName(image); - Path filePath = Paths.get(UPLOADS_DIR, fileName); - saveFile(filePath, image); - return "/uploads/thumbnails/" + fileName; - } - - @Override - public List uploadImageBulk(List images) { - return images.stream() // 병렬 처리를 피하고 일반 Stream 사용 - .map(this::uploadImage) - .collect(Collectors.toList()); - } - - @Override - @Deprecated - public List getProductsByUserId(Long userId) { - List products = productRepository.findByOwnerId(userId); - return convertToDTOList(products); - } - - @Override - public ProductResponse getProductByAuctionId(Long auctionId) { - Product product = productRepository.findByAuctionId(auctionId) - .orElseThrow(() -> new ProductNotFoundException("Product not found")); - return ProductResponse.of(product); - } - - @Transactional - @Override - public ProductResponse register(Long userId, Long auctionId, ProductRequest.Register request, List images) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new UserNotFoundException("User not found")); - Auction auction = auctionRepository.findById(auctionId) - .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); - - // 이미지 업로드 - List imageUrls = uploadImageBulk(images); - - List productImageList = imageUrls.parallelStream() - .map(ProductImage::create) - .toList(); - - List savedProductImageList = productImageRepository.saveAll(productImageList); - - // 상품 생성 - Product newProduct = Product.create( - request.getName(), - request.getDescription(), - user, - auction, - request.getCategory(), - savedProductImageList - ); - - Product createdProduct = productRepository.save(newProduct); - - return ProductResponse.of(createdProduct); - } - - @Override - public ProductResponse update(ProductRequest.Update request) { - Long productId = request.getProductId(); - Product foundProduct = productRepository.findById(productId) - .orElseThrow(() -> new ProductNotFoundException("Product not found")); - - foundProduct.update(request.getName(), request.getDescription()); - Product savedProduct = productRepository.save(foundProduct); - return ProductResponse.of(savedProduct); - } - - private List convertToDTOList(List products) { - return products.stream() - .map(ProductResponse::of) - .collect(Collectors.toList()); - } - - private void saveFile(Path filePath, MultipartFile image) { - try { - Files.createDirectories(filePath.getParent()); // 디렉토리 생성 (필요한 경우) - Files.copy(image.getInputStream(), filePath); // 파일 저장 - } catch (IOException e) { - throw new ProductImageUploadException("Failed to upload image: " + image.getOriginalFilename(), e); - } - } - - private String generateFileName(MultipartFile image) { - return UUID.randomUUID().toString().replace("-", "") + "_" + image.getOriginalFilename(); - } - -} diff --git a/src/main/java/com/tasksprints/auction/domain/review/dto/request/ReviewRequest.java b/src/main/java/com/tasksprints/auction/domain/review/dto/request/ReviewRequest.java deleted file mode 100644 index 92653ba4..00000000 --- a/src/main/java/com/tasksprints/auction/domain/review/dto/request/ReviewRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.tasksprints.auction.domain.review.dto.request; - -import lombok.Data; - -public class ReviewRequest { - @Data - public static class Create { - Integer rating; - String content; - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/review/dto/response/ReviewResponse.java b/src/main/java/com/tasksprints/auction/domain/review/dto/response/ReviewResponse.java deleted file mode 100644 index 0967b8ad..00000000 --- a/src/main/java/com/tasksprints/auction/domain/review/dto/response/ReviewResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.tasksprints.auction.domain.review.dto.response; - -import com.tasksprints.auction.domain.review.model.Review; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ReviewResponse { - Integer rating; - String content; - - public static ReviewResponse of(Review review) { - return ReviewResponse.builder() - .rating(review.getRating()) - .content(review.getContent()) - .build(); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/review/model/Review.java b/src/main/java/com/tasksprints/auction/domain/review/model/Review.java deleted file mode 100644 index 867b9e40..00000000 --- a/src/main/java/com/tasksprints/auction/domain/review/model/Review.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.tasksprints.auction.domain.review.model; - -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.user.model.User; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity(name = "reviews") -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Getter -public class Review { - @Id - @GeneratedValue - private Long id; - - @Column(unique = true) - private Integer rating; // 리뷰 평점 (1~5 등으로 제한할 수 있음) - - @Column(unique = true) - private String content; // 리뷰 내용 - - @ManyToOne - @JoinColumn(name = "writer_id") - private User writer; - - @OneToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "auction_id") - private Auction auction; - - public static Review create(String content, Integer rating, User writer, Auction auction) { - Review newReview = Review.builder() - .content(content) - .rating(rating) - .build(); - - newReview.addWriterAndAuction(writer, auction); - return newReview; - } - - public void addWriter(User writer) { - this.writer = writer; - } - - public void addAuction(Auction auction) { - this.auction = auction; - } - - public void addWriterAndAuction(User writer, Auction auction) { - addWriter(writer); - addAuction(auction); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/review/repository/ReviewRepository.java b/src/main/java/com/tasksprints/auction/domain/review/repository/ReviewRepository.java deleted file mode 100644 index a6d654bf..00000000 --- a/src/main/java/com/tasksprints/auction/domain/review/repository/ReviewRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.tasksprints.auction.domain.review.repository; - -import com.tasksprints.auction.domain.review.model.Review; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import java.util.List; -import java.util.Optional; - -public interface ReviewRepository extends JpaRepository { - @Query("select r from reviews r where r.writer.id = :userId") - List findByUserId(Long userId); - - @Query("select r from reviews r where r.auction.id = :auctionId") - Optional findByAuctionId(Long auctionId); -} diff --git a/src/main/java/com/tasksprints/auction/domain/review/service/ReviewService.java b/src/main/java/com/tasksprints/auction/domain/review/service/ReviewService.java deleted file mode 100644 index 725f186a..00000000 --- a/src/main/java/com/tasksprints/auction/domain/review/service/ReviewService.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.tasksprints.auction.domain.review.service; - -import com.tasksprints.auction.domain.review.dto.request.ReviewRequest; -import com.tasksprints.auction.domain.review.dto.response.ReviewResponse; - -import java.util.List; - -public interface ReviewService { - - ReviewResponse createReview(Long userId, Long auctionId, ReviewRequest.Create review); - - List getReviewsByUserId(Long userId); - - ReviewResponse getReviewByAuctionId(Long auctionId); - -} -/** - * 수정, 삭제 불가 - */ diff --git a/src/main/java/com/tasksprints/auction/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/review/service/ReviewServiceImpl.java deleted file mode 100644 index 1d4fe951..00000000 --- a/src/main/java/com/tasksprints/auction/domain/review/service/ReviewServiceImpl.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.tasksprints.auction.domain.review.service; - -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.review.dto.request.ReviewRequest; -import com.tasksprints.auction.domain.review.dto.response.ReviewResponse; -import com.tasksprints.auction.domain.review.model.Review; -import com.tasksprints.auction.domain.review.repository.ReviewRepository; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class ReviewServiceImpl implements ReviewService { - private final ReviewRepository reviewRepository; - private final UserRepository userRepository; - private final AuctionRepository auctionRepository; - - @Override - public ReviewResponse createReview(Long userId, Long auctionId, ReviewRequest.Create review) { - User user = userRepository.findById(userId) - .orElseThrow(); - Auction auction = auctionRepository.findAuctionById(auctionId) - .orElseThrow(); - Review createdReview = Review.create(review.getContent(), review.getRating(), user, auction); - Review savedReview = reviewRepository.save(createdReview); - return ReviewResponse.of(savedReview); - } - - @Override - public List getReviewsByUserId(Long userId) { - List reviews = reviewRepository.findByUserId(userId); - return reviews.stream() - .map(ReviewResponse::of) - .collect(Collectors.toList()); - } - - @Override - public ReviewResponse getReviewByAuctionId(Long auctionId) { - Review foundReview = reviewRepository.findByAuctionId(auctionId) - .orElseThrow(); - return ReviewResponse.of(foundReview); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/socket/dto/AddChatRoomDto.java b/src/main/java/com/tasksprints/auction/domain/socket/dto/AddChatRoomDto.java deleted file mode 100644 index 72dc7d28..00000000 --- a/src/main/java/com/tasksprints/auction/domain/socket/dto/AddChatRoomDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.tasksprints.auction.domain.socket.dto; - -import com.tasksprints.auction.domain.socket.model.ChatRoom; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@AllArgsConstructor -@NoArgsConstructor -public class AddChatRoomDto { - - private String name; - - public ChatRoom toEntity() { - return ChatRoom.builder() - .name(name) - .build(); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/socket/dto/MessageDto.java b/src/main/java/com/tasksprints/auction/domain/socket/dto/MessageDto.java deleted file mode 100644 index dafed068..00000000 --- a/src/main/java/com/tasksprints/auction/domain/socket/dto/MessageDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.tasksprints.auction.domain.socket.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@AllArgsConstructor -@NoArgsConstructor -public class MessageDto { - public enum MessageType { - ENTER, TALK, LEAVE - } - - private MessageType type; - private String roomId; - private String sender; - private String message; - - - public void setMessage(String s) { - this.message = s; - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/socket/model/ChatRoom.java b/src/main/java/com/tasksprints/auction/domain/socket/model/ChatRoom.java deleted file mode 100644 index 54ec2d97..00000000 --- a/src/main/java/com/tasksprints/auction/domain/socket/model/ChatRoom.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.tasksprints.auction.domain.socket.model; - -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ChatRoom { - - @Id - @Column(name = "id") - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @Column(name = "chatRoomId") - private String chatRoomId; - - @Column(name = "name") - private String name; - - @OneToMany - @Column(name = "chatterList") - private List chatters; - - @Builder - public ChatRoom(String name) { - this.chatRoomId = UUID.randomUUID().toString(); - this.name = name; - this.chatters = new ArrayList<>(); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/socket/model/Chatter.java b/src/main/java/com/tasksprints/auction/domain/socket/model/Chatter.java deleted file mode 100644 index 8f431919..00000000 --- a/src/main/java/com/tasksprints/auction/domain/socket/model/Chatter.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.tasksprints.auction.domain.socket.model; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Chatter { - - @Id - @Column(name = "chatterId") - private String chatterId; - - @Column(name = "sessionId") - private String sessionId; - - @Builder - public Chatter(String id, String sessionId) { - this.chatterId = id; - this.sessionId = sessionId; - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/socket/repository/ChatRoomRepository.java b/src/main/java/com/tasksprints/auction/domain/socket/repository/ChatRoomRepository.java deleted file mode 100644 index 7f46e10d..00000000 --- a/src/main/java/com/tasksprints/auction/domain/socket/repository/ChatRoomRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.tasksprints.auction.domain.socket.repository; - -import com.tasksprints.auction.domain.socket.model.ChatRoom; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface ChatRoomRepository extends JpaRepository { -} diff --git a/src/main/java/com/tasksprints/auction/domain/socket/repository/ChatterRepository.java b/src/main/java/com/tasksprints/auction/domain/socket/repository/ChatterRepository.java deleted file mode 100644 index 64b73b9f..00000000 --- a/src/main/java/com/tasksprints/auction/domain/socket/repository/ChatterRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.tasksprints.auction.domain.socket.repository; - -import com.tasksprints.auction.domain.socket.model.Chatter; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface ChatterRepository extends JpaRepository { -} diff --git a/src/main/java/com/tasksprints/auction/domain/socket/service/ChatService.java b/src/main/java/com/tasksprints/auction/domain/socket/service/ChatService.java deleted file mode 100644 index 2b29c6e9..00000000 --- a/src/main/java/com/tasksprints/auction/domain/socket/service/ChatService.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.tasksprints.auction.domain.socket.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.tasksprints.auction.domain.socket.model.ChatRoom; -import com.tasksprints.auction.domain.socket.repository.ChatRoomRepository; -import jakarta.annotation.PostConstruct; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; - -@Slf4j -@RequiredArgsConstructor -@Service -public class ChatService { - - private final ObjectMapper mapper; - private ConcurrentHashMap chatRoomMap; - private final ChatRoomRepository chatRoomRepository; - - @PostConstruct - private void init() { - chatRoomMap = new ConcurrentHashMap<>(); - } - - public List findAllRoom() { - return new ArrayList<>(chatRoomMap.values()); - } - - public ChatRoom findRoomById(String id) { - return chatRoomMap.get(id); - } - - @Transactional - public ChatRoom createRoom(String name) { - ChatRoom chatRoom = chatRoomRepository.save(new ChatRoom(name)); - log.info("Create Room : {} {}", chatRoom.getId(), chatRoom.getName()); - chatRoomMap.put(chatRoom.getChatRoomId(), chatRoom); - return chatRoom; - } -} - diff --git a/src/main/java/com/tasksprints/auction/domain/user/dto/request/UserRequest.java b/src/main/java/com/tasksprints/auction/domain/user/dto/request/UserRequest.java deleted file mode 100644 index 7960e859..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/dto/request/UserRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.tasksprints.auction.domain.user.dto.request; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; - - -public class UserRequest { - @Getter - @NoArgsConstructor - @AllArgsConstructor - @ToString - public static class Register { - String name; - String email; - String password; - String nickname; - } - - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class Update { - String name; - String password; - String nickname; - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java b/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java deleted file mode 100644 index 58f2ec21..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.tasksprints.auction.domain.user.dto.response; - -import com.tasksprints.auction.domain.user.model.User; -import lombok.AllArgsConstructor; -import lombok.Data; - -@AllArgsConstructor -@Data -public class UserDetailResponse { - private Long id; - private String name; - private String email; - private String password; - private String nickName; - - private UserDetailResponse(User user) { - this.id = user.getId(); - this.name = user.getName(); - this.email = user.getEmail(); - this.password = user.getPassword(); - this.nickName = user.getNickName(); - } - - public static UserDetailResponse of(User user) { - return new UserDetailResponse(user); - } - -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserResponse.java b/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserResponse.java deleted file mode 100644 index 1d4f1082..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tasksprints.auction.domain.user.dto.response; - -public class UserResponse { -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserSummaryResponse.java b/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserSummaryResponse.java deleted file mode 100644 index 19af4890..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserSummaryResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.tasksprints.auction.domain.user.dto.response; - -import com.tasksprints.auction.domain.user.model.User; -import lombok.AllArgsConstructor; -import lombok.Data; - -@AllArgsConstructor -@Data -public class UserSummaryResponse { - private Long id; - private String nickName; - private String name; - private String email; - - public UserSummaryResponse(User user) { - this.id = user.getId(); - this.nickName = user.getNickName(); - this.email = user.getEmail(); - this.name = user.getName(); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/exception/UserNotFoundException.java b/src/main/java/com/tasksprints/auction/domain/user/exception/UserNotFoundException.java deleted file mode 100644 index 4e83e58c..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/exception/UserNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.user.exception; - -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/model/User.java b/src/main/java/com/tasksprints/auction/domain/user/model/User.java deleted file mode 100644 index 4db7e595..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/model/User.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.tasksprints.auction.domain.user.model; - -import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; -import com.tasksprints.auction.domain.auction.model.Auction; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.SQLRestriction; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Builder -@NoArgsConstructor -@AllArgsConstructor -@SQLRestriction("deleted_at is null") -@Getter -@ToString -@Entity(name = "users") -public class User extends BaseEntityWithUpdate { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; - - @Column(nullable = false, unique = true) //unique 특성 추가 필요 - private String email; - - @Column(nullable = false) //unique 특성 추가 필요 - private String password; - - @Column(nullable = false) - private String nickName; //unique 특성 추가 필요 - - @Column(nullable = true) - private LocalDateTime deletedAt; - - @OneToMany(mappedBy = "seller", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - @Builder.Default - private List auctions = new ArrayList<>(); -// 추후 추가 -// @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) -// @Builder.Default -// private List bids = new ArrayList<>(); - - /** - * @descripton static factory pattern을 적용하여, 구현 - */ - public static User create(String name, String email, String password, String nickName) { - return User.builder() - .name(name) - .email(email) - .password(password) - .nickName(nickName) - .build(); - } - - public void setAuctions(List auctions) { - this.auctions = auctions; - } - - /** - * @description 인자 길이가 너무 길어서, 함수단위로 쪼갤지에 대한 고민중 - */ - - - public void update(String name, String password, String nickName) { - this.name = name; - this.password = password; - this.nickName = nickName; - } - - public void delete() { - this.deletedAt = LocalDateTime.now(); - } - - public void addAuction(Auction auction) { - this.auctions.add(auction); - } -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java b/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java deleted file mode 100644 index 007cf110..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tasksprints.auction.domain.user.repository; - -import com.tasksprints.auction.domain.user.model.User; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserRepository extends JpaRepository { -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java deleted file mode 100644 index 63755627..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.tasksprints.auction.domain.user.service; - -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.dto.response.UserSummaryResponse; - -import java.util.List; - -public interface UserService { - UserDetailResponse createUser(UserRequest.Register user); - - UserDetailResponse getUserDetailsById(Long id); - - List getUsersSummary(); - - UserDetailResponse updateUser(Long id, UserRequest.Update user); - - void deleteUser(Long id); -} diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java deleted file mode 100644 index ad9d0b6b..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.tasksprints.auction.domain.user.service; - -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.dto.response.UserSummaryResponse; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class UserServiceImpl implements UserService { - - private final UserRepository userRepository; - - @Override - public UserDetailResponse createUser(UserRequest.Register request) { - User user = User.create(request.getName(), request.getEmail(), request.getPassword(), request.getNickname()); - - User newUser = userRepository.save(user); - return UserDetailResponse.of(newUser); - } - - @Override - public UserDetailResponse getUserDetailsById(Long id) { - User foundUser = userRepository.findById(id) - .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); - return UserDetailResponse.of(foundUser); - } - - @Override - public List getUsersSummary() { - List foundUsers = userRepository.findAll(); - return foundUsers.stream() - .map(UserSummaryResponse::new) - .collect(Collectors.toList()); - } - - @Override - public UserDetailResponse updateUser(Long id, UserRequest.Update request) { - User user = userRepository.findById(id) - .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); - - user.update(request.getName(), request.getPassword(), request.getNickname()); - User updatedUser = userRepository.save(user); - return UserDetailResponse.of(updatedUser); - } - - @Override - public void deleteUser(Long id) { - User user = userRepository.findById(id) - .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); - user.delete(); // 사용자 상태를 '삭제됨'으로 변경 - userRepository.save(user); // 상태 업데이트를 저장 - } - -} diff --git a/src/main/resources/templates/chat/enter.html b/src/main/resources/templates/chat/enter.html deleted file mode 100644 index 09707d08..00000000 --- a/src/main/resources/templates/chat/enter.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - Websocket ChatRoom - - - - - - - -
-
-

- -
-
-
- -
- -
- -
-
-
    -
-
- - - - - - - - diff --git a/src/main/resources/templates/chat/room.html b/src/main/resources/templates/chat/room.html deleted file mode 100644 index 91d78e08..00000000 --- a/src/main/resources/templates/chat/room.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - Websocket Chat - - - - - - -
-
-
-

채팅방 리스트

-
-
-
-
- -
- -
- -
-
-
    -
-
- - - - - - diff --git a/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java b/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java deleted file mode 100644 index e21acad2..00000000 --- a/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java +++ /dev/null @@ -1,215 +0,0 @@ -package com.tasksprints.auction.api; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.tasksprints.auction.api.auction.AuctionController; -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.auction.service.AuctionService; -import com.tasksprints.auction.domain.bid.dto.BidResponse; -import com.tasksprints.auction.domain.bid.service.BidService; -import com.tasksprints.auction.domain.review.dto.request.ReviewRequest; -import com.tasksprints.auction.domain.review.dto.response.ReviewResponse; -import com.tasksprints.auction.domain.review.service.ReviewService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(AuctionController.class) -@MockBean(JpaMetamodelMappingContext.class) -public class AuctionControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private AuctionService auctionService; - - @MockBean - private BidService bidService; - - @MockBean - private ReviewService reviewService; - - private ObjectMapper objectMapper; - - @BeforeEach - public void setUp() { - objectMapper = new ObjectMapper(); - } - - @Test - @DisplayName("경매 생성 성공") - public void testCreateAuction_Success() throws Exception { - AuctionRequest.Create auctionRequest = new AuctionRequest.Create(); - AuctionResponse auctionDTO = new AuctionResponse(); // Populate with necessary fields - - when(auctionService.createAuction(anyLong(), any())).thenReturn(auctionDTO); - - mockMvc.perform(post("/api/v1/auction") - .param("userId", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(auctionRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.AUCTION_CREATED_SUCCESS)); - } - - @Test - @DisplayName("경매 생성 실패") - public void testCreateAuction_Failure() throws Exception { - AuctionRequest.Create auctionRequest = new AuctionRequest.Create(); - - when(auctionService.createAuction(anyLong(), any())).thenThrow(new RuntimeException("Error creating auction")); - - mockMvc.perform(post("/api/v1/auction") - .param("userId", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(auctionRequest))) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error creating auction")); - } - - @Test - @DisplayName("경매 종료 성공") - public void testCloseAuction_Success() throws Exception { - mockMvc.perform(post("/api/v1/auction/1/close")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.AUCTION_CLOSED_SUCCESS)); - - verify(auctionService, times(1)).closeAuction(1L); - } - - @Test - @DisplayName("경매 상태 조회 성공") - public void testGetAuctionStatus_Success() throws Exception { - String auctionStatus = "OPEN"; - when(auctionService.getAuctionStatus(1L)).thenReturn(auctionStatus); - - mockMvc.perform(get("/api/v1/auction/1/status")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.AUCTION_STATUS_RETRIEVED)) - .andExpect(jsonPath("$.data").value(auctionStatus)); - } - - @Test - @DisplayName("QueryString을 통한 경매 목록 조회") - public void testFindAuctionByUsingQueryString_Success() throws Exception { - // Given - List auctionResponseList = new ArrayList<>(); - Pageable pageable = PageRequest.of(0, 10); - Page pageAuctionResponse = new PageImpl<>(auctionResponseList, pageable, 0); - when(auctionService.getAuctionsByFilter(any(),any())).thenReturn(pageAuctionResponse); - - // When & Then - mockMvc.perform(get("/api/v1/auction") - .param("auctionCategory", AuctionCategory.PRIVATE_FREE.name()) - .param("productCategory", "여성의류") - .param("startTime", LocalDateTime.now().minusDays(1).toString()) - .param("endTime", LocalDateTime.now().toString()) - .param("minPrice", "100") - .param("maxPrice", "500") - .param("auctionStatus", AuctionStatus.ACTIVE.name()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED)); - } - -// @Test -// @DisplayName("잘못된 유형을 통한 경매목록 조회(기본값으로 대응)") -// public void testFindAuctionByUsingWrongAuctionCategory_Success() throws Exception { -// List auctionResponseList = new ArrayList<>(); -// when(auctionService.getAuctionsByFilter(any(), any())).thenReturn(auctionResponseList); -// -// mockMvc.perform(get("/api/v1/auction") -// .param("auctionCategory", "NON")) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.message").value(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED)); -// } - -// @Test -// @DisplayName("마감 기한이 24시간 이하로 남은 경매 목록 조회") -// public void testFindAuctionsEndWithin24Hours_Success() throws Exception { -// -// /* -// 테스트 코드 추가 -// * */ -// -// } - - @Test - @DisplayName("입찰 제출 성공") - public void testSubmitBid_Success() throws Exception { - BidResponse bidDTO = new BidResponse(); // Populate with necessary fields - when(bidService.submitBid(anyLong(), anyLong(), any())).thenReturn(bidDTO); - - mockMvc.perform(post("/api/v1/auction/1/bid") - .param("userId", "1") - .param("amount", "100.00")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_SUBMITTED_SUCCESS)); - } - - @Test - @DisplayName("입찰 금액 업데이트 성공") - public void testUpdateBid_Success() throws Exception { - BidResponse updatedBidDTO = new BidResponse(); // Populate with necessary fields - when(bidService.updateBidAmount(anyLong(), anyLong(), any())).thenReturn(updatedBidDTO); - - mockMvc.perform(put("/api/v1/auction/1/bid") - .param("userId", "1") - .param("amount", "150.00")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_UPDATED_SUCCESS)); - } - - @Test - @DisplayName("리뷰 생성 성공") - public void testCreateReview_Success() throws Exception { - ReviewRequest.Create reviewRequest = new ReviewRequest.Create(); - ReviewResponse reviewResponse = new ReviewResponse(); // Populate with necessary fields - when(reviewService.createReview(anyLong(), anyLong(), any())).thenReturn(reviewResponse); - - mockMvc.perform(post("/api/v1/auction/1/review") - .param("userId", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(reviewRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.REVIEW_CREATED_SUCCESS)); - } - - -// @Test -// @DisplayName("사용자 리뷰 조회 성공") -// public void testGetReviewsByUser_Success() throws Exception { -// List reviews = Collections.singletonList(new ReviewDTO()); // Populate with necessary fields -// when(reviewService.getReviewsByUserId(anyLong())).thenReturn(reviews); -// -// mockMvc.perform(get("/api/v1/auction/user/1/reviews")) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.message").value(ApiResponseMessages.REVIEWS_RETRIEVED)) -// .andExpect(jsonPath("$.data").isArray()); -// } -} diff --git a/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java b/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java deleted file mode 100644 index 54212bc5..00000000 --- a/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.tasksprints.auction.api; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.tasksprints.auction.api.product.ProductController; -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.domain.product.dto.request.ProductRequest; -import com.tasksprints.auction.domain.product.dto.response.ProductResponse; -import com.tasksprints.auction.domain.product.service.ProductService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.web.servlet.MockMvc; - -import java.nio.charset.StandardCharsets; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(ProductController.class) -@MockBean(JpaMetamodelMappingContext.class) -public class ProductControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private ProductService productService; - - @Autowired - private ObjectMapper objectMapper; - - private MockMultipartFile multipartFile1; - private MockMultipartFile multipartFile2; - private MockMultipartFile productRequestMultiPart; - - @BeforeEach - public void setup() throws Exception { - multipartFile1 = new MockMultipartFile( - "images", - "image1.png", - "image/png", - "fake image content 1".getBytes(StandardCharsets.UTF_8) - ); - - multipartFile2 = new MockMultipartFile( - "images", - "image2.jpg", - "image/jpeg", - "fake image content 2".getBytes(StandardCharsets.UTF_8) - ); - - ProductRequest.Register productRequest = new ProductRequest.Register("Sample Product", "This is a sample product.", "여성의류"); - String valueAsString = objectMapper.writeValueAsString(productRequest); - - productRequestMultiPart = new MockMultipartFile( - "productRequest", - "productRequest", - MediaType.APPLICATION_JSON_VALUE, - valueAsString.getBytes(StandardCharsets.UTF_8) - ); - } - - @DisplayName("제품 등록 성공") - @Test - public void testRegisterProduct_Success() throws Exception { - ProductResponse productResponse = new ProductResponse(); // Populate with necessary fields - when(productService.register(anyLong(), anyLong(), any(), any())).thenReturn(productResponse); - - mockMvc.perform(multipart("/api/v1/product?userId=1&auctionId=1") - .file(multipartFile1) - .file(multipartFile2) - .file(productRequestMultiPart) - ) - .andExpect(status().isOk()) // Expect status OK - .andExpect(jsonPath("$.message").value(ApiResponseMessages.PRODUCT_FOUND_SUCCESS)); - } - - @DisplayName("제품 등록 실패") - @Test - public void testRegisterProduct_Failure() throws Exception { - when(productService.register(anyLong(), anyLong(), any(), any())).thenThrow(new RuntimeException("Error registering product")); - - mockMvc.perform(multipart("/api/v1/product?userId=1&auctionId=1") - .file(multipartFile1) - .file(multipartFile2) - .file(productRequestMultiPart) - ) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error registering product")); - } - - @DisplayName("auctionId를 통해 제품 조회 성공") - @Test - public void testGetProductByAuctionId_Success() throws Exception { - ProductResponse ProductResponse = new ProductResponse(); // Populate with necessary fields - when(productService.getProductByAuctionId(anyLong())).thenReturn(ProductResponse); - - mockMvc.perform(get("/api/v1/product?auctionId=1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.PRODUCT_FOUND_SUCCESS)); // Adjust as needed - } - - @DisplayName("제품 수정 성공") - @Test - public void testUpdateProduct_Success() throws Exception { - ProductRequest.Update productRequest = new ProductRequest.Update(); // Populate with necessary fields - ProductResponse updatedProductResponse = new ProductResponse(); // Populate with necessary fields - when(productService.update(any())).thenReturn(updatedProductResponse); - - mockMvc.perform(put("/api/v1/product") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(productRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.REVIEW_RETRIEVED)); // Adjust as needed - } - -} diff --git a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java b/src/test/java/com/tasksprints/auction/api/UserControllerTest.java deleted file mode 100644 index 091985ab..00000000 --- a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.tasksprints.auction.api; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.tasksprints.auction.api.user.UserController; -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.dto.response.UserSummaryResponse; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.service.UserService; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.Collections; -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(UserController.class) -@MockBean(JpaMetamodelMappingContext.class) -class UserControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private UserService userService; - - @Autowired - private ObjectMapper objectMapper; - - @Nested - @DisplayName("성공적인 요청") - class SuccessfulTests { - - @Test - @DisplayName("POST /api/v1/user - 성공") - void registerUser() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123"); - - Mockito.when(userService.createUser(any(UserRequest.Register.class))).thenReturn(userDetailResponse); - - UserRequest.Register request = new UserRequest.Register("John", "john@example.com", "password", "john123"); - - mockMvc.perform(post("/api/v1/user") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_CREATED_SUCCESS)) - .andExpect(jsonPath("$.data.name").value("John")) - .andExpect(jsonPath("$.data.email").value("john@example.com")); - } - - @Test - @DisplayName("GET /api/v1/user/{id} - 성공") - void getUserById() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123"); - - Mockito.when(userService.getUserDetailsById(anyLong())).thenReturn(userDetailResponse); - - mockMvc.perform(get("/api/v1/user/{id}", 1L)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.name").value("John")) - .andExpect(jsonPath("$.data.email").value("john@example.com")); - } - - @Test - @DisplayName("GET /api/v1/user - 성공") - void getAllUsers() throws Exception { - UserSummaryResponse userSummaryResponse = new UserSummaryResponse(1L, "john123", "John", "john@example.com"); - - List users = Collections.singletonList(userSummaryResponse); - - Mockito.when(userService.getUsersSummary()).thenReturn(users); - - mockMvc.perform(get("/api/v1/user")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data[0].name").value("John")) - .andExpect(jsonPath("$.data[0].email").value("john@example.com")); - } - - @Test - @DisplayName("PUT /api/v1/user - 성공") - void updateUser() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John Updated", "john@example.com", "newpassword", "john123updated"); - - Mockito.when(userService.updateUser(anyLong(), any(UserRequest.Update.class))).thenReturn(userDetailResponse); - - UserRequest.Update request = new UserRequest.Update("John Updated", "newpassword", "john123updated"); - - mockMvc.perform(put("/api/v1/user") - .param("id", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_UPDATED_SUCCESS)) - .andExpect(jsonPath("$.data.name").value("John Updated")); - } - - @Test - @DisplayName("DELETE /api/v1/user - 성공") - void deleteUser() throws Exception { - Mockito.doNothing().when(userService).deleteUser(anyLong()); - - mockMvc.perform(delete("/api/v1/user") - .param("id", "1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_DELETED_SUCCESS)); - } - } - - @Nested - @DisplayName("실패한 요청") - class FailureTests { - - @Test - @DisplayName("POST /api/v1/user - 실패") - void registerUserFail() throws Exception { - Mockito.when(userService.createUser(any(UserRequest.Register.class))) - .thenThrow(new RuntimeException("Failed to create user")); - - UserRequest.Register request = new UserRequest.Register("John", "john@example.com", "password", "john123"); - - mockMvc.perform(post("/api/v1/user") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.message").value("Failed to create user")); - } - - @Test - @DisplayName("GET /api/v1/user/{id} - 실패 (사용자 없음)") - void getUserByIdFail() throws Exception { - Mockito.when(userService.getUserDetailsById(anyLong())) - .thenThrow(new UserNotFoundException("User not found with id 1")); - - mockMvc.perform(get("/api/v1/user/{id}", 1L)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.message").value("User not found")); - } - - @Test - @DisplayName("GET /api/v1/user - 실패") - void getAllUsersFail() throws Exception { - Mockito.when(userService.getUsersSummary()) - .thenThrow(new RuntimeException("Failed to retrieve users")); - - mockMvc.perform(get("/api/v1/user")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.message").value("Failed to retrieve users")); - } - - @Test - @DisplayName("PUT /api/v1/user - 실패") - void updateUserFail() throws Exception { - Mockito.when(userService.updateUser(anyLong(), any(UserRequest.Update.class))) - .thenThrow(new UserNotFoundException("User not found with id 1")); - - UserRequest.Update request = new UserRequest.Update("John Updated", "newpassword", "john123updated"); - - mockMvc.perform(put("/api/v1/user") - .param("id", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.message").value("User not found")); - } - - @Test - @DisplayName("DELETE /api/v1/user - 실패 (사용자 없음)") - void deleteUserFail() throws Exception { - Mockito.doThrow(new UserNotFoundException("User not found with id 1")) - .when(userService).deleteUser(anyLong()); - - mockMvc.perform(delete("/api/v1/user") - .param("id", "1")) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.message").value("User not found")); - } - } -} diff --git a/src/test/java/com/tasksprints/auction/domain/auction/repository/AuctionRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/auction/repository/AuctionRepositoryTest.java deleted file mode 100644 index 57ce46bf..00000000 --- a/src/test/java/com/tasksprints/auction/domain/auction/repository/AuctionRepositoryTest.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.tasksprints.auction.domain.auction.repository; - -import com.tasksprints.auction.common.config.QueryDslConfig; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.product.model.Product; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import com.tasksprints.auction.domain.product.repository.ProductRepository; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@DataJpaTest -@Import(QueryDslConfig.class) -@Slf4j -public class AuctionRepositoryTest { - - @Autowired - private AuctionRepository auctionRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private ProductRepository productRepository; - - private User seller; - - @BeforeEach - public void setUp() { - seller = User.builder() - .name("testUser") - .nickName("testNick") - .password("testPassword") - .email("test@example.com") - .build(); - userRepository.save(seller); - } - - private Auction createAuction(User seller, AuctionCategory category, AuctionStatus status) { - return Auction.create( - LocalDateTime.of(2024, 8, 1, 10, 0), - LocalDateTime.of(2024, 9, 1, 10, 0), - BigDecimal.valueOf(100.00), - category, - status, - seller - ); - } - - private Auction createAuction(User seller, AuctionCategory auctionCategory) { - return Auction.create( - LocalDateTime.of(2024, 8, 1, 10, 0), - LocalDateTime.of(2024, 9, 1, 10, 0), - BigDecimal.valueOf(100.00), - auctionCategory, - AuctionStatus.ACTIVE, - seller - ); - } - - private Product createProduct(User user, Auction auction, String productCategory) { - return Product.create( - "testName", - "testDescription", - user, - auction, - productCategory, - null - ); - } - - @Test - @DisplayName("사용자 ID로 경매 목록 조회") - public void testFindAuctionsByUserId() { - Auction auction = createAuction(seller, AuctionCategory.PUBLIC_FREE, AuctionStatus.ACTIVE); - auctionRepository.save(auction); - - List auctions = auctionRepository.findAuctionsByUserId(seller.getId()); - - assertThat(auctions).isNotEmpty(); - assertThat(auctions.get(0).getSeller().getId()).isEqualTo(seller.getId()); - } - - @Test - @DisplayName("경매 ID로 경매 조회") - public void testFindAuctionById() { - Auction auction = createAuction(seller, AuctionCategory.PUBLIC_FREE, AuctionStatus.PENDING); - auctionRepository.save(auction); - - Optional foundAuction = auctionRepository.findAuctionById(auction.getId()); - - assertTrue(foundAuction.isPresent()); - assertThat(foundAuction.get().getAuctionCategory()).isEqualTo(AuctionCategory.PUBLIC_FREE); - } - - @Test - @DisplayName("모든 경매 목록 조회") - public void testFindAll() { - Auction auction1 = createAuction(seller, AuctionCategory.PUBLIC_FREE, AuctionStatus.ACTIVE); - Auction auction2 = createAuction(seller, AuctionCategory.PUBLIC_PAID, AuctionStatus.PENDING); - - auctionRepository.save(auction1); - auctionRepository.save(auction2); - - List auctions = auctionRepository.findAll(); - - assertThat(auctions).hasSize(2); - - - } - @Deprecated - @Test - @DisplayName("경매 유형이 []인 경매 목록 조회") - public void testFindAuctionsByAuctionCategory() { - //given - Auction auction1 = createAuction(seller, AuctionCategory.PUBLIC_FREE); - Auction auction2 = createAuction(seller, AuctionCategory.PUBLIC_PAID); - Auction auction3 = createAuction(seller, AuctionCategory.PUBLIC_PAID); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - Product product1 = createProduct(seller, auction1, "TV"); - Product product2 = createProduct(seller, auction2, "TV"); - Product product3 = createProduct(seller, auction3, "TV"); - productRepository.saveAll(List.of(product1, product2, product3)); - - AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_PAID, ProductCategory.TV, null, null, null, null, null, null); - - Pageable pageable = PageRequest.of(0, 10); - - //when - Page auctions = auctionRepository.getAuctionsByFilters(pageable, condition); - - //then - assertThat(auctions).hasSize(2); - assertThat(auctions.getContent()).allMatch(auction -> auction.getAuctionCategory().equals(AuctionCategory.PUBLIC_PAID)); - assertThat(auctions.getContent()).allMatch(auction -> auction.getProduct().getCategory().equals(ProductCategory.TV)); - } - @Test - @DisplayName("QueryDSL 필터를 통해서 경매 목록 조회") - public void testFindAllUsingFilter() { - //given - Auction auction1 = createAuction(seller, AuctionCategory.PUBLIC_FREE); - Auction auction2 = createAuction(seller, AuctionCategory.PUBLIC_PAID); - auctionRepository.saveAll(List.of(auction1, auction2)); - - Product product1 = createProduct(seller, auction1, "TV"); - Product product2 = createProduct(seller, auction2, "TV"); - productRepository.saveAll(List.of(product1, product2)); - - AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_FREE, ProductCategory.TV, null, null, null, null, null, null); - Pageable pageable = PageRequest.of(0, 10); - - //when - Page auctions = auctionRepository.getAuctionsByFilters(pageable, condition); - log.info(auctions.toString()); - - //then - assertThat(auctions).hasSize(1); - assertThat(auctions.getContent().get(0).getAuctionCategory()).isEqualTo(AuctionCategory.PUBLIC_FREE); - assertThat(auctions.getContent().get(0).getProduct().getCategory()).isEqualTo(ProductCategory.TV); - - - } - -} diff --git a/src/test/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImplTest.java deleted file mode 100644 index 966a0587..00000000 --- a/src/test/java/com/tasksprints/auction/domain/auction/service/AuctionServiceImplTest.java +++ /dev/null @@ -1,405 +0,0 @@ -package com.tasksprints.auction.domain.auction.service; - -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; -import com.tasksprints.auction.domain.auction.exception.AuctionAlreadyClosedException; -import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; -import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class AuctionServiceImplTest { - - @Mock - private UserRepository userRepository; - - @Mock - private AuctionRepository auctionRepository; - - @InjectMocks - private AuctionServiceImpl auctionService; - - private User seller; - - @BeforeEach - void setUp() { - seller = User.builder() - .id(1L) - .name("testUser") - .nickName("testNick") - .password("testPassword") - .email("test@example.com") - .build(); - } - - private Auction createAuction(Long auctionId, User seller, AuctionStatus status) { - return Auction.builder() - .id(auctionId) - .auctionCategory(AuctionCategory.PUBLIC_PAID) - .auctionStatus(status) - .startTime(LocalDateTime.of(2024, 8, 1, 10, 0)) - .endTime(LocalDateTime.of(2024, 9, 1, 10, 0)) - .startingBid(BigDecimal.valueOf(100.00)) - .seller(seller) - .build(); - } - - private Auction createAuction(Long auctionId, LocalDateTime startTime) { - return Auction.builder() - .id(auctionId) - .auctionCategory(AuctionCategory.PUBLIC_PAID) - .auctionStatus(AuctionStatus.ACTIVE) - .startTime(startTime) - .endTime(LocalDateTime.of(2024, 9, 1, 10, 0)) - .startingBid(BigDecimal.valueOf(100.00)) - .seller(seller) - .build(); - } - - @Nested - @DisplayName("경매 생성 테스트") - class CreateAuctionTests { - - @Test - @DisplayName("성공") - void testCreateAuction_Success() { - AuctionRequest.Create auctionRequest = new AuctionRequest.Create( - LocalDateTime.now(), - LocalDateTime.now().plusDays(7), - BigDecimal.valueOf(100.00), - AuctionCategory.PRIVATE_FREE, - AuctionStatus.ACTIVE - ); - - when(userRepository.findById(1L)).thenReturn(Optional.of(seller)); - when(auctionRepository.save(any(Auction.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - AuctionResponse createdAuction = auctionService.createAuction(1L, auctionRequest); - - assertThat(createdAuction.getSellerId()).isEqualTo(seller.getId()); - assertThat(createdAuction.getCategory()).isEqualTo(AuctionCategory.PRIVATE_FREE.name()); - assertThat(createdAuction.getStatus()).isEqualTo(AuctionStatus.ACTIVE.name()); - - verify(auctionRepository).save(any(Auction.class)); - } - - @Test - @DisplayName("사용자 미존재 예외") - void testCreateAuction_UserNotFound() { - AuctionRequest.Create auctionRequest = new AuctionRequest.Create( - LocalDateTime.now(), - LocalDateTime.now().plusDays(7), - BigDecimal.valueOf(100.00), - AuctionCategory.PRIVATE_FREE, - AuctionStatus.ACTIVE - ); - - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> - auctionService.createAuction(1L, auctionRequest)); - - assertThat(exception.getMessage()).isEqualTo("User not found"); - verify(auctionRepository, never()).save(any(Auction.class)); - } - - @Test - @DisplayName("종료 시간이 시작 시간보다 이전인 경우 예외") - void testCreateAuction_EndTimeBeforeStartTime() { - AuctionRequest.Create auctionRequest = new AuctionRequest.Create( - LocalDateTime.now().plusDays(7), - LocalDateTime.now(), - BigDecimal.valueOf(100.00), - AuctionCategory.PRIVATE_FREE, - AuctionStatus.ACTIVE - ); - - when(userRepository.findById(1L)).thenReturn(Optional.of(seller)); - - InvalidAuctionTimeException exception = assertThrows(InvalidAuctionTimeException.class, () -> - auctionService.createAuction(1L, auctionRequest)); - - assertThat(exception.getMessage()).isEqualTo("End time must be after start time"); - verify(auctionRepository, never()).save(any(Auction.class)); - } - } - - @Nested - @DisplayName("경매 종료 테스트") - class CloseAuctionTests { - - @Test - @DisplayName("성공") - void testCloseAuction_Success() { -// Auction auction = createAuction(1L, seller, AuctionCategory.PRIVATE_FREE, AuctionStatus.ACTIVE); -// -// when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); -// when(auctionRepository.save(any(Auction.class))).thenAnswer(invocation -> invocation.getArgument(0)); -// -// Auctio closedAuction = auctionService.closeAuction(1L); -// -// assertThat(closedAuction.getAuctionStatus()).isEqualTo(AuctionStatus.CLOSED); -// verify(auctionRepository).save(auction); - } - - @Test - @DisplayName("경매 미존재 예외") - void testCloseAuction_AuctionNotFound() { - when(auctionRepository.findById(1L)).thenReturn(Optional.empty()); - - AuctionNotFoundException exception = assertThrows(AuctionNotFoundException.class, () -> - auctionService.closeAuction(1L)); - - assertThat(exception.getMessage()).isEqualTo("Auction not found"); - verify(auctionRepository, never()).save(any(Auction.class)); - } - - @Test - @DisplayName("이미 종료된 경매 예외") - void testCloseAuction_AuctionAlreadyClosed() { - Auction auction = Auction.builder() - .id(1L) - .auctionCategory(AuctionCategory.PRIVATE_FREE) - .auctionStatus(AuctionStatus.CLOSED) - .seller(seller) - .build(); - - when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); - - AuctionAlreadyClosedException exception = assertThrows(AuctionAlreadyClosedException.class, () -> - auctionService.closeAuction(1L)); - - assertThat(exception.getMessage()).isEqualTo("Auction is already closed"); - verify(auctionRepository, never()).save(any(Auction.class)); - } - } - - @Nested - @DisplayName("경매 상태 조회 테스트") - class GetAuctionStatusTests { - - @Test - @DisplayName("성공") - void testGetAuctionStatus_Success() { - Auction auction = createAuction(1L, seller, AuctionStatus.ACTIVE); - - when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); - - String auctionStatus = auctionService.getAuctionStatus(1L); - - assertThat(auctionStatus).isEqualTo(AuctionStatus.ACTIVE.toString()); - } - - @Test - @DisplayName("경매 미존재 예외") - void testGetAuctionStatus_AuctionNotFound() { - when(auctionRepository.findById(1L)).thenReturn(Optional.empty()); - - AuctionNotFoundException exception = assertThrows(AuctionNotFoundException.class, () -> - auctionService.getAuctionStatus(1L)); - - assertThat(exception.getMessage()).isEqualTo("Auction not found"); - } - } - - @Nested - @DisplayName("사용자별 경매 목록 조회 테스트") - class GetAuctionsByUserTests { - - @Test - @DisplayName("성공") - void testGetAuctionsByUser_Success() { - Auction auction1 = createAuction(1L, seller, AuctionStatus.PENDING); - Auction auction2 = createAuction(2L, seller, AuctionStatus.PENDING); - - // Mocking: 사용자 정보 반환 - when(userRepository.findById(1L)).thenReturn(Optional.of(seller)); - // Mocking: 경매 목록 반환 - when(auctionRepository.findAuctionsByUserId(1L)).thenReturn(List.of(auction1, auction2)); - - // Act: 사용자가 있는지 확인하고 경매 목록 가져오기 - List auctions = auctionService.getAuctionsByUser(1L); - - // Assert: 반환된 경매 목록 확인 - assertThat(auctions).hasSize(2); - assertThat(auctions.get(0).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); - assertThat(auctions.get(1).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); - } - - @Test - @DisplayName("사용자 미존재 예외") - void testGetAuctionsByUser_UserNotFound() { - // Arrange: 사용자가 존재하지 않을 때 findById를 모의 설정 - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - // Act & Assert: UserNotFoundException이 발생할 것으로 예상 - UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> - auctionService.getAuctionsByUser(1L)); - - assertThat(exception.getMessage()).isEqualTo("User not found"); - } - } - - @Nested - @DisplayName("모든 경매 목록 조회 테스트") - class GetAllAuctionsTests { - - @Test - @DisplayName("성공") - void testGetAllAuctions_Success() { - Auction auction1 = createAuction(1L, seller, AuctionStatus.PENDING); - Auction auction2 = createAuction(2L, seller, AuctionStatus.PENDING); - - when(auctionRepository.findAll()).thenReturn(List.of(auction1, auction2)); - - List auctions = auctionService.getAllAuctions(); - - assertThat(auctions).hasSize(2); - assertThat(auctions.get(0).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); - assertThat(auctions.get(1).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); - } - } - - @Nested - @DisplayName("경매 ID로 조회 테스트") - class GetAuctionByIdTests { - - @Test - @DisplayName("성공") - void testGetAuctionById_Success() { - Auction auction = createAuction(1L, seller, AuctionStatus.PENDING); - - when(auctionRepository.findAuctionById(1L)).thenReturn(Optional.of(auction)); - - AuctionResponse foundAuction = auctionService.getAuctionById(1L); - - assertThat(foundAuction.getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); - } - - @Test - @DisplayName("경매 미존재 예외") - void testGetAuctionById_AuctionNotFound() { - when(auctionRepository.findAuctionById(1L)).thenReturn(Optional.empty()); - - AuctionNotFoundException exception = assertThrows(AuctionNotFoundException.class, () -> - auctionService.getAuctionById(1L)); - - assertThat(exception.getMessage()).isEqualTo("Auction not found"); - } - } - - @Nested - @DisplayName("경매 유형이 []인 경매 목록 조회") - class GetAuctionsByAuctionCategoryTests { - @Test - @DisplayName("경매 유형 조회 : [성공]") - public void testGetAuctionsByAuctionCategory_Success() { - //given - AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(null, null, null, null, null, null, AuctionStatus.PENDING, null); - List auctions = List.of( - createAuction(1L, seller, AuctionStatus.PENDING), - createAuction(2L, seller, AuctionStatus.PENDING) - ); - - Pageable pageable = PageRequest.of(0, 10); - Page auctionPage = new PageImpl<>(auctions, pageable, auctions.size()); - when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); - - //when - Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); - - //then - assertThat(actualPage.getContent()).hasSize(2); - assertThat(actualPage.getContent()) - .allMatch(auctionResponseDetail -> auctionResponseDetail.getStatus().equals(AuctionStatus.PENDING.name())); - } - - @Test - @DisplayName("경매 유형 조회 : [성공] -Criteria 사용") - public void testGetAuctionsByAuctionCategory_Success_Criteria() { - //given - AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(null, null, null, null, null, null, AuctionStatus.PENDING, null); - List auctions = List.of( - createAuction(1L, seller, AuctionStatus.PENDING), - createAuction(2L, seller, AuctionStatus.PENDING) - ); - - Pageable pageable = PageRequest.of(0, 10); - Page auctionPage = new PageImpl<>(auctions, pageable, auctions.size()); - when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); - - //when - Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); - - //then - assertThat(actualPage.getContent()).hasSize(2); - assertThat(actualPage.getContent()) - .allMatch(auctionResponseDetail -> auctionResponseDetail.getStatus().equals(AuctionStatus.PENDING.name())); - } - - @Test - @DisplayName("경매 유형 조회 : [결과 없음]") - public void testGetAuctionsByAuctionCategory_AuctionNotFound() { - //given - List emptyAuctionList = List.of(); - AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_FREE, null, null, null, null, null, null, null); - - Pageable pageable = PageRequest.of(0, 10); - Page auctionPage = new PageImpl<>(emptyAuctionList, pageable, 0); - when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); - - //when - Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); - - //then - assertThat(actualPage.isEmpty()).isTrue(); - } - - @Test - @DisplayName("경매 유형 조회 : [결과 없음] - Criteria 사용") - public void testGetAuctionsByAuctionCategory_AuctionNotFound_Criteria() { - //given - List emptyAuctionList = List.of(); - AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_FREE, null, null, null, null, null, null, null); - - Pageable pageable = PageRequest.of(0, 10); - Page auctionPage = new PageImpl<>(emptyAuctionList, pageable, 0); - when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); - - //when - Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); - - //then - assertThat(actualPage.isEmpty()).isTrue(); - } - } - -} diff --git a/src/test/java/com/tasksprints/auction/domain/product/ProductRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/product/ProductRepositoryTest.java deleted file mode 100644 index 2972ffef..00000000 --- a/src/test/java/com/tasksprints/auction/domain/product/ProductRepositoryTest.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.tasksprints.auction.domain.product; - -import com.tasksprints.auction.common.config.QueryDslConfig; -import com.tasksprints.auction.domain.auction.dto.request.AuctionRequest; -import com.tasksprints.auction.domain.auction.dto.response.AuctionResponse; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.product.model.Product; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import com.tasksprints.auction.domain.product.model.ProductImage; -import com.tasksprints.auction.domain.product.repository.ProductRepository; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@DataJpaTest -@Import(QueryDslConfig.class) -public class ProductRepositoryTest { - - @Autowired - private ProductRepository productRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private AuctionRepository auctionRepository; - - private User owner; - private Auction auction; - - @BeforeEach - public void setUp() { - owner = createUser("testUser", "test", "test@naver.com"); - auction = createAuction(owner, BigDecimal.ONE, LocalDateTime.now(), LocalDateTime.now().plusHours(1)); - Product product = createProduct("sample product1", "description", owner, auction); - productRepository.save(product); - } - - @Test - @DisplayName("Find product by auction ID") - public void testFindByAuctionId() { - Optional foundProduct = productRepository.findByAuctionId(auction.getId()); - - assertTrue(foundProduct.isPresent()); - assertThat(foundProduct.get().getAuction().getId()).isEqualTo(auction.getId()); - } - - @Test - @DisplayName("Find all products by user ID") - public void testFindAllByUserId() { - List products = productRepository.findByOwnerId(owner.getId()); - - assertThat(products).isNotEmpty(); - assertThat(products.get(0).getOwner().getId()).isEqualTo(owner.getId()); - } - - @Test - @DisplayName("Save a new product") - public void testSaveProduct() { - Auction auction2 = createAuction(owner, BigDecimal.ONE, LocalDateTime.now(), LocalDateTime.now().plusHours(1)); - Product newProduct = createProduct("Another Product", "this is another product", owner, auction2); - - Product savedProduct = productRepository.save(newProduct); - - assertThat(savedProduct).isNotNull(); - assertThat(savedProduct.getId()).isNotNull(); - assertThat(savedProduct.getName()).isEqualTo("Another Product"); - } - - @Test - @DisplayName("Find product by ID") - public void testFindById() { - Product product = productRepository.findAll().get(0); // Use first product for testing - Optional foundProduct = productRepository.findById(product.getId()); - - assertTrue(foundProduct.isPresent()); - assertThat(foundProduct.get().getId()).isEqualTo(product.getId()); - } - - @Test - @DisplayName("Delete product by ID") - public void testDeleteById() { - Product product = productRepository.findAll().get(0); // Use first product for testing - productRepository.deleteById(product.getId()); - - Optional deletedProduct = productRepository.findById(product.getId()); - assertTrue(deletedProduct.isEmpty()); - } - @Deprecated - @Test - @DisplayName("queryDSL 검색 필터로 조회") - public void testFindAllUsingProductCategory() { - //given - Auction auction1 = createAuction(owner, AuctionStatus.PENDING); - Auction auction2 = createAuction(owner, AuctionStatus.ACTIVE); - - Product product1 = createProduct("product1", "description1", ProductCategory.TV, owner, auction1); - Product product2 = createProduct("product2", "description2", ProductCategory.DSLR, owner, auction2); - - product1.addAuction(auction1); - product2.addAuction(auction2); - - AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition( - null, ProductCategory.TV, null, null, null, null, AuctionStatus.PENDING, null - ); - Pageable pageable = PageRequest.of(0, 10); - - //when - Page auctionsByFilters = auctionRepository.getAuctionsByFilters(pageable, condition); - - //then - assertThat(auctionsByFilters).hasSize(1); - assertThat(auctionsByFilters.getContent().get(0).getProduct().getCategory()).isEqualTo(ProductCategory.TV); - assertThat(auctionsByFilters.getContent().get(0).getAuctionStatus()).isEqualTo(AuctionStatus.PENDING); - - - } - @Deprecated - @Test - @DisplayName("상품 카테고리로 경매를 조회 : Condition 쿼리스트링이 넘어온 경우") - public void testFindAllUsingConditionFilter() { - //given - Auction auction1 = createAuction(owner, AuctionStatus.ACTIVE); - Auction auction2 = createAuction(owner, AuctionStatus.PENDING); - - Product product1 = createProduct("product1", "description1", ProductCategory.TV, owner, auction1); - Product product2 = createProduct("product2", "description2", ProductCategory.DSLR, owner, auction2); - - product1.addAuction(auction1); - product2.addAuction(auction2); - - Pageable pageable = PageRequest.of(0, 10); - - AuctionRequest.SearchCondition searchCondition = new AuctionRequest.SearchCondition(null, null, null, - null, null, null, - AuctionStatus.PENDING, null) - ; - //when - Page auctionsByCategory = auctionRepository.getAuctionsByCategory(pageable, searchCondition, ProductCategory.DSLR); - - //then - assertThat(auctionsByCategory).hasSize(1); - assertThat(auctionsByCategory.getContent().get(0).getProduct().getCategory()).isEqualTo(ProductCategory.DSLR); - } - - - // Helper methods to minimize code duplication - private User createUser(String name, String nickName, String email) { - User user = User.builder() - .name(name) - .nickName(nickName) - .password("password") // Use a consistent password for all users - .email(email) - .build(); - return userRepository.save(user); - } - private Auction createAuction(User owner, BigDecimal startingBid, LocalDateTime startTime, LocalDateTime endTime) { - Auction auction = Auction.builder() - .startingBid(startingBid) - .startTime(startTime) - .endTime(endTime) - .auctionStatus(AuctionStatus.ACTIVE) - .auctionCategory(AuctionCategory.PRIVATE_FREE) - .seller(owner) - .build(); - return auctionRepository.save(auction); - } - - private Auction createAuction(User owner, AuctionStatus auctionStatus) { - Auction auction = Auction.builder() - .startingBid(BigDecimal.valueOf(100.00)) - .startTime(LocalDateTime.now()) - .endTime(LocalDateTime.now().plusHours(1)) - .auctionStatus(auctionStatus) - .auctionCategory(AuctionCategory.PRIVATE_FREE) - .seller(owner) - .build(); - return auctionRepository.save(auction); - } - - private Product createProduct(String name, String description, User owner, Auction auction) { - Product product = Product.builder() - .name(name) - .description(description) - .owner(owner) - .auction(auction) - .build(); - return productRepository.save(product); - } - - private Product createProduct(String name, String description, ProductCategory category, User owner, Auction auction) { - Product product = Product.builder() - .name(name) - .description(description) - .owner(owner) - .auction(auction) - .category(category) - .build(); - return productRepository.save(product); - } -} diff --git a/src/test/java/com/tasksprints/auction/domain/product/ProductServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/product/ProductServiceImplTest.java deleted file mode 100644 index b751f060..00000000 --- a/src/test/java/com/tasksprints/auction/domain/product/ProductServiceImplTest.java +++ /dev/null @@ -1,220 +0,0 @@ -package com.tasksprints.auction.domain.product; - -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.product.dto.request.ProductRequest; -import com.tasksprints.auction.domain.product.dto.response.ProductResponse; -import com.tasksprints.auction.domain.product.exception.ProductNotFoundException; -import com.tasksprints.auction.domain.product.model.Product; -import com.tasksprints.auction.domain.product.model.ProductCategory; -import com.tasksprints.auction.domain.product.repository.ProductImageRepository; -import com.tasksprints.auction.domain.product.repository.ProductRepository; -import com.tasksprints.auction.domain.product.service.ProductServiceImpl; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import org.junit.jupiter.api.*; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.math.BigDecimal; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -public class ProductServiceImplTest { - - private static final String UPLOADS_DIR = "src/main/resources/static/uploads/thumbnails/"; - @InjectMocks - private ProductServiceImpl productService; - @Mock - private ProductRepository productRepository; - @Mock - private ProductImageRepository productImageRepository; - @Mock - private UserRepository userRepository; - @Mock - private AuctionRepository auctionRepository; - private User user; - private Auction auction; - private Product product; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - user = User.builder() - .id(1L) - .name("testUser") - .nickName("test") - .password("password") - .email("test@naver.com") - .build(); - auction = Auction.builder() - .id(1L) - .startTime(LocalDateTime.now()) - .endTime(LocalDateTime.now().plusHours(1)) - .auctionStatus(AuctionStatus.ACTIVE) - .startingBid(BigDecimal.ONE) - .auctionCategory(AuctionCategory.PRIVATE_FREE) - .seller(user) - .build(); - product = Product.builder() - .id(1L) - .name("Test Product") - .description("Description") - .category(ProductCategory.fromDisplayName("여성의류")) - .owner(user) - .auction(auction) - .build(); - } - - @Nested - @DisplayName("제품 등록 기능") - class RegisterTests { - @AfterEach - public void cleanUp() throws IOException { - deleteAllImagesInDirectory(UPLOADS_DIR); - } - - private void deleteAllImagesInDirectory(String directoryPath) throws IOException { - Path dirPath = Paths.get(directoryPath); - - if (Files.exists(dirPath) && Files.isDirectory(dirPath)) { - try (var paths = Files.walk(dirPath)) { - paths.filter(Files::isRegularFile) // 정규 파일만 필터링 - .forEach(filePath -> { - try { - Files.delete(filePath); // 파일 삭제 - System.out.println("Deleted: " + filePath); // 삭제된 파일 로그 출력 - } catch (IOException e) { - System.err.println("Error deleting file: " + filePath + " - " + e.getMessage()); // 삭제 실패 로그 출력 - } - }); - } - } else { - System.out.println("Directory not found: " + directoryPath); // 디렉토리 없음 로그 출력 - } - } - - @Test - @DisplayName("새로운 제품 등록") - public void testRegister() { - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); - when(productRepository.save(any(Product.class))).thenReturn(product); - when(productImageRepository.saveAll(anyList())).thenReturn(Collections.emptyList()); // Mock 설정 추가 - - ProductRequest.Register request = new ProductRequest.Register("Test Product", "Description", "여성의류"); - - ProductResponse createdProductResponse = productService.register(1L, 1L, request, createMockImages()); - - assertEquals(createdProductResponse.getName(), "Test Product"); - verify(productRepository).save(any(Product.class)); - } - - @Test - @DisplayName("등록 시 사용자 미발견 예외 발생") - public void testRegisterUserNotFound() { - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - ProductRequest.Register request = new ProductRequest.Register("Test Product", "Description", "여성의류"); - - assertThrows(UserNotFoundException.class, () -> productService.register(1L, 1L, request, createMockImages())); - } - - @Test - @DisplayName("등록 시 경매 미발견 예외 발생") - public void testRegisterAuctionNotFound() { - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(auctionRepository.findById(1L)).thenReturn(Optional.empty()); - - ProductRequest.Register request = new ProductRequest.Register("Test Product", "Description", "여성의류"); - - assertThrows(RuntimeException.class, () -> productService.register(1L, 1L, request, createMockImages())); - } - - private List createMockImages() { - MockMultipartFile image1 = new MockMultipartFile("image1", "test1.jpg", "image/jpeg", "test image content 1".getBytes()); - MockMultipartFile image2 = new MockMultipartFile("image2", "test2.jpg", "image/jpeg", "test image content 2".getBytes()); - return List.of(image1, image2); // 이미지 리스트 반환 - } - - } - - @Nested - @DisplayName("제품 조회 기능") - class GetTests { - - @Test - @DisplayName("사용자 ID로 제품 조회") - public void testGetProductsByUserId() { - when(productRepository.findByOwnerId(1L)).thenReturn(Collections.singletonList(product)); - - List products = productService.getProductsByUserId(1L); - - assertEquals(products.size(), 1); - assertEquals(products.get(0).getName(), "Test Product"); - } - - @Test - @DisplayName("경매 ID로 제품 조회") - public void testGetProductByAuctionId() { - when(productRepository.findByAuctionId(1L)).thenReturn(Optional.of(product)); - - ProductResponse foundProductResponse = productService.getProductByAuctionId(1L); - - assertEquals(foundProductResponse.getName(), "Test Product"); - } - - @Test - @DisplayName("경매 ID로 제품 미발견 시 예외 발생") - public void testGetProductByAuctionIdNotFound() { - when(productRepository.findByAuctionId(1L)).thenReturn(Optional.empty()); - - assertThrows(ProductNotFoundException.class, () -> productService.getProductByAuctionId(1L)); - } - } - - @Nested - @DisplayName("제품 수정 기능") - class UpdateTests { - - @Test - @DisplayName("수정할 제품 미발견 시 예외 발생") - public void testUpdateProductNotFound() { - ProductRequest.Update updateRequest = new ProductRequest.Update(1L, "Updated Product", "Updated Description"); - - when(productRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(ProductNotFoundException.class, () -> productService.update(updateRequest)); - } - - @Test - @DisplayName("기존 제품 수정") - public void testUpdateProduct() { - when(productRepository.findById(1L)).thenReturn(Optional.of(product)); - when(productRepository.save(any(Product.class))).thenReturn(product); - - ProductRequest.Update updateRequest = new ProductRequest.Update(1L, "Updated Product", "Updated Description"); - - ProductResponse updatedProductResponse = productService.update(updateRequest); - - assertEquals(updatedProductResponse.getName(), "Updated Product"); - verify(productRepository).save(any(Product.class)); - } - } -} diff --git a/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java deleted file mode 100644 index 45f64ea9..00000000 --- a/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.tasksprints.auction.domain.user; - -import com.tasksprints.auction.common.config.QueryDslConfig; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; - -import java.util.Optional; - -/** - * [CRUD TEST] UserRepositoryTest - */ -@DataJpaTest -@Import(QueryDslConfig.class) -@Slf4j -public class UserRepositoryTest { - - @Autowired - private UserRepository userRepository; - - private User user; - - @BeforeEach - void setUp() { - user = User.builder() - .name("testUser") - .nickName("testNick") - .password("testPassword") - .email("test@example.com") - .build(); - } - - @DisplayName("findById 테스트") - @Test - void findUserById() { - User createdUser = userRepository.save(user); - User foundUser = userRepository.findById(createdUser.getId()).orElse(null); - - Assertions.assertNotNull(foundUser); - Assertions.assertEquals(createdUser.getId(), foundUser.getId()); - Assertions.assertEquals("testUser", foundUser.getName()); - Assertions.assertEquals("testNick", foundUser.getNickName()); - Assertions.assertEquals("test@example.com", foundUser.getEmail()); - - log.info("Found User: {}", foundUser); - } - - @DisplayName("delete 테스트") - @Test - void deleteUser() { - User createdUser = userRepository.save(user); - Long userId = createdUser.getId(); - - userRepository.deleteById(userId); - - Optional deletedUser = userRepository.findById(userId); - - Assertions.assertTrue(deletedUser.isEmpty(), "User should be deleted"); - log.info("User with ID {} deleted", userId); - } - - @DisplayName("save 테스트") - @Nested - class SaveTest { - @DisplayName("유저 업데이트 테스트") - @Test - void updateUser() { - User createdUser = userRepository.save(user); - createdUser.update("updatedName", "updatedPassword", "updatedNickName"); - User updatedUser = userRepository.save(createdUser); - - Assertions.assertNotNull(updatedUser); - Assertions.assertEquals("updatedName", updatedUser.getName()); - Assertions.assertEquals("updatedNickName", updatedUser.getNickName()); - Assertions.assertEquals("updatedPassword", updatedUser.getPassword()); - - log.info("Updated User: {}", updatedUser); - } - - @DisplayName("유저 생성 테스트") - @Test - void createUser() { - User createdUser = userRepository.save(user); - - Assertions.assertNotNull(createdUser); - Assertions.assertNotNull(createdUser.getId()); - Assertions.assertEquals("testUser", createdUser.getName()); - Assertions.assertEquals("testNick", createdUser.getNickName()); - Assertions.assertEquals("test@example.com", createdUser.getEmail()); - - log.info("Created User: {}", createdUser); - } - } -} diff --git a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java deleted file mode 100644 index 95c78348..00000000 --- a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.tasksprints.auction.domain.user; - -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import com.tasksprints.auction.domain.user.service.UserServiceImpl; -import org.junit.jupiter.api.*; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -import static org.mockito.Mockito.*; - -public class UserServiceImplTest { - - @Mock - private UserRepository userRepository; - - @InjectMocks - private UserServiceImpl userService; - - private User existingUser; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - existingUser = User.builder() - .id(1L) - .name("testUser") - .nickName("testNick") - .password("testPassword") - .email("test@example.com") - .build(); - } - - @Nested - @DisplayName("Create User") - class CreateUserTests { - - @Test - @DisplayName("should create a new user") - void shouldCreateNewUser() { - // Arrange - UserRequest.Register request = new UserRequest.Register("testUser", "test@example.com", "testPassword", "testNick"); - when(userRepository.save(any(User.class))).thenReturn(existingUser); - - // Act - UserDetailResponse createdUser = userService.createUser(request); - - // Assert - Assertions.assertNotNull(createdUser); - Assertions.assertEquals("testUser", createdUser.getName()); - Assertions.assertEquals("test@example.com", createdUser.getEmail()); - Assertions.assertEquals("testNick", createdUser.getNickName()); - verify(userRepository, times(1)).save(any(User.class)); - } - } - - @Nested - @DisplayName("Get User By ID") - class GetUserByIdTests { - - @Test - @DisplayName("should return user when found") - void shouldReturnUserWhenFound() { - // Arrange - when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser)); - - // Act - UserDetailResponse user = userService.getUserDetailsById(1L); - - // Assert - Assertions.assertNotNull(user); - Assertions.assertEquals(existingUser.getId(), user.getId()); - Assertions.assertEquals(existingUser.getName(), user.getName()); - verify(userRepository, times(1)).findById(1L); - } - - @Test - @DisplayName("should throw exception when user not found") - void shouldThrowExceptionWhenUserNotFound() { - // Arrange - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - // Act & Assert - Assertions.assertThrows(UserNotFoundException.class, () -> userService.getUserDetailsById(1L)); - verify(userRepository, times(1)).findById(1L); - } - } - - @Nested - @DisplayName("Update User") - class UpdateUserTests { - - @Test - @DisplayName("should update user details") - void shouldUpdateUserDetails() { - // Arrange - UserRequest.Update request = new UserRequest.Update("updatedName", "updatedPassword", "updatedNick"); - when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser)); - when(userRepository.save(any(User.class))).thenReturn(existingUser); - - // Act - UserDetailResponse updatedUser = userService.updateUser(1L, request); - - // Assert - Assertions.assertNotNull(updatedUser); - Assertions.assertEquals("updatedName", updatedUser.getName()); - Assertions.assertEquals("updatedPassword", updatedUser.getPassword()); - Assertions.assertEquals("updatedNick", updatedUser.getNickName()); - verify(userRepository, times(1)).findById(1L); - verify(userRepository, times(1)).save(any(User.class)); - } - - @Test - @DisplayName("should throw exception when user not found") - void shouldThrowExceptionWhenUserNotFound() { - // Arrange - UserRequest.Update request = new UserRequest.Update("updatedName", null, null); - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - // Act & Assert - Assertions.assertThrows(UserNotFoundException.class, () -> userService.updateUser(1L, request)); - verify(userRepository, times(1)).findById(1L); - verify(userRepository, never()).save(any(User.class)); - } - } - - @Nested - @DisplayName("Delete User") - class DeleteUserTests { - - @Test - @DisplayName("should delete user when found") - void shouldDeleteUserWhenFound() { - // Arrange - User existingUser = User.builder() - .id(1L) - .name("testUser") - .nickName("testNick") - .password("testPassword") - .email("test@example.com") - .build(); - when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser)); - when(userRepository.save(any(User.class))).thenReturn(existingUser); - - // Act - userService.deleteUser(1L); - - // Assert - verify(userRepository, times(1)).findById(1L); - verify(userRepository, times(1)).save(existingUser); - - // Verify that the user's deletedAt field was updated correctly - Assertions.assertNotNull(existingUser.getDeletedAt()); - } - - @Test - @DisplayName("should throw exception when user not found") - void shouldThrowExceptionWhenUserNotFound() { - // Arrange - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - // Act & Assert - Assertions.assertThrows(UserNotFoundException.class, () -> userService.deleteUser(1L)); - verify(userRepository, times(1)).findById(1L); - verify(userRepository, never()).delete(any(User.class)); - } - } -} From 1b83f20f2a1ae5f0158ef7ecd870d04465aea8e6 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:19:17 +0900 Subject: [PATCH 28/48] =?UTF-8?q?refactor(auction)=20:=20auction=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20DDD=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AuctionServiceImpl.java | 126 ++++++ .../domain/dto/request/AuctionRequest.java | 40 ++ .../domain/dto/response/AuctionResponse.java | 84 ++++ .../dto/response/AuctionStatusResponse.java | 8 + .../domain/entity/AuctionCategory.java | 18 + .../auction/domain/entity/AuctionStatus.java | 17 + .../domain/service/AuctionService.java | 31 ++ .../AuctionAlreadyClosedException.java | 7 + .../exception/AuctionEndedException.java | 7 + .../exception/AuctionNotFoundException.java | 7 + .../infrastructure/AuctionRepository.java | 22 + .../support/AuctionCriteriaRepository.java | 17 + .../AuctionCriteriaRepositoryImpl.java | 124 ++++++ .../presentation/AuctionController.java | 149 +++++++ .../application/AuctionServiceImplTest.java | 406 ++++++++++++++++++ .../infrastructure/AuctionRepositoryTest.java | 186 ++++++++ .../presentation/AuctionControllerTest.java | 215 ++++++++++ 17 files changed, 1464 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/dto/request/AuctionRequest.java create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionStatusResponse.java create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionCategory.java create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionStatus.java create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java create mode 100644 src/main/java/com/tasksprints/auction/auction/exception/AuctionAlreadyClosedException.java create mode 100644 src/main/java/com/tasksprints/auction/auction/exception/AuctionEndedException.java create mode 100644 src/main/java/com/tasksprints/auction/auction/exception/AuctionNotFoundException.java create mode 100644 src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java create mode 100644 src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepository.java create mode 100644 src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java create mode 100644 src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java create mode 100644 src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java create mode 100644 src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java create mode 100644 src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java diff --git a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java new file mode 100644 index 00000000..1a51b3d4 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java @@ -0,0 +1,126 @@ +package com.tasksprints.auction.auction.application.service; + +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.auction.domain.service.AuctionService; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.auction.exception.AuctionAlreadyClosedException; +import com.tasksprints.auction.auction.exception.AuctionNotFoundException; +import com.tasksprints.auction.auction.exception.InvalidAuctionTimeException; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AuctionServiceImpl implements AuctionService { + private final UserRepository userRepository; + private final AuctionRepository auctionRepository; + + @Override + public AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionRequest) { + User seller = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found")); + if (auctionRequest.getStartTime().isAfter(auctionRequest.getEndTime())) { + throw new InvalidAuctionTimeException("End time must be after start time"); + } + + Auction newAuction = Auction.create( + auctionRequest.getStartTime(), + auctionRequest.getEndTime(), + auctionRequest.getStartingBid(), + auctionRequest.getAuctionCategory(), + auctionRequest.getAuctionStatus(), + seller + ); + + Auction savedAuction = auctionRepository.save(newAuction); + /** + * Product 생성에 대한 부분 고려 필요 + * STEP 1 + * - S3 버킷에 올리는 api 따로 구성 ( 독립적 시행 ) url 반환 + * - 해당 url 을 토대로 Auction 생성 시, Product 도 같이 생성 + * STEP 2 + * - 각각의 기능을 완전 분리 + */ + return AuctionResponse.of(savedAuction); + } + + @Override + public void closeAuction(Long auctionId) { + Auction auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); + + if (auction.getAuctionStatus() == AuctionStatus.CLOSED) { + throw new AuctionAlreadyClosedException("Auction is already closed"); + } + auction.setAuctionStatus(AuctionStatus.CLOSED); + } + + @Override + public String getAuctionStatus(Long auctionId) { + Auction auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); + + return auction.getAuctionStatus().name(); + } + + @Override + public List getAuctionsByUser(Long userId) { + User seller = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found")); + + List foundAuctions = auctionRepository.findAuctionsByUserId(seller.getId()); + + return foundAuctions.stream() + .map(AuctionResponse::of) + .toList(); + } + + + @Override + public List getAllAuctions() { + List foundAuctions = auctionRepository.findAll(); + return foundAuctions.stream() + .map(AuctionResponse::of) + .toList(); + } + + @Override + public AuctionResponse getAuctionById(Long auctionId) { + Auction foundAuction = auctionRepository.findAuctionById(auctionId) + .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); + foundAuction.incrementViewCount(); + auctionRepository.save(foundAuction); + return AuctionResponse.of(foundAuction); + } + + /** + * NULL POINTER EXCEPTION 발생 + * NULL 안정성 보장을 해줬음 + **/ + @Override + public Page getAuctionsByFilter(Pageable pageable, AuctionRequest.SearchCondition searchCondition) { + Page auctions = auctionRepository.getAuctionsByFilters(pageable, searchCondition); + return auctions.map(AuctionResponse.Details::of); + } + + @Deprecated + @Override + public Page getAuctionsByProductCategory(Pageable pageable, AuctionRequest.SearchCondition searchCondition, ProductCategory category) { + Page auctions = auctionRepository.getAuctionsByFilters(pageable, searchCondition); + return auctions.map(AuctionResponse.Details::of); + } + +} diff --git a/src/main/java/com/tasksprints/auction/auction/domain/dto/request/AuctionRequest.java b/src/main/java/com/tasksprints/auction/auction/domain/dto/request/AuctionRequest.java new file mode 100644 index 00000000..f72a00ad --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/dto/request/AuctionRequest.java @@ -0,0 +1,40 @@ +package com.tasksprints.auction.auction.domain.dto.request; + +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public class AuctionRequest { + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Create { + private LocalDateTime startTime; + private LocalDateTime endTime; + private BigDecimal startingBid; + private AuctionCategory auctionCategory; + private AuctionStatus auctionStatus; + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class SearchCondition { + private AuctionCategory auctionCategory; + private ProductCategory productCategory; + private LocalDateTime startTime; + private LocalDateTime endTime; + private BigDecimal minPrice; + private BigDecimal maxPrice; + private AuctionStatus auctionStatus; + private String sortBy; + + } +} diff --git a/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java b/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java new file mode 100644 index 00000000..25254c70 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java @@ -0,0 +1,84 @@ +package com.tasksprints.auction.auction.domain.dto.response; + +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.product.domain.entity.ProductImage; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AuctionResponse { + private Long id; + private LocalDateTime startTime; + private LocalDateTime endTime; + private String category; + private String status; + private BigDecimal startingBid; + private Long viewCount; + private Long sellerId; + private String sellerNickName; + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Details { + private Long id; + private LocalDateTime startTime; + private LocalDateTime endTime; + private String category; + private String status; + private BigDecimal startingBid; + private Long viewCount; + private Long sellerId; + private String sellerNickName; + private Long productId; + private String productCategory; + @Setter + private List productImageUrls; + + public static AuctionResponse.Details of(Auction auction) { + return AuctionResponse.Details.builder() + .id(auction.getId()) + .startTime(auction.getStartTime()) + .endTime(auction.getEndTime()) + .category(auction.getAuctionCategory().name()) + .status(auction.getAuctionStatus().name()) + .startingBid(auction.getStartingBid()) + .viewCount(auction.getViewCount()) + .sellerId(auction.getSeller() != null ? auction.getSeller().getId() : null) + .sellerNickName(auction.getSeller() != null ? auction.getSeller().getNickName() : null) + .productId(auction.getProduct() != null ? auction.getProduct().getId() : null) + .productCategory(auction.getProduct() != null && auction.getProduct().getCategory() != null ? auction.getProduct().getCategory().name() : null) + .productImageUrls(auction.getProduct() != null && auction.getProduct().getProductImageList() != null + ? auction.getProduct().getProductImageList().stream() + .map(ProductImage::getImageUrl) + .collect(toList()) + : Collections.emptyList()) + .build(); + } + } + public static AuctionResponse of(Auction auction) { + return AuctionResponse.builder() + .id(auction.getId()) + .startTime(auction.getStartTime()) + .endTime(auction.getEndTime()) + .category(auction.getAuctionCategory().name()) + .status(auction.getAuctionStatus().name()) + .startingBid(auction.getStartingBid()) + .viewCount(auction.getViewCount()) + .sellerId(auction.getSeller().getId()) + .sellerNickName(auction.getSeller().getNickName()) + .build(); + } + + +} diff --git a/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionStatusResponse.java b/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionStatusResponse.java new file mode 100644 index 00000000..d11fd091 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionStatusResponse.java @@ -0,0 +1,8 @@ +package com.tasksprints.auction.auction.domain.dto.response; + +import lombok.Data; + +@Data +public class AuctionStatusResponse { + private String status; +} diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionCategory.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionCategory.java new file mode 100644 index 00000000..28189d11 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionCategory.java @@ -0,0 +1,18 @@ +package com.tasksprints.auction.auction.domain.entity; + +public enum AuctionCategory { + PRIVATE_FREE, + PUBLIC_FREE, + PRIVATE_PAID, + PUBLIC_PAID; + + + public static AuctionCategory fromDisplayName(String auctionCategory) { + for (AuctionCategory category : values()) { + if (category.name().equalsIgnoreCase(auctionCategory)) { + return category; + } + } + throw new IllegalArgumentException("Unknown auctionCategory: " + auctionCategory); + } +} diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionStatus.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionStatus.java new file mode 100644 index 00000000..c9d4a5b8 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/AuctionStatus.java @@ -0,0 +1,17 @@ +package com.tasksprints.auction.auction.domain.entity; + +public enum AuctionStatus { + PENDING, + ACTIVE, + CLOSED, + CANCELED; + + public static AuctionStatus fromDisplayName(String auctionStatus) { + for (AuctionStatus status : values() ) { + if (status.name().equalsIgnoreCase(auctionStatus)) { + return status; + } + } + throw new IllegalArgumentException("Unknown auctionStatus: " + auctionStatus); + } +} diff --git a/src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java b/src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java new file mode 100644 index 00000000..94105e3b --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java @@ -0,0 +1,31 @@ +package com.tasksprints.auction.auction.domain.service; + +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * 사용자가 맞는지 판단도 해야함. + */ +public interface AuctionService { + AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionRequest); + + void closeAuction(Long auctionId); + + String getAuctionStatus(Long auctionId); + + List getAuctionsByUser(Long userId); + + List getAllAuctions(); + + AuctionResponse getAuctionById(Long auctionId); + + Page getAuctionsByFilter(Pageable pageable, AuctionRequest.SearchCondition searchCondition); + + @Deprecated + Page getAuctionsByProductCategory(Pageable pageable, AuctionRequest.SearchCondition searchCondition, ProductCategory category); +} diff --git a/src/main/java/com/tasksprints/auction/auction/exception/AuctionAlreadyClosedException.java b/src/main/java/com/tasksprints/auction/auction/exception/AuctionAlreadyClosedException.java new file mode 100644 index 00000000..d01fe22a --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/exception/AuctionAlreadyClosedException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.auction.exception; + +public class AuctionAlreadyClosedException extends RuntimeException { + public AuctionAlreadyClosedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/auction/exception/AuctionEndedException.java b/src/main/java/com/tasksprints/auction/auction/exception/AuctionEndedException.java new file mode 100644 index 00000000..26766e96 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/exception/AuctionEndedException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.auction.exception; + +public class AuctionEndedException extends RuntimeException { + public AuctionEndedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/auction/exception/AuctionNotFoundException.java b/src/main/java/com/tasksprints/auction/auction/exception/AuctionNotFoundException.java new file mode 100644 index 00000000..08fdb375 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/exception/AuctionNotFoundException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.auction.exception; + +public class AuctionNotFoundException extends RuntimeException { + public AuctionNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java b/src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java new file mode 100644 index 00000000..76f82e81 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java @@ -0,0 +1,22 @@ +package com.tasksprints.auction.auction.infrastructure; + + +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.infrastructure.support.AuctionCriteriaRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface AuctionRepository extends JpaRepository, AuctionCriteriaRepository { + + @Query("SELECT a FROM auction a WHERE a.seller.id = :userId") + List findAuctionsByUserId(@Param("userId") Long userId); + + @Query("SELECT a FROM auction a WHERE a.id = :auctionId") + Optional findAuctionById(@Param("auctionId") Long auctionId); + +} + diff --git a/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepository.java b/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepository.java new file mode 100644 index 00000000..ac3ecacb --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepository.java @@ -0,0 +1,17 @@ +package com.tasksprints.auction.auction.infrastructure.support; + +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.entity.Auction; + +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + + +public interface AuctionCriteriaRepository { + Page getAuctionsByFilters(Pageable pageable, AuctionRequest.SearchCondition searchCondition); + @Deprecated + Page getAuctionsByCategory(Pageable pageable, + AuctionRequest.SearchCondition searchCondition, + ProductCategory category); +} diff --git a/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java b/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java new file mode 100644 index 00000000..1e59c708 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java @@ -0,0 +1,124 @@ +package com.tasksprints.auction.auction.infrastructure.support; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.tasksprints.auction.auction.domain.entity.QAuction.auction; +import static com.tasksprints.auction.product.domain.entity.QProduct.product; +import static com.tasksprints.auction.user.domain.entity.QUser.user; + +@RequiredArgsConstructor +@Repository +public class AuctionCriteriaRepositoryImpl implements AuctionCriteriaRepository { + private final JPAQueryFactory queryFactory; + + public Page getAuctionsByFilters(Pageable pageable, AuctionRequest.SearchCondition condition) { + BooleanBuilder builder = buildSearchCondition(condition); + OrderSpecifier sortOrder = getSortOrder(condition); + List result = buildQueryWithPaginationAndSorting(builder, pageable, sortOrder); + + // int 오버플로 주의 +// int total = queryFactory +// .selectFrom(auction) +// .where(builder) +// .fetch().size(); + + long total = result.size(); + + return new PageImpl<>(result, pageable, total); + } + + private List buildQueryWithPaginationAndSorting(BooleanBuilder builder, Pageable pageable, OrderSpecifier sortOrder) { + var mainQuery = queryFactory + .selectFrom(auction) + .leftJoin(auction.product, product) + .fetchJoin() + .leftJoin(auction.seller, user) + .fetchJoin() + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()); + + if (sortOrder != null) { + mainQuery.orderBy(sortOrder); + } + + return mainQuery.fetch(); + } + + @Deprecated + public Page getAuctionsByCategory(Pageable pageable, + AuctionRequest.SearchCondition condition, + ProductCategory category) { + + BooleanBuilder builder = buildSearchCondition(condition); + filterByCategory(category, builder); + OrderSpecifier sortOrder = getSortOrder(condition); + List result = buildQueryWithPaginationAndSorting(builder, pageable, sortOrder); + + int total = queryFactory + .selectFrom(auction) + .where(builder) + .fetch().size(); + + return new PageImpl<>(result, pageable, total); + + } + @Deprecated + private void filterByCategory(ProductCategory category, BooleanBuilder builder) { + if (category != null) { + builder.and(product.category.eq(category)); + } + } + + + private BooleanBuilder buildSearchCondition(AuctionRequest.SearchCondition condition) { + BooleanBuilder builder = new BooleanBuilder(); + + if (condition.getAuctionCategory() != null) { + builder.and(auction.auctionCategory.eq(condition.getAuctionCategory())); + } + if (condition.getProductCategory() != null) { + builder.and(product.category.eq(condition.getProductCategory())); + } + if (condition.getMinPrice() != null && condition.getMaxPrice() != null) { + builder.and(auction.startingBid.between(condition.getMinPrice(), condition.getMaxPrice())); + } + if (condition.getStartTime() != null && condition.getEndTime() != null) { + builder.and(auction.endTime.between(condition.getStartTime(), condition.getEndTime())); + } + if (condition.getAuctionStatus() != null) { + builder.and(auction.auctionStatus.eq(condition.getAuctionStatus())); + } + + return builder; + } + + private OrderSpecifier getSortOrder(AuctionRequest.SearchCondition condition) { + if (condition.getSortBy() != null) { + return switch (condition.getSortBy()) { + case "bidsAsc" -> auction.bids.size().asc(); + case "bidsDesc" -> auction.bids.size().desc(); + case "endTimeASC" -> auction.endTime.asc(); + case "startTimeASC" -> auction.startTime.asc(); + case "viewCountDESC" -> auction.viewCount.desc(); + + default -> null; + }; + } + return null; + } + + +} diff --git a/src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java b/src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java new file mode 100644 index 00000000..c1eac8d6 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java @@ -0,0 +1,149 @@ +package com.tasksprints.auction.auction.presentation; + +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; +import com.tasksprints.auction.auction.domain.service.AuctionService; +import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; +import com.tasksprints.auction.bid.domain.service.BidService; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; +import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; +import com.tasksprints.auction.review.domain.service.ReviewService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; + +@RestController +@RequestMapping("/api/v1/auction") +@RequiredArgsConstructor +@Tag(name = "Auction", description = "Operations related to auctions") +public class AuctionController { + private final AuctionService auctionService; + private final BidService bidService; + private final ReviewService reviewService; + + @PostMapping + @Operation(summary = "Create an auction", description = "Creates a new auction for a user.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Auction created successfully")}) + public ResponseEntity> createAuction(@Parameter(description = "ID of the user creating the auction") @RequestParam Long userId, @RequestBody AuctionRequest.Create auctionRequest) { + AuctionResponse createdAuction = auctionService.createAuction(userId, auctionRequest); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_CREATED_SUCCESS, createdAuction)); + } + + @PostMapping("/{auctionId}/close") + @Operation(summary = "Close an auction", description = "Closes the auction by its ID.") + @ApiResponse(responseCode = "200", description = "Auction closed successfully") + public ResponseEntity> closeAuction(@PathVariable Long auctionId) { + auctionService.closeAuction(auctionId); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_CLOSED_SUCCESS)); + } + + @GetMapping("/{auctionId}/status") + @Operation(summary = "Get auction status", description = "Retrieves the status of the auction.") + @ApiResponse(responseCode = "200", description = "Auction status retrieved successfully") + public ResponseEntity> getAuctionStatus(@PathVariable Long auctionId) { + String status = auctionService.getAuctionStatus(auctionId); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_STATUS_RETRIEVED, status)); + } +// 필터로 추가 + + +// @GetMapping("/") +// @Operation(summary = "Get auctions by user", description = "Retrieves all auctions created by a specific user.") +// @ApiResponse(responseCode = "200", description = "User auctions retrieved successfully") +// public ResponseEntity>> getAuctionsByUser(@RequestParam Long userId) { +// List userAuctions = auctionService.getAuctionsByUser(userId); +// return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_BY_USER_RETRIEVED, userAuctions)); +// } + + // @GetMapping +// @Operation(summary = "Get all auctions", description = "Retrieves all auctions.") +// @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") +// public ResponseEntity>> getAllAuctions(@RequestParam(required = false) AuctionRequest.AuctionCategoryParam auctionCategory) { +// List allAuctions; +// if (auctionCategory != null) { +// allAuctions = auctionService.getAuctionsByAuctionCategory(auctionCategory); +// } else { +// allAuctions = auctionService.getAllAuctions(); +// } +// return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED, allAuctions)); +// } + @GetMapping + @Operation(summary = "Get all auctions", description = "Retrieves all auctions.") + @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") + public ResponseEntity>> getAllAuctions(Pageable pageable, AuctionRequest.SearchCondition searchCondition) { + Page auctions = auctionService.getAuctionsByFilter(pageable, searchCondition); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED, auctions)); + } + + @GetMapping("/{auctionId}") + @Operation(summary = "Get auction by ID", description = "Retrieves auction details by its ID.") + @ApiResponse(responseCode = "200", description = "Auction retrieved successfully") + public ResponseEntity> getAuctionById(@PathVariable Long auctionId) { + AuctionResponse auction = auctionService.getAuctionById(auctionId); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_RETRIEVED, auction)); + } + + @Deprecated + @GetMapping("/category/{category}") + @Operation(summary = "Get auctions by ProductCategory", description = "Retrieve all auction by its ProductCategory.") + @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") + public ResponseEntity>> getAuctionByProductCategory(Pageable pageable, @PathVariable String category, AuctionRequest.SearchCondition searchCondition) { + Page auctions = auctionService.getAuctionsByProductCategory(pageable, searchCondition, ProductCategory.fromDisplayName(category)); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_RETRIEVED, auctions)); + } + + // Bid Endpoints + @PostMapping("/{auctionId}/bid") + @Operation(summary = "Submit a bid", description = "Submits a bid for the specified auction.") + @ApiResponse(responseCode = "200", description = "Bid submitted successfully") + public ResponseEntity> submitBid(@Parameter(description = "ID of the user submitting the bid") @RequestParam Long userId, @PathVariable Long auctionId, @Parameter(description = "Bid amount") @RequestParam BigDecimal amount) { + BidResponse bid = bidService.submitBid(userId, auctionId, amount); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_SUBMITTED_SUCCESS, bid)); + } + + @PutMapping("/{auctionId}/bid") + @Operation(summary = "Update a bid", description = "Updates the amount of an existing bid.") + @ApiResponse(responseCode = "200", description = "Bid updated successfully") + public ResponseEntity> updateBid(@Parameter(description = "ID of the user updating the bid") @RequestParam Long userId, @PathVariable Long auctionId, @Parameter(description = "New bid amount") @RequestParam BigDecimal amount) { + BidResponse updatedBid = bidService.updateBidAmount(userId, auctionId, amount); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_UPDATED_SUCCESS, updatedBid)); + } + + @GetMapping("/{auctionId}/bid/status") + @Operation(summary = "Check user bid status", description = "Checks if the user has already placed a bid on the auction.") + @ApiResponse(responseCode = "200", description = "Bid status checked successfully") + public ResponseEntity> checkUserBidStatus(@PathVariable Long auctionId, @Parameter(description = "ID of the user") @RequestParam Long userId) { + Boolean hasBidded = bidService.hasUserAlreadyBid(auctionId); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_STATUS_CHECKED, hasBidded)); + } + + // Review Endpoints + @PostMapping("/{auctionId}/review") + @Operation(summary = "Create a review", description = "Creates a review for a specific auction.") + @ApiResponse(responseCode = "200", description = "Review created successfully") + public ResponseEntity> createReview(@Parameter(description = "ID of the user creating the review") @RequestParam Long userId, @PathVariable Long auctionId, @RequestBody ReviewRequest.Create reviewRequest) { + ReviewResponse createdReview = reviewService.createReview(userId, auctionId, reviewRequest); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEW_CREATED_SUCCESS, createdReview)); + } + + + @GetMapping("/{auctionId}/review") + @Operation(summary = "Get review by auction ID", description = "Retrieves the review for a specific auction.") + @ApiResponse(responseCode = "200", description = "Review retrieved successfully") + public ResponseEntity> getReviewByAuctionId(@PathVariable Long auctionId) { + ReviewResponse review = reviewService.getReviewByAuctionId(auctionId); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEW_RETRIEVED, review)); + } +} diff --git a/src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java b/src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java new file mode 100644 index 00000000..4c190814 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java @@ -0,0 +1,406 @@ +package com.tasksprints.auction.domain.auction.service; + +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; +import com.tasksprints.auction.auction.exception.AuctionAlreadyClosedException; +import com.tasksprints.auction.auction.exception.AuctionNotFoundException; +import com.tasksprints.auction.auction.exception.InvalidAuctionTimeException; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.auction.application.service.AuctionServiceImpl; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AuctionServiceImplTest { + + @Mock + private UserRepository userRepository; + + @Mock + private AuctionRepository auctionRepository; + + @InjectMocks + private AuctionServiceImpl auctionService; + + private User seller; + + @BeforeEach + void setUp() { + seller = User.builder() + .id(1L) + .name("testUser") + .nickName("testNick") + .password("testPassword") + .email("test@example.com") + .build(); + } + + private Auction createAuction(Long auctionId, User seller, AuctionStatus status) { + return Auction.builder() + .id(auctionId) + .auctionCategory(AuctionCategory.PUBLIC_PAID) + .auctionStatus(status) + .startTime(LocalDateTime.of(2024, 8, 1, 10, 0)) + .endTime(LocalDateTime.of(2024, 9, 1, 10, 0)) + .startingBid(BigDecimal.valueOf(100.00)) + .seller(seller) + .build(); + } + + private Auction createAuction(Long auctionId, LocalDateTime startTime) { + return Auction.builder() + .id(auctionId) + .auctionCategory(AuctionCategory.PUBLIC_PAID) + .auctionStatus(AuctionStatus.ACTIVE) + .startTime(startTime) + .endTime(LocalDateTime.of(2024, 9, 1, 10, 0)) + .startingBid(BigDecimal.valueOf(100.00)) + .seller(seller) + .build(); + } + + @Nested + @DisplayName("경매 생성 테스트") + class CreateAuctionTests { + + @Test + @DisplayName("성공") + void testCreateAuction_Success() { + AuctionRequest.Create auctionRequest = new AuctionRequest.Create( + LocalDateTime.now(), + LocalDateTime.now().plusDays(7), + BigDecimal.valueOf(100.00), + AuctionCategory.PRIVATE_FREE, + AuctionStatus.ACTIVE + ); + + when(userRepository.findById(1L)).thenReturn(Optional.of(seller)); + when(auctionRepository.save(any(Auction.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AuctionResponse createdAuction = auctionService.createAuction(1L, auctionRequest); + + assertThat(createdAuction.getSellerId()).isEqualTo(seller.getId()); + assertThat(createdAuction.getCategory()).isEqualTo(AuctionCategory.PRIVATE_FREE.name()); + assertThat(createdAuction.getStatus()).isEqualTo(AuctionStatus.ACTIVE.name()); + + verify(auctionRepository).save(any(Auction.class)); + } + + @Test + @DisplayName("사용자 미존재 예외") + void testCreateAuction_UserNotFound() { + AuctionRequest.Create auctionRequest = new AuctionRequest.Create( + LocalDateTime.now(), + LocalDateTime.now().plusDays(7), + BigDecimal.valueOf(100.00), + AuctionCategory.PRIVATE_FREE, + AuctionStatus.ACTIVE + ); + + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> + auctionService.createAuction(1L, auctionRequest)); + + assertThat(exception.getMessage()).isEqualTo("User not found"); + verify(auctionRepository, never()).save(any(Auction.class)); + } + + @Test + @DisplayName("종료 시간이 시작 시간보다 이전인 경우 예외") + void testCreateAuction_EndTimeBeforeStartTime() { + AuctionRequest.Create auctionRequest = new AuctionRequest.Create( + LocalDateTime.now().plusDays(7), + LocalDateTime.now(), + BigDecimal.valueOf(100.00), + AuctionCategory.PRIVATE_FREE, + AuctionStatus.ACTIVE + ); + + when(userRepository.findById(1L)).thenReturn(Optional.of(seller)); + + InvalidAuctionTimeException exception = assertThrows(InvalidAuctionTimeException.class, () -> + auctionService.createAuction(1L, auctionRequest)); + + assertThat(exception.getMessage()).isEqualTo("End time must be after start time"); + verify(auctionRepository, never()).save(any(Auction.class)); + } + } + + @Nested + @DisplayName("경매 종료 테스트") + class CloseAuctionTests { + + @Test + @DisplayName("성공") + void testCloseAuction_Success() { +// Auction auction = createAuction(1L, seller, AuctionCategory.PRIVATE_FREE, AuctionStatus.ACTIVE); +// +// when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); +// when(auctionRepository.save(any(Auction.class))).thenAnswer(invocation -> invocation.getArgument(0)); +// +// Auctio closedAuction = auctionService.closeAuction(1L); +// +// assertThat(closedAuction.getAuctionStatus()).isEqualTo(AuctionStatus.CLOSED); +// verify(auctionRepository).save(auction); + } + + @Test + @DisplayName("경매 미존재 예외") + void testCloseAuction_AuctionNotFound() { + when(auctionRepository.findById(1L)).thenReturn(Optional.empty()); + + AuctionNotFoundException exception = assertThrows(AuctionNotFoundException.class, () -> + auctionService.closeAuction(1L)); + + assertThat(exception.getMessage()).isEqualTo("Auction not found"); + verify(auctionRepository, never()).save(any(Auction.class)); + } + + @Test + @DisplayName("이미 종료된 경매 예외") + void testCloseAuction_AuctionAlreadyClosed() { + Auction auction = Auction.builder() + .id(1L) + .auctionCategory(AuctionCategory.PRIVATE_FREE) + .auctionStatus(AuctionStatus.CLOSED) + .seller(seller) + .build(); + + when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); + + AuctionAlreadyClosedException exception = assertThrows(AuctionAlreadyClosedException.class, () -> + auctionService.closeAuction(1L)); + + assertThat(exception.getMessage()).isEqualTo("Auction is already closed"); + verify(auctionRepository, never()).save(any(Auction.class)); + } + } + + @Nested + @DisplayName("경매 상태 조회 테스트") + class GetAuctionStatusTests { + + @Test + @DisplayName("성공") + void testGetAuctionStatus_Success() { + Auction auction = createAuction(1L, seller, AuctionStatus.ACTIVE); + + when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); + + String auctionStatus = auctionService.getAuctionStatus(1L); + + assertThat(auctionStatus).isEqualTo(AuctionStatus.ACTIVE.toString()); + } + + @Test + @DisplayName("경매 미존재 예외") + void testGetAuctionStatus_AuctionNotFound() { + when(auctionRepository.findById(1L)).thenReturn(Optional.empty()); + + AuctionNotFoundException exception = assertThrows(AuctionNotFoundException.class, () -> + auctionService.getAuctionStatus(1L)); + + assertThat(exception.getMessage()).isEqualTo("Auction not found"); + } + } + + @Nested + @DisplayName("사용자별 경매 목록 조회 테스트") + class GetAuctionsByUserTests { + + @Test + @DisplayName("성공") + void testGetAuctionsByUser_Success() { + Auction auction1 = createAuction(1L, seller, AuctionStatus.PENDING); + Auction auction2 = createAuction(2L, seller, AuctionStatus.PENDING); + + // Mocking: 사용자 정보 반환 + when(userRepository.findById(1L)).thenReturn(Optional.of(seller)); + // Mocking: 경매 목록 반환 + when(auctionRepository.findAuctionsByUserId(1L)).thenReturn(List.of(auction1, auction2)); + + // Act: 사용자가 있는지 확인하고 경매 목록 가져오기 + List auctions = auctionService.getAuctionsByUser(1L); + + // Assert: 반환된 경매 목록 확인 + assertThat(auctions).hasSize(2); + assertThat(auctions.get(0).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); + assertThat(auctions.get(1).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); + } + + @Test + @DisplayName("사용자 미존재 예외") + void testGetAuctionsByUser_UserNotFound() { + // Arrange: 사용자가 존재하지 않을 때 findById를 모의 설정 + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + // Act & Assert: UserNotFoundException이 발생할 것으로 예상 + UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> + auctionService.getAuctionsByUser(1L)); + + assertThat(exception.getMessage()).isEqualTo("User not found"); + } + } + + @Nested + @DisplayName("모든 경매 목록 조회 테스트") + class GetAllAuctionsTests { + + @Test + @DisplayName("성공") + void testGetAllAuctions_Success() { + Auction auction1 = createAuction(1L, seller, AuctionStatus.PENDING); + Auction auction2 = createAuction(2L, seller, AuctionStatus.PENDING); + + when(auctionRepository.findAll()).thenReturn(List.of(auction1, auction2)); + + List auctions = auctionService.getAllAuctions(); + + assertThat(auctions).hasSize(2); + assertThat(auctions.get(0).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); + assertThat(auctions.get(1).getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); + } + } + + @Nested + @DisplayName("경매 ID로 조회 테스트") + class GetAuctionByIdTests { + + @Test + @DisplayName("성공") + void testGetAuctionById_Success() { + Auction auction = createAuction(1L, seller, AuctionStatus.PENDING); + + when(auctionRepository.findAuctionById(1L)).thenReturn(Optional.of(auction)); + + AuctionResponse foundAuction = auctionService.getAuctionById(1L); + + assertThat(foundAuction.getCategory()).isEqualTo(AuctionCategory.PUBLIC_PAID.name()); + } + + @Test + @DisplayName("경매 미존재 예외") + void testGetAuctionById_AuctionNotFound() { + when(auctionRepository.findAuctionById(1L)).thenReturn(Optional.empty()); + + AuctionNotFoundException exception = assertThrows(AuctionNotFoundException.class, () -> + auctionService.getAuctionById(1L)); + + assertThat(exception.getMessage()).isEqualTo("Auction not found"); + } + } + + @Nested + @DisplayName("경매 유형이 []인 경매 목록 조회") + class GetAuctionsByAuctionCategoryTests { + @Test + @DisplayName("경매 유형 조회 : [성공]") + public void testGetAuctionsByAuctionCategory_Success() { + //given + AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(null, null, null, null, null, null, AuctionStatus.PENDING, null); + List auctions = List.of( + createAuction(1L, seller, AuctionStatus.PENDING), + createAuction(2L, seller, AuctionStatus.PENDING) + ); + + Pageable pageable = PageRequest.of(0, 10); + Page auctionPage = new PageImpl<>(auctions, pageable, auctions.size()); + when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); + + //when + Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); + + //then + assertThat(actualPage.getContent()).hasSize(2); + assertThat(actualPage.getContent()) + .allMatch(auctionResponseDetail -> auctionResponseDetail.getStatus().equals(AuctionStatus.PENDING.name())); + } + + @Test + @DisplayName("경매 유형 조회 : [성공] -Criteria 사용") + public void testGetAuctionsByAuctionCategory_Success_Criteria() { + //given + AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(null, null, null, null, null, null, AuctionStatus.PENDING, null); + List auctions = List.of( + createAuction(1L, seller, AuctionStatus.PENDING), + createAuction(2L, seller, AuctionStatus.PENDING) + ); + + Pageable pageable = PageRequest.of(0, 10); + Page auctionPage = new PageImpl<>(auctions, pageable, auctions.size()); + when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); + + //when + Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); + + //then + assertThat(actualPage.getContent()).hasSize(2); + assertThat(actualPage.getContent()) + .allMatch(auctionResponseDetail -> auctionResponseDetail.getStatus().equals(AuctionStatus.PENDING.name())); + } + + @Test + @DisplayName("경매 유형 조회 : [결과 없음]") + public void testGetAuctionsByAuctionCategory_AuctionNotFound() { + //given + List emptyAuctionList = List.of(); + AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_FREE, null, null, null, null, null, null, null); + + Pageable pageable = PageRequest.of(0, 10); + Page auctionPage = new PageImpl<>(emptyAuctionList, pageable, 0); + when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); + + //when + Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); + + //then + assertThat(actualPage.isEmpty()).isTrue(); + } + + @Test + @DisplayName("경매 유형 조회 : [결과 없음] - Criteria 사용") + public void testGetAuctionsByAuctionCategory_AuctionNotFound_Criteria() { + //given + List emptyAuctionList = List.of(); + AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_FREE, null, null, null, null, null, null, null); + + Pageable pageable = PageRequest.of(0, 10); + Page auctionPage = new PageImpl<>(emptyAuctionList, pageable, 0); + when(auctionRepository.getAuctionsByFilters(pageable, condition)).thenReturn(auctionPage); + + //when + Page actualPage = auctionService.getAuctionsByFilter(pageable, condition); + + //then + assertThat(actualPage.isEmpty()).isTrue(); + } + } + +} diff --git a/src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java b/src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java new file mode 100644 index 00000000..ea29e9d5 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java @@ -0,0 +1,186 @@ +package com.tasksprints.auction.domain.auction.repository; + +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.common.config.QueryDslConfig; +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.product.domain.entity.Product; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import com.tasksprints.auction.product.infrastructure.ProductRepository; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DataJpaTest +@Import(QueryDslConfig.class) +@Slf4j +public class AuctionRepositoryTest { + + @Autowired + private AuctionRepository auctionRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ProductRepository productRepository; + + private User seller; + + @BeforeEach + public void setUp() { + seller = User.builder() + .name("testUser") + .nickName("testNick") + .password("testPassword") + .email("test@example.com") + .build(); + userRepository.save(seller); + } + + private Auction createAuction(User seller, AuctionCategory category, AuctionStatus status) { + return Auction.create( + LocalDateTime.of(2024, 8, 1, 10, 0), + LocalDateTime.of(2024, 9, 1, 10, 0), + BigDecimal.valueOf(100.00), + category, + status, + seller + ); + } + + private Auction createAuction(User seller, AuctionCategory auctionCategory) { + return Auction.create( + LocalDateTime.of(2024, 8, 1, 10, 0), + LocalDateTime.of(2024, 9, 1, 10, 0), + BigDecimal.valueOf(100.00), + auctionCategory, + AuctionStatus.ACTIVE, + seller + ); + } + + private Product createProduct(User user, Auction auction, String productCategory) { + return Product.create( + "testName", + "testDescription", + user, + auction, + productCategory, + null + ); + } + + @Test + @DisplayName("사용자 ID로 경매 목록 조회") + public void testFindAuctionsByUserId() { + Auction auction = createAuction(seller, AuctionCategory.PUBLIC_FREE, AuctionStatus.ACTIVE); + auctionRepository.save(auction); + + List auctions = auctionRepository.findAuctionsByUserId(seller.getId()); + + assertThat(auctions).isNotEmpty(); + assertThat(auctions.get(0).getSeller().getId()).isEqualTo(seller.getId()); + } + + @Test + @DisplayName("경매 ID로 경매 조회") + public void testFindAuctionById() { + Auction auction = createAuction(seller, AuctionCategory.PUBLIC_FREE, AuctionStatus.PENDING); + auctionRepository.save(auction); + + Optional foundAuction = auctionRepository.findAuctionById(auction.getId()); + + assertTrue(foundAuction.isPresent()); + assertThat(foundAuction.get().getAuctionCategory()).isEqualTo(AuctionCategory.PUBLIC_FREE); + } + + @Test + @DisplayName("모든 경매 목록 조회") + public void testFindAll() { + Auction auction1 = createAuction(seller, AuctionCategory.PUBLIC_FREE, AuctionStatus.ACTIVE); + Auction auction2 = createAuction(seller, AuctionCategory.PUBLIC_PAID, AuctionStatus.PENDING); + + auctionRepository.save(auction1); + auctionRepository.save(auction2); + + List auctions = auctionRepository.findAll(); + + assertThat(auctions).hasSize(2); + + + } + @Deprecated + @Test + @DisplayName("경매 유형이 []인 경매 목록 조회") + public void testFindAuctionsByAuctionCategory() { + //given + Auction auction1 = createAuction(seller, AuctionCategory.PUBLIC_FREE); + Auction auction2 = createAuction(seller, AuctionCategory.PUBLIC_PAID); + Auction auction3 = createAuction(seller, AuctionCategory.PUBLIC_PAID); + auctionRepository.saveAll(List.of(auction1, auction2, auction3)); + + Product product1 = createProduct(seller, auction1, "TV"); + Product product2 = createProduct(seller, auction2, "TV"); + Product product3 = createProduct(seller, auction3, "TV"); + productRepository.saveAll(List.of(product1, product2, product3)); + + AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_PAID, ProductCategory.TV, null, null, null, null, null, null); + + Pageable pageable = PageRequest.of(0, 10); + + //when + Page auctions = auctionRepository.getAuctionsByFilters(pageable, condition); + + //then + assertThat(auctions).hasSize(2); + assertThat(auctions.getContent()).allMatch(auction -> auction.getAuctionCategory().equals(AuctionCategory.PUBLIC_PAID)); + assertThat(auctions.getContent()).allMatch(auction -> auction.getProduct().getCategory().equals(ProductCategory.TV)); + } + @Test + @DisplayName("QueryDSL 필터를 통해서 경매 목록 조회") + public void testFindAllUsingFilter() { + //given + Auction auction1 = createAuction(seller, AuctionCategory.PUBLIC_FREE); + Auction auction2 = createAuction(seller, AuctionCategory.PUBLIC_PAID); + auctionRepository.saveAll(List.of(auction1, auction2)); + + Product product1 = createProduct(seller, auction1, "TV"); + Product product2 = createProduct(seller, auction2, "TV"); + productRepository.saveAll(List.of(product1, product2)); + + AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition(AuctionCategory.PUBLIC_FREE, ProductCategory.TV, null, null, null, null, null, null); + Pageable pageable = PageRequest.of(0, 10); + + //when + Page auctions = auctionRepository.getAuctionsByFilters(pageable, condition); + log.info(auctions.toString()); + + //then + assertThat(auctions).hasSize(1); + assertThat(auctions.getContent().get(0).getAuctionCategory()).isEqualTo(AuctionCategory.PUBLIC_FREE); + assertThat(auctions.getContent().get(0).getProduct().getCategory()).isEqualTo(ProductCategory.TV); + + + } + +} diff --git a/src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java b/src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java new file mode 100644 index 00000000..0a73c90f --- /dev/null +++ b/src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java @@ -0,0 +1,215 @@ +package com.tasksprints.auction.auction.presentation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.auction.presentation.AuctionController; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.auction.domain.service.AuctionService; +import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; +import com.tasksprints.auction.bid.domain.service.BidService; +import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; +import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; +import com.tasksprints.auction.review.domain.service.ReviewService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(AuctionController.class) +@MockBean(JpaMetamodelMappingContext.class) +public class AuctionControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AuctionService auctionService; + + @MockBean + private BidService bidService; + + @MockBean + private ReviewService reviewService; + + private ObjectMapper objectMapper; + + @BeforeEach + public void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + @DisplayName("경매 생성 성공") + public void testCreateAuction_Success() throws Exception { + AuctionRequest.Create auctionRequest = new AuctionRequest.Create(); + AuctionResponse auctionDTO = new AuctionResponse(); // Populate with necessary fields + + when(auctionService.createAuction(anyLong(), any())).thenReturn(auctionDTO); + + mockMvc.perform(post("/api/v1/auction") + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(auctionRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.AUCTION_CREATED_SUCCESS)); + } + + @Test + @DisplayName("경매 생성 실패") + public void testCreateAuction_Failure() throws Exception { + AuctionRequest.Create auctionRequest = new AuctionRequest.Create(); + + when(auctionService.createAuction(anyLong(), any())).thenThrow(new RuntimeException("Error creating auction")); + + mockMvc.perform(post("/api/v1/auction") + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(auctionRequest))) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error creating auction")); + } + + @Test + @DisplayName("경매 종료 성공") + public void testCloseAuction_Success() throws Exception { + mockMvc.perform(post("/api/v1/auction/1/close")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.AUCTION_CLOSED_SUCCESS)); + + verify(auctionService, times(1)).closeAuction(1L); + } + + @Test + @DisplayName("경매 상태 조회 성공") + public void testGetAuctionStatus_Success() throws Exception { + String auctionStatus = "OPEN"; + when(auctionService.getAuctionStatus(1L)).thenReturn(auctionStatus); + + mockMvc.perform(get("/api/v1/auction/1/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.AUCTION_STATUS_RETRIEVED)) + .andExpect(jsonPath("$.data").value(auctionStatus)); + } + + @Test + @DisplayName("QueryString을 통한 경매 목록 조회") + public void testFindAuctionByUsingQueryString_Success() throws Exception { + // Given + List auctionResponseList = new ArrayList<>(); + Pageable pageable = PageRequest.of(0, 10); + Page pageAuctionResponse = new PageImpl<>(auctionResponseList, pageable, 0); + when(auctionService.getAuctionsByFilter(any(),any())).thenReturn(pageAuctionResponse); + + // When & Then + mockMvc.perform(get("/api/v1/auction") + .param("auctionCategory", AuctionCategory.PRIVATE_FREE.name()) + .param("productCategory", "여성의류") + .param("startTime", LocalDateTime.now().minusDays(1).toString()) + .param("endTime", LocalDateTime.now().toString()) + .param("minPrice", "100") + .param("maxPrice", "500") + .param("auctionStatus", AuctionStatus.ACTIVE.name()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED)); + } + +// @Test +// @DisplayName("잘못된 유형을 통한 경매목록 조회(기본값으로 대응)") +// public void testFindAuctionByUsingWrongAuctionCategory_Success() throws Exception { +// List auctionResponseList = new ArrayList<>(); +// when(auctionService.getAuctionsByFilter(any(), any())).thenReturn(auctionResponseList); +// +// mockMvc.perform(get("/api/v1/auction") +// .param("auctionCategory", "NON")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.message").value(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED)); +// } + +// @Test +// @DisplayName("마감 기한이 24시간 이하로 남은 경매 목록 조회") +// public void testFindAuctionsEndWithin24Hours_Success() throws Exception { +// +// /* +// 테스트 코드 추가 +// * */ +// +// } + + @Test + @DisplayName("입찰 제출 성공") + public void testSubmitBid_Success() throws Exception { + BidResponse bidDTO = new BidResponse(); // Populate with necessary fields + when(bidService.submitBid(anyLong(), anyLong(), any())).thenReturn(bidDTO); + + mockMvc.perform(post("/api/v1/auction/1/bid") + .param("userId", "1") + .param("amount", "100.00")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_SUBMITTED_SUCCESS)); + } + + @Test + @DisplayName("입찰 금액 업데이트 성공") + public void testUpdateBid_Success() throws Exception { + BidResponse updatedBidDTO = new BidResponse(); // Populate with necessary fields + when(bidService.updateBidAmount(anyLong(), anyLong(), any())).thenReturn(updatedBidDTO); + + mockMvc.perform(put("/api/v1/auction/1/bid") + .param("userId", "1") + .param("amount", "150.00")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_UPDATED_SUCCESS)); + } + + @Test + @DisplayName("리뷰 생성 성공") + public void testCreateReview_Success() throws Exception { + ReviewRequest.Create reviewRequest = new ReviewRequest.Create(); + ReviewResponse reviewResponse = new ReviewResponse(); // Populate with necessary fields + when(reviewService.createReview(anyLong(), anyLong(), any())).thenReturn(reviewResponse); + + mockMvc.perform(post("/api/v1/auction/1/review") + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(reviewRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.REVIEW_CREATED_SUCCESS)); + } + + +// @Test +// @DisplayName("사용자 리뷰 조회 성공") +// public void testGetReviewsByUser_Success() throws Exception { +// List reviews = Collections.singletonList(new ReviewDTO()); // Populate with necessary fields +// when(reviewService.getReviewsByUserId(anyLong())).thenReturn(reviews); +// +// mockMvc.perform(get("/api/v1/auction/user/1/reviews")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.message").value(ApiResponseMessages.REVIEWS_RETRIEVED)) +// .andExpect(jsonPath("$.data").isArray()); +// } +} From 811946503e29df1c4b3549562621475264624f3a Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:19:26 +0900 Subject: [PATCH 29/48] =?UTF-8?q?refactor(auction)=20:=20auction=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20DDD=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/domain/entity/Auction.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java new file mode 100644 index 00000000..c5facf2b --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java @@ -0,0 +1,97 @@ +package com.tasksprints.auction.auction.domain.entity; + +import com.tasksprints.auction.bid.domain.entity.Bid; +import com.tasksprints.auction.common.entity.BaseEntity; +import com.tasksprints.auction.product.domain.entity.Product; +import com.tasksprints.auction.user.domain.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +@Entity(name = "auction") +public class Auction extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDateTime startTime; + + @Column(nullable = false) + private LocalDateTime endTime; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private AuctionCategory auctionCategory; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Setter + private AuctionStatus auctionStatus; + + @Column(nullable = false) + private BigDecimal startingBid; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + @ToString.Exclude + private User seller; + + @OneToOne + @Builder.Default + private Product product = null; + + @OneToMany + @Builder.Default + private List bids = new ArrayList<>(); + + @Column(nullable = false) + private Long viewCount; + + @PrePersist + protected void onCreate() { + if (viewCount == null) { + viewCount = 0L; // 기본값 설정 + } + } + + public static Auction create(LocalDateTime startTime, LocalDateTime endTime, BigDecimal startingBid, AuctionCategory auctionCategory, AuctionStatus auctionStatus, User seller) { + Auction newAuction = Auction.builder() + .startTime(startTime) + .endTime(endTime) + .startingBid(startingBid) + .auctionCategory(auctionCategory) + .auctionStatus(auctionStatus) + .build(); + newAuction.addUser(seller); + return newAuction; + } + + public void addProduct(Product product) { + //product.addAuction(this); product에서 auction을 추가하고 있어서 중복 + this.product = product; + } + + public void addUser(User seller) { + seller.addAuction(this); + this.seller = seller; + } + + public void incrementViewCount() { + if (viewCount == null) { + viewCount = 0L; + } + this.viewCount += 1; + } + + +} From 5ee6742a031915731e0d807676488906375e0c89 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:20:03 +0900 Subject: [PATCH 30/48] =?UTF-8?q?refactor(bid)=20:=20bid=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DDD=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InvalidAuctionTimeException.java | 7 ++ .../bid/application/BidServiceImpl.java | 99 +++++++++++++++++++ .../auction/bid/domain/dto/BidRequest.java | 13 +++ .../auction/bid/domain/dto/BidResponse.java | 31 ++++++ .../auction/bid/domain/entity/Bid.java | 68 +++++++++++++ .../bid/domain/entity/dto/BidRequest.java | 13 +++ .../bid/domain/entity/dto/BidResponse.java | 31 ++++++ .../bid/domain/service/BidService.java | 19 ++++ .../bid/exception/BidNotFoundException.java | 7 ++ .../exception/InvalidBidAmountException.java | 7 ++ .../bid/infrastructure/BidRepository.java | 20 ++++ .../bid/presentation/BidController.java | 42 ++++++++ 12 files changed, 357 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionTimeException.java create mode 100644 src/main/java/com/tasksprints/auction/bid/application/BidServiceImpl.java create mode 100644 src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java create mode 100644 src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java create mode 100644 src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java create mode 100644 src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java create mode 100644 src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java create mode 100644 src/main/java/com/tasksprints/auction/bid/domain/service/BidService.java create mode 100644 src/main/java/com/tasksprints/auction/bid/exception/BidNotFoundException.java create mode 100644 src/main/java/com/tasksprints/auction/bid/exception/InvalidBidAmountException.java create mode 100644 src/main/java/com/tasksprints/auction/bid/infrastructure/BidRepository.java create mode 100644 src/main/java/com/tasksprints/auction/bid/presentation/BidController.java diff --git a/src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionTimeException.java b/src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionTimeException.java new file mode 100644 index 00000000..b7560eba --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionTimeException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.auction.exception; + +public class InvalidAuctionTimeException extends RuntimeException { + public InvalidAuctionTimeException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/bid/application/BidServiceImpl.java b/src/main/java/com/tasksprints/auction/bid/application/BidServiceImpl.java new file mode 100644 index 00000000..f70763a0 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/application/BidServiceImpl.java @@ -0,0 +1,99 @@ +package com.tasksprints.auction.bid.application; + +import com.tasksprints.auction.bid.domain.service.BidService; +import com.tasksprints.auction.bid.exception.BidNotFoundException; +import com.tasksprints.auction.bid.exception.InvalidBidAmountException; +import com.tasksprints.auction.bid.domain.entity.Bid; +import com.tasksprints.auction.bid.infrastructure.BidRepository; +import com.tasksprints.auction.auction.exception.AuctionEndedException; +import com.tasksprints.auction.auction.exception.AuctionNotFoundException; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 실시간성으로 인해서 socket으로 대체할지에 대한 여부 고민 필요 + */ +@Service +@RequiredArgsConstructor +public class BidServiceImpl implements BidService { + + private final BidRepository bidRepository; + private final UserRepository userRepository; + private final AuctionRepository auctionRepository; + + @Override + public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { + // 입찰 시 유효성 검사 + User foundUser = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found")); + Auction foundAuction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); + + // 경매가 종료되었는지 확인 + if (foundAuction.getEndTime().isBefore(LocalDateTime.now())) { + throw new AuctionEndedException("This auction has already ended."); + } + + // 최소 입찰 금액 충족 여부 확인 + if (amount.compareTo(foundAuction.getStartingBid()) < 0) { + throw new InvalidBidAmountException("Bid amount is less than the minimum required bid amount."); + } + + // 입찰 생성 및 저장 + Bid createdBid = Bid.create(amount, foundUser, foundAuction); + Bid savedBid = bidRepository.save(createdBid); + return BidResponse.of(savedBid); + } + + @Override + public BidResponse updateBidAmount(Long userId, Long auctionId, BigDecimal newAmount) { + // 기존 입찰을 찾습니다. + Bid foundBid = bidRepository.findByUserIdAndAuctionId(userId, auctionId) + .orElseThrow(() -> new BidNotFoundException("Bid not found")); + + Auction foundAuction = foundBid.getAuction(); + + // 경매가 종료되었는지 확인합니다. + if (foundAuction.getEndTime().isBefore(LocalDateTime.now())) { + throw new AuctionEndedException("This auction has already ended."); + } + + // 새로운 입찰 금액이 기존 금액보다 큰지 확인합니다. + if (newAmount.compareTo(foundBid.getAmount()) <= 0) { + throw new InvalidBidAmountException("New bid amount must be greater than the previous bid amount."); + } + + // 새로운 입찰 금액이 최소 입찰 금액을 충족하는지 확인합니다. + if (newAmount.compareTo(foundAuction.getStartingBid()) < 0) { + throw new InvalidBidAmountException("Bid amount is less than the minimum required bid amount."); + } + + // 입찰 금액 업데이트 + foundBid.update(newAmount); + Bid updatedBid = bidRepository.save(foundBid); + return BidResponse.of(updatedBid); + } + + @Override + public Boolean hasUserAlreadyBid(Long auctionId) { + List bids = bidRepository.findByAuctionId(auctionId); + return !bids.isEmpty(); + } + + @Override + public BidResponse getBidByUuid(String uuid) { + Bid bid = bidRepository.findByUuid(uuid) + .orElseThrow(() -> new BidNotFoundException("Bid not found")); + return BidResponse.of(bid); + } +} diff --git a/src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java new file mode 100644 index 00000000..e8f5c3e3 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java @@ -0,0 +1,13 @@ +package com.tasksprints.auction.bid.domain.dto; + +import lombok.Data; + +import java.math.BigDecimal; + + +@Data +public class BidRequest { + private Long userId; + private Long auctionId; + private BigDecimal amount; +} diff --git a/src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java new file mode 100644 index 00000000..c0c38133 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java @@ -0,0 +1,31 @@ +package com.tasksprints.auction.bid.domain.dto; + +import com.tasksprints.auction.bid.domain.entity.Bid; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BidResponse { + Long userId; + String name; + Long auctionId; + BigDecimal amount; + String uuid; + + public static BidResponse of(Bid bid) { + return BidResponse.builder() + .userId(bid.getUser().getId()) + .name(bid.getUser().getName()) + .auctionId(bid.getAuction().getId()) + .amount(bid.getAmount()) + .build(); + /** 아이템 목록 추가**/ + } +} diff --git a/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java b/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java new file mode 100644 index 00000000..e011318e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java @@ -0,0 +1,68 @@ +package com.tasksprints.auction.bid.domain.entity; + +import com.tasksprints.auction.common.entity.BaseEntity; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.user.domain.entity.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Entity(name = "bids") +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +public class Bid extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + //UUID를 ID로 사용하게 되면 성능 저하 -> 기본 키는 Id를 유지하되, 외부에 공개할 키는 UUID로 설정 + private String uuid; + + private BigDecimal amount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_id") + @Builder.Default + private Auction auction = null; + + @ManyToOne(fetch = FetchType.LAZY) //지연 로딩 설정 이유 ? -> N + 1 문제 발생 가능성 있으므로 fetch join 으로 개선 필요 + @JoinColumn(name = "user_id") + @Builder.Default + private User user = null; //null 설정한 이유가 무엇인지? + + public static Bid create(BigDecimal amount, User user, Auction auction) { + Bid newBid = Bid.builder() + .amount(amount) + .build(); + + newBid.addUserAndAuction(user, auction); + return newBid; + } + + /** + * addUser 와 addAuction 을 양방향으로 묶을지 단방향으로 묶을지에 대한 고민 + */ + public void addUser(User user) { + this.user = user; + + } + + public void addAuction(Auction auction) { + this.auction = auction; + } + + public void addUserAndAuction(User user, Auction auction) { + addUser(user); + addAuction(auction); + } + + public void update(BigDecimal amount) { + this.amount = amount; + } +} diff --git a/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java b/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java new file mode 100644 index 00000000..39e82c39 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java @@ -0,0 +1,13 @@ +package com.tasksprints.auction.bid.domain.entity.dto; + +import lombok.Data; + +import java.math.BigDecimal; + + +@Data +public class BidRequest { + private Long userId; + private Long auctionId; + private BigDecimal amount; +} diff --git a/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java b/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java new file mode 100644 index 00000000..6b324dd6 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java @@ -0,0 +1,31 @@ +package com.tasksprints.auction.bid.domain.entity.dto; + +import com.tasksprints.auction.bid.domain.entity.Bid; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BidResponse { + Long userId; + String name; + Long auctionId; + BigDecimal amount; + String uuid; + + public static BidResponse of(Bid bid) { + return BidResponse.builder() + .userId(bid.getUser().getId()) + .name(bid.getUser().getName()) + .auctionId(bid.getAuction().getId()) + .amount(bid.getAmount()) + .build(); + /** 아이템 목록 추가**/ + } +} diff --git a/src/main/java/com/tasksprints/auction/bid/domain/service/BidService.java b/src/main/java/com/tasksprints/auction/bid/domain/service/BidService.java new file mode 100644 index 00000000..ffabfef1 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/domain/service/BidService.java @@ -0,0 +1,19 @@ +package com.tasksprints.auction.bid.domain.service; + +import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; + +import java.math.BigDecimal; + +public interface BidService { + /** + * 입찰 + * 입찰금액 변경 + */ + BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount); + + BidResponse updateBidAmount(Long userId, Long auctionId, BigDecimal amount); + + Boolean hasUserAlreadyBid(Long auctionId); + + BidResponse getBidByUuid(String uuid); +} diff --git a/src/main/java/com/tasksprints/auction/bid/exception/BidNotFoundException.java b/src/main/java/com/tasksprints/auction/bid/exception/BidNotFoundException.java new file mode 100644 index 00000000..159c8fa4 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/exception/BidNotFoundException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.bid.exception; + +public class BidNotFoundException extends RuntimeException { + public BidNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/bid/exception/InvalidBidAmountException.java b/src/main/java/com/tasksprints/auction/bid/exception/InvalidBidAmountException.java new file mode 100644 index 00000000..7d52fddf --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/exception/InvalidBidAmountException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.bid.exception; + +public class InvalidBidAmountException extends RuntimeException { + public InvalidBidAmountException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/bid/infrastructure/BidRepository.java b/src/main/java/com/tasksprints/auction/bid/infrastructure/BidRepository.java new file mode 100644 index 00000000..9064cd86 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/infrastructure/BidRepository.java @@ -0,0 +1,20 @@ +package com.tasksprints.auction.bid.infrastructure; + +import com.tasksprints.auction.bid.domain.entity.Bid; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface BidRepository extends JpaRepository { + + @Query("select b from bids b where b.auction.id = :auctionId") + List findByAuctionId(Long auctionId); + + @Query("select b from bids b where b.auction.id = :auctionId and b.user.id = :userId") + Optional findByUserIdAndAuctionId(Long userId, Long auctionId); + + @Query("select b from bids b where b.uuid = :uuid") + Optional findByUuid(String uuid); +} diff --git a/src/main/java/com/tasksprints/auction/bid/presentation/BidController.java b/src/main/java/com/tasksprints/auction/bid/presentation/BidController.java new file mode 100644 index 00000000..0cb64555 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/bid/presentation/BidController.java @@ -0,0 +1,42 @@ +package com.tasksprints.auction.bid.presentation; + +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; +import com.tasksprints.auction.bid.domain.entity.dto.BidRequest; +import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; +import com.tasksprints.auction.bid.domain.service.BidService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/api/v1/bid") +public class BidController { + private final BidService bidService; + private final SimpMessageSendingOperations simpMessageSendingOperations; + + @MessageMapping("/bid") + public void handleBid(BidRequest bidRequest) { + /** + * 입찰하는거 여기다가 추가하면 좋을 듯 합니다g. + */ + BidResponse bidResponse = bidService.submitBid(bidRequest.getUserId(), bidRequest.getAuctionId(), bidRequest.getAmount()); + simpMessageSendingOperations.convertAndSend("/bid/"+bidResponse.getUuid(), bidResponse); + } + + @GetMapping("/{uuid}") + @Operation(summary = "Get a bid", description = "Get a bid by bid uuid") + @ApiResponse(responseCode = "200", description = "Bid status retrieved successfully") + public ResponseEntity> getBidByUuid(@PathVariable(value = "uuid") String uuid) { + BidResponse bid = bidService.getBidByUuid(uuid); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_RETRIEVED, bid)); + } +} From 21fdc22a5119c4dd76d3abe4bc281e858bbfbd40 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:20:21 +0900 Subject: [PATCH 31/48] =?UTF-8?q?refactor(user)=20:=20user=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DDD=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/UserServiceImpl.java | 63 ++++++ .../user/domain/dto/request/UserRequest.java | 29 +++ .../dto/response/UserDetailResponse.java | 28 +++ .../domain/dto/response/UserResponse.java | 4 + .../dto/response/UserSummaryResponse.java | 21 ++ .../auction/user/domain/entity/User.java | 82 +++++++ .../user/domain/service/UserService.java | 19 ++ .../user/exception/UserNotFoundException.java | 7 + .../user/infrastructure/UserRepository.java | 7 + .../user/presentation/UserController.java | 89 ++++++++ .../user/application/UserServiceImplTest.java | 174 +++++++++++++++ .../infrastructure/UserRepositoryTest.java | 98 +++++++++ .../user/presentation/UserControllerTest.java | 203 ++++++++++++++++++ 13 files changed, 824 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/user/application/UserServiceImpl.java create mode 100644 src/main/java/com/tasksprints/auction/user/domain/dto/request/UserRequest.java create mode 100644 src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java create mode 100644 src/main/java/com/tasksprints/auction/user/domain/dto/response/UserResponse.java create mode 100644 src/main/java/com/tasksprints/auction/user/domain/dto/response/UserSummaryResponse.java create mode 100644 src/main/java/com/tasksprints/auction/user/domain/entity/User.java create mode 100644 src/main/java/com/tasksprints/auction/user/domain/service/UserService.java create mode 100644 src/main/java/com/tasksprints/auction/user/exception/UserNotFoundException.java create mode 100644 src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java create mode 100644 src/main/java/com/tasksprints/auction/user/presentation/UserController.java create mode 100644 src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java create mode 100644 src/test/java/com/tasksprints/auction/user/infrastructure/UserRepositoryTest.java create mode 100644 src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java diff --git a/src/main/java/com/tasksprints/auction/user/application/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/user/application/UserServiceImpl.java new file mode 100644 index 00000000..96d22fec --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/application/UserServiceImpl.java @@ -0,0 +1,63 @@ +package com.tasksprints.auction.user.application; + +import com.tasksprints.auction.user.domain.service.UserService; +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + @Override + public UserDetailResponse createUser(UserRequest.Register request) { + User user = User.create(request.getName(), request.getEmail(), request.getPassword(), request.getNickname()); + + User newUser = userRepository.save(user); + return UserDetailResponse.of(newUser); + } + + @Override + public UserDetailResponse getUserDetailsById(Long id) { + User foundUser = userRepository.findById(id) + .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); + return UserDetailResponse.of(foundUser); + } + + @Override + public List getUsersSummary() { + List foundUsers = userRepository.findAll(); + return foundUsers.stream() + .map(UserSummaryResponse::new) + .collect(Collectors.toList()); + } + + @Override + public UserDetailResponse updateUser(Long id, UserRequest.Update request) { + User user = userRepository.findById(id) + .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); + + user.update(request.getName(), request.getPassword(), request.getNickname()); + User updatedUser = userRepository.save(user); + return UserDetailResponse.of(updatedUser); + } + + @Override + public void deleteUser(Long id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); + user.delete(); // 사용자 상태를 '삭제됨'으로 변경 + userRepository.save(user); // 상태 업데이트를 저장 + } + +} diff --git a/src/main/java/com/tasksprints/auction/user/domain/dto/request/UserRequest.java b/src/main/java/com/tasksprints/auction/user/domain/dto/request/UserRequest.java new file mode 100644 index 00000000..10b13c5d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/domain/dto/request/UserRequest.java @@ -0,0 +1,29 @@ +package com.tasksprints.auction.user.domain.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + + +public class UserRequest { + @Getter + @NoArgsConstructor + @AllArgsConstructor + @ToString + public static class Register { + String name; + String email; + String password; + String nickname; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Update { + String name; + String password; + String nickname; + } +} diff --git a/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java new file mode 100644 index 00000000..1d78e74c --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java @@ -0,0 +1,28 @@ +package com.tasksprints.auction.user.domain.dto.response; + +import com.tasksprints.auction.user.domain.entity.User; +import lombok.AllArgsConstructor; +import lombok.Data; + +@AllArgsConstructor +@Data +public class UserDetailResponse { + private Long id; + private String name; + private String email; + private String password; + private String nickName; + + private UserDetailResponse(User user) { + this.id = user.getId(); + this.name = user.getName(); + this.email = user.getEmail(); + this.password = user.getPassword(); + this.nickName = user.getNickName(); + } + + public static UserDetailResponse of(User user) { + return new UserDetailResponse(user); + } + +} diff --git a/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserResponse.java b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserResponse.java new file mode 100644 index 00000000..a3ccb44e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserResponse.java @@ -0,0 +1,4 @@ +package com.tasksprints.auction.user.domain.dto.response; + +public class UserResponse { +} diff --git a/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserSummaryResponse.java b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserSummaryResponse.java new file mode 100644 index 00000000..19264394 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserSummaryResponse.java @@ -0,0 +1,21 @@ +package com.tasksprints.auction.user.domain.dto.response; + +import com.tasksprints.auction.user.domain.entity.User; +import lombok.AllArgsConstructor; +import lombok.Data; + +@AllArgsConstructor +@Data +public class UserSummaryResponse { + private Long id; + private String nickName; + private String name; + private String email; + + public UserSummaryResponse(User user) { + this.id = user.getId(); + this.nickName = user.getNickName(); + this.email = user.getEmail(); + this.name = user.getName(); + } +} diff --git a/src/main/java/com/tasksprints/auction/user/domain/entity/User.java b/src/main/java/com/tasksprints/auction/user/domain/entity/User.java new file mode 100644 index 00000000..51b14def --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/domain/entity/User.java @@ -0,0 +1,82 @@ +package com.tasksprints.auction.user.domain.entity; + +import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; +import com.tasksprints.auction.auction.domain.entity.Auction; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLRestriction; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@SQLRestriction("deleted_at is null") +@Getter +@ToString +@Entity(name = "users") +public class User extends BaseEntityWithUpdate { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, unique = true) //unique 특성 추가 필요 + private String email; + + @Column(nullable = false) //unique 특성 추가 필요 + private String password; + + @Column(nullable = false) + private String nickName; //unique 특성 추가 필요 + + @Column(nullable = true) + private LocalDateTime deletedAt; + + @OneToMany(mappedBy = "seller", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @Builder.Default + private List auctions = new ArrayList<>(); +// 추후 추가 +// @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) +// @Builder.Default +// private List bids = new ArrayList<>(); + + /** + * @descripton static factory pattern을 적용하여, 구현 + */ + public static User create(String name, String email, String password, String nickName) { + return User.builder() + .name(name) + .email(email) + .password(password) + .nickName(nickName) + .build(); + } + + public void setAuctions(List auctions) { + this.auctions = auctions; + } + + /** + * @description 인자 길이가 너무 길어서, 함수단위로 쪼갤지에 대한 고민중 + */ + + + public void update(String name, String password, String nickName) { + this.name = name; + this.password = password; + this.nickName = nickName; + } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + } + + public void addAuction(Auction auction) { + this.auctions.add(auction); + } +} diff --git a/src/main/java/com/tasksprints/auction/user/domain/service/UserService.java b/src/main/java/com/tasksprints/auction/user/domain/service/UserService.java new file mode 100644 index 00000000..2fcec68b --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/domain/service/UserService.java @@ -0,0 +1,19 @@ +package com.tasksprints.auction.user.domain.service; + +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; + +import java.util.List; + +public interface UserService { + UserDetailResponse createUser(UserRequest.Register user); + + UserDetailResponse getUserDetailsById(Long id); + + List getUsersSummary(); + + UserDetailResponse updateUser(Long id, UserRequest.Update user); + + void deleteUser(Long id); +} diff --git a/src/main/java/com/tasksprints/auction/user/exception/UserNotFoundException.java b/src/main/java/com/tasksprints/auction/user/exception/UserNotFoundException.java new file mode 100644 index 00000000..9b7e003a --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/exception/UserNotFoundException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.user.exception; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java b/src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java new file mode 100644 index 00000000..a4f16f29 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.user.infrastructure; + +import com.tasksprints.auction.user.domain.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/com/tasksprints/auction/user/presentation/UserController.java b/src/main/java/com/tasksprints/auction/user/presentation/UserController.java new file mode 100644 index 00000000..1d5d91ec --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/presentation/UserController.java @@ -0,0 +1,89 @@ +package com.tasksprints.auction.user.presentation; + +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; +import com.tasksprints.auction.user.domain.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/user") +@RequiredArgsConstructor +public class UserController { + private final UserService userService; + + @Operation(summary = "Register User", description = "Register a new user with the provided user information.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User created successfully."), + @ApiResponse(responseCode = "400", description = "Invalid user data.") + }) + @PostMapping() + public ResponseEntity> register(@RequestBody UserRequest.Register user) { + UserDetailResponse createdUser = userService.createUser(user); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.USER_CREATED_SUCCESS, createdUser)); + } + + @Operation(summary = "Get User by ID", description = "Retrieve user details based on the user ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User details retrieved successfully."), + @ApiResponse(responseCode = "404", description = "User not found.") + }) + @GetMapping("/{id}") + public ResponseEntity> getUserById(@PathVariable Long id) { + UserDetailResponse user = userService.getUserDetailsById(id); + return ResponseEntity.ok(ApiResult.success(null, user)); + } + + @Operation(summary = "Get All Users", description = "Retrieve a summary list of all users.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User summary list retrieved successfully."), + @ApiResponse(responseCode = "204", description = "No users found.") + }) + @GetMapping() + public ResponseEntity>> getAllUsers() { + List users = userService.getUsersSummary(); + return ResponseEntity.ok(ApiResult.success(null, users)); + } + + @Operation(summary = "Update User", description = "Update user information for a specific user ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User updated successfully."), + @ApiResponse(responseCode = "404", description = "User not found.") + }) + @PutMapping() + public ResponseEntity> updateUser(@RequestParam Long id, @RequestBody UserRequest.Update user) { + UserDetailResponse updatedUser = userService.updateUser(id, user); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.USER_UPDATED_SUCCESS, updatedUser)); + } + + @Operation(summary = "Delete User", description = "Delete a user by user ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User deleted successfully."), + @ApiResponse(responseCode = "404", description = "User not found.") + }) + @DeleteMapping() + public ResponseEntity> deleteUser(@RequestParam Long id) { + userService.deleteUser(id); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.USER_DELETED_SUCCESS)); + } +// @GetMapping("/reviews") +// @Operation(summary = "Get reviews by user", description = "Retrieves all reviews created by a specific user.") +// @ApiResponses(value = { +// @ApiResponse(responseCode = "200", description = "User reviews retrieved successfully"), +// @ApiResponse(responseCode = "404", description = "User not found") +// }) +// public ResponseEntity>> getReviewsByUser(@RequestParam Long userId) { +// List reviews = reviewService.getReviewsByUserId(userId); +// return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEWS_RETRIEVED, reviews)); +// } + +} diff --git a/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java b/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java new file mode 100644 index 00000000..7cdedd27 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java @@ -0,0 +1,174 @@ +package com.tasksprints.auction.user.application; + +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import com.tasksprints.auction.user.application.UserServiceImpl; +import org.junit.jupiter.api.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +public class UserServiceImplTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserServiceImpl userService; + + private User existingUser; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + existingUser = User.builder() + .id(1L) + .name("testUser") + .nickName("testNick") + .password("testPassword") + .email("test@example.com") + .build(); + } + + @Nested + @DisplayName("Create User") + class CreateUserTests { + + @Test + @DisplayName("should create a new user") + void shouldCreateNewUser() { + // Arrange + UserRequest.Register request = new UserRequest.Register("testUser", "test@example.com", "testPassword", "testNick"); + when(userRepository.save(any(User.class))).thenReturn(existingUser); + + // Act + UserDetailResponse createdUser = userService.createUser(request); + + // Assert + Assertions.assertNotNull(createdUser); + Assertions.assertEquals("testUser", createdUser.getName()); + Assertions.assertEquals("test@example.com", createdUser.getEmail()); + Assertions.assertEquals("testNick", createdUser.getNickName()); + verify(userRepository, times(1)).save(any(User.class)); + } + } + + @Nested + @DisplayName("Get User By ID") + class GetUserByIdTests { + + @Test + @DisplayName("should return user when found") + void shouldReturnUserWhenFound() { + // Arrange + when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser)); + + // Act + UserDetailResponse user = userService.getUserDetailsById(1L); + + // Assert + Assertions.assertNotNull(user); + Assertions.assertEquals(existingUser.getId(), user.getId()); + Assertions.assertEquals(existingUser.getName(), user.getName()); + verify(userRepository, times(1)).findById(1L); + } + + @Test + @DisplayName("should throw exception when user not found") + void shouldThrowExceptionWhenUserNotFound() { + // Arrange + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + // Act & Assert + Assertions.assertThrows(UserNotFoundException.class, () -> userService.getUserDetailsById(1L)); + verify(userRepository, times(1)).findById(1L); + } + } + + @Nested + @DisplayName("Update User") + class UpdateUserTests { + + @Test + @DisplayName("should update user details") + void shouldUpdateUserDetails() { + // Arrange + UserRequest.Update request = new UserRequest.Update("updatedName", "updatedPassword", "updatedNick"); + when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser)); + when(userRepository.save(any(User.class))).thenReturn(existingUser); + + // Act + UserDetailResponse updatedUser = userService.updateUser(1L, request); + + // Assert + Assertions.assertNotNull(updatedUser); + Assertions.assertEquals("updatedName", updatedUser.getName()); + Assertions.assertEquals("updatedPassword", updatedUser.getPassword()); + Assertions.assertEquals("updatedNick", updatedUser.getNickName()); + verify(userRepository, times(1)).findById(1L); + verify(userRepository, times(1)).save(any(User.class)); + } + + @Test + @DisplayName("should throw exception when user not found") + void shouldThrowExceptionWhenUserNotFound() { + // Arrange + UserRequest.Update request = new UserRequest.Update("updatedName", null, null); + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + // Act & Assert + Assertions.assertThrows(UserNotFoundException.class, () -> userService.updateUser(1L, request)); + verify(userRepository, times(1)).findById(1L); + verify(userRepository, never()).save(any(User.class)); + } + } + + @Nested + @DisplayName("Delete User") + class DeleteUserTests { + + @Test + @DisplayName("should delete user when found") + void shouldDeleteUserWhenFound() { + // Arrange + User existingUser = User.builder() + .id(1L) + .name("testUser") + .nickName("testNick") + .password("testPassword") + .email("test@example.com") + .build(); + when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser)); + when(userRepository.save(any(User.class))).thenReturn(existingUser); + + // Act + userService.deleteUser(1L); + + // Assert + verify(userRepository, times(1)).findById(1L); + verify(userRepository, times(1)).save(existingUser); + + // Verify that the user's deletedAt field was updated correctly + Assertions.assertNotNull(existingUser.getDeletedAt()); + } + + @Test + @DisplayName("should throw exception when user not found") + void shouldThrowExceptionWhenUserNotFound() { + // Arrange + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + // Act & Assert + Assertions.assertThrows(UserNotFoundException.class, () -> userService.deleteUser(1L)); + verify(userRepository, times(1)).findById(1L); + verify(userRepository, never()).delete(any(User.class)); + } + } +} diff --git a/src/test/java/com/tasksprints/auction/user/infrastructure/UserRepositoryTest.java b/src/test/java/com/tasksprints/auction/user/infrastructure/UserRepositoryTest.java new file mode 100644 index 00000000..688e57c8 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/user/infrastructure/UserRepositoryTest.java @@ -0,0 +1,98 @@ +package com.tasksprints.auction.user.infrastructure; + +import com.tasksprints.auction.common.config.QueryDslConfig; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.Optional; + +/** + * [CRUD TEST] UserRepositoryTest + */ +@DataJpaTest +@Import(QueryDslConfig.class) +@Slf4j +public class UserRepositoryTest { + + @Autowired + private UserRepository userRepository; + + private User user; + + @BeforeEach + void setUp() { + user = User.builder() + .name("testUser") + .nickName("testNick") + .password("testPassword") + .email("test@example.com") + .build(); + } + + @DisplayName("findById 테스트") + @Test + void findUserById() { + User createdUser = userRepository.save(user); + User foundUser = userRepository.findById(createdUser.getId()).orElse(null); + + Assertions.assertNotNull(foundUser); + Assertions.assertEquals(createdUser.getId(), foundUser.getId()); + Assertions.assertEquals("testUser", foundUser.getName()); + Assertions.assertEquals("testNick", foundUser.getNickName()); + Assertions.assertEquals("test@example.com", foundUser.getEmail()); + + log.info("Found User: {}", foundUser); + } + + @DisplayName("delete 테스트") + @Test + void deleteUser() { + User createdUser = userRepository.save(user); + Long userId = createdUser.getId(); + + userRepository.deleteById(userId); + + Optional deletedUser = userRepository.findById(userId); + + Assertions.assertTrue(deletedUser.isEmpty(), "User should be deleted"); + log.info("User with ID {} deleted", userId); + } + + @DisplayName("save 테스트") + @Nested + class SaveTest { + @DisplayName("유저 업데이트 테스트") + @Test + void updateUser() { + User createdUser = userRepository.save(user); + createdUser.update("updatedName", "updatedPassword", "updatedNickName"); + User updatedUser = userRepository.save(createdUser); + + Assertions.assertNotNull(updatedUser); + Assertions.assertEquals("updatedName", updatedUser.getName()); + Assertions.assertEquals("updatedNickName", updatedUser.getNickName()); + Assertions.assertEquals("updatedPassword", updatedUser.getPassword()); + + log.info("Updated User: {}", updatedUser); + } + + @DisplayName("유저 생성 테스트") + @Test + void createUser() { + User createdUser = userRepository.save(user); + + Assertions.assertNotNull(createdUser); + Assertions.assertNotNull(createdUser.getId()); + Assertions.assertEquals("testUser", createdUser.getName()); + Assertions.assertEquals("testNick", createdUser.getNickName()); + Assertions.assertEquals("test@example.com", createdUser.getEmail()); + + log.info("Created User: {}", createdUser); + } + } +} diff --git a/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java b/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java new file mode 100644 index 00000000..f3cf997d --- /dev/null +++ b/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java @@ -0,0 +1,203 @@ +package com.tasksprints.auction.user.presentation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.user.presentation.UserController; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.service.UserService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +@MockBean(JpaMetamodelMappingContext.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Autowired + private ObjectMapper objectMapper; + + @Nested + @DisplayName("성공적인 요청") + class SuccessfulTests { + + @Test + @DisplayName("POST /api/v1/user - 성공") + void registerUser() throws Exception { + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123"); + + Mockito.when(userService.createUser(any(UserRequest.Register.class))).thenReturn(userDetailResponse); + + UserRequest.Register request = new UserRequest.Register("John", "john@example.com", "password", "john123"); + + mockMvc.perform(post("/api/v1/user") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_CREATED_SUCCESS)) + .andExpect(jsonPath("$.data.name").value("John")) + .andExpect(jsonPath("$.data.email").value("john@example.com")); + } + + @Test + @DisplayName("GET /api/v1/user/{id} - 성공") + void getUserById() throws Exception { + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123"); + + Mockito.when(userService.getUserDetailsById(anyLong())).thenReturn(userDetailResponse); + + mockMvc.perform(get("/api/v1/user/{id}", 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.name").value("John")) + .andExpect(jsonPath("$.data.email").value("john@example.com")); + } + + @Test + @DisplayName("GET /api/v1/user - 성공") + void getAllUsers() throws Exception { + UserSummaryResponse userSummaryResponse = new UserSummaryResponse(1L, "john123", "John", "john@example.com"); + + List users = Collections.singletonList(userSummaryResponse); + + Mockito.when(userService.getUsersSummary()).thenReturn(users); + + mockMvc.perform(get("/api/v1/user")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data[0].name").value("John")) + .andExpect(jsonPath("$.data[0].email").value("john@example.com")); + } + + @Test + @DisplayName("PUT /api/v1/user - 성공") + void updateUser() throws Exception { + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John Updated", "john@example.com", "newpassword", "john123updated"); + + Mockito.when(userService.updateUser(anyLong(), any(UserRequest.Update.class))).thenReturn(userDetailResponse); + + UserRequest.Update request = new UserRequest.Update("John Updated", "newpassword", "john123updated"); + + mockMvc.perform(put("/api/v1/user") + .param("id", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_UPDATED_SUCCESS)) + .andExpect(jsonPath("$.data.name").value("John Updated")); + } + + @Test + @DisplayName("DELETE /api/v1/user - 성공") + void deleteUser() throws Exception { + Mockito.doNothing().when(userService).deleteUser(anyLong()); + + mockMvc.perform(delete("/api/v1/user") + .param("id", "1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_DELETED_SUCCESS)); + } + } + + @Nested + @DisplayName("실패한 요청") + class FailureTests { + + @Test + @DisplayName("POST /api/v1/user - 실패") + void registerUserFail() throws Exception { + Mockito.when(userService.createUser(any(UserRequest.Register.class))) + .thenThrow(new RuntimeException("Failed to create user")); + + UserRequest.Register request = new UserRequest.Register("John", "john@example.com", "password", "john123"); + + mockMvc.perform(post("/api/v1/user") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("Failed to create user")); + } + + @Test + @DisplayName("GET /api/v1/user/{id} - 실패 (사용자 없음)") + void getUserByIdFail() throws Exception { + Mockito.when(userService.getUserDetailsById(anyLong())) + .thenThrow(new UserNotFoundException("User not found with id 1")); + + mockMvc.perform(get("/api/v1/user/{id}", 1L)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("User not found")); + } + + @Test + @DisplayName("GET /api/v1/user - 실패") + void getAllUsersFail() throws Exception { + Mockito.when(userService.getUsersSummary()) + .thenThrow(new RuntimeException("Failed to retrieve users")); + + mockMvc.perform(get("/api/v1/user")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("Failed to retrieve users")); + } + + @Test + @DisplayName("PUT /api/v1/user - 실패") + void updateUserFail() throws Exception { + Mockito.when(userService.updateUser(anyLong(), any(UserRequest.Update.class))) + .thenThrow(new UserNotFoundException("User not found with id 1")); + + UserRequest.Update request = new UserRequest.Update("John Updated", "newpassword", "john123updated"); + + mockMvc.perform(put("/api/v1/user") + .param("id", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("User not found")); + } + + @Test + @DisplayName("DELETE /api/v1/user - 실패 (사용자 없음)") + void deleteUserFail() throws Exception { + Mockito.doThrow(new UserNotFoundException("User not found with id 1")) + .when(userService).deleteUser(anyLong()); + + mockMvc.perform(delete("/api/v1/user") + .param("id", "1")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("User not found")); + } + } +} From e98965dab2a090f0430cd43beb32c7ff32f6cf0c Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:20:44 +0900 Subject: [PATCH 32/48] =?UTF-8?q?refactor(product)=20:=20user=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DDD=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ProductServiceImpl.java | 131 +++++++++++ .../domain/dto/request/ProductRequest.java | 26 +++ .../domain/dto/response/ProductResponse.java | 45 ++++ .../product/domain/entity/Product.java | 78 +++++++ .../domain/entity/ProductCategory.java | 66 ++++++ .../product/domain/entity/ProductImage.java | 32 +++ .../domain/service/ProductService.java | 25 ++ .../ProductImageUploadException.java | 13 ++ .../exception/ProductNotFoundException.java | 7 + .../ProductImageRepository.java | 7 + .../infrastructure/ProductRepository.java | 14 ++ .../presentation/ProductController.java | 51 ++++ .../application/ProductServiceImplTest.java | 219 ++++++++++++++++++ .../infrastructure/ProductRepositoryTest.java | 217 +++++++++++++++++ .../presentation/ProductControllerTest.java | 127 ++++++++++ 15 files changed, 1058 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/product/application/ProductServiceImpl.java create mode 100644 src/main/java/com/tasksprints/auction/product/domain/dto/request/ProductRequest.java create mode 100644 src/main/java/com/tasksprints/auction/product/domain/dto/response/ProductResponse.java create mode 100644 src/main/java/com/tasksprints/auction/product/domain/entity/Product.java create mode 100644 src/main/java/com/tasksprints/auction/product/domain/entity/ProductCategory.java create mode 100644 src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java create mode 100644 src/main/java/com/tasksprints/auction/product/domain/service/ProductService.java create mode 100644 src/main/java/com/tasksprints/auction/product/exception/ProductImageUploadException.java create mode 100644 src/main/java/com/tasksprints/auction/product/exception/ProductNotFoundException.java create mode 100644 src/main/java/com/tasksprints/auction/product/infrastructure/ProductImageRepository.java create mode 100644 src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java create mode 100644 src/main/java/com/tasksprints/auction/product/presentation/ProductController.java create mode 100644 src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java create mode 100644 src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java create mode 100644 src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java diff --git a/src/main/java/com/tasksprints/auction/product/application/ProductServiceImpl.java b/src/main/java/com/tasksprints/auction/product/application/ProductServiceImpl.java new file mode 100644 index 00000000..a59d8e10 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/application/ProductServiceImpl.java @@ -0,0 +1,131 @@ +package com.tasksprints.auction.product.application; + +import com.tasksprints.auction.auction.exception.AuctionNotFoundException; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.product.domain.dto.request.ProductRequest; +import com.tasksprints.auction.product.domain.dto.response.ProductResponse; +import com.tasksprints.auction.product.domain.service.ProductService; +import com.tasksprints.auction.product.exception.ProductImageUploadException; +import com.tasksprints.auction.product.exception.ProductNotFoundException; +import com.tasksprints.auction.product.domain.entity.Product; +import com.tasksprints.auction.product.domain.entity.ProductImage; +import com.tasksprints.auction.product.infrastructure.ProductImageRepository; +import com.tasksprints.auction.product.infrastructure.ProductRepository; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ProductServiceImpl implements ProductService { + private static final String UPLOADS_DIR = "src/main/resources/static/uploads/thumbnails/"; + private final ProductRepository productRepository; + private final UserRepository userRepository; + private final AuctionRepository auctionRepository; + private final ProductImageRepository productImageRepository; + + @Override + public String uploadImage(MultipartFile image) { + String fileName = generateFileName(image); + Path filePath = Paths.get(UPLOADS_DIR, fileName); + saveFile(filePath, image); + return "/uploads/thumbnails/" + fileName; + } + + @Override + public List uploadImageBulk(List images) { + return images.stream() // 병렬 처리를 피하고 일반 Stream 사용 + .map(this::uploadImage) + .collect(Collectors.toList()); + } + + @Override + @Deprecated + public List getProductsByUserId(Long userId) { + List products = productRepository.findByOwnerId(userId); + return convertToDTOList(products); + } + + @Override + public ProductResponse getProductByAuctionId(Long auctionId) { + Product product = productRepository.findByAuctionId(auctionId) + .orElseThrow(() -> new ProductNotFoundException("Product not found")); + return ProductResponse.of(product); + } + + @Transactional + @Override + public ProductResponse register(Long userId, Long auctionId, ProductRequest.Register request, List images) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found")); + Auction auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); + + // 이미지 업로드 + List imageUrls = uploadImageBulk(images); + + List productImageList = imageUrls.parallelStream() + .map(ProductImage::create) + .toList(); + + List savedProductImageList = productImageRepository.saveAll(productImageList); + + // 상품 생성 + Product newProduct = Product.create( + request.getName(), + request.getDescription(), + user, + auction, + request.getCategory(), + savedProductImageList + ); + + Product createdProduct = productRepository.save(newProduct); + + return ProductResponse.of(createdProduct); + } + + @Override + public ProductResponse update(ProductRequest.Update request) { + Long productId = request.getProductId(); + Product foundProduct = productRepository.findById(productId) + .orElseThrow(() -> new ProductNotFoundException("Product not found")); + + foundProduct.update(request.getName(), request.getDescription()); + Product savedProduct = productRepository.save(foundProduct); + return ProductResponse.of(savedProduct); + } + + private List convertToDTOList(List products) { + return products.stream() + .map(ProductResponse::of) + .collect(Collectors.toList()); + } + + private void saveFile(Path filePath, MultipartFile image) { + try { + Files.createDirectories(filePath.getParent()); // 디렉토리 생성 (필요한 경우) + Files.copy(image.getInputStream(), filePath); // 파일 저장 + } catch (IOException e) { + throw new ProductImageUploadException("Failed to upload image: " + image.getOriginalFilename(), e); + } + } + + private String generateFileName(MultipartFile image) { + return UUID.randomUUID().toString().replace("-", "") + "_" + image.getOriginalFilename(); + } + +} diff --git a/src/main/java/com/tasksprints/auction/product/domain/dto/request/ProductRequest.java b/src/main/java/com/tasksprints/auction/product/domain/dto/request/ProductRequest.java new file mode 100644 index 00000000..2eb0b16c --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/domain/dto/request/ProductRequest.java @@ -0,0 +1,26 @@ +package com.tasksprints.auction.product.domain.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class ProductRequest { + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Register { + private String name; + private String description; + private String category; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Update { + private Long productId; + private String name; + private String description; + } + +} diff --git a/src/main/java/com/tasksprints/auction/product/domain/dto/response/ProductResponse.java b/src/main/java/com/tasksprints/auction/product/domain/dto/response/ProductResponse.java new file mode 100644 index 00000000..d8a16d4e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/domain/dto/response/ProductResponse.java @@ -0,0 +1,45 @@ +package com.tasksprints.auction.product.domain.dto.response; + +import com.tasksprints.auction.product.domain.entity.Product; +import com.tasksprints.auction.product.domain.entity.ProductImage; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class ProductResponse { + private Long productId; + private String name; + private String description; + private String category; + private Long ownerId; + private String ownerNickName; + private Long auctionId; + private List productImageList; + + private static List extractProductImageList(Product product) { + return product.getProductImageList() + .stream() + .map(ProductImage::getImageUrl) + .toList(); + } + + public static ProductResponse of(Product product) { + return ProductResponse.builder() + .productId(product.getId()) + .name(product.getName()) + .description(product.getDescription()) + .ownerId(product.getOwner().getId()) + .ownerNickName(product.getOwner().getNickName()) + .category(product.getCategory().getDisplayName()) + .auctionId(product.getAuction().getId()) + .productImageList(extractProductImageList(product)) + .build(); + } +} diff --git a/src/main/java/com/tasksprints/auction/product/domain/entity/Product.java b/src/main/java/com/tasksprints/auction/product/domain/entity/Product.java new file mode 100644 index 00000000..bb195282 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/domain/entity/Product.java @@ -0,0 +1,78 @@ +package com.tasksprints.auction.product.domain.entity; + +import com.tasksprints.auction.common.entity.BaseEntity; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.user.domain.entity.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity(name = "products") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class Product extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String description; + + @Enumerated(EnumType.STRING) + private ProductCategory category; //제품의 Category + + @ManyToOne + @JoinColumn(name = "owner_id") + private User owner; + + @OneToOne + //mappedby + @JoinColumn(name = "auction_id") + private Auction auction; + + @OneToMany + @Builder.Default + private List productImageList = new ArrayList<>(); + + public static Product create(String name, String description, User owner, Auction auction, String productCategory, List productImageList) { + Product product = Product.builder().name(name).description(description).category(ProductCategory.fromDisplayName(productCategory)).build(); + product.addOwnerAndAuction(owner, auction); + product.initProductImageList(productImageList); + return product; + } + + public void addOwner(User owner) { + this.owner = owner; + } + + public void addAuction(Auction auction) { + //양방향 매핑 + this.auction = auction; + auction.addProduct(this); + } + + public void initProductImageList(List productImageList) { + this.productImageList = productImageList; + } + + public void addOwnerAndAuction(User owner, Auction auction) { + addOwner(owner); + addAuction(auction); + } + + public void update(String name, String description) { + this.name = name; + this.description = description; + } + +} diff --git a/src/main/java/com/tasksprints/auction/product/domain/entity/ProductCategory.java b/src/main/java/com/tasksprints/auction/product/domain/entity/ProductCategory.java new file mode 100644 index 00000000..1d10dcc1 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/domain/entity/ProductCategory.java @@ -0,0 +1,66 @@ +package com.tasksprints.auction.product.domain.entity; + +public enum ProductCategory { + WOMENS_CLOTHING("여성의류"), + UNDERWEAR_SLEEPWEAR("언더웨어/잠옷"), + PLUS_SIZE("빅사이즈"), + SENIOR_CLOTHING("시니어의류"), + MENS_CLOTHING("남성의류"), + + OVERSEAS_COSMETICS("해외화장품"), + HAIR("헤어"), + DOMESTIC_COSMETICS("국내화장품"), + BODY("바디"), + PERFUME("향수"), + BEAUTY("미용"), + + CURTAINS("커튼"), + FURNITURE("가구"), + LIVING("생활"), + DECOR("장식소품"), + STORAGE("수납용품"), + KITCHEN("주방"), + INTERIOR("인테리어"), + BEDDING("침구"), + + TV("TV"), + REFRIGERATOR("냉장고"), + WASHING_MACHINE("세탁기"), + KITCHEN_APPLIANCES("주방/생활"), + BEAUTY_APPLIANCES("이미용가전"), + AIR_PURIFIER("청정/제습"), + FAN_AIRCON("선풍기/에어컨"), + + MIRRORLESS_CAMERA("미러리스"), + DSLR("DSLR"), + DIGITAL_CAMERA("디카"), + MP3_PMP_DICTIONARY("MP3/PMP/사전"), + GAMING_CONSOLE("게임기/타이틀"), + SMARTPHONE("휴대폰/스마트폰"), + LAPTOP("노트북"), + DESKTOP("데스크탑"), + MONITOR("모니터"), + PRINTER("프린터"), + ETC("기타제품"); + private final String displayName; + + ProductCategory(String displayName) { + this.displayName = displayName; + } + + /** + * 한정된 갯수의 Category 이기에 따로 Over-head는 발생하지 않을 것으로 판단 + ***/ + public static ProductCategory fromDisplayName(String displayName) { + for (ProductCategory category : values()) { + if (category.displayName.equals(displayName)) { + return category; + } + } + throw new IllegalArgumentException("Unknown category: " + displayName); + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java b/src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java new file mode 100644 index 00000000..4ebac704 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java @@ -0,0 +1,32 @@ +package com.tasksprints.auction.product.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "product_images") +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProductImage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = false) + @Getter + private String imageUrl; + + +// @ColumnDefault("false") +// private Boolean isPrime; + + public static ProductImage create(String imageUrl) { + return ProductImage.builder() + .imageUrl(imageUrl) + .build(); + } + +} diff --git a/src/main/java/com/tasksprints/auction/product/domain/service/ProductService.java b/src/main/java/com/tasksprints/auction/product/domain/service/ProductService.java new file mode 100644 index 00000000..60dd5e9d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/domain/service/ProductService.java @@ -0,0 +1,25 @@ +package com.tasksprints.auction.product.domain.service; + +import com.tasksprints.auction.product.domain.dto.request.ProductRequest; +import com.tasksprints.auction.product.domain.dto.response.ProductResponse; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +/** + * item으로 Auction 검색 + */ +public interface ProductService { + String uploadImage(MultipartFile image) throws IOException; + + List uploadImageBulk(List images); + + List getProductsByUserId(Long userId); + + ProductResponse getProductByAuctionId(Long auctionId); + + ProductResponse register(Long userId, Long auctionId, ProductRequest.Register product, List images); + + ProductResponse update(ProductRequest.Update product); +} diff --git a/src/main/java/com/tasksprints/auction/product/exception/ProductImageUploadException.java b/src/main/java/com/tasksprints/auction/product/exception/ProductImageUploadException.java new file mode 100644 index 00000000..8c1462cc --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/exception/ProductImageUploadException.java @@ -0,0 +1,13 @@ +package com.tasksprints.auction.product.exception; + + +public class ProductImageUploadException extends RuntimeException { + + public ProductImageUploadException(String message) { + super(message); + } + + public ProductImageUploadException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/tasksprints/auction/product/exception/ProductNotFoundException.java b/src/main/java/com/tasksprints/auction/product/exception/ProductNotFoundException.java new file mode 100644 index 00000000..30d60c26 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/exception/ProductNotFoundException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.product.exception; + +public class ProductNotFoundException extends RuntimeException { + public ProductNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/product/infrastructure/ProductImageRepository.java b/src/main/java/com/tasksprints/auction/product/infrastructure/ProductImageRepository.java new file mode 100644 index 00000000..b1e2b122 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/infrastructure/ProductImageRepository.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.product.infrastructure; + +import com.tasksprints.auction.product.domain.entity.ProductImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java b/src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java new file mode 100644 index 00000000..16597e97 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java @@ -0,0 +1,14 @@ +package com.tasksprints.auction.product.infrastructure; + +import com.tasksprints.auction.product.domain.entity.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ProductRepository extends JpaRepository { + Optional findByAuctionId(Long auctionId); + + // 쿼리를 메서드 이름으로 표현 + List findByOwnerId(Long ownerId); +} diff --git a/src/main/java/com/tasksprints/auction/product/presentation/ProductController.java b/src/main/java/com/tasksprints/auction/product/presentation/ProductController.java new file mode 100644 index 00000000..abb41626 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/product/presentation/ProductController.java @@ -0,0 +1,51 @@ +package com.tasksprints.auction.product.presentation; + +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; +import com.tasksprints.auction.product.domain.dto.request.ProductRequest; +import com.tasksprints.auction.product.domain.dto.response.ProductResponse; +import com.tasksprints.auction.product.domain.service.ProductService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/product") +@RequiredArgsConstructor +public class ProductController { + private final ProductService productService; + + @Operation(summary = "Register Product", description = "Register a new product for an auction by user ID and auction ID.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Product registered successfully."), @ApiResponse(responseCode = "404", description = "Auction not found.")}) + @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) + public ResponseEntity> registerProduct(@RequestParam Long userId, @RequestParam Long auctionId, @RequestPart("productRequest") ProductRequest.Register productRequest, @RequestPart("images") List images) { + ProductResponse ProductResponse = productService.register(userId, auctionId, productRequest, images); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.PRODUCT_FOUND_SUCCESS, ProductResponse)); + } + + @Operation(summary = "Get Products by Auction ID", description = "Retrieve products based on user ID or auction ID.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Product(s) retrieved successfully."), @ApiResponse(responseCode = "404", description = "Product(s) not found for the given user ID or auction ID.")}) + @GetMapping + public ResponseEntity> getProducts(@RequestParam Long auctionId) { + + ProductResponse ProductResponse = productService.getProductByAuctionId(auctionId); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.PRODUCT_FOUND_SUCCESS, ProductResponse)); + } + + @Operation(summary = "Update Product", description = "Update an existing product with new information.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Product updated successfully."), @ApiResponse(responseCode = "404", description = "Product not found.")}) + @PutMapping + public ResponseEntity> updateProduct(@RequestBody ProductRequest.Update productRequest) { + ProductResponse updatedProduct = productService.update(productRequest); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEW_RETRIEVED, updatedProduct)); + } + +} + diff --git a/src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java b/src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java new file mode 100644 index 00000000..40a5bb20 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java @@ -0,0 +1,219 @@ +package com.tasksprints.auction.product.application; + +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.product.domain.dto.request.ProductRequest; +import com.tasksprints.auction.product.domain.dto.response.ProductResponse; +import com.tasksprints.auction.product.exception.ProductNotFoundException; +import com.tasksprints.auction.product.domain.entity.Product; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import com.tasksprints.auction.product.infrastructure.ProductImageRepository; +import com.tasksprints.auction.product.infrastructure.ProductRepository; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import org.junit.jupiter.api.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +public class ProductServiceImplTest { + + private static final String UPLOADS_DIR = "src/main/resources/static/uploads/thumbnails/"; + @InjectMocks + private ProductServiceImpl productService; + @Mock + private ProductRepository productRepository; + @Mock + private ProductImageRepository productImageRepository; + @Mock + private UserRepository userRepository; + @Mock + private AuctionRepository auctionRepository; + private User user; + private Auction auction; + private Product product; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + user = User.builder() + .id(1L) + .name("testUser") + .nickName("test") + .password("password") + .email("test@naver.com") + .build(); + auction = Auction.builder() + .id(1L) + .startTime(LocalDateTime.now()) + .endTime(LocalDateTime.now().plusHours(1)) + .auctionStatus(AuctionStatus.ACTIVE) + .startingBid(BigDecimal.ONE) + .auctionCategory(AuctionCategory.PRIVATE_FREE) + .seller(user) + .build(); + product = Product.builder() + .id(1L) + .name("Test Product") + .description("Description") + .category(ProductCategory.fromDisplayName("여성의류")) + .owner(user) + .auction(auction) + .build(); + } + + @Nested + @DisplayName("제품 등록 기능") + class RegisterTests { + @AfterEach + public void cleanUp() throws IOException { + deleteAllImagesInDirectory(UPLOADS_DIR); + } + + private void deleteAllImagesInDirectory(String directoryPath) throws IOException { + Path dirPath = Paths.get(directoryPath); + + if (Files.exists(dirPath) && Files.isDirectory(dirPath)) { + try (var paths = Files.walk(dirPath)) { + paths.filter(Files::isRegularFile) // 정규 파일만 필터링 + .forEach(filePath -> { + try { + Files.delete(filePath); // 파일 삭제 + System.out.println("Deleted: " + filePath); // 삭제된 파일 로그 출력 + } catch (IOException e) { + System.err.println("Error deleting file: " + filePath + " - " + e.getMessage()); // 삭제 실패 로그 출력 + } + }); + } + } else { + System.out.println("Directory not found: " + directoryPath); // 디렉토리 없음 로그 출력 + } + } + + @Test + @DisplayName("새로운 제품 등록") + public void testRegister() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(auctionRepository.findById(1L)).thenReturn(Optional.of(auction)); + when(productRepository.save(any(Product.class))).thenReturn(product); + when(productImageRepository.saveAll(anyList())).thenReturn(Collections.emptyList()); // Mock 설정 추가 + + ProductRequest.Register request = new ProductRequest.Register("Test Product", "Description", "여성의류"); + + ProductResponse createdProductResponse = productService.register(1L, 1L, request, createMockImages()); + + assertEquals(createdProductResponse.getName(), "Test Product"); + verify(productRepository).save(any(Product.class)); + } + + @Test + @DisplayName("등록 시 사용자 미발견 예외 발생") + public void testRegisterUserNotFound() { + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + ProductRequest.Register request = new ProductRequest.Register("Test Product", "Description", "여성의류"); + + assertThrows(UserNotFoundException.class, () -> productService.register(1L, 1L, request, createMockImages())); + } + + @Test + @DisplayName("등록 시 경매 미발견 예외 발생") + public void testRegisterAuctionNotFound() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(auctionRepository.findById(1L)).thenReturn(Optional.empty()); + + ProductRequest.Register request = new ProductRequest.Register("Test Product", "Description", "여성의류"); + + assertThrows(RuntimeException.class, () -> productService.register(1L, 1L, request, createMockImages())); + } + + private List createMockImages() { + MockMultipartFile image1 = new MockMultipartFile("image1", "test1.jpg", "image/jpeg", "test image content 1".getBytes()); + MockMultipartFile image2 = new MockMultipartFile("image2", "test2.jpg", "image/jpeg", "test image content 2".getBytes()); + return List.of(image1, image2); // 이미지 리스트 반환 + } + + } + + @Nested + @DisplayName("제품 조회 기능") + class GetTests { + + @Test + @DisplayName("사용자 ID로 제품 조회") + public void testGetProductsByUserId() { + when(productRepository.findByOwnerId(1L)).thenReturn(Collections.singletonList(product)); + + List products = productService.getProductsByUserId(1L); + + assertEquals(products.size(), 1); + assertEquals(products.get(0).getName(), "Test Product"); + } + + @Test + @DisplayName("경매 ID로 제품 조회") + public void testGetProductByAuctionId() { + when(productRepository.findByAuctionId(1L)).thenReturn(Optional.of(product)); + + ProductResponse foundProductResponse = productService.getProductByAuctionId(1L); + + assertEquals(foundProductResponse.getName(), "Test Product"); + } + + @Test + @DisplayName("경매 ID로 제품 미발견 시 예외 발생") + public void testGetProductByAuctionIdNotFound() { + when(productRepository.findByAuctionId(1L)).thenReturn(Optional.empty()); + + assertThrows(ProductNotFoundException.class, () -> productService.getProductByAuctionId(1L)); + } + } + + @Nested + @DisplayName("제품 수정 기능") + class UpdateTests { + + @Test + @DisplayName("수정할 제품 미발견 시 예외 발생") + public void testUpdateProductNotFound() { + ProductRequest.Update updateRequest = new ProductRequest.Update(1L, "Updated Product", "Updated Description"); + + when(productRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThrows(ProductNotFoundException.class, () -> productService.update(updateRequest)); + } + + @Test + @DisplayName("기존 제품 수정") + public void testUpdateProduct() { + when(productRepository.findById(1L)).thenReturn(Optional.of(product)); + when(productRepository.save(any(Product.class))).thenReturn(product); + + ProductRequest.Update updateRequest = new ProductRequest.Update(1L, "Updated Product", "Updated Description"); + + ProductResponse updatedProductResponse = productService.update(updateRequest); + + assertEquals(updatedProductResponse.getName(), "Updated Product"); + verify(productRepository).save(any(Product.class)); + } + } +} diff --git a/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java b/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java new file mode 100644 index 00000000..a7067fdf --- /dev/null +++ b/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java @@ -0,0 +1,217 @@ +package com.tasksprints.auction.product.infrastructure; + +import com.tasksprints.auction.common.config.QueryDslConfig; +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.product.domain.entity.Product; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DataJpaTest +@Import(QueryDslConfig.class) +public class ProductRepositoryTest { + + @Autowired + private ProductRepository productRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private AuctionRepository auctionRepository; + + private User owner; + private Auction auction; + + @BeforeEach + public void setUp() { + owner = createUser("testUser", "test", "test@naver.com"); + auction = createAuction(owner, BigDecimal.ONE, LocalDateTime.now(), LocalDateTime.now().plusHours(1)); + Product product = createProduct("sample product1", "description", owner, auction); + productRepository.save(product); + } + + @Test + @DisplayName("Find product by auction ID") + public void testFindByAuctionId() { + Optional foundProduct = productRepository.findByAuctionId(auction.getId()); + + assertTrue(foundProduct.isPresent()); + assertThat(foundProduct.get().getAuction().getId()).isEqualTo(auction.getId()); + } + + @Test + @DisplayName("Find all products by user ID") + public void testFindAllByUserId() { + List products = productRepository.findByOwnerId(owner.getId()); + + assertThat(products).isNotEmpty(); + assertThat(products.get(0).getOwner().getId()).isEqualTo(owner.getId()); + } + + @Test + @DisplayName("Save a new product") + public void testSaveProduct() { + Auction auction2 = createAuction(owner, BigDecimal.ONE, LocalDateTime.now(), LocalDateTime.now().plusHours(1)); + Product newProduct = createProduct("Another Product", "this is another product", owner, auction2); + + Product savedProduct = productRepository.save(newProduct); + + assertThat(savedProduct).isNotNull(); + assertThat(savedProduct.getId()).isNotNull(); + assertThat(savedProduct.getName()).isEqualTo("Another Product"); + } + + @Test + @DisplayName("Find product by ID") + public void testFindById() { + Product product = productRepository.findAll().get(0); // Use first product for testing + Optional foundProduct = productRepository.findById(product.getId()); + + assertTrue(foundProduct.isPresent()); + assertThat(foundProduct.get().getId()).isEqualTo(product.getId()); + } + + @Test + @DisplayName("Delete product by ID") + public void testDeleteById() { + Product product = productRepository.findAll().get(0); // Use first product for testing + productRepository.deleteById(product.getId()); + + Optional deletedProduct = productRepository.findById(product.getId()); + assertTrue(deletedProduct.isEmpty()); + } + @Deprecated + @Test + @DisplayName("queryDSL 검색 필터로 조회") + public void testFindAllUsingProductCategory() { + //given + Auction auction1 = createAuction(owner, AuctionStatus.PENDING); + Auction auction2 = createAuction(owner, AuctionStatus.ACTIVE); + + Product product1 = createProduct("product1", "description1", ProductCategory.TV, owner, auction1); + Product product2 = createProduct("product2", "description2", ProductCategory.DSLR, owner, auction2); + + product1.addAuction(auction1); + product2.addAuction(auction2); + + AuctionRequest.SearchCondition condition = new AuctionRequest.SearchCondition( + null, ProductCategory.TV, null, null, null, null, AuctionStatus.PENDING, null + ); + Pageable pageable = PageRequest.of(0, 10); + + //when + Page auctionsByFilters = auctionRepository.getAuctionsByFilters(pageable, condition); + + //then + assertThat(auctionsByFilters).hasSize(1); + assertThat(auctionsByFilters.getContent().get(0).getProduct().getCategory()).isEqualTo(ProductCategory.TV); + assertThat(auctionsByFilters.getContent().get(0).getAuctionStatus()).isEqualTo(AuctionStatus.PENDING); + + + } + @Deprecated + @Test + @DisplayName("상품 카테고리로 경매를 조회 : Condition 쿼리스트링이 넘어온 경우") + public void testFindAllUsingConditionFilter() { + //given + Auction auction1 = createAuction(owner, AuctionStatus.ACTIVE); + Auction auction2 = createAuction(owner, AuctionStatus.PENDING); + + Product product1 = createProduct("product1", "description1", ProductCategory.TV, owner, auction1); + Product product2 = createProduct("product2", "description2", ProductCategory.DSLR, owner, auction2); + + product1.addAuction(auction1); + product2.addAuction(auction2); + + Pageable pageable = PageRequest.of(0, 10); + + AuctionRequest.SearchCondition searchCondition = new AuctionRequest.SearchCondition(null, null, null, + null, null, null, + AuctionStatus.PENDING, null) + ; + //when + Page auctionsByCategory = auctionRepository.getAuctionsByCategory(pageable, searchCondition, ProductCategory.DSLR); + + //then + assertThat(auctionsByCategory).hasSize(1); + assertThat(auctionsByCategory.getContent().get(0).getProduct().getCategory()).isEqualTo(ProductCategory.DSLR); + } + + + // Helper methods to minimize code duplication + private User createUser(String name, String nickName, String email) { + User user = User.builder() + .name(name) + .nickName(nickName) + .password("password") // Use a consistent password for all users + .email(email) + .build(); + return userRepository.save(user); + } + private Auction createAuction(User owner, BigDecimal startingBid, LocalDateTime startTime, LocalDateTime endTime) { + Auction auction = Auction.builder() + .startingBid(startingBid) + .startTime(startTime) + .endTime(endTime) + .auctionStatus(AuctionStatus.ACTIVE) + .auctionCategory(AuctionCategory.PRIVATE_FREE) + .seller(owner) + .build(); + return auctionRepository.save(auction); + } + + private Auction createAuction(User owner, AuctionStatus auctionStatus) { + Auction auction = Auction.builder() + .startingBid(BigDecimal.valueOf(100.00)) + .startTime(LocalDateTime.now()) + .endTime(LocalDateTime.now().plusHours(1)) + .auctionStatus(auctionStatus) + .auctionCategory(AuctionCategory.PRIVATE_FREE) + .seller(owner) + .build(); + return auctionRepository.save(auction); + } + + private Product createProduct(String name, String description, User owner, Auction auction) { + Product product = Product.builder() + .name(name) + .description(description) + .owner(owner) + .auction(auction) + .build(); + return productRepository.save(product); + } + + private Product createProduct(String name, String description, ProductCategory category, User owner, Auction auction) { + Product product = Product.builder() + .name(name) + .description(description) + .owner(owner) + .auction(auction) + .category(category) + .build(); + return productRepository.save(product); + } +} diff --git a/src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java b/src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java new file mode 100644 index 00000000..f6919051 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java @@ -0,0 +1,127 @@ +package com.tasksprints.auction.product.presentation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.product.presentation.ProductController; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.product.domain.dto.request.ProductRequest; +import com.tasksprints.auction.product.domain.dto.response.ProductResponse; +import com.tasksprints.auction.product.domain.service.ProductService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +import java.nio.charset.StandardCharsets; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ProductController.class) +@MockBean(JpaMetamodelMappingContext.class) +public class ProductControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ProductService productService; + + @Autowired + private ObjectMapper objectMapper; + + private MockMultipartFile multipartFile1; + private MockMultipartFile multipartFile2; + private MockMultipartFile productRequestMultiPart; + + @BeforeEach + public void setup() throws Exception { + multipartFile1 = new MockMultipartFile( + "images", + "image1.png", + "image/png", + "fake image content 1".getBytes(StandardCharsets.UTF_8) + ); + + multipartFile2 = new MockMultipartFile( + "images", + "image2.jpg", + "image/jpeg", + "fake image content 2".getBytes(StandardCharsets.UTF_8) + ); + + ProductRequest.Register productRequest = new ProductRequest.Register("Sample Product", "This is a sample product.", "여성의류"); + String valueAsString = objectMapper.writeValueAsString(productRequest); + + productRequestMultiPart = new MockMultipartFile( + "productRequest", + "productRequest", + MediaType.APPLICATION_JSON_VALUE, + valueAsString.getBytes(StandardCharsets.UTF_8) + ); + } + + @DisplayName("제품 등록 성공") + @Test + public void testRegisterProduct_Success() throws Exception { + ProductResponse productResponse = new ProductResponse(); // Populate with necessary fields + when(productService.register(anyLong(), anyLong(), any(), any())).thenReturn(productResponse); + + mockMvc.perform(multipart("/api/v1/product?userId=1&auctionId=1") + .file(multipartFile1) + .file(multipartFile2) + .file(productRequestMultiPart) + ) + .andExpect(status().isOk()) // Expect status OK + .andExpect(jsonPath("$.message").value(ApiResponseMessages.PRODUCT_FOUND_SUCCESS)); + } + + @DisplayName("제품 등록 실패") + @Test + public void testRegisterProduct_Failure() throws Exception { + when(productService.register(anyLong(), anyLong(), any(), any())).thenThrow(new RuntimeException("Error registering product")); + + mockMvc.perform(multipart("/api/v1/product?userId=1&auctionId=1") + .file(multipartFile1) + .file(multipartFile2) + .file(productRequestMultiPart) + ) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error registering product")); + } + + @DisplayName("auctionId를 통해 제품 조회 성공") + @Test + public void testGetProductByAuctionId_Success() throws Exception { + ProductResponse ProductResponse = new ProductResponse(); // Populate with necessary fields + when(productService.getProductByAuctionId(anyLong())).thenReturn(ProductResponse); + + mockMvc.perform(get("/api/v1/product?auctionId=1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.PRODUCT_FOUND_SUCCESS)); // Adjust as needed + } + + @DisplayName("제품 수정 성공") + @Test + public void testUpdateProduct_Success() throws Exception { + ProductRequest.Update productRequest = new ProductRequest.Update(); // Populate with necessary fields + ProductResponse updatedProductResponse = new ProductResponse(); // Populate with necessary fields + when(productService.update(any())).thenReturn(updatedProductResponse); + + mockMvc.perform(put("/api/v1/product") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(productRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.REVIEW_RETRIEVED)); // Adjust as needed + } + +} From aea583ce96f4e825eb5510c1409c4f2f5747f7f1 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:21:04 +0900 Subject: [PATCH 33/48] =?UTF-8?q?refactor(review)=20:=20review=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DDD=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewServiceImpl.java | 50 ++++++++++++++++ .../domain/dto/request/ReviewRequest.java | 11 ++++ .../domain/dto/response/ReviewResponse.java | 23 ++++++++ .../auction/review/domain/entity/Review.java | 57 +++++++++++++++++++ .../review/domain/service/ReviewService.java | 19 +++++++ .../infrastructure/ReviewRepository.java | 16 ++++++ 6 files changed, 176 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/review/application/ReviewServiceImpl.java create mode 100644 src/main/java/com/tasksprints/auction/review/domain/dto/request/ReviewRequest.java create mode 100644 src/main/java/com/tasksprints/auction/review/domain/dto/response/ReviewResponse.java create mode 100644 src/main/java/com/tasksprints/auction/review/domain/entity/Review.java create mode 100644 src/main/java/com/tasksprints/auction/review/domain/service/ReviewService.java create mode 100644 src/main/java/com/tasksprints/auction/review/infrastructure/ReviewRepository.java diff --git a/src/main/java/com/tasksprints/auction/review/application/ReviewServiceImpl.java b/src/main/java/com/tasksprints/auction/review/application/ReviewServiceImpl.java new file mode 100644 index 00000000..05652283 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/review/application/ReviewServiceImpl.java @@ -0,0 +1,50 @@ +package com.tasksprints.auction.review.application; + +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.review.domain.service.ReviewService; +import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; +import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; +import com.tasksprints.auction.review.domain.entity.Review; +import com.tasksprints.auction.review.infrastructure.ReviewRepository; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ReviewServiceImpl implements ReviewService { + private final ReviewRepository reviewRepository; + private final UserRepository userRepository; + private final AuctionRepository auctionRepository; + + @Override + public ReviewResponse createReview(Long userId, Long auctionId, ReviewRequest.Create review) { + User user = userRepository.findById(userId) + .orElseThrow(); + Auction auction = auctionRepository.findAuctionById(auctionId) + .orElseThrow(); + Review createdReview = Review.create(review.getContent(), review.getRating(), user, auction); + Review savedReview = reviewRepository.save(createdReview); + return ReviewResponse.of(savedReview); + } + + @Override + public List getReviewsByUserId(Long userId) { + List reviews = reviewRepository.findByUserId(userId); + return reviews.stream() + .map(ReviewResponse::of) + .collect(Collectors.toList()); + } + + @Override + public ReviewResponse getReviewByAuctionId(Long auctionId) { + Review foundReview = reviewRepository.findByAuctionId(auctionId) + .orElseThrow(); + return ReviewResponse.of(foundReview); + } +} diff --git a/src/main/java/com/tasksprints/auction/review/domain/dto/request/ReviewRequest.java b/src/main/java/com/tasksprints/auction/review/domain/dto/request/ReviewRequest.java new file mode 100644 index 00000000..c51dfcb2 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/review/domain/dto/request/ReviewRequest.java @@ -0,0 +1,11 @@ +package com.tasksprints.auction.review.domain.dto.request; + +import lombok.Data; + +public class ReviewRequest { + @Data + public static class Create { + Integer rating; + String content; + } +} diff --git a/src/main/java/com/tasksprints/auction/review/domain/dto/response/ReviewResponse.java b/src/main/java/com/tasksprints/auction/review/domain/dto/response/ReviewResponse.java new file mode 100644 index 00000000..aca97c14 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/review/domain/dto/response/ReviewResponse.java @@ -0,0 +1,23 @@ +package com.tasksprints.auction.review.domain.dto.response; + +import com.tasksprints.auction.review.domain.entity.Review; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ReviewResponse { + Integer rating; + String content; + + public static ReviewResponse of(Review review) { + return ReviewResponse.builder() + .rating(review.getRating()) + .content(review.getContent()) + .build(); + } +} diff --git a/src/main/java/com/tasksprints/auction/review/domain/entity/Review.java b/src/main/java/com/tasksprints/auction/review/domain/entity/Review.java new file mode 100644 index 00000000..76ae130f --- /dev/null +++ b/src/main/java/com/tasksprints/auction/review/domain/entity/Review.java @@ -0,0 +1,57 @@ +package com.tasksprints.auction.review.domain.entity; + +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.user.domain.entity.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "reviews") +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +public class Review { + @Id + @GeneratedValue + private Long id; + + @Column(unique = true) + private Integer rating; // 리뷰 평점 (1~5 등으로 제한할 수 있음) + + @Column(unique = true) + private String content; // 리뷰 내용 + + @ManyToOne + @JoinColumn(name = "writer_id") + private User writer; + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "auction_id") + private Auction auction; + + public static Review create(String content, Integer rating, User writer, Auction auction) { + Review newReview = Review.builder() + .content(content) + .rating(rating) + .build(); + + newReview.addWriterAndAuction(writer, auction); + return newReview; + } + + public void addWriter(User writer) { + this.writer = writer; + } + + public void addAuction(Auction auction) { + this.auction = auction; + } + + public void addWriterAndAuction(User writer, Auction auction) { + addWriter(writer); + addAuction(auction); + } +} diff --git a/src/main/java/com/tasksprints/auction/review/domain/service/ReviewService.java b/src/main/java/com/tasksprints/auction/review/domain/service/ReviewService.java new file mode 100644 index 00000000..cd55da32 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/review/domain/service/ReviewService.java @@ -0,0 +1,19 @@ +package com.tasksprints.auction.review.domain.service; + +import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; +import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; + +import java.util.List; + +public interface ReviewService { + + ReviewResponse createReview(Long userId, Long auctionId, ReviewRequest.Create review); + + List getReviewsByUserId(Long userId); + + ReviewResponse getReviewByAuctionId(Long auctionId); + +} +/** + * 수정, 삭제 불가 + */ diff --git a/src/main/java/com/tasksprints/auction/review/infrastructure/ReviewRepository.java b/src/main/java/com/tasksprints/auction/review/infrastructure/ReviewRepository.java new file mode 100644 index 00000000..33e5b798 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/review/infrastructure/ReviewRepository.java @@ -0,0 +1,16 @@ +package com.tasksprints.auction.review.infrastructure; + +import com.tasksprints.auction.review.domain.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface ReviewRepository extends JpaRepository { + @Query("select r from reviews r where r.writer.id = :userId") + List findByUserId(Long userId); + + @Query("select r from reviews r where r.auction.id = :auctionId") + Optional findByAuctionId(Long auctionId); +} From d88f1894f88d4898ee4e97fd5b035d79657529ef Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:21:13 +0900 Subject: [PATCH 34/48] =?UTF-8?q?refactor(auction)=20:=20auction=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20DDD=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/SearchConditionResolver.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java diff --git a/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java b/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java new file mode 100644 index 00000000..6ca52788 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java @@ -0,0 +1,60 @@ +package com.tasksprints.auction.auction.application.resolver; + +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Component +public class SearchConditionResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + // 메서드 파라미터 타입이 AuctionRequest.SearchCondition일 때 처리 + return parameter.getParameterType().equals(AuctionRequest.SearchCondition.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + // QueryString에서 값을 추출 + String auctionCategory = webRequest.getParameter("auctionCategory"); + String productCategory = webRequest.getParameter("productCategory"); + String startTime = webRequest.getParameter("startTime"); + String endTime = webRequest.getParameter("endTime"); + String minPrice = webRequest.getParameter("minPrice"); + String maxPrice = webRequest.getParameter("maxPrice"); + String auctionStatus = webRequest.getParameter("auctionStatus"); + String sortBy = webRequest.getParameter("sortBy"); + // 파싱 및 변환 + AuctionCategory parsedAuctionCategory = auctionCategory != null ? AuctionCategory.fromDisplayName(auctionCategory) : null; + ProductCategory parsedProductCategory = productCategory != null ? ProductCategory.fromDisplayName(productCategory) : null; + LocalDateTime parsedStartTime = startTime != null ? LocalDateTime.parse(startTime, DateTimeFormatter.ISO_DATE_TIME) : null; + LocalDateTime parsedEndTime = endTime != null ? LocalDateTime.parse(endTime, DateTimeFormatter.ISO_DATE_TIME) : null; + BigDecimal parsedMinPrice = minPrice != null ? new BigDecimal(minPrice) : null; + BigDecimal parsedMaxPrice = maxPrice != null ? new BigDecimal(maxPrice) : null; + AuctionStatus parsedAuctionStatus = auctionStatus != null ? AuctionStatus.fromDisplayName(auctionStatus) : null; + + // SearchCondition 객체 생성 및 반환 + return new AuctionRequest.SearchCondition( + parsedAuctionCategory, + parsedProductCategory, + parsedStartTime, + parsedEndTime, + parsedMinPrice, + parsedMaxPrice, + parsedAuctionStatus, + sortBy + ); + } +} From beeb42c69982094eb2c243f5aec0e5a5464a7da2 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sat, 14 Dec 2024 02:21:48 +0900 Subject: [PATCH 35/48] =?UTF-8?q?style(all)=20:=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=8F=20import=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - .../auction/common/config/WebConfig.java | 3 +-- .../handler/GlobalExceptionHandler.java | 16 +++++++------- .../initializer/AuctionInitializer.java | 21 +++++++++---------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 8b098edb..3192d57a 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,6 @@ dependencies { //thymeleaf implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' } - tasks.named('test') { useJUnitPlatform() } diff --git a/src/main/java/com/tasksprints/auction/common/config/WebConfig.java b/src/main/java/com/tasksprints/auction/common/config/WebConfig.java index 0f7bb22a..06abb497 100644 --- a/src/main/java/com/tasksprints/auction/common/config/WebConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/WebConfig.java @@ -1,7 +1,6 @@ package com.tasksprints.auction.common.config; -import com.tasksprints.auction.common.resolver.SearchConditionResolver; -import lombok.RequiredArgsConstructor; +import com.tasksprints.auction.auction.application.resolver.SearchConditionResolver; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; diff --git a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java index 4b8a90aa..921f7094 100644 --- a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java @@ -2,14 +2,14 @@ import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.domain.auction.exception.AuctionAlreadyClosedException; -import com.tasksprints.auction.domain.auction.exception.AuctionEndedException; -import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; -import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException; -import com.tasksprints.auction.domain.bid.exception.BidNotFoundException; -import com.tasksprints.auction.domain.bid.exception.InvalidBidAmountException; -import com.tasksprints.auction.domain.product.exception.ProductNotFoundException; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; +import com.tasksprints.auction.auction.exception.AuctionAlreadyClosedException; +import com.tasksprints.auction.auction.exception.AuctionEndedException; +import com.tasksprints.auction.auction.exception.AuctionNotFoundException; +import com.tasksprints.auction.auction.exception.InvalidAuctionTimeException; +import com.tasksprints.auction.bid.exception.BidNotFoundException; +import com.tasksprints.auction.bid.exception.InvalidBidAmountException; +import com.tasksprints.auction.product.exception.ProductNotFoundException; +import com.tasksprints.auction.user.exception.UserNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; diff --git a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java index c9ea2a40..e149989a 100644 --- a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java +++ b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java @@ -1,19 +1,18 @@ package com.tasksprints.auction.common.initializer; -import com.tasksprints.auction.domain.auction.model.Auction; -import com.tasksprints.auction.domain.auction.model.AuctionCategory; -import com.tasksprints.auction.domain.auction.model.AuctionStatus; -import com.tasksprints.auction.domain.auction.repository.AuctionRepository; -import com.tasksprints.auction.domain.product.model.Product; -import com.tasksprints.auction.domain.product.model.ProductImage; -import com.tasksprints.auction.domain.product.repository.ProductImageRepository; -import com.tasksprints.auction.domain.product.repository.ProductRepository; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.domain.entity.AuctionCategory; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.product.domain.entity.Product; +import com.tasksprints.auction.product.domain.entity.ProductImage; +import com.tasksprints.auction.product.infrastructure.ProductImageRepository; +import com.tasksprints.auction.product.infrastructure.ProductRepository; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.infrastructure.UserRepository; import jakarta.transaction.Transactional; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.math.BigDecimal; From aafa263706d023af10e219a57ccf54b6df549eb3 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:03:15 +0900 Subject: [PATCH 36/48] =?UTF-8?q?migrate(product)=20:=20product=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20DDD=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{domain => application}/service/ProductService.java | 2 +- .../product/application/{ => service}/ProductServiceImpl.java | 3 +-- .../auction/product/presentation/ProductController.java | 2 +- .../auction/product/application/ProductServiceImplTest.java | 1 + .../auction/product/presentation/ProductControllerTest.java | 3 +-- 5 files changed, 5 insertions(+), 6 deletions(-) rename src/main/java/com/tasksprints/auction/product/{domain => application}/service/ProductService.java (92%) rename src/main/java/com/tasksprints/auction/product/application/{ => service}/ProductServiceImpl.java (97%) diff --git a/src/main/java/com/tasksprints/auction/product/domain/service/ProductService.java b/src/main/java/com/tasksprints/auction/product/application/service/ProductService.java similarity index 92% rename from src/main/java/com/tasksprints/auction/product/domain/service/ProductService.java rename to src/main/java/com/tasksprints/auction/product/application/service/ProductService.java index 60dd5e9d..6a362e20 100644 --- a/src/main/java/com/tasksprints/auction/product/domain/service/ProductService.java +++ b/src/main/java/com/tasksprints/auction/product/application/service/ProductService.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.product.domain.service; +package com.tasksprints.auction.product.application.service; import com.tasksprints.auction.product.domain.dto.request.ProductRequest; import com.tasksprints.auction.product.domain.dto.response.ProductResponse; diff --git a/src/main/java/com/tasksprints/auction/product/application/ProductServiceImpl.java b/src/main/java/com/tasksprints/auction/product/application/service/ProductServiceImpl.java similarity index 97% rename from src/main/java/com/tasksprints/auction/product/application/ProductServiceImpl.java rename to src/main/java/com/tasksprints/auction/product/application/service/ProductServiceImpl.java index a59d8e10..e3986000 100644 --- a/src/main/java/com/tasksprints/auction/product/application/ProductServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/product/application/service/ProductServiceImpl.java @@ -1,11 +1,10 @@ -package com.tasksprints.auction.product.application; +package com.tasksprints.auction.product.application.service; import com.tasksprints.auction.auction.exception.AuctionNotFoundException; import com.tasksprints.auction.auction.domain.entity.Auction; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; import com.tasksprints.auction.product.domain.dto.request.ProductRequest; import com.tasksprints.auction.product.domain.dto.response.ProductResponse; -import com.tasksprints.auction.product.domain.service.ProductService; import com.tasksprints.auction.product.exception.ProductImageUploadException; import com.tasksprints.auction.product.exception.ProductNotFoundException; import com.tasksprints.auction.product.domain.entity.Product; diff --git a/src/main/java/com/tasksprints/auction/product/presentation/ProductController.java b/src/main/java/com/tasksprints/auction/product/presentation/ProductController.java index abb41626..6f4d3169 100644 --- a/src/main/java/com/tasksprints/auction/product/presentation/ProductController.java +++ b/src/main/java/com/tasksprints/auction/product/presentation/ProductController.java @@ -4,7 +4,7 @@ import com.tasksprints.auction.common.response.ApiResult; import com.tasksprints.auction.product.domain.dto.request.ProductRequest; import com.tasksprints.auction.product.domain.dto.response.ProductResponse; -import com.tasksprints.auction.product.domain.service.ProductService; +import com.tasksprints.auction.product.application.service.ProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; diff --git a/src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java b/src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java index 40a5bb20..633f591c 100644 --- a/src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/product/application/ProductServiceImplTest.java @@ -4,6 +4,7 @@ import com.tasksprints.auction.auction.domain.entity.AuctionCategory; import com.tasksprints.auction.auction.domain.entity.AuctionStatus; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.product.application.service.ProductServiceImpl; import com.tasksprints.auction.product.domain.dto.request.ProductRequest; import com.tasksprints.auction.product.domain.dto.response.ProductResponse; import com.tasksprints.auction.product.exception.ProductNotFoundException; diff --git a/src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java b/src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java index f6919051..91fa4d84 100644 --- a/src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java +++ b/src/test/java/com/tasksprints/auction/product/presentation/ProductControllerTest.java @@ -1,11 +1,10 @@ package com.tasksprints.auction.product.presentation; import com.fasterxml.jackson.databind.ObjectMapper; -import com.tasksprints.auction.product.presentation.ProductController; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.product.domain.dto.request.ProductRequest; import com.tasksprints.auction.product.domain.dto.response.ProductResponse; -import com.tasksprints.auction.product.domain.service.ProductService; +import com.tasksprints.auction.product.application.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From 1eb5f6f3fa933239fe185644092299917a814279 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:03:35 +0900 Subject: [PATCH 37/48] =?UTF-8?q?migrate(review)=20:=20review=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DDD=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/{domain => application}/service/ReviewService.java | 2 +- .../review/application/{ => service}/ReviewServiceImpl.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename src/main/java/com/tasksprints/auction/review/{domain => application}/service/ReviewService.java (88%) rename src/main/java/com/tasksprints/auction/review/application/{ => service}/ReviewServiceImpl.java (94%) diff --git a/src/main/java/com/tasksprints/auction/review/domain/service/ReviewService.java b/src/main/java/com/tasksprints/auction/review/application/service/ReviewService.java similarity index 88% rename from src/main/java/com/tasksprints/auction/review/domain/service/ReviewService.java rename to src/main/java/com/tasksprints/auction/review/application/service/ReviewService.java index cd55da32..313fd07e 100644 --- a/src/main/java/com/tasksprints/auction/review/domain/service/ReviewService.java +++ b/src/main/java/com/tasksprints/auction/review/application/service/ReviewService.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.review.domain.service; +package com.tasksprints.auction.review.application.service; import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; diff --git a/src/main/java/com/tasksprints/auction/review/application/ReviewServiceImpl.java b/src/main/java/com/tasksprints/auction/review/application/service/ReviewServiceImpl.java similarity index 94% rename from src/main/java/com/tasksprints/auction/review/application/ReviewServiceImpl.java rename to src/main/java/com/tasksprints/auction/review/application/service/ReviewServiceImpl.java index 05652283..90648e2e 100644 --- a/src/main/java/com/tasksprints/auction/review/application/ReviewServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/review/application/service/ReviewServiceImpl.java @@ -1,8 +1,7 @@ -package com.tasksprints.auction.review.application; +package com.tasksprints.auction.review.application.service; import com.tasksprints.auction.auction.domain.entity.Auction; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; -import com.tasksprints.auction.review.domain.service.ReviewService; import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; import com.tasksprints.auction.review.domain.entity.Review; From f379d235070a34d2d7362dce4fff28500eb731d5 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:03:56 +0900 Subject: [PATCH 38/48] =?UTF-8?q?migrate(user)=20:=20user=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DDD=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/UserService.java | 5 ++++- .../application/{ => service}/UserServiceImpl.java | 8 ++++++-- .../auction/user/domain/entity/UserRole.java | 13 +++++++++++++ .../auction/user/presentation/UserController.java | 2 +- .../user/application/UserServiceImplTest.java | 2 +- .../user/presentation/UserControllerTest.java | 3 +-- 6 files changed, 26 insertions(+), 7 deletions(-) rename src/main/java/com/tasksprints/auction/user/{domain => application}/service/UserService.java (78%) rename src/main/java/com/tasksprints/auction/user/application/{ => service}/UserServiceImpl.java (90%) create mode 100644 src/main/java/com/tasksprints/auction/user/domain/entity/UserRole.java diff --git a/src/main/java/com/tasksprints/auction/user/domain/service/UserService.java b/src/main/java/com/tasksprints/auction/user/application/service/UserService.java similarity index 78% rename from src/main/java/com/tasksprints/auction/user/domain/service/UserService.java rename to src/main/java/com/tasksprints/auction/user/application/service/UserService.java index 2fcec68b..9c605d86 100644 --- a/src/main/java/com/tasksprints/auction/user/domain/service/UserService.java +++ b/src/main/java/com/tasksprints/auction/user/application/service/UserService.java @@ -1,8 +1,9 @@ -package com.tasksprints.auction.user.domain.service; +package com.tasksprints.auction.user.application.service; import com.tasksprints.auction.user.domain.dto.request.UserRequest; import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; +import com.tasksprints.auction.user.domain.entity.User; import java.util.List; @@ -16,4 +17,6 @@ public interface UserService { UserDetailResponse updateUser(Long id, UserRequest.Update user); void deleteUser(Long id); + + User getUserById(Long id); } diff --git a/src/main/java/com/tasksprints/auction/user/application/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java similarity index 90% rename from src/main/java/com/tasksprints/auction/user/application/UserServiceImpl.java rename to src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java index 96d22fec..93309ade 100644 --- a/src/main/java/com/tasksprints/auction/user/application/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java @@ -1,6 +1,5 @@ -package com.tasksprints.auction.user.application; +package com.tasksprints.auction.user.application.service; -import com.tasksprints.auction.user.domain.service.UserService; import com.tasksprints.auction.user.domain.dto.request.UserRequest; import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; @@ -60,4 +59,9 @@ public void deleteUser(Long id) { userRepository.save(user); // 상태 업데이트를 저장 } + @Override + public User getUserById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); + } } diff --git a/src/main/java/com/tasksprints/auction/user/domain/entity/UserRole.java b/src/main/java/com/tasksprints/auction/user/domain/entity/UserRole.java new file mode 100644 index 00000000..29969ba3 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/user/domain/entity/UserRole.java @@ -0,0 +1,13 @@ +package com.tasksprints.auction.user.domain.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum UserRole { + ADMIN("admin"), + USER("user"); + + private final String userRole; +} diff --git a/src/main/java/com/tasksprints/auction/user/presentation/UserController.java b/src/main/java/com/tasksprints/auction/user/presentation/UserController.java index 1d5d91ec..67522527 100644 --- a/src/main/java/com/tasksprints/auction/user/presentation/UserController.java +++ b/src/main/java/com/tasksprints/auction/user/presentation/UserController.java @@ -5,7 +5,7 @@ import com.tasksprints.auction.user.domain.dto.request.UserRequest; import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; -import com.tasksprints.auction.user.domain.service.UserService; +import com.tasksprints.auction.user.application.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; diff --git a/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java b/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java index 7cdedd27..95ef7830 100644 --- a/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java @@ -5,7 +5,7 @@ import com.tasksprints.auction.user.exception.UserNotFoundException; import com.tasksprints.auction.user.domain.entity.User; import com.tasksprints.auction.user.infrastructure.UserRepository; -import com.tasksprints.auction.user.application.UserServiceImpl; +import com.tasksprints.auction.user.application.service.UserServiceImpl; import org.junit.jupiter.api.*; import org.mockito.InjectMocks; import org.mockito.Mock; diff --git a/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java b/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java index f3cf997d..682dcdd0 100644 --- a/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java +++ b/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java @@ -1,13 +1,12 @@ package com.tasksprints.auction.user.presentation; import com.fasterxml.jackson.databind.ObjectMapper; -import com.tasksprints.auction.user.presentation.UserController; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.user.domain.dto.request.UserRequest; import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; import com.tasksprints.auction.user.exception.UserNotFoundException; -import com.tasksprints.auction.user.domain.service.UserService; +import com.tasksprints.auction.user.application.service.UserService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; From d2ae42881312908033b0d17b61ec0e518b4c0739 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:04:15 +0900 Subject: [PATCH 39/48] =?UTF-8?q?migrate(bid)=20:=20bid=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DDD=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BidService.java | 6 +- .../{ => service}/BidServiceImpl.java | 14 +++- .../auction/bid/domain/dto/BidRequest.java | 1 + .../auction/bid/domain/dto/BidResponse.java | 1 - .../bid/domain/entity/dto/BidRequest.java | 13 --- .../bid/domain/entity/dto/BidResponse.java | 31 ------- .../bid/presentation/BidController.java | 74 +++++++++++++++-- .../auction/bid/BidControllerTest.java | 80 +++++++++++++++++++ 8 files changed, 162 insertions(+), 58 deletions(-) rename src/main/java/com/tasksprints/auction/bid/{domain => application}/service/BidService.java (70%) rename src/main/java/com/tasksprints/auction/bid/application/{ => service}/BidServiceImpl.java (89%) delete mode 100644 src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java delete mode 100644 src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java create mode 100644 src/test/java/com/tasksprints/auction/bid/BidControllerTest.java diff --git a/src/main/java/com/tasksprints/auction/bid/domain/service/BidService.java b/src/main/java/com/tasksprints/auction/bid/application/service/BidService.java similarity index 70% rename from src/main/java/com/tasksprints/auction/bid/domain/service/BidService.java rename to src/main/java/com/tasksprints/auction/bid/application/service/BidService.java index ffabfef1..7ef99a30 100644 --- a/src/main/java/com/tasksprints/auction/bid/domain/service/BidService.java +++ b/src/main/java/com/tasksprints/auction/bid/application/service/BidService.java @@ -1,6 +1,6 @@ -package com.tasksprints.auction.bid.domain.service; +package com.tasksprints.auction.bid.application.service; -import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; +import com.tasksprints.auction.bid.domain.dto.BidResponse; import java.math.BigDecimal; @@ -16,4 +16,6 @@ public interface BidService { Boolean hasUserAlreadyBid(Long auctionId); BidResponse getBidByUuid(String uuid); + + boolean isBidEnd(Long auctionId); } diff --git a/src/main/java/com/tasksprints/auction/bid/application/BidServiceImpl.java b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java similarity index 89% rename from src/main/java/com/tasksprints/auction/bid/application/BidServiceImpl.java rename to src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java index f70763a0..8a3906c3 100644 --- a/src/main/java/com/tasksprints/auction/bid/application/BidServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java @@ -1,6 +1,6 @@ -package com.tasksprints.auction.bid.application; +package com.tasksprints.auction.bid.application.service; -import com.tasksprints.auction.bid.domain.service.BidService; +import com.tasksprints.auction.bid.application.service.BidService; import com.tasksprints.auction.bid.exception.BidNotFoundException; import com.tasksprints.auction.bid.exception.InvalidBidAmountException; import com.tasksprints.auction.bid.domain.entity.Bid; @@ -9,7 +9,7 @@ import com.tasksprints.auction.auction.exception.AuctionNotFoundException; import com.tasksprints.auction.auction.domain.entity.Auction; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; -import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; +import com.tasksprints.auction.bid.domain.dto.BidResponse; import com.tasksprints.auction.user.exception.UserNotFoundException; import com.tasksprints.auction.user.domain.entity.User; import com.tasksprints.auction.user.infrastructure.UserRepository; @@ -96,4 +96,12 @@ public BidResponse getBidByUuid(String uuid) { .orElseThrow(() -> new BidNotFoundException("Bid not found")); return BidResponse.of(bid); } + + @Override + public boolean isBidEnd(Long auctionId) { + Auction auction = auctionRepository.findById(auctionId) + .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); + + return auction.getEndTime().isBefore(LocalDateTime.now()); + } } diff --git a/src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java index e8f5c3e3..474fa65a 100644 --- a/src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java +++ b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidRequest.java @@ -9,5 +9,6 @@ public class BidRequest { private Long userId; private Long auctionId; + private String chatRoomId; private BigDecimal amount; } diff --git a/src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java index c0c38133..56b12302 100644 --- a/src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java +++ b/src/main/java/com/tasksprints/auction/bid/domain/dto/BidResponse.java @@ -1,5 +1,4 @@ package com.tasksprints.auction.bid.domain.dto; - import com.tasksprints.auction.bid.domain.entity.Bid; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java b/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java deleted file mode 100644 index 39e82c39..00000000 --- a/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tasksprints.auction.bid.domain.entity.dto; - -import lombok.Data; - -import java.math.BigDecimal; - - -@Data -public class BidRequest { - private Long userId; - private Long auctionId; - private BigDecimal amount; -} diff --git a/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java b/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java deleted file mode 100644 index 6b324dd6..00000000 --- a/src/main/java/com/tasksprints/auction/bid/domain/entity/dto/BidResponse.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.tasksprints.auction.bid.domain.entity.dto; - -import com.tasksprints.auction.bid.domain.entity.Bid; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class BidResponse { - Long userId; - String name; - Long auctionId; - BigDecimal amount; - String uuid; - - public static BidResponse of(Bid bid) { - return BidResponse.builder() - .userId(bid.getUser().getId()) - .name(bid.getUser().getName()) - .auctionId(bid.getAuction().getId()) - .amount(bid.getAmount()) - .build(); - /** 아이템 목록 추가**/ - } -} diff --git a/src/main/java/com/tasksprints/auction/bid/presentation/BidController.java b/src/main/java/com/tasksprints/auction/bid/presentation/BidController.java index 0cb64555..ad7babaf 100644 --- a/src/main/java/com/tasksprints/auction/bid/presentation/BidController.java +++ b/src/main/java/com/tasksprints/auction/bid/presentation/BidController.java @@ -1,38 +1,96 @@ package com.tasksprints.auction.bid.presentation; +import com.tasksprints.auction.bid.domain.dto.BidRequest; +import com.tasksprints.auction.bid.domain.dto.BidResponse; +import com.tasksprints.auction.bid.application.service.BidService; +import com.tasksprints.auction.chat.application.service.ChatService; +import com.tasksprints.auction.chat.domain.dto.AddChatRoomDto; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.bid.domain.entity.dto.BidRequest; -import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; -import com.tasksprints.auction.bid.domain.service.BidService; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.application.service.UserService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.math.BigDecimal; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; @Controller @RequiredArgsConstructor +@Slf4j @RequestMapping("/api/v1/bid") public class BidController { private final BidService bidService; + private final ChatService chatService; + private final UserService userService; private final SimpMessageSendingOperations simpMessageSendingOperations; - @MessageMapping("/bid") + @MessageMapping("/apply") public void handleBid(BidRequest bidRequest) { /** - * 입찰하는거 여기다가 추가하면 좋을 듯 합니다g. + * 입찰하는거 여기다가 추가하면 좋을 듯 합니다. */ - BidResponse bidResponse = bidService.submitBid(bidRequest.getUserId(), bidRequest.getAuctionId(), bidRequest.getAmount()); - simpMessageSendingOperations.convertAndSend("/bid/"+bidResponse.getUuid(), bidResponse); + UserDetailResponse userDetailResponse = userService.getUserDetailsById(bidRequest.getUserId()); + if (chatService.isUserOwner(bidRequest.getChatRoomId(), userDetailResponse.getId())) { + return; + } + + if (bidService.isBidEnd(bidRequest.getAuctionId())) { + return; + } //경매가 종료되었을 경우 채팅 입력 금지 + + BidResponse bidResponse = bidService.updateBidAmount(bidRequest.getUserId(), bidRequest.getAuctionId(), + bidRequest.getAmount()); + simpMessageSendingOperations.convertAndSend("/bid/" + bidResponse.getUuid(), bidResponse); + } + + @PostMapping() + @Operation(summary = "Submit a bid", description = "Submits a bid for the specified auction.") + @ApiResponse(responseCode = "200", description = "Bid submitted successfully") + public ResponseEntity> submitBid( + @Parameter(description = "auction ID") @RequestParam Long auctionId, + @Parameter(description = "ID of the user submitting the bid") @RequestParam Long userId, + @Parameter(description = "Bid amount") @RequestParam BigDecimal amount) { + BidResponse bid = bidService.submitBid(userId, auctionId, amount); + User user = userService.getUserById(userId); + chatService.createRoom(new AddChatRoomDto(bid.getName(), user)); //입찰 생성 시 채팅방 생성 후 저장 + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_SUBMITTED_SUCCESS, bid)); } - @GetMapping("/{uuid}") + @PutMapping() + @Operation(summary = "Update a bid", description = "Updates the amount of an existing bid.") + @ApiResponse(responseCode = "200", description = "Bid updated successfully") + public ResponseEntity> updateBid( + @Parameter(description = "auctionId") @RequestParam Long auctionId, + @Parameter(description = "ID of the user updating the bid") @RequestParam Long userId, + @Parameter(description = "New bid amount") @RequestParam BigDecimal amount) { + log.info("a"); + BidResponse updatedBid = bidService.updateBidAmount(userId, auctionId, amount); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_UPDATED_SUCCESS, updatedBid)); + } + +// @GetMapping("/{auctionId}/bid/status") +// @Operation(summary = "Check user bid status", description = "Checks if the user has already placed a bid on the auction.") +// @ApiResponse(responseCode = "200", description = "Bid status checked successfully") +// public ResponseEntity> checkUserBidStatus(@PathVariable Long auctionId, +// @Parameter(description = "ID of the user") @RequestParam Long userId) { +// Boolean hasBid = bidService.hasUserAlreadyBid(auctionId); +// return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_STATUS_CHECKED, hasBid)); +// } + + @GetMapping("/bid/{uuid}") @Operation(summary = "Get a bid", description = "Get a bid by bid uuid") @ApiResponse(responseCode = "200", description = "Bid status retrieved successfully") public ResponseEntity> getBidByUuid(@PathVariable(value = "uuid") String uuid) { diff --git a/src/test/java/com/tasksprints/auction/bid/BidControllerTest.java b/src/test/java/com/tasksprints/auction/bid/BidControllerTest.java new file mode 100644 index 00000000..1795cb48 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/bid/BidControllerTest.java @@ -0,0 +1,80 @@ +package com.tasksprints.auction.bid; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.bid.domain.dto.BidResponse; +import com.tasksprints.auction.bid.application.service.BidService; +import com.tasksprints.auction.bid.presentation.BidController; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.chat.application.service.ChatService; +import com.tasksprints.auction.user.application.service.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(BidController.class) +@MockBean(JpaMetamodelMappingContext.class) +public class BidControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + BidService bidService; + + @MockBean + ChatService chatService; + + @MockBean + UserService userService; + @MockBean + private SimpMessageSendingOperations simpMessageSendingOperations; + private ObjectMapper objectMapper; + + @BeforeEach + public void setUp() { + objectMapper = new ObjectMapper(); + } + @Test + @DisplayName("입찰 제출 성공") + public void testSubmitBid_Success() throws Exception { + BidResponse bidDTO = new BidResponse(); // Populate with necessary fields + when(bidService.submitBid(anyLong(), anyLong(), any())).thenReturn(bidDTO); + + mockMvc.perform(post("/api/v1/bid") + .param("auctionId", "1") + .param("userId", "1") + .param("amount", "100.00")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_SUBMITTED_SUCCESS)); + } + @Test + @DisplayName("입찰 금액 업데이트 성공") + public void testUpdateBid_Success() throws Exception { + BidResponse updatedBidDTO = new BidResponse(); // Populate with necessary fields + when(bidService.updateBidAmount(anyLong(), anyLong(), any())).thenReturn(updatedBidDTO); + + mockMvc.perform(put("/api/v1/bid") + .param("auctionId", "1") + .param("userId", "1") + .param("amount", "150.00") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedBidDTO))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_UPDATED_SUCCESS)); + } +} From ecb1d2d280227ff06c3ade16133517bba8bf37f3 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:04:49 +0900 Subject: [PATCH 40/48] =?UTF-8?q?migrate(auction)=20:=20auction=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20DDD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AuctionServiceImpl.java | 1 - .../domain/service/AuctionService.java | 31 --------- .../presentation/AuctionController.java | 66 +++++++------------ .../presentation/AuctionControllerTest.java | 34 +--------- 4 files changed, 27 insertions(+), 105 deletions(-) delete mode 100644 src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java diff --git a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java index 1a51b3d4..7f059082 100644 --- a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java @@ -4,7 +4,6 @@ import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; import com.tasksprints.auction.auction.domain.entity.Auction; import com.tasksprints.auction.auction.domain.entity.AuctionStatus; -import com.tasksprints.auction.auction.domain.service.AuctionService; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; import com.tasksprints.auction.auction.exception.AuctionAlreadyClosedException; import com.tasksprints.auction.auction.exception.AuctionNotFoundException; diff --git a/src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java b/src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java deleted file mode 100644 index 94105e3b..00000000 --- a/src/main/java/com/tasksprints/auction/auction/domain/service/AuctionService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.tasksprints.auction.auction.domain.service; - -import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; -import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; -import com.tasksprints.auction.product.domain.entity.ProductCategory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.List; - -/** - * 사용자가 맞는지 판단도 해야함. - */ -public interface AuctionService { - AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionRequest); - - void closeAuction(Long auctionId); - - String getAuctionStatus(Long auctionId); - - List getAuctionsByUser(Long userId); - - List getAllAuctions(); - - AuctionResponse getAuctionById(Long auctionId); - - Page getAuctionsByFilter(Pageable pageable, AuctionRequest.SearchCondition searchCondition); - - @Deprecated - Page getAuctionsByProductCategory(Pageable pageable, AuctionRequest.SearchCondition searchCondition, ProductCategory category); -} diff --git a/src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java b/src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java index c1eac8d6..810793d6 100644 --- a/src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java +++ b/src/main/java/com/tasksprints/auction/auction/presentation/AuctionController.java @@ -1,16 +1,14 @@ package com.tasksprints.auction.auction.presentation; -import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.common.response.ApiResult; import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; -import com.tasksprints.auction.auction.domain.service.AuctionService; -import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; -import com.tasksprints.auction.bid.domain.service.BidService; +import com.tasksprints.auction.auction.application.service.AuctionService; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; import com.tasksprints.auction.product.domain.entity.ProductCategory; import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; -import com.tasksprints.auction.review.domain.service.ReviewService; +import com.tasksprints.auction.review.application.service.ReviewService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -20,9 +18,13 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.math.BigDecimal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/auction") @@ -30,13 +32,14 @@ @Tag(name = "Auction", description = "Operations related to auctions") public class AuctionController { private final AuctionService auctionService; - private final BidService bidService; private final ReviewService reviewService; @PostMapping @Operation(summary = "Create an auction", description = "Creates a new auction for a user.") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Auction created successfully")}) - public ResponseEntity> createAuction(@Parameter(description = "ID of the user creating the auction") @RequestParam Long userId, @RequestBody AuctionRequest.Create auctionRequest) { + public ResponseEntity> createAuction( + @Parameter(description = "ID of the user creating the auction") @RequestParam Long userId, + @RequestBody AuctionRequest.Create auctionRequest) { AuctionResponse createdAuction = auctionService.createAuction(userId, auctionRequest); return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_CREATED_SUCCESS, createdAuction)); } @@ -58,7 +61,6 @@ public ResponseEntity> getAuctionStatus(@PathVariable Long auc } // 필터로 추가 - // @GetMapping("/") // @Operation(summary = "Get auctions by user", description = "Retrieves all auctions created by a specific user.") // @ApiResponse(responseCode = "200", description = "User auctions retrieved successfully") @@ -82,7 +84,8 @@ public ResponseEntity> getAuctionStatus(@PathVariable Long auc @GetMapping @Operation(summary = "Get all auctions", description = "Retrieves all auctions.") @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") - public ResponseEntity>> getAllAuctions(Pageable pageable, AuctionRequest.SearchCondition searchCondition) { + public ResponseEntity>> getAllAuctions(Pageable pageable, + AuctionRequest.SearchCondition searchCondition) { Page auctions = auctionService.getAuctionsByFilter(pageable, searchCondition); return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.ALL_AUCTIONS_RETRIEVED, auctions)); } @@ -99,46 +102,25 @@ public ResponseEntity> getAuctionById(@PathVariable L @GetMapping("/category/{category}") @Operation(summary = "Get auctions by ProductCategory", description = "Retrieve all auction by its ProductCategory.") @ApiResponse(responseCode = "200", description = "All auctions retrieved successfully") - public ResponseEntity>> getAuctionByProductCategory(Pageable pageable, @PathVariable String category, AuctionRequest.SearchCondition searchCondition) { - Page auctions = auctionService.getAuctionsByProductCategory(pageable, searchCondition, ProductCategory.fromDisplayName(category)); + public ResponseEntity>> getAuctionByProductCategory(Pageable pageable, + @PathVariable String category, + AuctionRequest.SearchCondition searchCondition) { + Page auctions = auctionService.getAuctionsByProductCategory(pageable, searchCondition, + ProductCategory.fromDisplayName(category)); return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.AUCTION_RETRIEVED, auctions)); } - // Bid Endpoints - @PostMapping("/{auctionId}/bid") - @Operation(summary = "Submit a bid", description = "Submits a bid for the specified auction.") - @ApiResponse(responseCode = "200", description = "Bid submitted successfully") - public ResponseEntity> submitBid(@Parameter(description = "ID of the user submitting the bid") @RequestParam Long userId, @PathVariable Long auctionId, @Parameter(description = "Bid amount") @RequestParam BigDecimal amount) { - BidResponse bid = bidService.submitBid(userId, auctionId, amount); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_SUBMITTED_SUCCESS, bid)); - } - - @PutMapping("/{auctionId}/bid") - @Operation(summary = "Update a bid", description = "Updates the amount of an existing bid.") - @ApiResponse(responseCode = "200", description = "Bid updated successfully") - public ResponseEntity> updateBid(@Parameter(description = "ID of the user updating the bid") @RequestParam Long userId, @PathVariable Long auctionId, @Parameter(description = "New bid amount") @RequestParam BigDecimal amount) { - BidResponse updatedBid = bidService.updateBidAmount(userId, auctionId, amount); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_UPDATED_SUCCESS, updatedBid)); - } - - @GetMapping("/{auctionId}/bid/status") - @Operation(summary = "Check user bid status", description = "Checks if the user has already placed a bid on the auction.") - @ApiResponse(responseCode = "200", description = "Bid status checked successfully") - public ResponseEntity> checkUserBidStatus(@PathVariable Long auctionId, @Parameter(description = "ID of the user") @RequestParam Long userId) { - Boolean hasBidded = bidService.hasUserAlreadyBid(auctionId); - return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.BID_STATUS_CHECKED, hasBidded)); - } - // Review Endpoints @PostMapping("/{auctionId}/review") @Operation(summary = "Create a review", description = "Creates a review for a specific auction.") @ApiResponse(responseCode = "200", description = "Review created successfully") - public ResponseEntity> createReview(@Parameter(description = "ID of the user creating the review") @RequestParam Long userId, @PathVariable Long auctionId, @RequestBody ReviewRequest.Create reviewRequest) { + public ResponseEntity> createReview( + @Parameter(description = "ID of the user creating the review") @RequestParam Long userId, + @PathVariable Long auctionId, @RequestBody ReviewRequest.Create reviewRequest) { ReviewResponse createdReview = reviewService.createReview(userId, auctionId, reviewRequest); return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.REVIEW_CREATED_SUCCESS, createdReview)); } - @GetMapping("/{auctionId}/review") @Operation(summary = "Get review by auction ID", description = "Retrieves the review for a specific auction.") @ApiResponse(responseCode = "200", description = "Review retrieved successfully") diff --git a/src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java b/src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java index 0a73c90f..def2047e 100644 --- a/src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java +++ b/src/test/java/com/tasksprints/auction/auction/presentation/AuctionControllerTest.java @@ -1,18 +1,16 @@ package com.tasksprints.auction.auction.presentation; import com.fasterxml.jackson.databind.ObjectMapper; -import com.tasksprints.auction.auction.presentation.AuctionController; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; import com.tasksprints.auction.auction.domain.entity.AuctionCategory; import com.tasksprints.auction.auction.domain.entity.AuctionStatus; -import com.tasksprints.auction.auction.domain.service.AuctionService; -import com.tasksprints.auction.bid.domain.entity.dto.BidResponse; -import com.tasksprints.auction.bid.domain.service.BidService; +import com.tasksprints.auction.auction.application.service.AuctionService; +import com.tasksprints.auction.bid.application.service.BidService; import com.tasksprints.auction.review.domain.dto.request.ReviewRequest; import com.tasksprints.auction.review.domain.dto.response.ReviewResponse; -import com.tasksprints.auction.review.domain.service.ReviewService; +import com.tasksprints.auction.review.application.service.ReviewService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -159,32 +157,6 @@ public void testFindAuctionByUsingQueryString_Success() throws Exception { // // } - @Test - @DisplayName("입찰 제출 성공") - public void testSubmitBid_Success() throws Exception { - BidResponse bidDTO = new BidResponse(); // Populate with necessary fields - when(bidService.submitBid(anyLong(), anyLong(), any())).thenReturn(bidDTO); - - mockMvc.perform(post("/api/v1/auction/1/bid") - .param("userId", "1") - .param("amount", "100.00")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_SUBMITTED_SUCCESS)); - } - - @Test - @DisplayName("입찰 금액 업데이트 성공") - public void testUpdateBid_Success() throws Exception { - BidResponse updatedBidDTO = new BidResponse(); // Populate with necessary fields - when(bidService.updateBidAmount(anyLong(), anyLong(), any())).thenReturn(updatedBidDTO); - - mockMvc.perform(put("/api/v1/auction/1/bid") - .param("userId", "1") - .param("amount", "150.00")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.BID_UPDATED_SUCCESS)); - } - @Test @DisplayName("리뷰 생성 성공") public void testCreateReview_Success() throws Exception { From cd60215f011e7708071dbc648f74ec944ef1fe50 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:05:02 +0900 Subject: [PATCH 41/48] =?UTF-8?q?migrate(auction)=20:=20auction=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20DDD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/AuctionService.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/auction/application/service/AuctionService.java diff --git a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionService.java b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionService.java new file mode 100644 index 00000000..3ff947a3 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionService.java @@ -0,0 +1,31 @@ +package com.tasksprints.auction.auction.application.service; + +import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; +import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; +import com.tasksprints.auction.product.domain.entity.ProductCategory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * 사용자가 맞는지 판단도 해야함. + */ +public interface AuctionService { + AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionRequest); + + void closeAuction(Long auctionId); + + String getAuctionStatus(Long auctionId); + + List getAuctionsByUser(Long userId); + + List getAllAuctions(); + + AuctionResponse getAuctionById(Long auctionId); + + Page getAuctionsByFilter(Pageable pageable, AuctionRequest.SearchCondition searchCondition); + + @Deprecated + Page getAuctionsByProductCategory(Pageable pageable, AuctionRequest.SearchCondition searchCondition, ProductCategory category); +} From 6254b2f710d6c1bcd43d537397532d888b8ac775 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:05:41 +0900 Subject: [PATCH 42/48] =?UTF-8?q?config=20:=20config=20=EA=B0=84=EC=9D=98?= =?UTF-8?q?=20conflict=20=EC=99=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ build.gradle | 4 ++++ .../auction/common/config/WebSockConfig.java | 10 ++++++++-- src/main/resources/application.yml | 3 +++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c2065bc2..8daee5e0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### yml file ### +/src/main/resources/application-jwt.yml diff --git a/build.gradle b/build.gradle index 3192d57a..49842485 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,10 @@ dependencies { //thymeleaf implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + + //jjwt + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' } tasks.named('test') { useJUnitPlatform() diff --git a/src/main/java/com/tasksprints/auction/common/config/WebSockConfig.java b/src/main/java/com/tasksprints/auction/common/config/WebSockConfig.java index b2cb4ae6..2fd79b21 100644 --- a/src/main/java/com/tasksprints/auction/common/config/WebSockConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/WebSockConfig.java @@ -12,10 +12,16 @@ @EnableWebSocketMessageBroker public class WebSockConfig implements WebSocketMessageBrokerConfigurer { + /* + /topic : 채팅방 전체 메시지 (공지) 경로 + /bid : bid 메시지 (주로 bid 금액 업데이트 관련) 경로 + /whisper : 귓속말 경로 + */ + @Override public void configureMessageBroker(MessageBrokerRegistry brokerRegistry) { - brokerRegistry.enableSimpleBroker("/sub"); //server - brokerRegistry.setApplicationDestinationPrefixes("/pub"); //client + brokerRegistry.enableSimpleBroker("/topic", "/bid", "/whisper"); + brokerRegistry.setApplicationDestinationPrefixes("/app"); } @Override diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a6bb2474..99934123 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -29,6 +29,9 @@ spring: mode: HTML encoding: UTF-8 cache: false + profiles: + include: + -jwt springdoc: api-docs: From e7f59806e5fa06322334a5d23f5774c62adf64ee Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:06:28 +0900 Subject: [PATCH 43/48] =?UTF-8?q?refactor(jwt)=20:=20jwt=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/common/jwt/JwtProperties.java | 27 +++++ .../auction/common/jwt/JwtProvider.java | 59 +++++++++ .../auction/common/jwt/JwtUtil.java | 13 ++ .../common/jwt/dto/response/JwtResponse.java | 19 +++ .../auction/common/jwt/JwtProviderTest.java | 114 ++++++++++++++++++ 5 files changed, 232 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/JwtUtil.java create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java create mode 100644 src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java new file mode 100644 index 00000000..e87ce1d1 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java @@ -0,0 +1,27 @@ +package com.tasksprints.auction.common.jwt; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Getter +public class JwtProperties { + @Value("${jwt.header}") + private String header; + + @Value("${jwt.prefix}") + private String prefix; + + @Value("${jwt.expire-ms}") + private Long expireMs; + + @Value("${jwt.expire-ms}") + private Long refreshExpireMs; + + @Value("${jwt.issuer}") + private String issuer; + + @Value("${jwt.secret}") + private String secretKey; +} diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java new file mode 100644 index 00000000..033e8ead --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -0,0 +1,59 @@ +package com.tasksprints.auction.common.jwt; + +import static com.tasksprints.auction.common.util.TimeUtil.*; + +import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.time.Clock; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtProvider { + + private final JwtProperties jwtProperties; + private final Clock clock; + + public JwtResponse generateToken(Long userId, String userRole) { + return JwtResponse.of(createAccessToken(userId, userRole), createRefreshToken()); + } + + public String createAccessToken(Long userId, String userRole) { + + Date now = localDateTimeToDate(LocalDateTime.now(clock)); + + return Jwts.builder().setIssuer(jwtProperties.getIssuer()).claim("userId", userId).claim("userRole", userRole) + .setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtProperties.getExpireMs())) + .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); + } + + public String createRefreshToken() { + + Date now = localDateTimeToDate(LocalDateTime.now(clock)); + + return Jwts.builder().setIssuer(jwtProperties.getIssuer()).setIssuedAt(now) + .setExpiration(new Date(now.getTime() + jwtProperties.getRefreshExpireMs())) + .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); + } + + public boolean verifyToken(String token) { + + Date now = localDateTimeToDate(LocalDateTime.now(clock)); + + Claims claims = getClaims(token); + + return !claims.getExpiration().before(now); + } + + public Claims getClaims(String token) { + return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())) + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtUtil.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtUtil.java new file mode 100644 index 00000000..4704d489 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtUtil.java @@ -0,0 +1,13 @@ +package com.tasksprints.auction.common.jwt; + +import java.nio.charset.StandardCharsets; + +public class JwtUtil { + + /** + * secret-key 를 인코딩 합니다. + * */ + public static byte[] encodeSecretKey(String secretKey) { + return secretKey.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java b/src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java new file mode 100644 index 00000000..1d209eca --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java @@ -0,0 +1,19 @@ +package com.tasksprints.auction.common.jwt.dto.response; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JwtResponse { + private String refreshToken; + private String accessToken; + + public static JwtResponse of(String accessToken, String refreshToken) { + return JwtResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java new file mode 100644 index 00000000..ea1bafc5 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -0,0 +1,114 @@ +package com.tasksprints.auction.common.jwt; + +import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; +import io.jsonwebtoken.ExpiredJwtException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class JwtProviderTest { + @Mock + private JwtProperties jwtProperties; + @Mock + private Clock clock; + @InjectMocks + private JwtProvider jwtProvider; + + private final Long VALID_EXPIRE_MS = 36000000L; + private final Long REFRESH_EXPIRE_MS = 72000000L; + private final Long EXPIRED_EXPIRE_MS = 0L; + private final String ISSUER = "testIssuer"; + private final String SECRET_KEY = "testSecretKey"; + private final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul"); + + @BeforeEach + public void setUp() { + when(clock.instant()).thenReturn(Instant.now()); + when(clock.getZone()).thenReturn(ZONE_ID); + when(jwtProperties.getIssuer()).thenReturn(ISSUER); + when(jwtProperties.getSecretKey()).thenReturn(SECRET_KEY); + } + + private void stubAccessTokenExpiration(Long expireMs) { + when(jwtProperties.getExpireMs()).thenReturn(expireMs); + } + + private void stubRefreshTokenExpiration(Long expireMs) { + when(jwtProperties.getRefreshExpireMs()).thenReturn(expireMs); + } + + @Test + @DisplayName("token generator 을 통한 access token, refresh token 발급 테스트") + void generateToken() { + JwtResponse jwtResponse = jwtProvider.generateToken(1L, "admin"); + + assertNotNull(jwtResponse.getAccessToken(), "access token 이 발급되어야 합니다."); + assertNotNull(jwtResponse.getRefreshToken(), "refresh token 이 발급되어야 합니다."); + } + + @Test + @DisplayName("access token 발급 테스트") + void createAccessToken() { + stubAccessTokenExpiration(VALID_EXPIRE_MS); + + String token = jwtProvider.createAccessToken(1L, "admin"); + + assertNotNull(token, "access token 이 발급되어야 합니다."); + } + + @Test + @DisplayName("refresh token 발급 테스트") + void createRefreshToken() { + stubRefreshTokenExpiration(REFRESH_EXPIRE_MS); + String token = jwtProvider.createRefreshToken(); + assertNotNull(token, "refresh token 이 발급되어야 합니다."); + } + + @Test + @DisplayName("유효한 토큰 테스트") + void verifyToken_valid() { + stubAccessTokenExpiration(VALID_EXPIRE_MS); + + String token = jwtProvider.createAccessToken(1L, "admin"); + + Assertions.assertTrue(jwtProvider.verifyToken(token)); + } + + @Test + @DisplayName("만료된 토큰 테스트") + void verifyToken_expired() { + stubAccessTokenExpiration(EXPIRED_EXPIRE_MS); + + String token = jwtProvider.createAccessToken(1L, "admin"); + + Assertions.assertThrows(ExpiredJwtException.class, () -> { + jwtProvider.verifyToken(token); + }, "토큰이 즉시 만료되어야 합니다."); + } + + @Test + @DisplayName("디코딩 된 페이로드 정확성 테스트") + void getClaims() { + stubAccessTokenExpiration(VALID_EXPIRE_MS); + + String token = jwtProvider.createAccessToken(1L, "admin"); + Long decodedUserId = jwtProvider.getClaims(token).get("userId", Long.class); + String decodedUserRole = jwtProvider.getClaims(token).get("userRole", String.class); + + assertThat(decodedUserId).isEqualTo(1L); + assertThat(decodedUserRole).isEqualTo("admin"); + } +} From c74204626faf2e3b295abcab4d131cf390350e85 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:06:50 +0900 Subject: [PATCH 44/48] =?UTF-8?q?refactor(clock)=20:=20clock,=20time=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=ED=86=B5=ED=95=A9(TZ?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/common/config/ClockConfig.java | 14 ++++++ .../auction/common/util/TimeUtil.java | 13 ++++++ .../auction/common/util/TimeUtilTest.java | 43 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/common/config/ClockConfig.java create mode 100644 src/main/java/com/tasksprints/auction/common/util/TimeUtil.java create mode 100644 src/test/java/com/tasksprints/auction/common/util/TimeUtilTest.java diff --git a/src/main/java/com/tasksprints/auction/common/config/ClockConfig.java b/src/main/java/com/tasksprints/auction/common/config/ClockConfig.java new file mode 100644 index 00000000..1757f93a --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/config/ClockConfig.java @@ -0,0 +1,14 @@ +package com.tasksprints.auction.common.config; + +import java.time.Clock; +import java.time.ZoneId; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ClockConfig { + @Bean + public Clock clock() { + return Clock.system(ZoneId.of("Asia/seoul")); + } +} diff --git a/src/main/java/com/tasksprints/auction/common/util/TimeUtil.java b/src/main/java/com/tasksprints/auction/common/util/TimeUtil.java new file mode 100644 index 00000000..6da05171 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/util/TimeUtil.java @@ -0,0 +1,13 @@ +package com.tasksprints.auction.common.util; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +public class TimeUtil { + private static final String zoneId = "Asia/Seoul"; + + public static Date localDateTimeToDate(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneId.of(zoneId)).toInstant()); + } +} diff --git a/src/test/java/com/tasksprints/auction/common/util/TimeUtilTest.java b/src/test/java/com/tasksprints/auction/common/util/TimeUtilTest.java new file mode 100644 index 00000000..990213b6 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/common/util/TimeUtilTest.java @@ -0,0 +1,43 @@ +package com.tasksprints.auction.common.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TimeUtilTest { + private final ZoneId zoneId = ZoneId.of("Asia/Seoul"); + private final Instant fixedInstant = Instant.parse("2024-10-22T10:00:00Z"); + @Mock + private Clock clock; + + @BeforeEach + void setUp() { + when(clock.instant()).thenReturn(fixedInstant); + when(clock.getZone()).thenReturn(zoneId); + } + + @Test + @DisplayName("LocalDateTime -> Date 변환 테스트") + void localDateTimeToDate() { + // given + LocalDateTime localDateTime = LocalDateTime.now(clock); + + // when + Date date = TimeUtil.localDateTimeToDate(localDateTime); + + // then + assertEquals(localDateTime.atZone(zoneId).toInstant(), date.toInstant()); + } +} From 34e2cf0cc0e1e8c64355b3d1ee8077403c9d5eaa Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:41:19 +0900 Subject: [PATCH 45/48] =?UTF-8?q?migrate(payment)=20:=20payment=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=20DDD=20=EC=95=84=ED=82=A4?= =?UTF-8?q?=ED=85=8D=EC=B2=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/payment/api/Response.java | 2 +- .../application/service/PaymentService.java | 6 ++--- .../service/PaymentServiceImpl.java | 20 ++++++++-------- .../domain/dto/request/PaymentRequest.java | 6 ++--- .../dto/response/PaymentErrorResponse.java | 2 +- .../domain/dto/response/PaymentResponse.java | 2 +- .../payment/domain/entity/PayStatus.java | 2 +- .../payment/domain/entity/PayType.java | 2 +- .../payment/domain/entity/Payment.java | 6 ++--- .../exception/InvalidSessionException.java | 2 +- .../PaymentDataMismatchException.java | 2 +- .../PaymentUserNotFoundException.java | 2 +- .../infrastructure/PaymentRepository.java | 4 ++-- .../infrastructure/client/ClientWrapper.java | 4 ++-- .../client/HttpClientWrapper.java | 4 ++-- .../client/PaymentApiSerializer.java | 6 ++--- .../client/TossPaymentImpl.java | 10 ++++---- .../presentation/PaymentController.java | 14 +++++------ .../application/PaymentServiceImplTest.java | 23 ++++++++++--------- .../infrastructure/PaymentRepositoryTest.java | 11 +++++---- .../client/ClientWrapperTest.java | 5 ++-- .../client/TossPaymentImplTest.java | 13 +++++++---- .../presentation/PaymentControllerTest.java | 16 ++++++------- 23 files changed, 85 insertions(+), 79 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/payment/api/Response.java b/src/main/java/com/tasksprints/auction/payment/api/Response.java index 41482d29..ef8e1db7 100644 --- a/src/main/java/com/tasksprints/auction/payment/api/Response.java +++ b/src/main/java/com/tasksprints/auction/payment/api/Response.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.api; +package com.tasksprints.auction.payment.api; import lombok.Getter; diff --git a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java index fc9f65ca..fe2c6e70 100644 --- a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java +++ b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java @@ -1,8 +1,8 @@ -package com.tasksprints.auction.domain.payment.service; +package com.tasksprints.auction.payment.application.service; -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; import jakarta.servlet.http.HttpSession; import java.io.IOException; diff --git a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java index 6940efe2..1d92715f 100644 --- a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java @@ -1,13 +1,13 @@ -package com.tasksprints.auction.domain.payment.service; +package com.tasksprints.auction.payment.application.service; -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.client.PaymentApiSerializer; -import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; -import com.tasksprints.auction.domain.payment.model.Payment; -import com.tasksprints.auction.domain.payment.repository.PaymentRepository; -import com.tasksprints.auction.domain.wallet.model.Wallet; -import com.tasksprints.auction.domain.wallet.service.WalletService; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.infrastructure.client.PaymentApiSerializer; +import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; +import com.tasksprints.auction.payment.domain.entity.Payment; +import com.tasksprints.auction.payment.infrastructure.PaymentRepository; +import com.tasksprints.auction.wallet.domain.entity.Wallet; +import com.tasksprints.auction.wallet.application.service.WalletService; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -66,7 +66,7 @@ private Response handlePaymentSuccess(Long userId, PaymentRequest.Confir } } - Response handlePaymentFailure(Response response) { + public Response handlePaymentFailure(Response response) { log.info("결제 실패"); return response; } diff --git a/src/main/java/com/tasksprints/auction/payment/domain/dto/request/PaymentRequest.java b/src/main/java/com/tasksprints/auction/payment/domain/dto/request/PaymentRequest.java index 8fee6d93..8bc00c67 100644 --- a/src/main/java/com/tasksprints/auction/payment/domain/dto/request/PaymentRequest.java +++ b/src/main/java/com/tasksprints/auction/payment/domain/dto/request/PaymentRequest.java @@ -1,7 +1,7 @@ -package com.tasksprints.auction.domain.payment.dto.request; +package com.tasksprints.auction.payment.domain.dto.request; -import com.tasksprints.auction.domain.payment.model.PayType; -import com.tasksprints.auction.domain.payment.model.Payment; +import com.tasksprints.auction.payment.domain.entity.PayType; +import com.tasksprints.auction.payment.domain.entity.Payment; import lombok.*; import java.math.BigDecimal; diff --git a/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentErrorResponse.java b/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentErrorResponse.java index c24ac52d..bb0193b1 100644 --- a/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentErrorResponse.java +++ b/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentErrorResponse.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.dto.response; +package com.tasksprints.auction.payment.domain.dto.response; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentResponse.java b/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentResponse.java index 7aaab351..6e810fcb 100644 --- a/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentResponse.java +++ b/src/main/java/com/tasksprints/auction/payment/domain/dto/response/PaymentResponse.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.dto.response; +package com.tasksprints.auction.payment.domain.dto.response; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/tasksprints/auction/payment/domain/entity/PayStatus.java b/src/main/java/com/tasksprints/auction/payment/domain/entity/PayStatus.java index e3de7c55..e1547511 100644 --- a/src/main/java/com/tasksprints/auction/payment/domain/entity/PayStatus.java +++ b/src/main/java/com/tasksprints/auction/payment/domain/entity/PayStatus.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.model; +package com.tasksprints.auction.payment.domain.entity; import lombok.Getter; diff --git a/src/main/java/com/tasksprints/auction/payment/domain/entity/PayType.java b/src/main/java/com/tasksprints/auction/payment/domain/entity/PayType.java index ae4a3d3f..7c4ec703 100644 --- a/src/main/java/com/tasksprints/auction/payment/domain/entity/PayType.java +++ b/src/main/java/com/tasksprints/auction/payment/domain/entity/PayType.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.model; +package com.tasksprints.auction.payment.domain.entity; import lombok.Getter; diff --git a/src/main/java/com/tasksprints/auction/payment/domain/entity/Payment.java b/src/main/java/com/tasksprints/auction/payment/domain/entity/Payment.java index 4669e460..746de281 100644 --- a/src/main/java/com/tasksprints/auction/payment/domain/entity/Payment.java +++ b/src/main/java/com/tasksprints/auction/payment/domain/entity/Payment.java @@ -1,8 +1,8 @@ -package com.tasksprints.auction.domain.payment.model; +package com.tasksprints.auction.payment.domain.entity; import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; -import com.tasksprints.auction.domain.wallet.model.Wallet; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; +import com.tasksprints.auction.wallet.domain.entity.Wallet; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/tasksprints/auction/payment/exception/InvalidSessionException.java b/src/main/java/com/tasksprints/auction/payment/exception/InvalidSessionException.java index 4342867a..3b774d8c 100644 --- a/src/main/java/com/tasksprints/auction/payment/exception/InvalidSessionException.java +++ b/src/main/java/com/tasksprints/auction/payment/exception/InvalidSessionException.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.exception; +package com.tasksprints.auction.payment.exception; public class InvalidSessionException extends RuntimeException { public InvalidSessionException(String message) { diff --git a/src/main/java/com/tasksprints/auction/payment/exception/PaymentDataMismatchException.java b/src/main/java/com/tasksprints/auction/payment/exception/PaymentDataMismatchException.java index de29bb44..072da633 100644 --- a/src/main/java/com/tasksprints/auction/payment/exception/PaymentDataMismatchException.java +++ b/src/main/java/com/tasksprints/auction/payment/exception/PaymentDataMismatchException.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.exception; +package com.tasksprints.auction.payment.exception; public class PaymentDataMismatchException extends RuntimeException { public PaymentDataMismatchException(String message) { diff --git a/src/main/java/com/tasksprints/auction/payment/exception/PaymentUserNotFoundException.java b/src/main/java/com/tasksprints/auction/payment/exception/PaymentUserNotFoundException.java index d840b871..fa892e10 100644 --- a/src/main/java/com/tasksprints/auction/payment/exception/PaymentUserNotFoundException.java +++ b/src/main/java/com/tasksprints/auction/payment/exception/PaymentUserNotFoundException.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.payment.exception; +package com.tasksprints.auction.payment.exception; public class PaymentUserNotFoundException extends RuntimeException { public PaymentUserNotFoundException(String message) { diff --git a/src/main/java/com/tasksprints/auction/payment/infrastructure/PaymentRepository.java b/src/main/java/com/tasksprints/auction/payment/infrastructure/PaymentRepository.java index cbc9456c..e513d984 100644 --- a/src/main/java/com/tasksprints/auction/payment/infrastructure/PaymentRepository.java +++ b/src/main/java/com/tasksprints/auction/payment/infrastructure/PaymentRepository.java @@ -1,6 +1,6 @@ -package com.tasksprints.auction.domain.payment.repository; +package com.tasksprints.auction.payment.infrastructure; -import com.tasksprints.auction.domain.payment.model.Payment; +import com.tasksprints.auction.payment.domain.entity.Payment; import org.springframework.data.jpa.repository.JpaRepository; public interface PaymentRepository extends JpaRepository { diff --git a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapper.java b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapper.java index fafee92d..d83db2eb 100644 --- a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapper.java +++ b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapper.java @@ -1,6 +1,6 @@ -package com.tasksprints.auction.domain.payment.client; +package com.tasksprints.auction.payment.infrastructure.client; -import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.payment.api.Response; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/HttpClientWrapper.java b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/HttpClientWrapper.java index 9471b6f3..f18635ad 100644 --- a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/HttpClientWrapper.java +++ b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/HttpClientWrapper.java @@ -1,6 +1,6 @@ -package com.tasksprints.auction.domain.payment.client; +package com.tasksprints.auction.payment.infrastructure.client; -import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.payment.api.Response; import java.io.IOException; import java.net.http.HttpRequest; diff --git a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/PaymentApiSerializer.java b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/PaymentApiSerializer.java index 79cd5aff..ad0cc63e 100644 --- a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/PaymentApiSerializer.java +++ b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/PaymentApiSerializer.java @@ -1,7 +1,7 @@ -package com.tasksprints.auction.domain.payment.client; +package com.tasksprints.auction.payment.infrastructure.client; -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; import java.io.IOException; diff --git a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImpl.java b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImpl.java index d2d74101..8bd5dba2 100644 --- a/src/main/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImpl.java +++ b/src/main/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImpl.java @@ -1,13 +1,13 @@ -package com.tasksprints.auction.domain.payment.client; +package com.tasksprints.auction.payment.infrastructure.client; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.tasksprints.auction.common.properties.PaymentProperties; -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; -import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; +import com.tasksprints.auction.payment.domain.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java index 82dbd29f..35abe18a 100644 --- a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java +++ b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java @@ -1,13 +1,13 @@ -package com.tasksprints.auction.api.payment; +package com.tasksprints.auction.payment.presentation; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; -import com.tasksprints.auction.domain.payment.exception.InvalidSessionException; -import com.tasksprints.auction.domain.payment.exception.PaymentDataMismatchException; -import com.tasksprints.auction.domain.payment.service.PaymentService; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; +import com.tasksprints.auction.payment.exception.InvalidSessionException; +import com.tasksprints.auction.payment.exception.PaymentDataMismatchException; +import com.tasksprints.auction.payment.application.service.PaymentService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpSession; diff --git a/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java b/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java index ededbd97..7151f42f 100644 --- a/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java @@ -1,14 +1,15 @@ -package com.tasksprints.auction.domain.payment.service; - -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.client.PaymentApiSerializer; -import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; -import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; -import com.tasksprints.auction.domain.payment.model.Payment; -import com.tasksprints.auction.domain.payment.repository.PaymentRepository; -import com.tasksprints.auction.domain.wallet.model.Wallet; -import com.tasksprints.auction.domain.wallet.service.WalletService; +package com.tasksprints.auction.payment.application; + +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.infrastructure.client.PaymentApiSerializer; +import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; +import com.tasksprints.auction.payment.domain.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; +import com.tasksprints.auction.payment.domain.entity.Payment; +import com.tasksprints.auction.payment.infrastructure.PaymentRepository; +import com.tasksprints.auction.payment.application.service.PaymentServiceImpl; +import com.tasksprints.auction.wallet.domain.entity.Wallet; +import com.tasksprints.auction.wallet.application.service.WalletService; import com.tasksprints.auction.user.domain.entity.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/tasksprints/auction/payment/infrastructure/PaymentRepositoryTest.java b/src/test/java/com/tasksprints/auction/payment/infrastructure/PaymentRepositoryTest.java index 4aecc0b7..e6dd0b86 100644 --- a/src/test/java/com/tasksprints/auction/payment/infrastructure/PaymentRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/payment/infrastructure/PaymentRepositoryTest.java @@ -1,10 +1,11 @@ -package com.tasksprints.auction.domain.payment.repository; +package com.tasksprints.auction.payment.infrastructure; import com.tasksprints.auction.common.config.QueryDslConfig; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; -import com.tasksprints.auction.domain.payment.model.PayStatus; -import com.tasksprints.auction.domain.payment.model.PayType; -import com.tasksprints.auction.domain.payment.model.Payment; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; +import com.tasksprints.auction.payment.domain.entity.PayStatus; +import com.tasksprints.auction.payment.domain.entity.PayType; +import com.tasksprints.auction.payment.domain.entity.Payment; +import com.tasksprints.auction.payment.infrastructure.PaymentRepository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapperTest.java b/src/test/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapperTest.java index 8ea3231b..d2d0a234 100644 --- a/src/test/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapperTest.java +++ b/src/test/java/com/tasksprints/auction/payment/infrastructure/client/ClientWrapperTest.java @@ -1,6 +1,7 @@ -package com.tasksprints.auction.domain.payment.client; +package com.tasksprints.auction.payment.infrastructure.client; -import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.infrastructure.client.ClientWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImplTest.java b/src/test/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImplTest.java index 90d4ddae..af9c93d4 100644 --- a/src/test/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImplTest.java +++ b/src/test/java/com/tasksprints/auction/payment/infrastructure/client/TossPaymentImplTest.java @@ -1,11 +1,14 @@ -package com.tasksprints.auction.domain.payment.client; +package com.tasksprints.auction.payment.infrastructure.client; import com.fasterxml.jackson.databind.ObjectMapper; import com.tasksprints.auction.common.properties.PaymentProperties; -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; -import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.infrastructure.client.HttpClientWrapper; +import com.tasksprints.auction.payment.infrastructure.client.PaymentApiSerializer; +import com.tasksprints.auction.payment.infrastructure.client.TossPaymentImpl; +import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; +import com.tasksprints.auction.payment.domain.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java index 076c1f3c..771a3590 100644 --- a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java +++ b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java @@ -1,13 +1,13 @@ -package com.tasksprints.auction.api; +package com.tasksprints.auction.payment.presentation; -import com.tasksprints.auction.api.payment.PaymentController; +import com.tasksprints.auction.payment.presentation.PaymentController; import com.tasksprints.auction.common.constant.ApiResponseMessages; -import com.tasksprints.auction.domain.payment.api.Response; -import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; -import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; -import com.tasksprints.auction.domain.payment.exception.InvalidSessionException; -import com.tasksprints.auction.domain.payment.exception.PaymentDataMismatchException; -import com.tasksprints.auction.domain.payment.service.PaymentService; +import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.domain.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; +import com.tasksprints.auction.payment.exception.InvalidSessionException; +import com.tasksprints.auction.payment.exception.PaymentDataMismatchException; +import com.tasksprints.auction.payment.application.service.PaymentService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; From a3201843eab1e94acc1a95a8f625280669d1cee5 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:43:36 +0900 Subject: [PATCH 46/48] =?UTF-8?q?refactor(user)=20:=20wallet=20=EA=B4=80?= =?UTF-8?q?=EA=B3=84=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20import=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/service/UserService.java | 10 +++--- .../application/service/UserServiceImpl.java | 16 ++++----- .../auction/user/domain/entity/User.java | 35 ++++++++++++++++--- .../user/application/UserServiceImplTest.java | 31 ++++++++++------ 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/user/application/service/UserService.java b/src/main/java/com/tasksprints/auction/user/application/service/UserService.java index 2724c622..9c605d86 100644 --- a/src/main/java/com/tasksprints/auction/user/application/service/UserService.java +++ b/src/main/java/com/tasksprints/auction/user/application/service/UserService.java @@ -1,10 +1,10 @@ -package com.tasksprints.auction.domain.user.service; +package com.tasksprints.auction.user.application.service; -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.dto.response.UserSummaryResponse; +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; +import com.tasksprints.auction.user.domain.entity.User; -import com.tasksprints.auction.domain.user.model.User; import java.util.List; public interface UserService { diff --git a/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java index 867c3375..a6e4b75d 100644 --- a/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java @@ -1,11 +1,11 @@ -package com.tasksprints.auction.domain.user.service; +package com.tasksprints.auction.user.application.service; -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.dto.response.UserSummaryResponse; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.dto.response.UserSummaryResponse; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.infrastructure.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -38,7 +38,7 @@ public List getUsersSummary() { List foundUsers = userRepository.findAll(); return foundUsers.stream() .map(UserSummaryResponse::new) - .collect(Collectors.toList()); + .toList(); } @Override diff --git a/src/main/java/com/tasksprints/auction/user/domain/entity/User.java b/src/main/java/com/tasksprints/auction/user/domain/entity/User.java index 4db7e595..de95660f 100644 --- a/src/main/java/com/tasksprints/auction/user/domain/entity/User.java +++ b/src/main/java/com/tasksprints/auction/user/domain/entity/User.java @@ -1,7 +1,8 @@ -package com.tasksprints.auction.domain.user.model; +package com.tasksprints.auction.user.domain.entity; +import com.tasksprints.auction.auction.domain.entity.Auction; import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; -import com.tasksprints.auction.domain.auction.model.Auction; +import com.tasksprints.auction.wallet.domain.entity.Wallet; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.SQLRestriction; @@ -40,22 +41,43 @@ public class User extends BaseEntityWithUpdate { @OneToMany(mappedBy = "seller", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @Builder.Default private List auctions = new ArrayList<>(); + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "wallet_id") + private Wallet wallet; + // 추후 추가 // @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // @Builder.Default // private List bids = new ArrayList<>(); + public static User create(String name, String email, String password, String nickName) { + return User.builder() + .name(name) + .email(email) + .password(password) + .nickName(nickName) + .build(); + } /** * @descripton static factory pattern을 적용하여, 구현 */ - public static User create(String name, String email, String password, String nickName) { - return User.builder() + public static User createWithWallet(String name, String email, String password, String nickName) { + User user = User.builder() .name(name) .email(email) .password(password) .nickName(nickName) .build(); + Wallet wallet = Wallet.create(user); + user.addWallet(wallet); + return user; } +// public Wallet createWalletForUser(User user) { +// Wallet wallet = new Wallet(); +// wallet.setUser(user); +// return wallet; +// } public void setAuctions(List auctions) { this.auctions = auctions; @@ -79,4 +101,9 @@ public void delete() { public void addAuction(Auction auction) { this.auctions.add(auction); } + + public void addWallet(Wallet wallet) { + this.wallet = wallet; + wallet.addUser(this); + } } diff --git a/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java b/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java index 95c78348..7a632353 100644 --- a/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/user/application/UserServiceImplTest.java @@ -1,16 +1,17 @@ -package com.tasksprints.auction.domain.user; - -import com.tasksprints.auction.domain.user.dto.request.UserRequest; -import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; -import com.tasksprints.auction.domain.user.model.User; -import com.tasksprints.auction.domain.user.repository.UserRepository; -import com.tasksprints.auction.domain.user.service.UserServiceImpl; +package com.tasksprints.auction.user.application; +import com.tasksprints.auction.wallet.domain.entity.Wallet; +import com.tasksprints.auction.user.application.service.UserServiceImpl; +import com.tasksprints.auction.user.domain.dto.request.UserRequest; +import com.tasksprints.auction.user.domain.dto.response.UserDetailResponse; +import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.user.exception.UserNotFoundException; +import com.tasksprints.auction.user.infrastructure.UserRepository; import org.junit.jupiter.api.*; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.math.BigDecimal; import java.util.Optional; import static org.mockito.Mockito.*; @@ -19,11 +20,10 @@ public class UserServiceImplTest { @Mock private UserRepository userRepository; - @InjectMocks private UserServiceImpl userService; - private User existingUser; + private Wallet existingWallet; @BeforeEach void setUp() { @@ -34,7 +34,17 @@ void setUp() { .nickName("testNick") .password("testPassword") .email("test@example.com") + .wallet(existingWallet) .build(); + + existingWallet = Wallet.builder() + .id(1L) + .balance(BigDecimal.ZERO) + .userName(existingUser.getName()) + .user(existingUser) + .build(); + + existingUser.addWallet(existingWallet); } @Nested @@ -47,7 +57,6 @@ void shouldCreateNewUser() { // Arrange UserRequest.Register request = new UserRequest.Register("testUser", "test@example.com", "testPassword", "testNick"); when(userRepository.save(any(User.class))).thenReturn(existingUser); - // Act UserDetailResponse createdUser = userService.createUser(request); From d4587e98e47234a436d1fb0695f162f8b37edbf3 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Wed, 18 Dec 2024 12:44:12 +0900 Subject: [PATCH 47/48] =?UTF-8?q?refactor(exception)=20:=20payment=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B3=B5=ED=86=B5=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EA=B8=B0=EC=A0=80=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GlobalExceptionHandler.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java index 4b8a90aa..149bee76 100644 --- a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java @@ -1,15 +1,17 @@ package com.tasksprints.auction.common.handler; +import com.tasksprints.auction.auction.exception.AuctionAlreadyClosedException; +import com.tasksprints.auction.auction.exception.AuctionEndedException; +import com.tasksprints.auction.auction.exception.AuctionNotFoundException; +import com.tasksprints.auction.auction.exception.InvalidAuctionTimeException; +import com.tasksprints.auction.bid.exception.BidNotFoundException; +import com.tasksprints.auction.bid.exception.InvalidBidAmountException; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.common.response.ApiResult; -import com.tasksprints.auction.domain.auction.exception.AuctionAlreadyClosedException; -import com.tasksprints.auction.domain.auction.exception.AuctionEndedException; -import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; -import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException; -import com.tasksprints.auction.domain.bid.exception.BidNotFoundException; -import com.tasksprints.auction.domain.bid.exception.InvalidBidAmountException; -import com.tasksprints.auction.domain.product.exception.ProductNotFoundException; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; +import com.tasksprints.auction.payment.exception.InvalidSessionException; +import com.tasksprints.auction.payment.exception.PaymentDataMismatchException; +import com.tasksprints.auction.product.exception.ProductNotFoundException; +import com.tasksprints.auction.user.exception.UserNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -61,6 +63,18 @@ public ResponseEntity> handleAuctionEndedException(AuctionEnde return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message)); } + @ExceptionHandler(InvalidSessionException.class) + public ResponseEntity> handleInvalidSessionException(InvalidSessionException ex) { + String message = "Invalid Session Error. "; + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message)); + } + + @ExceptionHandler(PaymentDataMismatchException.class) + public ResponseEntity> PaymentDataMismatchException(PaymentDataMismatchException ex) { + String message = "Session Data Mismatch Error. "; + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message)); + } + @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(ex.getMessage())); From 70d44f3a8c88d58ad85462fe4a404c517a3af1d9 Mon Sep 17 00:00:00 2001 From: taehyun <126179088+KNU-K@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:25:35 +0900 Subject: [PATCH 48/48] =?UTF-8?q?fix(user)=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/domain/user/service/UserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java index 3249ed70..6d2e5302 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java @@ -69,11 +69,11 @@ public UserDetailResponse getUserDetailByEmail(String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new UserNotFoundException("User not found with email " + email)); return UserDetailResponse.of(user); + } @Override public User getUserById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); - } }