diff --git a/build.gradle b/build.gradle index 8005ef46..23e36c35 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { // gson implementation 'com.google.code.gson:gson:2.8.6' + // base64 + implementation 'commons-codec:commons-codec:1.15' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index fc4dea9f..54301e7e 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -11,6 +11,13 @@ operation::post-controller-test/create-post[snippets='http-request,curl-request, operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] +[[개사굴-공유-url-조회]] +=== `GET` 게시글 공유 url 조회 + +operation::post-controller-test/find-post_share-url[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] + +[[게시글-목록-조회]] + [[사진-투표-현황-조회]] === `GET` 사진 투표 현황 조회 @@ -32,8 +39,6 @@ operation::post-controller-test/find-voted-post[snippets='http-request,curl-requ operation::post-controller-test/close-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] -[[게시글-수정]] - [[게시글-삭제]] === `DELETE` 게시글 삭제 diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index bbce4488..99a4e109 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,6 +7,7 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; import lombok.RequiredArgsConstructor; @@ -14,7 +15,6 @@ import org.springframework.transaction.annotation.Transactional; @Service -@RequiredArgsConstructor public class AuthService { private final JwtService jwtService; @@ -23,6 +23,19 @@ public class AuthService { 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 TokenPair oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java index 4b678645..91de4d43 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -1,6 +1,7 @@ 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; @@ -27,11 +28,14 @@ import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; @Slf4j -@RequiredArgsConstructor 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 { diff --git a/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java new file mode 100644 index 00000000..90a6e2db --- /dev/null +++ b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java @@ -0,0 +1,16 @@ +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/annotation/ShareUrlCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java new file mode 100644 index 00000000..2ea4c9f4 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java @@ -0,0 +1,16 @@ +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}) +public @interface ShareUrlCryptoService { + + String QUALIFIER = "shareUrlCryptoService"; +} diff --git a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java new file mode 100644 index 00000000..0249cb1a --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java @@ -0,0 +1,23 @@ +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CryptoConfig { + + @GuestTokenCryptoService + @Bean(name = GuestTokenCryptoService.QUALIFIER) + public CryptoService guestTokenCryptoService() throws Exception { + return new CryptoService(); + } + + @ShareUrlCryptoService + @Bean(name = ShareUrlCryptoService.QUALIFIER) + public CryptoService shareUrlCryptoService() throws Exception { + return new CryptoService(); + } +} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index e5de05a9..0994c3fe 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -5,6 +5,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.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; @@ -23,13 +24,10 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; -import java.util.List; - @Configuration @EnableWebSecurity public class SecurityConfig { @@ -39,7 +37,7 @@ public class SecurityConfig { public SecurityConfig( @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver, - CryptoService cryptoService + @GuestTokenCryptoService CryptoService cryptoService ) { this.handlerExceptionResolver = handlerExceptionResolver; this.cryptoService = cryptoService; @@ -112,7 +110,8 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), mvc.pattern("/auth/guest/token"), - mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), + mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), + mvc.pattern(HttpMethod.GET, "/posts/{postId}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), // mvc.pattern("/posts/{postId}/votes/guest/**"), 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 644eff7a..301755c9 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -4,9 +4,12 @@ import com.swyp8team2.auth.application.jwt.TokenPair; 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.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; @@ -16,6 +19,7 @@ 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; @@ -26,17 +30,38 @@ @Profile({"dev", "local"}) @Component -@RequiredArgsConstructor public class DataInitializer { private final NicknameAdjectiveRepository nicknameAdjectiveRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final PostRepository postRepository; + private final CryptoService shaereUrlCryptoService; private final JwtService jwtService; private final VoteService voteService; private final CommentRepository commentRepository; + public DataInitializer( + NicknameAdjectiveRepository nicknameAdjectiveRepository, + UserRepository userRepository, + ImageFileRepository imageFileRepository, + PostRepository postRepository, + @ShareUrlCryptoService CryptoService shaereUrlCryptoService, + JwtService jwtService, + VoteService voteService, + CommentRepository commentRepository + ) { + this.nicknameAdjectiveRepository = nicknameAdjectiveRepository; + this.userRepository = userRepository; + this.imageFileRepository = imageFileRepository; + this.postRepository = postRepository; + this.shaereUrlCryptoService = shaereUrlCryptoService; + this.jwtService = jwtService; + this.voteService = voteService; + this.commentRepository = commentRepository; + } + + @Transactional public void init() { if (userRepository.count() > 0) { @@ -56,7 +81,9 @@ 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"))); - posts.add(postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"))); + Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())))); + post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); + posts.add(post); } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index bbc5ea04..e717db55 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -35,6 +35,7 @@ public enum ErrorCode { POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"), POST_IMAGE_NOT_FOUND("게시글 이미지 없음"), + SHARE_URL_ALREADY_EXISTS("공유 URL이 이미 존재"), //503 SERVICE_UNAVAILABLE("서비스 이용 불가"), diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java index 25fddd7b..612755d1 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -4,17 +4,15 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import org.apache.commons.codec.binary.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; -import java.util.Base64; @Slf4j -@Service public class CryptoService { private static final String ALGORITHM = "AES"; @@ -31,7 +29,9 @@ public String encrypt(String data) { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedBytes = cipher.doFinal(data.getBytes()); - return Base64.getEncoder().encodeToString(encryptedBytes); + return Base64.encodeBase64URLSafeString(encryptedBytes) + .replace('+', 'A') + .replace('/', 'B'); } catch (Exception e) { log.error("encrypt error {}", e.getMessage()); throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); @@ -42,8 +42,12 @@ public String decrypt(String encryptedData) { try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); - byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); - return new String(decryptedBytes); + byte[] decoded = Base64.decodeBase64( + encryptedData + .replace('A', '+') + .replace('B', '/') + ); + return new String(cipher.doFinal(decoded)); } catch (IllegalBlockSizeException | BadPaddingException e) { log.debug("decrypt error {}", e.getMessage()); throw new BadRequestException(ErrorCode.INVALID_TOKEN); diff --git a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java index ab1edd99..b764e035 100644 --- a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java +++ b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java @@ -16,6 +16,6 @@ public String generate() { if (index >= alphabets.length) { throw new InternalServerException(ErrorCode.POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND); } - return "뽀또" + alphabets[index++]; + return "뽀또 " + alphabets[index++]; } } diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 5b643b77..1e97d25d 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,9 +1,11 @@ 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; @@ -29,7 +31,6 @@ @Service @Transactional(readOnly = true) -@RequiredArgsConstructor public class PostService { private final PostRepository postRepository; @@ -37,12 +38,30 @@ public class PostService { 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; + } @Transactional public Long create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); - Post post = Post.create(userId, request.description(), postImages, "TODO: location"); + Post post = Post.create(userId, request.description(), postImages); Post save = postRepository.save(post); + save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); return save.getId(); } @@ -148,4 +167,9 @@ public void close(Long userId, Long postId) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.close(userId); } + + public PostResponse findByShareUrl(Long userId, String shareUrl) { + String decrypt = shareUrlCryptoService.decrypt(shareUrl); + return findById(userId, Long.valueOf(decrypt)); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 96ad72dd..5d8d2bb2 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -20,8 +20,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; - -import static com.swyp8team2.common.util.Validator.*; +import java.util.Objects; @Getter @Entity @@ -69,8 +68,8 @@ private void validateDescription(String description) { } } - public static Post create(Long userId, String description, List images, String shareUrl) { - return new Post(null, userId, description, State.PROGRESS, images, shareUrl); + public static Post create(Long userId, String description, List images) { + return new Post(null, userId, description, State.PROGRESS, images, null); } public PostImage getBestPickedImage() { @@ -114,4 +113,11 @@ public void validateProgress() { throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); } } + + public void setShareUrl(String shareUrl) { + if (Objects.nonNull(this.shareUrl)) { + throw new InternalServerException(ErrorCode.SHARE_URL_ALREADY_EXISTS); + } + this.shareUrl = shareUrl; + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 3d4273a0..c07264d7 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -4,6 +4,7 @@ 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; @@ -51,6 +52,17 @@ public ResponseEntity findPost( return ResponseEntity.ok(postService.findById(userId, postId)); } + @GetMapping("/shareUrl/{shareUrl}") + public ResponseEntity findPostByShareUrl( + @PathVariable("shareUrl") String shareUrl, + @AuthenticationPrincipal UserInfo userInfo + ) { + Long userId = Optional.ofNullable(userInfo) + .map(UserInfo::userId) + .orElse(null); + return ResponseEntity.ok(postService.findByShareUrl(userId, shareUrl)); + } + @GetMapping("/{postId}/status") public ResponseEntity> findVoteStatus( @PathVariable("postId") Long postId diff --git a/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java similarity index 52% rename from src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java rename to src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java index 44467835..f6629c64 100644 --- a/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java @@ -1,4 +1,4 @@ -package com.swyp8team2.post.presentation; +package com.swyp8team2.post.presentation.dto; public record CreatePostResponse(Long postId) { } diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index b089de58..b97757fb 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -29,7 +29,7 @@ public Long createUser(String nickname, String profileImageUrl) { private String getProfileImage(String profileImageUrl) { return Optional.ofNullable(profileImageUrl) - .orElse("defailt_profile_image"); + .orElse("https://t1.kakaocdn.net/account_images/default_profile.jpeg"); } private String getNickname(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 85ad7041..5686f490 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -29,21 +29,18 @@ public class User extends BaseEntity { private String profileUrl; - private String seq; - @Enumerated(jakarta.persistence.EnumType.STRING) public Role role; - public User(Long id, String nickname, String profileUrl, String seq, Role role) { + public User(Long id, String nickname, String profileUrl, Role role) { this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; - this.seq = seq; this.role = role; } public static User create(String nickname, String profileUrl) { - return new User(null, nickname, profileUrl, UUID.randomUUID().toString(), Role.USER); + return new User(null, nickname, profileUrl, Role.USER); } public static User createGuest() { @@ -51,7 +48,6 @@ public static User createGuest() { null, "guest_" + System.currentTimeMillis(), "https://image.photopic.site/images-dev/resized_202502240006030.png", - UUID.randomUUID().toString(), Role.GUEST ); } diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index bd5b9844..65ca8fbe 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -74,7 +74,7 @@ void findComments() { Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); 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", "seq", Role.USER); + User user = new User(100L, "닉네임","http://example.com/profile.png", Role.USER); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); diff --git a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java index 626bb21f..8f763b94 100644 --- a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java @@ -26,7 +26,7 @@ void generate() throws Exception { String generate2 = postImageNameGenerator.generate(); //then - assertThat(generate1).isEqualTo("뽀또A"); - assertThat(generate2).isEqualTo("뽀또B"); + assertThat(generate1).isEqualTo("뽀또 A"); + assertThat(generate2).isEqualTo("뽀또 B"); } } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 4f5118d9..dde9b629 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -73,10 +73,10 @@ void create() throws Exception { () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), - () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), + () -> assertThat(images.get(0).getName()).isEqualTo("뽀또 A"), () -> assertThat(images.get(0).getVoteCount()).isEqualTo(0), () -> assertThat(images.get(1).getImageFileId()).isEqualTo(2L), - () -> assertThat(images.get(1).getName()).isEqualTo("뽀또B"), + () -> assertThat(images.get(1).getName()).isEqualTo("뽀또 B"), () -> assertThat(images.get(1).getVoteCount()).isEqualTo(0) ); } diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index ab59071e..c0908cf2 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -2,8 +2,6 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.InternalServerException; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,17 +23,15 @@ void create() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - String shareUrl = "shareUrl"; //when - Post post = Post.create(userId, description, postImages, shareUrl); + Post post = Post.create(userId, description, postImages); //then List images = post.getImages(); assertAll( () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(post.getDescription()).isEqualTo(description), - () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), () -> assertThat(post.getState()).isEqualTo(State.PROGRESS), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), @@ -56,7 +52,7 @@ void create_invalidPostImageCount() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, "description", postImages, "shareUrl")) + assertThatThrownBy(() -> Post.create(1L, "description", postImages)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); } @@ -72,7 +68,7 @@ void create_descriptionCountExceeded() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, description, postImages, "shareUrl")) + assertThatThrownBy(() -> Post.create(1L, description, postImages)) .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 0d3701f1..9d70892f 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.common.dto.CursorBasePaginatedResponse; 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; @@ -134,6 +135,58 @@ void findPost() throws Exception { )); } + @Test + @WithAnonymousUser + @DisplayName("게시글 공유 url 상세 조회") + void findPost_shareUrl() 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", true), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) + ), + "https://photopic.site/shareurl", + true, + LocalDateTime.of(2025, 2, 13, 12, 0) + ); + given(postService.findByShareUrl(any(), any())) + .willReturn(response); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/shareUrl/{shareUrl}", "JNOfBVfcG2z89afSiRrOyQ")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + pathParameters( + parameterWithName("shareUrl").description("공유 url") + ), + 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[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), + fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), + fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") + ) + )); + } + @Test @WithMockUserInfo @DisplayName("게시글 투표 상태 조회") diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index feafc88d..a11bf052 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -17,8 +17,7 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) - ), - "shareUrl" + key + ) ); }