Skip to content

Commit d513704

Browse files
author
STGRAM\gffd9
committed
답변 단건 -> 다건 조회 변경
1 parent 65e05fd commit d513704

File tree

12 files changed

+252
-80
lines changed

12 files changed

+252
-80
lines changed

src/main/java/com/oronaminc/join/answer/api/AnswerController.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
package com.oronaminc.join.answer.api;
22

33
import com.oronaminc.join.answer.dto.AnswerGetResponse;
4+
import com.oronaminc.join.answer.dto.AnswerListResponse;
5+
import com.oronaminc.join.answer.mapper.AnswerMapper;
46
import com.oronaminc.join.answer.service.AnswerService;
57
import com.oronaminc.join.member.security.MemberDetails;
68
import io.swagger.v3.oas.annotations.Operation;
79
import io.swagger.v3.oas.annotations.responses.ApiResponse;
10+
import java.time.LocalDateTime;
811
import lombok.RequiredArgsConstructor;
12+
import org.springframework.data.domain.Slice;
913
import org.springframework.http.HttpStatus;
1014
import org.springframework.http.ResponseEntity;
1115
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1216
import org.springframework.web.bind.annotation.GetMapping;
1317
import org.springframework.web.bind.annotation.PathVariable;
1418
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RequestParam;
1520
import org.springframework.web.bind.annotation.ResponseStatus;
1621
import org.springframework.web.bind.annotation.RestController;
1722

@@ -33,14 +38,19 @@ public class AnswerController {
3338
)
3439
@GetMapping("/rooms/{roomId}/questions/{questionId}/answers")
3540
@ResponseStatus(HttpStatus.OK)
36-
public ResponseEntity<AnswerGetResponse> getAnswer(
41+
public ResponseEntity<AnswerListResponse> getAnswers(
3742
@PathVariable Long roomId,
3843
@PathVariable Long questionId,
39-
@AuthenticationPrincipal MemberDetails memberDetails
44+
@AuthenticationPrincipal MemberDetails memberDetails,
45+
@RequestParam(required = false) Long lastId,
46+
@RequestParam(required = false) LocalDateTime lastCreatedAt,
47+
@RequestParam(defaultValue = "10") int size
4048
) {
4149
Long memberId = memberDetails.getId();
42-
AnswerGetResponse response = answerService.getAnswer(roomId, questionId, memberId);
43-
return ResponseEntity.ok(response);
50+
51+
Slice<AnswerGetResponse> response = answerService.getAnswers(roomId, questionId, memberId,
52+
lastId, lastCreatedAt, size);
53+
return ResponseEntity.ok(AnswerMapper.toAnswerListResponse(response));
4454
}
4555

4656
}
Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,62 @@
11
package com.oronaminc.join.answer.dao;
22

3+
import com.oronaminc.join.answer.domain.Answer;
4+
import com.oronaminc.join.question.domain.Question;
5+
import java.time.LocalDateTime;
36
import java.util.List;
47
import java.util.Optional;
8+
import org.springframework.data.domain.Pageable;
59
import org.springframework.data.jpa.repository.JpaRepository;
6-
import com.oronaminc.join.answer.domain.Answer;
7-
import com.oronaminc.join.question.domain.Question;
810
import org.springframework.data.jpa.repository.Query;
911
import org.springframework.data.repository.query.Param;
1012

