diff --git a/build.gradle b/build.gradle index 1d26fe12..94d4ae88 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,7 @@ dependencies { implementation 'com.google.code.findbugs:jsr305:3.0.2' // webflux implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64' } dependencyManagement { diff --git a/src/main/java/com/server/capple/config/security/SecurityConfig.java b/src/main/java/com/server/capple/config/security/SecurityConfig.java index 2410ca51..7a00c445 100644 --- a/src/main/java/com/server/capple/config/security/SecurityConfig.java +++ b/src/main/java/com/server/capple/config/security/SecurityConfig.java @@ -58,6 +58,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/reports", "/reports/**").authenticated() .requestMatchers("/boards", "/boards/**").authenticated() .requestMatchers("/boardComments", "/boardComments/**").authenticated() + .requestMatchers("/dummy","/dummy/**").hasRole(Role.ROLE_ADMIN.getName()) .anyRequest().denyAll()); http .addFilterBefore(new JwtFilter(jwtService), UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/server/capple/domain/answer/dto/AnswerResponse.java b/src/main/java/com/server/capple/domain/answer/dto/AnswerResponse.java index 9c4594e4..7f35b5d3 100644 --- a/src/main/java/com/server/capple/domain/answer/dto/AnswerResponse.java +++ b/src/main/java/com/server/capple/domain/answer/dto/AnswerResponse.java @@ -16,11 +16,14 @@ public static class AnswerId { @Builder public static class AnswerInfo { private Long answerId; + private Long writerId; private String profileImage; private String nickname; private String content; - private Boolean isMyAnswer; + private Boolean isMine; private Boolean isReported; + private Boolean isLiked; + private String writeAt; } @Getter @@ -44,11 +47,13 @@ public static class AnswerLike { public static class MemberAnswerInfo { private Long questionId; private Long answerId; + private Long writerId; private String nickname; private String profileImage; private String content; private int heartCount; private String writeAt; + private Boolean isLiked; } @Getter diff --git a/src/main/java/com/server/capple/domain/answer/mapper/AnswerMapper.java b/src/main/java/com/server/capple/domain/answer/mapper/AnswerMapper.java index 295adb23..4900087b 100644 --- a/src/main/java/com/server/capple/domain/answer/mapper/AnswerMapper.java +++ b/src/main/java/com/server/capple/domain/answer/mapper/AnswerMapper.java @@ -10,7 +10,6 @@ import com.server.capple.domain.question.entity.Question; import org.springframework.stereotype.Component; -import java.time.format.DateTimeFormatter; import java.util.List; @Component @@ -29,26 +28,31 @@ public AnswerList toAnswerList(List answerInfoList) { .build(); } - public AnswerInfo toAnswerInfo(Answer answer, Long memberId, Boolean isReported) { + public AnswerInfo toAnswerInfo(Answer answer, Long memberId, Boolean isReported, Boolean isLiked, Boolean isMine) { return AnswerInfo.builder() .answerId(answer.getId()) + .writerId(answer.getMember().getId()) .profileImage(answer.getMember().getProfileImage()) .nickname(answer.getMember().getNickname()) .content(answer.getContent()) - .isMyAnswer(answer.getMember().getId() == memberId) + .isMine(isMine) .isReported(isReported) + .isLiked(isLiked) + .writeAt(answer.getCreatedAt().toString()) .build(); } - public MemberAnswerInfo toMemberAnswerInfo(Answer answer, int heartCount) { + public MemberAnswerInfo toMemberAnswerInfo(Answer answer, int heartCount, Boolean isLiked) { return MemberAnswerInfo.builder() .questionId(answer.getQuestion().getId()) .answerId(answer.getId()) + .writerId(answer.getMember().getId()) .nickname(answer.getMember().getNickname()) .profileImage(answer.getMember().getProfileImage()) .content(answer.getContent()) .heartCount(heartCount) - .writeAt(answer.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"))) + .writeAt(answer.getCreatedAt().toString()) + .isLiked(isLiked) .build(); } diff --git a/src/main/java/com/server/capple/domain/answer/repository/AnswerHeartRedisRepository.java b/src/main/java/com/server/capple/domain/answer/repository/AnswerHeartRedisRepository.java index 81bc8814..1b5bab35 100644 --- a/src/main/java/com/server/capple/domain/answer/repository/AnswerHeartRedisRepository.java +++ b/src/main/java/com/server/capple/domain/answer/repository/AnswerHeartRedisRepository.java @@ -54,4 +54,10 @@ public Set getMemberHeartsAnswer(Long memberId) { } return answerIds; } + + public boolean isMemberLikedAnswer(Long memberId, Long answerId) { + String key = ANSWER_HEART_KEY_PREFIX + answerId; + String memberKey = MEMBER_KEY_PREFIX + memberId; + return redisTemplate.opsForSet().isMember(key, memberKey); + } } diff --git a/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java b/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java index 027bbb24..1dad6dd0 100644 --- a/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java +++ b/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java @@ -2,6 +2,7 @@ import com.server.capple.domain.answer.dto.AnswerRequest; import com.server.capple.domain.answer.dto.AnswerResponse; +import com.server.capple.domain.answer.dto.AnswerResponse.AnswerLike; import com.server.capple.domain.answer.dto.AnswerResponse.AnswerList; import com.server.capple.domain.answer.dto.AnswerResponse.MemberAnswerList; import com.server.capple.domain.answer.entity.Answer; @@ -40,8 +41,13 @@ public AnswerResponse.AnswerId createAnswer(Member loginMember, Long questionId, Member member = memberService.findMember(loginMember.getId()); Question question = questionService.findQuestion(questionId); + if (answerRepository.existsByQuestionAndMember(question, loginMember)) { + throw new RestApiException(AnswerErrorCode.ANSWER_ALREADY_EXIST); + } + //답변 저장 Answer answer = answerRepository.save(answerMapper.toAnswerEntity(request, member, question)); +// answer.getQuestion().increaseCommentCount(); return new AnswerResponse.AnswerId(answer.getId()); } @@ -66,6 +72,8 @@ public AnswerResponse.AnswerId deleteAnswer(Member loginMember, Long answerId) { Answer answer = findAnswer(answerId); checkPermission(loginMember, answer); +// answer.getQuestion().decreaseCommentCount(); + answer.delete(); @@ -75,11 +83,11 @@ public AnswerResponse.AnswerId deleteAnswer(Member loginMember, Long answerId) { //답변 좋아요 / 취소 @Override - public AnswerResponse.AnswerLike toggleAnswerHeart(Member loginMember, Long answerId) { + public AnswerLike toggleAnswerHeart(Member loginMember, Long answerId) { Member member = memberService.findMember(loginMember.getId()); Boolean isLiked = answerHeartRedisRepository.toggleAnswerHeart(member.getId(), answerId); - return new AnswerResponse.AnswerLike(answerId, isLiked); + return new AnswerLike(answerId, isLiked); } @Override @@ -90,14 +98,14 @@ public AnswerList getAnswerList(Long memberId, Long questionId, String keyword, answerRepository.findByQuestion(questionId, pageable).orElseThrow(() -> new RestApiException(AnswerErrorCode.ANSWER_NOT_FOUND)) .stream() - .map(answer -> answerMapper.toAnswerInfo(answer, memberId, reportRepository.existsReportByAnswer(answer))) + .map(answer -> answerMapper.toAnswerInfo(answer, memberId, reportRepository.existsReportByAnswer(answer), answerHeartRedisRepository.isMemberLikedAnswer(memberId, answer.getId()), answer.getMember().getId().equals(memberId))) .toList()); } else { return answerMapper.toAnswerList( answerRepository.findByQuestionAndKeyword(questionId, keyword, pageable).orElseThrow(() -> new RestApiException(AnswerErrorCode.ANSWER_NOT_FOUND)) .stream() - .map(answer -> answerMapper.toAnswerInfo(answer, memberId, reportRepository.existsReportByAnswer(answer))) + .map(answer -> answerMapper.toAnswerInfo(answer, memberId, reportRepository.existsReportByAnswer(answer), answerHeartRedisRepository.isMemberLikedAnswer(memberId, answer.getId()), answer.getMember().getId().equals(memberId))) .toList()); } @@ -109,7 +117,7 @@ public MemberAnswerList getMemberAnswer(Member member) { List answers = answerRepository.findByMember(member).orElse(null); return answerMapper.toMemberAnswerList( answers.stream() - .map(answer -> answerMapper.toMemberAnswerInfo(answer, answerHeartRedisRepository.getAnswerHeartsCount(answer.getId()))) + .map(answer -> answerMapper.toMemberAnswerInfo(answer, answerHeartRedisRepository.getAnswerHeartsCount(answer.getId()), answerHeartRedisRepository.isMemberLikedAnswer(member.getId(), answer.getId()))) .toList() ); } @@ -120,12 +128,11 @@ public MemberAnswerList getMemberHeartAnswer(Member member) { return answerMapper.toMemberAnswerList( answerHeartRedisRepository.getMemberHeartsAnswer(member.getId()) .stream() - .map(answerId -> answerMapper.toMemberAnswerInfo(findAnswer((answerId)), answerHeartRedisRepository.getAnswerHeartsCount(answerId))) + .map(answerId -> answerMapper.toMemberAnswerInfo(findAnswer((answerId)), answerHeartRedisRepository.getAnswerHeartsCount(answerId), answerHeartRedisRepository.isMemberLikedAnswer(member.getId(), answerId))) .toList() ); } - //로그인된 유저와 작성자가 같은지 체크 private void checkPermission(Member loginMember, Answer answer) { Member member = memberService.findMember(loginMember.getId()); diff --git a/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java b/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java index 6f77d4a1..f877645b 100644 --- a/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java +++ b/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java @@ -25,7 +25,7 @@ public static class AnswerCommentHeart { @Builder public static class AnswerCommentInfo { private Long answerCommentId; - private String writer; + private Long writerId; private String content; private Long heartCount; private LocalDateTime createdAt; diff --git a/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java index d8b8816f..698cf40a 100644 --- a/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java +++ b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java @@ -20,7 +20,7 @@ public AnswerComment toAnswerCommentEntity(Member member, Answer answer, String public AnswerCommentInfo toAnswerCommentInfo(AnswerComment comment, Long heartCount) { return AnswerCommentInfo.builder() .answerCommentId(comment.getId()) - .writer(comment.getMember().getNickname()) + .writerId(comment.getMember().getId()) .content(comment.getContent()) .heartCount(heartCount) .createdAt(comment.getCreatedAt()) diff --git a/src/main/java/com/server/capple/domain/board/controller/BoardController.java b/src/main/java/com/server/capple/domain/board/controller/BoardController.java index 82035305..cd17a43e 100644 --- a/src/main/java/com/server/capple/domain/board/controller/BoardController.java +++ b/src/main/java/com/server/capple/domain/board/controller/BoardController.java @@ -1,23 +1,17 @@ package com.server.capple.domain.board.controller; import com.server.capple.config.security.AuthMember; -import com.server.capple.domain.answer.dto.AnswerResponse; import com.server.capple.domain.board.dto.BoardRequest; import com.server.capple.domain.board.dto.BoardResponse; import com.server.capple.domain.board.entity.BoardType; import com.server.capple.domain.board.service.BoardService; import com.server.capple.domain.member.entity.Member; -import com.server.capple.domain.question.dto.response.QuestionResponse; import com.server.capple.global.common.BaseResponse; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; @Tag(name = "게시판 API", description = "게시판 관련 API") @@ -40,17 +34,32 @@ private BaseResponse createBoard( return BaseResponse.onSuccess(boardService.createBoard(member, request.getBoardType(), request.getContent())); } - @Operation(summary = "카테고리별 게시글 조회 API", description = "카테고리별 게시글을 조회합니다.") + @Operation(summary = "카테고리별 게시글 조회 with REDIS API(프론트 사용 X, 성능 테스트 용)", description = "카테고리별 게시글을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + }) + @GetMapping("/redis") + private BaseResponse getBoardsByBoardTypeWithRedis( + @AuthMember Member member, + @RequestParam(name = "boardType", required = false) BoardType boardType + // TODO: 페이징 프론트 이슈로 추후 구현 +// @PageableDefault(sort = "created_at", direction = Sort.Direction.DESC) @Parameter(hidden = true) Pageable pageable + ) { + return BaseResponse.onSuccess(boardService.getBoardsByBoardTypeWithRedis(member, boardType)); + } + + @Operation(summary = "카테고리별 게시글 조회", description = "카테고리별 게시글을 조회합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "COMMON200", description = "성공"), }) @GetMapping() private BaseResponse getBoardsByBoardType( + @AuthMember Member member, @RequestParam(name = "boardType", required = false) BoardType boardType // TODO: 페이징 프론트 이슈로 추후 구현 // @PageableDefault(sort = "created_at", direction = Sort.Direction.DESC) @Parameter(hidden = true) Pageable pageable - ) { - return BaseResponse.onSuccess(boardService.getBoardsByBoardType(boardType)); + ) { + return BaseResponse.onSuccess(boardService.getBoardsByBoardType(member, boardType)); } @Operation(summary = "게시글 삭제 API", description = "게시글을 삭제합니다.") @@ -79,8 +88,7 @@ private BaseResponse searchBoardsByKeyword( @Operation(summary = "게시글 좋아요/취소 API", description = " 게시글 좋아요/취소 API 입니다." + "pathvariable 으로 boardId를 주세요.") @PostMapping("/{boardId}/heart") - public BaseResponse toggleBoardHeart(@AuthMember Member member, @PathVariable(value = "boardId") Long boardId) { + public BaseResponse toggleBoardHeart(@AuthMember Member member, @PathVariable(value = "boardId") Long boardId) { return BaseResponse.onSuccess(boardService.toggleBoardHeart(member, boardId)); } - } diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java index 5627f505..3d40cf12 100644 --- a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java +++ b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java @@ -1,13 +1,11 @@ package com.server.capple.domain.board.dto; -import com.server.capple.domain.member.entity.Member; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; public class BoardResponse { @@ -25,7 +23,7 @@ public static class BoardCreate { @AllArgsConstructor @NoArgsConstructor public static class BoardsGetByBoardType { - private List boards = new ArrayList<>(); + private List boards; } @Getter @@ -39,6 +37,9 @@ public static class BoardsGetByBoardTypeBoardInfo { private Integer heartCount; private Integer commentCount; private LocalDateTime createAt; + private Boolean isMine; + private Boolean isReported; + private Boolean isLiked; } @Getter @@ -54,7 +55,7 @@ public static class BoardDelete { @AllArgsConstructor @NoArgsConstructor public static class BoardsSearchByKeyword { - private List boards = new ArrayList<>(); + private List boards; } @Getter @@ -74,7 +75,7 @@ public static class BoardsSearchByKeywordBoardInfo { @Builder @AllArgsConstructor @NoArgsConstructor - public static class BoardToggleHeart { + public static class ToggleBoardHeart { private Long boardId; private Boolean isLiked; } diff --git a/src/main/java/com/server/capple/domain/board/entity/Board.java b/src/main/java/com/server/capple/domain/board/entity/Board.java index af811828..fd83f5fa 100644 --- a/src/main/java/com/server/capple/domain/board/entity/Board.java +++ b/src/main/java/com/server/capple/domain/board/entity/Board.java @@ -4,6 +4,7 @@ import com.server.capple.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLRestriction; @@ -29,6 +30,25 @@ public class Board extends BaseEntity { @Column(nullable = false) private String content; - @Column(nullable = false) + @ColumnDefault("0") private Integer commentCount; + + @ColumnDefault("0") + private Integer heartCount; + + public void setHeartCount(boolean isLiked) { + if (isLiked) { + this.heartCount++; + } else { + this.heartCount--; + } + } + + public void increaseCommentCount() { + this.commentCount++; + } + + public void decreaseCommentCount() { + this.commentCount--; + } } diff --git a/src/main/java/com/server/capple/domain/board/entity/BoardHeart.java b/src/main/java/com/server/capple/domain/board/entity/BoardHeart.java new file mode 100644 index 00000000..96d02d03 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/entity/BoardHeart.java @@ -0,0 +1,34 @@ +package com.server.capple.domain.board.entity; + +import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLRestriction; + +@Getter +@Builder +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@SQLRestriction("deleted_at is null") +public class BoardHeart extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id", nullable = false) + private Board board; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + private boolean isLiked; + + public boolean toggleHeart() { + this.isLiked = !this.isLiked; + return isLiked; + } +} diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardHeartMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardHeartMapper.java new file mode 100644 index 00000000..da20afb8 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardHeartMapper.java @@ -0,0 +1,18 @@ +package com.server.capple.domain.board.mapper; + +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.entity.BoardHeart; +import com.server.capple.domain.member.entity.Member; +import org.springframework.stereotype.Component; + + +@Component +public class BoardHeartMapper { + public BoardHeart toBoardHeart(Board board, Member member) { + return BoardHeart.builder() + .board(board) + .member(member) + .isLiked(false) + .build(); + } +} diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java index 337ffe5a..c6f06663 100644 --- a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java @@ -11,24 +11,17 @@ @Component public class BoardMapper { - public Board toBoard( - Member member, - BoardType boardType, - String content, - Integer heartCount, - Integer commentCount - ) { + public Board toBoard(Member member, BoardType boardType, String content) { return Board.builder() .writer(member) .boardType(boardType) .content(content) - .commentCount(commentCount) + .commentCount(0) + .heartCount(0) .build(); } - public BoardResponse.BoardCreate toBoardCreate( - Board board - ) { + public BoardResponse.BoardCreate toBoardCreate(Board board) { return BoardResponse.BoardCreate.builder() .boardId(board.getId()) .build(); @@ -42,17 +35,43 @@ public BoardResponse.BoardsGetByBoardType toBoardsGetByBoardType( .build(); } + //redis public BoardResponse.BoardsGetByBoardTypeBoardInfo toBoardsGetByBoardTypeBoardInfo( Board board, - Integer boardHeartsCount) { + Integer boardHeartsCount, + Boolean isLiked, + Boolean isMine, + Boolean isReported) { return BoardResponse.BoardsGetByBoardTypeBoardInfo.builder() .boardId(board.getId()) .writerId(board.getWriter().getId()) .content(board.getContent()) .heartCount(boardHeartsCount) - // TODO : 댓글 작성 API 나오면 추후 구현 - .commentCount(0) + .commentCount(board.getCommentCount()) + .createAt(board.getCreatedAt()) + .isLiked(isLiked) + .isMine(isMine) + // TODO: BoardReport 관련 테이블 구현 후 수정 요망 + .isReported(isReported) + .build(); + } + + //rdb + public BoardResponse.BoardsGetByBoardTypeBoardInfo toBoardsGetByBoardTypeBoardInfo(Board board, + Boolean isLiked, + Boolean isMine, + Boolean isReported) { + return BoardResponse.BoardsGetByBoardTypeBoardInfo.builder() + .boardId(board.getId()) + .writerId(board.getWriter().getId()) + .content(board.getContent()) + .heartCount(board.getHeartCount()) + .commentCount(board.getCommentCount()) .createAt(board.getCreatedAt()) + .isLiked(isLiked) + .isMine(isMine) + // TODO: BoardReport 관련 테이블 구현 후 수정 요망 + .isReported(isReported) .build(); } diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java index 7f1432fa..04781c69 100644 --- a/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java +++ b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.time.LocalDateTime; import java.util.HashSet; +import java.util.Random; import java.util.Set; import static java.lang.Boolean.FALSE; @@ -23,10 +24,10 @@ public class BoardHeartRedisRepository implements Serializable { private final RedisTemplate redisTemplate; // 게시판 좋아요 토글 - public Boolean toggleBoardHeart(Long boardId, Long memberId) { + public Boolean toggleBoardHeart(Long memberId, Long boardId) { String key = BOARD_HEART_KEY_PREFIX + boardId.toString(); String member = MEMBER_KEY_PREFIX + memberId.toString(); - String createAtKey = key + ":" + member + ":createAt"; // member ID를 포함한 createAtKeyㄱ + String createAtKey = key + ":" + member + ":createAt"; // member ID를 포함한 createAtKey SetOperations setOperations = redisTemplate.opsForSet(); ValueOperations valueOperations = redisTemplate.opsForValue(); @@ -44,7 +45,6 @@ public Boolean toggleBoardHeart(Long boardId, Long memberId) { } } - // public String getBoardHeartCreateAt(Long boardId, Long memberId) { String createAtKey = BOARD_HEART_KEY_PREFIX + boardId.toString() + ":" + MEMBER_KEY_PREFIX + memberId.toString() + ":createAt"; return redisTemplate.opsForValue().get(createAtKey); @@ -71,4 +71,26 @@ public Set getMemberHeartsBoard(Long memberId) { } return boardIds; } + + public boolean isMemberLikedBoard(Long memberId, Long boardId) { + String key = BOARD_HEART_KEY_PREFIX + boardId; + String memberKey = MEMBER_KEY_PREFIX + memberId; + return redisTemplate.opsForSet().isMember(key, memberKey); + } + + //더미 데이터 생성용 + public void generateDummyBoardLikes(int memberCount, int boardCount) { + SetOperations setOperations = redisTemplate.opsForSet(); + + Random random = new Random(); + for (int boardId = 1; boardId <= boardCount; boardId++) { + for (int memberId = 1; memberId <= memberCount; memberId++) { + if(random.nextBoolean()) { + String key = BOARD_HEART_KEY_PREFIX + boardId; + String member = MEMBER_KEY_PREFIX + memberId; + setOperations.add(key, member); + } + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardHeartRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRepository.java new file mode 100644 index 00000000..2d0c2c3d --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRepository.java @@ -0,0 +1,13 @@ +package com.server.capple.domain.board.repository; + +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.entity.BoardHeart; +import com.server.capple.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BoardHeartRepository extends JpaRepository { + Optional findByMemberAndBoard(Member member, Board board); + +} diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java index 789beffb..62ab079f 100644 --- a/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java +++ b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java @@ -1,15 +1,11 @@ package com.server.capple.domain.board.repository; -import com.server.capple.domain.answer.entity.Answer; import com.server.capple.domain.board.entity.Board; import com.server.capple.domain.board.entity.BoardType; -import io.lettuce.core.dynamic.annotation.Param; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; -import java.util.Optional; public interface BoardRepository extends JpaRepository { diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java index 92c5a005..2062bbb1 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardService.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -1,21 +1,22 @@ package com.server.capple.domain.board.service; -import com.server.capple.domain.board.dto.BoardResponse; import com.server.capple.domain.board.dto.BoardResponse.*; import com.server.capple.domain.board.entity.Board; import com.server.capple.domain.board.entity.BoardType; import com.server.capple.domain.member.entity.Member; -import org.springframework.data.domain.Pageable; public interface BoardService { BoardCreate createBoard(Member member, BoardType boardType, String content); - BoardsGetByBoardType getBoardsByBoardType(BoardType boardType); + BoardsGetByBoardType getBoardsByBoardTypeWithRedis(Member member, BoardType boardType); + + BoardsGetByBoardType getBoardsByBoardType(Member member, BoardType boardType); BoardDelete deleteBoard(Member member, Long boardId); BoardsSearchByKeyword searchBoardsByKeyword(String keyword); - BoardToggleHeart toggleBoardHeart(Member member, Long boardId); + ToggleBoardHeart toggleBoardHeart(Member member, Long boardId); + Board findBoard(Long boardId); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index 2498871e..b52edb10 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -1,10 +1,14 @@ package com.server.capple.domain.board.service; import com.server.capple.domain.board.dto.BoardResponse; +import com.server.capple.domain.board.dto.BoardResponse.ToggleBoardHeart; import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.entity.BoardHeart; import com.server.capple.domain.board.entity.BoardType; +import com.server.capple.domain.board.mapper.BoardHeartMapper; import com.server.capple.domain.board.mapper.BoardMapper; import com.server.capple.domain.board.repository.BoardHeartRedisRepository; +import com.server.capple.domain.board.repository.BoardHeartRepository; import com.server.capple.domain.board.repository.BoardRepository; import com.server.capple.domain.boardSubscribeMember.service.BoardSubscribeMemberService; import com.server.capple.domain.member.entity.Member; @@ -15,7 +19,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; @Service @@ -26,6 +29,8 @@ public class BoardServiceImpl implements BoardService { private final BoardRepository boardRepository; private final BoardHeartRedisRepository boardHeartRedisRepository; private final BoardMapper boardMapper; + private final BoardHeartRepository boardHeartRepository; + private final BoardHeartMapper boardHeartMapper; private final NotificationService notificationService; private final BoardSubscribeMemberService boardSubscribeMemberService; @@ -33,7 +38,7 @@ public class BoardServiceImpl implements BoardService { public BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, String content) { Board board; if (content != null) { - board = boardRepository.save(boardMapper.toBoard(member, boardType, content, 0, 0)); + board = boardRepository.save(boardMapper.toBoard(member, boardType, content)); } else { throw new RestApiException(BoardErrorCode.BOARD_BAD_REQUEST); } @@ -41,9 +46,10 @@ public BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, return boardMapper.toBoardCreate(board); } + //redis @Override - public BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardType) { - List boards = new ArrayList<>(); + public BoardResponse.BoardsGetByBoardType getBoardsByBoardTypeWithRedis(Member member, BoardType boardType) { + List boards; if (boardType == null) { boards = boardRepository.findAll(); } else if (boardType == BoardType.FREEBOARD) { @@ -54,8 +60,38 @@ public BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardTy throw new RestApiException(BoardErrorCode.BOARD_BAD_REQUEST); } return boardMapper.toBoardsGetByBoardType(boards.stream() - .map(board -> boardMapper.toBoardsGetByBoardTypeBoardInfo(board, boardHeartRedisRepository.getBoardHeartsCount(board.getId()))) - .toList() + // TODO: BoardReport 관련 테이블 구현 후 수정 요망 + .map(board -> { + int heartCount = boardHeartRedisRepository.getBoardHeartsCount(board.getId()); + boolean isLiked = boardHeartRedisRepository.isMemberLikedBoard(member.getId(), board.getId()); + boolean isMine = board.getWriter().getId().equals(member.getId()); + return boardMapper.toBoardsGetByBoardTypeBoardInfo(board, heartCount, isLiked, isMine, false); + }) + .toList() + ); + } + + //rdb + @Override + public BoardResponse.BoardsGetByBoardType getBoardsByBoardType(Member member, BoardType boardType) { + List boards; + if (boardType == null) { + boards = boardRepository.findAll(); + } else if (boardType == BoardType.FREEBOARD) { + boards = boardRepository.findBoardsByBoardType(BoardType.FREEBOARD); + } else if (boardType == BoardType.HOTBOARD) { + boards = boardRepository.findBoardsByBoardType(BoardType.HOTBOARD); + } else { + throw new RestApiException(BoardErrorCode.BOARD_BAD_REQUEST); + } + return boardMapper.toBoardsGetByBoardType(boards.stream() + // TODO: BoardReport 관련 테이블 구현 후 수정 요망 + .map(board -> { + boolean isLiked = boardHeartRepository.findByMemberAndBoard(member,board).isPresent(); + boolean isMine = board.getWriter().getId().equals(member.getId()); + return boardMapper.toBoardsGetByBoardTypeBoardInfo(board, isLiked, isMine,false); + }) + .toList() ); } @@ -75,16 +111,25 @@ public BoardResponse.BoardDelete deleteBoard(Member member, Long boardId) { public BoardResponse.BoardsSearchByKeyword searchBoardsByKeyword(String keyword) { List boards = boardRepository.findBoardsByKeyword(keyword); return boardMapper.toBoardsSearchByKeyword(boards.stream() - .map(board -> boardMapper.toBoardsSearchByKeywordBoardInfo(board, boardHeartRedisRepository.getBoardHeartsCount(board.getId()))) - .toList()); + .map(board -> boardMapper.toBoardsSearchByKeywordBoardInfo(board, board.getHeartCount())) + .toList()); } @Override - public BoardResponse.BoardToggleHeart toggleBoardHeart(Member member, Long boardId) { + @Transactional + public ToggleBoardHeart toggleBoardHeart(Member member, Long boardId) { Board board = findBoard(boardId); - boolean isLiked = boardHeartRedisRepository.toggleBoardHeart(member.getId(), board.getId()); + // 좋아요 눌렀는지 확인 + //boardHeart에 없다면 새로 저장 + BoardHeart boardHeart = boardHeartRepository.findByMemberAndBoard(member, board) + .orElseGet(() -> { + BoardHeart newHeart = boardHeartMapper.toBoardHeart(board, member); + return boardHeartRepository.save(newHeart); + }); + boolean isLiked = boardHeart.toggleHeart(); + board.setHeartCount(boardHeart.isLiked()); if (isLiked) notificationService.sendBoardHeartNotification(member.getId(), board); - return new BoardResponse.BoardToggleHeart(boardId, isLiked); + return new ToggleBoardHeart(boardId, isLiked); } @Override @@ -92,6 +137,4 @@ public Board findBoard(Long boardId) { return boardRepository.findById(boardId) .orElseThrow(() -> new RestApiException(BoardErrorCode.BOARD_NOT_FOUND)); } - - } diff --git a/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java b/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java index 12ce6420..2935de54 100644 --- a/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java +++ b/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java @@ -3,7 +3,7 @@ import com.server.capple.config.security.AuthMember; import com.server.capple.domain.boardComment.dto.BoardCommentRequest; -import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.ToggleBoardCommentHeart; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentId; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; import com.server.capple.domain.boardComment.service.BoardCommentService; @@ -46,8 +46,8 @@ public BaseResponse deleteBoardComment(@AuthMember Member member @Operation(summary = "게시글 댓글 좋아요/취소 토글 API", description = " 게시글 댓글 좋아요/취소 토글 API 입니다. pathVariable 으로 commentId를 주세요.") @PatchMapping("/heart/{commentId}") - public BaseResponse heartBoardComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { - return BaseResponse.onSuccess(boardCommentService.heartBoardComment(member, commentId)); + public BaseResponse heartBoardComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { + return BaseResponse.onSuccess(boardCommentService.toggleBoardCommentHeart(member, commentId)); } @Operation(summary = "게시글 댓글 리스트 조회 API", description = " 게시글 댓글 리스트 조회 API 입니다. pathVariable 으로 boardId를 주세요.") diff --git a/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java b/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java index b87ed7cd..38d46df9 100644 --- a/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java +++ b/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java @@ -17,7 +17,7 @@ public static class BoardCommentId { @Getter @AllArgsConstructor - public static class BoardCommentHeart { + public static class ToggleBoardCommentHeart { private Long boardCommentId; private Boolean isLiked; } @@ -26,10 +26,11 @@ public static class BoardCommentHeart { @Builder public static class BoardCommentInfo { private Long boardCommentId; - private String writer; + private Long writerId; private String content; - private Long heartCount; + private Integer heartCount; private Boolean isLiked; + private Boolean isMine; private LocalDateTime createdAt; } diff --git a/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java b/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java index b9dc2176..4ae88376 100644 --- a/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java +++ b/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java @@ -5,6 +5,8 @@ import com.server.capple.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLRestriction; @Getter @@ -13,6 +15,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @SQLRestriction("deleted_at is null") +@DynamicInsert public class BoardComment extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -29,7 +32,18 @@ public class BoardComment extends BaseEntity { @Column(nullable = false) private String content; + @ColumnDefault("0") + private Integer heartCount; + public void update(String content) { this.content = content; } + + public void setHeartCount(boolean isLiked) { + if (isLiked) { + this.heartCount++; + } else { + this.heartCount--; + } + } } diff --git a/src/main/java/com/server/capple/domain/boardComment/entity/BoardCommentHeart.java b/src/main/java/com/server/capple/domain/boardComment/entity/BoardCommentHeart.java new file mode 100644 index 00000000..bf9194ec --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/entity/BoardCommentHeart.java @@ -0,0 +1,31 @@ +package com.server.capple.domain.boardComment.entity; + +import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Builder +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class BoardCommentHeart extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private BoardComment boardComment; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + private boolean isLiked; + + public boolean toggleHeart() { + this.isLiked = !this.isLiked; + return isLiked; + } +} diff --git a/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentHeartMapper.java b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentHeartMapper.java new file mode 100644 index 00000000..1db7e9ed --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentHeartMapper.java @@ -0,0 +1,18 @@ +package com.server.capple.domain.boardComment.mapper; + +import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.domain.boardComment.entity.BoardCommentHeart; +import com.server.capple.domain.member.entity.Member; +import org.springframework.stereotype.Component; + +@Component +public class BoardCommentHeartMapper { + + public BoardCommentHeart toBoardCommentHeart(BoardComment boardComment, Member member) { + return BoardCommentHeart.builder() + .boardComment(boardComment) + .member(member) + .isLiked(false) + .build(); + } +} diff --git a/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java index dbc38ff7..38ccca34 100644 --- a/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java +++ b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java @@ -8,21 +8,37 @@ @Component public class BoardCommentMapper { - public BoardComment toBoardCommentEntity(Member member, Board board, String comment) { + public BoardComment toBoardComment(Member member, Board board, String comment) { return BoardComment.builder() .member(member) .board(board) .content(comment) + .heartCount(0) .build(); } - public BoardCommentInfo toBoardCommentInfo(BoardComment comment, Long heartCount, Boolean isLiked) { + //redis + public BoardCommentInfo toBoardCommentInfo(BoardComment comment, Integer boardHeart, Boolean isLiked, Boolean isMine) { return BoardCommentInfo.builder() .boardCommentId(comment.getId()) - .writer(comment.getMember().getNickname()) + .writerId(comment.getMember().getId()) .content(comment.getContent()) - .heartCount(heartCount) + .heartCount(boardHeart) .isLiked(isLiked) + .isMine(isMine) + .createdAt(comment.getCreatedAt()) + .build(); + } + + //rdb + public BoardCommentInfo toBoardCommentInfo(BoardComment comment, Boolean isLiked, Boolean isMine) { + return BoardCommentInfo.builder() + .boardCommentId(comment.getId()) + .writerId(comment.getMember().getId()) + .content(comment.getContent()) + .heartCount(comment.getHeartCount()) + .isLiked(isLiked) + .isMine(isMine) .createdAt(comment.getCreatedAt()) .build(); } diff --git a/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java index 10c08801..a001e9e2 100644 --- a/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java +++ b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java @@ -45,9 +45,10 @@ public Boolean isMemberLiked(Long commentId, Long memberId) { return setOperations.isMember(key, member); } - public Long getBoardCommentsCount(Long commentId) { + public Integer getBoardCommentsHeartCount(Long commentId) { String key = BOARD_COMMENT_HEART_KEY_PREFIX + commentId.toString(); - return redisTemplate.opsForSet().size(key); + Long size = redisTemplate.opsForSet().size(key); + return (size != null) ? size.intValue() : 0; } } diff --git a/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRepository.java b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRepository.java new file mode 100644 index 00000000..b869a6be --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRepository.java @@ -0,0 +1,13 @@ +package com.server.capple.domain.boardComment.repository; + +import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.domain.boardComment.entity.BoardCommentHeart; +import com.server.capple.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BoardCommentHeartRepository extends JpaRepository { + Optional findByMemberAndBoardComment(Member member, BoardComment boardComment); + +} diff --git a/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java index 3255443d..09001247 100644 --- a/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java +++ b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java @@ -1,7 +1,7 @@ package com.server.capple.domain.boardComment.service; import com.server.capple.domain.boardComment.dto.BoardCommentRequest; -import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.ToggleBoardCommentHeart; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentId; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; import com.server.capple.domain.boardComment.entity.BoardComment; @@ -11,7 +11,7 @@ public interface BoardCommentService { BoardCommentId createBoardComment(Member member, Long boardId, BoardCommentRequest request); BoardCommentId updateBoardComment(Member member, Long commentId,BoardCommentRequest request); BoardCommentId deleteBoardComment(Member member, Long commentId); - BoardCommentHeart heartBoardComment(Member member, Long commentId); + ToggleBoardCommentHeart toggleBoardCommentHeart(Member member, Long commentId); BoardCommentInfos getBoardCommentInfos(Member member, Long boardId); BoardComment findBoardComment(Long commentId); } diff --git a/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java index 9be22805..335fb8b2 100644 --- a/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java +++ b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java @@ -3,13 +3,16 @@ import com.server.capple.domain.board.entity.Board; import com.server.capple.domain.board.service.BoardService; import com.server.capple.domain.boardComment.dto.BoardCommentRequest; -import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentId; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfo; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.ToggleBoardCommentHeart; import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.domain.boardComment.entity.BoardCommentHeart; +import com.server.capple.domain.boardComment.mapper.BoardCommentHeartMapper; import com.server.capple.domain.boardComment.mapper.BoardCommentMapper; import com.server.capple.domain.boardComment.repository.BoardCommentHeartRedisRepository; +import com.server.capple.domain.boardComment.repository.BoardCommentHeartRepository; import com.server.capple.domain.boardComment.repository.BoardCommentRepository; import com.server.capple.domain.boardSubscribeMember.service.BoardSubscribeMemberService; import com.server.capple.domain.member.entity.Member; @@ -31,7 +34,9 @@ public class BoardCommentServiceImpl implements BoardCommentService { private final BoardService boardService; private final BoardCommentRepository boardCommentRepository; private final BoardCommentHeartRedisRepository boardCommentHeartRedisRepository; + private final BoardCommentHeartRepository boardCommentHeartRepository; private final BoardCommentMapper boardCommentMapper; + private final BoardCommentHeartMapper boardCommentHeartMapper; private final NotificationService notificationService; private final BoardSubscribeMemberService boardSubscribeMemberService; @@ -42,10 +47,11 @@ public BoardCommentId createBoardComment(Member member, Long boardId, BoardComme Board board = boardService.findBoard(boardId); BoardComment boardComment = boardCommentRepository.save( - boardCommentMapper.toBoardCommentEntity(loginMember, board, request.getComment())); + boardCommentMapper.toBoardComment(loginMember, board, request.getComment())); notificationService.sendBoardCommentNotification(loginMember.getId(), board, boardComment); // 게시글 댓글 알림 boardSubscribeMemberService.createBoardSubscribeMember(loginMember, board); // 알림 리스트 추가 + board.increaseCommentCount(); return new BoardCommentId(boardComment.getId()); } @@ -65,33 +71,42 @@ public BoardCommentId deleteBoardComment(Member member, Long commentId) { BoardComment boardComment = findBoardComment(commentId); checkPermission(member, boardComment); + Board board = boardComment.getBoard(); + boardComment.delete(); + board.decreaseCommentCount(); return new BoardCommentId(boardComment.getId()); } @Override @Transactional - public BoardCommentHeart heartBoardComment(Member member, Long commentId) { - Boolean isLiked = boardCommentHeartRedisRepository. - toggleBoardCommentHeart(commentId, member.getId()); - if(isLiked) { - BoardComment boardComment = boardCommentRepository.findById(commentId).get(); - if(!boardComment.getMember().getId().equals(member.getId())) - notificationService.sendBoardCommentHeartNotification(member.getId(), boardComment.getBoard(), boardComment); + public ToggleBoardCommentHeart toggleBoardCommentHeart(Member member, Long boardCommentId) { + BoardComment boardComment = findBoardComment(boardCommentId); + //boardCommentHeart에 없다면 새로 저장 + BoardCommentHeart boardCommentHeart = boardCommentHeartRepository.findByMemberAndBoardComment(member, boardComment) + .orElseGet(() -> { + BoardCommentHeart newHeart = boardCommentHeartMapper.toBoardCommentHeart(boardComment, member); + return boardCommentHeartRepository.save(newHeart); + }); + boolean isLiked = boardCommentHeart.toggleHeart(); + boardComment.setHeartCount(boardCommentHeart.isLiked()); + if(isLiked && !boardComment.getMember().getId().equals(member.getId())) { + notificationService.sendBoardCommentHeartNotification(member.getId(), boardComment.getBoard(), boardComment); } - - return new BoardCommentHeart(commentId, isLiked); + return new ToggleBoardCommentHeart(boardCommentId, isLiked); } + //rdb @Override public BoardCommentInfos getBoardCommentInfos(Member member, Long boardId) { List commentInfos = boardCommentRepository .findBoardCommentByBoardIdOrderByCreatedAt(boardId).stream().map( comment -> { - Long heartCount = boardCommentHeartRedisRepository.getBoardCommentsCount(comment.getId()); - Boolean isLiked = boardCommentHeartRedisRepository.isMemberLiked(comment.getId(), member.getId()); - return boardCommentMapper.toBoardCommentInfo(comment, heartCount, isLiked); + Boolean isLiked = boardCommentHeartRepository.findByMemberAndBoardComment(member, comment) + .isPresent(); + Boolean isMine = comment.getMember().getId().equals(member.getId()); + return boardCommentMapper.toBoardCommentInfo(comment, isLiked, isMine); }).toList(); return new BoardCommentInfos(commentInfos); diff --git a/src/main/java/com/server/capple/domain/question/controller/QuestionController.java b/src/main/java/com/server/capple/domain/question/controller/QuestionController.java index 2720784e..097ddcda 100644 --- a/src/main/java/com/server/capple/domain/question/controller/QuestionController.java +++ b/src/main/java/com/server/capple/domain/question/controller/QuestionController.java @@ -2,8 +2,9 @@ import com.server.capple.config.security.AuthMember; import com.server.capple.domain.member.entity.Member; -import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionSummary; +import com.server.capple.domain.question.dto.response.QuestionResponse; import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionInfos; +import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionSummary; import com.server.capple.domain.question.service.QuestionService; import com.server.capple.global.common.BaseResponse; import io.swagger.v3.oas.annotations.Operation; @@ -11,9 +12,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "질문 API", description = "질문 관련 API") @RestController @@ -41,6 +40,13 @@ private BaseResponse getQuestions(@AuthMember Member member) { return BaseResponse.onSuccess(questionService.getQuestions(member)); } + @Operation(summary = "질문 좋아요/취소 API", description = " 질문 좋아요/취소 API 입니다." + + "pathvariable 으로 questionId를 주세요.") + @PostMapping("/{questionId}/heart") + public BaseResponse toggleBoardHeart(@AuthMember Member member, @PathVariable(value = "questionId") Long questionId) { + return BaseResponse.onSuccess(questionService.toggleQuestionHeart(member, questionId)); + } + // @Operation(summary = "최근 지난 질문 조회 API", description = "최근 지난 질문을 조회합니다.") // @ApiResponses(value = { diff --git a/src/main/java/com/server/capple/domain/question/dto/response/QuestionResponse.java b/src/main/java/com/server/capple/domain/question/dto/response/QuestionResponse.java index 157ac337..d792f632 100644 --- a/src/main/java/com/server/capple/domain/question/dto/response/QuestionResponse.java +++ b/src/main/java/com/server/capple/domain/question/dto/response/QuestionResponse.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.List; @@ -18,6 +19,8 @@ public static class QuestionSummary { private Long questionId; private QuestionStatus questionStatus; private String content; +// private Integer likeCount; +// private Integer commentCount; private Boolean isAnswered; } @@ -31,9 +34,8 @@ public static class QuestionInfo { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime livedAt; private String content; - // 추후 추가 예정 -// private Long likeCount; -// private Long commentCount; +// private Integer likeCount; +// private Integer commentCount; private Boolean isAnswered; } @@ -50,4 +52,13 @@ public static class QuestionId { public static class QuestionInfos { private List questionInfos; } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class QuestionToggleHeart { + private Long questionId; + private Boolean isLiked; + } } diff --git a/src/main/java/com/server/capple/domain/question/entity/Question.java b/src/main/java/com/server/capple/domain/question/entity/Question.java index f1d77239..27a82495 100644 --- a/src/main/java/com/server/capple/domain/question/entity/Question.java +++ b/src/main/java/com/server/capple/domain/question/entity/Question.java @@ -36,6 +36,9 @@ public class Question extends BaseEntity { private LocalDateTime livedAt; +// @Column(nullable = false) +// private Integer commentCount; + //question Status를 바꾸는 함수 public void setQuestionStatus(QuestionStatus questionStatus) { this.questionStatus = questionStatus; @@ -44,4 +47,12 @@ public void setQuestionStatus(QuestionStatus questionStatus) { this.livedAt = LocalDateTime.now(); } +// public void increaseCommentCount() { +// this.commentCount += 1; +// } +// +// public void decreaseCommentCount() { +// this.commentCount -= 1; +// } + } diff --git a/src/main/java/com/server/capple/domain/question/mapper/QuestionMapper.java b/src/main/java/com/server/capple/domain/question/mapper/QuestionMapper.java index e404bd07..ee9e4360 100644 --- a/src/main/java/com/server/capple/domain/question/mapper/QuestionMapper.java +++ b/src/main/java/com/server/capple/domain/question/mapper/QuestionMapper.java @@ -16,28 +16,30 @@ public Question toQuestion(QuestionCreate request) { return Question.builder() .questionStatus(request.getQuestionStatus()) .content(request.getContent()) +// .commentCount(0) .build(); } - public QuestionSummary toQuestionSummary(Question question, boolean isAnswered) { + public QuestionSummary toQuestionSummary(Question question, boolean isAnswered/*, Integer likeCount*/) { return QuestionSummary.builder() .questionId(question.getId()) .questionStatus(question.getQuestionStatus()) .content(question.getContent()) .isAnswered(isAnswered) +// .likeCount(likeCount) +// .commentCount(question.getCommentCount()) .build(); } - public QuestionInfo toQuestionInfo(Question question, boolean isAnswered) { + public QuestionInfo toQuestionInfo(Question question, boolean isAnswered/*, Integer likeCount*/) { return QuestionInfo.builder() .questionId(question.getId()) .questionStatus(question.getQuestionStatus()) .livedAt(question.getLivedAt()) .content(question.getContent()) - // Count는 추후 수정 예정(필드 수정해야함...) -// .likeCount(100L) -// .commentCount(0L) +// .likeCount(likeCount) +// .commentCount(question.getCommentCount()) .isAnswered(isAnswered) .build(); } diff --git a/src/main/java/com/server/capple/domain/question/repository/QuestionHeartRedisRepository.java b/src/main/java/com/server/capple/domain/question/repository/QuestionHeartRedisRepository.java new file mode 100644 index 00000000..3efe6c8d --- /dev/null +++ b/src/main/java/com/server/capple/domain/question/repository/QuestionHeartRedisRepository.java @@ -0,0 +1,73 @@ +package com.server.capple.domain.question.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SetOperations; +import org.springframework.stereotype.Repository; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +@Repository +@RequiredArgsConstructor +public class QuestionHeartRedisRepository implements Serializable { + public static final String QUESTION_HEART_KEY_PREFIX = "questionHeart-"; + public static final String MEMBER_KEY_PREFIX = "member-"; + + private final RedisTemplate redisTemplate; + + // 질문 좋아요 토글 + public Boolean toggleBoardHeart(Long memberId, Long boardId) { + String key = QUESTION_HEART_KEY_PREFIX + boardId.toString(); + String member = MEMBER_KEY_PREFIX + memberId.toString(); + SetOperations setOperations = redisTemplate.opsForSet(); + + //해당 key에 member가 존재하지 않으면 추가, 존재하면 삭제 + if (FALSE.equals(setOperations.isMember(key, member))) { + setOperations.add(key, member); + return TRUE; + } else { + setOperations.remove(key, member); + // 좋아요 취소 시 생성 시간도 삭제할 수 있음 + return FALSE; + } + } + + + public String getQuestionHeartCreateAt(Long questionId, Long memberId) { + String createAtKey = QUESTION_HEART_KEY_PREFIX + questionId.toString() + ":" + MEMBER_KEY_PREFIX + memberId.toString() + ":createAt"; + return redisTemplate.opsForValue().get(createAtKey); + } + + // 질문 좋아요 수 조회 + public Integer getQuestionHeartsCount(Long questionId) { + String key = QUESTION_HEART_KEY_PREFIX + questionId.toString(); + Set members = redisTemplate.opsForSet().members(key); + return members != null ? members.size() : 0; + } + + // 좋아요 누른 질문 조회 + public Set getMemberHeartsQuestion(Long memberId) { + String member = MEMBER_KEY_PREFIX + memberId.toString(); + Set keys = redisTemplate.keys(QUESTION_HEART_KEY_PREFIX + "*"); // 모든 키 조회 + Set questionIds = new HashSet<>(); + + for (String key : keys) { + if (redisTemplate.opsForSet().isMember(key, member)) { + String questionId = key.substring(QUESTION_HEART_KEY_PREFIX.length()); + questionIds.add(Long.parseLong(questionId)); + } + } + return questionIds; + } + + public boolean isMemberLikedQuestion(Long memberId, Long questionId) { + String key = QUESTION_HEART_KEY_PREFIX + questionId; + String memberKey = MEMBER_KEY_PREFIX + memberId; + return redisTemplate.opsForSet().isMember(key, memberKey); + } +} \ No newline at end of file diff --git a/src/main/java/com/server/capple/domain/question/service/QuestionService.java b/src/main/java/com/server/capple/domain/question/service/QuestionService.java index 5b4e8415..eaa8580c 100644 --- a/src/main/java/com/server/capple/domain/question/service/QuestionService.java +++ b/src/main/java/com/server/capple/domain/question/service/QuestionService.java @@ -1,8 +1,9 @@ package com.server.capple.domain.question.service; -import com.server.capple.config.security.AuthMember; + import com.server.capple.domain.member.entity.Member; -import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionSummary; +import com.server.capple.domain.question.dto.response.QuestionResponse; import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionInfos; +import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionSummary; import com.server.capple.domain.question.entity.Question; public interface QuestionService { @@ -10,4 +11,6 @@ public interface QuestionService { QuestionSummary getMainQuestion(Member member); QuestionInfos getQuestions(Member member); + + QuestionResponse.QuestionToggleHeart toggleQuestionHeart(Member member, Long questionId); } diff --git a/src/main/java/com/server/capple/domain/question/service/QuestionServiceImpl.java b/src/main/java/com/server/capple/domain/question/service/QuestionServiceImpl.java index 03fc1a83..3a027e4e 100644 --- a/src/main/java/com/server/capple/domain/question/service/QuestionServiceImpl.java +++ b/src/main/java/com/server/capple/domain/question/service/QuestionServiceImpl.java @@ -1,12 +1,15 @@ package com.server.capple.domain.question.service; import com.server.capple.domain.answer.repository.AnswerRepository; +import com.server.capple.domain.answerComment.repository.AnswerCommentHeartRedisRepository; import com.server.capple.domain.member.entity.Member; import com.server.capple.domain.question.dao.QuestionInfoInterface; +import com.server.capple.domain.question.dto.response.QuestionResponse; import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionInfos; import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionSummary; import com.server.capple.domain.question.entity.Question; import com.server.capple.domain.question.mapper.QuestionMapper; +import com.server.capple.domain.question.repository.QuestionHeartRedisRepository; import com.server.capple.domain.question.repository.QuestionRepository; import com.server.capple.global.exception.RestApiException; import com.server.capple.global.exception.errorCode.QuestionErrorCode; @@ -23,6 +26,8 @@ public class QuestionServiceImpl implements QuestionService { private final QuestionRepository questionRepository; private final AnswerRepository answerRepository; private final QuestionMapper questionMapper; + private final QuestionHeartRedisRepository questionHeartRepository; + private final AnswerCommentHeartRedisRepository answerCommentHeartRepository; @Override public Question findQuestion(Long questionId) { @@ -37,7 +42,7 @@ public QuestionSummary getMainQuestion(Member member) { boolean isAnswered = answerRepository.existsByQuestionAndMember(mainQuestion, member); - return questionMapper.toQuestionSummary(mainQuestion, isAnswered); + return questionMapper.toQuestionSummary(mainQuestion, isAnswered/*, questionHeartRepository.getQuestionHeartsCount(mainQuestion.getId())*/); } @Override @@ -47,7 +52,16 @@ public QuestionInfos getQuestions(Member member) { return questionMapper.toQuestionInfos(questions.stream() .map(questionInfo -> questionMapper.toQuestionInfo(questionInfo.getQuestion(), - questionInfo.getIsAnsweredByMember()) + questionInfo.getIsAnsweredByMember()/*, + questionHeartRepository.getQuestionHeartsCount(questionInfo.getQuestion().getId())*/) ).toList()); } + + @Override + public QuestionResponse.QuestionToggleHeart toggleQuestionHeart(Member member, Long questionId) { + Question question = findQuestion(questionId); + + Boolean isLiked = questionHeartRepository.toggleBoardHeart(member.getId(), question.getId()); + return new QuestionResponse.QuestionToggleHeart(questionId, isLiked); + } } diff --git a/src/main/java/com/server/capple/dummy/DummyController.java b/src/main/java/com/server/capple/dummy/DummyController.java new file mode 100644 index 00000000..dcf46856 --- /dev/null +++ b/src/main/java/com/server/capple/dummy/DummyController.java @@ -0,0 +1,31 @@ +package com.server.capple.dummy; + +import com.server.capple.global.common.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "더미데이터 생성 API", description = "더미데이터 생성을 위한 API입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/dummy") +public class DummyController { + + private final DummyService dummyService; + + @Operation(summary = "멤버, 게시글,게시글 좋아요 더미 생성 API", description = "멤버, 게시글과 게시글 좋아요 더미를 생성합니다." + + "생성하고싶은 멤버 수와, 게시글 수를 파라미터로 입력해주세요. 좋아요는 redis와 rdb에 동시에 저장됩니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + }) + @PostMapping() + private BaseResponse generateDummyBoards(@RequestParam("memberCount") int memberCount, @RequestParam("boardCount") int boardCount) { + return BaseResponse.onSuccess(dummyService.generateDummy(memberCount, boardCount)); + } +} diff --git a/src/main/java/com/server/capple/dummy/DummyService.java b/src/main/java/com/server/capple/dummy/DummyService.java new file mode 100644 index 00000000..3fe489e5 --- /dev/null +++ b/src/main/java/com/server/capple/dummy/DummyService.java @@ -0,0 +1,39 @@ +package com.server.capple.dummy; + + +import com.server.capple.domain.board.repository.BoardHeartRedisRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class DummyService { + private final BoardHeartRedisRepository boardHeartRedisRepository; + + @PersistenceContext + private EntityManager em; + + //멤버, 게시글, 게시글 좋아요 생성 + @Transactional + public Object generateDummy(int memberCount, int boardCount) { + generateDummyData(memberCount,boardCount); + em.flush(); + boardHeartRedisRepository.generateDummyBoardLikes(memberCount, boardCount); + return null; + } + + @Transactional + public void generateDummyData(int memberCount, int boardCount) { + em.createNativeQuery("select generate_dummy_data(:memberCount, :boardCount)") + .setParameter("memberCount", memberCount) + .setParameter("boardCount", boardCount) + .getSingleResult(); //프로시저 수행 후 아무것도 반환하지 않음 + //executeUpdate() 수행시 int 값을 반환해야하는데, 반환하지 않아서 Error -> getSingleResult() 사용해서 null값 받음 + } + + + +} diff --git a/src/main/java/com/server/capple/dummy/generate_dummy_data.sql b/src/main/java/com/server/capple/dummy/generate_dummy_data.sql new file mode 100644 index 00000000..26fe0704 --- /dev/null +++ b/src/main/java/com/server/capple/dummy/generate_dummy_data.sql @@ -0,0 +1,68 @@ +create function generate_dummy_data(member_count integer, board_count integer) returns void + language plpgsql +as +$$ +DECLARE + member_id INT; + board_id INT; + random_content TEXT; + random_comment_count INT; + random_board_type SMALLINT; + random_value FLOAT; +BEGIN + -- 1. 멤버 더미 데이터 생성 +FOR member_id IN 1..member_count LOOP + INSERT INTO member (nickname, email, sub, role, created_at, updated_at) + VALUES ( + 'User' || member_id, + 'user' || member_id || '@example.com', + 'sub' || member_id, + 'ROLE_ACADEMIER', + NOW(), + NULL + ); +END LOOP; + + -- 2. 보드 더미 데이터 생성 +FOR board_id IN 1..board_count LOOP + random_content := 'This is a dummy content for board ' || board_id; + random_comment_count := FLOOR(RANDOM() * 100); + + -- FREEBOARD와 HOTBOARD를 번갈아가며 설정 (0은 FREEBOARD, 1은 HOTBOARD) + IF board_id % 2 = 0 THEN + random_board_type := 0; + ELSE + random_board_type := 1; + END IF; + + -- 보드 데이터 삽입 +INSERT INTO board (member_id, board_type, content, comment_count, created_at, updated_at) +VALUES ( + FLOOR(RANDOM() * member_count) + 1, + random_board_type, + random_content, + random_comment_count, + NOW(), + NULL + ); +END LOOP; + + -- 3. 보드하트 더미 데이터 생성 +FOR board_id IN 1..board_count LOOP + FOR member_id IN 1..member_count LOOP + random_value := RANDOM(); + IF random_value < 0.5 THEN + INSERT INTO board_heart (board_id, member_id, is_liked, created_at, updated_at) + VALUES ( + board_id, + member_id, + TRUE, + NOW(), + NULL + ); + END IF; + END LOOP; +END LOOP; + +END; +$$; \ No newline at end of file diff --git a/src/main/java/com/server/capple/global/exception/errorCode/AnswerErrorCode.java b/src/main/java/com/server/capple/global/exception/errorCode/AnswerErrorCode.java index 56b0da4d..f42150ac 100644 --- a/src/main/java/com/server/capple/global/exception/errorCode/AnswerErrorCode.java +++ b/src/main/java/com/server/capple/global/exception/errorCode/AnswerErrorCode.java @@ -10,7 +10,9 @@ @AllArgsConstructor public enum AnswerErrorCode implements ErrorCodeInterface { ANSWER_NOT_FOUND("ANSWER001", "답변을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), - ANSWER_UNAUTHORIZED("ANSWER002", "답변에 대한 권한이 업습니다.", HttpStatus.FORBIDDEN); + ANSWER_UNAUTHORIZED("ANSWER002", "답변에 대한 권한이 업습니다.", HttpStatus.FORBIDDEN), + ANSWER_ALREADY_EXIST("ANSWER003", "이미 답변한 질문입니다.", HttpStatus.BAD_REQUEST), + ; private final String code; private final String message; diff --git a/src/test/java/com/server/capple/domain/answer/controller/AnswerControllerTest.java b/src/test/java/com/server/capple/domain/answer/controller/AnswerControllerTest.java index 7bb8c0b1..347f2f74 100644 --- a/src/test/java/com/server/capple/domain/answer/controller/AnswerControllerTest.java +++ b/src/test/java/com/server/capple/domain/answer/controller/AnswerControllerTest.java @@ -149,7 +149,7 @@ public void getMyPageMemberAnswerTest() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value("COMMON200")) .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) - .andExpect(jsonPath("$.result.memberAnswerInfos[0].nickname").value("루시")) +// .andExpect(jsonPath("$.result.memberAnswerInfos[0].nickname").value("루시")) .andExpect(jsonPath("$.result.memberAnswerInfos[0].content").value("나는 무자비한 사람이 좋아")); } diff --git a/src/test/java/com/server/capple/domain/answer/service/AnswerServiceTest.java b/src/test/java/com/server/capple/domain/answer/service/AnswerServiceTest.java index 78ae87ba..564c01d5 100644 --- a/src/test/java/com/server/capple/domain/answer/service/AnswerServiceTest.java +++ b/src/test/java/com/server/capple/domain/answer/service/AnswerServiceTest.java @@ -4,6 +4,7 @@ import com.server.capple.domain.answer.dto.AnswerResponse; import com.server.capple.domain.answer.entity.Answer; import com.server.capple.domain.tag.service.TagService; +import com.server.capple.global.exception.RestApiException; import com.server.capple.support.ServiceTestConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,8 +14,7 @@ import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; @DisplayName("Answer 서비스의 ") @SpringBootTest @@ -25,9 +25,25 @@ public class AnswerServiceTest extends ServiceTestConfig { private TagService tagService; @Test - @DisplayName("Answer 생성 테스트") + @DisplayName("Answer 중복 생성 시 예외 발생 테스트") + @Transactional + public void createDuplicateAnswerTest() { + // given + AnswerRequest request = getAnswerRequest(); + + // 이미 `setUp()`에서 답변이 생성된 상태이므로, 다시 답변을 생성하면 예외 발생 + // then + assertThrows(RestApiException.class, () -> { + answerService.createAnswer(member, liveQuestion.getId(), request); + }); + } + + @Test + @DisplayName("Answer 중복 생성 방지 후 새로운 답변 생성 테스트") @Transactional public void createAnswerTest() { + // 기존 답변 삭제 + answerService.deleteAnswer(member, answer.getId()); //given AnswerRequest request = getAnswerRequest(); @@ -41,9 +57,11 @@ public void createAnswerTest() { } @Test - @DisplayName("Answer 수정 테스트") + @DisplayName("Answer 중복 생성 방지 후 새로운 답변 수정 테스트") @Transactional public void updateAnswerTest() { + // 기존 답변 삭제 + answerService.deleteAnswer(member, answer.getId()); //given AnswerRequest request = getAnswerRequest(); Long answerId = answerService.createAnswer(member, liveQuestion.getId(), request).getAnswerId(); @@ -61,9 +79,11 @@ public void updateAnswerTest() { } @Test - @DisplayName("Answer 삭제 테스트") + @DisplayName("Answer 중복 생성 방지 후 새로운 답변 삭제 테스트") @Transactional public void deleteAnswerTest() { + // 기존 답변 삭제 + answerService.deleteAnswer(member, answer.getId()); //given AnswerRequest request = getAnswerRequest(); Long answerId = answerService.createAnswer(member, liveQuestion.getId(), request).getAnswerId(); diff --git a/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java b/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java index e4838564..0bc3d5c0 100644 --- a/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java +++ b/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java @@ -149,9 +149,9 @@ public void getAnswerCommentInfosTest() throws Exception { .andExpect(jsonPath("$.code").value("COMMON200")) .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) .andExpect(jsonPath("$.result.answerCommentInfos[0].answerCommentId").value(1L)) - .andExpect(jsonPath("$.result.answerCommentInfos[0].writer").value("루시")) + .andExpect(jsonPath("$.result.answerCommentInfos[0].writerId").value(1L)) .andExpect(jsonPath("$.result.answerCommentInfos[0].content").value("댓글 1")) - .andExpect(jsonPath("$.result.answerCommentInfos[0].heartCount").value(3L)) + .andExpect(jsonPath("$.result.answerCommentInfos[0].heartCount").value(3)) .andExpect(jsonPath("$.result.answerCommentInfos[0].createdAt").value("2022-11-01T12:02:00")); } diff --git a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java index df077628..87962724 100644 --- a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java +++ b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java @@ -101,7 +101,7 @@ public void getAnswerCommentsTest() { AnswerCommentInfos response = answerCommentService.getAnswerCommentInfos(answer.getId()); //then - assertEquals("루시", response.getAnswerCommentInfos().get(0).getWriter()); + assertEquals(member.getId(), response.getAnswerCommentInfos().get(0).getWriterId()); assertEquals("답변에 대한 댓글이어유", response.getAnswerCommentInfos().get(0).getContent()); assertEquals(0L, response.getAnswerCommentInfos().get(0).getHeartCount()); } diff --git a/src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java b/src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java index f1af70b0..0837364f 100644 --- a/src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java +++ b/src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java @@ -109,9 +109,9 @@ public void deleteBoardCommentTest() throws Exception { public void heartBoardCommentTest() throws Exception { //given final String url = "/boardComments/heart/{commentId}"; - BoardCommentHeart response = new BoardCommentHeart(1L, Boolean.TRUE); + ToggleBoardCommentHeart response = new ToggleBoardCommentHeart(1L, Boolean.TRUE); - doReturn(response).when(boardCommentService).heartBoardComment(any(Member.class), any(Long.class)); + doReturn(response).when(boardCommentService).toggleBoardCommentHeart(any(Member.class), any(Long.class)); //when ResultActions resultActions = this.mockMvc.perform(patch(url, 1L) @@ -149,7 +149,7 @@ public void getBoardCommentInfosTest() throws Exception { .andExpect(jsonPath("$.code").value("COMMON200")) .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) .andExpect(jsonPath("$.result.boardCommentInfos[0].boardCommentId").value(1L)) - .andExpect(jsonPath("$.result.boardCommentInfos[0].writer").value("루시")) +// .andExpect(jsonPath("$.result.boardCommentInfos[0].writer").value("루시")) .andExpect(jsonPath("$.result.boardCommentInfos[0].content").value("댓글")) .andExpect(jsonPath("$.result.boardCommentInfos[0].heartCount").value(2L)) .andExpect(jsonPath("$.result.boardCommentInfos[0].isLiked").value(true)); diff --git a/src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java b/src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java index d1accde2..51638d54 100644 --- a/src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java +++ b/src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java @@ -1,8 +1,8 @@ package com.server.capple.domain.boardComment.service; import com.server.capple.domain.boardComment.dto.BoardCommentRequest; -import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.ToggleBoardCommentHeart; import com.server.capple.domain.boardComment.entity.BoardComment; import com.server.capple.support.ServiceTestConfig; import org.junit.jupiter.api.DisplayName; @@ -27,13 +27,14 @@ public class BoardCommentServiceTest extends ServiceTestConfig { public void createBoardCommentTest() { //given BoardCommentRequest request = getBoardCommentRequest(); - + int commentCount = board.getCommentCount(); //when Long boardCommentId = boardCommentService.createBoardComment(member, board.getId(), request).getBoardCommentId(); BoardComment comment = boardCommentService.findBoardComment(boardCommentId); //then assertEquals("게시글 댓글", comment.getContent()); + assertEquals(commentCount + 1, board.getCommentCount()); } @Test @@ -61,6 +62,7 @@ public void deleteBoardCommentTest() { //given BoardCommentRequest request = getBoardCommentRequest(); Long boardCommentId = boardCommentService.createBoardComment(member, board.getId(), request).getBoardCommentId(); + int commentCount = board.getCommentCount(); //when boardCommentService.deleteBoardComment(member, boardCommentId); @@ -68,6 +70,7 @@ public void deleteBoardCommentTest() { //then assertNotNull(comment.getDeletedAt()); + assertEquals(commentCount - 1, board.getCommentCount()); } @Test @@ -76,22 +79,27 @@ public void deleteBoardCommentTest() { public void heartBoardCommentTest() { //1. 좋아요 //given & when - BoardCommentHeart liked = boardCommentService.heartBoardComment(member, boardComment.getId()); - BoardCommentInfos likedResponse = boardCommentService.getBoardCommentInfos(member, board.getId()); + int heartCount = boardComment.getHeartCount(); + ToggleBoardCommentHeart liked = boardCommentService.toggleBoardCommentHeart(member, boardComment.getId()); + + //BoardCommentInfos likedResponse = boardCommentService.getBoardCommentInfos(member, board.getId()); //then assertEquals(boardComment.getId(), liked.getBoardCommentId()); assertEquals(true, liked.getIsLiked()); - assertEquals(true, likedResponse.getBoardCommentInfos().get(0).getIsLiked()); + assertEquals(heartCount + 1, boardComment.getHeartCount()); + //assertEquals(true, likedResponse.getBoardCommentInfos().get(0).getIsLiked()); //2. 좋아요 취소 //given & when - BoardCommentHeart unLiked = boardCommentService.heartBoardComment(member, boardComment.getId()); - BoardCommentInfos unLikedResponse = boardCommentService.getBoardCommentInfos(member, board.getId()); + heartCount = boardComment.getHeartCount(); + ToggleBoardCommentHeart unLiked = boardCommentService.toggleBoardCommentHeart(member, boardComment.getId()); + //BoardCommentInfos unLikedResponse = boardCommentService.getBoardCommentInfos(member, board.getId()); //then assertEquals(boardComment.getId(), unLiked.getBoardCommentId()); assertEquals(false, unLiked.getIsLiked()); - assertEquals(false, unLikedResponse.getBoardCommentInfos().get(0).getIsLiked()); + assertEquals(heartCount - 1, boardComment.getHeartCount()); + //assertEquals(false, unLikedResponse.getBoardCommentInfos().get(0).getIsLiked()); } @@ -103,9 +111,10 @@ public void getBoardCommentsTest() { BoardCommentInfos response = boardCommentService.getBoardCommentInfos(member, board.getId()); //then - assertEquals("루시", response.getBoardCommentInfos().get(0).getWriter()); + assertEquals(member.getId(), response.getBoardCommentInfos().get(0).getWriterId()); assertEquals("게시글 댓글", response.getBoardCommentInfos().get(0).getContent()); - assertEquals(0L, response.getBoardCommentInfos().get(0).getHeartCount()); + assertEquals(0, response.getBoardCommentInfos().get(0).getHeartCount()); assertEquals(false, response.getBoardCommentInfos().get(0).getIsLiked()); + assertEquals(true, response.getBoardCommentInfos().get(0).getIsMine()); } } \ No newline at end of file diff --git a/src/test/java/com/server/capple/support/ControllerTestConfig.java b/src/test/java/com/server/capple/support/ControllerTestConfig.java index 5144dca7..f458899d 100644 --- a/src/test/java/com/server/capple/support/ControllerTestConfig.java +++ b/src/test/java/com/server/capple/support/ControllerTestConfig.java @@ -100,7 +100,7 @@ protected MemberAnswerList getMemberAnswerList () { List memberAnswerInfos = List.of(AnswerResponse.MemberAnswerInfo.builder() .questionId(answer.getQuestion().getId()) .answerId(answer.getId()) - .nickname(answer.getMember().getNickname()) + .writerId(member.getId()) .profileImage(answer.getMember().getProfileImage()) .content(answer.getContent()) .heartCount(1) @@ -117,10 +117,10 @@ protected BoardCommentInfos getBoardCommentInfos() { List commentInfos = List.of(BoardCommentInfo.builder() .boardCommentId(1L) - .writer(member.getNickname()) + .writerId(member.getId()) .content("댓글") .createdAt(LocalDateTime.now()) - .heartCount(2L) + .heartCount(2) .isLiked(TRUE) .build()); @@ -136,7 +136,7 @@ protected AnswerCommentRequest getAnswerCommentRequest() { protected AnswerCommentInfos getAnswerCommentInfos () { List answerCommentInfos = List.of(AnswerCommentInfo.builder() .answerCommentId(1L) - .writer(member.getNickname()) + .writerId(member.getId()) .content("댓글 1") .createdAt(LocalDateTime.of(2022, 11, 1, 12, 02)) .heartCount(3L) diff --git a/src/test/java/com/server/capple/support/ServiceTestConfig.java b/src/test/java/com/server/capple/support/ServiceTestConfig.java index 61943193..61cad7cd 100644 --- a/src/test/java/com/server/capple/support/ServiceTestConfig.java +++ b/src/test/java/com/server/capple/support/ServiceTestConfig.java @@ -129,7 +129,7 @@ protected Board createBoard() { .boardType(BoardType.FREEBOARD) .writer(member) .content("오늘 밥먹을 사람!") - .commentCount(2) + .commentCount(0) .build()); } protected BoardComment createBoardComment() { @@ -138,6 +138,7 @@ protected BoardComment createBoardComment() { .member(member) .board(board) .content("게시글 댓글") + .heartCount(0) .build()); }