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: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ dependencies {
// bucket4j
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'

// commons-lang3
implementation 'org.apache.commons:commons-lang3:3.18.0'
// caffeine
implementation 'com.github.ben-manes.caffeine:caffeine'

Expand Down
18 changes: 14 additions & 4 deletions src/main/java/com/oronaminc/join/answer/api/AnswerController.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package com.oronaminc.join.answer.api;

import com.oronaminc.join.answer.dto.AnswerGetResponse;
import com.oronaminc.join.answer.dto.AnswerListResponse;
import com.oronaminc.join.answer.mapper.AnswerMapper;
import com.oronaminc.join.answer.service.AnswerService;
import com.oronaminc.join.member.security.MemberDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -33,14 +38,19 @@ public class AnswerController {
)
@GetMapping("/rooms/{roomId}/questions/{questionId}/answers")
@ResponseStatus(HttpStatus.OK)
public ResponseEntity<AnswerGetResponse> getAnswer(
public ResponseEntity<AnswerListResponse> getAnswers(
@PathVariable Long roomId,
@PathVariable Long questionId,
@AuthenticationPrincipal MemberDetails memberDetails
@AuthenticationPrincipal MemberDetails memberDetails,
@RequestParam(required = false) Long lastId,
@RequestParam(required = false) LocalDateTime lastCreatedAt,
@RequestParam(defaultValue = "10") int size
) {
Long memberId = memberDetails.getId();
AnswerGetResponse response = answerService.getAnswer(roomId, questionId, memberId);
return ResponseEntity.ok(response);

Slice<AnswerGetResponse> response = answerService.getAnswers(roomId, questionId, memberId,
lastId, lastCreatedAt, size);
return ResponseEntity.ok(AnswerMapper.toAnswerListResponse(response));
}

}
49 changes: 38 additions & 11 deletions src/main/java/com/oronaminc/join/answer/dao/AnswerRepository.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
package com.oronaminc.join.answer.dao;

import com.oronaminc.join.answer.domain.Answer;
import com.oronaminc.join.question.domain.Question;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import com.oronaminc.join.answer.domain.Answer;
import com.oronaminc.join.question.domain.Question;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface AnswerRepository extends JpaRepository<Answer, Long> {

Optional<Answer> findByQuestionId(Long questionId);

boolean existsByQuestionIdAndMemberId(Long questionId, Long memberId);
@Query("""
SELECT a
FROM Answer a
JOIN FETCH a.member m
WHERE a.question.id = :questionId
ORDER BY a.createdAt DESC, a.id DESC
""")
List<Answer> findFirstPageByQuestionId(
@Param("questionId") Long questionId,
Pageable pageable
);

@Query("""
SELECT a
FROM Answer a
JOIN FETCH a.member m
WHERE a.question.id = :questionId
AND (a.createdAt < :lastCreatedAt OR (a.createdAt = :lastCreatedAt AND a.id < :lastId))
ORDER BY a.createdAt DESC, a.id DESC
""")
List<Answer> findByQuestionIdWithCursor(
@Param("questionId") Long questionId,
@Param("lastCreatedAt") LocalDateTime lastCreatedAt,
@Param("lastId") Long lastId,
Pageable pageable
);

void deleteByQuestionId(Long questionId);

void deleteByQuestionIn(List<Question> questions);

@Query("""
select count(distinct a.question.id)
from Answer a
where a.question.room.id = :roomId
""")
select count(distinct a.question.id)
from Answer a
where a.question.room.id = :roomId
""")
Long countAnsweredQuestionsByRoomId(@Param("roomId") Long roomId);

@Query("""
select a
from Answer a
where a.question.id in :questionIds
""")
select a
from Answer a
where a.question.id in :questionIds
""")
List<Answer> findAllByQuestionIds(@Param("questionIds") List<Long> questionIds);

}
1 change: 0 additions & 1 deletion src/main/java/com/oronaminc/join/answer/domain/Answer.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// TODO: ddl-auto: create,update에만 유효 -> 추후 flyway sql 생성
@Table(name = "answer", indexes = {
@Index(name = "idx_answer_question_member", columnList = "question_id, member_id")
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.oronaminc.join.answer.dto;

import com.oronaminc.join.global.dto.WriterDto;
import com.oronaminc.join.websocket.common.EventType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
Expand All @@ -13,7 +14,7 @@ public record AnswerCreateResponse(
@Schema(description = "답변이 생성될 질문 ID")
Long questionId,
@Schema(description = "답변 생성/삭제/수정 상태", example = "CREATE")
String event,
EventType event,
@Schema(description = "답변 ID", example = "11")
Long answerId,
@Schema(description = "답변 내용", example = "답변입니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.oronaminc.join.answer.dto;

import com.oronaminc.join.websocket.common.EventType;
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "답변 삭제 응답 DTO")
public record AnswerDeleteResponse(
Long answerId,
@Schema(description = "삭제 이벤트", example = "DELETE")
String event
EventType event
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public record AnswerGetResponse(
@Schema(description = "답변 내용에 대한 공감 수", example = "23")
Long emojiCount,
@Schema(description = "답변 공감 여부", example = "true")
boolean Emojied,
boolean isEmojied,
@Schema(description = "답변 내용", example = "답변입니다.")
String content,
@Schema(description = "작성자 정보 DTO")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.oronaminc.join.answer.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

@Schema(description = "답변 목록을 묶기 위한 DTO")
public record AnswerListResponse(
List<AnswerGetResponse> answers
) {

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.oronaminc.join.answer.dto;

import com.oronaminc.join.websocket.common.EventType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

Expand All @@ -8,7 +9,7 @@
public record AnswerUpdateResponse(
Long answerId,
@Schema(description = "수정 이벤트", example = "UPDATE")
String event,
EventType event,
@Schema(description = "수정된 내용", example = "수정된 답변입니다.")
String content

Expand Down
23 changes: 17 additions & 6 deletions src/main/java/com/oronaminc/join/answer/mapper/AnswerMapper.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package com.oronaminc.join.answer.mapper;

import com.oronaminc.join.answer.domain.Answer;
import com.oronaminc.join.answer.dto.AnswerRequest;
import com.oronaminc.join.answer.dto.AnswerCreateResponse;
import com.oronaminc.join.answer.dto.AnswerGetResponse;
import com.oronaminc.join.answer.dto.AnswerListResponse;
import com.oronaminc.join.answer.dto.AnswerRequest;
import com.oronaminc.join.answer.dto.AnswerUpdateResponse;
import com.oronaminc.join.emoji.domain.TargetType;
import com.oronaminc.join.emoji.service.EmojiReader;
import com.oronaminc.join.global.dto.WriterDto;
import com.oronaminc.join.member.domain.Member;
import com.oronaminc.join.question.domain.Question;
import com.oronaminc.join.websocket.common.EventType;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.domain.Slice;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AnswerMapper {

public static AnswerCreateResponse toAnswerCreateResponse(Answer answer) {
return AnswerCreateResponse.builder()
.questionId(answer.getQuestion().getId())
.event("CREATE")
.event(EventType.CREATE)
.answerId(answer.getId())
.content(answer.getContent())
.emojiCount(0)
Expand All @@ -30,11 +35,12 @@ public static AnswerCreateResponse toAnswerCreateResponse(Answer answer) {
.build();
}

public static AnswerGetResponse toAnswerGetResponse(Answer answer, Long emojiCount, boolean isEmojied) {
public static AnswerGetResponse toAnswerGetResponse(Answer answer, boolean isEmojied) {

return AnswerGetResponse.builder()
.answerId(answer.getId())
.emojiCount(emojiCount)
.Emojied(isEmojied)
.emojiCount(answer.getEmojiCount())
.isEmojied(isEmojied)
.content(answer.getContent())
.writer(new WriterDto(
answer.getMember().getId(),
Expand All @@ -51,9 +57,14 @@ public static Answer toEntity(Question question, Member member, AnswerRequest re
public static AnswerUpdateResponse toAnswerUpdateResponse(Answer answer) {
return AnswerUpdateResponse.builder()
.answerId(answer.getId())
.event("UPDATE")
.event(EventType.UPDATE)
.content(answer.getContent())
.build();
}

public static AnswerListResponse toAnswerListResponse(
Slice<AnswerGetResponse> slice) {
return new AnswerListResponse(slice.getContent());
}

}
31 changes: 17 additions & 14 deletions src/main/java/com/oronaminc/join/answer/service/AnswerReader.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
package com.oronaminc.join.answer.service;

import static com.oronaminc.join.global.exception.ErrorCode.*;

import com.oronaminc.join.global.exception.ErrorCode;
import com.oronaminc.join.room.domain.Room;
import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Component;

import com.oronaminc.join.answer.dao.AnswerRepository;
import com.oronaminc.join.answer.domain.Answer;
import com.oronaminc.join.global.exception.ErrorCode;
import com.oronaminc.join.global.exception.ErrorException;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AnswerReader {
private final AnswerRepository answerRepository;

public boolean existsByQuestionIdAndMemberId(Long questionId, Long memberId) {
return answerRepository.existsByQuestionIdAndMemberId(questionId, memberId);
}
private final AnswerRepository answerRepository;

public Optional<Answer> findById(Long answerId) {
return answerRepository.findById(answerId);
}

public List<Answer> getFirstPageByQuestionId(Long questionId, Pageable pageable) {
return answerRepository.findFirstPageByQuestionId(questionId, pageable);
}

public List<Answer> getAnswerByQuestionIdWithCursor(Long questionId,
LocalDateTime lastCreatedAt, Long lastId, Pageable pageable) {
return answerRepository.findByQuestionIdWithCursor(questionId, lastCreatedAt, lastId,
pageable);
}

public Answer getByQuestionId(Long questionId) {
return answerRepository.findByQuestionId(questionId)
.orElseThrow(() -> new ErrorException(ErrorCode.NOT_FOUND_EXIST_ANSWER));
Expand Down
48 changes: 39 additions & 9 deletions src/main/java/com/oronaminc/join/answer/service/AnswerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@
import com.oronaminc.join.answer.util.PermissionValidator;
import com.oronaminc.join.emoji.domain.TargetType;
import com.oronaminc.join.emoji.service.EmojiReader;
import com.oronaminc.join.global.util.SliceUtil;
import com.oronaminc.join.member.domain.Member;
import com.oronaminc.join.member.service.MemberReader;
import com.oronaminc.join.participant.service.ParticipantService;
import com.oronaminc.join.question.domain.Question;
import com.oronaminc.join.question.service.QuestionReader;
import com.oronaminc.join.room.domain.Room;
import com.oronaminc.join.room.service.RoomReader;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
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 Down Expand Up @@ -48,18 +53,43 @@ public Answer create(Long roomId, Long memberId, Long questionId,

}

@Transactional
public AnswerGetResponse getAnswer(Long roomId, Long questionId, Long memberId) {
Member member = memberReader.getById(memberId);
@Transactional(readOnly = true)
public Slice<AnswerGetResponse> getAnswers(
Long roomId,
Long questionId,
Long memberId,
Long lastId,
LocalDateTime lastCreatedAt,
int size
) {
memberReader.getById(memberId);
roomReader.getById(roomId);
questionReader.getByIdAndRoomId(questionId, roomId);
Answer answer = answerReader.getByQuestionId(questionId);
answerReader.getByQuestionId(questionId);

Pageable pageable = PageRequest.of(0, size + 1);

List<Answer> answers = (lastCreatedAt == null || lastId == null)
? answerReader.getFirstPageByQuestionId(questionId, pageable)
: answerReader.getAnswerByQuestionIdWithCursor(questionId, lastCreatedAt, lastId,
pageable);

// 공감 여부 일괄 조회
List<Long> answerIds = answers.stream().map(Answer::getId).toList();

Set<Long> emojiedAnswerIds = memberId != null
? emojiReader.findTargetIdsByMemberAndTargetTypeInBatch(memberId, TargetType.ANSWER, answerIds)
: Set.of();

List<AnswerGetResponse> responseList = answers.stream()
.map(answer -> {
boolean isEmojied = emojiedAnswerIds.contains(answer.getId());
return AnswerMapper.toAnswerGetResponse(answer, isEmojied);
})
.toList();

Long emojiCount = answer.getEmojiCount();
boolean isEmojied = emojiReader.findByMemberIdAndTargetIdAndTargetType(member.getId(),
answer.getId(), TargetType.ANSWER).isPresent();
return SliceUtil.toSlice(responseList, PageRequest.of(0, size));

return AnswerMapper.toAnswerGetResponse(answer, emojiCount, isEmojied);
}

@Transactional
Expand Down
Loading