1113
public interface AnswerRepository extends JpaRepository<Answer, Long> {
1214

1315
Optional<Answer> findByQuestionId(Long questionId);
1416

15-
boolean existsByQuestionIdAndMemberId(Long questionId, Long memberId);
17+
@Query("""
18+
SELECT a
19+
FROM Answer a
20+
JOIN FETCH a.member m
21+
WHERE a.question.id = :questionId
22+
ORDER BY a.createdAt DESC, a.id DESC
23+
""")
24+
List<Answer> findFirstPageByQuestionId(
25+
@Param("questionId") Long questionId,
26+
Pageable pageable
27+
);
28+
29+
@Query("""
30+
SELECT a
31+
FROM Answer a
32+
JOIN FETCH a.member m
33+
WHERE a.question.id = :questionId
34+
AND (a.createdAt < :lastCreatedAt OR (a.createdAt = :lastCreatedAt AND a.id < :lastId))
35+
ORDER BY a.createdAt DESC, a.id DESC
36+
""")
37+
List<Answer> findByQuestionIdWithCursor(
38+
@Param("questionId") Long questionId,
39+
@Param("lastCreatedAt") LocalDateTime lastCreatedAt,
40+
@Param("lastId") Long lastId,
41+
Pageable pageable
42+
);
1643

1744
void deleteByQuestionId(Long questionId);
1845

1946
void deleteByQuestionIn(List<Question> questions);
2047

2148
@Query("""
22-
select count(distinct a.question.id)
23-
from Answer a
24-
where a.question.room.id = :roomId
25-
""")
49+
select count(distinct a.question.id)
50+
from Answer a
51+
where a.question.room.id = :roomId
52+
""")
2653
Long countAnsweredQuestionsByRoomId(@Param("roomId") Long roomId);
2754

2855
@Query("""
29-
select a
30-
from Answer a
31-
where a.question.id in :questionIds
32-
""")
56+
select a
57+
from Answer a
58+
where a.question.id in :questionIds
59+
""")
3360
List<Answer> findAllByQuestionIds(@Param("questionIds") List<Long> questionIds);
3461

3562
}

src/main/java/com/oronaminc/join/answer/domain/Answer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
@Builder
2727
@AllArgsConstructor(access = AccessLevel.PRIVATE)
2828
@NoArgsConstructor(access = AccessLevel.PROTECTED)
29-
// TODO: ddl-auto: create,update에만 유효 -> 추후 flyway sql 생성
3029
@Table(name = "answer", indexes = {
3130
@Index(name = "idx_answer_question_member", columnList = "question_id, member_id")
3231
})

