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
2 changes: 1 addition & 1 deletion server-config
6 changes: 2 additions & 4 deletions src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
== 인증 API

[[카카오-로그인]]
=== 카카오 로그인
=== `POST` 카카오 로그인

operation::auth-controller-test/kakao-o-auth-sign-in[snippets='http-request,curl-request,request-fields,http-response,response-cookies,response-fields']

[[네이버-로그인]]

[[토큰-재발급]]
=== 토큰 재발급
=== `POST` 토큰 재발급

operation::auth-controller-test/reissue[snippets='http-request,curl-request,request-cookies,http-response,response-cookies,response-fields']
2 changes: 1 addition & 1 deletion src/docs/asciidoc/images.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
== 이미지 API

[[이미지-업로드]]
=== 이미지 업로드
=== `POST` 이미지 업로드

operation::image-controller-test/create-image-file[snippets='http-request,curl-request,request-headers,request-parts,http-response,response-fields']
10 changes: 10 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ Content-Type: application/json;charset=UTF-8
Authorization: Bearer accessToken
```

#### 테스트용 개발 토큰

```
user1
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE3NDAyOTQyMzEsImlzcyI6InN3eXA4dGVhbTIiLCJleHAiOjMzMjc2Mjk0MjMxfQ.gqA245tRiBQB9owKRWIpX1we1T362R-xDTt4YT9AhRY

user2
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIiLCJpYXQiOjE3NDA0NDM0ODIsImlzcyI6InN3eXA4dGVhbTIiLCJleHAiOjMzMjc2NDQzNDgyfQ.2sTlCtSHb4eGzhlL6WlRT6xvJLtvipnHp6EAmC4j1UQ
```

[[인증-예외]]
=== 인증 예외

Expand Down
6 changes: 3 additions & 3 deletions src/docs/asciidoc/posts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
== 게시글 API

[[게시글-작성]]
=== 게시글 작성
=== `POST` 게시글 작성

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

Expand All @@ -12,12 +12,12 @@ 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']

[[내가-작성한-게시글-조회]]
=== 내가 작성한 게시글 조회 (미구현)
=== `GET` 내가 작성한 게시글 조회

operation::post-controller-test/find-my-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields']

[[내가-참여한-게시글-조회]]
=== 내가 참여한 게시글 조회 (미구현)
=== `GET` 내가 참여한 게시글 조회

operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields']

Expand Down
12 changes: 6 additions & 6 deletions src/docs/asciidoc/votes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
== 투표 API

[[투표]]
=== 투표 (미구현)
=== `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']
// 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']
// operation::vote-controller-test/guest-change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response']
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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;

