Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/docs/asciidoc/comments.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
== 댓글 API

[[댓글-생성]]
=== 댓글 생성
=== `POST` 댓글 생성

operation::comment-controller-test/create-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response']

[[댓글-조회]]
=== 댓글 조회
=== `GET` 댓글 조회

operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields']

Expand Down
9 changes: 8 additions & 1 deletion src/docs/asciidoc/posts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ 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/close-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response']

[[게시글-수정]]

[[게시글-삭제]]
=== 게시글 삭제 (미구현)
=== `DELETE` 게시글 삭제

operation::post-controller-test/delete-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response']
2 changes: 1 addition & 1 deletion src/docs/asciidoc/users.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
== 유저 API

[[유저-정보-조회]]
=== 유저 정보 조회 (미구현)
=== `GET` 유저 정보 조회

operation::user-controller-test/find-user-info[snippets='http-request,curl-request,path-parameters,http-response,response-fields']
2 changes: 1 addition & 1 deletion src/docs/asciidoc/votes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response']

[[게스트-투표]]
=== `POST` 게스트 투표 (미구현)
=== `POST` 게스트 투표

operation::vote-controller-test/guest-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response']

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public record CommentResponse(
Long commentId,
String content,
AuthorDto author,
Long voteId,
Long imageId,
LocalDateTime createdAt
) implements CursorDto {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import javax.naming.AuthenticationException;
Expand Down Expand Up @@ -68,6 +70,20 @@ public ResponseEntity<ErrorResponse> handle(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN));
}

@ExceptionHandler(MissingRequestHeaderException.class)
public ResponseEntity<ErrorResponse> handle(MissingRequestHeaderException e) {
log.debug("MissingRequestHeaderException {}", e.getMessage());
return ResponseEntity.badRequest()
.body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT));
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handle(MethodArgumentTypeMismatchException e) {
log.debug("MethodArgumentTypeMismatchException {}", e.getMessage());
return ResponseEntity.badRequest()
.body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handle(Exception e) {
log.error("Exception", e);
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/swyp8team2/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public enum ErrorCode {
POST_NOT_FOUND("존재하지 않는 게시글"),
DESCRIPTION_LENGTH_EXCEEDED("게시글 설명 길이 초과"),
INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"),
NOT_POST_AUTHOR("게시글 작성자가 아님"),
POST_ALREADY_CLOSED("이미 마감된 게시글"),

//401
EXPIRED_TOKEN("토큰 만료"),
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/com/swyp8team2/post/application/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public List<PostImageVoteStatusResponse> findPostStatus(Long postId) {
int totalVoteCount = getTotalVoteCount(post.getImages());
return post.getImages().stream()
.map(image -> {
String ratio = ratioCalculator.calculate(image.getVoteCount(), totalVoteCount);
String ratio = ratioCalculator.calculate(totalVoteCount, image.getVoteCount());
return new PostImageVoteStatusResponse(image.getId(), image.getName(), image.getVoteCount(), ratio);
}).toList();
}
Expand All @@ -131,4 +131,19 @@ private int getTotalVoteCount(List<PostImage> images) {
}
return totalVoteCount;
}

@Transactional
public void delete(Long userId, Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND));
post.validateOwner(userId);
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);
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/swyp8team2/post/domain/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,24 @@ public void cancelVote(Long imageId) {
.orElseThrow(() -> new InternalServerException(ErrorCode.POST_IMAGE_NOT_FOUND));
image.decreaseVoteCount();
}

public void close(Long userId) {
validateOwner(userId);
if (state == State.CLOSED) {
throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED);
}
this.state = State.CLOSED;
}

public void validateOwner(Long userId) {
if (!this.userId.equals(userId)) {
throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR);
}
}

public void validateProgress() {
if (!this.state.equals(State.PROGRESS)) {
throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED);
}
}
}
15 changes: 13 additions & 2 deletions src/main/java/com/swyp8team2/post/presentation/PostController.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down Expand Up @@ -80,11 +81,21 @@ public ResponseEntity<PostResponse> findPost(@PathVariable("shareUrl") String sh
));
}