src/main/java/com/oronaminc/join/answer/dto/AnswerGetResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public record AnswerGetResponse(
1212
@Schema(description = "답변 내용에 대한 공감 수", example = "23")
1313
Long emojiCount,
1414
@Schema(description = "답변 공감 여부", example = "true")
15-
boolean Emojied,
15+
boolean isEmojied,
1616
@Schema(description = "답변 내용", example = "답변입니다.")
1717
String content,
1818
@Schema(description = "작성자 정보 DTO")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.oronaminc.join.answer.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import java.util.List;
5+
6+
@Schema(description = "답변 목록을 묶기 위한 DTO")
7+
public record AnswerListResponse(
8+
List<AnswerGetResponse> answers
9+
) {
10+
11+
}

src/main/java/com/oronaminc/join/answer/mapper/AnswerMapper.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package com.oronaminc.join.answer.mapper;
22

33
import com.oronaminc.join.answer.domain.Answer;
4-
import com.oronaminc.join.answer.dto.AnswerRequest;
54
import com.oronaminc.join.answer.dto.AnswerCreateResponse;
65
import com.oronaminc.join.answer.dto.AnswerGetResponse;
6+
import com.oronaminc.join.answer.dto.AnswerListResponse;
7+
import com.oronaminc.join.answer.dto.AnswerRequest;
78
import com.oronaminc.join.answer.dto.AnswerUpdateResponse;
9+
import com.oronaminc.join.emoji.domain.TargetType;
10+
import com.oronaminc.join.emoji.service.EmojiReader;
811
import com.oronaminc.join.global.dto.WriterDto;
912
import com.oronaminc.join.member.domain.Member;
1013
import com.oronaminc.join.question.domain.Question;
1114
import lombok.AccessLevel;
1215
import lombok.NoArgsConstructor;
16+
import org.springframework.data.domain.Slice;
1317

1418
@NoArgsConstructor(access = AccessLevel.PRIVATE)
1519
public class AnswerMapper {
@@ -30,11 +34,12 @@ public static AnswerCreateResponse toAnswerCreateResponse(Answer answer) {
3034
.build();
3135
}
3236

33-
public static AnswerGetResponse toAnswerGetResponse(Answer answer, Long emojiCount, boolean isEmojied) {
37+
public static AnswerGetResponse toAnswerGetResponse(Answer answer, boolean isEmojied) {
38+
3439
return AnswerGetResponse.builder()
3540
.answerId(answer.getId())
36-
.emojiCount(emojiCount)
37-
.Emojied(isEmojied)
41+
.emojiCount(answer.getEmojiCount())
42+
.isEmojied(isEmojied)
3843
.content(answer.getContent())
3944
.writer(new WriterDto(
4045
answer.getMember().getId(),
@@ -56,4 +61,9 @@ public static AnswerUpdateResponse toAnswerUpdateResponse(Answer answer) {
5661
.build();
5762
}
5863

64+
public static AnswerListResponse toAnswerListResponse(
65+
Slice<AnswerGetResponse> slice) {
66+
return new AnswerListResponse(slice.getContent());
67+
}
68+
5969
}

src/main/java/com/oronaminc/join/answer/service/AnswerReader.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
package com.oronaminc.join.answer.service;
22

3-
import static com.oronaminc.join.global.exception.ErrorCode.*;
4-
5-
import com.oronaminc.join.global.exception.ErrorCode;
6-
import com.oronaminc.join.room.domain.Room;
7-
import java.util.List;
8-
import java.util.Optional;
9-
10-
import org.springframework.stereotype.Component;
11-
123
import com.oronaminc.join.answer.dao.AnswerRepository;
134
import com.oronaminc.join.answer.domain.Answer;
5+
import com.oronaminc.join.global.exception.ErrorCode;
146
import com.oronaminc.join.global.exception.ErrorException;
15-
7+
import java.time.LocalDateTime;
8+
import java.util.List;
9+
import java.util.Optional;
1610
import lombok.RequiredArgsConstructor;
11+
import org.springframework.data.domain.Pageable;
12+
import org.springframework.stereotype.Component;
1713

1814
@Component
1915
@RequiredArgsConstructor
2016
public class AnswerReader {
21-
private final AnswerRepository answerRepository;
2217

23-
public boolean existsByQuestionIdAndMemberId(Long questionId, Long memberId) {
24-
return answerRepository.existsByQuestionIdAndMemberId(questionId, memberId);
25-
}
18+
private final AnswerRepository answerRepository;
2619

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

24+
public List<Answer> getFirstPageByQuestionId(Long questionId, Pageable pageable) {
25+
return answerRepository.findFirstPageByQuestionId(questionId, pageable);
26+
}
27+
28+
public List<Answer> getAnswerByQuestionIdWithCursor(Long questionId,
29+
LocalDateTime lastCreatedAt, Long lastId, Pageable pageable) {
30+
return answerRepository.findByQuestionIdWithCursor(questionId, lastCreatedAt, lastId,
31+
pageable);
32+
}
33+
3134
public Answer getByQuestionId(Long questionId) {
3235
return answerRepository.findByQuestionId(questionId)
3336
.orElseThrow(() -> new ErrorException(ErrorCode.NOT_FOUND_EXIST_ANSWER));

src/main/java/com/oronaminc/join/answer/service/AnswerService.java

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@
99
import com.oronaminc.join.answer.util.PermissionValidator;
1010
import com.oronaminc.join.emoji.domain.TargetType;
1111
import com.oronaminc.join.emoji.service.EmojiReader;
12+
import com.oronaminc.join.global.util.SliceUtil;
1213
import com.oronaminc.join.member.domain.Member;
1314
import com.oronaminc.join.member.service.MemberReader;
14-
import com.oronaminc.join.participant.service.ParticipantService;
1515
import com.oronaminc.join.question.domain.Question;
1616
import com.oronaminc.join.question.service.QuestionReader;
1717
import com.oronaminc.join.room.domain.Room;
1818
import com.oronaminc.join.room.service.RoomReader;
19+
import java.time.LocalDateTime;
1920
import java.util.List;
21+
import java.util.Set;
2022
import lombok.RequiredArgsConstructor;
23+
import org.springframework.data.domain.PageRequest;
24+
import org.springframework.data.domain.Pageable;
25+
import org.springframework.data.domain.Slice;
2126
import org.springframework.stereotype.Service;
2227
import org.springframework.transaction.annotation.Transactional;
2328

@@ -48,18 +53,43 @@ public Answer create(Long roomId, Long memberId, Long questionId,
4853

4954
}
5055

51-
@Transactional
52-
public AnswerGetResponse getAnswer(Long roomId, Long questionId, Long memberId) {
53-
Member member = memberReader.getById(memberId);
56+
@Transactional(readOnly = true)
57+
public Slice<AnswerGetResponse> getAnswers(
58+
Long roomId,
59+
Long questionId,
60+
Long memberId,
61+
Long lastId,
62+
LocalDateTime lastCreatedAt,
63+
int size
64+
) {
65+
memberReader.getById(memberId);
5466
roomReader.getById(roomId);
5567
questionReader.getByIdAndRoomId(questionId, roomId);
56-
Answer answer = answerReader.getByQuestionId(questionId);
68+
answerReader.getByQuestionId(questionId);
69+
70+
Pageable pageable = PageRequest.of(0, size + 1);
71+
72+
List<Answer> answers = (lastCreatedAt == null || lastId == null)
73+
? answerReader.getFirstPageByQuestionId(questionId, pageable)
74+
: answerReader.getAnswerByQuestionIdWithCursor(questionId, lastCreatedAt, lastId,
75+
pageable);
76+
77+
// 공감 여부 일괄 조회
78+
List<Long> answerIds = answers.stream().map(Answer::getId).toList();
79+
80+
Set<Long> emojiedAnswerIds = memberId != null
81+
? emojiReader.findTargetIdsByMemberAndTargetTypeInBatch(memberId, TargetType.ANSWER, answerIds)
82+
: Set.of();
83+
84+
List<AnswerGetResponse> responseList = answers.stream()
85+
.map(answer -> {
86+
boolean isEmojied = emojiedAnswerIds.contains(answer.getId());
87+
return AnswerMapper.toAnswerGetResponse(answer, isEmojied);
88+
})
89+
.toList();
5790

58-
Long emojiCount = answer.getEmojiCount();
59-
boolean isEmojied = emojiReader.findByMemberIdAndTargetIdAndTargetType(member.getId(),
60-
answer.getId(), TargetType.ANSWER).isPresent();
91+
return SliceUtil.toSlice(responseList, PageRequest.of(0, size));
6192

62-
return AnswerMapper.toAnswerGetResponse(answer, emojiCount, isEmojied);
6393
}
6494

6595
@Transactional

src/main/java/com/oronaminc/join/emoji/dao/EmojiRepository.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import com.oronaminc.join.emoji.domain.Emoji;
44
import com.oronaminc.join.emoji.domain.TargetType;
5+
import java.util.List;
56
import java.util.Optional;
7+
import java.util.Set;
68
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
711

812
public interface EmojiRepository extends JpaRepository<Emoji, String> {
913

@@ -16,4 +20,17 @@ Optional<Emoji> findByMemberIdAndTargetIdAndTargetType(Long memberId, Long targe
1620

1721
boolean existsByMemberIdAndTargetIdAndTargetType(Long memberId, Long targetId,
1822
TargetType targetType);
23+
24+
@Query("""
25+
SELECT e.targetId
26+
FROM Emoji e
27+
WHERE e.member.id = :memberId
28+
AND e.targetType = :targetType
29+
AND e.targetId IN :targetIds
30+
""")
31+
Set<Long> findTargetIdsByMemberIdAndTargetTypeAndTargetIdIn(
32+
@Param("memberId") Long memberId,
33+
@Param("targetType") TargetType targetType,
34+
@Param("targetIds") List<Long> targetIds
35+
);
1936
}

src/main/java/com/oronaminc/join/emoji/service/EmojiReader.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import com.oronaminc.join.emoji.domain.TargetType;
66
import com.oronaminc.join.global.exception.ErrorCode;
77
import com.oronaminc.join.global.exception.ErrorException;
8+
import java.util.List;
89
import java.util.Optional;
10+
import java.util.Set;
911
import lombok.RequiredArgsConstructor;
1012
import org.springframework.stereotype.Component;
1113

@@ -38,4 +40,9 @@ public boolean existsByMemberIdAndTargetIdAndTargetType(Long memberId, Long targ
3840
targetType);
3941
}
4042

43+
public Set<Long> findTargetIdsByMemberAndTargetTypeInBatch(Long memberId, TargetType targetType, List<Long> targetIds) {
44+
if (targetIds.isEmpty() || targetType == null) return Set.of();
45+
return emojiRepository.findTargetIdsByMemberIdAndTargetTypeAndTargetIdIn(memberId, targetType, targetIds);
46+
}
47+
4148
}

0 commit comments

Comments
 (0)