@Repository
Expand All @@ -14,11 +15,12 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
FROM Comment c
WHERE c.postId = :postId
AND (:cursor is null or c.id > :cursor)
ORDER BY c.createdAt DESC
ORDER BY c.createdAt DESC
""")
Slice<Comment> findByPostId(
Long postId,
Long cursor,
Pageable pageable);
@Param("postId") Long postId,
@Param("cursor") Long cursor,
Pageable pageable
);

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.swyp8team2.comment.presentation.dto.CreateCommentRequest;
import com.swyp8team2.common.dto.CursorBasePaginatedResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand Down Expand Up @@ -42,8 +43,8 @@ public ResponseEntity<Void> createComment(
@GetMapping("")
public ResponseEntity<CursorBasePaginatedResponse<CommentResponse>> selectComments(
@PathVariable("postId") Long postId,
@RequestParam(value = "cursor", required = false) Long cursor,
@RequestParam(value = "size", required = false, defaultValue = "10") int size,
@RequestParam(value = "cursor", required = false) @Min(0) Long cursor,
@RequestParam(value = "size", required = false, defaultValue = "10") @Min(1) int size,
@AuthenticationPrincipal UserInfo userInfo
) {
CursorBasePaginatedResponse<CommentResponse> response = commentService.findComments(postId, cursor, size);
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/swyp8team2/common/dev/DataInitConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.swyp8team2.common.dev;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Profile({"dev", "local"})
@Component
@RequiredArgsConstructor
public class DataInitConfig {

private final DataInitializer dataInitializer;

@PostConstruct
public void init() {
dataInitializer.init();
}
}
39 changes: 36 additions & 3 deletions src/main/java/com/swyp8team2/common/dev/DataInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,59 @@

import com.swyp8team2.auth.application.jwt.JwtService;
import com.swyp8team2.auth.application.jwt.TokenPair;
import com.swyp8team2.image.domain.ImageFile;
import com.swyp8team2.image.domain.ImageFileRepository;
import com.swyp8team2.image.presentation.dto.ImageFileDto;
import com.swyp8team2.post.domain.Post;
import com.swyp8team2.post.domain.PostImage;
import com.swyp8team2.post.domain.PostRepository;
import com.swyp8team2.user.domain.User;
import com.swyp8team2.user.domain.UserRepository;
import com.swyp8team2.vote.application.VoteService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@Profile({"dev", "local"})
@Component
@RequiredArgsConstructor
public class DataInitializer {

private final UserRepository userRepository;
private final ImageFileRepository imageFileRepository;
private final PostRepository postRepository;
private final JwtService jwtService;
private final VoteService voteService;

@PostConstruct
@Transactional
public void init() {
User save = userRepository.save(User.create("nickname", "defailt_profile_image"));
TokenPair tokenPair = jwtService.createToken(save.getId());
User testUser = userRepository.save(User.create("nickname", "defailt_profile_image"));
TokenPair tokenPair = jwtService.createToken(testUser.getId());
System.out.println("accessToken = " + tokenPair.accessToken());
System.out.println("refreshToken = " + tokenPair.refreshToken());
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = userRepository.save(User.create("nickname" + i, "defailt_profile_image"));
users.add(user);
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")));
postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"));
}
}
List<Post> posts = postRepository.findAll();
for (User user : users) {
for (Post post : posts) {
Random random = new Random();
int num = random.nextInt(2);
voteService.vote(user.getId(), post.getId(), post.getImages().get(num).getId());
}
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/swyp8team2/common/domain/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

import java.time.LocalDateTime;

@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public abstract class BaseEntity {

@CreatedBy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.web.bind.MethodArgumentNotValidException;
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.servlet.resource.NoResourceFoundException;

import javax.naming.AuthenticationException;
Expand Down Expand Up @@ -46,6 +47,12 @@ public ResponseEntity<ErrorResponse> handle(NoResourceFoundException e) {
return ResponseEntity.notFound().build();
}

@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<ErrorResponse> handle(HandlerMethodValidationException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT));
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handle(AuthenticationException e) {
log.info(e.getMessage());
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 @@ -30,6 +30,8 @@ public enum ErrorCode {
INVALID_INPUT_VALUE("잘못된 입력 값"),
SOCIAL_AUTHENTICATION_FAILED("소셜 로그인 실패"),
POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"),
IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"),
POST_IMAGE_NOT_FOUND("게시글 이미지 없음"),

//503
SERVICE_UNAVAILABLE("서비스 이용 불가"),
Expand Down
72 changes: 72 additions & 0 deletions src/main/java/com/swyp8team2/post/application/PostService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
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.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.PostResponse;
import com.swyp8team2.post.presentation.dto.SimplePostResponse;
import com.swyp8team2.post.presentation.dto.VoteResponseDto;
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.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -18,6 +33,9 @@ public class PostService {

private final PostRepository postRepository;
private final UserRepository userRepository;
private final RatioCalculator ratioCalculator;
private final ImageFileRepository imageFileRepository;
private final VoteRepository voteRepository;

@Transactional
public Long create(Long userId, CreatePostRequest request) {
Expand All @@ -35,4 +53,58 @@ private List<PostImage> createPostImages(CreatePostRequest request) {
voteRequestDto.imageFileId()
)).toList();
}

public PostResponse findById(Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND));
User user = userRepository.findById(post.getUserId())
.orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND));
List<PostImage> images = post.getImages();
List<VoteResponseDto> votes = images.stream()
.map(image -> createVoteResponseDto(image, images))
.toList();
return PostResponse.of(post, user, votes);
}

private VoteResponseDto createVoteResponseDto(PostImage image, List<PostImage> images) {
ImageFile imageFile = imageFileRepository.findById(image.getImageFileId())
.orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND));
return new VoteResponseDto(
image.getId(),
imageFile.getImageUrl(),
image.getVoteCount(),
ratioCalculator.calculateRatio(getTotalVoteCount(images), image.getVoteCount()),
false //TODO: implement
);
}

private int getTotalVoteCount(List<PostImage> images) {
int totalVoteCount = 0;
for (PostImage image : images) {
totalVoteCount += image.getVoteCount();
}
return totalVoteCount;
}

public CursorBasePaginatedResponse<SimplePostResponse> findMyPosts(Long userId, Long cursor, int size) {
Slice<Post> postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size));
return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse)
);
}

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<SimplePostResponse> findVotedPosts(Long userId, Long cursor, int size) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND));
List<Long> postIds = voteRepository.findByUserSeq(user.getSeq())
.map(Vote::getPostId)
.toList();
Slice<Post> postSlice = postRepository.findByIdIn(postIds, cursor, PageRequest.ofSize(size));
return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse));
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/swyp8team2/post/application/RatioCalculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.swyp8team2.post.application;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.math.RoundingMode;

@Component
public class RatioCalculator {

public String calculateRatio(int totalVoteCount, int voteCount) {
if (totalVoteCount == 0) {
return "0.0";
}
BigDecimal totalCount = new BigDecimal(totalVoteCount);
BigDecimal count = new BigDecimal(voteCount);
BigDecimal bigDecimal = count.divide(totalCount, 3, RoundingMode.HALF_UP)
.multiply(new BigDecimal(100));
return String.format("%.1f", bigDecimal);
}
}
Loading