diff --git a/src/main/java/balancetalk/global/exception/ConstraintExceptionHandler.java b/src/main/java/balancetalk/global/exception/ConstraintExceptionHandler.java deleted file mode 100644 index 23ac89674..000000000 --- a/src/main/java/balancetalk/global/exception/ConstraintExceptionHandler.java +++ /dev/null @@ -1,20 +0,0 @@ -package balancetalk.global.exception; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - - -@Slf4j -@RestControllerAdvice -public class ConstraintExceptionHandler { - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleNoticeTitleSizeExceededExceptions(MethodArgumentNotValidException e) { - ErrorResponse errorResponse = ErrorResponse.from(ErrorCode.EXCEED_VALIDATION_LENGTH); - log.error("exception message = {}", e.getMessage()); - - return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); - } -} \ No newline at end of file diff --git a/src/main/java/balancetalk/global/exception/ConstraintViolationHandler.java b/src/main/java/balancetalk/global/exception/ConstraintViolationHandler.java new file mode 100644 index 000000000..353815b34 --- /dev/null +++ b/src/main/java/balancetalk/global/exception/ConstraintViolationHandler.java @@ -0,0 +1,31 @@ +package balancetalk.global.exception; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class ConstraintViolationHandler { + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException(ConstraintViolationException e) { + Map errors = new HashMap<>(); + Set> violations = e.getConstraintViolations(); + String errorMessage = violations.stream() + .map(violation -> violation.getMessage()) + .collect(Collectors.joining(", ")); + errors.put("message" , errorMessage); + log.info("errorMessage={}", errorMessage); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors); + } +} \ No newline at end of file diff --git a/src/main/java/balancetalk/global/exception/MethodArgumentException.java b/src/main/java/balancetalk/global/exception/MethodArgumentException.java new file mode 100644 index 000000000..29c451dfe --- /dev/null +++ b/src/main/java/balancetalk/global/exception/MethodArgumentException.java @@ -0,0 +1,26 @@ +package balancetalk.global.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.util.HashMap; +import java.util.Map; + + +@Slf4j +@RestControllerAdvice +public class MethodArgumentException { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleNoticeTitleSizeExceededExceptions(MethodArgumentNotValidException e) { + Map errors = new HashMap<>(); + FieldError fieldError = e.getBindingResult().getFieldError(); + String errorMessage = fieldError.getDefaultMessage(); + errors.put("message" , errorMessage); + log.info("errorMessage={}", errorMessage); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors); + } +} \ No newline at end of file diff --git a/src/main/java/balancetalk/module/authmail/dto/EmailRequest.java b/src/main/java/balancetalk/module/authmail/dto/EmailRequest.java index d6b769051..31b45753f 100644 --- a/src/main/java/balancetalk/module/authmail/dto/EmailRequest.java +++ b/src/main/java/balancetalk/module/authmail/dto/EmailRequest.java @@ -2,6 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -11,7 +13,9 @@ @NoArgsConstructor public class EmailRequest { - @Email + @NotNull + @Size(max = 30) + @Email(regexp = "^[a-zA-Z0-9._%+-]{1,20}@[a-zA-Z0-9.-]{1,10}\\.[a-zA-Z]{2,}$") @Schema(description = "인증 번호를 받을 이메일 주소", example = "test1234@naver.com") private String email; } diff --git a/src/main/java/balancetalk/module/authmail/dto/EmailVerification.java b/src/main/java/balancetalk/module/authmail/dto/EmailVerification.java index f2faa1345..81e673a66 100644 --- a/src/main/java/balancetalk/module/authmail/dto/EmailVerification.java +++ b/src/main/java/balancetalk/module/authmail/dto/EmailVerification.java @@ -2,6 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -11,7 +13,9 @@ @NoArgsConstructor public class EmailVerification { - @Email + @NotNull + @Size(max = 30) + @Email(regexp = "^[a-zA-Z0-9._%+-]{1,20}@[a-zA-Z0-9.-]{1,10}\\.[a-zA-Z]{2,}$") @Schema(description = "인증 번호를 검증할 이메일 주소", example = "test1234@naver.com") private String email; diff --git a/src/main/java/balancetalk/module/bookmark/presentation/BookmarkController.java b/src/main/java/balancetalk/module/bookmark/presentation/BookmarkController.java index 2c15aefb7..3969bee91 100644 --- a/src/main/java/balancetalk/module/bookmark/presentation/BookmarkController.java +++ b/src/main/java/balancetalk/module/bookmark/presentation/BookmarkController.java @@ -11,15 +11,14 @@ @RestController @RequiredArgsConstructor -@Tag(name = "bookmark", description = "게시글 북마크 API") -@RequestMapping("/bookmark") +@RequestMapping("/bookmarks") @Tag(name = "bookmark", description = "게시글 북마크 API") public class BookmarkController { private final BookmarkService bookmarkService; @ResponseStatus(HttpStatus.CREATED) - @PostMapping("/posts/{postId}") + @PostMapping("/{postId}") @Operation(summary = "북마크 추가", description = "post-id에 해당하는 게시글을 북마크에 추가한다.") public String createBookmark(@PathVariable Long postId) { bookmarkService.createBookmark(postId); diff --git a/src/main/java/balancetalk/module/comment/presentation/CommentController.java b/src/main/java/balancetalk/module/comment/presentation/CommentController.java index 736993cf4..6fd5f40c1 100644 --- a/src/main/java/balancetalk/module/comment/presentation/CommentController.java +++ b/src/main/java/balancetalk/module/comment/presentation/CommentController.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -24,7 +25,7 @@ public class CommentController { @ResponseStatus(HttpStatus.CREATED) @PostMapping @Operation(summary = "댓글 작성", description = "post-id에 해당하는 게시글에 댓글을 작성한다.") - public String createComment(@PathVariable Long postId, @RequestBody CommentRequest request) { + public String createComment(@PathVariable Long postId, @Valid @RequestBody CommentRequest request) { commentService.createComment(request, postId); return "댓글이 정상적으로 작성되었습니다."; } @@ -64,7 +65,7 @@ public String deleteComment(@PathVariable Long commentId, @PathVariable Long pos @ResponseStatus(HttpStatus.CREATED) @PostMapping("/{commentId}/replies") @Operation(summary = "답글 작성", description = "comment-id에 해당하는 댓글에 답글을 작성한다.") - public String createComment(@PathVariable Long postId, @PathVariable Long commentId, @RequestBody ReplyCreateRequest request) { + public String createComment(@PathVariable Long postId, @PathVariable Long commentId, @Valid @RequestBody ReplyCreateRequest request) { commentService.createReply(postId, commentId, request); return "답글이 정상적으로 작성되었습니다."; } diff --git a/src/main/java/balancetalk/module/file/application/FileUploadService.java b/src/main/java/balancetalk/module/file/application/FileService.java similarity index 76% rename from src/main/java/balancetalk/module/file/application/FileUploadService.java rename to src/main/java/balancetalk/module/file/application/FileService.java index c33c62e6d..ef49a2034 100644 --- a/src/main/java/balancetalk/module/file/application/FileUploadService.java +++ b/src/main/java/balancetalk/module/file/application/FileService.java @@ -17,11 +17,15 @@ import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetUrlRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @Service @RequiredArgsConstructor -public class FileUploadService { +public class FileService { + + private static final String S3_URL = "https://balance-talk-static-files.s3.ap-northeast-2.amazonaws.com/"; + private static final String UPLOAD_DIR = "balance-talk-images/balance-option/"; private final S3Client s3Client; private final FileRepository fileRepository; @@ -31,15 +35,15 @@ public class FileUploadService { @Transactional public FileResponse uploadImage(MultipartFile multipartFile) { - String uploadDir = "balance-talk-images/balance-option/"; String originalName = multipartFile.getOriginalFilename(); String storedName = String.format("%s_%s", UUID.randomUUID(), originalName); long contentLength = multipartFile.getSize(); FileType fileType = convertMimeTypeToFileType(multipartFile.getContentType()); try (InputStream inputStream = multipartFile.getInputStream()) { - putObjectToS3(uploadDir + storedName, inputStream, contentLength); - File file = fileRepository.save(createFile(originalName, storedName, uploadDir, fileType, contentLength)); + putObjectToS3(UPLOAD_DIR + storedName, inputStream, contentLength); + File file = fileRepository.save( + createFile(originalName, storedName, S3_URL + UPLOAD_DIR, fileType, contentLength)); return FileResponse.fromEntity(file); } catch (IOException e) { @@ -67,15 +71,24 @@ private void putObjectToS3(String key, InputStream inputStream, long contentLeng s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, contentLength)); } - private File createFile(String originalName, String storedName, String uploadDir, FileType fileType, + private File createFile(String originalName, String storedName, String path, FileType fileType, long contentLength) { return File.builder() .originalName(originalName) .storedName(storedName) - .path(uploadDir) + .path(path) .type(fileType) .size(contentLength) .build(); } + + @Transactional(readOnly = true) + public String getUrl(String key) { + GetUrlRequest request = GetUrlRequest.builder() + .bucket(bucket) + .key(key) + .build(); + return s3Client.utilities().getUrl(request).toString(); + } } diff --git a/src/main/java/balancetalk/module/file/domain/File.java b/src/main/java/balancetalk/module/file/domain/File.java index 7f6011423..3b2270e31 100644 --- a/src/main/java/balancetalk/module/file/domain/File.java +++ b/src/main/java/balancetalk/module/file/domain/File.java @@ -46,4 +46,8 @@ public class File { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "notice_id") private Notice notice; + + public String getUrl() { + return path + storedName; + } } diff --git a/src/main/java/balancetalk/module/file/presentation/FileController.java b/src/main/java/balancetalk/module/file/presentation/FileController.java index 2f2c5b980..d4e2b351e 100644 --- a/src/main/java/balancetalk/module/file/presentation/FileController.java +++ b/src/main/java/balancetalk/module/file/presentation/FileController.java @@ -1,6 +1,6 @@ package balancetalk.module.file.presentation; -import balancetalk.module.file.application.FileUploadService; +import balancetalk.module.file.application.FileService; import balancetalk.module.file.dto.FileResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -16,12 +16,12 @@ @Tag(name = "file", description = "파일 API") public class FileController { - private final FileUploadService fileUploadService; + private final FileService fileService; @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "/image/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "파일 업로드", description = "파일을 업로드 한다.") public FileResponse uploadImage(@RequestPart("file") MultipartFile file) { - return fileUploadService.uploadImage(file); + return fileService.uploadImage(file); } } \ No newline at end of file diff --git a/src/main/java/balancetalk/module/member/application/MemberService.java b/src/main/java/balancetalk/module/member/application/MemberService.java index 3d1918d2f..92afe0a68 100644 --- a/src/main/java/balancetalk/module/member/application/MemberService.java +++ b/src/main/java/balancetalk/module/member/application/MemberService.java @@ -9,16 +9,13 @@ import balancetalk.module.member.domain.Member; import balancetalk.module.member.domain.MemberRepository; import balancetalk.module.member.dto.*; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/balancetalk/module/member/dto/JoinRequest.java b/src/main/java/balancetalk/module/member/dto/JoinRequest.java index 7ef074701..d9d02abdf 100644 --- a/src/main/java/balancetalk/module/member/dto/JoinRequest.java +++ b/src/main/java/balancetalk/module/member/dto/JoinRequest.java @@ -4,9 +4,7 @@ import balancetalk.module.member.domain.Member; import balancetalk.module.member.domain.Role; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -18,9 +16,14 @@ @AllArgsConstructor public class JoinRequest { + @NotBlank + @Size(min = 2, max = 10) @Schema(description = "회원 닉네임", example = "닉네임") private String nickname; + @NotNull + @Size(max = 30) + @Email(regexp = "^[a-zA-Z0-9._%+-]{1,20}@[a-zA-Z0-9.-]{1,10}\\.[a-zA-Z]{2,}$") @Schema(description = "회원 이메일", example = "test1234@naver.com") private String email; diff --git a/src/main/java/balancetalk/module/member/presentation/MemberController.java b/src/main/java/balancetalk/module/member/presentation/MemberController.java index 9922b8154..fe2f25f07 100644 --- a/src/main/java/balancetalk/module/member/presentation/MemberController.java +++ b/src/main/java/balancetalk/module/member/presentation/MemberController.java @@ -1,14 +1,19 @@ package balancetalk.module.member.presentation; + import balancetalk.module.member.application.MemberService; import balancetalk.module.member.dto.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -16,6 +21,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/members") +@Validated @Tag(name = "member", description = "회원 API") public class MemberController { @@ -53,7 +59,8 @@ public List findAllMemberInfo() { @ResponseStatus(HttpStatus.OK) @PutMapping("/nickname") @Operation(summary = "회원 닉네임 수정", description = "회원 닉네임을 수정한다.") - public String updateNickname(@Valid @RequestBody String newNickname, HttpServletRequest request) { + public String updateNickname(@Valid @NotBlank @RequestBody @Size(min = 2, max = 10)String newNickname, HttpServletRequest request) { + // TODO: RequestBody 빈 값일 때 에러체킹 x memberService.updateNickname(newNickname, request); return "회원 닉네임이 변경되었습니다."; } @@ -61,7 +68,10 @@ public String updateNickname(@Valid @RequestBody String newNickname, HttpServlet @ResponseStatus(HttpStatus.OK) @PutMapping("/password") @Operation(summary = "회원 비밀번호 수정", description = "회원 패스워드를 수정한다.") - public String updatePassword(@Valid @RequestBody String newPassword, HttpServletRequest request) { + public String updatePassword(@RequestBody @Size(min = 10, max = 20) + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{10,20}$") + String newPassword, HttpServletRequest request) { + // TODO: RequestBody 빈 값일 때 에러체킹 x memberService.updatePassword(newPassword, request); return "회원 비밀번호가 변경되었습니다."; } @@ -85,7 +95,8 @@ public String logout() { @ResponseStatus(HttpStatus.OK) @GetMapping("/duplicate") @Operation(summary = "닉네임 중복 검증", description = "중복된 닉네임이 존재하는지 체크한다.") - public String verifyNickname(@RequestParam String nickname) { + public String verifyNickname(@RequestParam @NotBlank + @Size(min = 2, max = 10)String nickname) { memberService.verifyNickname(nickname); return "사용 가능한 닉네임 입니다."; } diff --git a/src/main/java/balancetalk/module/post/application/PostService.java b/src/main/java/balancetalk/module/post/application/PostService.java index 33c1da1ec..299badcb8 100644 --- a/src/main/java/balancetalk/module/post/application/PostService.java +++ b/src/main/java/balancetalk/module/post/application/PostService.java @@ -11,7 +11,7 @@ import balancetalk.module.member.domain.MemberRepository; import balancetalk.module.member.domain.Role; import balancetalk.module.post.domain.*; -import balancetalk.module.post.dto.BalanceOptionDto; +import balancetalk.module.post.dto.BalanceOptionRequest; import balancetalk.module.post.dto.PostRequest; import balancetalk.module.post.dto.PostResponse; import java.util.stream.Collectors; @@ -41,6 +41,7 @@ public PostResponse save(final PostRequest request) { if (redisService.getValues(writer.getEmail()) == null) { throw new BalanceTalkException(FORBIDDEN_POST_CREATE); } + List images = getImages(request); Post post = request.toEntity(writer, images); @@ -56,11 +57,11 @@ public PostResponse save(final PostRequest request) { return PostResponse.fromEntity(postRepository.save(post), false, false, false); } - private List getImages(PostRequest postRequestDto) { - List balanceOptions = postRequestDto.getBalanceOptions(); + private List getImages(PostRequest postRequest) { + List balanceOptions = postRequest.getBalanceOptions(); return balanceOptions.stream() - .filter(optionDto -> optionDto.getStoredFileName() != null && !optionDto.getStoredFileName().isEmpty()) - .map(optionDto -> fileRepository.findByStoredName(optionDto.getStoredFileName()) + .filter(optionDto -> optionDto.getStoredImageName() != null && !optionDto.getStoredImageName().isEmpty()) + .map(optionDto -> fileRepository.findByStoredName(optionDto.getStoredImageName()) .orElseThrow(() -> new BalanceTalkException(NOT_FOUND_FILE))) .toList(); } diff --git a/src/main/java/balancetalk/module/post/dto/BalanceOptionDto.java b/src/main/java/balancetalk/module/post/dto/BalanceOptionRequest.java similarity index 68% rename from src/main/java/balancetalk/module/post/dto/BalanceOptionDto.java rename to src/main/java/balancetalk/module/post/dto/BalanceOptionRequest.java index ce4b91f2c..a94a48ae0 100644 --- a/src/main/java/balancetalk/module/post/dto/BalanceOptionDto.java +++ b/src/main/java/balancetalk/module/post/dto/BalanceOptionRequest.java @@ -2,7 +2,10 @@ import balancetalk.module.file.domain.File; import balancetalk.module.post.domain.BalanceOption; +import balancetalk.module.post.domain.BalanceOption.BalanceOptionBuilder; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -13,22 +16,26 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class BalanceOptionDto { +public class BalanceOptionRequest { @Schema(description = "선택지 id", example = "1") private Long balanceOptionId; + @NotBlank + @Size(max = 50) @Schema(description = "선택지 제목", example = "선택지 제목1") private String title; + @NotBlank + @Size(max = 100) @Schema(description = "선택지 내용", example = "선택지 내용1") private String description; @Schema(description = "DB에 저장되는 이미지 이름", example = "4df23447-2355-45h2-8783-7f6gd2ceb848_고양이.jpg") - private String storedFileName; + private String storedImageName; public BalanceOption toEntity(@Nullable File image) { - BalanceOption.BalanceOptionBuilder builder = BalanceOption.builder() + BalanceOptionBuilder builder = BalanceOption.builder() .title(title) .description(description); if (image != null) { @@ -37,13 +44,13 @@ public BalanceOption toEntity(@Nullable File image) { return builder.build(); } - public static BalanceOptionDto fromEntity(BalanceOption balanceOption) { - BalanceOptionDtoBuilder builder = BalanceOptionDto.builder() + public static BalanceOptionRequest fromEntity(BalanceOption balanceOption) { + BalanceOptionRequestBuilder builder = BalanceOptionRequest.builder() .balanceOptionId(balanceOption.getId()) .title(balanceOption.getTitle()) .description(balanceOption.getDescription()); if (balanceOption.getFile() != null) { - builder.storedFileName(balanceOption.getFile().getStoredName()); + builder.storedImageName(balanceOption.getFile().getStoredName()); } return builder.build(); } diff --git a/src/main/java/balancetalk/module/post/dto/BalanceOptionResponse.java b/src/main/java/balancetalk/module/post/dto/BalanceOptionResponse.java new file mode 100644 index 000000000..c8730fe25 --- /dev/null +++ b/src/main/java/balancetalk/module/post/dto/BalanceOptionResponse.java @@ -0,0 +1,48 @@ +package balancetalk.module.post.dto; + +import balancetalk.module.file.domain.File; +import balancetalk.module.post.domain.BalanceOption; +import balancetalk.module.post.domain.BalanceOption.BalanceOptionBuilder; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BalanceOptionResponse { + + @Schema(description = "선택지 제목", example = "선택지 제목1") + private String title; + + @Schema(description = "선택지 내용", example = "선택지 내용1") + private String description; + + @Schema(description = "이미지 URL", + example = "https://balance-talk-static-files4df23447-2355-45h2-8783-7f6gd2ceb848_고양이.jpg") + private String imageUrl; + + public BalanceOption toEntity(@Nullable File image) { + BalanceOptionBuilder builder = BalanceOption.builder() + .title(title) + .description(description); + if (image != null) { + builder.file(image); + } + return builder.build(); + } + + public static BalanceOptionResponse fromEntity(BalanceOption balanceOption) { + BalanceOptionResponseBuilder builder = BalanceOptionResponse.builder() + .title(balanceOption.getTitle()) + .description(balanceOption.getDescription()); + if (balanceOption.getFile() != null) { + builder.imageUrl(balanceOption.getFile().getUrl()); + } + return builder.build(); + } +} diff --git a/src/main/java/balancetalk/module/post/dto/PostRequest.java b/src/main/java/balancetalk/module/post/dto/PostRequest.java index cd8e5cc9c..af753a25f 100644 --- a/src/main/java/balancetalk/module/post/dto/PostRequest.java +++ b/src/main/java/balancetalk/module/post/dto/PostRequest.java @@ -8,6 +8,7 @@ import balancetalk.module.post.domain.PostTag; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -24,19 +25,27 @@ @AllArgsConstructor public class PostRequest { + @NotBlank + @Size(max = 50) @Schema(description = "게시글 제목", example = "게시글 제목") private String title; + @NotNull + @Future @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss") @Schema(description = "투료 종료 기한", example = "2024/12/25 15:30:00", type = "string") private LocalDateTime deadline; + @NotNull @Schema(description = "게시글 카테고리", example = "CASUAL") private PostCategory category; - @Schema(description = "선택지 옵션 리스트", example = "[{\"title\": \"선택지 제목1\", \"description\": \"선택지 내용1\" , \"storedFileName\": null}," + - "{\"title\": \"선택지 제목2\", \"description\": \"선택지 내용2\", \"storedFileName\": null}]") - private List balanceOptions; + @Schema(description = "선택지 옵션 리스트", example = + "[{\"title\": \"선택지 제목1\", \"description\": \"선택지 내용1\" , " + + "\"storedFileName\": 4df23447-2355-45h2-8783-7f6gd2ceb848_강아지.jpg}," + + "{\"title\": \"선택지 제목2\", \"description\": \"선택지 내용2\", " + + "\"storedFileName\": 4df23447-2355-45h2-8783-7f6gd2ceb848_고양이.jpg}]") + private List balanceOptions; @Schema(description = "태그 리스트", example = "[\"태그1\", \"태그2\", \"태그3\"]") private List tags; @@ -55,15 +64,16 @@ public Post toEntity(Member member, List images) { private List getBalanceOptions(List images) { if (images.isEmpty()) { return balanceOptions.stream() - .map(balanceOptionDto -> balanceOptionDto.toEntity(null)) + .map(balanceOption -> balanceOption.toEntity(null)) .collect(Collectors.toList()); } else { Map fileNameToFileMap = images.stream() .collect(Collectors.toMap(File::getStoredName, Function.identity())); return balanceOptions.stream() - .map(balanceOptionDto -> balanceOptionDto.toEntity(fileNameToFileMap.getOrDefault(balanceOptionDto.getStoredFileName(), - null))) + .map(balanceOption -> + balanceOption.toEntity( + fileNameToFileMap.getOrDefault(balanceOption.getStoredImageName(), null))) .collect(Collectors.toList()); } } diff --git a/src/main/java/balancetalk/module/post/dto/PostResponse.java b/src/main/java/balancetalk/module/post/dto/PostResponse.java index e3bd9efd0..289dde1fe 100644 --- a/src/main/java/balancetalk/module/post/dto/PostResponse.java +++ b/src/main/java/balancetalk/module/post/dto/PostResponse.java @@ -42,9 +42,11 @@ public class PostResponse { @Schema(description = "게시글 카테고리", example = "CASUAL") private PostCategory category; - @Schema(description = "선택지 옵션 리스트", example = "[{\"title\": \"선택지 제목1\", \"description\": \"선택지 내용1\" , \"storedFileName\": null}," + - "{\"title\": \"선택지 제목2\", \"description\": \"선택지 내용2\", \"storedFileName\": null}]") - private List balanceOptions; + @Schema(description = "선택지 옵션 리스트", example = + "[{\"title\": \"선택지 제목1\", \"description\": \"선택지 내용1\" , \"storedFileName\": null}," + + "{\"title\": \"선택지 제목2\", \"description\": \"선택지 내용2\", " + + "\"imageUrl\": https://balance-talk-static-files/4df23447-2355-45h2-8783-7f6gd2ceb848_고양이.jpg}]") + private List balanceOptions; @Schema(description = "태그 리스트", example = "[\"태그1\", \"태그2\", \"태그3\"]") private List postTags; @@ -81,9 +83,9 @@ private static List getPostTags(Post post) { .collect(Collectors.toList()); } - private static List getBalanceOptions(Post post) { + private static List getBalanceOptions(Post post) { return post.getOptions().stream() - .map(BalanceOptionDto::fromEntity) + .map(BalanceOptionResponse::fromEntity) .collect(Collectors.toList()); } } diff --git a/src/main/java/balancetalk/module/post/presentation/PostController.java b/src/main/java/balancetalk/module/post/presentation/PostController.java index 17cae7e72..1560f12df 100644 --- a/src/main/java/balancetalk/module/post/presentation/PostController.java +++ b/src/main/java/balancetalk/module/post/presentation/PostController.java @@ -6,6 +6,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; + +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -23,7 +25,7 @@ public class PostController { @ResponseStatus(HttpStatus.CREATED) @PostMapping @Operation(summary = "게시글 생성" , description = "로그인 상태인 회원이 게시글을 작성한다.") - public PostResponse createPost(@RequestBody final PostRequest postRequestDto) { + public PostResponse createPost(@Valid @RequestBody final PostRequest postRequestDto) { return postService.save(postRequestDto); } diff --git a/src/main/java/balancetalk/module/vote/dto/VoteRequest.java b/src/main/java/balancetalk/module/vote/dto/VoteRequest.java index ca07f3572..201c70a26 100644 --- a/src/main/java/balancetalk/module/vote/dto/VoteRequest.java +++ b/src/main/java/balancetalk/module/vote/dto/VoteRequest.java @@ -13,7 +13,7 @@ @Builder public class VoteRequest { - @Schema(description = "투표한 선택지 id", example = "23") + @Schema(description = "투표한 선택지 id", example = "2") private Long selectedOptionId; @Schema(description = "회원 여부", example = "true") diff --git a/src/main/java/balancetalk/module/vote/presentation/VoteController.java b/src/main/java/balancetalk/module/vote/presentation/VoteController.java index ce15320f5..48412c576 100644 --- a/src/main/java/balancetalk/module/vote/presentation/VoteController.java +++ b/src/main/java/balancetalk/module/vote/presentation/VoteController.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -24,7 +25,7 @@ public class VoteController { @PostMapping @Operation(summary = "선택지 투표", description = "post-id에 해당하는 게시글에서 마음에 드는 선택지에 투표한다.") @ApiResponse(responseCode = "201", description = "투표가 정상적으로 처리되었습니다.") - public String createVote(@PathVariable Long postId, @RequestBody VoteRequest voteRequest) { + public String createVote(@PathVariable Long postId, final @Valid @RequestBody VoteRequest voteRequest) { voteService.createVote(postId, voteRequest); return "투표가 정상적으로 처리되었습니다."; } diff --git a/src/test/java/balancetalk/module/post/application/PostServiceTest.java b/src/test/java/balancetalk/module/post/application/PostServiceTest.java index 60cb6d9e9..709160162 100644 --- a/src/test/java/balancetalk/module/post/application/PostServiceTest.java +++ b/src/test/java/balancetalk/module/post/application/PostServiceTest.java @@ -33,10 +33,10 @@ import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.*; + import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class PostServiceTest {