@DeleteMapping("/{shareUrl}")
@PostMapping("/{postId}/close")
public ResponseEntity<Void> closePost(
@PathVariable("postId") Long postId,
@AuthenticationPrincipal UserInfo userInfo
) {
postService.close(userInfo.userId(), postId);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{postId}")
public ResponseEntity<PostResponse> deletePost(
@PathVariable("shareUrl") String shareUrl,
@PathVariable("postId") Long postId,
@AuthenticationPrincipal UserInfo userInfo
) {
postService.delete(userInfo.userId(), postId);
return ResponseEntity.ok().build();
}

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/swyp8team2/user/application/UserService.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.swyp8team2.user.application;

import com.swyp8team2.common.exception.BadRequestException;
import com.swyp8team2.common.exception.ErrorCode;
import com.swyp8team2.user.domain.User;
import com.swyp8team2.user.domain.UserRepository;
import com.swyp8team2.user.presentation.dto.UserInfoResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {

Expand All @@ -29,4 +33,10 @@ private String getNickname(String email) {
return Optional.ofNullable(email)
.orElseGet(() -> "user_" + System.currentTimeMillis());
}

public UserInfoResponse findById(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND));
return UserInfoResponse.of(user);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.swyp8team2.user.presentation;

import com.swyp8team2.user.application.UserService;
import com.swyp8team2.user.presentation.dto.UserInfoResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -13,8 +14,10 @@
@RequestMapping("/users")
public class UserController {

private final UserService userService;

@GetMapping("/{userId}")
public ResponseEntity<UserInfoResponse> findUserInfo(@PathVariable("userId") Long userId) {
return ResponseEntity.ok(new UserInfoResponse(1L, "nickname", "https://image.com/profile-image"));
return ResponseEntity.ok(userService.findById(userId));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.swyp8team2.user.presentation.dto;

import com.swyp8team2.user.domain.User;

public record UserInfoResponse(
Long id,
String nickname,
String profileUrl
) {
public static UserInfoResponse of(User user) {
return new UserInfoResponse(user.getId(), user.getNickname(), user.getProfileUrl());
}
}
41 changes: 26 additions & 15 deletions src/main/java/com/swyp8team2/vote/application/VoteService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

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.user.domain.User;
import com.swyp8team2.user.domain.UserRepository;
import com.swyp8team2.vote.domain.Vote;
import com.swyp8team2.vote.domain.VoteRepository;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -21,27 +23,36 @@ public class VoteService {
private final PostRepository postRepository;

@Transactional
public Long vote(Long userId, Long postId, Long imageId) {
User user = userRepository.findById(userId)
public Long vote(Long voterId, Long postId, Long imageId) {
User voter = userRepository.findById(voterId)
.orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND));
voteRepository.findByUserSeqAndPostId(user.getSeq(), postId)
.ifPresent(vote -> deleteExistingVote(postId, vote));
Vote vote = createVote(postId, imageId, user);
deleteVoteIfExisting(postId, voter.getSeq());
Vote vote = createVote(postId, imageId, voter.getSeq());
return vote.getId();
}

private Vote createVote(Long postId, Long imageId, User user) {
Vote vote = voteRepository.save(Vote.of(postId, imageId, user.getSeq()));
postRepository.findById(postId)
.orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND))
.vote(imageId);
private void deleteVoteIfExisting(Long postId, String userSeq) {
voteRepository.findByUserSeqAndPostId(userSeq, postId)
.ifPresent(vote -> {
voteRepository.delete(vote);
postRepository.findById(postId)
.orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND))
.cancelVote(vote.getPostImageId());
});
}

private Vote createVote(Long postId, Long imageId, String userSeq) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND));
post.validateProgress();
Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userSeq));
post.vote(imageId);
return vote;
}

private void deleteExistingVote(Long postId, Vote vote) {
voteRepository.delete(vote);
postRepository.findById(postId)
.orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND))
.cancelVote(vote.getPostImageId());
public Long guestVote(String guestId, Long postId, Long imageId) {
deleteVoteIfExisting(postId, guestId);
Vote vote = createVote(postId, imageId, guestId);
return vote.getId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
import com.swyp8team2.common.presentation.CustomHeader;
import com.swyp8team2.vote.application.VoteService;
import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest;
import com.swyp8team2.vote.presentation.dto.GuestVoteRequest;
import com.swyp8team2.vote.presentation.dto.VoteRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
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;
Expand All @@ -33,7 +30,7 @@ public ResponseEntity<Void> vote(
@Valid @RequestBody VoteRequest request,
@AuthenticationPrincipal UserInfo userInfo
) {
voteService.vote(userInfo.userId(), postId, request.voteId());
voteService.vote(userInfo.userId(), postId, request.imageId());
return ResponseEntity.ok().build();
}

Expand All @@ -43,6 +40,7 @@ public ResponseEntity<Void> guestVote(
@RequestHeader(CustomHeader.GUEST_ID) String guestId,
@Valid @RequestBody VoteRequest request
) {
voteService.guestVote(guestId, postId, request.imageId());
return ResponseEntity.ok().build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

public record ChangeVoteRequest(
@NotNull
Long voteId
Long imageId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

public record VoteRequest(
@NotNull
Long voteId
Long imageId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ void findComments() throws Exception {
fieldWithPath("data[].author.profileUrl")
.type(JsonFieldType.STRING)
.description("작성자 프로필 이미지 url"),
fieldWithPath("data[].voteId")
fieldWithPath("data[].imageId")
.type(JsonFieldType.NUMBER)
.optional()
.description("작성자 투표 Id (투표 없을 시 null)"),
.description("작성자가 투표한 이미지 Id (투표 없을 시 null)"),
fieldWithPath("data[].createdAt")
.type(JsonFieldType.STRING)
.description("댓글 작성일")
Expand Down
Loading