From 3562bfc5afb82eaa04bc034c41ed06673d528734 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 15:56:29 +0900 Subject: [PATCH 01/95] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/filter/GuestAuthFilter.java | 66 ------------------- .../common/config/SecurityConfig.java | 23 +------ .../support/config/TestSecurityConfig.java | 3 - 3 files changed, 1 insertion(+), 91 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java deleted file mode 100644 index defa35bb..00000000 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.swyp8team2.auth.presentation.filter; - -import com.swyp8team2.auth.domain.UserInfo; -import com.swyp8team2.common.annotation.GuestTokenCryptoService; -import com.swyp8team2.common.exception.ApplicationException; -import com.swyp8team2.common.exception.BadRequestException; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.presentation.CustomHeader; -import com.swyp8team2.crypto.application.CryptoService; -import com.swyp8team2.user.domain.Role; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.AntPathMatcher; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.util.Objects; - -import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; - -@Slf4j -public class GuestAuthFilter extends OncePerRequestFilter { - - private final CryptoService cryptoService; - - public GuestAuthFilter(@GuestTokenCryptoService CryptoService cryptoService) { - this.cryptoService = cryptoService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - try { - AntPathMatcher matcher = new AntPathMatcher(); - if (!matcher.match("/posts/{postId}/votes/guest", request.getRequestURI()) && - !matcher.match("/posts/shareUrl/{shareUrl}", request.getRequestURI())) { - return; - } - String token = request.getHeader(CustomHeader.GUEST_TOKEN); - if (Objects.isNull(token)) { - throw new BadRequestException(ErrorCode.INVALID_GUEST_HEADER); - } - String guestId = cryptoService.decrypt(token); - Authentication authentication = getAuthentication(Long.parseLong(guestId)); - SecurityContextHolder.getContext().setAuthentication(authentication); - } catch (ApplicationException e) { - request.setAttribute(EXCEPTION_KEY, e); - } catch (Exception e) { - log.debug("GuestAuthFilter error", e); - request.setAttribute(EXCEPTION_KEY, new BadRequestException(ErrorCode.INVALID_TOKEN)); - } finally { - doFilter(request, response, filterChain); - } - } - - private Authentication getAuthentication(long userId) { - UserInfo userInfo = new UserInfo(userId, Role.GUEST); - return new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); - } -} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index f283fa95..22753748 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -1,13 +1,9 @@ package com.swyp8team2.common.config; import com.swyp8team2.auth.application.jwt.JwtProvider; -import com.swyp8team2.auth.presentation.filter.GuestAuthFilter; import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; -import com.swyp8team2.common.annotation.GuestTokenCryptoService; -import com.swyp8team2.crypto.application.CryptoService; -import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; @@ -33,14 +29,11 @@ public class SecurityConfig { private final HandlerExceptionResolver handlerExceptionResolver; - private final CryptoService cryptoService; public SecurityConfig( - @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver, - @GuestTokenCryptoService CryptoService cryptoService + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver ) { this.handlerExceptionResolver = handlerExceptionResolver; - this.cryptoService = cryptoService; } @Bean @@ -88,17 +81,11 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize .requestMatchers(getWhiteList(introspect)).permitAll() - .requestMatchers(getGuestTokenRequestList(introspect)) - .hasAnyRole(Role.USER.name(), Role.GUEST.name()) .anyRequest().authenticated()) .addFilterBefore( new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor()), UsernamePasswordAuthenticationFilter.class) - .addFilterAfter( - new GuestAuthFilter(cryptoService), - JwtAuthFilter.class - ) .exceptionHandling(exception -> exception.authenticationEntryPoint( new JwtAuthenticationEntryPoint(handlerExceptionResolver))); @@ -109,18 +96,10 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), - mvc.pattern("/auth/guest/token"), mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), mvc.pattern("/auth/oauth2/**"), }; } - - public static MvcRequestMatcher[] getGuestTokenRequestList(HandlerMappingIntrospector introspect) { - MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); - return new MvcRequestMatcher[]{ - mvc.pattern("/posts/{postId}/votes/guest"), - }; - } } diff --git a/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java index 2fd6de4a..63961e27 100644 --- a/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java +++ b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java @@ -10,9 +10,6 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; -import static com.swyp8team2.common.config.SecurityConfig.getGuestTokenRequestList; -import static com.swyp8team2.common.config.SecurityConfig.getWhiteList; - @TestConfiguration public class TestSecurityConfig { From 9b2060f1d0387f5113bba53fb19431c3626be755 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 15:57:51 +0900 Subject: [PATCH 02/95] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 5 ---- .../auth/application/AuthService.java | 24 ++----------------- .../auth/presentation/AuthController.java | 6 ----- .../annotation/GuestTokenCryptoService.java | 16 ------------- .../common/config/CryptoConfig.java | 10 -------- .../auth/presentation/AuthControllerTest.java | 19 --------------- 6 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index bf0d05db..4d3525e3 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -11,11 +11,6 @@ operation::auth-controller-test/kakao-o-auth-sign-in[snippets='http-request,curl operation::auth-controller-test/reissue[snippets='http-request,curl-request,request-cookies,http-response,response-cookies,response-fields'] -[[게스트-토큰-발급]] -=== `POST` 게스트 토큰 발급 - -operation::auth-controller-test/guest-token[snippets='http-request,curl-request,http-response,response-fields'] - [[로그아웃]] === `POST` 로그아웃 diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 24b8c3fc..517d11d0 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,33 +7,19 @@ import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; import com.swyp8team2.auth.presentation.dto.TokenResponse; -import com.swyp8team2.common.annotation.GuestTokenCryptoService; -import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final OAuthService oAuthService; private final SocialAccountRepository socialAccountRepository; private final UserService userService; - private final CryptoService cryptoService; - - public AuthService( - JwtService jwtService, - OAuthService oAuthService, - SocialAccountRepository socialAccountRepository, - UserService userService, - @GuestTokenCryptoService CryptoService cryptoService) { - this.jwtService = jwtService; - this.oAuthService = oAuthService; - this.socialAccountRepository = socialAccountRepository; - this.userService = userService; - this.cryptoService = cryptoService; - } @Transactional public TokenResponse oauthSignIn(String code, String redirectUri) { @@ -55,12 +41,6 @@ public TokenResponse reissue(String refreshToken) { return jwtService.reissue(refreshToken); } - @Transactional - public String createGuestToken() { - Long guestId = userService.createGuest(); - return cryptoService.encrypt(String.valueOf(guestId)); - } - @Transactional public void signOut(Long userId, String refreshToken) { jwtService.signOut(userId, refreshToken); diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index f0c6eeb4..049c80ac 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -60,12 +60,6 @@ public ResponseEntity reissue( return ResponseEntity.ok(new AuthResponse(tokenPair.accessToken(), tokenResponse.userId())); } - @PostMapping("/guest/token") - public ResponseEntity guestToken() { - String guestToken = authService.createGuestToken(); - return ResponseEntity.ok(new GuestTokenResponse(guestToken)); - } - @PostMapping("/sign-out") public ResponseEntity signOut( @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, diff --git a/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java deleted file mode 100644 index 90a6e2db..00000000 --- a/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.swyp8team2.common.annotation; - -import org.springframework.beans.factory.annotation.Qualifier; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Qualifier(GuestTokenCryptoService.QUALIFIER) -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER, ElementType.METHOD}) -public @interface GuestTokenCryptoService { - - String QUALIFIER = "guestTokenCryptoService"; -} diff --git a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java index 7e722dfd..1f814a28 100644 --- a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java +++ b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java @@ -1,6 +1,5 @@ package com.swyp8team2.common.config; -import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.common.annotation.ShareUrlCryptoService; import org.springframework.beans.factory.annotation.Value; @@ -11,26 +10,17 @@ @Configuration public class CryptoConfig { - private final String guestTokenSymmetricKey; private final String shareUrlSymmetricKey; private final String salt; public CryptoConfig( - @Value("${crypto.secret-key.guest-token}") String guestTokenSymmetricKey, @Value("${crypto.secret-key.share-url}") String shareUrlSymmetricKey, @Value("${crypto.salt}") String salt ) { - this.guestTokenSymmetricKey = guestTokenSymmetricKey; this.shareUrlSymmetricKey = shareUrlSymmetricKey; this.salt = salt; } - @GuestTokenCryptoService - @Bean(name = GuestTokenCryptoService.QUALIFIER) - public CryptoService guestTokenCryptoService() throws Exception { - return new CryptoService(new AesBytesEncryptor(guestTokenSymmetricKey, salt)); - } - @ShareUrlCryptoService @Bean(name = ShareUrlCryptoService.QUALIFIER) public CryptoService shareUrlCryptoService() throws Exception { diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 60390dfb..9f9ab5ae 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -157,25 +157,6 @@ void reissue_refreshTokenMismatched() throws Exception { .andExpect(content().json(objectMapper.writeValueAsString(response))); } - @Test - @DisplayName("게스트 토큰 발급") - void guestToken() throws Exception { - //given - String guestToken = "guestToken"; - given(authService.createGuestToken()) - .willReturn(guestToken); - - //when then - mockMvc.perform(post("/auth/guest/token")) - .andExpect(status().isOk()) - .andExpect(content().json(objectMapper.writeValueAsString(new GuestTokenResponse(guestToken)))) - .andDo(restDocs.document( - responseFields( - fieldWithPath("guestToken").description("게스트 토큰") - ) - )); - } - @Test @WithMockUserInfo @DisplayName("로그아웃") From b4af6ff1b602f0ebff5fc2cf36ca0497215a0e45 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 16:39:48 +0900 Subject: [PATCH 03/95] =?UTF-8?q?refactor:=20jwt=20claim=20role=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/jwt/JwtClaim.java | 15 ++++++++++++--- .../auth/application/jwt/JwtProvider.java | 5 ++++- .../auth/application/jwt/JwtService.java | 18 +++++++----------- .../presentation/filter/JwtAuthFilter.java | 6 +++--- .../auth/application/JwtClaimTest.java | 9 +++++++-- .../auth/application/JwtProviderTest.java | 9 +++++---- .../auth/application/JwtServiceTest.java | 9 +++++---- 7 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtClaim.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtClaim.java index 149aa64d..6ae7cc87 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtClaim.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtClaim.java @@ -1,17 +1,22 @@ package com.swyp8team2.auth.application.jwt; +import com.swyp8team2.user.domain.Role; + public class JwtClaim { public static final String ID = "id"; + public static final String ROLE = "role"; private final String id; + private final Role role; - public JwtClaim(long id) { + public JwtClaim(long id, Role role) { this.id = String.valueOf(id); + this.role = role; } - public static JwtClaim from(long id) { - return new JwtClaim(id); + public static JwtClaim from(long id, Role role) { + return new JwtClaim(id, role); } public Long idAsLong() { @@ -21,4 +26,8 @@ public Long idAsLong() { public String id() { return id; } + + public Role role() { + return role; + } } \ No newline at end of file diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java index 343f285b..033bb733 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; import com.swyp8team2.common.exception.UnauthorizedException; +import com.swyp8team2.user.domain.Role; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; @@ -63,6 +64,7 @@ private String createToken(JwtClaim claim, long expiration) { return Jwts.builder() .claim(JwtClaim.ID, claim.id()) + .claim(JwtClaim.ROLE, claim.role()) .setIssuedAt(Date.from(now)) .setIssuer(issuer) .setExpiration(Date.from(expiredAt)) @@ -80,7 +82,8 @@ public JwtClaim parseToken(String token) { Claims claims = parser.parseClaimsJws(token) .getBody(); String userId = (String) claims.get(JwtClaim.ID); - return new JwtClaim(Long.parseLong(userId)); + Role role = Role.valueOf((String) claims.get(JwtClaim.ROLE)); + return new JwtClaim(Long.parseLong(userId), role); } catch (ExpiredJwtException e) { log.trace("Expired Jwt Token: {}", e.getMessage()); throw new UnauthorizedException(ErrorCode.EXPIRED_TOKEN); diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java index 75228fbd..5c178d8d 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java @@ -19,16 +19,14 @@ public class JwtService { private final RefreshTokenRepository refreshTokenRepository; @Transactional - public TokenResponse createToken(long userId) { - TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(userId)); - RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId) - .orElseGet(() -> new RefreshToken(userId, tokenPair.refreshToken())); + public TokenResponse createToken(JwtClaim claim) { + TokenPair tokenPair = jwtProvider.createToken(claim); + RefreshToken refreshToken = refreshTokenRepository.findByUserId(claim.idAsLong()) + .orElseGet(() -> new RefreshToken(claim.idAsLong(), tokenPair.refreshToken())); refreshToken.setRefreshToken(tokenPair.refreshToken()); refreshTokenRepository.save(refreshToken); - log.debug("createToken userId: {} accessToken: {} refreshToken: {}", - userId, tokenPair.accessToken(), tokenPair.refreshToken()); - return new TokenResponse(tokenPair, userId); + return new TokenResponse(tokenPair, claim.idAsLong(), claim.role()); } @Transactional @@ -37,12 +35,10 @@ public TokenResponse reissue(String refreshToken) { RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(claim.idAsLong()) .orElseThrow(() -> new BadRequestException(ErrorCode.REFRESH_TOKEN_NOT_FOUND)); - TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(claim.idAsLong())); + TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(claim.idAsLong(), claim.role())); findRefreshToken.rotate(refreshToken, tokenPair.refreshToken()); - log.debug("reissue userId: {} accessToken: {} refreshToken: {}", - claim.id(), tokenPair.accessToken(), tokenPair.refreshToken()); - return new TokenResponse(tokenPair, claim.idAsLong()); + return new TokenResponse(tokenPair, claim.idAsLong(), claim.role()); } @Transactional diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index b7f16f08..bcc2dc21 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -35,7 +35,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); JwtClaim claim = jwtProvider.parseToken(headerTokenExtractor.extractToken(authorization)); - Authentication authentication = getAuthentication(claim.idAsLong()); + Authentication authentication = getAuthentication(claim); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (ApplicationException e) { request.setAttribute(EXCEPTION_KEY, e); @@ -44,8 +44,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } - private Authentication getAuthentication(long userId) { - UserInfo userInfo = new UserInfo(userId, Role.USER); + private Authentication getAuthentication(JwtClaim claim) { + UserInfo userInfo = new UserInfo(claim.idAsLong(), claim.role()); return new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); } } diff --git a/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java b/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java index ef7a31e5..fd0d5ffa 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java @@ -1,10 +1,13 @@ package com.swyp8team2.auth.application; import com.swyp8team2.auth.application.jwt.JwtClaim; +import com.swyp8team2.user.domain.Role; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; + class JwtClaimTest { @Test @@ -12,11 +15,13 @@ class JwtClaimTest { void idAsLong() { // given long givenId = 1; + Role givenRole = Role.GUEST; // when - JwtClaim jwtClaim = JwtClaim.from(givenId); + JwtClaim jwtClaim = JwtClaim.from(givenId, givenRole); // then - Assertions.assertThat(jwtClaim.idAsLong()).isEqualTo(givenId); + assertThat(jwtClaim.idAsLong()).isEqualTo(givenId); + assertThat(jwtClaim.role()).isEqualTo(givenRole); } } diff --git a/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java index d3a5fcae..0d2da3e7 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java @@ -5,6 +5,7 @@ import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.UnauthorizedException; +import com.swyp8team2.user.domain.Role; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -24,7 +25,7 @@ class JwtProviderTest { void createToken() throws Exception { //given JwtProvider jwtProvider = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "issuer", Clock.systemDefaultZone()); - JwtClaim givenClaim = new JwtClaim(1L); + JwtClaim givenClaim = new JwtClaim(1L, Role.USER); //when TokenPair tokenPair = jwtProvider.createToken(givenClaim); @@ -49,7 +50,7 @@ void parseClaim_expiredToken() throws Exception { .willReturn(clock.instant().minus(24, ChronoUnit.HOURS)); //when then - TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(1L)); + TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(1L, Role.USER)); assertThatThrownBy(() -> jwtProvider.parseToken(tokenPair.accessToken())) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.EXPIRED_TOKEN.getMessage()); @@ -64,7 +65,7 @@ void parseClaim_differentKey() throws Exception { JwtProvider jwtProviderWithDifferentKey = new JwtProvider("1211qwerqwerqwer1111qwerqwerqwer", "issuer", clock); //when then - TokenPair tokenPair = jwtProviderWithDifferentKey.createToken(new JwtClaim(1L)); + TokenPair tokenPair = jwtProviderWithDifferentKey.createToken(new JwtClaim(1L, Role.USER)); assertThatThrownBy(() -> jwtProvider.parseToken(tokenPair.accessToken())) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); @@ -79,7 +80,7 @@ void parseClaim_differentIssuer() throws Exception { JwtProvider jwtProviderWithDifferentIssuer = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "asdf", clock); //when then - TokenPair tokenPair = jwtProviderWithDifferentIssuer.createToken(new JwtClaim(1L)); + TokenPair tokenPair = jwtProviderWithDifferentIssuer.createToken(new JwtClaim(1L, Role.USER)); assertThatThrownBy(() -> jwtProvider.parseToken(tokenPair.accessToken())) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); diff --git a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java index 40cefbdb..d33ce4ba 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java @@ -10,6 +10,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.support.IntegrationTest; +import com.swyp8team2.user.domain.Role; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -41,7 +42,7 @@ void createToken() throws Exception { .willReturn(expectedTokenPair); //when - TokenResponse tokenResponse = jwtService.createToken(givenUserId); + TokenResponse tokenResponse = jwtService.createToken(new JwtClaim(givenUserId, Role.USER)); //then TokenPair tokenPair = tokenResponse.tokenPair(); @@ -59,7 +60,7 @@ void reissue() throws Exception { String newRefreshToken = "newRefreshToken"; TokenPair expectedTokenPair = new TokenPair("newAccessToken", newRefreshToken); given(jwtProvider.parseToken(any(String.class))) - .willReturn(new JwtClaim(givenUserId)); + .willReturn(new JwtClaim(givenUserId, Role.USER)); given(jwtProvider.createToken(any(JwtClaim.class))) .willReturn(expectedTokenPair); refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); @@ -79,7 +80,7 @@ void reissue() throws Exception { void reissue_refreshTokenNotFound() throws Exception { //given given(jwtProvider.parseToken(any(String.class))) - .willReturn(new JwtClaim(1L)); + .willReturn(new JwtClaim(1L, Role.USER)); //when assertThatThrownBy(() -> jwtService.reissue("refreshToken")) @@ -94,7 +95,7 @@ void reissue_refreshTokenMismatched() throws Exception { long givenUserId = 1L; String givenRefreshToken = "refreshToken"; given(jwtProvider.parseToken(any(String.class))) - .willReturn(new JwtClaim(givenUserId)); + .willReturn(new JwtClaim(givenUserId, Role.USER)); given(jwtProvider.createToken(any(JwtClaim.class))) .willReturn(new TokenPair("accessToken", "newRefreshToken")); refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); From 0014733151508665db1ee599e3250cde559d8785 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 16:41:00 +0900 Subject: [PATCH 04/95] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AuthService.java | 38 ++++++++++++++++--- .../auth/presentation/AuthController.java | 23 +++++++++-- .../auth/presentation/dto/AuthResponse.java | 2 +- .../auth/presentation/dto/TokenResponse.java | 7 ++-- .../common/config/SecurityConfig.java | 5 ++- .../common/dev/DataInitializer.java | 3 +- .../auth/presentation/AuthControllerTest.java | 17 +++++---- 7 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 517d11d0..a015170a 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -1,5 +1,6 @@ package com.swyp8team2.auth.application; +import com.swyp8team2.auth.application.jwt.JwtClaim; import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.oauth.OAuthService; import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; @@ -8,11 +9,18 @@ import com.swyp8team2.auth.domain.SocialAccountRepository; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.user.application.UserService; +import com.swyp8team2.user.domain.Role; +import com.swyp8team2.user.domain.User; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Objects; + +@Slf4j @Service +@Transactional @RequiredArgsConstructor public class AuthService { @@ -21,14 +29,20 @@ public class AuthService { private final SocialAccountRepository socialAccountRepository; private final UserService userService; - @Transactional public TokenResponse oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); - SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( + SocialAccount socialAccount = getSocialAccount(oAuthUserInfo); + + TokenResponse response = jwtService.createToken(new JwtClaim(socialAccount.getUserId(), Role.USER)); + log.debug("oauthSignIn userId: {} tokenPair: {}", response.userId(), response.tokenPair()); + return response; + } + + private SocialAccount getSocialAccount(OAuthUserInfo oAuthUserInfo) { + return socialAccountRepository.findBySocialIdAndProvider( oAuthUserInfo.socialId(), Provider.KAKAO ).orElseGet(() -> createUser(oAuthUserInfo)); - return jwtService.createToken(socialAccount.getUserId()); } private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { @@ -36,13 +50,25 @@ private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo)); } - @Transactional public TokenResponse reissue(String refreshToken) { - return jwtService.reissue(refreshToken); + TokenResponse response = jwtService.reissue(refreshToken); + log.debug("reissue userId: {} tokenPair: {}", response.userId(), response.tokenPair()); + return response; } - @Transactional public void signOut(Long userId, String refreshToken) { jwtService.signOut(userId, refreshToken); } + + public TokenResponse guestSignIn(String refreshToken) { + TokenResponse response; + if (Objects.isNull(refreshToken)) { + User user = userService.createGuest(); + response = jwtService.createToken(new JwtClaim(user.getId(), user.getRole())); + } else { + response = jwtService.reissue(refreshToken); + } + log.debug("guestSignIn userId: {} tokenPair: {}", response.userId(), response.tokenPair()); + return response; + } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 049c80ac..93ccec61 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -4,7 +4,6 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.domain.UserInfo; -import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.auth.presentation.dto.AuthResponse; @@ -42,7 +41,19 @@ public ResponseEntity kakaoOAuthSignIn( TokenPair tokenPair = tokenResponse.tokenPair(); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); response.addCookie(cookie); - return ResponseEntity.ok(new AuthResponse(tokenPair.accessToken(), tokenResponse.userId())); + return ResponseEntity.ok(new AuthResponse(tokenPair.accessToken(), tokenResponse.userId(), tokenResponse.role())); + } + + @PostMapping("/guest/sign-in") + public ResponseEntity guestSignIn( + @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, + HttpServletResponse response + ) { + TokenResponse tokenResponse = authService.guestSignIn(refreshToken); + TokenPair tokenPair = tokenResponse.tokenPair(); + Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); + response.addCookie(cookie); + return ResponseEntity.ok(new AuthResponse(tokenPair.accessToken(), tokenResponse.userId(), tokenResponse.role())); } @PostMapping("/reissue") @@ -57,7 +68,13 @@ public ResponseEntity reissue( TokenPair tokenPair = tokenResponse.tokenPair(); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); response.addCookie(cookie); - return ResponseEntity.ok(new AuthResponse(tokenPair.accessToken(), tokenResponse.userId())); + return ResponseEntity.ok( + new AuthResponse( + tokenPair.accessToken(), + tokenResponse.userId(), + tokenResponse.role() + ) + ); } @PostMapping("/sign-out") diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java index 65d2c20a..08048542 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java @@ -1,4 +1,4 @@ package com.swyp8team2.auth.presentation.dto; -public record AuthResponse(String accessToken, Long userId) { +public record AuthResponse(String accessToken, Long userId, com.swyp8team2.user.domain.Role role) { } diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java index dad3501e..dc29f892 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java @@ -1,9 +1,10 @@ package com.swyp8team2.auth.presentation.dto; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.user.domain.Role; public record TokenResponse( TokenPair tokenPair, - Long userId -) { -} + Long userId, + Role role +) { } diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 22753748..9ea8dea8 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -95,11 +95,12 @@ public SecurityFilterChain securityFilterChain( public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ - mvc.pattern("/auth/reissue"), + mvc.pattern(HttpMethod.POST, "/auth/oauth2/code/kakao"), + mvc.pattern(HttpMethod.POST, "/auth/guest/sign-in"), + mvc.pattern(HttpMethod.POST, "/auth/reissue"), mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), - mvc.pattern("/auth/oauth2/**"), }; } } diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 2328c4ef..39d8077f 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -1,5 +1,6 @@ package com.swyp8team2.common.dev; +import com.swyp8team2.auth.application.jwt.JwtClaim; import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.presentation.dto.TokenResponse; @@ -70,7 +71,7 @@ public void init() { } List adjectives = nicknameAdjectiveRepository.findAll(); User testUser = userRepository.save(User.create("nickname", "https://t1.kakaocdn.net/account_images/default_profile.jpeg")); - TokenResponse tokenResponse = jwtService.createToken(testUser.getId()); + TokenResponse tokenResponse = jwtService.createToken(new JwtClaim(testUser.getId(), testUser.getRole())); TokenPair tokenPair = tokenResponse.tokenPair(); System.out.println("accessToken = " + tokenPair.accessToken()); System.out.println("refreshToken = " + tokenPair.refreshToken()); diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 9f9ab5ae..d92180c3 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -2,7 +2,6 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; -import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.AuthResponse; import com.swyp8team2.auth.presentation.dto.TokenResponse; @@ -12,6 +11,7 @@ import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; +import com.swyp8team2.user.domain.Role; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,7 +22,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; @@ -47,9 +46,9 @@ class AuthControllerTest extends RestDocsTest { void kakaoOAuthSignIn() throws Exception { //given TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); - AuthResponse response = new AuthResponse(expectedTokenPair.accessToken(), 1L); + AuthResponse response = new AuthResponse(expectedTokenPair.accessToken(), 1L, Role.USER); given(authService.oauthSignIn(anyString(), anyString())) - .willReturn(new TokenResponse(expectedTokenPair, 1L)); + .willReturn(new TokenResponse(expectedTokenPair, 1L, Role.USER)); OAuthSignInRequest request = new OAuthSignInRequest("code", "https://dev.photopic.site"); //when then @@ -71,7 +70,8 @@ void kakaoOAuthSignIn() throws Exception { ), responseFields( fieldWithPath("accessToken").description("액세스 토큰"), - fieldWithPath("userId").description("유저 Id") + fieldWithPath("userId").description("유저 Id"), + fieldWithPath("role").description("유저 권한") ), responseCookies( cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") @@ -87,8 +87,8 @@ void reissue() throws Exception { String newRefreshToken = "newRefreshToken"; TokenPair tokenPair = new TokenPair("accessToken", newRefreshToken); given(authService.reissue(anyString())) - .willReturn(new TokenResponse(tokenPair, 1L)); - AuthResponse response = new AuthResponse(tokenPair.accessToken(), 1L); + .willReturn(new TokenResponse(tokenPair, 1L, Role.USER)); + AuthResponse response = new AuthResponse(tokenPair.accessToken(), 1L, Role.USER); //when then mockMvc.perform(post("/auth/reissue") @@ -110,7 +110,8 @@ void reissue() throws Exception { ), responseFields( fieldWithPath("accessToken").description("새 액세스 토큰"), - fieldWithPath("userId").description("유저 Id") + fieldWithPath("userId").description("유저 Id"), + fieldWithPath("role").description("유저 권한") ) )); } From 43f38bc3f4f2b55ce6ee7b3c6cb00806d5cc5ea0 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 16:41:28 +0900 Subject: [PATCH 05/95] =?UTF-8?q?refactor:=20=EB=82=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/common/config/SecurityConfig.java | 13 ++++++++++++- .../swyp8team2/user/application/UserService.java | 12 +++++++++--- .../user/presentation/UserController.java | 5 +++-- .../user/presentation/dto/UserMyInfoResponse.java | 15 +++++++++++++++ .../user/presentation/UserControllerTest.java | 9 ++++++--- 5 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/swyp8team2/user/presentation/dto/UserMyInfoResponse.java diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 9ea8dea8..9b711487 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -4,6 +4,7 @@ import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; +import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; @@ -81,7 +82,9 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize .requestMatchers(getWhiteList(introspect)).permitAll() - .anyRequest().authenticated()) + .requestMatchers(getGuestAllowedList(introspect)) + .hasAnyRole(Role.USER.name(), Role.GUEST.name()) + .anyRequest().hasRole(Role.USER.name())) .addFilterBefore( new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor()), @@ -103,4 +106,12 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), }; } + + public static MvcRequestMatcher[] getGuestAllowedList(HandlerMappingIntrospector introspect) { + MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); + return new MvcRequestMatcher[]{ + mvc.pattern(HttpMethod.POST, "/posts/{postId}/votes"), + mvc.pattern(HttpMethod.GET, "/users/me"), + }; + } } diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index b97757fb..4b0bc6fc 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -7,6 +7,7 @@ import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.user.presentation.dto.UserInfoResponse; +import com.swyp8team2.user.presentation.dto.UserMyInfoResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,9 +44,8 @@ private String getNickname(String nickname) { } @Transactional - public Long createGuest() { - User user = userRepository.save(User.createGuest()); - return user.getId(); + public User createGuest() { + return userRepository.save(User.createGuest()); } public UserInfoResponse findById(Long userId) { @@ -53,4 +53,10 @@ public UserInfoResponse findById(Long userId) { .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); return UserInfoResponse.of(user); } + + public UserMyInfoResponse findByMe(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + return UserMyInfoResponse.of(user); + } } diff --git a/src/main/java/com/swyp8team2/user/presentation/UserController.java b/src/main/java/com/swyp8team2/user/presentation/UserController.java index 332679ed..3f81d259 100644 --- a/src/main/java/com/swyp8team2/user/presentation/UserController.java +++ b/src/main/java/com/swyp8team2/user/presentation/UserController.java @@ -3,6 +3,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.user.application.UserService; import com.swyp8team2.user.presentation.dto.UserInfoResponse; +import com.swyp8team2.user.presentation.dto.UserMyInfoResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -24,9 +25,9 @@ public ResponseEntity findUserInfo(@PathVariable("userId") Lon } @GetMapping("/me") - public ResponseEntity findMyInfo( + public ResponseEntity findMyInfo( @AuthenticationPrincipal UserInfo userInfo ) { - return ResponseEntity.ok(userService.findById(userInfo.userId())); + return ResponseEntity.ok(userService.findByMe(userInfo.userId())); } } diff --git a/src/main/java/com/swyp8team2/user/presentation/dto/UserMyInfoResponse.java b/src/main/java/com/swyp8team2/user/presentation/dto/UserMyInfoResponse.java new file mode 100644 index 00000000..7aafd096 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/presentation/dto/UserMyInfoResponse.java @@ -0,0 +1,15 @@ +package com.swyp8team2.user.presentation.dto; + +import com.swyp8team2.user.domain.Role; +import com.swyp8team2.user.domain.User; + +public record UserMyInfoResponse( + Long id, + String nickname, + String profileImageUrl, + Role role +) { + public static UserMyInfoResponse of(User user) { + return new UserMyInfoResponse(user.getId(), user.getNickname(), user.getProfileUrl(), user.getRole()); + } +} diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 8490a66a..90b947f3 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -2,7 +2,9 @@ import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; +import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.presentation.dto.UserInfoResponse; +import com.swyp8team2.user.presentation.dto.UserMyInfoResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -52,8 +54,8 @@ void findUserInfo() throws Exception { @DisplayName("본인 정보 조회") void findMe() throws Exception { //given - UserInfoResponse response = new UserInfoResponse(1L, "nickname", "https://image.com/profile-image"); - given(userService.findById(1L)) + UserMyInfoResponse response = new UserMyInfoResponse(1L, "nickname", "https://image.com/profile-image", Role.USER); + given(userService.findByMe(1L)) .willReturn(response); //when then @@ -66,7 +68,8 @@ void findMe() throws Exception { responseFields( fieldWithPath("id").description("유저 아이디").type(NUMBER), fieldWithPath("nickname").description("닉네임").type(STRING), - fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING) + fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING), + fieldWithPath("role").description("유저 권한").type(STRING) ) )); } From 79ad8a6665dac59c1fef5ea8f2e143df3ee7db8b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 17:10:50 +0900 Subject: [PATCH 06/95] =?UTF-8?q?feat:=20=EA=B6=8C=ED=95=9C=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/CustomAccessDenialHandler.java | 33 +++++++++++++++++++ .../common/config/SecurityConfig.java | 8 ++++- .../swyp8team2/common/dev/DataInitConfig.java | 2 +- .../ApplicationControllerAdvice.java | 10 +++--- .../common/exception/ErrorCode.java | 3 ++ .../common/exception/ForbiddenException.java | 8 +++++ 6 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDenialHandler.java create mode 100644 src/main/java/com/swyp8team2/common/exception/ForbiddenException.java diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDenialHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDenialHandler.java new file mode 100644 index 00000000..c024d97c --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDenialHandler.java @@ -0,0 +1,33 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.swyp8team2.common.exception.ForbiddenException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.util.Objects; + +public class CustomAccessDenialHandler implements AccessDeniedHandler { + + private final HandlerExceptionResolver exceptionResolver; + + public CustomAccessDenialHandler( + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver exceptionResolver + ) { + this.exceptionResolver = exceptionResolver; + } + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) { + if (Objects.nonNull(accessDeniedException)) { + exceptionResolver.resolveException(request, response, null, new ForbiddenException()); + } + } +} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 9b711487..c0fa5956 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -1,6 +1,7 @@ package com.swyp8team2.common.config; import com.swyp8team2.auth.application.jwt.JwtProvider; +import com.swyp8team2.auth.presentation.filter.CustomAccessDenialHandler; import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; @@ -91,7 +92,12 @@ public SecurityFilterChain securityFilterChain( UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exception -> exception.authenticationEntryPoint( - new JwtAuthenticationEntryPoint(handlerExceptionResolver))); + new JwtAuthenticationEntryPoint(handlerExceptionResolver)) + .accessDeniedHandler((request, response, accessDeniedException) -> + new CustomAccessDenialHandler(handlerExceptionResolver) + .handle(request, response, accessDeniedException) + ) + ); return http.build(); } diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java b/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java index 59bd22d8..561a4c99 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java @@ -12,7 +12,7 @@ public class DataInitConfig { private final DataInitializer dataInitializer; -// @PostConstruct + @PostConstruct public void init() { dataInitializer.init(); } diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 4dc5fd55..12d04778 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -69,14 +69,14 @@ public ResponseEntity handle(NoResourceFoundException e) { @ExceptionHandler(AuthenticationException.class) public ResponseEntity handle(AuthenticationException e) { - log.info(e.getMessage()); + log.debug(e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); } - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity handle(AccessDeniedException e) { - log.info(e.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handle(ForbiddenException e) { + log.debug(e.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(ErrorCode.FORBIDDEN)); } @ExceptionHandler(Exception.class) diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 6ee4f4b3..0cb2b026 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -29,6 +29,9 @@ public enum ErrorCode { INVALID_AUTH_HEADER("잘못된 인증 헤더"), OAUTH_LOGIN_FAILED("소셜 로그인 실패"), + //403 + FORBIDDEN("권한 없음"), + //500 INTERNAL_SERVER_ERROR("서버 내부 오류"), INVALID_INPUT_VALUE("잘못된 입력 값"), diff --git a/src/main/java/com/swyp8team2/common/exception/ForbiddenException.java b/src/main/java/com/swyp8team2/common/exception/ForbiddenException.java new file mode 100644 index 00000000..0e39910f --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/ForbiddenException.java @@ -0,0 +1,8 @@ +package com.swyp8team2.common.exception; + +public class ForbiddenException extends ApplicationException { + + public ForbiddenException() { + super(ErrorCode.FORBIDDEN); + } +} From c22947edaf0bee452acfb5ef82913f01c07ef94f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 17:25:23 +0900 Subject: [PATCH 07/95] =?UTF-8?q?docs:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 17 ++++++++ src/docs/asciidoc/index.adoc | 12 +++--- .../auth/presentation/AuthControllerTest.java | 39 +++++++++++++++++++ .../user/presentation/UserControllerTest.java | 2 +- .../templates/request-cookies.snippet | 10 +++++ 5 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/org/springframework/restdocs/templates/request-cookies.snippet diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 4d3525e3..0468ec86 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -6,6 +6,23 @@ operation::auth-controller-test/kakao-o-auth-sign-in[snippets='http-request,curl-request,request-fields,http-response,response-cookies,response-fields'] +[[게스트-로그인]] +=== `POST` 게스트 로그인 + +``` +1. 리프레시 토큰이 있는 경우 + +토큰 재발급 시도 (토큰이 잘못된 경우 400에러 발생) + +2. 리프레시 토큰이 없는 경우 + +게스트 계정 생성 +``` + +operation::auth-controller-test/guest-sign-in[snippets='http-request,curl-request,request-cookies,http-response,response-cookies,response-fields'] + +[[로그인]] + [[토큰-재발급]] === `POST` 토큰 재발급 diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index be2279b8..0c15f4d8 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -28,6 +28,7 @@ HTTP 상태 코드 본 REST API에서 사용하는 HTTP 상태 코드는 가능 | `200 OK`| 요청을 성공적으로 처리함 | `400 Bad Request`| 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 | `401 Unauthorization`| 인증에 실패한 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 +| `403 Forbidden`| 권한이 없는 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 | `404 Not Found`| 요청한 리소스가 없음. | `500 Internal Server Error`| 서버 내부 오류가 발생한 경우. | `503 Service Unavailable`| 서버가 요청을 처리할 준비가 되지 않은 경우. @@ -77,15 +78,16 @@ eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIiLCJpYXQiOjE3NDA0NDM0ODIsImlzcyI6InN3eXA4dGVhbTI === 인증 예외 ``` -인증 토큰관련 예외가 발생하면 다음과 같은 에러 코드와 함께 401 상태 코드를 응답함 +인증 토큰관련 예외가 발생하면 다음과 같은 에러 코드와 함께 상태 코드를 응답함 ``` |=== -| 에러 코드 | 용례 -|`EXPIRED_TOKEN`| 토큰이 만료되었을 경우 -|`INVALID_TOKEN`| 잘못된 형식의 토큰인 경우 -|`INVALID_AUTH_HEADER`| Authorization 헤더가 존재하지 않거나 Bearer 형식이 아닌 경우 +| 에러 코드 | 상태 코드 | 용례 +|`EXPIRED_TOKEN`| 401 | 토큰이 만료되었을 경우 +|`INVALID_TOKEN`| 401 | 잘못된 형식의 토큰인 경우 +|`INVALID_AUTH_HEADER`| 401 | Authorization 헤더가 존재하지 않거나 Bearer 형식이 아닌 경우 +|`FORBIDDEN`| 403 | 권한이 없는 경우 (ex, 게스트가 투표 생성) |=== 예시 diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index d92180c3..3682a11e 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -20,6 +20,7 @@ import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; @@ -79,6 +80,44 @@ void kakaoOAuthSignIn() throws Exception { )); } + @Test + @DisplayName("게스트 로그인") + void guestSignIn() throws Exception { + //given + TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); + AuthResponse response = new AuthResponse(expectedTokenPair.accessToken(), 1L, Role.USER); + given(authService.guestSignIn(any())) + .willReturn(new TokenResponse(expectedTokenPair, 1L, Role.USER)); + + //when then + mockMvc.perform(post("/auth/guest/sign-in") + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken"))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andExpect(cookie().value(CustomHeader.CustomCookie.REFRESH_TOKEN, expectedTokenPair.refreshToken())) + .andExpect(cookie().httpOnly(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().path(CustomHeader.CustomCookie.REFRESH_TOKEN, "/")) + .andExpect(cookie().secure(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().attribute(CustomHeader.CustomCookie.REFRESH_TOKEN, "SameSite", "None")) + .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 60 * 60 * 24 * 14)) + .andDo(restDocs.document( + requestCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN) + .optional() + .description("리프레시 토큰") + ), + responseFields( + fieldWithPath("accessToken").description("액세스 토큰"), + fieldWithPath("userId").description("유저 Id"), + fieldWithPath("role").description("유저 권한") + ), + responseCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") + ) + )); + } + @Test @WithAnonymousUser @DisplayName("토큰 재발급") diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 90b947f3..02906b05 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -68,7 +68,7 @@ void findMe() throws Exception { responseFields( fieldWithPath("id").description("유저 아이디").type(NUMBER), fieldWithPath("nickname").description("닉네임").type(STRING), - fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING), + fieldWithPath("profileImageUrl").description("프로필 이미지 URL").type(STRING), fieldWithPath("role").description("유저 권한").type(STRING) ) )); diff --git a/src/test/resources/org/springframework/restdocs/templates/request-cookies.snippet b/src/test/resources/org/springframework/restdocs/templates/request-cookies.snippet new file mode 100644 index 00000000..2c412000 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/request-cookies.snippet @@ -0,0 +1,10 @@ +|=== + |파라미터|필수값|설명 + +{{#cookies}} + |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/cookies}} + +|=== \ No newline at end of file From 188bd217223e86230fd46299213509384767cdf2 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 17:29:55 +0900 Subject: [PATCH 08/95] =?UTF-8?q?refactor:=20=EC=95=88=20=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/common/exception/ErrorCode.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 0cb2b026..6555767b 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -20,14 +20,12 @@ public enum ErrorCode { INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"), NOT_POST_AUTHOR("게시글 작성자가 아님"), POST_ALREADY_CLOSED("이미 마감된 게시글"), - INVALID_GUEST_HEADER("잘못된 게스트 토큰 헤더"), FILE_NAME_TOO_LONG("파일 이름이 너무 김"), //401 EXPIRED_TOKEN("토큰 만료"), INVALID_TOKEN("유효하지 않은 토큰"), INVALID_AUTH_HEADER("잘못된 인증 헤더"), - OAUTH_LOGIN_FAILED("소셜 로그인 실패"), //403 FORBIDDEN("권한 없음"), From ec8cc4ab7cba78035c6dcd789c68bbcc856ecb32 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Mar 2025 17:42:47 +0900 Subject: [PATCH 09/95] =?UTF-8?q?refactor:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/NicknameGenerator.java | 20 +++++++ .../user/application/UserService.java | 24 +++----- .../java/com/swyp8team2/user/domain/Role.java | 9 ++- .../java/com/swyp8team2/user/domain/User.java | 13 ++-- .../application/NicknameGeneratorTest.java | 59 +++++++++++++++++++ 5 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/swyp8team2/user/application/NicknameGenerator.java create mode 100644 src/test/java/com/swyp8team2/user/application/NicknameGeneratorTest.java diff --git a/src/main/java/com/swyp8team2/user/application/NicknameGenerator.java b/src/main/java/com/swyp8team2/user/application/NicknameGenerator.java new file mode 100644 index 00000000..508a4546 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/application/NicknameGenerator.java @@ -0,0 +1,20 @@ +package com.swyp8team2.user.application; + +import com.swyp8team2.user.domain.NicknameAdjectiveRepository; +import com.swyp8team2.user.domain.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NicknameGenerator { + + private final NicknameAdjectiveRepository nicknameAdjectiveRepository; + + public String generate(Role role) { + long randomIndex = (long)(Math.random() * 500); + return nicknameAdjectiveRepository.findNicknameAdjectiveById(randomIndex) + .map(adjective -> adjective.getAdjective() + " " + role.getNickname()) + .orElse("user_" + System.currentTimeMillis()); + } +} diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index 4b0bc6fc..e008f3f2 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -2,8 +2,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.user.domain.NicknameAdjective; -import com.swyp8team2.user.domain.NicknameAdjectiveRepository; +import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.user.presentation.dto.UserInfoResponse; @@ -20,7 +19,7 @@ public class UserService { private final UserRepository userRepository; - private final NicknameAdjectiveRepository nicknameAdjectiveRepository; + private final NicknameGenerator nicknameGenerator; @Transactional public Long createUser(String nickname, String profileImageUrl) { @@ -28,24 +27,19 @@ public Long createUser(String nickname, String profileImageUrl) { return user.getId(); } - private String getProfileImage(String profileImageUrl) { - return Optional.ofNullable(profileImageUrl) - .orElse("https://t1.kakaocdn.net/account_images/default_profile.jpeg"); - } - private String getNickname(String nickname) { return Optional.ofNullable(nickname) - .orElseGet(() -> { - long randomIndex = (long)(Math.random() * 500); - Optional adjective = nicknameAdjectiveRepository.findNicknameAdjectiveById(randomIndex); - return adjective.map(NicknameAdjective::getAdjective) - .orElse("user_" + System.currentTimeMillis()); - }); + .orElseGet(() -> nicknameGenerator.generate(Role.USER)); + } + + private String getProfileImage(String profileImageUrl) { + return Optional.ofNullable(profileImageUrl) + .orElse(User.DEFAULT_PROFILE_URL); } @Transactional public User createGuest() { - return userRepository.save(User.createGuest()); + return userRepository.save(User.createGuest(nicknameGenerator.generate(Role.GUEST))); } public UserInfoResponse findById(Long userId) { diff --git a/src/main/java/com/swyp8team2/user/domain/Role.java b/src/main/java/com/swyp8team2/user/domain/Role.java index 3792a4c5..83f409d8 100644 --- a/src/main/java/com/swyp8team2/user/domain/Role.java +++ b/src/main/java/com/swyp8team2/user/domain/Role.java @@ -1,5 +1,12 @@ package com.swyp8team2.user.domain; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor public enum Role { - GUEST, USER + GUEST("낫또"), USER("뽀또"); + + private final String nickname; } diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 5686f490..e4772bdc 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -10,17 +10,14 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - -import static com.swyp8team2.common.util.Validator.validateEmptyString; -import static com.swyp8team2.common.util.Validator.validateNull; - @Getter @Entity @Table(name = "users") @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) public class User extends BaseEntity { + public static final String DEFAULT_PROFILE_URL = "https://image.photopic.site/images-dev/resized_202502240006030.png"; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -43,11 +40,11 @@ public static User create(String nickname, String profileUrl) { return new User(null, nickname, profileUrl, Role.USER); } - public static User createGuest() { + public static User createGuest(String nickname) { return new User( null, - "guest_" + System.currentTimeMillis(), - "https://image.photopic.site/images-dev/resized_202502240006030.png", + nickname, + DEFAULT_PROFILE_URL, Role.GUEST ); } diff --git a/src/test/java/com/swyp8team2/user/application/NicknameGeneratorTest.java b/src/test/java/com/swyp8team2/user/application/NicknameGeneratorTest.java new file mode 100644 index 00000000..39f13de1 --- /dev/null +++ b/src/test/java/com/swyp8team2/user/application/NicknameGeneratorTest.java @@ -0,0 +1,59 @@ +package com.swyp8team2.user.application; + +import com.swyp8team2.user.domain.NicknameAdjective; +import com.swyp8team2.user.domain.NicknameAdjectiveRepository; +import com.swyp8team2.user.domain.Role; +import com.swyp8team2.user.domain.User; +import org.assertj.core.api.Assertions; +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 java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class NicknameGeneratorTest { + + @InjectMocks + NicknameGenerator nicknameGenerator; + + @Mock + NicknameAdjectiveRepository nicknameAdjectiveRepository; + + @Test + @DisplayName("닉네임 생성 테스트") + void generate() throws Exception { + //given + Role role = Role.USER; + given(nicknameAdjectiveRepository.findNicknameAdjectiveById(any())) + .willReturn(Optional.of(new NicknameAdjective("호기심 많은"))); + + //when + String nickname = nicknameGenerator.generate(role); + + //then + Assertions.assertThat(nickname).isEqualTo("호기심 많은 뽀또"); + } + + @Test + @DisplayName("닉네임 생성 테스트 - 게스트") + void generate_guest() throws Exception { + //given + Role role = Role.GUEST; + given(nicknameAdjectiveRepository.findNicknameAdjectiveById(any())) + .willReturn(Optional.of(new NicknameAdjective("호기심 많은"))); + + //when + String nickname = nicknameGenerator.generate(role); + + //then + Assertions.assertThat(nickname).isEqualTo("호기심 많은 낫또"); + } +} From 4415cb139af5bd19c5dd33ece432bdcf9109499b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 10:27:31 +0900 Subject: [PATCH 10/95] =?UTF-8?q?docs:=20=EC=95=88=20=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?api=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/votes.adoc | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc index 98b1cdc6..0b661b8f 100644 --- a/src/docs/asciidoc/votes.adoc +++ b/src/docs/asciidoc/votes.adoc @@ -5,18 +5,3 @@ === `POST` 투표 operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] - -[[게스트-투표]] -=== `POST` 게스트 투표 - -operation::vote-controller-test/guest-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] - -[[투표-변경]] -=== 투표 변경 (투표 API로 통일) - -// operation::vote-controller-test/change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] - -[[게스트-투표-변경]] -=== 게스트 투표 변경 (미구현, 게스트 투표 API로 통일) - -// operation::vote-controller-test/guest-change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] From c559c5bbec2d6fffd31c4fcea16c7d126d3b7e90 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 11:19:16 +0900 Subject: [PATCH 11/95] =?UTF-8?q?fix:=20=ED=88=AC=ED=91=9C=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20=EB=B0=A9=EC=96=B4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/common/exception/ErrorCode.java | 1 + .../swyp8team2/post/application/PostService.java | 15 +++++++++++++-- .../java/com/swyp8team2/post/domain/Post.java | 10 +++++----- .../post/presentation/PostController.java | 5 +++-- .../post/application/PostServiceTest.java | 4 ++-- .../post/presentation/PostControllerTest.java | 2 +- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 6555767b..2ed851b7 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -21,6 +21,7 @@ public enum ErrorCode { NOT_POST_AUTHOR("게시글 작성자가 아님"), POST_ALREADY_CLOSED("이미 마감된 게시글"), FILE_NAME_TOO_LONG("파일 이름이 너무 김"), + ACCESS_DENIED_VOTE_STATUS("투표 현황 조회 권한 없음"), //401 EXPIRED_TOKEN("토큰 만료"), diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index f48b9c13..48c866ee 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -134,9 +134,10 @@ public CursorBasePaginatedResponse findVotedPosts(Long userI return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse)); } - public List findPostStatus(Long postId) { + public List findVoteStatus(Long userId, Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + validateVoteStatus(userId, post); int totalVoteCount = getTotalVoteCount(post.getImages()); return post.getImages().stream() .map(image -> { @@ -145,6 +146,14 @@ public List findPostStatus(Long postId) { }).toList(); } + private void validateVoteStatus(Long userId, Post post) { + boolean voted = voteRepository.findByUserIdAndPostId(userId, post.getId()) + .isPresent(); + if (!(post.isAuthor(userId) || voted)) { + throw new BadRequestException(ErrorCode.ACCESS_DENIED_VOTE_STATUS); + } + } + private int getTotalVoteCount(List images) { int totalVoteCount = 0; for (PostImage image : images) { @@ -157,7 +166,9 @@ private int getTotalVoteCount(List images) { public void delete(Long userId, Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - post.validateOwner(userId); + if (!post.isAuthor(userId)) { + throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); + } postRepository.delete(post); } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 79ff5700..55bb0fbf 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -95,17 +95,17 @@ public void cancelVote(Long imageId) { } public void close(Long userId) { - validateOwner(userId); + if (!isAuthor(userId)) { + throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); + } if (status == Status.CLOSED) { throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); } this.status = Status.CLOSED; } - public void validateOwner(Long userId) { - if (!this.userId.equals(userId)) { - throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); - } + public boolean isAuthor(Long userId) { + return this.userId.equals(userId); } public void validateProgress() { diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 625897dd..0ef4b6c0 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -65,9 +65,10 @@ public ResponseEntity findPostByShareUrl( @GetMapping("/{postId}/status") public ResponseEntity> findVoteStatus( - @PathVariable("postId") Long postId + @PathVariable("postId") Long postId, + @AuthenticationPrincipal UserInfo userInfo ) { - return ResponseEntity.ok(postService.findPostStatus(postId)); + return ResponseEntity.ok(postService.findVoteStatus(userInfo.userId(), postId)); } @PostMapping("/{postId}/close") diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index b85b73ee..615bf338 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -228,7 +228,7 @@ void findVotedPosts() throws Exception { @Test @DisplayName("투표 현황 조회") - void findPostStatus() throws Exception { + void findVoteStatus() throws Exception { //given User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); @@ -237,7 +237,7 @@ void findPostStatus() throws Exception { voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); //when - var response = postService.findPostStatus(post.getId()); + var response = postService.findVoteStatus(user.getId(), post.getId()); //then assertAll( diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index befad202..0acd0a94 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -204,7 +204,7 @@ void findVoteStatus() throws Exception { new PostImageVoteStatusResponse(1L, "뽀또A", 2, "66.7"), new PostImageVoteStatusResponse(2L, "뽀또B", 1, "33.3") ); - given(postService.findPostStatus(1L)) + given(postService.findVoteStatus(1L, 1L)) .willReturn(response); //when then From 48ab5a3614b9370eddef206d0fe49f5fdc4630d7 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 11:26:29 +0900 Subject: [PATCH 12/95] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostServiceTest.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 615bf338..6ea74765 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -253,6 +253,49 @@ void findVoteStatus() throws Exception { ); } + @Test + @DisplayName("투표 현황 조회 - 투표한 사람인 경우") + void findVoteStatus_voteUser() throws Exception { + //given + User author = userRepository.save(createUser(1)); + User voter = userRepository.save(createUser(2)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); + voteService.vote(voter.getId(), post.getId(), post.getImages().get(0).getId()); + + //when + var response = postService.findVoteStatus(voter.getId(), post.getId()); + + //then + assertAll( + () -> assertThat(response).hasSize(2), + () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), + () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), + () -> assertThat(response.get(0).voteCount()).isEqualTo(1), + () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), + () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), + () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), + () -> assertThat(response.get(1).voteCount()).isEqualTo(0), + () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") + ); + } + + @Test + @DisplayName("투표 현황 조회 - 작성자 아니고 투표 안 한 사람인 경우") + void findVoteStatus_notAuthorAndVoter() throws Exception { + //given + User author = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); + + //when + assertThatThrownBy(() -> postService.findVoteStatus(2L, post.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.ACCESS_DENIED_VOTE_STATUS.getMessage()); + } + @Test @DisplayName("투표 마감") void close() throws Exception { From b2bf9b6bb7c655766e3c998450afb1d678f1fa34 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 13:11:33 +0900 Subject: [PATCH 13/95] =?UTF-8?q?fix:=20batch=20fetch=20=EB=8E=81=EC=8A=A4?= =?UTF-8?q?=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index a9f449f3..578cee26 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit a9f449f36c6328393160acb9014cbbac33b9d82d +Subproject commit 578cee264b3154e9d6bd2c04f9c5b3975e66136d From 14974debcf269bfb1557e7ef8ef0b545fa3e18e8 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 15:36:03 +0900 Subject: [PATCH 14/95] =?UTF-8?q?fix:=20404=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ApplicationControllerAdvice.java | 18 ++++++++---------- .../swyp8team2/common/exception/ErrorCode.java | 3 +++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 12d04778..1de6b396 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -54,19 +54,17 @@ public ResponseEntity invalidArgument(Exception e) { .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); } - @ExceptionHandler({HttpRequestMethodNotSupportedException.class, MethodArgumentTypeMismatchException.class}) - public ResponseEntity notFound(HttpRequestMethodNotSupportedException e) { + @ExceptionHandler({ + HttpRequestMethodNotSupportedException.class, + MethodArgumentTypeMismatchException.class, + NoResourceFoundException.class + }) + public ResponseEntity notFound(Exception e) { log.debug("notFound: {}", e.getMessage()); - return ResponseEntity.notFound().build(); + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(ErrorCode.NOT_FOUND)); } - @ExceptionHandler(NoResourceFoundException.class) - public ResponseEntity handle(NoResourceFoundException e) { - log.debug("NoResourceFoundException {}", e.getMessage()); - return ResponseEntity.notFound().build(); - } - - @ExceptionHandler(AuthenticationException.class) public ResponseEntity handle(AuthenticationException e) { log.debug(e.getMessage()); diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 6555767b..3f6c9bd3 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -30,6 +30,9 @@ public enum ErrorCode { //403 FORBIDDEN("권한 없음"), + //404 + NOT_FOUND("리소스를 찾을 수 없음"), + //500 INTERNAL_SERVER_ERROR("서버 내부 오류"), INVALID_INPUT_VALUE("잘못된 입력 값"), From 67df796b580ec7b3bffcbe9a3e95918204b3ace8 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 17:36:06 +0900 Subject: [PATCH 15/95] =?UTF-8?q?docs:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/index.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 0c15f4d8..91db8505 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -68,10 +68,10 @@ Authorization: Bearer accessToken ``` user1 -eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE3NDAyOTQyMzEsImlzcyI6InN3eXA4dGVhbTIiLCJleHAiOjMzMjc2Mjk0MjMxfQ.gqA245tRiBQB9owKRWIpX1we1T362R-xDTt4YT9AhRY +eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJyb2xlIjoiVVNFUiIsImlhdCI6MTc0MTA2MTc2NSwiaXNzIjoic3d5cDh0ZWFtMiIsImV4cCI6MzMyNzcwNjE3NjV9.3o2uNN3IuGZ-uLrAPdkHBBHF9kk9KALlP373eF27HI4 user2 -eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIiLCJpYXQiOjE3NDA0NDM0ODIsImlzcyI6InN3eXA4dGVhbTIiLCJleHAiOjMzMjc2NDQzNDgyfQ.2sTlCtSHb4eGzhlL6WlRT6xvJLtvipnHp6EAmC4j1UQ +eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIiLCJyb2xlIjoiVVNFUiIsImlhdCI6MTc0MTA2MjkxMiwiaXNzIjoic3d5cDh0ZWFtMiIsImV4cCI6MzMyNzcwNjI5MTJ9.eC4oUp9ROb6udMarevZQcImTWojcL_3kkY1YgatpuJg ``` [[인증-예외]] From 0622820375b814839bbed8619d08dc4479e5838a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 17:36:43 +0900 Subject: [PATCH 16/95] =?UTF-8?q?refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=95?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=20id=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/post/domain/PostRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java index 0653564c..fb060599 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -17,7 +17,7 @@ public interface PostRepository extends JpaRepository { FROM Post p WHERE p.userId = :userId AND (:postId IS NULL OR p.id < :postId) - ORDER BY p.createdAt DESC + ORDER BY p.id DESC """ ) Slice findByUserId(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); @@ -27,7 +27,7 @@ public interface PostRepository extends JpaRepository { FROM Post p WHERE p.id IN :postIds AND (:postId IS NULL OR p.id < :postId) - ORDER BY p.createdAt DESC + ORDER BY p.id DESC """ ) Slice findByIdIn(@Param("postIds") List postIds, @Param("postId") Long postId, Pageable pageable); From 100405258025d315628ead3ce25f3375e3a71216 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Mar 2025 17:37:42 +0900 Subject: [PATCH 17/95] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20fetch=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/post/application/PostService.java | 7 +++---- .../com/swyp8team2/post/domain/PostRepository.java | 10 ++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index f48b9c13..1d27bb07 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -75,7 +75,7 @@ private List createPostImages(CreatePostRequest request) { } public PostResponse findById(Long userId, Long postId) { - Post post = postRepository.findById(postId) + Post post = postRepository.findByIdFetchPostImage(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); User author = userRepository.findById(post.getUserId()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); @@ -114,8 +114,7 @@ private Boolean getVoted(PostImage image, Long userId, Long postId) { public CursorBasePaginatedResponse findUserPosts(Long userId, Long cursor, int size) { Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); - return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse) - ); + return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse)); } private SimplePostResponse createSimplePostResponse(Post post) { @@ -135,7 +134,7 @@ public CursorBasePaginatedResponse findVotedPosts(Long userI } public List findPostStatus(Long postId) { - Post post = postRepository.findById(postId) + Post post = postRepository.findByIdFetchPostImage(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); int totalVoteCount = getTotalVoteCount(post.getImages()); return post.getImages().stream() diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java index fb060599..66b6b529 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface PostRepository extends JpaRepository { @@ -31,4 +32,13 @@ public interface PostRepository extends JpaRepository { """ ) Slice findByIdIn(@Param("postIds") List postIds, @Param("postId") Long postId, Pageable pageable); + + @Query(""" + SELECT p + FROM Post p + JOIN FETCH p.images + WHERE p.id = :postId + """ + ) + Optional findByIdFetchPostImage(@Param("postId") Long postId); } From b08183c1c148eba2557f2992a3ec9cff9a8f601c Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 20:22:27 +0900 Subject: [PATCH 18/95] =?UTF-8?q?chore:=20webp=20=ED=94=8C=EB=9F=AC?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20AWS=20?= =?UTF-8?q?=EB=9E=8C=EB=8B=A4=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index f036af26..2cf46445 100644 --- a/build.gradle +++ b/build.gradle @@ -40,9 +40,8 @@ dependencies { // image implementation 'software.amazon.awssdk:s3:2.30.18' - implementation 'org.imgscalr:imgscalr-lib:4.2' - implementation 'com.sksamuel.scrimage:scrimage-core:4.3.0' - implementation 'com.sksamuel.scrimage:scrimage-webp:4.3.0' + implementation 'software.amazon.awssdk:lambda:2.30.18' + implementation 'com.twelvemonkeys.imageio:imageio-webp:3.9.4' // gson implementation 'com.google.code.gson:gson:2.8.6' From c93abf33ee663b1f90ede533b3fe091f4d6d16e8 Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 20:23:44 +0900 Subject: [PATCH 19/95] =?UTF-8?q?chore:=20AWS=20=EB=9E=8C=EB=8B=A4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/image/config/S3Config.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/swyp8team2/image/config/S3Config.java b/src/main/java/com/swyp8team2/image/config/S3Config.java index 3e65b030..91dee5e2 100644 --- a/src/main/java/com/swyp8team2/image/config/S3Config.java +++ b/src/main/java/com/swyp8team2/image/config/S3Config.java @@ -8,6 +8,7 @@ import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.lambda.LambdaClient; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Configuration; @@ -25,6 +26,12 @@ public class S3Config { @Value("${r2.endpoint}") private String endpoint; + @Value("${aws.access-key}") + private String awsAccessKey; + + @Value("${aws.secret-key}") + private String awsSecretKey; + @Bean public S3Client s3Client() { return S3Client.builder() @@ -39,4 +46,12 @@ public S3Client s3Client() { .responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED) .build(); } + @Bean + public LambdaClient lambdaClient() { + return LambdaClient.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(awsAccessKey, awsSecretKey))) + .build(); + } } From 62f5945f3d3475423306252f30a1970e527f773a Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 20:25:49 +0900 Subject: [PATCH 20/95] =?UTF-8?q?refactor:=20=EB=A6=AC=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=ED=95=A8=EC=88=98=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20webp=20=EC=99=B8=20jpeg=20=ED=99=95=EC=9E=A5=EC=9E=90=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image/application/R2Storage.java | 160 ++++++++---------- .../swyp8team2/image/util/FileValidator.java | 5 +- 2 files changed, 77 insertions(+), 88 deletions(-) diff --git a/src/main/java/com/swyp8team2/image/application/R2Storage.java b/src/main/java/com/swyp8team2/image/application/R2Storage.java index 8222f2b0..4af95958 100644 --- a/src/main/java/com/swyp8team2/image/application/R2Storage.java +++ b/src/main/java/com/swyp8team2/image/application/R2Storage.java @@ -1,37 +1,46 @@ package com.swyp8team2.image.application; -import com.sksamuel.scrimage.ImmutableImage; -import com.swyp8team2.common.exception.BadRequestException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; import com.swyp8team2.common.exception.ServiceUnavailableException; import com.swyp8team2.common.util.DateTime; import com.swyp8team2.image.presentation.dto.ImageFileDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.imgscalr.Scalr; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; +import software.amazon.awssdk.services.lambda.model.InvokeResponse; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import javax.imageio.IIOImage; import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.FileImageOutputStream; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Base64; @Component @Slf4j @RequiredArgsConstructor public class R2Storage { + private static final String CONVERT_EXTENSION = ".jpeg"; + @Value("${file.endpoint}") private String imageDomainUrl; @@ -41,126 +50,103 @@ public class R2Storage { @Value("${r2.bucket.path}") private String filePath; - @Value("${r2.bucket.resize-path}") - private String resizedFilePath; - - @Value("${r2.bucket.resize-height}") - private int resizeHeight; + @Value("${aws.lambda-arn}") + private String lambdaFunctionName; private final S3Client s3Client; + private final LambdaClient lambdaClient; public List uploadImageFile(MultipartFile... files) { List imageFiles = new ArrayList<>(); - List tempFiles = new ArrayList<>(); try { for (int i = 0; i < files.length; i++) { MultipartFile file = files[i]; - String originFileName = file.getOriginalFilename(); - if (originFileName.length() > 100) { - throw new BadRequestException(ErrorCode.FILE_NAME_TOO_LONG); - } - String realFileName = getRealFileName(originFileName, filePath, i); - File tempFile = File.createTempFile("upload_", "_" + originFileName); - file.transferTo(tempFile); + String originFilename = file.getOriginalFilename(); + String fileExtension = originFilename.substring(originFilename.lastIndexOf(".")); + Map metadata = new HashMap<>(); + String realFileName = getRealFileName(filePath, i, CONVERT_EXTENSION); - int splitIndex = originFileName.lastIndexOf("/"); - String imageType = originFileName.substring(splitIndex + 1).toLowerCase(); + File tempFile = File.createTempFile("upload_", originFilename); + file.transferTo(tempFile); + switch(fileExtension) { + case ".heic", ".heif" -> { + convertHeicToWebp(tempFile, originFilename, realFileName); + } case ".png", ".gif" -> { + metadata.put("Content-Type", "image/jpeg"); + s3PutObject(convertToJpg(tempFile), realFileName, metadata); + } default -> { + realFileName = getRealFileName(filePath, i, fileExtension); + s3PutObject(tempFile, realFileName, metadata); + } + } - File originFile = s3PutObject(tempFile, realFileName, imageType); String imageUrl = imageDomainUrl + realFileName; - String resizeImageUrl = resizeImage(tempFile, realFileName, resizeHeight); - - log.debug("uploadImageFile originFileName: {}, imageUrl: {}, resizeImageUrl: {}", - originFileName, imageUrl, resizeImageUrl); - - imageFiles.add(new ImageFileDto(originFileName, imageUrl, resizeImageUrl)); - tempFiles.add(originFile); + imageFiles.add(new ImageFileDto(originFilename, imageUrl, imageUrl)); + deleteTempFile(tempFile); } return imageFiles; } catch (IOException e) { - log.error("Failed to create temp file", e); + log.error("Failed to upload file", e); throw new ServiceUnavailableException(ErrorCode.SERVICE_UNAVAILABLE); - } finally { - tempFiles.forEach(this::deleteTempFile); } } - private String resizeImage(File file, String realFileName, int targetHeight) { - try { - String ext = Optional.of(realFileName) - .filter(name -> name.contains(".")) - .map(name -> name.substring(name.lastIndexOf('.') + 1)) - .orElseThrow(() -> new BadRequestException(ErrorCode.MISSING_FILE_EXTENSION)) - .toLowerCase(); - - BufferedImage srcImage; - if ("webp".equals(ext)) { - srcImage = ImmutableImage.loader().fromFile(file).awt(); - } else { - srcImage = ImageIO.read(file); - } - BufferedImage resizedImage = highQualityResize(srcImage, targetHeight); + private void convertHeicToWebp(File sourceFile, String originFilename, String realFileName) throws IOException { + byte[] fileContent = Files.readAllBytes(sourceFile.toPath()); + String base64Content = Base64.getEncoder().encodeToString(fileContent); - int splitIndex = realFileName.lastIndexOf("/") + 1; - realFileName = realFileName.substring(splitIndex); - String dstKey = resizedFilePath + realFileName; - String imageType = realFileName.substring(realFileName.lastIndexOf(".") + 1).toLowerCase(); + Map payload = new HashMap<>(); + payload.put("fileContent", base64Content); + payload.put("originFilename", originFilename); + payload.put("key", realFileName); - File tempFile = File.createTempFile("resized_", "." + imageType); - ImageIO.write(resizedImage, imageType, tempFile); + ObjectMapper objectMapper = new ObjectMapper(); + String payloadJson = objectMapper.writeValueAsString(payload); + InvokeRequest invokeRequest = InvokeRequest.builder() + .functionName(lambdaFunctionName) + .payload(SdkBytes.fromUtf8String(payloadJson)) + .build(); - s3PutObject(tempFile, dstKey, imageType); - deleteTempFile(tempFile); + InvokeResponse response = lambdaClient.invoke(invokeRequest); + String responseJson = response.payload().asUtf8String(); + Map responseMap = objectMapper.readValue(responseJson, Map.class); - return imageDomainUrl + dstKey; - } catch (IOException e) { - log.error("Failed to create temp file", e); - throw new ServiceUnavailableException(ErrorCode.SERVICE_UNAVAILABLE); + if (responseMap.containsKey("errorMessage")) { + log.error("Lambda service error, {}", responseMap.get("errorMessage")); + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); } } - private BufferedImage highQualityResize(BufferedImage originalImage, int targetHeight) { - int originalWidth = originalImage.getWidth(); - int originalHeight = originalImage.getHeight(); + private File convertToJpg(File sourceFile) throws IOException { + BufferedImage image = ImageIO.read(sourceFile); + File jpgFile = File.createTempFile("converted_", ".jpeg"); - double scale = (double) targetHeight / originalHeight; - int targetWidth = (int) (originalWidth * scale); + ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next(); + try (FileImageOutputStream output = new FileImageOutputStream(jpgFile)) { + writer.setOutput(output); + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.9f); - return Scalr.resize(originalImage, Scalr.Method.QUALITY, Scalr.Mode.FIT_EXACT, targetWidth, targetHeight); + writer.write(null, new IIOImage(image, null, null), param); + writer.dispose(); + return jpgFile; + } } - private File s3PutObject(File file, String realFileName, String imageType) { - Map metadata = getMetadataMap(imageType); + private void s3PutObject(File file, String realFileName, Map metadata) { PutObjectRequest objectRequest = PutObjectRequest.builder() .bucket(bucketName) .metadata(metadata) .key(realFileName) .build(); - PutObjectResponse putObjectResponse = s3Client.putObject(objectRequest, RequestBody.fromFile(file)); - return file; - } - - private Map getMetadataMap(String imageType) { - Map metadata = new HashMap<>(); - switch (imageType) { - case "png" -> { - metadata.put("Content-Type", "image/png"); - } case "gif" -> { - metadata.put("Content-Type", "image/gif"); - } case "webp" -> { - metadata.put("Content-Type", "image/webp"); - } default -> { - metadata.put("Content-Type", "image/jpeg"); - } - } - return metadata; + s3Client.putObject(objectRequest, RequestBody.fromFile(file)); } - private String getRealFileName(String originFileName, String filePath, int sequence) { - String objectType = originFileName.substring(originFileName.lastIndexOf(".")).toLowerCase(); - return filePath + DateTime.getCurrentTimestamp() + sequence + objectType; + private String getRealFileName(String filePath, int sequence, String extension) { + return filePath + DateTime.getCurrentTimestamp() + sequence + extension; } private void deleteTempFile(File tempFile) { diff --git a/src/main/java/com/swyp8team2/image/util/FileValidator.java b/src/main/java/com/swyp8team2/image/util/FileValidator.java index 32a00c82..b3071d1b 100644 --- a/src/main/java/com/swyp8team2/image/util/FileValidator.java +++ b/src/main/java/com/swyp8team2/image/util/FileValidator.java @@ -36,8 +36,11 @@ private void validate(MultipartFile file) { } String originalFilename = file.getOriginalFilename(); + if (originalFilename.length() > 100) { + throw new BadRequestException(ErrorCode.FILE_NAME_TOO_LONG); + } + String ext = Optional.of(originalFilename) - .filter(name -> name.contains(".")) .map(name -> name.substring(name.lastIndexOf('.') + 1)) .orElseThrow(() -> new BadRequestException(ErrorCode.MISSING_FILE_EXTENSION)) .toLowerCase(); From ebd0cf91b849e350ac8846e1d699f5509a6ceb4d Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 20:41:37 +0900 Subject: [PATCH 21/95] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=9C=ED=8D=BC?= =?UTF-8?q?=ED=8B=B0=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index a9f449f3..0072a031 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit a9f449f36c6328393160acb9014cbbac33b9d82d +Subproject commit 0072a031b6cc2fd906fc7dccc35577f10ff0553c From b3e83616de2331acaf5f7b0de6644581128bcd39 Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 20:59:14 +0900 Subject: [PATCH 22/95] =?UTF-8?q?test:=20=ED=8C=8C=EC=9D=BC=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EC=B2=B4=ED=81=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/image/util/FileValidator.java | 1 + .../image/util/FileValidatorTest.java | 30 +++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/swyp8team2/image/util/FileValidator.java b/src/main/java/com/swyp8team2/image/util/FileValidator.java index b3071d1b..850ca2bc 100644 --- a/src/main/java/com/swyp8team2/image/util/FileValidator.java +++ b/src/main/java/com/swyp8team2/image/util/FileValidator.java @@ -41,6 +41,7 @@ private void validate(MultipartFile file) { } String ext = Optional.of(originalFilename) + .filter(name -> name.contains(".")) .map(name -> name.substring(name.lastIndexOf('.') + 1)) .orElseThrow(() -> new BadRequestException(ErrorCode.MISSING_FILE_EXTENSION)) .toLowerCase(); diff --git a/src/test/java/com/swyp8team2/image/util/FileValidatorTest.java b/src/test/java/com/swyp8team2/image/util/FileValidatorTest.java index 14fb3a6f..17aecc86 100644 --- a/src/test/java/com/swyp8team2/image/util/FileValidatorTest.java +++ b/src/test/java/com/swyp8team2/image/util/FileValidatorTest.java @@ -20,13 +20,13 @@ class FileValidatorTest { @BeforeEach void setUp() { - String allowedExtensions = "gif,jpg,jpeg,png"; + String allowedExtensions = "gif,jpg,jpeg,png,webp,heic,heif"; fileValidator = new FileValidator(allowedExtensions); } @Test @DisplayName("파일 유효성 체크 - 파일 크기 초과") - void validate_validFile_shouldPass() { + void validate_exceedMaxFileSize() { // given byte[] largeContent = new byte[(int) (MAX_FILE_SIZE + 1)]; MockMultipartFile file = new MockMultipartFile( @@ -44,7 +44,7 @@ void validate_validFile_shouldPass() { @Test @DisplayName("파일 유효성 체크 - 지원하지 않는 확장자") - void validate_unsupportedExtension_shouldThrowException() { + void validate_unsupportedFileExtension() { // given MockMultipartFile file = new MockMultipartFile( "file", @@ -59,14 +59,32 @@ void validate_unsupportedExtension_shouldThrowException() { .hasMessage(ErrorCode.UNSUPPORTED_FILE_EXTENSION.getMessage()); } + @Test + @DisplayName("파일 유효성 체크 - 파일명 너무 김") + void validate_fileNameTooLong() { + // given + String filename = new String(new char[101])+".jpeg"; + MockMultipartFile file = new MockMultipartFile( + "file", + filename, + "", + "dummy content".getBytes(StandardCharsets.UTF_8) + ); + + // when then + assertThatThrownBy(() -> fileValidator.validate(file)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.FILE_NAME_TOO_LONG.getMessage()); + } + @Test @DisplayName("파일 유효성 체크 - 확장자 누락") - void validate_missingFileExtension_shouldThrowException() { + void validate_missingFileExtension() { // given MockMultipartFile file = new MockMultipartFile( "file", "test", - "", + "text/plain", "dummy content".getBytes(StandardCharsets.UTF_8) ); @@ -78,7 +96,7 @@ void validate_missingFileExtension_shouldThrowException() { @Test @DisplayName("파일 유효성 체크 - 여러 파일 중 하나가 유효성 실패") - void validate_multipleFiles_oneInvalid_shouldThrowException() { + void validate_multipleFilesOneInvalid() { // given MockMultipartFile file1 = new MockMultipartFile( "file", From ceb9feab6b1d54acc5665cb76973c9fd0ba1a4cb Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 22:35:57 +0900 Subject: [PATCH 23/95] =?UTF-8?q?docs:=20=EB=8C=93=EA=B8=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/comments.adoc | 5 ++++ .../presentation/CommentController.java | 15 ++++++++-- .../dto/UpdateCommentRequest.java | 9 ++++++ .../presentation/CommentControllerTest.java | 29 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index 1491db53..9019ce65 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -11,6 +11,11 @@ operation::comment-controller-test/create-comment[snippets='http-request,curl-re operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] +[[댓글-수정]] +=== `POST` 댓글 수정 + +operation::comment-controller-test/update-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] + [[댓글-삭제]] === 댓글 삭제 (미구현) diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index b727fb2b..14f3bdc7 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -2,9 +2,9 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.comment.application.CommentService; -import com.swyp8team2.comment.presentation.dto.AuthorDto; import com.swyp8team2.comment.presentation.dto.CommentResponse; import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.comment.presentation.dto.UpdateCommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; @@ -20,8 +20,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; @RestController @@ -52,6 +50,17 @@ public ResponseEntity> selectCommen return ResponseEntity.ok(commentService.findComments(userId, postId, cursor, size)); } + @PostMapping("/{commentId}") + public ResponseEntity updateComment( + @PathVariable("postId") Long postId, + @PathVariable("commentId") Long commentId, + @RequestBody UpdateCommentRequest request, + @AuthenticationPrincipal UserInfo userInfo + ) { + + return ResponseEntity.ok().build(); + } + @DeleteMapping("/{commentId}") public ResponseEntity deleteComment( @PathVariable("postId") Long postId, diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java b/src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java new file mode 100644 index 00000000..87d28028 --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java @@ -0,0 +1,9 @@ +package com.swyp8team2.comment.presentation.dto; + +import jakarta.validation.constraints.NotNull; + +public record UpdateCommentRequest( + @NotNull + String content +) { +} diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index da30223a..b7d65f22 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -4,6 +4,7 @@ import com.swyp8team2.comment.presentation.dto.AuthorDto; import com.swyp8team2.comment.presentation.dto.CommentResponse; import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.comment.presentation.dto.UpdateCommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; @@ -136,6 +137,34 @@ void findComments() throws Exception { verify(commentService, times(1)).findComments(eq(null), eq(postId), eq(cursor), eq(size)); } + @Test + @WithMockUserInfo + @DisplayName("댓글 수정") + void updateComment() throws Exception { + //given + UpdateCommentRequest request = new UpdateCommentRequest("수정 댓글"); + + //when then + mockMvc.perform(post("/posts/{postId}/comments/{commentId}", "1", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id"), + parameterWithName("commentId").description("댓글 Id") + ), + requestFields( + fieldWithPath("content") + .type(JsonFieldType.STRING) + .description("댓글 내용") + .attributes(constraints("최대 ?글자")) + ) + )); + } + @Test @WithMockUserInfo @DisplayName("댓글 삭제") From 5dc9701313ae08d0e8cd14229bbeb3e05c342671 Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 22:38:16 +0900 Subject: [PATCH 24/95] =?UTF-8?q?docs:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 5 +++++ .../auth/presentation/AuthController.java | 11 +++++++++++ .../auth/presentation/AuthControllerTest.java | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 0468ec86..93708379 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -32,3 +32,8 @@ operation::auth-controller-test/reissue[snippets='http-request,curl-request,requ === `POST` 로그아웃 operation::auth-controller-test/sign-out[snippets='http-request,curl-request,request-cookies,request-headers,http-response,response-cookies'] + +[[회원탈퇴]] +=== `POST` 회원탈퇴 + +operation::auth-controller-test/withdraw[snippets='http-request,curl-request,request-cookies,request-headers,http-response'] \ No newline at end of file diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 93ccec61..eb99d8be 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -90,4 +90,15 @@ public ResponseEntity signOut( authService.signOut(userInfo.userId(), refreshToken); return ResponseEntity.ok().build(); } + + @PostMapping("/withdraw") + public ResponseEntity withdraw( + @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, + @AuthenticationPrincipal UserInfo userInfo + ) { + if (Objects.isNull(refreshToken)) { + throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); + } + return ResponseEntity.ok().build(); + } } diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 3682a11e..b4c2ef32 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -223,4 +223,23 @@ void signOut() throws Exception { ) )); } + + @Test + @WithMockUserInfo + @DisplayName("회원탈퇴") + void withdraw() throws Exception { + //given + + //when then + mockMvc.perform(post("/auth/withdraw") + .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken")) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + requestCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") + ) + )); + } } From 5654dc23a0a522e663e26550250603f0bd74bfae Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 5 Mar 2025 22:45:26 +0900 Subject: [PATCH 25/95] =?UTF-8?q?docs:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EB=85=B8=EC=B6=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 10 ++++ .../post/presentation/PostController.java | 23 ++++++-- .../presentation/dto/UpdatePostRequest.java | 9 +++ .../post/presentation/PostControllerTest.java | 55 ++++++++++++++++--- 4 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/UpdatePostRequest.java diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 7087793d..58e0f244 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -34,6 +34,16 @@ operation::post-controller-test/find-my-post[snippets='http-request,curl-request operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] +[[게시글-투표-수정]] +=== `POST` 게시글 노출 변경 + +operation::post-controller-test/toggle-status-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] + +[[게시글-투표-수정]] +=== `POST` 게시글 투표 수정 + +operation::post-controller-test/update-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] + [[게시글-투표-마감]] === `POST` 게시글 투표 마감 diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 0ef4b6c0..0ec7e37b 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -3,11 +3,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.application.PostService; -import com.swyp8team2.post.presentation.dto.CreatePostRequest; -import com.swyp8team2.post.presentation.dto.CreatePostResponse; -import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; -import com.swyp8team2.post.presentation.dto.PostResponse; -import com.swyp8team2.post.presentation.dto.SimplePostResponse; +import com.swyp8team2.post.presentation.dto.*; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; @@ -71,6 +67,23 @@ public ResponseEntity> findVoteStatus( return ResponseEntity.ok(postService.findVoteStatus(userInfo.userId(), postId)); } + @PostMapping("/{postId}/status") + public ResponseEntity toggleStatusPost( + @PathVariable("postId") Long postId, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } + + @PostMapping("/{postId}/update") + public ResponseEntity updatePost( + @PathVariable("postId") Long postId, + @Valid @RequestBody UpdatePostRequest request, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } + @PostMapping("/{postId}/close") public ResponseEntity closePost( @PathVariable("postId") Long postId, diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/UpdatePostRequest.java b/src/main/java/com/swyp8team2/post/presentation/dto/UpdatePostRequest.java new file mode 100644 index 00000000..bf3b9b79 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/UpdatePostRequest.java @@ -0,0 +1,9 @@ +package com.swyp8team2.post.presentation.dto; + +import jakarta.validation.constraints.NotNull; + +public record UpdatePostRequest( + @NotNull + String description +) { +} diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 0acd0a94..8c017294 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -2,14 +2,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.domain.Status; -import com.swyp8team2.post.presentation.dto.AuthorDto; -import com.swyp8team2.post.presentation.dto.CreatePostRequest; -import com.swyp8team2.post.presentation.dto.CreatePostResponse; -import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; -import com.swyp8team2.post.presentation.dto.PostResponse; -import com.swyp8team2.post.presentation.dto.SimplePostResponse; -import com.swyp8team2.post.presentation.dto.PostImageRequestDto; -import com.swyp8team2.post.presentation.dto.PostImageResponse; +import com.swyp8team2.post.presentation.dto.*; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; import org.junit.jupiter.api.DisplayName; @@ -35,6 +28,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -358,6 +352,51 @@ void findVotedPost() throws Exception { )); } + @Test + @WithMockUserInfo + @DisplayName("게시글 노출 변경") + void toggleStatusPost() throws Exception { + //given + + //when then + mockMvc.perform(post("/posts/{postId}/status", 1) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ) + )); + } + + @Test + @WithMockUserInfo + @DisplayName("게시글 수정") + void updatePost() throws Exception { + //given + UpdatePostRequest request = new UpdatePostRequest("설명"); + + //when then + mockMvc.perform(post("/posts/{postId}/update", 1) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ), + requestFields( + fieldWithPath("description") + .type(JsonFieldType.STRING) + .description("설명") + .attributes(constraints("0~100자 사이")) + ) + )); + } + @Test @WithMockUserInfo @DisplayName("게시글 마감") From aaad0f2a7214ed50e8c4e5ad840dfb264f84b516 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 08:11:35 +0900 Subject: [PATCH 26/95] =?UTF-8?q?chore:=20=ED=95=A8=EC=88=98=EB=AA=85=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 --- src/main/java/com/swyp8team2/image/application/R2Storage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/image/application/R2Storage.java b/src/main/java/com/swyp8team2/image/application/R2Storage.java index 4af95958..2ecca142 100644 --- a/src/main/java/com/swyp8team2/image/application/R2Storage.java +++ b/src/main/java/com/swyp8team2/image/application/R2Storage.java @@ -70,7 +70,7 @@ public List uploadImageFile(MultipartFile... files) { file.transferTo(tempFile); switch(fileExtension) { case ".heic", ".heif" -> { - convertHeicToWebp(tempFile, originFilename, realFileName); + convertHeicToJpg(tempFile, originFilename, realFileName); } case ".png", ".gif" -> { metadata.put("Content-Type", "image/jpeg"); s3PutObject(convertToJpg(tempFile), realFileName, metadata); @@ -92,7 +92,7 @@ public List uploadImageFile(MultipartFile... files) { } } - private void convertHeicToWebp(File sourceFile, String originFilename, String realFileName) throws IOException { + private void convertHeicToJpg(File sourceFile, String originFilename, String realFileName) throws IOException { byte[] fileContent = Files.readAllBytes(sourceFile.toPath()); String base64Content = Base64.getEncoder().encodeToString(fileContent); From b4d7b47f3bd347e120b78b6ca500405778f5ecf0 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 6 Mar 2025 11:56:44 +0900 Subject: [PATCH 27/95] =?UTF-8?q?chore:=20querydsl=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ build.gradle | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 653e7c70..5425fc8d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ out/ .DS_Store application*.yml + +/src/main/generated/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index f036af26..6341ffa2 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,12 @@ dependencies { // base64 implementation 'io.seruco.encoding:base62:0.1.3' + // querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' @@ -62,6 +68,7 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } +// rest docs ext { snippetsDir = file('build/generated-snippets') } @@ -73,7 +80,7 @@ asciidoctor { dependsOn test } -task copyDocument(type: Copy) { +tasks.register('copyDocument', Copy) { dependsOn asciidoctor doFirst { delete file('src/main/resources/static/docs') @@ -89,6 +96,24 @@ bootJar { } } +// querydsl +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +tasks.withType(JavaCompile).configureEach { + options.getGeneratedSourceOutputDirectory().set(file(querydslDir)) +} + +clean.doLast { + file(querydslDir).deleteDir() +} + +clean { + delete file('src/main/generated') +} + tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() From a072d44f464460e061f4bb32e54a8b412933873d Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 6 Mar 2025 11:56:57 +0900 Subject: [PATCH 28/95] =?UTF-8?q?feat:=20querydsl=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/QueryDslConfig.java | 15 +++++++++++++ .../swyp8team2/common/util/QueryDslUtil.java | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/main/java/com/swyp8team2/common/config/QueryDslConfig.java create mode 100644 src/main/java/com/swyp8team2/common/util/QueryDslUtil.java diff --git a/src/main/java/com/swyp8team2/common/config/QueryDslConfig.java b/src/main/java/com/swyp8team2/common/config/QueryDslConfig.java new file mode 100644 index 00000000..7d25990b --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/QueryDslConfig.java @@ -0,0 +1,15 @@ +package com.swyp8team2.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } +} diff --git a/src/main/java/com/swyp8team2/common/util/QueryDslUtil.java b/src/main/java/com/swyp8team2/common/util/QueryDslUtil.java new file mode 100644 index 00000000..c817219a --- /dev/null +++ b/src/main/java/com/swyp8team2/common/util/QueryDslUtil.java @@ -0,0 +1,21 @@ +package com.swyp8team2.common.util; + +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.NumberPath; + +import java.util.List; + +public abstract class QueryDslUtil { + + public static Predicate ltCursor(Long cursor, NumberPath id) { + return cursor == null ? null : id.lt(cursor); + } + + public static boolean removeLastIfHasNext(int size, List content) { + if (content.size() > size) { + content.remove(size); + return true; + } + return false; + } +} From 36423fddfb6731bb6927d0c0bc4d34b30340ee62 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 6 Mar 2025 11:57:46 +0900 Subject: [PATCH 29/95] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=EA=B0=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=ED=95=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/PostQueryRepository.java | 43 ++++++++++ .../PostQueryRepositoryTest.java | 81 +++++++++++++++++++ .../swyp8team2/support/RepositoryTest.java | 8 +- 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java create mode 100644 src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java diff --git a/src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java b/src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java new file mode 100644 index 00000000..5d5148ec --- /dev/null +++ b/src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java @@ -0,0 +1,43 @@ +package com.swyp8team2.post.infrastructure; + +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.swyp8team2.common.util.QueryDslUtil; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.QPost; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.swyp8team2.common.util.QueryDslUtil.*; +import static com.swyp8team2.common.util.QueryDslUtil.ltCursor; + +@Repository +@RequiredArgsConstructor +public class PostQueryRepository { + + private final JPAQueryFactory factory; + + /** + * SELECT p + * FROM Post p + * WHERE p.userId = :userId + * AND (:cursor IS NULL OR p.id < :cursor) + * ORDER BY p.id DESC + **/ + Slice findByUserId(Long userId, Long cursor, Pageable pageable) { + List postList = factory.selectFrom(QPost.post) + .where(QPost.post.userId.eq(userId) + .and(ltCursor(cursor, QPost.post.id))) + .orderBy(QPost.post.id.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + boolean hasNext = removeLastIfHasNext(pageable.getPageSize(), postList); + return new SliceImpl<>(postList, pageable, hasNext); + } +} diff --git a/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java b/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java new file mode 100644 index 00000000..7efea94a --- /dev/null +++ b/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java @@ -0,0 +1,81 @@ +package com.swyp8team2.post.infrastructure; + +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.support.RepositoryTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +import java.util.ArrayList; +import java.util.List; + +import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; +import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class PostQueryRepositoryTest extends RepositoryTest { + + @Autowired + PostQueryRepository postQueryRepository; + + @Autowired + PostRepository postRepository; + + @Test + @DisplayName("유저가 작성한 게시글 조회 - 게시글이 15개일 경우 15번쨰부터 10개 조회해야 함") + void select_post_findByUserId1() throws Exception { + //given + long userId = 1L; + List posts = createPosts(userId); + int size = 10; + + //when + Slice res = postQueryRepository.findByUserId(userId, null, PageRequest.ofSize(size)); + + //then + assertAll( + () -> assertThat(res.getContent().size()).isEqualTo(size), + () -> assertThat(res.getContent().get(0).getId()).isEqualTo(posts.get(posts.size() - 1).getId()), + () -> assertThat(res.getContent().get(1).getId()).isEqualTo(posts.get(posts.size() - 2).getId()), + () -> assertThat(res.getContent().get(2).getId()).isEqualTo(posts.get(posts.size() - 3).getId()), + () -> assertThat(res.hasNext()).isTrue() + ); + } + + @Test + @DisplayName("유저가 작성한 게시글 조회 - 15개 중에 커서가 5번째 게시글의 id면 4번째부터 0번째까지 조회해야 함") + void select_post_findByUserId2() throws Exception { + //given + long userId = 1L; + List posts = createPosts(userId); + int size = 10; + int cursorIndex = 5; + + //when + Slice res = postQueryRepository.findByUserId(userId, posts.get(cursorIndex).getId(), PageRequest.ofSize(size)); + + //then + assertAll( + () -> assertThat(res.getContent().size()).isEqualTo(5), + () -> assertThat(res.getContent().get(0).getId()).isEqualTo(posts.get(cursorIndex - 1).getId()), + () -> assertThat(res.getContent().get(1).getId()).isEqualTo(posts.get(cursorIndex - 2).getId()), + () -> assertThat(res.getContent().get(2).getId()).isEqualTo(posts.get(cursorIndex - 3).getId()), + () -> assertThat(res.hasNext()).isFalse() + ); + } + + private List createPosts(long userId) { + List posts = new ArrayList<>(); + for (int i = 0; i < 30; i += 2) { + ImageFile imageFile1 = createImageFile(i); + ImageFile imageFile2 = createImageFile(i + 1); + posts.add(postRepository.save(createPost(userId, imageFile1, imageFile2, i))); + } + return posts; + } +} diff --git a/src/test/java/com/swyp8team2/support/RepositoryTest.java b/src/test/java/com/swyp8team2/support/RepositoryTest.java index 9b3ded7e..eaad71c8 100644 --- a/src/test/java/com/swyp8team2/support/RepositoryTest.java +++ b/src/test/java/com/swyp8team2/support/RepositoryTest.java @@ -1,10 +1,16 @@ package com.swyp8team2.support; import com.swyp8team2.common.config.JpaConfig; +import com.swyp8team2.common.config.QueryDslConfig; +import com.swyp8team2.post.infrastructure.PostQueryRepository; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +@Import({ + JpaConfig.class, + QueryDslConfig.class, + PostQueryRepository.class +}) @DataJpaTest -@Import(JpaConfig.class) public abstract class RepositoryTest { } From 1ff6a0506efa4ad95572d721258a30599857a8e6 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 6 Mar 2025 11:58:57 +0900 Subject: [PATCH 30/95] =?UTF-8?q?refactor:=20post=20jpaRepository=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/common/dev/DataInitializer.java | 9 +++------ .../com/swyp8team2/post/application/PostService.java | 6 +++--- .../PostJpaRepository.java} | 5 +++-- .../com/swyp8team2/vote/application/VoteService.java | 4 ++-- .../com/swyp8team2/post/application/PostServiceTest.java | 4 ++-- ...ostRepositoryTest.java => PostJpaRepositoryTest.java} | 5 +++-- .../post/infrastructure/PostQueryRepositoryTest.java | 3 +-- .../com/swyp8team2/vote/application/VoteServiceTest.java | 4 ++-- 8 files changed, 19 insertions(+), 21 deletions(-) rename src/main/java/com/swyp8team2/post/{domain/PostRepository.java => infrastructure/PostJpaRepository.java} (88%) rename src/test/java/com/swyp8team2/post/domain/{PostRepositoryTest.java => PostJpaRepositoryTest.java} (96%) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 39d8077f..07353fae 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -11,17 +11,14 @@ import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; -import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.infrastructure.PostJpaRepository; import com.swyp8team2.user.domain.NicknameAdjective; import com.swyp8team2.user.domain.NicknameAdjectiveRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.application.VoteService; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -37,7 +34,7 @@ public class DataInitializer { private final NicknameAdjectiveRepository nicknameAdjectiveRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; - private final PostRepository postRepository; + private final PostJpaRepository postRepository; private final CryptoService shaereUrlCryptoService; private final JwtService jwtService; private final VoteService voteService; @@ -47,7 +44,7 @@ public DataInitializer( NicknameAdjectiveRepository nicknameAdjectiveRepository, UserRepository userRepository, ImageFileRepository imageFileRepository, - PostRepository postRepository, + PostJpaRepository postRepository, @ShareUrlCryptoService CryptoService shaereUrlCryptoService, JwtService jwtService, VoteService voteService, diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 1d27bb07..d9ced967 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -10,7 +10,7 @@ import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.infrastructure.PostJpaRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostResponse; @@ -33,7 +33,7 @@ @Transactional(readOnly = true) public class PostService { - private final PostRepository postRepository; + private final PostJpaRepository postRepository; private final UserRepository userRepository; private final RatioCalculator ratioCalculator; private final ImageFileRepository imageFileRepository; @@ -41,7 +41,7 @@ public class PostService { private final CryptoService shareUrlCryptoService; public PostService( - PostRepository postRepository, + PostJpaRepository postRepository, UserRepository userRepository, RatioCalculator ratioCalculator, ImageFileRepository imageFileRepository, diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/infrastructure/PostJpaRepository.java similarity index 88% rename from src/main/java/com/swyp8team2/post/domain/PostRepository.java rename to src/main/java/com/swyp8team2/post/infrastructure/PostJpaRepository.java index 66b6b529..8c955825 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/infrastructure/PostJpaRepository.java @@ -1,5 +1,6 @@ -package com.swyp8team2.post.domain; +package com.swyp8team2.post.infrastructure; +import com.swyp8team2.post.domain.Post; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,7 +12,7 @@ import java.util.Optional; @Repository -public interface PostRepository extends JpaRepository { +public interface PostJpaRepository extends JpaRepository { @Query(""" SELECT p diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index e7f093d9..40783775 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -3,7 +3,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.infrastructure.PostJpaRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -19,7 +19,7 @@ public class VoteService { private final VoteRepository voteRepository; private final UserRepository userRepository; - private final PostRepository postRepository; + private final PostJpaRepository postRepository; @Transactional public Long vote(Long voterId, Long postId, Long imageId) { diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index b85b73ee..e443f2c7 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -8,7 +8,7 @@ import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.infrastructure.PostJpaRepository; import com.swyp8team2.post.domain.Status; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; @@ -44,7 +44,7 @@ class PostServiceTest extends IntegrationTest { PostService postService; @Autowired - PostRepository postRepository; + PostJpaRepository postRepository; @Autowired UserRepository userRepository; diff --git a/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java b/src/test/java/com/swyp8team2/post/domain/PostJpaRepositoryTest.java similarity index 96% rename from src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java rename to src/test/java/com/swyp8team2/post/domain/PostJpaRepositoryTest.java index e5b5157b..cc29da00 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostJpaRepositoryTest.java @@ -1,6 +1,7 @@ package com.swyp8team2.post.domain; import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.post.infrastructure.PostJpaRepository; import com.swyp8team2.support.RepositoryTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,10 +17,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -class PostRepositoryTest extends RepositoryTest { +class PostJpaRepositoryTest extends RepositoryTest { @Autowired - PostRepository postRepository; + PostJpaRepository postRepository; @Test @DisplayName("유저가 작성한 게시글 조회 - 게시글이 15개일 경우 15번쨰부터 10개 조회해야 함") diff --git a/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java b/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java index 7efea94a..e60f295e 100644 --- a/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java +++ b/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java @@ -2,7 +2,6 @@ import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.support.RepositoryTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -24,7 +23,7 @@ class PostQueryRepositoryTest extends RepositoryTest { PostQueryRepository postQueryRepository; @Autowired - PostRepository postRepository; + PostJpaRepository postRepository; @Test @DisplayName("유저가 작성한 게시글 조회 - 게시글이 15개일 경우 15번쨰부터 10개 조회해야 함") diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index fff0993c..b1f3de96 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -6,7 +6,7 @@ import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.infrastructure.PostJpaRepository; import com.swyp8team2.post.domain.Status; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; @@ -38,7 +38,7 @@ class VoteServiceTest extends IntegrationTest { VoteRepository voteRepository; @Autowired - PostRepository postRepository; + PostJpaRepository postRepository; @Autowired ImageFileRepository imageFileRepository; From 9e0987b5f0e7a326b9f09653b1de1784b296548b Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 15:17:03 +0900 Subject: [PATCH 31/95] =?UTF-8?q?docs:=20=EB=AF=B8=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=88=98=EC=8B=9D=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 2 +- src/docs/asciidoc/comments.adoc | 4 ++-- src/docs/asciidoc/posts.adoc | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 93708379..fcf60714 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -34,6 +34,6 @@ operation::auth-controller-test/reissue[snippets='http-request,curl-request,requ operation::auth-controller-test/sign-out[snippets='http-request,curl-request,request-cookies,request-headers,http-response,response-cookies'] [[회원탈퇴]] -=== `POST` 회원탈퇴 +=== `POST` 회원탈퇴 (미구현) operation::auth-controller-test/withdraw[snippets='http-request,curl-request,request-cookies,request-headers,http-response'] \ No newline at end of file diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index 9019ce65..1cd90b9e 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -12,11 +12,11 @@ operation::comment-controller-test/create-comment[snippets='http-request,curl-re operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] [[댓글-수정]] -=== `POST` 댓글 수정 +=== `POST` 댓글 수정 (미구현) operation::comment-controller-test/update-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] [[댓글-삭제]] -=== 댓글 삭제 (미구현) +=== `DELETE` 댓글 삭제 (미구현) operation::comment-controller-test/delete-comment[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 58e0f244..064912a8 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -35,12 +35,12 @@ operation::post-controller-test/find-my-post[snippets='http-request,curl-request operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] [[게시글-투표-수정]] -=== `POST` 게시글 노출 변경 +=== `POST` 게시글 노출 변경 (미구현) operation::post-controller-test/toggle-status-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] [[게시글-투표-수정]] -=== `POST` 게시글 투표 수정 +=== `POST` 게시글 투표 수정 (미구현) operation::post-controller-test/update-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] From 229fa672e98813fa2565300c7cda7b2f4fa63ce9 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 15:17:56 +0900 Subject: [PATCH 32/95] =?UTF-8?q?chore:=20PR=20=EC=88=98=EC=A0=95=EA=B1=B4?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/auth/presentation/AuthController.java | 4 ---- .../swyp8team2/comment/presentation/CommentController.java | 2 +- .../swyp8team2/auth/presentation/AuthControllerTest.java | 6 +----- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index eb99d8be..33493d43 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -93,12 +93,8 @@ public ResponseEntity signOut( @PostMapping("/withdraw") public ResponseEntity withdraw( - @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, @AuthenticationPrincipal UserInfo userInfo ) { - if (Objects.isNull(refreshToken)) { - throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); - } return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index 14f3bdc7..9fd14128 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -54,7 +54,7 @@ public ResponseEntity> selectCommen public ResponseEntity updateComment( @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId, - @RequestBody UpdateCommentRequest request, + @Valid @RequestBody UpdateCommentRequest request, @AuthenticationPrincipal UserInfo userInfo ) { diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index b4c2ef32..cee695b9 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -232,14 +232,10 @@ void withdraw() throws Exception { //when then mockMvc.perform(post("/auth/withdraw") - .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken")) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken")) .andExpect(status().isOk()) .andDo(restDocs.document( - requestHeaders(authorizationHeader()), - requestCookies( - cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") - ) + requestHeaders(authorizationHeader()) )); } } From 3f670504b2c8bbfb08d7935a056f32ce7d60d3b0 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 16:57:41 +0900 Subject: [PATCH 33/95] =?UTF-8?q?chore:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=9D=BC=EC=9E=90=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=82=AD=EC=A0=9C=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/common/domain/BaseEntity.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/swyp8team2/common/domain/BaseEntity.java b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java index 1e75cd58..fb55f796 100644 --- a/src/main/java/com/swyp8team2/common/domain/BaseEntity.java +++ b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java @@ -1,5 +1,6 @@ package com.swyp8team2.common.domain; +import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import lombok.Getter; @@ -17,9 +18,11 @@ public abstract class BaseEntity { @CreatedBy + @Column(updatable = false) private Long createdBy; @CreatedDate + @Column(updatable = false) private LocalDateTime createdAt; @LastModifiedBy @@ -30,4 +33,9 @@ public abstract class BaseEntity { private boolean deleted = false; private LocalDateTime deletedAt; + + public void delete() { + this.deleted = true; + this.deletedAt = LocalDateTime.now(); + } } From db8faa4ca012d303648fd250be2bdfc7e000903b Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 16:58:14 +0900 Subject: [PATCH 34/95] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 14 ++++++++++++++ .../comment/domain/CommentRepository.java | 10 ++++++++++ .../comment/presentation/CommentController.java | 2 +- .../com/swyp8team2/common/exception/ErrorCode.java | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index bfe61db3..4532e623 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -8,6 +8,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -50,4 +51,17 @@ private CommentResponse createCommentResponse(Comment comment, Long userId) { .orElse(null); return CommentResponse.of(comment, author, author.getId().equals(userId), voteImageId); } + + @Transactional + public void deleteComment(Long commentId, UserInfo userInfo) { + Comment comment = commentRepository.findByIdAndNotDeleted(commentId) + .orElseThrow(() -> new BadRequestException(ErrorCode.COMMENT_NOT_FOUND)); + + if (!comment.getUserNo().equals(userInfo.userId())) { + throw new UnauthorizedException(ErrorCode.FORBIDDEN); + } + + comment.delete(); + commentRepository.save(comment); + } } diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java index 0dfbfbb9..5b6a5af4 100644 --- a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -7,6 +7,8 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface CommentRepository extends JpaRepository { @@ -14,6 +16,7 @@ public interface CommentRepository extends JpaRepository { SELECT c FROM Comment c WHERE c.postId = :postId + AND c.deleted = false AND (:cursor is null or c.id > :cursor) ORDER BY c.createdAt ASC """) @@ -23,4 +26,11 @@ Slice findByPostId( Pageable pageable ); + @Query(""" + SELECT c + FROM Comment c + WHERE c.id = :commentId + AND c.deleted = false + """) + Optional findByIdAndNotDeleted(@Param("commentId") Long commentId); } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index b727fb2b..f6a22316 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -54,10 +54,10 @@ public ResponseEntity> selectCommen @DeleteMapping("/{commentId}") public ResponseEntity deleteComment( - @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId, @AuthenticationPrincipal UserInfo userInfo ) { + commentService.deleteComment(commentId, userInfo); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 2ed851b7..c6a875f5 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -22,6 +22,7 @@ public enum ErrorCode { POST_ALREADY_CLOSED("이미 마감된 게시글"), FILE_NAME_TOO_LONG("파일 이름이 너무 김"), ACCESS_DENIED_VOTE_STATUS("투표 현황 조회 권한 없음"), + COMMENT_NOT_FOUND("존재하지 않는 댓글"), //401 EXPIRED_TOKEN("토큰 만료"), From 21fd66e9318b89ea0196547802426da79d11cce5 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 16:58:32 +0900 Subject: [PATCH 35/95] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CommentServiceTest.java | 57 ++++++++++++++++++- .../comment/domain/CommentRepositoryTest.java | 15 +++++ .../presentation/CommentControllerTest.java | 6 ++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 65ca8fbe..82e20bb1 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -8,6 +8,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -24,9 +25,10 @@ import java.util.List; import java.util.Optional; +import static java.util.Optional.empty; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.eq; @@ -78,7 +80,7 @@ void findComments() { // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); - given(voteRepository.findByUserIdAndPostId(eq(user.getId()), eq(postId))).willReturn(Optional.empty()); + given(voteRepository.findByUserIdAndPostId(eq(user.getId()), eq(postId))).willReturn(empty()); // 각 댓글마다 user_no=100L 이므로, findById(100L)만 호출됨 given(userRepository.findById(100L)).willReturn(Optional.of(user)); @@ -115,11 +117,60 @@ void findComments_userNotFound() { ); given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); - given(userRepository.findById(100L)).willReturn(Optional.empty()); + given(userRepository.findById(100L)).willReturn(empty()); // when & then assertThatThrownBy(() -> commentService.findComments(1L, postId, cursor, size)) .isInstanceOf(BadRequestException.class) .hasMessage((ErrorCode.USER_NOT_FOUND.getMessage())); } + + @Test + @DisplayName("댓글 삭제") + void deleteComment() { + // given + Long postId = 1L; + UserInfo userInfo = new UserInfo(100L, Role.USER); + Comment comment = new Comment(1L, postId, 100L, "첫 번째 댓글"); + when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); + + // when + commentService.deleteComment(1L, userInfo); + + // then + assertAll( + () -> assertTrue(comment.isDeleted()), + () -> assertNotNull(comment.getDeletedAt()) + ); + } + + @Test + @DisplayName("댓글 삭제 - 존재하지 않는 댓글") + void deleteComment_commentNotFound() { + // given + UserInfo userInfo = new UserInfo(100L, Role.USER); + when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.empty()); + + // when then + assertThatThrownBy(() -> commentService.deleteComment(1L, userInfo)) + .isInstanceOf(BadRequestException.class) + .hasMessage((ErrorCode.COMMENT_NOT_FOUND.getMessage())); + } + + @Test + @DisplayName("댓글 삭제 - 권한 없는 사용자") + void deleteComment_Unauthorized() { + // given + Long postId = 1L; + UserInfo userInfo = new UserInfo(100L, Role.USER); + Comment comment = new Comment(1L, postId, 110L, "첫 번째 댓글"); + when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); + + // when then + assertThatThrownBy(() -> commentService.deleteComment(1L, userInfo)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage((ErrorCode.FORBIDDEN.getMessage())); + assertFalse(comment.isDeleted()); + assertNull(comment.getDeletedAt()); + } } diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index aaa6f9ca..133b0ace 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -37,4 +37,19 @@ void select_CommentUser() { // then2 assertThat(result2.getContent()).hasSize(2); } + + @Test + @DisplayName("댓글 조회 - 단일 조회") + void select_CommentById() { + // given + Comment comment = new Comment(1L, 100L, "content"); + commentRepository.save(comment); + + // when + Comment selectComment = commentRepository.findByIdAndNotDeleted(1L) + .orElse(new Comment(2L, 2L, 101L, "content")); + + // then + assertThat(comment.getId()).isEqualTo(selectComment.getId()); + } } \ No newline at end of file diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index da30223a..c3046ff8 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -1,6 +1,7 @@ package com.swyp8team2.comment.presentation; import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.presentation.dto.AuthorDto; import com.swyp8team2.comment.presentation.dto.CommentResponse; import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; @@ -18,6 +19,7 @@ import java.util.Collections; import java.util.List; +import static org.awaitility.Awaitility.given; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -141,6 +143,8 @@ void findComments() throws Exception { @DisplayName("댓글 삭제") void deleteComment() throws Exception { //given + Long commentId = 1L; + doNothing().when(commentService).deleteComment(eq(commentId), any(UserInfo.class)); //when then mockMvc.perform(delete("/posts/{postId}/comments/{commentId}", "1", "1") @@ -153,5 +157,7 @@ void deleteComment() throws Exception { parameterWithName("commentId").description("댓글 Id") ) )); + + verify(commentService, times(1)).deleteComment(eq(commentId), any(UserInfo.class)); } } From cb3971e82db99133389670a6b1b53edc8733671a Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 17:04:22 +0900 Subject: [PATCH 36/95] =?UTF-8?q?docs:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/comments.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index 1491db53..187b1a28 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -12,6 +12,6 @@ operation::comment-controller-test/create-comment[snippets='http-request,curl-re operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] [[댓글-삭제]] -=== 댓글 삭제 (미구현) +=== `DELETE` 댓글 삭제 operation::comment-controller-test/delete-comment[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] From 9186e12ce95882a8397d4d4783965e796e2a71c7 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Mar 2025 17:14:02 +0900 Subject: [PATCH 37/95] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단일 테스트, 전체 테스트 시 결과가 다르게 나옴 --- .../swyp8team2/comment/domain/CommentRepositoryTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index 133b0ace..d4e3a366 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -30,12 +30,6 @@ void select_CommentUser() { // then assertThat(result1.getContent()).hasSize(3); - - // when2 - Slice result2 = commentRepository.findByPostId(1L, 1L, PageRequest.of(0, 10)); - - // then2 - assertThat(result2.getContent()).hasSize(2); } @Test From eca66a63a731d8f4001fc1e9082daa9c172ec303 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 7 Mar 2025 18:30:02 +0900 Subject: [PATCH 38/95] =?UTF-8?q?chore:=20PR=20=EC=88=98=EC=A0=95=EA=B1=B4?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/comment/application/CommentService.java | 3 ++- .../swyp8team2/comment/presentation/CommentController.java | 1 + .../swyp8team2/comment/application/CommentServiceTest.java | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index 4532e623..3d2898fa 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -8,6 +8,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ForbiddenException; import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -58,7 +59,7 @@ public void deleteComment(Long commentId, UserInfo userInfo) { .orElseThrow(() -> new BadRequestException(ErrorCode.COMMENT_NOT_FOUND)); if (!comment.getUserNo().equals(userInfo.userId())) { - throw new UnauthorizedException(ErrorCode.FORBIDDEN); + throw new ForbiddenException(); } comment.delete(); diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index f6a22316..2d2fca61 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -54,6 +54,7 @@ public ResponseEntity> selectCommen @DeleteMapping("/{commentId}") public ResponseEntity deleteComment( + @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId, @AuthenticationPrincipal UserInfo userInfo ) { diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 82e20bb1..b6794971 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -8,6 +8,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ForbiddenException; import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; @@ -159,7 +160,7 @@ void deleteComment_commentNotFound() { @Test @DisplayName("댓글 삭제 - 권한 없는 사용자") - void deleteComment_Unauthorized() { + void deleteComment_forbiddenException() { // given Long postId = 1L; UserInfo userInfo = new UserInfo(100L, Role.USER); @@ -168,8 +169,7 @@ void deleteComment_Unauthorized() { // when then assertThatThrownBy(() -> commentService.deleteComment(1L, userInfo)) - .isInstanceOf(UnauthorizedException.class) - .hasMessage((ErrorCode.FORBIDDEN.getMessage())); + .isInstanceOf(ForbiddenException.class); assertFalse(comment.isDeleted()); assertNull(comment.getDeletedAt()); } From 6d52e49b6bfa3d69ee244b6a3a33b46216951560 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 09:22:35 +0900 Subject: [PATCH 39/95] =?UTF-8?q?chore:=20=EB=8C=93=EA=B8=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80/=EC=88=98=EC=A0=95=20Request=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{CreateCommentRequest.java => CommentRequest.java} | 2 +- .../comment/presentation/dto/UpdateCommentRequest.java | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) rename src/main/java/com/swyp8team2/comment/presentation/dto/{CreateCommentRequest.java => CommentRequest.java} (80%) delete mode 100644 src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CreateCommentRequest.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentRequest.java similarity index 80% rename from src/main/java/com/swyp8team2/comment/presentation/dto/CreateCommentRequest.java rename to src/main/java/com/swyp8team2/comment/presentation/dto/CommentRequest.java index 624a0fed..5abf1633 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CreateCommentRequest.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentRequest.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotEmpty; -public record CreateCommentRequest( +public record CommentRequest( @NotEmpty String content ) { diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java b/src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java deleted file mode 100644 index 87d28028..00000000 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/UpdateCommentRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.swyp8team2.comment.presentation.dto; - -import jakarta.validation.constraints.NotNull; - -public record UpdateCommentRequest( - @NotNull - String content -) { -} From 7956754bf9256dadfc08b0510b37a94ee711dc5b Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 09:24:21 +0900 Subject: [PATCH 40/95] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 18 +++++++++++++++--- .../com/swyp8team2/comment/domain/Comment.java | 5 +++++ .../presentation/CommentController.java | 9 ++++----- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index 3d2898fa..f17ebda1 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -4,12 +4,11 @@ import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.comment.presentation.dto.CommentResponse; -import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.comment.presentation.dto.CommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.ForbiddenException; -import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -32,7 +31,7 @@ public class CommentService { private final VoteRepository voteRepository; @Transactional - public void createComment(Long postId, CreateCommentRequest request, UserInfo userInfo) { + public void createComment(Long postId, CommentRequest request, UserInfo userInfo) { Comment comment = new Comment(postId, userInfo.userId(), request.content()); commentRepository.save(comment); } @@ -53,6 +52,19 @@ private CommentResponse createCommentResponse(Comment comment, Long userId) { return CommentResponse.of(comment, author, author.getId().equals(userId), voteImageId); } + @Transactional + public void updateComment(Long commentId, CommentRequest request, UserInfo userInfo) { + Comment comment = commentRepository.findByIdAndNotDeleted(commentId) + .orElseThrow(() -> new BadRequestException(ErrorCode.COMMENT_NOT_FOUND)); + + if (!comment.getUserNo().equals(userInfo.userId())) { + throw new ForbiddenException(); + } + + comment.updateComment(request.content()); + commentRepository.save(comment); + } + @Transactional public void deleteComment(Long commentId, UserInfo userInfo) { Comment comment = commentRepository.findByIdAndNotDeleted(commentId) diff --git a/src/main/java/com/swyp8team2/comment/domain/Comment.java b/src/main/java/com/swyp8team2/comment/domain/Comment.java index 686c4d38..c02b1ebe 100644 --- a/src/main/java/com/swyp8team2/comment/domain/Comment.java +++ b/src/main/java/com/swyp8team2/comment/domain/Comment.java @@ -49,4 +49,9 @@ public Comment(Long postId, Long userNo, String content) { this.userNo = userNo; this.content = content; } + + public void updateComment(String content) { + validateEmptyString(content); + this.content = content; + } } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index 595fa18f..e57d691f 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -3,8 +3,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.comment.application.CommentService; import com.swyp8team2.comment.presentation.dto.CommentResponse; -import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; -import com.swyp8team2.comment.presentation.dto.UpdateCommentRequest; +import com.swyp8team2.comment.presentation.dto.CommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; @@ -32,7 +31,7 @@ public class CommentController { @PostMapping("") public ResponseEntity createComment( @PathVariable("postId") Long postId, - @Valid @RequestBody CreateCommentRequest request, + @Valid @RequestBody CommentRequest request, @AuthenticationPrincipal UserInfo userInfo ) { commentService.createComment(postId, request, userInfo); @@ -54,10 +53,10 @@ public ResponseEntity> selectCommen public ResponseEntity updateComment( @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId, - @Valid @RequestBody UpdateCommentRequest request, + @Valid @RequestBody CommentRequest request, @AuthenticationPrincipal UserInfo userInfo ) { - + commentService.updateComment(commentId, request, userInfo); return ResponseEntity.ok().build(); } From a6695deff0963cba512d0a41c1a55e9f7927db12 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 09:24:49 +0900 Subject: [PATCH 41/95] =?UTF-8?q?docs:=20=EB=8C=93=EA=B8=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/comments.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index c5c36dc3..3282c5e6 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -12,7 +12,7 @@ operation::comment-controller-test/create-comment[snippets='http-request,curl-re operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] [[댓글-수정]] -=== `POST` 댓글 수정 (미구현) +=== `POST` 댓글 수정 operation::comment-controller-test/update-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] From 60ae5bcb6590277580a45d7841c2bd1456075ceb Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 09:25:08 +0900 Subject: [PATCH 42/95] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CommentServiceTest.java | 58 ++++++++++++++++++- .../presentation/CommentControllerTest.java | 13 ++--- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index b6794971..bef41406 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -4,12 +4,11 @@ import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.comment.presentation.dto.CommentResponse; -import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.comment.presentation.dto.CommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.ForbiddenException; -import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -23,6 +22,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.SliceImpl; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -55,7 +55,7 @@ class CommentServiceTest { void createComment() { // given Long postId = 1L; - CreateCommentRequest request = new CreateCommentRequest("테스트 댓글"); + CommentRequest request = new CommentRequest("테스트 댓글"); UserInfo userInfo = new UserInfo(100L, Role.USER); Comment comment = new Comment(postId, userInfo.userId(), request.content()); @@ -126,6 +126,58 @@ void findComments_userNotFound() { .hasMessage((ErrorCode.USER_NOT_FOUND.getMessage())); } + @Test + @DisplayName("댓글 수정") + void updateComment() { + // given + Long postId = 1L; + UserInfo userInfo = new UserInfo(100L, Role.USER); + Comment comment = new Comment(1L, postId, 100L, "첫 번째 댓글"); + CommentRequest request = new CommentRequest("수정 댓글"); + when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); + + // when + commentService.updateComment(1L, request, userInfo); + + // then + assertAll( + () -> assertThat(comment.getId()).isEqualTo(1L), + () -> assertThat(comment.getContent()).isEqualTo("수정 댓글") + ); + } + + @Test + @DisplayName("댓글 수정 - 존재하지 않은 댓글") + void updateComment_commentNotFound() { + // given + CommentRequest request = new CommentRequest("수정 댓글"); + UserInfo userInfo = new UserInfo(100L, Role.USER); + when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.empty()); + + // when then + assertThatThrownBy(() -> commentService.updateComment(1L, request, userInfo)) + .isInstanceOf(BadRequestException.class) + .hasMessage((ErrorCode.COMMENT_NOT_FOUND.getMessage())); + } + + @Test + @DisplayName("댓글 수정 - 권한 없는 사용자") + void updateComment_forbiddenException() { + // given + Long postId = 1L; + UserInfo userInfo = new UserInfo(100L, Role.USER); + Comment comment = new Comment(1L, postId, 110L, "첫 번째 댓글"); + CommentRequest request = new CommentRequest("수정 댓글"); + when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); + + // when then + assertAll( + () -> assertThatThrownBy(() -> commentService.updateComment(1L, request, userInfo)) + .isInstanceOf(ForbiddenException.class), + () -> assertThat(comment.getContent()).isEqualTo("첫 번째 댓글") + ); + } + @Test @DisplayName("댓글 삭제") void deleteComment() { diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index 1610038e..53d6fc00 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -1,11 +1,9 @@ package com.swyp8team2.comment.presentation; import com.swyp8team2.auth.domain.UserInfo; -import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.presentation.dto.AuthorDto; import com.swyp8team2.comment.presentation.dto.CommentResponse; -import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; -import com.swyp8team2.comment.presentation.dto.UpdateCommentRequest; +import com.swyp8team2.comment.presentation.dto.CommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; @@ -20,7 +18,6 @@ import java.util.Collections; import java.util.List; -import static org.awaitility.Awaitility.given; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -39,9 +36,9 @@ class CommentControllerTest extends RestDocsTest { void createComment() throws Exception { //given Long postId = 1L; - CreateCommentRequest request = new CreateCommentRequest("content"); + CommentRequest request = new CommentRequest("content"); - doNothing().when(commentService).createComment(eq(postId), any(CreateCommentRequest.class), any(UserInfo.class)); + doNothing().when(commentService).createComment(eq(postId), any(CommentRequest.class), any(UserInfo.class)); //when then mockMvc.perform(post("/posts/{postId}/comments", "1") @@ -59,7 +56,7 @@ void createComment() throws Exception { ) )); - verify(commentService, times(1)).createComment(eq(postId), any(CreateCommentRequest.class), any(UserInfo.class)); + verify(commentService, times(1)).createComment(eq(postId), any(CommentRequest.class), any(UserInfo.class)); } @Test @@ -144,7 +141,7 @@ void findComments() throws Exception { @DisplayName("댓글 수정") void updateComment() throws Exception { //given - UpdateCommentRequest request = new UpdateCommentRequest("수정 댓글"); + CommentRequest request = new CommentRequest("수정 댓글"); //when then mockMvc.perform(post("/posts/{postId}/comments/{commentId}", "1", "1") From 7c3503836db583fc350ee3ef62d957aeaa08c352 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 09:53:41 +0900 Subject: [PATCH 43/95] =?UTF-8?q?feat:=20=EC=88=98=EC=A0=95=EB=90=9C=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 4 ++-- .../java/com/swyp8team2/comment/domain/Comment.java | 5 ++--- .../comment/application/CommentServiceTest.java | 12 +++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index f17ebda1..794f94ad 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -61,8 +61,8 @@ public void updateComment(Long commentId, CommentRequest request, UserInfo userI throw new ForbiddenException(); } - comment.updateComment(request.content()); - commentRepository.save(comment); + Comment newComment = comment.updateComment(request.content()); + commentRepository.save(newComment); } @Transactional diff --git a/src/main/java/com/swyp8team2/comment/domain/Comment.java b/src/main/java/com/swyp8team2/comment/domain/Comment.java index c02b1ebe..28a6bc69 100644 --- a/src/main/java/com/swyp8team2/comment/domain/Comment.java +++ b/src/main/java/com/swyp8team2/comment/domain/Comment.java @@ -50,8 +50,7 @@ public Comment(Long postId, Long userNo, String content) { this.content = content; } - public void updateComment(String content) { - validateEmptyString(content); - this.content = content; + public Comment updateComment(String content) { + return new Comment(this.id, this.postId, this.userNo, content); } } diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index bef41406..98ee4657 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -137,12 +137,13 @@ void updateComment() { when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); // when + Comment newComment = comment.updateComment(request.content()); commentService.updateComment(1L, request, userInfo); // then assertAll( - () -> assertThat(comment.getId()).isEqualTo(1L), - () -> assertThat(comment.getContent()).isEqualTo("수정 댓글") + () -> assertThat(comment.getId()).isEqualTo(newComment.getId()), + () -> assertThat(comment.getContent()).isNotEqualTo(newComment.getContent()) ); } @@ -171,11 +172,8 @@ void updateComment_forbiddenException() { when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); // when then - assertAll( - () -> assertThatThrownBy(() -> commentService.updateComment(1L, request, userInfo)) - .isInstanceOf(ForbiddenException.class), - () -> assertThat(comment.getContent()).isEqualTo("첫 번째 댓글") - ); + assertThatThrownBy(() -> commentService.updateComment(1L, request, userInfo)) + .isInstanceOf(ForbiddenException.class); } @Test From 51a7194a05f565af52279ea3f04809101cbbe6b5 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 10:12:54 +0900 Subject: [PATCH 44/95] =?UTF-8?q?chore:=20=EC=9D=B4=EC=A0=84=20=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 4 ++-- .../java/com/swyp8team2/comment/domain/Comment.java | 5 +++-- .../comment/application/CommentServiceTest.java | 12 +++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index 794f94ad..f17ebda1 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -61,8 +61,8 @@ public void updateComment(Long commentId, CommentRequest request, UserInfo userI throw new ForbiddenException(); } - Comment newComment = comment.updateComment(request.content()); - commentRepository.save(newComment); + comment.updateComment(request.content()); + commentRepository.save(comment); } @Transactional diff --git a/src/main/java/com/swyp8team2/comment/domain/Comment.java b/src/main/java/com/swyp8team2/comment/domain/Comment.java index 28a6bc69..c02b1ebe 100644 --- a/src/main/java/com/swyp8team2/comment/domain/Comment.java +++ b/src/main/java/com/swyp8team2/comment/domain/Comment.java @@ -50,7 +50,8 @@ public Comment(Long postId, Long userNo, String content) { this.content = content; } - public Comment updateComment(String content) { - return new Comment(this.id, this.postId, this.userNo, content); + public void updateComment(String content) { + validateEmptyString(content); + this.content = content; } } diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 98ee4657..bef41406 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -137,13 +137,12 @@ void updateComment() { when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); // when - Comment newComment = comment.updateComment(request.content()); commentService.updateComment(1L, request, userInfo); // then assertAll( - () -> assertThat(comment.getId()).isEqualTo(newComment.getId()), - () -> assertThat(comment.getContent()).isNotEqualTo(newComment.getContent()) + () -> assertThat(comment.getId()).isEqualTo(1L), + () -> assertThat(comment.getContent()).isEqualTo("수정 댓글") ); } @@ -172,8 +171,11 @@ void updateComment_forbiddenException() { when(commentRepository.findByIdAndNotDeleted(1L)).thenReturn(Optional.of(comment)); // when then - assertThatThrownBy(() -> commentService.updateComment(1L, request, userInfo)) - .isInstanceOf(ForbiddenException.class); + assertAll( + () -> assertThatThrownBy(() -> commentService.updateComment(1L, request, userInfo)) + .isInstanceOf(ForbiddenException.class), + () -> assertThat(comment.getContent()).isEqualTo("첫 번째 댓글") + ); } @Test From 98682089f2cab6bb87649281a8ae9d631a6e6c72 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 10:26:11 +0900 Subject: [PATCH 45/95] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/comment/application/CommentServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index bef41406..809d321c 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -22,7 +22,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.SliceImpl; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; From 9ca4a8fad27cb7f3eee69491462c1156e2f23fba Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 10:27:01 +0900 Subject: [PATCH 46/95] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=86=8C=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/comment/application/CommentService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index f17ebda1..4a307a9c 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -62,7 +62,6 @@ public void updateComment(Long commentId, CommentRequest request, UserInfo userI } comment.updateComment(request.content()); - commentRepository.save(comment); } @Transactional @@ -75,6 +74,5 @@ public void deleteComment(Long commentId, UserInfo userInfo) { } comment.delete(); - commentRepository.save(comment); } } From bbe7766869a39539165057c7e60611b5c0adf9cf Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 10:46:51 +0900 Subject: [PATCH 47/95] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=EB=B2=94=EC=9C=84=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/post/domain/Post.java | 15 +++++++++++++-- .../java/com/swyp8team2/post/domain/Scope.java | 5 +++++ .../vote/application/VoteServiceTest.java | 7 +++---- 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/domain/Scope.java diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 55bb0fbf..3e4665bc 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -39,18 +39,22 @@ public class Post extends BaseEntity { @Enumerated(EnumType.STRING) private Status status; + @Enumerated(EnumType.STRING) + private Scope scope; + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.ALL) private List images = new ArrayList<>(); private String shareUrl; - public Post(Long id, Long userId, String description, Status status, List images, String shareUrl) { + public Post(Long id, Long userId, String description, Status status, Scope scope, List images, String shareUrl) { validateDescription(description); validatePostImages(images); this.id = id; this.description = description; this.userId = userId; this.status = status; + this.scope = scope; this.images = images; images.forEach(image -> image.setPost(this)); this.shareUrl = shareUrl; @@ -69,7 +73,7 @@ private void validateDescription(String description) { } public static Post create(Long userId, String description, List images) { - return new Post(null, userId, description, Status.PROGRESS, images, null); + return new Post(null, userId, description, Status.PROGRESS, Scope.PRIVATE, images, null); } public PostImage getBestPickedImage() { @@ -120,4 +124,11 @@ public void setShareUrl(String shareUrl) { } this.shareUrl = shareUrl; } + + public void toggleScope(Long userId) { + if (!isAuthor(userId)) { + throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); + } + this.scope = scope.equals(Scope.PRIVATE) ? Scope.PUBLIC : Scope.PRIVATE; + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Scope.java b/src/main/java/com/swyp8team2/post/domain/Scope.java new file mode 100644 index 00000000..31cda011 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/Scope.java @@ -0,0 +1,5 @@ +package com.swyp8team2.post.domain; + +public enum Scope { + PUBLIC, PRIVATE +} diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index fff0993c..36a3afb0 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -4,10 +4,7 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; -import com.swyp8team2.post.domain.Status; +import com.swyp8team2.post.domain.*; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -103,6 +100,7 @@ void vote_alreadyClosed() { user.getId(), "description", Status.CLOSED, + Scope.PRIVATE, List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) @@ -179,6 +177,7 @@ void guestVote_alreadyClosed() { user.getId(), "description", Status.CLOSED, + Scope.PRIVATE, List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) From 6ba520d568612de92c70f2582b316669b7f10efa Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 10:47:09 +0900 Subject: [PATCH 48/95] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=EB=B2=94=EC=9C=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/post/application/PostService.java | 7 +++++++ .../com/swyp8team2/post/presentation/PostController.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 48c866ee..cb36fa84 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -183,4 +183,11 @@ public PostResponse findByShareUrl(Long userId, String shareUrl) { String decrypt = shareUrlCryptoService.decrypt(shareUrl); return findById(userId, Long.valueOf(decrypt)); } + + @Transactional + public void toggleScope(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.toggleScope(userId); + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 0ec7e37b..f3083f97 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -67,11 +67,12 @@ public ResponseEntity> findVoteStatus( return ResponseEntity.ok(postService.findVoteStatus(userInfo.userId(), postId)); } - @PostMapping("/{postId}/status") - public ResponseEntity toggleStatusPost( + @PostMapping("/{postId}/scope") + public ResponseEntity toggleScopePost( @PathVariable("postId") Long postId, @AuthenticationPrincipal UserInfo userInfo ) { + postService.toggleScope(userInfo.userId(), postId); return ResponseEntity.ok().build(); } From fd58279bc942ed0664648888f56bc235a07525ca Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 10:47:21 +0900 Subject: [PATCH 49/95] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=EB=B2=94=EC=9C=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 2 +- .../com/swyp8team2/post/domain/PostTest.java | 43 +++++++++++++++++-- .../post/presentation/PostControllerTest.java | 13 ++++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 064912a8..42f83565 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -35,7 +35,7 @@ operation::post-controller-test/find-my-post[snippets='http-request,curl-request operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] [[게시글-투표-수정]] -=== `POST` 게시글 노출 변경 (미구현) +=== `POST` 게시글 노출 변경 operation::post-controller-test/toggle-status-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index a41a2609..75c0822a 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -82,7 +82,7 @@ void close() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.PROGRESS, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); //when post.close(userId); @@ -100,7 +100,7 @@ void close_alreadyClosed() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.CLOSED, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.CLOSED, Scope.PRIVATE, postImages, "shareUrl"); //when then assertThatThrownBy(() -> post.close(userId)) @@ -117,11 +117,48 @@ void close_notPostAuthor() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.PROGRESS, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); //when then assertThatThrownBy(() -> post.close(2L)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.NOT_POST_AUTHOR.getMessage()); } + + @Test + @DisplayName("게시글 공개 범위 수정") + void toggleScope() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); + + //when then + post.toggleScope(userId); + assertThat(post.getScope()).isEqualTo(Scope.PUBLIC); + + //when then + post.toggleScope(userId); + assertThat(post.getScope()).isEqualTo(Scope.PRIVATE); + } + + @Test + @DisplayName("게시글 공개 범위 수정 - 게시글 작성자가 아닌 경우") + void toggleScope_notPostAuthor() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); + + //when then + assertThatThrownBy(() -> post.toggleScope(2L)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.NOT_POST_AUTHOR.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 8c017294..3725308d 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -1,5 +1,6 @@ package com.swyp8team2.post.presentation; +import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.domain.Status; import com.swyp8team2.post.presentation.dto.*; @@ -17,9 +18,9 @@ import java.util.List; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -354,12 +355,14 @@ void findVotedPost() throws Exception { @Test @WithMockUserInfo - @DisplayName("게시글 노출 변경") + @DisplayName("게시글 공개 범위 변경") void toggleStatusPost() throws Exception { //given + Long postId = 1L; + doNothing().when(postService).toggleScope(any(), eq(postId)); //when then - mockMvc.perform(post("/posts/{postId}/status", 1) + mockMvc.perform(post("/posts/{postId}/scope", 1) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( @@ -368,6 +371,8 @@ void toggleStatusPost() throws Exception { parameterWithName("postId").description("게시글 Id") ) )); + + verify(postService, times(1)).toggleScope(any(), eq(postId)); } @Test From 28ef9ad2dd08e702351bfe41605481d4f3927da2 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 17:03:57 +0900 Subject: [PATCH 50/95] =?UTF-8?q?test:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=95=B1=20=ED=82=A4=20=EC=88=98=EC=A0=95=20=ED=9B=84=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 0072a031..ab7c9da2 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 0072a031b6cc2fd906fc7dccc35577f10ff0553c +Subproject commit ab7c9da2d8a271f49d62d5a49422ab538f7fa807 From 954d2dce0d6373e96b64ac3ede2258d6a0f308f3 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 17:12:26 +0900 Subject: [PATCH 51/95] =?UTF-8?q?chore:=20=EC=95=B1=20=ED=82=A4=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index ab7c9da2..ae2a39db 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit ab7c9da2d8a271f49d62d5a49422ab538f7fa807 +Subproject commit ae2a39db0c8a117a0f28a1da506c65ed5c5ad4fb From b16a67df1ff9e39fce94a41559918245002a9701 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 17:21:51 +0900 Subject: [PATCH 52/95] =?UTF-8?q?chore:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EC=95=B1=20=ED=82=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index ae2a39db..e9b5529b 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit ae2a39db0c8a117a0f28a1da506c65ed5c5ad4fb +Subproject commit e9b5529bb0d03ac8377754e99563c6693df7b93b From 3736e33015db4cb96707e05736fd9f3b1ec2610a Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 8 Mar 2025 17:35:32 +0900 Subject: [PATCH 53/95] =?UTF-8?q?chore:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=20=EC=95=B1=20=ED=82=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index e9b5529b..6def491a 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit e9b5529bb0d03ac8377754e99563c6693df7b93b +Subproject commit 6def491ab548ae5aaab21ddf185147c469b33fa7 From 60d7fc000a183f02bfc40b22a2c6880ffb5801fb Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 14:43:37 +0900 Subject: [PATCH 54/95] =?UTF-8?q?refactor:=20querydsl=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 24 ------ .../post/domain/PostRepository.java | 4 + .../infrastructure/PostQueryRepository.java | 43 ---------- .../infrastructure/PostRepositoryImpl.java | 4 + .../PostQueryRepositoryTest.java | 80 ------------------- 5 files changed, 8 insertions(+), 147 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/domain/PostRepository.java delete mode 100644 src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java create mode 100644 src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java delete mode 100644 src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java diff --git a/build.gradle b/build.gradle index 6341ffa2..0f28ba07 100644 --- a/build.gradle +++ b/build.gradle @@ -50,12 +50,6 @@ dependencies { // base64 implementation 'io.seruco.encoding:base62:0.1.3' - // querydsl - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' - annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" - annotationProcessor "jakarta.annotation:jakarta.annotation-api" - annotationProcessor "jakarta.persistence:jakarta.persistence-api" - compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' @@ -96,24 +90,6 @@ bootJar { } } -// querydsl -def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile -sourceSets { - main.java.srcDirs += [ querydslDir ] -} - -tasks.withType(JavaCompile).configureEach { - options.getGeneratedSourceOutputDirectory().set(file(querydslDir)) -} - -clean.doLast { - file(querydslDir).deleteDir() -} - -clean { - delete file('src/main/generated') -} - tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java new file mode 100644 index 00000000..f51ea745 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -0,0 +1,4 @@ +package com.swyp8team2.post.domain; + +public interface PostRepository { +} diff --git a/src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java b/src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java deleted file mode 100644 index 5d5148ec..00000000 --- a/src/main/java/com/swyp8team2/post/infrastructure/PostQueryRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.swyp8team2.post.infrastructure; - -import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.NumberPath; -import com.querydsl.jpa.impl.JPAQueryFactory; -import com.swyp8team2.common.util.QueryDslUtil; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.QPost; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; -import org.springframework.stereotype.Repository; - -import java.util.List; - -import static com.swyp8team2.common.util.QueryDslUtil.*; -import static com.swyp8team2.common.util.QueryDslUtil.ltCursor; - -@Repository -@RequiredArgsConstructor -public class PostQueryRepository { - - private final JPAQueryFactory factory; - - /** - * SELECT p - * FROM Post p - * WHERE p.userId = :userId - * AND (:cursor IS NULL OR p.id < :cursor) - * ORDER BY p.id DESC - **/ - Slice findByUserId(Long userId, Long cursor, Pageable pageable) { - List postList = factory.selectFrom(QPost.post) - .where(QPost.post.userId.eq(userId) - .and(ltCursor(cursor, QPost.post.id))) - .orderBy(QPost.post.id.desc()) - .limit(pageable.getPageSize() + 1) - .fetch(); - boolean hasNext = removeLastIfHasNext(pageable.getPageSize(), postList); - return new SliceImpl<>(postList, pageable, hasNext); - } -} diff --git a/src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java b/src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java new file mode 100644 index 00000000..1b566e98 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java @@ -0,0 +1,4 @@ +package com.swyp8team2.post.infrastructure; + +public class PostRepositoryImpl { +} diff --git a/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java b/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java deleted file mode 100644 index e60f295e..00000000 --- a/src/test/java/com/swyp8team2/post/infrastructure/PostQueryRepositoryTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.swyp8team2.post.infrastructure; - -import com.swyp8team2.image.domain.ImageFile; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.support.RepositoryTest; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; - -import java.util.ArrayList; -import java.util.List; - -import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; -import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -class PostQueryRepositoryTest extends RepositoryTest { - - @Autowired - PostQueryRepository postQueryRepository; - - @Autowired - PostJpaRepository postRepository; - - @Test - @DisplayName("유저가 작성한 게시글 조회 - 게시글이 15개일 경우 15번쨰부터 10개 조회해야 함") - void select_post_findByUserId1() throws Exception { - //given - long userId = 1L; - List posts = createPosts(userId); - int size = 10; - - //when - Slice res = postQueryRepository.findByUserId(userId, null, PageRequest.ofSize(size)); - - //then - assertAll( - () -> assertThat(res.getContent().size()).isEqualTo(size), - () -> assertThat(res.getContent().get(0).getId()).isEqualTo(posts.get(posts.size() - 1).getId()), - () -> assertThat(res.getContent().get(1).getId()).isEqualTo(posts.get(posts.size() - 2).getId()), - () -> assertThat(res.getContent().get(2).getId()).isEqualTo(posts.get(posts.size() - 3).getId()), - () -> assertThat(res.hasNext()).isTrue() - ); - } - - @Test - @DisplayName("유저가 작성한 게시글 조회 - 15개 중에 커서가 5번째 게시글의 id면 4번째부터 0번째까지 조회해야 함") - void select_post_findByUserId2() throws Exception { - //given - long userId = 1L; - List posts = createPosts(userId); - int size = 10; - int cursorIndex = 5; - - //when - Slice res = postQueryRepository.findByUserId(userId, posts.get(cursorIndex).getId(), PageRequest.ofSize(size)); - - //then - assertAll( - () -> assertThat(res.getContent().size()).isEqualTo(5), - () -> assertThat(res.getContent().get(0).getId()).isEqualTo(posts.get(cursorIndex - 1).getId()), - () -> assertThat(res.getContent().get(1).getId()).isEqualTo(posts.get(cursorIndex - 2).getId()), - () -> assertThat(res.getContent().get(2).getId()).isEqualTo(posts.get(cursorIndex - 3).getId()), - () -> assertThat(res.hasNext()).isFalse() - ); - } - - private List createPosts(long userId) { - List posts = new ArrayList<>(); - for (int i = 0; i < 30; i += 2) { - ImageFile imageFile1 = createImageFile(i); - ImageFile imageFile2 = createImageFile(i + 1); - posts.add(postRepository.save(createPost(userId, imageFile1, imageFile2, i))); - } - return posts; - } -} From 4a6af22f324fc80f419d969003189cc9c11b034c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 14:47:03 +0900 Subject: [PATCH 55/95] =?UTF-8?q?refactor:=20infra=20=EA=B3=84=EC=B8=B5=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- .../common/dev/DataInitializer.java | 6 +-- .../image/domain/ImageFileRepository.java | 3 ++ .../post/application/PostService.java | 6 +-- .../post/domain/PostRepository.java | 42 ++++++++++++++++- .../infrastructure/PostJpaRepository.java | 45 ------------------- .../infrastructure/PostRepositoryImpl.java | 4 -- .../vote/application/VoteService.java | 4 +- .../post/application/PostServiceTest.java | 7 ++- ...itoryTest.java => PostRepositoryTest.java} | 5 +-- .../swyp8team2/support/RepositoryTest.java | 2 - .../vote/application/VoteServiceTest.java | 4 +- 12 files changed, 62 insertions(+), 68 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/post/infrastructure/PostJpaRepository.java delete mode 100644 src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java rename src/test/java/com/swyp8team2/post/domain/{PostJpaRepositoryTest.java => PostRepositoryTest.java} (96%) diff --git a/server-config b/server-config index 578cee26..6def491a 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 578cee264b3154e9d6bd2c04f9c5b3975e66136d +Subproject commit 6def491ab548ae5aaab21ddf185147c469b33fa7 diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 07353fae..f63f4ff9 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -13,7 +13,7 @@ import com.swyp8team2.image.presentation.dto.ImageFileDto; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.infrastructure.PostJpaRepository; +import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.user.domain.NicknameAdjective; import com.swyp8team2.user.domain.NicknameAdjectiveRepository; import com.swyp8team2.user.domain.User; @@ -34,7 +34,7 @@ public class DataInitializer { private final NicknameAdjectiveRepository nicknameAdjectiveRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; - private final PostJpaRepository postRepository; + private final PostRepository postRepository; private final CryptoService shaereUrlCryptoService; private final JwtService jwtService; private final VoteService voteService; @@ -44,7 +44,7 @@ public DataInitializer( NicknameAdjectiveRepository nicknameAdjectiveRepository, UserRepository userRepository, ImageFileRepository imageFileRepository, - PostJpaRepository postRepository, + PostRepository postRepository, @ShareUrlCryptoService CryptoService shaereUrlCryptoService, JwtService jwtService, VoteService voteService, diff --git a/src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java b/src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java index c0ae2259..f277f9d5 100644 --- a/src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java +++ b/src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java @@ -3,6 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface ImageFileRepository extends JpaRepository { + List findByIdIn(List bestPickedImageIds); } diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index d9ced967..1d27bb07 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -10,7 +10,7 @@ import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.infrastructure.PostJpaRepository; +import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostResponse; @@ -33,7 +33,7 @@ @Transactional(readOnly = true) public class PostService { - private final PostJpaRepository postRepository; + private final PostRepository postRepository; private final UserRepository userRepository; private final RatioCalculator ratioCalculator; private final ImageFileRepository imageFileRepository; @@ -41,7 +41,7 @@ public class PostService { private final CryptoService shareUrlCryptoService; public PostService( - PostJpaRepository postRepository, + PostRepository postRepository, UserRepository userRepository, RatioCalculator ratioCalculator, ImageFileRepository imageFileRepository, diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java index f51ea745..66b6b529 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -1,4 +1,44 @@ package com.swyp8team2.post.domain; -public interface PostRepository { +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PostRepository extends JpaRepository { + + @Query(""" + SELECT p + FROM Post p + WHERE p.userId = :userId + AND (:postId IS NULL OR p.id < :postId) + ORDER BY p.id DESC + """ + ) + Slice findByUserId(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); + + @Query(""" + SELECT p + FROM Post p + WHERE p.id IN :postIds + AND (:postId IS NULL OR p.id < :postId) + ORDER BY p.id DESC + """ + ) + Slice findByIdIn(@Param("postIds") List postIds, @Param("postId") Long postId, Pageable pageable); + + @Query(""" + SELECT p + FROM Post p + JOIN FETCH p.images + WHERE p.id = :postId + """ + ) + Optional findByIdFetchPostImage(@Param("postId") Long postId); } diff --git a/src/main/java/com/swyp8team2/post/infrastructure/PostJpaRepository.java b/src/main/java/com/swyp8team2/post/infrastructure/PostJpaRepository.java deleted file mode 100644 index 8c955825..00000000 --- a/src/main/java/com/swyp8team2/post/infrastructure/PostJpaRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.swyp8team2.post.infrastructure; - -import com.swyp8team2.post.domain.Post; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface PostJpaRepository extends JpaRepository { - - @Query(""" - SELECT p - FROM Post p - WHERE p.userId = :userId - AND (:postId IS NULL OR p.id < :postId) - ORDER BY p.id DESC - """ - ) - Slice findByUserId(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); - - @Query(""" - SELECT p - FROM Post p - WHERE p.id IN :postIds - AND (:postId IS NULL OR p.id < :postId) - ORDER BY p.id DESC - """ - ) - Slice findByIdIn(@Param("postIds") List postIds, @Param("postId") Long postId, Pageable pageable); - - @Query(""" - SELECT p - FROM Post p - JOIN FETCH p.images - WHERE p.id = :postId - """ - ) - Optional findByIdFetchPostImage(@Param("postId") Long postId); -} diff --git a/src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java b/src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java deleted file mode 100644 index 1b566e98..00000000 --- a/src/main/java/com/swyp8team2/post/infrastructure/PostRepositoryImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.swyp8team2.post.infrastructure; - -public class PostRepositoryImpl { -} diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 40783775..e7f093d9 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -3,7 +3,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.infrastructure.PostJpaRepository; +import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -19,7 +19,7 @@ public class VoteService { private final VoteRepository voteRepository; private final UserRepository userRepository; - private final PostJpaRepository postRepository; + private final PostRepository postRepository; @Transactional public Long vote(Long voterId, Long postId, Long imageId) { diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index e443f2c7..19323d16 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -8,7 +8,7 @@ import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.infrastructure.PostJpaRepository; +import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.domain.Status; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; @@ -21,6 +21,7 @@ import com.swyp8team2.vote.application.VoteService; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -44,7 +45,7 @@ class PostServiceTest extends IntegrationTest { PostService postService; @Autowired - PostJpaRepository postRepository; + PostRepository postRepository; @Autowired UserRepository userRepository; @@ -155,6 +156,7 @@ void findById() throws Exception { } @Test + @Disabled @DisplayName("유저가 작성한 게시글 조회 - 커서 null인 경우") void findUserPosts() throws Exception { //given @@ -174,6 +176,7 @@ void findUserPosts() throws Exception { } @Test + @Disabled @DisplayName("유저가 작성한 게시글 조회 - 커서 있는 경우") void findUserPosts2() throws Exception { //given diff --git a/src/test/java/com/swyp8team2/post/domain/PostJpaRepositoryTest.java b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java similarity index 96% rename from src/test/java/com/swyp8team2/post/domain/PostJpaRepositoryTest.java rename to src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java index cc29da00..e5b5157b 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostJpaRepositoryTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java @@ -1,7 +1,6 @@ package com.swyp8team2.post.domain; import com.swyp8team2.image.domain.ImageFile; -import com.swyp8team2.post.infrastructure.PostJpaRepository; import com.swyp8team2.support.RepositoryTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,10 +16,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -class PostJpaRepositoryTest extends RepositoryTest { +class PostRepositoryTest extends RepositoryTest { @Autowired - PostJpaRepository postRepository; + PostRepository postRepository; @Test @DisplayName("유저가 작성한 게시글 조회 - 게시글이 15개일 경우 15번쨰부터 10개 조회해야 함") diff --git a/src/test/java/com/swyp8team2/support/RepositoryTest.java b/src/test/java/com/swyp8team2/support/RepositoryTest.java index eaad71c8..9802414e 100644 --- a/src/test/java/com/swyp8team2/support/RepositoryTest.java +++ b/src/test/java/com/swyp8team2/support/RepositoryTest.java @@ -2,14 +2,12 @@ import com.swyp8team2.common.config.JpaConfig; import com.swyp8team2.common.config.QueryDslConfig; -import com.swyp8team2.post.infrastructure.PostQueryRepository; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; @Import({ JpaConfig.class, QueryDslConfig.class, - PostQueryRepository.class }) @DataJpaTest public abstract class RepositoryTest { diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index b1f3de96..fff0993c 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -6,7 +6,7 @@ import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.infrastructure.PostJpaRepository; +import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.domain.Status; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; @@ -38,7 +38,7 @@ class VoteServiceTest extends IntegrationTest { VoteRepository voteRepository; @Autowired - PostJpaRepository postRepository; + PostRepository postRepository; @Autowired ImageFileRepository imageFileRepository; From 09cd049e3789dabddbf258416bb845ecd7063fed Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 14:47:27 +0900 Subject: [PATCH 56/95] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=EA=B0=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=ED=95=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostService.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 1d27bb07..637d13cf 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -23,6 +23,7 @@ import com.swyp8team2.vote.domain.VoteRepository; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -114,7 +115,29 @@ private Boolean getVoted(PostImage image, Long userId, Long postId) { public CursorBasePaginatedResponse findUserPosts(Long userId, Long cursor, int size) { Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); - return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse)); + List bestPickedImageIds = postSlice.getContent().stream() + .map(Post::getBestPickedImage) + .map(PostImage::getImageFileId) + .toList(); + List imageIds = imageFileRepository.findByIdIn(bestPickedImageIds); + + List responseContent = postSlice.getContent().stream() + .map(post -> getSimplePostResponse(post, imageIds)) + .toList(); + + return CursorBasePaginatedResponse.of(new SliceImpl<>( + responseContent, + postSlice.getPageable(), + postSlice.hasNext()) + ); + } + + private SimplePostResponse getSimplePostResponse(Post post, List imageIds) { + ImageFile bestPickedImage = imageIds.stream() + .filter(imageFile -> imageFile.getId().equals(post.getBestPickedImage().getImageFileId())) + .findFirst() + .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); + return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); } private SimplePostResponse createSimplePostResponse(Post post) { From 8badd9c24f7ba341b2524fa17902185caea635c9 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 15:29:46 +0900 Subject: [PATCH 57/95] =?UTF-8?q?refactor:=20=ED=88=AC=ED=91=9C=ED=95=9C?= =?UTF-8?q?=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/QueryDslConfig.java | 15 ------ .../swyp8team2/common/util/QueryDslUtil.java | 21 --------- .../post/application/PostService.java | 47 +++++++++---------- .../swyp8team2/support/RepositoryTest.java | 6 +-- 4 files changed, 24 insertions(+), 65 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/common/config/QueryDslConfig.java delete mode 100644 src/main/java/com/swyp8team2/common/util/QueryDslUtil.java diff --git a/src/main/java/com/swyp8team2/common/config/QueryDslConfig.java b/src/main/java/com/swyp8team2/common/config/QueryDslConfig.java deleted file mode 100644 index 7d25990b..00000000 --- a/src/main/java/com/swyp8team2/common/config/QueryDslConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.swyp8team2.common.config; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import jakarta.persistence.EntityManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class QueryDslConfig { - - @Bean - public JPAQueryFactory jpaQueryFactory(EntityManager em) { - return new JPAQueryFactory(em); - } -} diff --git a/src/main/java/com/swyp8team2/common/util/QueryDslUtil.java b/src/main/java/com/swyp8team2/common/util/QueryDslUtil.java deleted file mode 100644 index c817219a..00000000 --- a/src/main/java/com/swyp8team2/common/util/QueryDslUtil.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.swyp8team2.common.util; - -import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.NumberPath; - -import java.util.List; - -public abstract class QueryDslUtil { - - public static Predicate ltCursor(Long cursor, NumberPath id) { - return cursor == null ? null : id.lt(cursor); - } - - public static boolean removeLastIfHasNext(int size, List content) { - if (content.size() > size) { - content.remove(size); - return true; - } - return false; - } -} diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 637d13cf..fe90f7d2 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -115,21 +115,7 @@ private Boolean getVoted(PostImage image, Long userId, Long postId) { public CursorBasePaginatedResponse findUserPosts(Long userId, Long cursor, int size) { Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); - List bestPickedImageIds = postSlice.getContent().stream() - .map(Post::getBestPickedImage) - .map(PostImage::getImageFileId) - .toList(); - List imageIds = imageFileRepository.findByIdIn(bestPickedImageIds); - - List responseContent = postSlice.getContent().stream() - .map(post -> getSimplePostResponse(post, imageIds)) - .toList(); - - return CursorBasePaginatedResponse.of(new SliceImpl<>( - responseContent, - postSlice.getPageable(), - postSlice.hasNext()) - ); + return getCursorPaginatedResponse(postSlice); } private SimplePostResponse getSimplePostResponse(Post post, List imageIds) { @@ -140,20 +126,33 @@ private SimplePostResponse getSimplePostResponse(Post post, List imag return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); } - private SimplePostResponse createSimplePostResponse(Post post) { - ImageFile bestPickedImage = imageFileRepository.findById(post.getBestPickedImage().getImageFileId()) - .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); - return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); - } - public CursorBasePaginatedResponse findVotedPosts(Long userId, Long cursor, int size) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - List postIds = voteRepository.findByUserId(user.getId()) + List votedPostIds = voteRepository.findByUserId(user.getId()) .map(Vote::getPostId) .toList(); - Slice postSlice = postRepository.findByIdIn(postIds, cursor, PageRequest.ofSize(size)); - return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse)); + Slice votedPostSlice = postRepository.findByIdIn(votedPostIds, cursor, PageRequest.ofSize(size)); + + return getCursorPaginatedResponse(votedPostSlice); + } + + private CursorBasePaginatedResponse getCursorPaginatedResponse(Slice postSlice) { + List bestPickedImageIds = postSlice.getContent().stream() + .map(Post::getBestPickedImage) + .map(PostImage::getImageFileId) + .toList(); + List imageIds = imageFileRepository.findByIdIn(bestPickedImageIds); + + List responseContent = postSlice.getContent().stream() + .map(post -> getSimplePostResponse(post, imageIds)) + .toList(); + + return CursorBasePaginatedResponse.of(new SliceImpl<>( + responseContent, + postSlice.getPageable(), + postSlice.hasNext() + )); } public List findPostStatus(Long postId) { diff --git a/src/test/java/com/swyp8team2/support/RepositoryTest.java b/src/test/java/com/swyp8team2/support/RepositoryTest.java index 9802414e..08df0a0a 100644 --- a/src/test/java/com/swyp8team2/support/RepositoryTest.java +++ b/src/test/java/com/swyp8team2/support/RepositoryTest.java @@ -1,14 +1,10 @@ package com.swyp8team2.support; import com.swyp8team2.common.config.JpaConfig; -import com.swyp8team2.common.config.QueryDslConfig; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -@Import({ - JpaConfig.class, - QueryDslConfig.class, -}) +@Import(JpaConfig.class) @DataJpaTest public abstract class RepositoryTest { } From 87e0e5af48443d043f1c251f7b8951766a278d05 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 15:42:11 +0900 Subject: [PATCH 58/95] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EB=B0=A9=EC=8B=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/dev/DataInitializer.java | 6 +-- .../post/application/PostService.java | 2 +- .../java/com/swyp8team2/post/domain/Post.java | 18 +++++++-- .../com/swyp8team2/post/domain/VoteType.java | 5 +++ .../presentation/dto/CreatePostRequest.java | 6 ++- .../post/application/PostServiceTest.java | 38 +++++++++++++------ .../com/swyp8team2/post/domain/PostTest.java | 16 ++++---- .../post/presentation/PostControllerTest.java | 9 ++++- .../support/fixture/FixtureGenerator.java | 5 ++- .../vote/application/VoteServiceTest.java | 26 +++++++------ 10 files changed, 86 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/domain/VoteType.java diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 39d8077f..903803ed 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -11,17 +11,15 @@ import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; -import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.user.domain.NicknameAdjective; import com.swyp8team2.user.domain.NicknameAdjectiveRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.application.VoteService; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -84,7 +82,7 @@ public void init() { for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())))); + Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), VoteType.SINGLE)); post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); posts.add(post); } diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index cb36fa84..27d5fbec 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -59,7 +59,7 @@ public PostService( @Transactional public CreatePostResponse create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); - Post post = Post.create(userId, request.description(), postImages); + Post post = Post.create(userId, request.description(), postImages, request.voteType()); Post save = postRepository.save(post); save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); return new CreatePostResponse(save.getId(), save.getShareUrl()); diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 3e4665bc..4c5e37b2 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -47,7 +47,18 @@ public class Post extends BaseEntity { private String shareUrl; - public Post(Long id, Long userId, String description, Status status, Scope scope, List images, String shareUrl) { + private VoteType voteType; + + public Post( + Long id, + Long userId, + String description, + Status status, + Scope scope, + List images, + String shareUrl, + VoteType voteType + ) { validateDescription(description); validatePostImages(images); this.id = id; @@ -58,6 +69,7 @@ public Post(Long id, Long userId, String description, Status status, Scope scope this.images = images; images.forEach(image -> image.setPost(this)); this.shareUrl = shareUrl; + this.voteType = voteType; } private void validatePostImages(List images) { @@ -72,8 +84,8 @@ private void validateDescription(String description) { } } - public static Post create(Long userId, String description, List images) { - return new Post(null, userId, description, Status.PROGRESS, Scope.PRIVATE, images, null); + public static Post create(Long userId, String description, List images, VoteType voteType) { + return new Post(null, userId, description, Status.PROGRESS, Scope.PRIVATE, images, null, voteType); } public PostImage getBestPickedImage() { diff --git a/src/main/java/com/swyp8team2/post/domain/VoteType.java b/src/main/java/com/swyp8team2/post/domain/VoteType.java new file mode 100644 index 00000000..43ae8f03 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/VoteType.java @@ -0,0 +1,5 @@ +package com.swyp8team2.post.domain; + +public enum VoteType { + SINGLE, MULTIPLE +} diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java index 1efb3b7a..0f25813c 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java @@ -1,5 +1,6 @@ package com.swyp8team2.post.presentation.dto; +import com.swyp8team2.post.domain.VoteType; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -10,6 +11,9 @@ public record CreatePostRequest( String description, @Valid @NotNull - List images + List images, + + @NotNull + VoteType voteType ) { } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 6ea74765..88db57a2 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -10,6 +10,7 @@ import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.domain.Status; +import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostResponse; @@ -67,10 +68,14 @@ class PostServiceTest extends IntegrationTest { void create() throws Exception { //given long userId = 1L; - CreatePostRequest request = new CreatePostRequest("description", List.of( - new PostImageRequestDto(1L), - new PostImageRequestDto(2L) - )); + CreatePostRequest request = new CreatePostRequest( + "description", + List.of( + new PostImageRequestDto(1L), + new PostImageRequestDto(2L) + ), + VoteType.SINGLE + ); String shareUrl = "shareUrl"; given(shareUrlCryptoService.encrypt(any())) .willReturn(shareUrl); @@ -85,6 +90,8 @@ void create() throws Exception { () -> assertThat(post.getDescription()).isEqualTo("description"), () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), + () -> assertThat(post.getStatus()).isEqualTo(Status.PROGRESS), + () -> assertThat(post.getVoteType()).isEqualTo(VoteType.SINGLE), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), @@ -100,10 +107,13 @@ void create() throws Exception { void create_invalidPostImageCount() throws Exception { //given long userId = 1L; - CreatePostRequest request = new CreatePostRequest("description", List.of( - new PostImageRequestDto(1L) - )); - + CreatePostRequest request = new CreatePostRequest( + "description", + List.of( + new PostImageRequestDto(1L) + ), + VoteType.SINGLE + ); //when then assertThatThrownBy(() -> postService.create(userId, request)) .isInstanceOf(BadRequestException.class) @@ -115,10 +125,14 @@ void create_invalidPostImageCount() throws Exception { void create_descriptionCountExceeded() throws Exception { //given long userId = 1L; - CreatePostRequest request = new CreatePostRequest("a".repeat(101), List.of( - new PostImageRequestDto(1L), - new PostImageRequestDto(2L) - )); + CreatePostRequest request = new CreatePostRequest( + "a".repeat(101), + List.of( + new PostImageRequestDto(1L), + new PostImageRequestDto(2L) + ), + VoteType.SINGLE + ); //when then assertThatThrownBy(() -> postService.create(userId, request)) diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index 75c0822a..9539c1eb 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -25,7 +25,7 @@ void create() throws Exception { ); //when - Post post = Post.create(userId, description, postImages); + Post post = Post.create(userId, description, postImages, VoteType.SINGLE); //then List images = post.getImages(); @@ -52,7 +52,7 @@ void create_invalidPostImageCount() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, "description", postImages)) + assertThatThrownBy(() -> Post.create(1L, "description", postImages, VoteType.SINGLE)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); } @@ -68,7 +68,7 @@ void create_descriptionCountExceeded() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, description, postImages)) + assertThatThrownBy(() -> Post.create(1L, description, postImages, VoteType.SINGLE)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } @@ -82,7 +82,7 @@ void close() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl", VoteType.SINGLE); //when post.close(userId); @@ -100,7 +100,7 @@ void close_alreadyClosed() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.CLOSED, Scope.PRIVATE, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.CLOSED, Scope.PRIVATE, postImages, "shareUrl", VoteType.SINGLE); //when then assertThatThrownBy(() -> post.close(userId)) @@ -117,7 +117,7 @@ void close_notPostAuthor() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl", VoteType.SINGLE); //when then assertThatThrownBy(() -> post.close(2L)) @@ -134,7 +134,7 @@ void toggleScope() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl", VoteType.SINGLE); //when then post.toggleScope(userId); @@ -154,7 +154,7 @@ void toggleScope_notPostAuthor() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl", VoteType.SINGLE); //when then assertThatThrownBy(() -> post.toggleScope(2L)) diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 3725308d..8bfa658c 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -3,6 +3,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.domain.Status; +import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.post.presentation.dto.*; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; @@ -43,7 +44,8 @@ void createPost() throws Exception { //given CreatePostRequest request = new CreatePostRequest( "제목", - List.of(new PostImageRequestDto(1L), new PostImageRequestDto(2L)) + List.of(new PostImageRequestDto(1L), new PostImageRequestDto(2L)), + VoteType.SINGLE ); CreatePostResponse response = new CreatePostResponse(1L, "shareUrl"); given(postService.create(any(), any())) @@ -69,7 +71,10 @@ void createPost() throws Exception { .attributes(constraints("최소 2개")), fieldWithPath("images[].imageFileId") .type(JsonFieldType.NUMBER) - .description("투표 후보 이미지 ID") + .description("투표 후보 이미지 ID"), + fieldWithPath("voteType") + .type(JsonFieldType.STRING) + .description("투표 방식 (SINGLE, MULTIPLE)") ), responseFields( fieldWithPath("postId") diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index a11bf052..2667a080 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -4,6 +4,7 @@ import com.swyp8team2.image.presentation.dto.ImageFileDto; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.user.domain.User; import java.util.List; @@ -17,8 +18,8 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) - ) - ); + ), + VoteType.SINGLE); } public static User createUser(int key) { diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index 36a3afb0..486bdec0 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -96,17 +96,18 @@ void vote_alreadyClosed() { ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(new Post( - null, - user.getId(), - "description", - Status.CLOSED, - Scope.PRIVATE, - List.of( - PostImage.create("뽀또A", imageFile1.getId()), - PostImage.create("뽀또B", imageFile2.getId()) - ), - "shareUrl" - )); + null, + user.getId(), + "description", + Status.CLOSED, + Scope.PRIVATE, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl", + VoteType.SINGLE + )); // when assertThatThrownBy(() -> voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId())) @@ -182,7 +183,8 @@ void guestVote_alreadyClosed() { PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) ), - "shareUrl" + "shareUrl", + VoteType.SINGLE )); // when From 9a394c34244834658fdd6a304ee8e62cd3fa5c32 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 16:13:31 +0900 Subject: [PATCH 59/95] =?UTF-8?q?refactor:=20=EC=95=88=20=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/presentation/VoteController.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index 6f97d68f..3b50f317 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -43,22 +43,4 @@ public ResponseEntity guestVote( voteService.guestVote(userInfo.userId(), postId, request.imageId()); return ResponseEntity.ok().build(); } - - @PatchMapping("") - public ResponseEntity changeVote( - @PathVariable("postId") Long postId, - @Valid @RequestBody ChangeVoteRequest request, - @AuthenticationPrincipal UserInfo userInfo - ) { - return ResponseEntity.ok().build(); - } - - @PatchMapping("/guest") - public ResponseEntity changeGuestVote( - @PathVariable("postId") Long postId, - @RequestHeader(CustomHeader.GUEST_TOKEN) String guestId, - @Valid @RequestBody ChangeVoteRequest request - ) { - return ResponseEntity.ok().build(); - } } From 39748c4534bf42472e092c5bb80a0fb44131a56a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 17:12:04 +0900 Subject: [PATCH 60/95] =?UTF-8?q?feat:=20=EB=8B=A8=EC=9D=BC/=EB=B3=B5?= =?UTF-8?q?=EC=88=98=20=ED=88=AC=ED=91=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/application/VoteService.java | 39 ++++---- .../vote/domain/VoteRepository.java | 2 + .../vote/presentation/VoteController.java | 2 +- .../support/fixture/FixtureGenerator.java | 15 ++- .../vote/application/VoteServiceTest.java | 92 +++++-------------- .../vote/presentation/VoteControllerTest.java | 2 +- 6 files changed, 62 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index e7f093d9..7e22012b 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -4,6 +4,7 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -12,6 +13,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -23,36 +26,36 @@ public class VoteService { @Transactional public Long vote(Long voterId, Long postId, Long imageId) { + Optional existsVote = voteRepository.findByUserIdAndPostImageId(voterId, imageId); + if (existsVote.isPresent()) { + return existsVote.get().getId(); + } + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.validateProgress(); + User voter = userRepository.findById(voterId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - deleteVoteIfExisting(postId, voter.getId()); - Vote vote = createVote(postId, imageId, voter.getId()); + + VoteType voteType = post.getVoteType(); + if (VoteType.SINGLE.equals(voteType)) { + deleteVoteIfExisting(post, voter.getId()); + } + Vote vote = createVote(post, imageId, voter.getId()); return vote.getId(); } - private void deleteVoteIfExisting(Long postId, Long userId) { - voteRepository.findByUserIdAndPostId(userId, postId) + private void deleteVoteIfExisting(Post post, Long userId) { + voteRepository.findByUserIdAndPostId(userId, post.getId()) .ifPresent(vote -> { voteRepository.delete(vote); - postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) - .cancelVote(vote.getPostImageId()); + post.cancelVote(vote.getPostImageId()); }); } - private Vote createVote(Long postId, Long imageId, Long userId) { - Post post = postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - post.validateProgress(); + private Vote createVote(Post post, Long imageId, Long userId) { Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userId)); post.vote(imageId); return vote; } - - @Transactional - public Long guestVote(Long userId, Long postId, Long imageId) { - deleteVoteIfExisting(postId, userId); - Vote vote = createVote(postId, imageId, userId); - return vote.getId(); - } } diff --git a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java index 05c2ccf5..2d1ad8a4 100644 --- a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java +++ b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java @@ -11,4 +11,6 @@ public interface VoteRepository extends JpaRepository { Optional findByUserIdAndPostId(Long userId, Long postId); Slice findByUserId(Long userId); + + Optional findByUserIdAndPostImageId(Long voterId, Long imageId); } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index 3b50f317..e05a050f 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -40,7 +40,7 @@ public ResponseEntity guestVote( @Valid @RequestBody VoteRequest request, @AuthenticationPrincipal UserInfo userInfo ) { - voteService.guestVote(userInfo.userId(), postId, request.imageId()); + voteService.vote(userInfo.userId(), postId, request.imageId()); return ResponseEntity.ok().build(); } } diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index 2667a080..4ee291bd 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -19,7 +19,20 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) ), - VoteType.SINGLE); + VoteType.SINGLE + ); + } + + public static Post createMultiplePost(Long userId, ImageFile imageFile1, ImageFile imageFile2, int key) { + return Post.create( + userId, + "description" + key, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + VoteType.MULTIPLE + ); } public static User createUser(int key) { diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index 486bdec0..c003676b 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -17,6 +17,7 @@ import java.util.List; import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; +import static com.swyp8team2.support.fixture.FixtureGenerator.createMultiplePost; import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; import static org.assertj.core.api.Assertions.assertThat; @@ -41,8 +42,8 @@ class VoteServiceTest extends IntegrationTest { ImageFileRepository imageFileRepository; @Test - @DisplayName("투표하기") - void vote() { + @DisplayName("단일 투표하기") + void singleVote() { // given User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); @@ -64,10 +65,10 @@ void vote() { } @Test - @DisplayName("투표하기 - 다른 이미지로 투표 변경한 경우") - void vote_change() { + @DisplayName("단일 투표하기 - 다른 이미지로 투표 변경한 경우") + void singleVote_change() { // given - User user = userRepository.save(createUser(1)); + User user = userRepository.save(createUser(2)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); @@ -89,88 +90,41 @@ void vote_change() { } @Test - @DisplayName("투표하기 - 투표 마감된 경우") - void vote_alreadyClosed() { + @DisplayName("복수 투표하기") + void multipleVote() { // given User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(new Post( - null, - user.getId(), - "description", - Status.CLOSED, - Scope.PRIVATE, - List.of( - PostImage.create("뽀또A", imageFile1.getId()), - PostImage.create("뽀또B", imageFile2.getId()) - ), - "shareUrl", - VoteType.SINGLE - )); + Post post = postRepository.save(createMultiplePost(user.getId(), imageFile1, imageFile2, 1)); // when - assertThatThrownBy(() -> voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId())) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); - } - - @Test - @DisplayName("게스트 투표하기") - void guestVote() { - // given - User user = userRepository.save(createUser(1)); - Long guestId = user.getId() + 1L; - ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); - - // when - Long voteId = voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId()); + Long voteId1 = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + Long voteId2 = voteService.vote(user.getId(), post.getId(), post.getImages().get(1).getId()); // then - Vote vote = voteRepository.findById(voteId).get(); + Vote vote1 = voteRepository.findById(voteId1).get(); + Vote vote2 = voteRepository.findById(voteId2).get(); Post findPost = postRepository.findById(post.getId()).get(); assertAll( - () -> assertThat(vote.getUserId()).isEqualTo(guestId), - () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), - () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), - () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) - ); - } - - @Test - @DisplayName("게스트 투표하기 - 다른 이미지로 투표 변경한 경우") - void guestVote_change() { - // given - User user = userRepository.save(createUser(1)); - Long guestId = user.getId() + 1L; - ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); - voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId()); + () -> assertThat(vote1.getUserId()).isEqualTo(user.getId()), + () -> assertThat(vote1.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote1.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), - // when - Long voteId = voteService.guestVote(guestId, post.getId(), post.getImages().get(1).getId()); + () -> assertThat(vote2.getUserId()).isEqualTo(user.getId()), + () -> assertThat(vote2.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote2.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), - // then - Vote vote = voteRepository.findById(voteId).get(); - Post findPost = postRepository.findById(post.getId()).get(); - assertAll( - () -> assertThat(vote.getUserId()).isEqualTo(guestId), - () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), - () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), - () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1), () -> assertThat(findPost.getImages().get(1).getVoteCount()).isEqualTo(1) ); } @Test - @DisplayName("게스트 투표하기 - 투표 마감된 경우") - void guestVote_alreadyClosed() { + @DisplayName("투표하기 - 투표 마감된 경우") + void vote_alreadyClosed() { // given User user = userRepository.save(createUser(1)); - Long guestId = user.getId() + 1L; ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(new Post( @@ -188,7 +142,7 @@ void guestVote_alreadyClosed() { )); // when - assertThatThrownBy(() -> voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId())) + assertThatThrownBy(() -> voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId())) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); } diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index c1c9d502..b636af6c 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -77,7 +77,7 @@ void guestVote() throws Exception { .description("투표 후보 Id") ) )); - verify(voteService, times(1)).guestVote(any(), any(), any()); + verify(voteService, times(1)).vote(any(), any(), any()); } @Test From 2e9fa2e1656e11b12bad96f83889480c128a1e12 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 17:59:20 +0900 Subject: [PATCH 61/95] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/SecurityConfig.java | 1 + .../common/exception/ErrorCode.java | 2 + .../vote/application/VoteService.java | 21 +++++- .../java/com/swyp8team2/vote/domain/Vote.java | 4 + .../vote/domain/VoteRepository.java | 2 + .../vote/presentation/VoteController.java | 13 ++-- .../vote/application/VoteServiceTest.java | 38 ++++++++++ .../vote/presentation/VoteControllerTest.java | 73 ++----------------- 8 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index c0fa5956..08c07a05 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -117,6 +117,7 @@ public static MvcRequestMatcher[] getGuestAllowedList(HandlerMappingIntrospector MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ mvc.pattern(HttpMethod.POST, "/posts/{postId}/votes"), + mvc.pattern(HttpMethod.DELETE, "/votes/{voteId}"), mvc.pattern(HttpMethod.GET, "/users/me"), }; } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index c6a875f5..4ed664e1 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -23,6 +23,8 @@ public enum ErrorCode { FILE_NAME_TOO_LONG("파일 이름이 너무 김"), ACCESS_DENIED_VOTE_STATUS("투표 현황 조회 권한 없음"), COMMENT_NOT_FOUND("존재하지 않는 댓글"), + VOTE_NOT_FOUND("존재하지 않는 투표"), + NOT_VOTER("투표자가 아님"), //401 EXPIRED_TOKEN("토큰 만료"), diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 7e22012b..5f8eb9bc 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -47,10 +47,10 @@ public Long vote(Long voterId, Long postId, Long imageId) { private void deleteVoteIfExisting(Post post, Long userId) { voteRepository.findByUserIdAndPostId(userId, post.getId()) - .ifPresent(vote -> { - voteRepository.delete(vote); - post.cancelVote(vote.getPostImageId()); - }); + .ifPresent(vote -> { + voteRepository.delete(vote); + post.cancelVote(vote.getPostImageId()); + }); } private Vote createVote(Post post, Long imageId, Long userId) { @@ -58,4 +58,17 @@ private Vote createVote(Post post, Long imageId, Long userId) { post.vote(imageId); return vote; } + + @Transactional + public void cancelVote(Long userId, Long voteId) { + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new BadRequestException(ErrorCode.VOTE_NOT_FOUND)); + if (!vote.isVoter(userId)) { + throw new BadRequestException(ErrorCode.NOT_VOTER); + } + voteRepository.delete(vote); + Post post = postRepository.findById(vote.getPostId()) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.cancelVote(vote.getPostImageId()); + } } diff --git a/src/main/java/com/swyp8team2/vote/domain/Vote.java b/src/main/java/com/swyp8team2/vote/domain/Vote.java index 23bd38a9..30d3928a 100644 --- a/src/main/java/com/swyp8team2/vote/domain/Vote.java +++ b/src/main/java/com/swyp8team2/vote/domain/Vote.java @@ -36,4 +36,8 @@ public Vote(Long id, Long postId, Long postImageId, Long userId) { public static Vote of(Long postId, Long postImageId, Long userId) { return new Vote(null, postId, postImageId, userId); } + + public boolean isVoter(Long userId) { + return this.userId.equals(userId); + } } diff --git a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java index 2d1ad8a4..8bea21a1 100644 --- a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java +++ b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java @@ -13,4 +13,6 @@ public interface VoteRepository extends JpaRepository { Slice findByUserId(Long userId); Optional findByUserIdAndPostImageId(Long voterId, Long imageId); + + Optional findByIdAndUserId(Long voteId, Long userId); } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index e05a050f..dc319f70 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -19,12 +20,11 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/posts/{postId}/votes") public class VoteController { private final VoteService voteService; - @PostMapping("") + @PostMapping("/posts/{postId}/votes") public ResponseEntity vote( @PathVariable("postId") Long postId, @Valid @RequestBody VoteRequest request, @@ -34,13 +34,12 @@ public ResponseEntity vote( return ResponseEntity.ok().build(); } - @PostMapping("/guest") - public ResponseEntity guestVote( - @PathVariable("postId") Long postId, - @Valid @RequestBody VoteRequest request, + @DeleteMapping("/votes/{voteId}") + public ResponseEntity cancelVote( + @PathVariable("voteId") Long voteId, @AuthenticationPrincipal UserInfo userInfo ) { - voteService.vote(userInfo.userId(), postId, request.imageId()); + voteService.cancelVote(userInfo.userId(), voteId); return ResponseEntity.ok().build(); } } diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index c003676b..71f2d1b9 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -146,4 +146,42 @@ void vote_alreadyClosed() { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); } + + @Test + @DisplayName("투표 취소") + void cancelVote() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Long voteId = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + + // when + voteService.cancelVote(user.getId(), voteId); + + // then + boolean res = voteRepository.findById(voteId).isEmpty(); + Post findPost = postRepository.findById(post.getId()).get(); + assertAll( + () -> assertThat(res).isEqualTo(true), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0) + ); + } + + @Test + @DisplayName("투표 취소 - 투표자가 아닌 경우") + void cancelVote_notVoter() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Long voteId = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + + // when then + assertThatThrownBy(() -> voteService.cancelVote(2L, voteId)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.NOT_VOTER.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index b636af6c..0d061df2 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -53,82 +54,22 @@ void vote() throws Exception { verify(voteService, times(1)).vote(any(), any(), any()); } - @Test - @WithMockUserInfo(role = Role.GUEST) - @DisplayName("게스트 투표") - void guestVote() throws Exception { - //given - VoteRequest request = new VoteRequest(1L); - - //when test - mockMvc.perform(post("/posts/{postId}/votes/guest", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_TOKEN, "guestToken")) - .andExpect(status().isOk()) - .andDo(restDocs.document( - requestHeaders(guestHeader()), - pathParameters( - parameterWithName("postId").description("게시글 Id") - ), - requestFields( - fieldWithPath("imageId") - .type(JsonFieldType.NUMBER) - .description("투표 후보 Id") - ) - )); - verify(voteService, times(1)).vote(any(), any(), any()); - } - @Test @WithMockUserInfo - @DisplayName("투표 변경") - void changeVote() throws Exception { + @DisplayName("투표 취소") + void cancelVote() throws Exception { //given - ChangeVoteRequest request = new ChangeVoteRequest(1L); - //when - mockMvc.perform(patch("/posts/{postId}/votes", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + //when test + mockMvc.perform(delete("/votes/{voteId}", "1") .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(authorizationHeader()), pathParameters( - parameterWithName("postId").description("변경할 게시글 Id") - ), - requestFields( - fieldWithPath("imageId") - .type(JsonFieldType.NUMBER) - .description("변경할 투표 이미지 Id") - ) - )); - } - - @Test - @WithMockUserInfo(role = Role.GUEST) - @DisplayName("게스트 투표 변경") - void guestChangeVote() throws Exception { - //given - ChangeVoteRequest request = new ChangeVoteRequest(1L); - - //when - mockMvc.perform(patch("/posts/{postId}/votes/guest", "1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_TOKEN, "guestToken")) - .andExpect(status().isOk()) - .andDo(restDocs.document( - requestHeaders(guestHeader()), - pathParameters( - parameterWithName("postId").description("변경활 게시글 Id") - ), - requestFields( - fieldWithPath("imageId") - .type(JsonFieldType.NUMBER) - .description("변경할 투표 이미지 Id") + parameterWithName("voteId").description("투표 Id") ) )); + verify(voteService, times(1)).cancelVote(any(), any()); } } From d70ec2d6828b2dc0b8880f08b26efeb02e943af1 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 18:01:16 +0900 Subject: [PATCH 62/95] =?UTF-8?q?docs:=20=ED=88=AC=ED=91=9C=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/votes.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc index 0b661b8f..aca6fcfd 100644 --- a/src/docs/asciidoc/votes.adoc +++ b/src/docs/asciidoc/votes.adoc @@ -5,3 +5,8 @@ === `POST` 투표 operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] + +[[투표-취소]] +=== `DELETE` 투표 취소 + +operation::vote-controller-test/cancel-vote[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] From 3a90cdec6e6bcf939efede7f236b193d099c23f2 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 18:15:15 +0900 Subject: [PATCH 63/95] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20->=20=ED=88=AC=ED=91=9C=20Id=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostService.java | 22 ++++++++----------- .../post/presentation/PostController.java | 1 - .../presentation/dto/PostImageResponse.java | 2 +- .../post/application/PostServiceTest.java | 4 ++-- .../post/presentation/PostControllerTest.java | 12 +++++----- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 27d5fbec..6b504a96 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -27,7 +27,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Objects; @Service @Transactional(readOnly = true) @@ -79,37 +78,34 @@ public PostResponse findById(Long userId, Long postId) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); User author = userRepository.findById(post.getUserId()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - List votes = createPostImageResponse(userId, postId, post); + List votes = createPostImageResponse(userId, post); boolean isAuthor = post.getUserId().equals(userId); return PostResponse.of(post, author, votes, isAuthor); } - private List createPostImageResponse(Long userId, Long postId, Post post) { + private List createPostImageResponse(Long userId, Post post) { List images = post.getImages(); return images.stream() - .map(image -> createVoteResponseDto(image, userId, postId)) + .map(image -> createVoteResponseDto(image, userId)) .toList(); } - private PostImageResponse createVoteResponseDto(PostImage image, Long userId, Long postId) { + private PostImageResponse createVoteResponseDto(PostImage image, Long userId) { ImageFile imageFile = imageFileRepository.findById(image.getImageFileId()) .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); - boolean voted = Objects.nonNull(userId) && getVoted(image, userId, postId); return new PostImageResponse( image.getId(), image.getName(), imageFile.getImageUrl(), imageFile.getThumbnailUrl(), - voted + getVoteId(image, userId) ); } - private Boolean getVoted(PostImage image, Long userId, Long postId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - return voteRepository.findByUserIdAndPostId(user.getId(), postId) - .map(vote -> vote.getPostImageId().equals(image.getId())) - .orElse(false); + private Long getVoteId(PostImage image, Long userId) { + return voteRepository.findByUserIdAndPostImageId(userId, image.getId()) + .map(Vote::getId) + .orElse(null); } public CursorBasePaginatedResponse findUserPosts(Long userId, Long cursor, int size) { diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index f3083f97..067c1fe2 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -33,7 +33,6 @@ public ResponseEntity createPost( @Valid @RequestBody CreatePostRequest request, @AuthenticationPrincipal UserInfo userInfo ) { - return ResponseEntity.ok(postService.create(userInfo.userId(), request)); } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java index 0fd9881e..7eff9968 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java @@ -5,6 +5,6 @@ public record PostImageResponse( String imageName, String imageUrl, String thumbnailUrl, - boolean voted + Long voteId ) { } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 88db57a2..bf7b2a96 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -162,9 +162,9 @@ void findById() throws Exception { () -> assertThat(response.shareUrl()).isEqualTo(post.getShareUrl()), () -> assertThat(votes).hasSize(2), () -> assertThat(votes.get(0).imageUrl()).isEqualTo(imageFile1.getImageUrl()), - () -> assertThat(votes.get(0).voted()).isFalse(), + () -> assertThat(votes.get(0).voteId()).isNull(), () -> assertThat(votes.get(1).imageUrl()).isEqualTo(imageFile2.getImageUrl()), - () -> assertThat(votes.get(1).voted()).isFalse() + () -> assertThat(votes.get(1).voteId()).isNull() ); } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 8bfa658c..a69c4382 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -101,8 +101,8 @@ void findPost() throws Exception { ), "description", List.of( - new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), - new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", 1L), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", null) ), "https://photopic.site/shareurl", true, @@ -132,7 +132,7 @@ void findPost() throws Exception { fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), - fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), + fieldWithPath("images[].voteId").type(JsonFieldType.NUMBER).optional().description("투표 Id (투표 안 한 경우 null)"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), fieldWithPath("status").type(JsonFieldType.STRING).description("게시글 마감 여부 (PROGRESS, CLOSED)"), @@ -155,8 +155,8 @@ void findPost_shareUrl() throws Exception { ), "description", List.of( - new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), - new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", 1L), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", null) ), "https://photopic.site/shareurl", true, @@ -186,7 +186,7 @@ void findPost_shareUrl() throws Exception { fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), - fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), + fieldWithPath("images[].voteId").type(JsonFieldType.NUMBER).optional().description("투표 Id (투표 안 한 경우 null)"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), fieldWithPath("status").type(JsonFieldType.STRING).description("게시글 마감 여부 (PROGRESS, CLOSED)"), From 8525a86d4d9c11b86cd813d4a2f125d69d2a9a52 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 20:06:44 +0900 Subject: [PATCH 64/95] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/post/application/PostService.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 14e9c9b2..e96a1444 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -114,14 +114,6 @@ public CursorBasePaginatedResponse findUserPosts(Long userId return getCursorPaginatedResponse(postSlice); } - private SimplePostResponse getSimplePostResponse(Post post, List imageIds) { - ImageFile bestPickedImage = imageIds.stream() - .filter(imageFile -> imageFile.getId().equals(post.getBestPickedImage().getImageFileId())) - .findFirst() - .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); - return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); - } - public CursorBasePaginatedResponse findVotedPosts(Long userId, Long cursor, int size) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); @@ -151,6 +143,14 @@ private CursorBasePaginatedResponse getCursorPaginatedRespon )); } + private SimplePostResponse getSimplePostResponse(Post post, List imageIds) { + ImageFile bestPickedImage = imageIds.stream() + .filter(imageFile -> imageFile.getId().equals(post.getBestPickedImage().getImageFileId())) + .findFirst() + .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); + return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); + } + public List findVoteStatus(Long userId, Long postId) { Post post = postRepository.findByIdFetchPostImage(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); From 26a04807ab738a996dad9b554dffc867acf0a882 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 20:25:45 +0900 Subject: [PATCH 65/95] =?UTF-8?q?test:=20=EB=B2=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=94=BD=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/post/domain/PostTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index 9539c1eb..b634be3b 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -161,4 +161,45 @@ void toggleScope_notPostAuthor() throws Exception { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.NOT_POST_AUTHOR.getMessage()); } + + @Test + @DisplayName("게시글 베스트 픽 조회") + void getBestPickedImage() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl", VoteType.SINGLE); + post.getImages().get(0).increaseVoteCount(); + post.getImages().get(0).increaseVoteCount(); + post.getImages().get(1).increaseVoteCount(); + + //when + PostImage bestPickedImage = post.getBestPickedImage(); + + //then + assertThat(bestPickedImage.getName()).isEqualTo("뽀또A"); + } + + @Test + @DisplayName("게시글 베스트 픽 조회 - 동일 투표수인 경우 첫 번째 이미지가 선택됨") + void getBestPickedImage_saveVoteCount() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", Status.PROGRESS, Scope.PRIVATE, postImages, "shareUrl", VoteType.SINGLE); + post.getImages().get(0).increaseVoteCount(); + post.getImages().get(1).increaseVoteCount(); + + //when + PostImage bestPickedImage = post.getBestPickedImage(); + + //then + assertThat(bestPickedImage.getName()).isEqualTo("뽀또A"); + } } From 1821c134d4e8f6db2092ddab1d5a1ddaaebf171b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 20:49:15 +0900 Subject: [PATCH 66/95] =?UTF-8?q?refactor:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=ED=98=84=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20api=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20command=20query=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostCommandService.java | 66 ++++++ .../post/application/PostQueryService.java | 121 +++++++++++ .../post/application/PostService.java | 193 ++--------------- .../post/presentation/PostController.java | 9 - .../application/RatioCalculator.java | 2 +- .../vote/application/VoteService.java | 32 +++ .../vote/presentation/VoteController.java | 17 +- .../dto/PostImageVoteStatusResponse.java | 2 +- ...eTest.java => PostCommandServiceTest.java} | 199 ++---------------- .../application/PostQueryServiceTest.java | 146 +++++++++++++ .../post/presentation/PostControllerTest.java | 32 --- .../application/RatioCalculatorTest.java | 2 +- .../vote/application/VoteServiceTest.java | 71 +++++++ .../vote/presentation/VoteControllerTest.java | 42 +++- 14 files changed, 522 insertions(+), 412 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/application/PostCommandService.java create mode 100644 src/main/java/com/swyp8team2/post/application/PostQueryService.java rename src/main/java/com/swyp8team2/{post => vote}/application/RatioCalculator.java (93%) rename src/main/java/com/swyp8team2/{post => vote}/presentation/dto/PostImageVoteStatusResponse.java (75%) rename src/test/java/com/swyp8team2/post/application/{PostServiceTest.java => PostCommandServiceTest.java} (51%) create mode 100644 src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java rename src/test/java/com/swyp8team2/{post => vote}/application/RatioCalculatorTest.java (95%) diff --git a/src/main/java/com/swyp8team2/post/application/PostCommandService.java b/src/main/java/com/swyp8team2/post/application/PostCommandService.java new file mode 100644 index 00000000..8bf8649c --- /dev/null +++ b/src/main/java/com/swyp8team2/post/application/PostCommandService.java @@ -0,0 +1,66 @@ +package com.swyp8team2.post.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; +import com.swyp8team2.user.domain.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class PostCommandService { + + private final PostRepository postRepository; + private final CryptoService shareUrlCryptoService; + + public CreatePostResponse create(Long userId, CreatePostRequest request) { + List postImages = createPostImages(request); + Post post = Post.create(userId, request.description(), postImages, request.voteType()); + Post save = postRepository.save(post); + save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); + return new CreatePostResponse(save.getId(), save.getShareUrl()); + } + + private List createPostImages(CreatePostRequest request) { + PostImageNameGenerator nameGenerator = new PostImageNameGenerator(); + return request.images().stream() + .map(voteRequestDto -> PostImage.create( + nameGenerator.generate(), + voteRequestDto.imageFileId() + )).toList(); + } + + @Transactional + public void delete(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + if (!post.isAuthor(userId)) { + throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); + } + postRepository.delete(post); + } + + @Transactional + public void close(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.close(userId); + } + + @Transactional + public void toggleScope(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.toggleScope(userId); + } +} diff --git a/src/main/java/com/swyp8team2/post/application/PostQueryService.java b/src/main/java/com/swyp8team2/post/application/PostQueryService.java new file mode 100644 index 00000000..e2a47dfd --- /dev/null +++ b/src/main/java/com/swyp8team2/post/application/PostQueryService.java @@ -0,0 +1,121 @@ +package com.swyp8team2.post.application; + +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.presentation.dto.PostImageResponse; +import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.SimplePostResponse; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; +import com.swyp8team2.vote.domain.VoteRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class PostQueryService { + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final ImageFileRepository imageFileRepository; + private final VoteRepository voteRepository; + private final CryptoService shareUrlCryptoService; + + public PostResponse findByShareUrl(Long userId, String shareUrl) { + String decrypt = shareUrlCryptoService.decrypt(shareUrl); + return findById(userId, Long.valueOf(decrypt)); + } + + public PostResponse findById(Long userId, Long postId) { + Post post = postRepository.findByIdFetchPostImage(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + User author = userRepository.findById(post.getUserId()) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + List votes = createPostImageResponse(userId, post); + boolean isAuthor = post.getUserId().equals(userId); + return PostResponse.of(post, author, votes, isAuthor); + } + + private List createPostImageResponse(Long userId, Post post) { + List images = post.getImages(); + return images.stream() + .map(image -> createVoteResponseDto(image, userId)) + .toList(); + } + + private PostImageResponse createVoteResponseDto(PostImage image, Long userId) { + ImageFile imageFile = imageFileRepository.findById(image.getImageFileId()) + .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); + return new PostImageResponse( + image.getId(), + image.getName(), + imageFile.getImageUrl(), + imageFile.getThumbnailUrl(), + getVoteId(image, userId) + ); + } + + private Long getVoteId(PostImage image, Long userId) { + return voteRepository.findByUserIdAndPostImageId(userId, image.getId()) + .map(Vote::getId) + .orElse(null); + } + + public CursorBasePaginatedResponse findUserPosts(Long userId, Long cursor, int size) { + Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); + return getCursorPaginatedResponse(postSlice); + } + + public CursorBasePaginatedResponse findVotedPosts(Long userId, Long cursor, int size) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + List votedPostIds = voteRepository.findByUserId(user.getId()) + .map(Vote::getPostId) + .toList(); + Slice votedPostSlice = postRepository.findByIdIn(votedPostIds, cursor, PageRequest.ofSize(size)); + + return getCursorPaginatedResponse(votedPostSlice); + } + + private CursorBasePaginatedResponse getCursorPaginatedResponse(Slice postSlice) { + List bestPickedImageIds = postSlice.getContent().stream() + .map(Post::getBestPickedImage) + .map(PostImage::getImageFileId) + .toList(); + List imageIds = imageFileRepository.findByIdIn(bestPickedImageIds); + + List responseContent = postSlice.getContent().stream() + .map(post -> getSimplePostResponse(post, imageIds)) + .toList(); + + return CursorBasePaginatedResponse.of(new SliceImpl<>( + responseContent, + postSlice.getPageable(), + postSlice.hasNext() + )); + } + + private SimplePostResponse getSimplePostResponse(Post post, List imageIds) { + ImageFile bestPickedImage = imageIds.stream() + .filter(imageFile -> imageFile.getId().equals(post.getBestPickedImage().getImageFileId())) + .findFirst() + .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); + return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); + } +} diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index e96a1444..5666352a 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,210 +1,55 @@ package com.swyp8team2.post.application; -import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; -import com.swyp8team2.common.exception.BadRequestException; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.InternalServerException; -import com.swyp8team2.crypto.application.CryptoService; -import com.swyp8team2.image.domain.ImageFile; -import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostResponse; -import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; -import com.swyp8team2.post.presentation.dto.PostImageResponse; -import com.swyp8team2.user.domain.User; -import com.swyp8team2.user.domain.UserRepository; -import com.swyp8team2.vote.domain.Vote; -import com.swyp8team2.vote.domain.VoteRepository; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @Transactional(readOnly = true) +@RequiredArgsConstructor public class PostService { - private final PostRepository postRepository; - private final UserRepository userRepository; - private final RatioCalculator ratioCalculator; - private final ImageFileRepository imageFileRepository; - private final VoteRepository voteRepository; - private final CryptoService shareUrlCryptoService; - - public PostService( - PostRepository postRepository, - UserRepository userRepository, - RatioCalculator ratioCalculator, - ImageFileRepository imageFileRepository, - VoteRepository voteRepository, - @ShareUrlCryptoService CryptoService shareUrlCryptoService - ) { - this.postRepository = postRepository; - this.userRepository = userRepository; - this.ratioCalculator = ratioCalculator; - this.imageFileRepository = imageFileRepository; - this.voteRepository = voteRepository; - this.shareUrlCryptoService = shareUrlCryptoService; - } + private final PostCommandService postCommandService; + private final PostQueryService postQueryService; @Transactional public CreatePostResponse create(Long userId, CreatePostRequest request) { - List postImages = createPostImages(request); - Post post = Post.create(userId, request.description(), postImages, request.voteType()); - Post save = postRepository.save(post); - save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); - return new CreatePostResponse(save.getId(), save.getShareUrl()); - } - - private List createPostImages(CreatePostRequest request) { - PostImageNameGenerator nameGenerator = new PostImageNameGenerator(); - return request.images().stream() - .map(voteRequestDto -> PostImage.create( - nameGenerator.generate(), - voteRequestDto.imageFileId() - )).toList(); + return postCommandService.create(userId, request); } - public PostResponse findById(Long userId, Long postId) { - Post post = postRepository.findByIdFetchPostImage(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - User author = userRepository.findById(post.getUserId()) - .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - List votes = createPostImageResponse(userId, post); - boolean isAuthor = post.getUserId().equals(userId); - return PostResponse.of(post, author, votes, isAuthor); + @Transactional + public void delete(Long userId, Long postId) { + postCommandService.delete(userId, postId); } - private List createPostImageResponse(Long userId, Post post) { - List images = post.getImages(); - return images.stream() - .map(image -> createVoteResponseDto(image, userId)) - .toList(); + @Transactional + public void close(Long userId, Long postId) { + postCommandService.close(userId, postId); } - private PostImageResponse createVoteResponseDto(PostImage image, Long userId) { - ImageFile imageFile = imageFileRepository.findById(image.getImageFileId()) - .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); - return new PostImageResponse( - image.getId(), - image.getName(), - imageFile.getImageUrl(), - imageFile.getThumbnailUrl(), - getVoteId(image, userId) - ); + @Transactional + public void toggleScope(Long userId, Long postId) { + postCommandService.toggleScope(userId, postId); } - private Long getVoteId(PostImage image, Long userId) { - return voteRepository.findByUserIdAndPostImageId(userId, image.getId()) - .map(Vote::getId) - .orElse(null); + public PostResponse findById(Long userId, Long postId) { + return postQueryService.findById(userId, postId); } public CursorBasePaginatedResponse findUserPosts(Long userId, Long cursor, int size) { - Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); - return getCursorPaginatedResponse(postSlice); + return postQueryService.findUserPosts(userId, cursor, size); } public CursorBasePaginatedResponse findVotedPosts(Long userId, Long cursor, int size) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - List votedPostIds = voteRepository.findByUserId(user.getId()) - .map(Vote::getPostId) - .toList(); - Slice votedPostSlice = postRepository.findByIdIn(votedPostIds, cursor, PageRequest.ofSize(size)); - - return getCursorPaginatedResponse(votedPostSlice); - } - - private CursorBasePaginatedResponse getCursorPaginatedResponse(Slice postSlice) { - List bestPickedImageIds = postSlice.getContent().stream() - .map(Post::getBestPickedImage) - .map(PostImage::getImageFileId) - .toList(); - List imageIds = imageFileRepository.findByIdIn(bestPickedImageIds); - - List responseContent = postSlice.getContent().stream() - .map(post -> getSimplePostResponse(post, imageIds)) - .toList(); - - return CursorBasePaginatedResponse.of(new SliceImpl<>( - responseContent, - postSlice.getPageable(), - postSlice.hasNext() - )); - } - - private SimplePostResponse getSimplePostResponse(Post post, List imageIds) { - ImageFile bestPickedImage = imageIds.stream() - .filter(imageFile -> imageFile.getId().equals(post.getBestPickedImage().getImageFileId())) - .findFirst() - .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); - return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); - } - - public List findVoteStatus(Long userId, Long postId) { - Post post = postRepository.findByIdFetchPostImage(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - validateVoteStatus(userId, post); - int totalVoteCount = getTotalVoteCount(post.getImages()); - return post.getImages().stream() - .map(image -> { - String ratio = ratioCalculator.calculate(totalVoteCount, image.getVoteCount()); - return new PostImageVoteStatusResponse(image.getId(), image.getName(), image.getVoteCount(), ratio); - }).toList(); - } - - private void validateVoteStatus(Long userId, Post post) { - boolean voted = voteRepository.findByUserIdAndPostId(userId, post.getId()) - .isPresent(); - if (!(post.isAuthor(userId) || voted)) { - throw new BadRequestException(ErrorCode.ACCESS_DENIED_VOTE_STATUS); - } - } - - private int getTotalVoteCount(List images) { - int totalVoteCount = 0; - for (PostImage image : images) { - totalVoteCount += image.getVoteCount(); - } - return totalVoteCount; - } - - @Transactional - public void delete(Long userId, Long postId) { - Post post = postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - if (!post.isAuthor(userId)) { - throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); - } - postRepository.delete(post); - } - - @Transactional - public void close(Long userId, Long postId) { - Post post = postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - post.close(userId); + return postQueryService.findVotedPosts(userId, cursor, size); } public PostResponse findByShareUrl(Long userId, String shareUrl) { - String decrypt = shareUrlCryptoService.decrypt(shareUrl); - return findById(userId, Long.valueOf(decrypt)); - } - - @Transactional - public void toggleScope(Long userId, Long postId) { - Post post = postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - post.toggleScope(userId); + return postQueryService.findByShareUrl(userId, shareUrl); } } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 067c1fe2..6cc78d69 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -18,7 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.List; import java.util.Optional; @RestController @@ -58,14 +57,6 @@ public ResponseEntity findPostByShareUrl( return ResponseEntity.ok(postService.findByShareUrl(userId, shareUrl)); } - @GetMapping("/{postId}/status") - public ResponseEntity> findVoteStatus( - @PathVariable("postId") Long postId, - @AuthenticationPrincipal UserInfo userInfo - ) { - return ResponseEntity.ok(postService.findVoteStatus(userInfo.userId(), postId)); - } - @PostMapping("/{postId}/scope") public ResponseEntity toggleScopePost( @PathVariable("postId") Long postId, diff --git a/src/main/java/com/swyp8team2/post/application/RatioCalculator.java b/src/main/java/com/swyp8team2/vote/application/RatioCalculator.java similarity index 93% rename from src/main/java/com/swyp8team2/post/application/RatioCalculator.java rename to src/main/java/com/swyp8team2/vote/application/RatioCalculator.java index 1f4c8ec1..3b7e954b 100644 --- a/src/main/java/com/swyp8team2/post/application/RatioCalculator.java +++ b/src/main/java/com/swyp8team2/vote/application/RatioCalculator.java @@ -1,4 +1,4 @@ -package com.swyp8team2.post.application; +package com.swyp8team2.vote.application; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 5f8eb9bc..486afca2 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -3,8 +3,10 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.domain.VoteType; +import com.swyp8team2.vote.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -13,6 +15,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; @Service @@ -23,6 +26,7 @@ public class VoteService { private final VoteRepository voteRepository; private final UserRepository userRepository; private final PostRepository postRepository; + private final RatioCalculator ratioCalculator; @Transactional public Long vote(Long voterId, Long postId, Long imageId) { @@ -71,4 +75,32 @@ public void cancelVote(Long userId, Long voteId) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.cancelVote(vote.getPostImageId()); } + + public List findVoteStatus(Long userId, Long postId) { + Post post = postRepository.findByIdFetchPostImage(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + validateVoteStatus(userId, post); + int totalVoteCount = getTotalVoteCount(post.getImages()); + return post.getImages().stream() + .map(image -> { + String ratio = ratioCalculator.calculate(totalVoteCount, image.getVoteCount()); + return new PostImageVoteStatusResponse(image.getId(), image.getName(), image.getVoteCount(), ratio); + }).toList(); + } + + private void validateVoteStatus(Long userId, Post post) { + boolean voted = voteRepository.findByUserIdAndPostId(userId, post.getId()) + .isPresent(); + if (!(post.isAuthor(userId) || voted)) { + throw new BadRequestException(ErrorCode.ACCESS_DENIED_VOTE_STATUS); + } + } + + private int getTotalVoteCount(List images) { + int totalVoteCount = 0; + for (PostImage image : images) { + totalVoteCount += image.getVoteCount(); + } + return totalVoteCount; + } } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index dc319f70..058e0bc7 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -1,23 +1,22 @@ package com.swyp8team2.vote.presentation; import com.swyp8team2.auth.domain.UserInfo; -import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.vote.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.vote.application.VoteService; -import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; import com.swyp8team2.vote.presentation.dto.VoteRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PatchMapping; +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.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController @RequiredArgsConstructor public class VoteController { @@ -42,4 +41,12 @@ public ResponseEntity cancelVote( voteService.cancelVote(userInfo.userId(), voteId); return ResponseEntity.ok().build(); } + + @GetMapping("/posts/{postId}/votes/status") + public ResponseEntity> findVoteStatus( + @PathVariable("postId") Long postId, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok(voteService.findVoteStatus(userInfo.userId(), postId)); + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java b/src/main/java/com/swyp8team2/vote/presentation/dto/PostImageVoteStatusResponse.java similarity index 75% rename from src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java rename to src/main/java/com/swyp8team2/vote/presentation/dto/PostImageVoteStatusResponse.java index 56c0f57f..86f2e4b0 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/PostImageVoteStatusResponse.java @@ -1,4 +1,4 @@ -package com.swyp8team2.post.presentation.dto; +package com.swyp8team2.vote.presentation.dto; public record PostImageVoteStatusResponse( Long id, diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java similarity index 51% rename from src/test/java/com/swyp8team2/post/application/PostServiceTest.java rename to src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java index dfb31d2a..35d9fd9c 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java @@ -13,16 +13,11 @@ import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; -import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.PostImageRequestDto; -import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; -import com.swyp8team2.vote.application.VoteService; -import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -36,11 +31,11 @@ import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -class PostServiceTest extends IntegrationTest { +public class PostCommandServiceTest extends IntegrationTest { @Autowired PostService postService; @@ -54,14 +49,7 @@ class PostServiceTest extends IntegrationTest { @Autowired ImageFileRepository imageFileRepository; - @Autowired - VoteRepository voteRepository; - - @Autowired - VoteService voteService; - @MockitoBean - @ShareUrlCryptoService CryptoService shareUrlCryptoService; @Test @@ -141,178 +129,6 @@ void create_descriptionCountExceeded() throws Exception { .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } - @Test - @DisplayName("게시글 조회") - void findById() throws Exception { - //given - User user = userRepository.save(createUser(1)); - ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); - - //when - PostResponse response = postService.findById(user.getId(), post.getId()); - - //then - List votes = response.images(); - assertAll( - () -> assertThat(response.description()).isEqualTo(post.getDescription()), - () -> assertThat(response.id()).isEqualTo(post.getId()), - () -> assertThat(response.author().nickname()).isEqualTo(user.getNickname()), - () -> assertThat(response.author().profileUrl()).isEqualTo(user.getProfileUrl()), - () -> assertThat(response.shareUrl()).isEqualTo(post.getShareUrl()), - () -> assertThat(votes).hasSize(2), - () -> assertThat(votes.get(0).imageUrl()).isEqualTo(imageFile1.getImageUrl()), - () -> assertThat(votes.get(0).voteId()).isNull(), - () -> assertThat(votes.get(1).imageUrl()).isEqualTo(imageFile2.getImageUrl()), - () -> assertThat(votes.get(1).voteId()).isNull() - ); - } - - @Test - @Disabled - @DisplayName("유저가 작성한 게시글 조회 - 커서 null인 경우") - void findUserPosts() throws Exception { - //given - User user = userRepository.save(createUser(1)); - List posts = createPosts(user); - int size = 10; - - //when - var response = postService.findUserPosts(user.getId(), null, size); - - //then - assertAll( - () -> assertThat(response.data()).hasSize(size), - () -> assertThat(response.hasNext()).isTrue(), - () -> assertThat(response.nextCursor()).isEqualTo(posts.get(posts.size() - size).getId()) - ); - } - - @Test - @Disabled - @DisplayName("유저가 작성한 게시글 조회 - 커서 있는 경우") - void findUserPosts2() throws Exception { - //given - User user = userRepository.save(createUser(1)); - List posts = createPosts(user); - int size = 10; - - //when - var response = postService.findUserPosts(user.getId(), posts.get(3).getId(), size); - - //then - assertAll( - () -> assertThat(response.data()).hasSize(3), - () -> assertThat(response.hasNext()).isFalse(), - () -> assertThat(response.nextCursor()).isEqualTo(posts.get(0).getId()) - ); - } - - private List createPosts(User user) { - List posts = new ArrayList<>(); - for (int i = 0; i < 30; i += 2) { - ImageFile imageFile1 = imageFileRepository.save(createImageFile(i)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(i + 1)); - posts.add(postRepository.save(createPost(user.getId(), imageFile1, imageFile2, i))); - } - return posts; - } - - @Test - @DisplayName("유저가 투표한 게시글 조회 - 커서 null인 경우") - void findVotedPosts() throws Exception { - //given - User user = userRepository.save(createUser(1)); - List posts = createPosts(user); - for (int i = 0; i < 15; i++) { - Post post = posts.get(i); - voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getId())); - } - int size = 10; - - //when - var response = postService.findVotedPosts(user.getId(), null, size); - - //then - int 전체_15개에서_맨_마지막_데이터_인덱스 = posts.size() - size; - assertAll( - () -> assertThat(response.data()).hasSize(size), - () -> assertThat(response.hasNext()).isTrue(), - () -> assertThat(response.nextCursor()).isEqualTo(posts.get(전체_15개에서_맨_마지막_데이터_인덱스).getId()) - ); - } - - @Test - @DisplayName("투표 현황 조회") - void findVoteStatus() throws Exception { - //given - User user = userRepository.save(createUser(1)); - ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); - voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); - - //when - var response = postService.findVoteStatus(user.getId(), post.getId()); - - //then - assertAll( - () -> assertThat(response).hasSize(2), - () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), - () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), - () -> assertThat(response.get(0).voteCount()).isEqualTo(1), - () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), - () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), - () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), - () -> assertThat(response.get(1).voteCount()).isEqualTo(0), - () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") - ); - } - - @Test - @DisplayName("투표 현황 조회 - 투표한 사람인 경우") - void findVoteStatus_voteUser() throws Exception { - //given - User author = userRepository.save(createUser(1)); - User voter = userRepository.save(createUser(2)); - ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); - voteService.vote(voter.getId(), post.getId(), post.getImages().get(0).getId()); - - //when - var response = postService.findVoteStatus(voter.getId(), post.getId()); - - //then - assertAll( - () -> assertThat(response).hasSize(2), - () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), - () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), - () -> assertThat(response.get(0).voteCount()).isEqualTo(1), - () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), - () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), - () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), - () -> assertThat(response.get(1).voteCount()).isEqualTo(0), - () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") - ); - } - - @Test - @DisplayName("투표 현황 조회 - 작성자 아니고 투표 안 한 사람인 경우") - void findVoteStatus_notAuthorAndVoter() throws Exception { - //given - User author = userRepository.save(createUser(1)); - ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); - - //when - assertThatThrownBy(() -> postService.findVoteStatus(2L, post.getId())) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.ACCESS_DENIED_VOTE_STATUS.getMessage()); - } - @Test @DisplayName("투표 마감") void close() throws Exception { @@ -387,4 +203,15 @@ void delete() throws Exception { //then assertThat(postRepository.findById(post.getId())).isEmpty(); } + + private List createPosts(User user) { + List posts = new ArrayList<>(); + for (int i = 0; i < 30; i += 2) { + ImageFile imageFile1 = imageFileRepository.save(createImageFile(i)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(i + 1)); + posts.add(postRepository.save(createPost(user.getId(), imageFile1, imageFile2, i))); + } + return posts; + } + } diff --git a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java new file mode 100644 index 00000000..45f16a28 --- /dev/null +++ b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java @@ -0,0 +1,146 @@ +package com.swyp8team2.post.application; + +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.PostImageResponse; +import com.swyp8team2.support.IntegrationTest; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; +import com.swyp8team2.vote.domain.VoteRepository; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; +import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; +import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class PostQueryServiceTest extends IntegrationTest { + + @Autowired + PostService postService; + + @Autowired + PostRepository postRepository; + + @Autowired + UserRepository userRepository; + + @Autowired + ImageFileRepository imageFileRepository; + + @Autowired + VoteRepository voteRepository; + + @Test + @DisplayName("게시글 조회") + void findById() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + //when + PostResponse response = postService.findById(user.getId(), post.getId()); + + //then + List votes = response.images(); + assertAll( + () -> assertThat(response.description()).isEqualTo(post.getDescription()), + () -> assertThat(response.id()).isEqualTo(post.getId()), + () -> assertThat(response.author().nickname()).isEqualTo(user.getNickname()), + () -> assertThat(response.author().profileUrl()).isEqualTo(user.getProfileUrl()), + () -> assertThat(response.shareUrl()).isEqualTo(post.getShareUrl()), + () -> assertThat(votes).hasSize(2), + () -> assertThat(votes.get(0).imageUrl()).isEqualTo(imageFile1.getImageUrl()), + () -> assertThat(votes.get(0).voteId()).isNull(), + () -> assertThat(votes.get(1).imageUrl()).isEqualTo(imageFile2.getImageUrl()), + () -> assertThat(votes.get(1).voteId()).isNull() + ); + } + + @Test + @Disabled + @DisplayName("유저가 작성한 게시글 조회 - 커서 null인 경우") + void findUserPosts() throws Exception { + //given + User user = userRepository.save(createUser(1)); + List posts = createPosts(user); + int size = 10; + + //when + var response = postService.findUserPosts(user.getId(), null, size); + + //then + assertAll( + () -> assertThat(response.data()).hasSize(size), + () -> assertThat(response.hasNext()).isTrue(), + () -> assertThat(response.nextCursor()).isEqualTo(posts.get(posts.size() - size).getId()) + ); + } + + @Test + @Disabled + @DisplayName("유저가 작성한 게시글 조회 - 커서 있는 경우") + void findUserPosts2() throws Exception { + //given + User user = userRepository.save(createUser(1)); + List posts = createPosts(user); + int size = 10; + + //when + var response = postService.findUserPosts(user.getId(), posts.get(3).getId(), size); + + //then + assertAll( + () -> assertThat(response.data()).hasSize(3), + () -> assertThat(response.hasNext()).isFalse(), + () -> assertThat(response.nextCursor()).isEqualTo(posts.get(0).getId()) + ); + } + + private List createPosts(User user) { + List posts = new ArrayList<>(); + for (int i = 0; i < 30; i += 2) { + ImageFile imageFile1 = imageFileRepository.save(createImageFile(i)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(i + 1)); + posts.add(postRepository.save(createPost(user.getId(), imageFile1, imageFile2, i))); + } + return posts; + } + + @Test + @DisplayName("유저가 투표한 게시글 조회 - 커서 null인 경우") + void findVotedPosts() throws Exception { + //given + User user = userRepository.save(createUser(1)); + List posts = createPosts(user); + for (int i = 0; i < 15; i++) { + Post post = posts.get(i); + voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getId())); + } + int size = 10; + + //when + var response = postService.findVotedPosts(user.getId(), null, size); + + //then + int 전체_15개에서_맨_마지막_데이터_인덱스 = posts.size() - size; + assertAll( + () -> assertThat(response.data()).hasSize(size), + () -> assertThat(response.hasNext()).isTrue(), + () -> assertThat(response.nextCursor()).isEqualTo(posts.get(전체_15개에서_맨_마지막_데이터_인덱스).getId()) + ); + } +} diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index a69c4382..e4b0eee4 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -195,38 +195,6 @@ void findPost_shareUrl() throws Exception { )); } - @Test - @WithMockUserInfo - @DisplayName("게시글 투표 상태 조회") - void findVoteStatus() throws Exception { - //given - var response = List.of( - new PostImageVoteStatusResponse(1L, "뽀또A", 2, "66.7"), - new PostImageVoteStatusResponse(2L, "뽀또B", 1, "33.3") - ); - given(postService.findVoteStatus(1L, 1L)) - .willReturn(response); - - //when then - mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{postId}/status", 1) - .header(HttpHeaders.AUTHORIZATION, "Bearer token")) - .andExpect(status().isOk()) - .andExpect(content().json(objectMapper.writeValueAsString(response))) - .andDo(restDocs.document( - requestHeaders(authorizationHeader()), - pathParameters( - parameterWithName("postId").description("게시글 Id") - ), - responseFields( - fieldWithPath("[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), - fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("이미지 Id"), - fieldWithPath("[].imageName").type(JsonFieldType.STRING).description("사진 이름"), - fieldWithPath("[].voteCount").type(JsonFieldType.NUMBER).description("투표 수"), - fieldWithPath("[].voteRatio").type(JsonFieldType.STRING).description("투표 비율") - ) - )); - } - @Test @WithMockUserInfo @DisplayName("게시글 삭제") diff --git a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java b/src/test/java/com/swyp8team2/vote/application/RatioCalculatorTest.java similarity index 95% rename from src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java rename to src/test/java/com/swyp8team2/vote/application/RatioCalculatorTest.java index 27e95e3d..8502377d 100644 --- a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java +++ b/src/test/java/com/swyp8team2/vote/application/RatioCalculatorTest.java @@ -1,4 +1,4 @@ -package com.swyp8team2.post.application; +package com.swyp8team2.vote.application; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index 71f2d1b9..26f3c9a6 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -184,4 +184,75 @@ void cancelVote_notVoter() { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.NOT_VOTER.getMessage()); } + + @Test + @DisplayName("투표 현황 조회") + void findVoteStatus() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + + //when + var response = voteService.findVoteStatus(user.getId(), post.getId()); + + //then + assertAll( + () -> assertThat(response).hasSize(2), + () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), + () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), + () -> assertThat(response.get(0).voteCount()).isEqualTo(1), + () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), + () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), + () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), + () -> assertThat(response.get(1).voteCount()).isEqualTo(0), + () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") + ); + } + + @Test + @DisplayName("투표 현황 조회 - 투표한 사람인 경우") + void findVoteStatus_voteUser() throws Exception { + //given + User author = userRepository.save(createUser(1)); + User voter = userRepository.save(createUser(2)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); + voteService.vote(voter.getId(), post.getId(), post.getImages().get(0).getId()); + + //when + var response = voteService.findVoteStatus(voter.getId(), post.getId()); + + //then + assertAll( + () -> assertThat(response).hasSize(2), + () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), + () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), + () -> assertThat(response.get(0).voteCount()).isEqualTo(1), + () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), + () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), + () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), + () -> assertThat(response.get(1).voteCount()).isEqualTo(0), + () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") + ); + } + + @Test + @DisplayName("투표 현황 조회 - 작성자 아니고 투표 안 한 사람인 경우") + void findVoteStatus_notAuthorAndVoter() throws Exception { + //given + User author = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); + + //when + assertThatThrownBy(() -> voteService.findVoteStatus(2L, post.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.ACCESS_DENIED_VOTE_STATUS.getMessage()); + } + } diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index 0d061df2..f323ef9c 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -1,18 +1,20 @@ package com.swyp8team2.vote.presentation; -import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.vote.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; -import com.swyp8team2.user.domain.Role; -import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; import com.swyp8team2.vote.presentation.dto.VoteRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; +import java.util.List; + import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; @@ -21,8 +23,10 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class VoteControllerTest extends RestDocsTest { @@ -72,4 +76,36 @@ void cancelVote() throws Exception { )); verify(voteService, times(1)).cancelVote(any(), any()); } + + @Test + @WithMockUserInfo + @DisplayName("게시글 투표 상태 조회") + void findVoteStatus() throws Exception { + //given + var response = List.of( + new PostImageVoteStatusResponse(1L, "뽀또A", 2, "66.7"), + new PostImageVoteStatusResponse(2L, "뽀또B", 1, "33.3") + ); + given(voteService.findVoteStatus(1L, 1L)) + .willReturn(response); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{postId}/votes/status", 1) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ), + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("이미지 Id"), + fieldWithPath("[].imageName").type(JsonFieldType.STRING).description("사진 이름"), + fieldWithPath("[].voteCount").type(JsonFieldType.NUMBER).description("투표 수"), + fieldWithPath("[].voteRatio").type(JsonFieldType.STRING).description("투표 비율") + ) + )); + } } From 0ad6f0079b240153ed7fc4a3c929d0e66e158d23 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 9 Mar 2025 20:56:20 +0900 Subject: [PATCH 67/95] =?UTF-8?q?refactor:=20=EA=B3=B5=EC=9C=A0=20url=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EC=97=86=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/CryptoConfig.java | 29 ------------------- .../common/dev/DataInitializer.java | 10 +++---- .../post/application/PostCommandService.java | 6 ++-- .../post/application/PostQueryService.java | 5 ++-- .../application/ShareUrlService.java} | 15 ++++++++-- .../application/PostCommandServiceTest.java | 7 ++--- .../application/PostQueryServiceTest.java | 2 -- .../application/ShareUrlServiceTest.java} | 25 +++++++--------- 8 files changed, 34 insertions(+), 65 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/common/config/CryptoConfig.java rename src/main/java/com/swyp8team2/{crypto/application/CryptoService.java => post/application/ShareUrlService.java} (77%) rename src/test/java/com/swyp8team2/{crypto/application/CryptoServiceTest.java => post/application/ShareUrlServiceTest.java} (64%) diff --git a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java deleted file mode 100644 index 1f814a28..00000000 --- a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.swyp8team2.common.config; - -import com.swyp8team2.crypto.application.CryptoService; -import com.swyp8team2.common.annotation.ShareUrlCryptoService; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.crypto.encrypt.AesBytesEncryptor; - -@Configuration -public class CryptoConfig { - - private final String shareUrlSymmetricKey; - private final String salt; - - public CryptoConfig( - @Value("${crypto.secret-key.share-url}") String shareUrlSymmetricKey, - @Value("${crypto.salt}") String salt - ) { - this.shareUrlSymmetricKey = shareUrlSymmetricKey; - this.salt = salt; - } - - @ShareUrlCryptoService - @Bean(name = ShareUrlCryptoService.QUALIFIER) - public CryptoService shareUrlCryptoService() throws Exception { - return new CryptoService(new AesBytesEncryptor(shareUrlSymmetricKey, salt)); - } -} diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 903803ed..b7567290 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -7,7 +7,7 @@ import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.common.annotation.ShareUrlCryptoService; -import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.post.application.ShareUrlService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; @@ -36,7 +36,7 @@ public class DataInitializer { private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final PostRepository postRepository; - private final CryptoService shaereUrlCryptoService; + private final ShareUrlService shaereUrlShareUrlService; private final JwtService jwtService; private final VoteService voteService; private final CommentRepository commentRepository; @@ -46,7 +46,7 @@ public DataInitializer( UserRepository userRepository, ImageFileRepository imageFileRepository, PostRepository postRepository, - @ShareUrlCryptoService CryptoService shaereUrlCryptoService, + @ShareUrlCryptoService ShareUrlService shaereUrlShareUrlService, JwtService jwtService, VoteService voteService, CommentRepository commentRepository @@ -55,7 +55,7 @@ public DataInitializer( this.userRepository = userRepository; this.imageFileRepository = imageFileRepository; this.postRepository = postRepository; - this.shaereUrlCryptoService = shaereUrlCryptoService; + this.shaereUrlShareUrlService = shaereUrlShareUrlService; this.jwtService = jwtService; this.voteService = voteService; this.commentRepository = commentRepository; @@ -83,7 +83,7 @@ public void init() { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), VoteType.SINGLE)); - post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); + post.setShareUrl(shaereUrlShareUrlService.encrypt(String.valueOf(post.getId()))); posts.add(post); } diff --git a/src/main/java/com/swyp8team2/post/application/PostCommandService.java b/src/main/java/com/swyp8team2/post/application/PostCommandService.java index 8bf8649c..2fc07198 100644 --- a/src/main/java/com/swyp8team2/post/application/PostCommandService.java +++ b/src/main/java/com/swyp8team2/post/application/PostCommandService.java @@ -2,13 +2,11 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; -import com.swyp8team2.user.domain.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,13 +19,13 @@ public class PostCommandService { private final PostRepository postRepository; - private final CryptoService shareUrlCryptoService; + private final ShareUrlService shareUrlShareUrlService; public CreatePostResponse create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); Post post = Post.create(userId, request.description(), postImages, request.voteType()); Post save = postRepository.save(post); - save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); + save.setShareUrl(shareUrlShareUrlService.encrypt(String.valueOf(save.getId()))); return new CreatePostResponse(save.getId(), save.getShareUrl()); } diff --git a/src/main/java/com/swyp8team2/post/application/PostQueryService.java b/src/main/java/com/swyp8team2/post/application/PostQueryService.java index e2a47dfd..80335f9a 100644 --- a/src/main/java/com/swyp8team2/post/application/PostQueryService.java +++ b/src/main/java/com/swyp8team2/post/application/PostQueryService.java @@ -4,7 +4,6 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; -import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; @@ -35,10 +34,10 @@ public class PostQueryService { private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final VoteRepository voteRepository; - private final CryptoService shareUrlCryptoService; + private final ShareUrlService shareUrlShareUrlService; public PostResponse findByShareUrl(Long userId, String shareUrl) { - String decrypt = shareUrlCryptoService.decrypt(shareUrl); + String decrypt = shareUrlShareUrlService.decrypt(shareUrl); return findById(userId, Long.valueOf(decrypt)); } diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/post/application/ShareUrlService.java similarity index 77% rename from src/main/java/com/swyp8team2/crypto/application/CryptoService.java rename to src/main/java/com/swyp8team2/post/application/ShareUrlService.java index 4ff34182..b16bb5e0 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/post/application/ShareUrlService.java @@ -1,4 +1,4 @@ -package com.swyp8team2.crypto.application; +package com.swyp8team2.post.application; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; @@ -6,17 +6,26 @@ import io.seruco.encoding.base62.Base62; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.encrypt.AesBytesEncryptor; +import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.nio.charset.StandardCharsets; @Slf4j -@RequiredArgsConstructor -public class CryptoService { +@Service +public class ShareUrlService { private final AesBytesEncryptor encryptor; + public ShareUrlService( + @Value("${crypto.secret-key.share-url}") String shareUrlSymmetricKey, + @Value("${crypto.salt}") String salt + ) { + this.encryptor = new AesBytesEncryptor(shareUrlSymmetricKey, salt); + } + public String encrypt(String data) { try { byte[] encrypt = encryptor.encrypt(data.getBytes(StandardCharsets.UTF_8)); diff --git a/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java index 35d9fd9c..e91fede6 100644 --- a/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java @@ -1,9 +1,7 @@ package com.swyp8team2.post.application; -import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; @@ -17,7 +15,6 @@ import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; -import com.swyp8team2.vote.domain.VoteRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -50,7 +47,7 @@ public class PostCommandServiceTest extends IntegrationTest { ImageFileRepository imageFileRepository; @MockitoBean - CryptoService shareUrlCryptoService; + ShareUrlService shareUrlShareUrlService; @Test @DisplayName("게시글 작성") @@ -66,7 +63,7 @@ void create() throws Exception { VoteType.SINGLE ); String shareUrl = "shareUrl"; - given(shareUrlCryptoService.encrypt(any())) + given(shareUrlShareUrlService.encrypt(any())) .willReturn(shareUrl); //when diff --git a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java index 45f16a28..78f4c59c 100644 --- a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java @@ -71,7 +71,6 @@ void findById() throws Exception { } @Test - @Disabled @DisplayName("유저가 작성한 게시글 조회 - 커서 null인 경우") void findUserPosts() throws Exception { //given @@ -91,7 +90,6 @@ void findUserPosts() throws Exception { } @Test - @Disabled @DisplayName("유저가 작성한 게시글 조회 - 커서 있는 경우") void findUserPosts2() throws Exception { //given diff --git a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java b/src/test/java/com/swyp8team2/post/application/ShareUrlServiceTest.java similarity index 64% rename from src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java rename to src/test/java/com/swyp8team2/post/application/ShareUrlServiceTest.java index 89b9fd1e..64549dfc 100644 --- a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/ShareUrlServiceTest.java @@ -1,23 +1,20 @@ -package com.swyp8team2.crypto.application; +package com.swyp8team2.post.application; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -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.security.crypto.encrypt.AesBytesEncryptor; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; -class CryptoServiceTest { +class ShareUrlServiceTest { - CryptoService cryptoService; + ShareUrlService shareUrlService; @BeforeEach void setUp() throws Exception { - cryptoService = new CryptoService(new AesBytesEncryptor("asdfd", "1541235432")); + shareUrlService = new ShareUrlService("asdfd", "1541235432"); } @Test @@ -27,9 +24,9 @@ void encryptAndDecrypt() { String plainText = "15411"; // when - String encryptedText = cryptoService.encrypt(plainText); + String encryptedText = shareUrlService.encrypt(plainText); System.out.println("encryptedText = " + encryptedText); - String decryptedText = cryptoService.decrypt(encryptedText); + String decryptedText = shareUrlService.decrypt(encryptedText); // then assertThat(decryptedText).isEqualTo(plainText); @@ -40,11 +37,11 @@ void encryptAndDecrypt() { void encryptAndDecrypt_differentKey() throws Exception { // given String plainText = "Hello, World!"; - CryptoService differentCryptoService = new CryptoService(new AesBytesEncryptor("different", "234562")); - String encryptedText = differentCryptoService.encrypt(plainText); + ShareUrlService differentShareUrlService = new ShareUrlService("different", "234562"); + String encryptedText = differentShareUrlService.encrypt(plainText); // when then - assertThatThrownBy(() -> cryptoService.decrypt(encryptedText)) + assertThatThrownBy(() -> shareUrlService.decrypt(encryptedText)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } @@ -56,7 +53,7 @@ void decrypt_invalidToken() { String invalid = "invalidToken"; // when then - assertThatThrownBy(() -> cryptoService.decrypt(invalid)) + assertThatThrownBy(() -> shareUrlService.decrypt(invalid)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } @@ -68,7 +65,7 @@ void decrypt_emptyString() { String invalid = ""; // when then - assertThatThrownBy(() -> cryptoService.decrypt(invalid)) + assertThatThrownBy(() -> shareUrlService.decrypt(invalid)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } From 44d8b17cb7e20fc92c5913b1662de2e302e7fd92 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 10:12:58 +0900 Subject: [PATCH 68/95] =?UTF-8?q?fix:=20scope=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/post/application/PostService.java | 2 +- src/main/java/com/swyp8team2/post/domain/Post.java | 4 ++-- .../swyp8team2/post/presentation/dto/CreatePostRequest.java | 4 ++++ .../com/swyp8team2/post/presentation/dto/FeedResponse.java | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 6b504a96..a08f58a2 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -58,7 +58,7 @@ public PostService( @Transactional public CreatePostResponse create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); - Post post = Post.create(userId, request.description(), postImages, request.voteType()); + Post post = Post.create(userId, request.description(), postImages, request.scope(), request.voteType()); Post save = postRepository.save(post); save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); return new CreatePostResponse(save.getId(), save.getShareUrl()); diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 4c5e37b2..74bc69f8 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -84,8 +84,8 @@ private void validateDescription(String description) { } } - public static Post create(Long userId, String description, List images, VoteType voteType) { - return new Post(null, userId, description, Status.PROGRESS, Scope.PRIVATE, images, null, voteType); + public static Post create(Long userId, String description, List images, Scope scope, VoteType voteType) { + return new Post(null, userId, description, Status.PROGRESS, scope, images, null, voteType); } public PostImage getBestPickedImage() { diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java index 0f25813c..5697ae28 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java @@ -1,5 +1,6 @@ package com.swyp8team2.post.presentation.dto; +import com.swyp8team2.post.domain.Scope; import com.swyp8team2.post.domain.VoteType; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -13,6 +14,9 @@ public record CreatePostRequest( @Valid @NotNull List images, + @NotNull + Scope scope, + @NotNull VoteType voteType ) { diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java new file mode 100644 index 00000000..65e2428b --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java @@ -0,0 +1,2 @@ +package com.swyp8team2.post.presentation.dto;public record FeedResponse() { +} From 5093db84efc00566ee0d8b596ea3ad2995af6769 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 10:13:59 +0900 Subject: [PATCH 69/95] =?UTF-8?q?chore:=20=EC=83=98=ED=94=8C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20scope=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/common/dev/DataInitializer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 903803ed..dca7dd85 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -14,6 +14,7 @@ import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.Scope; import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.user.domain.NicknameAdjective; import com.swyp8team2.user.domain.NicknameAdjectiveRepository; @@ -82,7 +83,7 @@ public void init() { for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), VoteType.SINGLE)); + Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), Scope.PUBLIC, VoteType.SINGLE)); post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); posts.add(post); } From 9dd75f51044211fe6ad3e33fc863aa70dd1015a3 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 10:15:51 +0900 Subject: [PATCH 70/95] =?UTF-8?q?test:=20scope=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/post/application/PostServiceTest.java | 9 ++++----- src/test/java/com/swyp8team2/post/domain/PostTest.java | 6 +++--- .../swyp8team2/post/presentation/PostControllerTest.java | 8 +++++--- .../com/swyp8team2/support/fixture/FixtureGenerator.java | 3 +++ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index bf7b2a96..e8ab85ed 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -6,11 +6,7 @@ import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; -import com.swyp8team2.post.domain.Status; -import com.swyp8team2.post.domain.VoteType; +import com.swyp8team2.post.domain.*; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostResponse; @@ -74,6 +70,7 @@ void create() throws Exception { new PostImageRequestDto(1L), new PostImageRequestDto(2L) ), + Scope.PRIVATE, VoteType.SINGLE ); String shareUrl = "shareUrl"; @@ -112,6 +109,7 @@ void create_invalidPostImageCount() throws Exception { List.of( new PostImageRequestDto(1L) ), + Scope.PRIVATE, VoteType.SINGLE ); //when then @@ -131,6 +129,7 @@ void create_descriptionCountExceeded() throws Exception { new PostImageRequestDto(1L), new PostImageRequestDto(2L) ), + Scope.PRIVATE, VoteType.SINGLE ); diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index 9539c1eb..734e06fc 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -25,7 +25,7 @@ void create() throws Exception { ); //when - Post post = Post.create(userId, description, postImages, VoteType.SINGLE); + Post post = Post.create(userId, description, postImages, Scope.PRIVATE, VoteType.SINGLE); //then List images = post.getImages(); @@ -52,7 +52,7 @@ void create_invalidPostImageCount() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, "description", postImages, VoteType.SINGLE)) + assertThatThrownBy(() -> Post.create(1L, "description", postImages, Scope.PRIVATE, VoteType.SINGLE)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); } @@ -68,7 +68,7 @@ void create_descriptionCountExceeded() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, description, postImages, VoteType.SINGLE)) + assertThatThrownBy(() -> Post.create(1L, description, postImages, Scope.PRIVATE, VoteType.SINGLE)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index a69c4382..7683353d 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -1,7 +1,7 @@ package com.swyp8team2.post.presentation; -import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.post.domain.Scope; import com.swyp8team2.post.domain.Status; import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.post.presentation.dto.*; @@ -23,14 +23,12 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -45,6 +43,7 @@ void createPost() throws Exception { CreatePostRequest request = new CreatePostRequest( "제목", List.of(new PostImageRequestDto(1L), new PostImageRequestDto(2L)), + Scope.PRIVATE, VoteType.SINGLE ); CreatePostResponse response = new CreatePostResponse(1L, "shareUrl"); @@ -72,6 +71,9 @@ void createPost() throws Exception { fieldWithPath("images[].imageFileId") .type(JsonFieldType.NUMBER) .description("투표 후보 이미지 ID"), + fieldWithPath("scope") + .type(JsonFieldType.STRING) + .description("투표 공개범위 (PRIVATE, PUBLIC)"), fieldWithPath("voteType") .type(JsonFieldType.STRING) .description("투표 방식 (SINGLE, MULTIPLE)") diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index 4ee291bd..6c1032b5 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -4,6 +4,7 @@ import com.swyp8team2.image.presentation.dto.ImageFileDto; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.Scope; import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.user.domain.User; @@ -19,6 +20,7 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) ), + Scope.PRIVATE, VoteType.SINGLE ); } @@ -31,6 +33,7 @@ public static Post createMultiplePost(Long userId, ImageFile imageFile1, ImageFi PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) ), + Scope.PRIVATE, VoteType.MULTIPLE ); } From 87444cb176fed4226e8cd4c0c653a0398d586ddd Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 13:22:21 +0900 Subject: [PATCH 71/95] =?UTF-8?q?test:=20=EC=83=98=ED=94=8C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=97=90=20scope=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 5 +++ .../comment/domain/CommentRepository.java | 3 ++ .../post/application/PostService.java | 20 ++++++++++ .../post/domain/PostRepository.java | 11 +++++ .../post/presentation/PostController.java | 17 +++++++- .../post/presentation/dto/FeedResponse.java | 40 ++++++++++++++++++- .../vote/domain/VoteRepository.java | 3 ++ .../support/fixture/FixtureGenerator.java | 8 ++-- .../vote/application/VoteServiceTest.java | 10 ++--- 9 files changed, 106 insertions(+), 11 deletions(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 42f83565..6cf03008 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -53,3 +53,8 @@ operation::post-controller-test/close-post[snippets='http-request,curl-request,p === `DELETE` 게시글 삭제 operation::post-controller-test/delete-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] + +[[피드-조회]] +=== `GET` 피드 조회 + +operation::post-controller-test/find-feed[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] \ No newline at end of file diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java index 5b6a5af4..6f0e5d47 100644 --- a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -33,4 +34,6 @@ Slice findByPostId( AND c.deleted = false """) Optional findByIdAndNotDeleted(@Param("commentId") Long commentId); + + List findByPostIdAndDeletedFalse(Long postId); } diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index a08f58a2..65adc9e1 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,5 +1,7 @@ package com.swyp8team2.post.application; +import com.swyp8team2.comment.domain.Comment; +import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; @@ -17,6 +19,7 @@ import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.PostImageResponse; +import com.swyp8team2.post.presentation.dto.FeedResponse; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -37,6 +40,7 @@ public class PostService { private final RatioCalculator ratioCalculator; private final ImageFileRepository imageFileRepository; private final VoteRepository voteRepository; + private final CommentRepository commentRepository; private final CryptoService shareUrlCryptoService; public PostService( @@ -45,6 +49,7 @@ public PostService( RatioCalculator ratioCalculator, ImageFileRepository imageFileRepository, VoteRepository voteRepository, + CommentRepository commentRepository, @ShareUrlCryptoService CryptoService shareUrlCryptoService ) { this.postRepository = postRepository; @@ -52,6 +57,7 @@ public PostService( this.ratioCalculator = ratioCalculator; this.imageFileRepository = imageFileRepository; this.voteRepository = voteRepository; + this.commentRepository = commentRepository; this.shareUrlCryptoService = shareUrlCryptoService; } @@ -186,4 +192,18 @@ public void toggleScope(Long userId, Long postId) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.toggleScope(userId); } + + public CursorBasePaginatedResponse findFeed(Long userId, Long cursor, int size) { + Slice postSlice = postRepository.findByScopeAndDeletedFalse(userId, cursor, PageRequest.ofSize(size)); + return CursorBasePaginatedResponse.of(postSlice.map(post -> createFeedResponse(userId, post))); + } + + private FeedResponse createFeedResponse(Long userId, Post post) { + List images = createPostImageResponse(userId, post); + List votes = voteRepository.findByPostIdAndDeletedFalse(post.getId()); + List comments = commentRepository.findByPostIdAndDeletedFalse(post.getId()); + boolean isAuthor = post.getUserId().equals(userId); + + return FeedResponse.of(post, images, votes.size(), comments.size(), isAuthor); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java index 0653564c..fedfeccd 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -31,4 +31,15 @@ public interface PostRepository extends JpaRepository { """ ) Slice findByIdIn(@Param("postIds") List postIds, @Param("postId") Long postId, Pageable pageable); + + @Query(""" + SELECT p + FROM Post p + WHERE p.deleted = false + AND (p.scope = 'PUBLIC' OR (p.userId = :userId AND p.scope = 'PRIVATE')) + AND (:postId IS NULL OR p.id < :postId) + ORDER BY p.createdAt DESC + """ + ) + Slice findByScopeAndDeletedFalse(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 067c1fe2..93bbca69 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -3,7 +3,13 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.application.PostService; -import com.swyp8team2.post.presentation.dto.*; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; +import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; +import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.UpdatePostRequest; +import com.swyp8team2.post.presentation.dto.SimplePostResponse; +import com.swyp8team2.post.presentation.dto.FeedResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; @@ -119,4 +125,13 @@ public ResponseEntity> findVoted ) { return ResponseEntity.ok(postService.findVotedPosts(userId, cursor, size)); } + + @GetMapping("/feed") + public ResponseEntity> findFeed( + @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, + @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok(postService.findFeed(userInfo.userId(), cursor, size)); + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java index 65e2428b..e8caff59 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java @@ -1,2 +1,40 @@ -package com.swyp8team2.post.presentation.dto;public record FeedResponse() { +package com.swyp8team2.post.presentation.dto; + +import com.swyp8team2.common.dto.CursorDto; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.Status; + +import java.util.List; + +public record FeedResponse( + Long id, + List images, + Status status, + String description, + boolean isAuthor, + int participantCount, + int commentCount + +) implements CursorDto { + + public static FeedResponse of(Post post, + List images, + int participantCount, + int commentCount, + boolean isAuthor) { + return new FeedResponse( + post.getId(), + images, + post.getStatus(), + post.getDescription(), + isAuthor, + participantCount, + commentCount + ); + } + + @Override + public long getId() { + return id; + } } diff --git a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java index 8bea21a1..8634624c 100644 --- a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java +++ b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -15,4 +16,6 @@ public interface VoteRepository extends JpaRepository { Optional findByUserIdAndPostImageId(Long voterId, Long imageId); Optional findByIdAndUserId(Long voteId, Long userId); + + List findByPostIdAndDeletedFalse(Long postId); } diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index 6c1032b5..f6e2ee61 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -12,7 +12,7 @@ public abstract class FixtureGenerator { - public static Post createPost(Long userId, ImageFile imageFile1, ImageFile imageFile2, int key) { + public static Post createPost(Long userId, Scope scope, ImageFile imageFile1, ImageFile imageFile2, int key) { return Post.create( userId, "description" + key, @@ -20,12 +20,12 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) ), - Scope.PRIVATE, + scope, VoteType.SINGLE ); } - public static Post createMultiplePost(Long userId, ImageFile imageFile1, ImageFile imageFile2, int key) { + public static Post createMultiplePost(Long userId, Scope scope, ImageFile imageFile1, ImageFile imageFile2, int key) { return Post.create( userId, "description" + key, @@ -33,7 +33,7 @@ public static Post createMultiplePost(Long userId, ImageFile imageFile1, ImageFi PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) ), - Scope.PRIVATE, + scope, VoteType.MULTIPLE ); } diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index 71f2d1b9..ef1e50a1 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -48,7 +48,7 @@ void singleVote() { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); // when Long voteId = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); @@ -71,7 +71,7 @@ void singleVote_change() { User user = userRepository.save(createUser(2)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); // when @@ -96,7 +96,7 @@ void multipleVote() { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createMultiplePost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createMultiplePost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); // when Long voteId1 = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); @@ -154,7 +154,7 @@ void cancelVote() { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); Long voteId = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); // when @@ -176,7 +176,7 @@ void cancelVote_notVoter() { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); Long voteId = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); // when then From be1b292fee7f9cb2815b2424dbcf8d156efbca22 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 13:22:50 +0900 Subject: [PATCH 72/95] =?UTF-8?q?test:=20=ED=94=BC=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostServiceTest.java | 38 +++++++++---- .../post/domain/PostRepositoryTest.java | 29 ++++++++-- .../post/presentation/PostControllerTest.java | 53 +++++++++++++++++++ 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index e8ab85ed..79338644 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -146,7 +146,7 @@ void findById() throws Exception { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); //when PostResponse response = postService.findById(user.getId(), post.getId()); @@ -210,7 +210,7 @@ private List createPosts(User user) { for (int i = 0; i < 30; i += 2) { ImageFile imageFile1 = imageFileRepository.save(createImageFile(i)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(i + 1)); - posts.add(postRepository.save(createPost(user.getId(), imageFile1, imageFile2, i))); + posts.add(postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, i))); } return posts; } @@ -246,7 +246,7 @@ void findVoteStatus() throws Exception { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); //when @@ -274,7 +274,7 @@ void findVoteStatus_voteUser() throws Exception { User voter = userRepository.save(createUser(2)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(author.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); voteService.vote(voter.getId(), post.getId(), post.getImages().get(0).getId()); //when @@ -301,7 +301,7 @@ void findVoteStatus_notAuthorAndVoter() throws Exception { User author = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(author.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(author.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); //when assertThatThrownBy(() -> postService.findVoteStatus(2L, post.getId())) @@ -316,7 +316,7 @@ void close() throws Exception { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); //when post.close(user.getId()); @@ -333,7 +333,7 @@ void close_notPostAuthor() throws Exception { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); //when then assertThatThrownBy(() -> post.close(2L)) @@ -348,7 +348,7 @@ void close_alreadyClosed() throws Exception { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); post.close(user.getId()); //when then @@ -375,7 +375,7 @@ void delete() throws Exception { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); //when postService.delete(user.getId(), post.getId()); @@ -383,4 +383,24 @@ void delete() throws Exception { //then assertThat(postRepository.findById(post.getId())).isEmpty(); } + + @Test + @DisplayName("피드 조회") + void findFeed() throws Exception { + //given + User user = userRepository.save(createUser(1)); + List myPosts = createPosts(user); + List privatePosts = createPosts(userRepository.save(createUser(2))); + List publicPosts = createPosts(userRepository.save(createUser(2))); + int size = 10; + + //when + var response = postService.findFeed(user.getId(), null, size); + + //then + assertAll( + () -> assertThat(response.data()).hasSize(size), + () -> assertThat(response.hasNext()).isTrue() + ); + } } diff --git a/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java index e5b5157b..bbbf47c3 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java @@ -26,7 +26,7 @@ class PostRepositoryTest extends RepositoryTest { void select_post_findByUserId1() throws Exception { //given long userId = 1L; - List posts = createPosts(userId); + List posts = createPosts(userId, Scope.PRIVATE); int size = 10; //when @@ -47,7 +47,7 @@ void select_post_findByUserId1() throws Exception { void select_post_findByUserId2() throws Exception { //given long userId = 1L; - List posts = createPosts(userId); + List posts = createPosts(userId, Scope.PRIVATE); int size = 10; int cursorIndex = 5; @@ -68,7 +68,7 @@ void select_post_findByUserId2() throws Exception { @DisplayName("id 리스트에 포함되는 게시글 조회") void select_post_findByIdIn() throws Exception { //given - List posts = createPosts(1L); + List posts = createPosts(1L, Scope.PRIVATE); List postIds = List.of(posts.get(0).getId(), posts.get(1).getId(), posts.get(2).getId()); //when @@ -84,13 +84,32 @@ void select_post_findByIdIn() throws Exception { ); } - private List createPosts(long userId) { + private List createPosts(long userId, Scope scope) { List posts = new ArrayList<>(); for (int i = 0; i < 30; i += 2) { ImageFile imageFile1 = createImageFile(i); ImageFile imageFile2 = createImageFile(i + 1); - posts.add(postRepository.save(createPost(userId, imageFile1, imageFile2, i))); + posts.add(postRepository.save(createPost(userId, scope, imageFile1, imageFile2, i))); } return posts; } + + @Test + @DisplayName("피드 조회") + void select_post_findByScopeAndDeletedFalse() { + //given + List myPosts = createPosts(1L, Scope.PRIVATE); + List privatePosts = createPosts(2L, Scope.PRIVATE); + List publicPosts = createPosts(2L, Scope.PUBLIC); + int size = 10; + + //when + Slice res = postRepository.findByScopeAndDeletedFalse(1L, null, PageRequest.ofSize(size)); + + //then + assertAll( + () -> assertThat(res.getContent().size()).isEqualTo(size), + () -> assertThat(res.hasNext()).isTrue() + ); + } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 7683353d..6482ccb3 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -427,4 +427,57 @@ void closePost() throws Exception { )); verify(postService, times(1)).close(any(), any()); } + + @Test + @WithMockUserInfo + @DisplayName("피드 조회") + void findFeed() throws Exception { + //given + var response = new CursorBasePaginatedResponse<> ( + 1L, + false, + List.of( + new FeedResponse( + 1L, + List.of( + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", 1L), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", null) + ), + Status.PROGRESS, + "description", + true, + 1, + 2 + ) + ) + ); + given(postService.findFeed(1L, null, 10)).willReturn(response); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/feed") + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + queryParameters(cursorQueryParams()), + responseFields( + fieldWithPath("nextCursor").type(JsonFieldType.NUMBER).optional().description("다음 조회 커서 값"), + fieldWithPath("hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부 (기본 값 10)"), + fieldWithPath("data[]").type(JsonFieldType.ARRAY).description("게시글 데이터"), + fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("게시글 Id"), + fieldWithPath("data[].images[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("data[].images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), + fieldWithPath("data[].images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), + fieldWithPath("data[].images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), + fieldWithPath("data[].images[].thumbnailUrl").type(JsonFieldType.STRING).description("나중에 없어질 예정"), + fieldWithPath("data[].images[].voteId").type(JsonFieldType.NUMBER).optional().description("투표 Id (투표 안 한 경우 null)"), + fieldWithPath("data[].status").type(JsonFieldType.STRING).description("게시글 마감 여부 (PROGRESS, CLOSED)"), + fieldWithPath("data[].description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("data[].isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부"), + fieldWithPath("data[].participantCount").type(JsonFieldType.NUMBER).description("투표 참여자 수"), + fieldWithPath("data[].commentCount").type(JsonFieldType.NUMBER).description("투표 댓글 수") + ) + )); + } } From 92ec285f4f54259facccf2a2aaa720df861f695f Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 16:15:11 +0900 Subject: [PATCH 73/95] =?UTF-8?q?test:=20=ED=94=BC=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=EB=8B=A8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostServiceTest.java | 72 +++++++++++++------ .../support/fixture/FixtureGenerator.java | 10 +++ 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 79338644..32928ebe 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -1,17 +1,16 @@ package com.swyp8team2.post.application; +import com.swyp8team2.comment.domain.Comment; +import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.common.annotation.ShareUrlCryptoService; +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.*; -import com.swyp8team2.post.presentation.dto.CreatePostRequest; -import com.swyp8team2.post.presentation.dto.CreatePostResponse; -import com.swyp8team2.post.presentation.dto.PostResponse; -import com.swyp8team2.post.presentation.dto.PostImageRequestDto; -import com.swyp8team2.post.presentation.dto.PostImageResponse; +import com.swyp8team2.post.presentation.dto.*; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -26,12 +25,10 @@ import java.util.ArrayList; import java.util.List; -import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; -import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; -import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; +import static com.swyp8team2.support.fixture.FixtureGenerator.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; @@ -55,6 +52,9 @@ class PostServiceTest extends IntegrationTest { @Autowired VoteService voteService; + @Autowired + CommentRepository commentRepository; + @MockitoBean @ShareUrlCryptoService CryptoService shareUrlCryptoService; @@ -172,7 +172,7 @@ void findById() throws Exception { void findUserPosts() throws Exception { //given User user = userRepository.save(createUser(1)); - List posts = createPosts(user); + List posts = createPosts(user, Scope.PRIVATE); int size = 10; //when @@ -191,7 +191,7 @@ void findUserPosts() throws Exception { void findUserPosts2() throws Exception { //given User user = userRepository.save(createUser(1)); - List posts = createPosts(user); + List posts = createPosts(user, Scope.PRIVATE); int size = 10; //when @@ -205,12 +205,12 @@ void findUserPosts2() throws Exception { ); } - private List createPosts(User user) { + private List createPosts(User user, Scope scope) { List posts = new ArrayList<>(); for (int i = 0; i < 30; i += 2) { ImageFile imageFile1 = imageFileRepository.save(createImageFile(i)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(i + 1)); - posts.add(postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, i))); + posts.add(postRepository.save(createPost(user.getId(), scope, imageFile1, imageFile2, i))); } return posts; } @@ -220,7 +220,7 @@ private List createPosts(User user) { void findVotedPosts() throws Exception { //given User user = userRepository.save(createUser(1)); - List posts = createPosts(user); + List posts = createPosts(user, Scope.PRIVATE); for (int i = 0; i < 15; i++) { Post post = posts.get(i); voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getId())); @@ -385,22 +385,48 @@ void delete() throws Exception { } @Test - @DisplayName("피드 조회") + @DisplayName("피드 조회 - 내 게시글 1개, 공개 게시글 15개, 투표 10개, 댓글 20개") void findFeed() throws Exception { //given - User user = userRepository.save(createUser(1)); - List myPosts = createPosts(user); - List privatePosts = createPosts(userRepository.save(createUser(2))); - List publicPosts = createPosts(userRepository.save(createUser(2))); - int size = 10; + int size = 20; + User user1 = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + + Post myPost = postRepository.save(createPost(user1.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); + List privatePosts = createPosts(userRepository.save(createUser(2)), Scope.PRIVATE); + List publicPosts = createPosts(userRepository.save(createUser(2)), Scope.PUBLIC); + + createVotes(user1, myPost); + createComments(user1, myPost); //when - var response = postService.findFeed(user.getId(), null, size); + List votes = voteRepository.findByPostIdAndDeletedFalse(myPost.getId()); + List comments = commentRepository.findByPostIdAndDeletedFalse(myPost.getId()); + CursorBasePaginatedResponse response = postService.findFeed(user1.getId(), null, size); //then assertAll( - () -> assertThat(response.data()).hasSize(size), - () -> assertThat(response.hasNext()).isTrue() + () -> assertThat(response.data().size()).isEqualTo(16), + () -> assertThat(response.data().getLast().participantCount()).isEqualTo(votes.size()), + () -> assertThat(response.data().getLast().commentCount()).isEqualTo(comments.size()), + () -> assertThat(response.data().getLast().isAuthor()).isTrue(), + () -> assertThat(response.data().getFirst().isAuthor()).isFalse() ); } + + private void createVotes(User user, Post post) { + for (int i = 0; i < 5; i++) { + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + voteRepository.save(createVote(user.getId(), post.getId(), imageFile1.getId())); + voteRepository.save(createVote(user.getId(), post.getId(), imageFile2.getId())); + } + } + + private void createComments(User user, Post post) { + for (int i = 0; i < 20; i++) { + commentRepository.save(createComment(user.getId(), post.getId())); + } + } } diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index f6e2ee61..8544ee7a 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -1,5 +1,6 @@ package com.swyp8team2.support.fixture; +import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.presentation.dto.ImageFileDto; import com.swyp8team2.post.domain.Post; @@ -7,6 +8,7 @@ import com.swyp8team2.post.domain.Scope; import com.swyp8team2.post.domain.VoteType; import com.swyp8team2.user.domain.User; +import com.swyp8team2.vote.domain.Vote; import java.util.List; @@ -51,4 +53,12 @@ public static ImageFile createImageFile(int key) { ) ); } + + public static Vote createVote(Long userId, Long postId, Long imageId) { + return Vote.of(userId, postId, imageId); + } + + public static Comment createComment(Long userId, Long postId) { + return new Comment(userId, postId, "내용"); + } } From dc1dc0d77959346ac266bdd78c301d6778706b89 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 10 Mar 2025 18:40:07 +0900 Subject: [PATCH 74/95] =?UTF-8?q?fix:=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/annotation/ShareUrlCryptoService.java | 16 ---------------- .../swyp8team2/common/dev/DataInitializer.java | 3 +-- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java diff --git a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java deleted file mode 100644 index 82be1bc7..00000000 --- a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.swyp8team2.common.annotation; - -import org.springframework.beans.factory.annotation.Qualifier; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Qualifier(ShareUrlCryptoService.QUALIFIER) -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) -public @interface ShareUrlCryptoService { - - String QUALIFIER = "shareUrlCryptoService"; -} diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index b7567290..d981ffb2 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -6,7 +6,6 @@ import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; -import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.post.application.ShareUrlService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; @@ -46,7 +45,7 @@ public DataInitializer( UserRepository userRepository, ImageFileRepository imageFileRepository, PostRepository postRepository, - @ShareUrlCryptoService ShareUrlService shaereUrlShareUrlService, + ShareUrlService shaereUrlShareUrlService, JwtService jwtService, VoteService voteService, CommentRepository commentRepository From cd1c34669cd2621dc2674fa5d9a9447236535c12 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 21:37:43 +0900 Subject: [PATCH 75/95] =?UTF-8?q?fix:=20=EB=B3=B5=EC=88=98=ED=88=AC?= =?UTF-8?q?=ED=91=9C=20=EC=8B=9C=20=ED=88=AC=ED=91=9C=ED=98=84=ED=99=A9=20?= =?UTF-8?q?=EB=A6=AC=ED=84=B4=EA=B0=92=202=EA=B0=9C=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 10 +++++++--- .../comment/presentation/dto/CommentResponse.java | 5 +++-- .../swyp8team2/vote/application/VoteService.java | 15 +++++++-------- .../swyp8team2/vote/domain/VoteRepository.java | 3 ++- .../comment/application/CommentServiceTest.java | 7 ++++++- .../post/application/PostCommandServiceTest.java | 6 +----- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index 4a307a9c..85147560 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -20,6 +20,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.stream.Collectors; + @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -46,10 +49,11 @@ public CursorBasePaginatedResponse findComments(Long userId, Lo private CommentResponse createCommentResponse(Comment comment, Long userId) { User author = userRepository.findById(comment.getUserNo()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - Long voteImageId = voteRepository.findByUserIdAndPostId(userId, comment.getPostId()) + List votes = voteRepository.findByUserIdAndPostId(userId, comment.getPostId()); + List voteImageIds = votes.stream() .map(Vote::getPostImageId) - .orElse(null); - return CommentResponse.of(comment, author, author.getId().equals(userId), voteImageId); + .collect(Collectors.toList()); + return CommentResponse.of(comment, author, author.getId().equals(userId), voteImageIds); } @Transactional diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index 89848202..4a238493 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -6,12 +6,13 @@ import com.swyp8team2.user.domain.User; import java.time.LocalDateTime; +import java.util.List; public record CommentResponse( Long commentId, String content, AuthorDto author, - Long voteImageId, + List voteImageId, LocalDateTime createdAt, boolean isAuthor ) implements CursorDto { @@ -22,7 +23,7 @@ public long getId() { return commentId; } - public static CommentResponse of(Comment comment, User user, boolean isAuthor, Long voteImageId) { + public static CommentResponse of(Comment comment, User user, boolean isAuthor, List voteImageId) { return new CommentResponse(comment.getId(), comment.getContent(), new AuthorDto(user.getId(), user.getNickname(), user.getProfileUrl()), diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 486afca2..8dc8cf3a 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -50,11 +50,11 @@ public Long vote(Long voterId, Long postId, Long imageId) { } private void deleteVoteIfExisting(Post post, Long userId) { - voteRepository.findByUserIdAndPostId(userId, post.getId()) - .ifPresent(vote -> { - voteRepository.delete(vote); - post.cancelVote(vote.getPostImageId()); - }); + List votes = voteRepository.findByUserIdAndPostId(userId, post.getId()); + for (Vote vote : votes) { + voteRepository.delete(vote); + post.cancelVote(vote.getPostImageId()); + } } private Vote createVote(Post post, Long imageId, Long userId) { @@ -89,9 +89,8 @@ public List findVoteStatus(Long userId, Long postId } private void validateVoteStatus(Long userId, Post post) { - boolean voted = voteRepository.findByUserIdAndPostId(userId, post.getId()) - .isPresent(); - if (!(post.isAuthor(userId) || voted)) { + List votes = voteRepository.findByUserIdAndPostId(userId, post.getId()); + if (!(post.isAuthor(userId) || !votes.isEmpty())) { throw new BadRequestException(ErrorCode.ACCESS_DENIED_VOTE_STATUS); } } diff --git a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java index 8bea21a1..f95590ce 100644 --- a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java +++ b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java @@ -4,11 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface VoteRepository extends JpaRepository { - Optional findByUserIdAndPostId(Long userId, Long postId); + List findByUserIdAndPostId(Long userId, Long postId); Slice findByUserId(Long userId); diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 809d321c..4649e88e 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -12,6 +12,7 @@ import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -77,10 +78,14 @@ void findComments() { Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); User user = new User(100L, "닉네임","http://example.com/profile.png", Role.USER); + List votes = List.of( + Vote.of(1L, 100L, 1L), + Vote.of(1L, 101L, 1L) + ); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); - given(voteRepository.findByUserIdAndPostId(eq(user.getId()), eq(postId))).willReturn(empty()); + given(voteRepository.findByUserIdAndPostId(eq(user.getId()), eq(postId))).willReturn(votes); // 각 댓글마다 user_no=100L 이므로, findById(100L)만 호출됨 given(userRepository.findById(100L)).willReturn(Optional.of(user)); diff --git a/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java index e91fede6..08a09759 100644 --- a/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java @@ -4,11 +4,7 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; -import com.swyp8team2.post.domain.Status; -import com.swyp8team2.post.domain.VoteType; +import com.swyp8team2.post.domain.*; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageRequestDto; From 51ff6796595a39b086e9aa8ad9becdf4c1625f24 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 22:00:41 +0900 Subject: [PATCH 76/95] =?UTF-8?q?fix:=20JPEG=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=A8=EB=84=90=20=EA=B4=80=EB=A0=A8=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image/application/R2Storage.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/image/application/R2Storage.java b/src/main/java/com/swyp8team2/image/application/R2Storage.java index 2ecca142..df824f99 100644 --- a/src/main/java/com/swyp8team2/image/application/R2Storage.java +++ b/src/main/java/com/swyp8team2/image/application/R2Storage.java @@ -19,11 +19,14 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import javax.imageio.IIOImage; import javax.imageio.ImageIO; +import javax.imageio.IIOImage; +import javax.imageio.IIOException; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.FileImageOutputStream; +import java.awt.Graphics2D; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -132,6 +135,31 @@ private File convertToJpg(File sourceFile) throws IOException { writer.write(null, new IIOImage(image, null, null), param); writer.dispose(); return jpgFile; + } catch (IIOException e) { + log.error("Failed to convert image to jpg", e); + + // 알파 채널 처리를 위해 새 RGB 이미지 생성 (알파 채널 제거) + BufferedImage rgbImage = new BufferedImage( + image.getWidth(), + image.getHeight(), + BufferedImage.TYPE_INT_RGB + ); + + // 원본 이미지를 새 RGB 이미지에 그림 + // 흰색 배경 설정 + Graphics2D graphics = rgbImage.createGraphics(); + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, rgbImage.getWidth(), rgbImage.getHeight()); + graphics.drawImage(image, 0, 0, null); + graphics.dispose(); + + try { + ImageIO.write(rgbImage, "jpeg", jpgFile); + return jpgFile; + } catch (IOException io) { + log.error("Error in JPG conversion: {}", io.getMessage()); + throw io; + } } } From 88859b3a35c0e64e2e3c4da424feaa3248a5f670 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 22:21:39 +0900 Subject: [PATCH 77/95] =?UTF-8?q?docs:=20=ED=88=AC=ED=91=9C=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C,=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=AC=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 8 -------- src/docs/asciidoc/votes.adoc | 5 +++++ .../comment/presentation/CommentControllerTest.java | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 42f83565..5558be90 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -16,14 +16,6 @@ operation::post-controller-test/find-post[snippets='http-request,curl-request,pa operation::post-controller-test/find-post_share-url[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] -[[게시글-목록-조회]] - -[[사진-투표-현황-조회]] -=== `GET` 사진 투표 현황 조회 - -operation::post-controller-test/find-vote-status[snippets='http-request,curl-request,request-headers,path-parameters,http-response,response-fields'] - - [[유저가-작성한-게시글-조회]] === `GET` 유저가 작성한 게시글 조회 diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc index aca6fcfd..98fb98bd 100644 --- a/src/docs/asciidoc/votes.adoc +++ b/src/docs/asciidoc/votes.adoc @@ -1,6 +1,11 @@ [[투표-API]] == 투표 API +[[투표-현황-조회]] +=== `GET` 투표 현황 조회 + +operation::vote-controller-test/find-vote-status[snippets='http-request,curl-request,request-headers,path-parameters,http-response,response-fields'] + [[투표]] === `POST` 투표 diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index 53d6fc00..0eda57c8 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -71,7 +71,7 @@ void findComments() throws Exception { 1L, "댓글 내용", new AuthorDto(100L, "닉네임", "http://example.com/profile.png"), - null, + List.of(1L, 2L), LocalDateTime.now(), false ); @@ -120,8 +120,8 @@ void findComments() throws Exception { fieldWithPath("data[].author.profileUrl") .type(JsonFieldType.STRING) .description("작성자 프로필 이미지 url"), - fieldWithPath("data[].voteImageId") - .type(JsonFieldType.NUMBER) + fieldWithPath("data[].voteImageId[]") + .type(JsonFieldType.ARRAY) .optional() .description("작성자가 투표한 이미지 Id (투표 없을 시 null)"), fieldWithPath("data[].createdAt") From 06563e0f5ed731e07bc1b2844d7060e3e02cda29 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Mar 2025 22:26:51 +0900 Subject: [PATCH 78/95] =?UTF-8?q?test:=20=EC=B6=A9=EB=8F=8C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=86=8C=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostCommandServiceTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java index e91fede6..974f0c3e 100644 --- a/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostCommandServiceTest.java @@ -4,11 +4,7 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostImage; -import com.swyp8team2.post.domain.PostRepository; -import com.swyp8team2.post.domain.Status; -import com.swyp8team2.post.domain.VoteType; +import com.swyp8team2.post.domain.*; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageRequestDto; @@ -60,6 +56,7 @@ void create() throws Exception { new PostImageRequestDto(1L), new PostImageRequestDto(2L) ), + Scope.PRIVATE, VoteType.SINGLE ); String shareUrl = "shareUrl"; @@ -98,6 +95,7 @@ void create_invalidPostImageCount() throws Exception { List.of( new PostImageRequestDto(1L) ), + Scope.PRIVATE, VoteType.SINGLE ); //when then @@ -117,6 +115,7 @@ void create_descriptionCountExceeded() throws Exception { new PostImageRequestDto(1L), new PostImageRequestDto(2L) ), + Scope.PRIVATE, VoteType.SINGLE ); From be08b9972af33368ac33b05078c5d4a43c3626b1 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 11 Mar 2025 11:53:18 +0900 Subject: [PATCH 79/95] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=ED=88=AC=ED=91=9C=20=EA=B0=9C=EC=88=98=20=EB=82=B4?= =?UTF-8?q?=EB=A6=BC=EC=B0=A8=EC=88=9C=EC=9C=BC=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/application/VoteService.java | 5 +++- .../support/fixture/FixtureGenerator.java | 13 +++++++++++ .../vote/application/VoteServiceTest.java | 23 ++++++++++++------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 8dc8cf3a..d3f69e12 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -85,7 +86,9 @@ public List findVoteStatus(Long userId, Long postId .map(image -> { String ratio = ratioCalculator.calculate(totalVoteCount, image.getVoteCount()); return new PostImageVoteStatusResponse(image.getId(), image.getName(), image.getVoteCount(), ratio); - }).toList(); + }) + .sorted(Comparator.comparingInt(PostImageVoteStatusResponse::voteCount).reversed()) + .toList(); } private void validateVoteStatus(Long userId, Post post) { diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index 8544ee7a..2a3ebe58 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -11,6 +11,7 @@ import com.swyp8team2.vote.domain.Vote; import java.util.List; +import java.util.stream.Collectors; public abstract class FixtureGenerator { @@ -27,6 +28,18 @@ public static Post createPost(Long userId, Scope scope, ImageFile imageFile1, Im ); } + public static Post createPost(Long userId, Scope scope, List imageFiles, int key) { + return Post.create( + userId, + "description" + key, + imageFiles.stream() + .map(imageFile -> PostImage.create("뽀또"+key, imageFile.getId())) + .toList(), + scope, + VoteType.SINGLE + ); + } + public static Post createMultiplePost(Long userId, Scope scope, ImageFile imageFile1, ImageFile imageFile2, int key) { return Post.create( userId, diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index 9b524d3e..c05f5dca 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -192,23 +192,30 @@ void findVoteStatus() throws Exception { User user = userRepository.save(createUser(1)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); - voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + ImageFile imageFile3 = imageFileRepository.save(createImageFile(3)); + Post post = postRepository.save(createPost(user.getId(), Scope.PRIVATE, List.of(imageFile1, imageFile2, imageFile3), 1)); + voteService.vote(user.getId(), post.getId(), post.getImages().get(1).getId()); //when var response = voteService.findVoteStatus(user.getId(), post.getId()); //then assertAll( - () -> assertThat(response).hasSize(2), - () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), - () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), + () -> assertThat(response).hasSize(3), + () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(1).getId()), + () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(1).getName()), () -> assertThat(response.get(0).voteCount()).isEqualTo(1), () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), - () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), - () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), + + () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(0).getId()), + () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(0).getName()), () -> assertThat(response.get(1).voteCount()).isEqualTo(0), - () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") + () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0"), + + () -> assertThat(response.get(2).id()).isEqualTo(post.getImages().get(2).getId()), + () -> assertThat(response.get(2).imageName()).isEqualTo(post.getImages().get(2).getName()), + () -> assertThat(response.get(2).voteCount()).isEqualTo(0), + () -> assertThat(response.get(2).voteRatio()).isEqualTo("0.0") ); } From 4bb4daf82e71e972023db0085bcd34b94260f903 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 11 Mar 2025 19:06:57 +0900 Subject: [PATCH 80/95] =?UTF-8?q?feat:=20=EC=9E=91=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/post/application/PostQueryService.java | 4 +++- .../com/swyp8team2/post/presentation/dto/FeedResponse.java | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostQueryService.java b/src/main/java/com/swyp8team2/post/application/PostQueryService.java index 41ae7628..64e26253 100644 --- a/src/main/java/com/swyp8team2/post/application/PostQueryService.java +++ b/src/main/java/com/swyp8team2/post/application/PostQueryService.java @@ -128,11 +128,13 @@ public CursorBasePaginatedResponse findFeed(Long userId, Long curs } private FeedResponse createFeedResponse(Long userId, Post post) { + User user = userRepository.findById(post.getUserId()) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); List images = createPostImageResponse(userId, post); List votes = voteRepository.findByPostIdAndDeletedFalse(post.getId()); List comments = commentRepository.findByPostIdAndDeletedFalse(post.getId()); boolean isAuthor = post.getUserId().equals(userId); - return FeedResponse.of(post, images, votes.size(), comments.size(), isAuthor); + return FeedResponse.of(post, user, images, votes.size(), comments.size(), isAuthor); } } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java index e8caff59..14262714 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java @@ -3,11 +3,13 @@ import com.swyp8team2.common.dto.CursorDto; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.Status; +import com.swyp8team2.user.domain.User; import java.util.List; public record FeedResponse( Long id, + AuthorDto author, List images, Status status, String description, @@ -18,12 +20,14 @@ public record FeedResponse( ) implements CursorDto { public static FeedResponse of(Post post, + User user, List images, int participantCount, int commentCount, boolean isAuthor) { return new FeedResponse( post.getId(), + AuthorDto.of(user), images, post.getStatus(), post.getDescription(), From 8033940c48bb0bd410815457908cedde52a0acd1 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 11 Mar 2025 19:07:08 +0900 Subject: [PATCH 81/95] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/PostQueryServiceTest.java | 31 +++++++++++++++++-- .../post/presentation/PostControllerTest.java | 9 ++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java index e2c3c64f..35f9f8f4 100644 --- a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java @@ -3,11 +3,11 @@ import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.post.domain.Post; -import com.swyp8team2.post.domain.PostRepository; -import com.swyp8team2.post.domain.Scope; +import com.swyp8team2.post.domain.*; import com.swyp8team2.post.presentation.dto.FeedResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.PostImageResponse; @@ -26,6 +26,7 @@ import static com.swyp8team2.support.fixture.FixtureGenerator.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; class PostQueryServiceTest extends IntegrationTest { @@ -179,6 +180,30 @@ void findFeed() throws Exception { ); } + @Test + @DisplayName("피드 조회 - 존재하지 않는 유저") + void findFeed_userNotFound() throws Exception { + //given + User user1 = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post myPost = postRepository.save(createPost(user1.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); + postRepository.save(Post.create( + 2L, + "설명", + List.of( + PostImage.create("1", imageFile1.getId()), + PostImage.create("1", imageFile1.getId()) + ), + Scope.PUBLIC, + VoteType.SINGLE)); + + //when then + assertThatThrownBy(() -> postService.findFeed(1L, null, 10)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.USER_NOT_FOUND.getMessage()); + } + private void createVotes(User user, Post post) { for (int i = 0; i < 5; i++) { ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index bc274ceb..b3a12e3a 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -407,6 +407,11 @@ void findFeed() throws Exception { List.of( new FeedResponse( 1L, + new AuthorDto( + 1L, + "author", + "https://image.photopic.site/profile-image" + ), List.of( new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", 1L), new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", null) @@ -434,6 +439,10 @@ void findFeed() throws Exception { fieldWithPath("hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부 (기본 값 10)"), fieldWithPath("data[]").type(JsonFieldType.ARRAY).description("게시글 데이터"), fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("게시글 Id"), + fieldWithPath("data[].author").type(JsonFieldType.OBJECT).description("게시글 작성자 정보"), + fieldWithPath("data[].author.id").type(JsonFieldType.NUMBER).description("게시글 작성자 유저 ID"), + fieldWithPath("data[].author.nickname").type(JsonFieldType.STRING).description("게시글 작성자 닉네임"), + fieldWithPath("data[].author.profileUrl").type(JsonFieldType.STRING).description("게시글 작성자 프로필 이미지"), fieldWithPath("data[].images[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), fieldWithPath("data[].images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), fieldWithPath("data[].images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), From efaf49d67694c8f612e6b05a61068b20f878806f Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 11 Mar 2025 19:29:35 +0900 Subject: [PATCH 82/95] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20URL=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/post/presentation/dto/FeedResponse.java | 2 ++ .../com/swyp8team2/post/presentation/PostControllerTest.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java index 14262714..bb48017f 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java @@ -13,6 +13,7 @@ public record FeedResponse( List images, Status status, String description, + String shareUrl, boolean isAuthor, int participantCount, int commentCount @@ -31,6 +32,7 @@ public static FeedResponse of(Post post, images, post.getStatus(), post.getDescription(), + post.getShareUrl(), isAuthor, participantCount, commentCount diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index b3a12e3a..adcd3fb9 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -418,6 +418,7 @@ void findFeed() throws Exception { ), Status.PROGRESS, "description", + "https://photopic.site/shareurl", true, 1, 2 @@ -451,6 +452,7 @@ void findFeed() throws Exception { fieldWithPath("data[].images[].voteId").type(JsonFieldType.NUMBER).optional().description("투표 Id (투표 안 한 경우 null)"), fieldWithPath("data[].status").type(JsonFieldType.STRING).description("게시글 마감 여부 (PROGRESS, CLOSED)"), fieldWithPath("data[].description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("data[].shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("data[].isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부"), fieldWithPath("data[].participantCount").type(JsonFieldType.NUMBER).description("투표 참여자 수"), fieldWithPath("data[].commentCount").type(JsonFieldType.NUMBER).description("투표 댓글 수") From 9db088e574e6b1f3527076f56b26d486c89e8b19 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 11 Mar 2025 19:57:09 +0900 Subject: [PATCH 83/95] =?UTF-8?q?docs:=20shareUrl=20=ED=98=95=EC=8B=9D=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 --- .../com/swyp8team2/post/presentation/PostControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index adcd3fb9..45282ff4 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -418,7 +418,7 @@ void findFeed() throws Exception { ), Status.PROGRESS, "description", - "https://photopic.site/shareurl", + "anioefw78f329jcs9", true, 1, 2 From 56ce05508987a9c9c94a4ca6b715d9e7c408e0ff Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 12 Mar 2025 15:22:39 +0900 Subject: [PATCH 84/95] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20invalid=20to?= =?UTF-8?q?ken=20=EC=98=88=EC=99=B8=20=EB=82=98=EC=98=A4=EA=B2=8C=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 --- .../java/com/swyp8team2/auth/application/jwt/JwtProvider.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java index 033bb733..96121e2f 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java @@ -87,11 +87,9 @@ public JwtClaim parseToken(String token) { } catch (ExpiredJwtException e) { log.trace("Expired Jwt Token: {}", e.getMessage()); throw new UnauthorizedException(ErrorCode.EXPIRED_TOKEN); - } catch (JwtException e) { + } catch (Exception e) { log.trace("Invalid Jwt Token: {}", e.getMessage()); throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); - } catch (Exception e) { - throw new InternalServerException(e); } } } From 77af609f45b67771d7338c465a2bb6c9631fb634 Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 12 Mar 2025 16:56:42 +0900 Subject: [PATCH 85/95] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/index.html | 52 ---------------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/main/resources/static/index.html diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html deleted file mode 100644 index ab7a06bc..00000000 --- a/src/main/resources/static/index.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - 멀티 파일 업로드 테스트 - - -

여러 파일 업로드 테스트

- - - - - - - From 2b95efb7d57a6ee88c3a783443a68437b8dad61a Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 12 Mar 2025 17:29:37 +0900 Subject: [PATCH 86/95] =?UTF-8?q?fix:=20=EA=B8=B0=EB=B3=B8=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/user/domain/User.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index e4772bdc..ff9f52ed 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -16,7 +16,7 @@ @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) public class User extends BaseEntity { - public static final String DEFAULT_PROFILE_URL = "https://image.photopic.site/images-dev/resized_202502240006030.png"; + public static final String DEFAULT_PROFILE_URL = "https://image.photopic.site/default_profile.png"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From d6112ee7ee71d0702767c5e6ad8f726b907b11dc Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 12 Mar 2025 17:31:00 +0900 Subject: [PATCH 87/95] =?UTF-8?q?chore:=20hibernate=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=EC=95=88=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 6def491a..d8edc244 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 6def491ab548ae5aaab21ddf185147c469b33fa7 +Subproject commit d8edc2441ac8e617fc2dea4483bbd28c439c5277 From ad022fc6a5a7ea3d5fa1cbc99b0e689ad9cc4469 Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 12 Mar 2025 18:47:13 +0900 Subject: [PATCH 88/95] =?UTF-8?q?chore:=20=EC=9C=A0=ED=9C=B4=20=EC=BB=A4?= =?UTF-8?q?=EB=84=A5=EC=85=98,=20=EC=B5=9C=EB=8C=80=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index d8edc244..526c6e2d 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit d8edc2441ac8e617fc2dea4483bbd28c439c5277 +Subproject commit 526c6e2dbc39eb60357eefe04b97b3b9fdbb0802 From 817d1edba88b4232c2bc6d94f016dd7edf71f0bd Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 12 Mar 2025 21:22:34 +0900 Subject: [PATCH 89/95] =?UTF-8?q?fix:=20logFilter=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=95=88=20=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20api=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/filter/JwtAuthFilter.java | 3 + .../presentation/HttpLoggingFilter.java | 3 + .../post/presentation/PostController.java | 11 ---- .../post/presentation/PostControllerTest.java | 56 +------------------ 4 files changed, 7 insertions(+), 66 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index bcc2dc21..5829e4b2 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -32,14 +32,17 @@ public class JwtAuthFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { + log.debug("JwtAuthFilter.doFilterInternal start"); String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); JwtClaim claim = jwtProvider.parseToken(headerTokenExtractor.extractToken(authorization)); Authentication authentication = getAuthentication(claim); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (ApplicationException e) { + log.debug("JwtAuthFilter.doFilterInternal application exception {}", e.getMessage()); request.setAttribute(EXCEPTION_KEY, e); } finally { + log.debug("JwtAuthFilter.doFilterInternal end"); doFilter(request, response, filterChain); } } diff --git a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java index 1794ab22..c5ee8834 100644 --- a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java +++ b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java @@ -10,6 +10,8 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -29,6 +31,7 @@ import java.util.stream.Stream; @Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) @Component public class HttpLoggingFilter extends OncePerRequestFilter { diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 9a1506ad..489431f5 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -40,17 +40,6 @@ public ResponseEntity createPost( return ResponseEntity.ok(postService.create(userInfo.userId(), request)); } - @GetMapping("/{postId}") - public ResponseEntity findPost( - @PathVariable("postId") Long postId, - @AuthenticationPrincipal UserInfo userInfo - ) { - Long userId = Optional.ofNullable(userInfo) - .map(UserInfo::userId) - .orElse(null); - return ResponseEntity.ok(postService.findById(userId, postId)); - } - @GetMapping("/shareUrl/{shareUrl}") public ResponseEntity findPostByShareUrl( @PathVariable("shareUrl") String shareUrl, diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 45282ff4..28dea4e8 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -88,61 +88,7 @@ void createPost() throws Exception { ) )); } - - @Test - @WithAnonymousUser - @DisplayName("게시글 상세 조회") - void findPost() throws Exception { - //given - PostResponse response = new PostResponse( - 1L, - new AuthorDto( - 1L, - "author", - "https://image.photopic.site/profile-image" - ), - "description", - List.of( - new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", 1L), - new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", null) - ), - "https://photopic.site/shareurl", - true, - Status.PROGRESS, - LocalDateTime.of(2025, 2, 13, 12, 0) - ); - given(postService.findById(any(), any())) - .willReturn(response); - - //when then - mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{postId}", 1)) - .andExpect(status().isOk()) - .andExpect(content().json(objectMapper.writeValueAsString(response))) - .andDo(restDocs.document( - pathParameters( - parameterWithName("postId").description("게시글 Id") - ), - responseFields( - fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 Id"), - fieldWithPath("author").type(JsonFieldType.OBJECT).description("게시글 작성자 정보"), - fieldWithPath("author.id").type(JsonFieldType.NUMBER).description("게시글 작성자 유저 Id"), - fieldWithPath("author.nickname").type(JsonFieldType.STRING).description("게시글 작성자 닉네임"), - fieldWithPath("author.profileUrl").type(JsonFieldType.STRING).description("게시글 작성자 프로필 이미지"), - fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), - fieldWithPath("images[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), - fieldWithPath("images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), - fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), - fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), - fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), - fieldWithPath("images[].voteId").type(JsonFieldType.NUMBER).optional().description("투표 Id (투표 안 한 경우 null)"), - fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), - fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), - fieldWithPath("status").type(JsonFieldType.STRING).description("게시글 마감 여부 (PROGRESS, CLOSED)"), - fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") - ) - )); - } - + @Test @WithAnonymousUser @DisplayName("게시글 공유 url 상세 조회") From b96fed7412c9fdf0b7a6108fa60d71f7d81b1fa9 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 12 Mar 2025 21:42:11 +0900 Subject: [PATCH 90/95] =?UTF-8?q?fix:=20userInfo=20null=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/common/config/SecurityConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 08c07a05..9bfdfd1f 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -108,7 +108,6 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros mvc.pattern(HttpMethod.POST, "/auth/guest/sign-in"), mvc.pattern(HttpMethod.POST, "/auth/reissue"), mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), - mvc.pattern(HttpMethod.GET, "/posts/{postId}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), }; } From 81044f3cd6cf065d073e14bc0f2eed0ada1ad53c Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 13 Mar 2025 21:50:35 +0900 Subject: [PATCH 91/95] =?UTF-8?q?refactor:=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit images 데이터만 분리시키고 나머지는 쿼리 하나에서 처리 --- .../post/application/PostQueryService.java | 23 +++++++-------- .../post/domain/PostImageRepository.java | 29 +++++++++++++++++++ .../post/domain/PostRepository.java | 20 ++++++++++--- .../post/presentation/dto/FeedDto.java | 15 ++++++++++ .../post/presentation/dto/FeedResponse.java | 27 +++++++---------- 5 files changed, 80 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/domain/PostImageRepository.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/FeedDto.java diff --git a/src/main/java/com/swyp8team2/post/application/PostQueryService.java b/src/main/java/com/swyp8team2/post/application/PostQueryService.java index 64e26253..19eb7e4d 100644 --- a/src/main/java/com/swyp8team2/post/application/PostQueryService.java +++ b/src/main/java/com/swyp8team2/post/application/PostQueryService.java @@ -1,7 +1,5 @@ package com.swyp8team2.post.application; -import com.swyp8team2.comment.domain.Comment; -import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; @@ -10,11 +8,14 @@ import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.PostImageRepository; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.FeedResponse; import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; +import com.swyp8team2.post.presentation.dto.AuthorDto; +import com.swyp8team2.post.presentation.dto.FeedDto; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; @@ -34,11 +35,11 @@ public class PostQueryService { private final PostRepository postRepository; + private final PostImageRepository postImageRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final VoteRepository voteRepository; private final ShareUrlService shareUrlShareUrlService; - private final CommentRepository commentRepository; public PostResponse findByShareUrl(Long userId, String shareUrl) { String decrypt = shareUrlShareUrlService.decrypt(shareUrl); @@ -123,18 +124,14 @@ private SimplePostResponse getSimplePostResponse(Post post, List imag } public CursorBasePaginatedResponse findFeed(Long userId, Long cursor, int size) { - Slice postSlice = postRepository.findByScopeAndDeletedFalse(userId, cursor, PageRequest.ofSize(size)); + Slice postSlice = postRepository.findFeedByScopeWithUser(userId, cursor, PageRequest.ofSize(size)); return CursorBasePaginatedResponse.of(postSlice.map(post -> createFeedResponse(userId, post))); } - private FeedResponse createFeedResponse(Long userId, Post post) { - User user = userRepository.findById(post.getUserId()) - .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - List images = createPostImageResponse(userId, post); - List votes = voteRepository.findByPostIdAndDeletedFalse(post.getId()); - List comments = commentRepository.findByPostIdAndDeletedFalse(post.getId()); - boolean isAuthor = post.getUserId().equals(userId); - - return FeedResponse.of(post, user, images, votes.size(), comments.size(), isAuthor); + private FeedResponse createFeedResponse(Long userId, FeedDto dto) { + AuthorDto author = new AuthorDto(dto.postUserId(), dto.nickname(), dto.profileUrl()); + List postImages = postImageRepository.findByPostId(userId, dto.postId()); + boolean isAuthor = dto.postUserId().equals(userId); + return FeedResponse.of(dto, author, postImages, isAuthor); } } diff --git a/src/main/java/com/swyp8team2/post/domain/PostImageRepository.java b/src/main/java/com/swyp8team2/post/domain/PostImageRepository.java new file mode 100644 index 00000000..c580bb05 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/PostImageRepository.java @@ -0,0 +1,29 @@ +package com.swyp8team2.post.domain; + +import com.swyp8team2.post.presentation.dto.PostImageResponse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PostImageRepository extends JpaRepository { + + @Query(""" + SELECT new com.swyp8team2.post.presentation.dto.PostImageResponse( + pi.id, + pi.name, + i.imageUrl, + i.thumbnailUrl, + (SELECT v.id FROM Vote v WHERE v.postImageId = pi.id AND v.userId = :userId) + ) + FROM PostImage pi + INNER JOIN ImageFile i ON pi.imageFileId = i.id + WHERE pi.post.id = :postId + ORDER BY pi.id ASC + """ + ) + List findByPostId(@Param("userId") Long userId, @Param("postId") Long postId); +} diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java index 6920c281..5eb8a0b5 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -1,5 +1,6 @@ package com.swyp8team2.post.domain; +import com.swyp8team2.post.presentation.dto.FeedDto; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -42,14 +43,25 @@ public interface PostRepository extends JpaRepository { ) Optional findByIdFetchPostImage(@Param("postId") Long postId); - @Query(""" - SELECT p + @Query(""" + SELECT new com.swyp8team2.post.presentation.dto.FeedDto( + p.id, + p.status , + p.description , + p.shareUrl , + p.userId , + u.nickname, + u.profileUrl, + cast((select count(distinct v.id) from Vote v where p.id = v.postId) as long), + cast((select count(*) from Comment c where p.id = c.postId and c.deleted = false) as long) + ) FROM Post p + INNER JOIN User u on p.userId = u.id WHERE p.deleted = false - AND (p.scope = 'PUBLIC' OR (p.userId = :userId AND p.scope = 'PRIVATE')) + AND p.scope = 'PUBLIC' AND (:postId IS NULL OR p.id < :postId) ORDER BY p.createdAt DESC """ ) - Slice findByScopeAndDeletedFalse(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); + Slice findFeedByScopeWithUser(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/FeedDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/FeedDto.java new file mode 100644 index 00000000..276f87f3 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/FeedDto.java @@ -0,0 +1,15 @@ +package com.swyp8team2.post.presentation.dto; + +import com.swyp8team2.post.domain.Status; + +public record FeedDto( + Long postId, + Status status, + String description, + String shareUrl, + Long postUserId, + String nickname, + String profileUrl, + Long participantCount, + Long commentCount) { +} \ No newline at end of file diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java index bb48017f..45212cdb 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/FeedResponse.java @@ -1,9 +1,7 @@ package com.swyp8team2.post.presentation.dto; import com.swyp8team2.common.dto.CursorDto; -import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.Status; -import com.swyp8team2.user.domain.User; import java.util.List; @@ -15,27 +13,22 @@ public record FeedResponse( String description, String shareUrl, boolean isAuthor, - int participantCount, - int commentCount + Long participantCount, + Long commentCount ) implements CursorDto { - public static FeedResponse of(Post post, - User user, - List images, - int participantCount, - int commentCount, - boolean isAuthor) { + public static FeedResponse of(FeedDto feedDto, AuthorDto author, List images, boolean isAuthor) { return new FeedResponse( - post.getId(), - AuthorDto.of(user), + feedDto.postId(), + author, images, - post.getStatus(), - post.getDescription(), - post.getShareUrl(), + feedDto.status(), + feedDto.description(), + feedDto.shareUrl(), isAuthor, - participantCount, - commentCount + feedDto.participantCount(), + feedDto.commentCount() ); } From d62c037c65160c5c73839604626e9ff06c259562 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 13 Mar 2025 21:50:56 +0900 Subject: [PATCH 92/95] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/PostQueryServiceTest.java | 36 +++---------------- .../post/domain/PostRepositoryTest.java | 19 ++++++---- .../post/presentation/PostControllerTest.java | 4 +-- 3 files changed, 20 insertions(+), 39 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java index 35f9f8f4..94157049 100644 --- a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java @@ -3,20 +3,19 @@ import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; -import com.swyp8team2.common.exception.BadRequestException; -import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.post.domain.*; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.Scope; import com.swyp8team2.post.presentation.dto.FeedResponse; -import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.PostImageResponse; +import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -26,8 +25,7 @@ import static com.swyp8team2.support.fixture.FixtureGenerator.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; class PostQueryServiceTest extends IntegrationTest { @@ -180,30 +178,6 @@ void findFeed() throws Exception { ); } - @Test - @DisplayName("피드 조회 - 존재하지 않는 유저") - void findFeed_userNotFound() throws Exception { - //given - User user1 = userRepository.save(createUser(1)); - ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); - ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post myPost = postRepository.save(createPost(user1.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); - postRepository.save(Post.create( - 2L, - "설명", - List.of( - PostImage.create("1", imageFile1.getId()), - PostImage.create("1", imageFile1.getId()) - ), - Scope.PUBLIC, - VoteType.SINGLE)); - - //when then - assertThatThrownBy(() -> postService.findFeed(1L, null, 10)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.USER_NOT_FOUND.getMessage()); - } - private void createVotes(User user, Post post) { for (int i = 0; i < 5; i++) { ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); diff --git a/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java index bbbf47c3..6e05019c 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java @@ -1,7 +1,10 @@ package com.swyp8team2.post.domain; import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.post.presentation.dto.FeedDto; import com.swyp8team2.support.RepositoryTest; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -11,8 +14,7 @@ import java.util.ArrayList; import java.util.List; -import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; -import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; +import static com.swyp8team2.support.fixture.FixtureGenerator.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -21,6 +23,9 @@ class PostRepositoryTest extends RepositoryTest { @Autowired PostRepository postRepository; + @Autowired + UserRepository userRepository; + @Test @DisplayName("유저가 작성한 게시글 조회 - 게시글이 15개일 경우 15번쨰부터 10개 조회해야 함") void select_post_findByUserId1() throws Exception { @@ -98,13 +103,15 @@ private List createPosts(long userId, Scope scope) { @DisplayName("피드 조회") void select_post_findByScopeAndDeletedFalse() { //given - List myPosts = createPosts(1L, Scope.PRIVATE); - List privatePosts = createPosts(2L, Scope.PRIVATE); - List publicPosts = createPosts(2L, Scope.PUBLIC); + User user1 = userRepository.save(createUser(1)); + User user2 = userRepository.save(createUser(2)); + List myPosts = createPosts(user1.getId(), Scope.PRIVATE); + List privatePosts = createPosts(user2.getId(), Scope.PRIVATE); + List publicPosts = createPosts(user2.getId(), Scope.PUBLIC); int size = 10; //when - Slice res = postRepository.findByScopeAndDeletedFalse(1L, null, PageRequest.ofSize(size)); + Slice res = postRepository.findFeedByScopeWithUser(1L, null, PageRequest.ofSize(size)); //then assertAll( diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 45282ff4..6372dd2b 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -420,8 +420,8 @@ void findFeed() throws Exception { "description", "anioefw78f329jcs9", true, - 1, - 2 + 1L, + 2L ) ) ); From 0ea0e32bb16134ee0be28a347fc7ba4884f757f7 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 13 Mar 2025 21:51:25 +0900 Subject: [PATCH 93/95] =?UTF-8?q?chore:=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ErrorCode.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 7e338d19..bf26eccb 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -7,29 +7,29 @@ @RequiredArgsConstructor public enum ErrorCode { //400 - USER_NOT_FOUND("존재하지 않는 유저"), - INVALID_ARGUMENT("잘못된 파라미터 요청"), - REFRESH_TOKEN_MISMATCHED("리프레시 토큰 불일치"), - REFRESH_TOKEN_NOT_FOUND("리프레시 토큰을 찾을 수 없음"), - INVALID_REFRESH_TOKEN_HEADER("잘못된 리프레시 토큰 헤더"), - MISSING_FILE_EXTENSION("확장자가 누락됨"), - UNSUPPORTED_FILE_EXTENSION("지원하지 않는 확장자"), - EXCEED_MAX_FILE_SIZE("파일 크기 초과"), - POST_NOT_FOUND("존재하지 않는 게시글"), - DESCRIPTION_LENGTH_EXCEEDED("게시글 설명 길이 초과"), - INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"), - NOT_POST_AUTHOR("게시글 작성자가 아님"), - POST_ALREADY_CLOSED("이미 마감된 게시글"), - FILE_NAME_TOO_LONG("파일 이름이 너무 김"), - ACCESS_DENIED_VOTE_STATUS("투표 현황 조회 권한 없음"), - COMMENT_NOT_FOUND("존재하지 않는 댓글"), - VOTE_NOT_FOUND("존재하지 않는 투표"), - NOT_VOTER("투표자가 아님"), + USER_NOT_FOUND("존재하지 않는 유저입니다."), + INVALID_ARGUMENT("잘못된 파라미터 요청입니다."), + REFRESH_TOKEN_MISMATCHED("리프레시 토큰이 불일치합니다."), + REFRESH_TOKEN_NOT_FOUND("리프레시 토큰을 찾을 수 없습니다."), + INVALID_REFRESH_TOKEN_HEADER("잘못된 리프레시 토큰 헤더입니다."), + MISSING_FILE_EXTENSION("확장자가 누락됐습니다."), + UNSUPPORTED_FILE_EXTENSION("지원하지 않는 확장자입니다."), + EXCEED_MAX_FILE_SIZE("파일 크기가 초과했습니다."), + POST_NOT_FOUND("존재하지 않는 게시글입니다."), + DESCRIPTION_LENGTH_EXCEEDED("게시글 설명 길이가 초과했습니다."), + INVALID_POST_IMAGE_COUNT("게시글 이미지 개수가 범위를 벗어났습니다."), + NOT_POST_AUTHOR("게시글 작성자가 아닙니다."), + POST_ALREADY_CLOSED("이미 마감된 게시글입니다."), + FILE_NAME_TOO_LONG("파일 이름이 너무 깁니다."), + ACCESS_DENIED_VOTE_STATUS("투표 현황 조회 권한이 없습니다."), + COMMENT_NOT_FOUND("존재하지 않는 댓글입니다."), + VOTE_NOT_FOUND("존재하지 않는 투표입니다."), + NOT_VOTER("투표자가 아닙니다."), //401 - EXPIRED_TOKEN("토큰 만료"), - INVALID_TOKEN("유효하지 않은 토큰"), - INVALID_AUTH_HEADER("잘못된 인증 헤더"), + EXPIRED_TOKEN("토큰이 만료됐습니다."), + INVALID_TOKEN("유효하지 않은 토큰입니다."), + INVALID_AUTH_HEADER("잘못된 인증 헤더입니다."), //403 FORBIDDEN("권한 없음"), @@ -38,13 +38,13 @@ public enum ErrorCode { NOT_FOUND("리소스를 찾을 수 없음"), //500 - INTERNAL_SERVER_ERROR("서버 내부 오류"), - INVALID_INPUT_VALUE("잘못된 입력 값"), - SOCIAL_AUTHENTICATION_FAILED("소셜 로그인 실패"), + INTERNAL_SERVER_ERROR("서버 내부 오류 발생"), + INVALID_INPUT_VALUE("잘못된 입력 값입니다."), + SOCIAL_AUTHENTICATION_FAILED("소셜 로그인이 실패했습니다."), POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), - IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"), - POST_IMAGE_NOT_FOUND("게시글 이미지 없음"), - SHARE_URL_ALREADY_EXISTS("공유 URL이 이미 존재"), + IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지입니다."), + POST_IMAGE_NOT_FOUND("게시글 이미지가 없습니다."), + SHARE_URL_ALREADY_EXISTS("공유 URL이 이미 존재합니다."), //503 SERVICE_UNAVAILABLE("서비스 이용 불가"), From 67d0a0907542c305e5a8eabaea85a9089fc7a8fb Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 13 Mar 2025 21:55:27 +0900 Subject: [PATCH 94/95] =?UTF-8?q?chore:=20=EC=9A=B4=EC=98=81=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EC=97=90=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 526c6e2d..95ed10ad 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 526c6e2dbc39eb60357eefe04b97b3b9fdbb0802 +Subproject commit 95ed10ad85caba6bf8a623d60088547ae3bd3205 From e18e7bd30edf2f49a65238a16128a1ff2a4531e4 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 13 Mar 2025 22:48:58 +0900 Subject: [PATCH 95/95] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/PostQueryServiceTest.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java index 94157049..81569469 100644 --- a/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostQueryServiceTest.java @@ -153,28 +153,30 @@ void findFeed() throws Exception { //given int size = 20; User user1 = userRepository.save(createUser(1)); + User user2 = userRepository.save(createUser(2)); ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); - Post myPost = postRepository.save(createPost(user1.getId(), Scope.PRIVATE, imageFile1, imageFile2, 1)); - List privatePosts = createPosts(userRepository.save(createUser(2)), Scope.PRIVATE); - List publicPosts = createPosts(userRepository.save(createUser(2)), Scope.PUBLIC); + List publicPosts = createPosts(user2, Scope.PUBLIC); + List privatePosts = createPosts(user2, Scope.PRIVATE); + Post myPost = postRepository.save(createPost(user1.getId(), Scope.PUBLIC, imageFile1, imageFile2, 1)); - createVotes(user1, myPost); - createComments(user1, myPost); + createVotes(user1, publicPosts.getFirst()); + createComments(user1, publicPosts.getFirst()); + + List publicPostVotes = voteRepository.findByPostIdAndDeletedFalse(publicPosts.getFirst().getId()); + List publicPostComments = commentRepository.findByPostIdAndDeletedFalse(publicPosts.getFirst().getId()); //when - List votes = voteRepository.findByPostIdAndDeletedFalse(myPost.getId()); - List comments = commentRepository.findByPostIdAndDeletedFalse(myPost.getId()); CursorBasePaginatedResponse response = postService.findFeed(user1.getId(), null, size); //then assertAll( () -> assertThat(response.data().size()).isEqualTo(16), - () -> assertThat(response.data().getLast().participantCount()).isEqualTo(votes.size()), - () -> assertThat(response.data().getLast().commentCount()).isEqualTo(comments.size()), - () -> assertThat(response.data().getLast().isAuthor()).isTrue(), - () -> assertThat(response.data().getFirst().isAuthor()).isFalse() + () -> assertThat(response.data().getLast().participantCount()).isEqualTo(publicPostVotes.size()), + () -> assertThat(response.data().getLast().commentCount()).isEqualTo(publicPostComments.size()), + () -> assertThat(response.data().getLast().isAuthor()).isFalse(), + () -> assertThat(response.data().getFirst().isAuthor()).isTrue() ); }