
질문 상세 조회 화면

질문 검색 결과 화면

질문 리스트 조회 화면
현재 해당 질문에 실제 댓글은 3개가 달려있지만, 질문 검색 결과 화면에서는 5개로 나오고 있다. 질문 상세 조회 화면, 질문 리스트 조회 화면에서는 정상적으로 출력된다.
관련된 코드를 보자
package qnb.question.dto;
import qnb.book.dto.BookResponseDto;
import qnb.question.entity.Question;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class QuestionResponseDto {
private final BookResponseDto book; // 도서
private final Long userId; // 질문 작성자 ID
private final Integer questionId; // 질문 ID
private final String userNickname; // 질문 작성자 닉네임
private final String profileUrl; //사용자 프로필
private final String questionContent; // 질문 내용
private final Integer answerCount; //답변 수
private final Integer likeCount; // 좋아요 수
private final Integer scrapCount; // 스크랩 수
private final LocalDateTime createdAt; // 생성일시
public QuestionResponseDto(
BookResponseDto book, Long userId, Integer questionId,
String userNickname, String profileUrl, String questionContent,
Integer answerCount, Integer likeCount, Integer scrapCount,
LocalDateTime createdAt) {
this.book = book;
this.userId = userId;
this.questionId = questionId;
this.userNickname = userNickname;
this.profileUrl = profileUrl;
this.questionContent = questionContent;
this.answerCount = answerCount;
this.likeCount = likeCount;
this.scrapCount = scrapCount;
this.createdAt = createdAt;
}
public static QuestionResponseDto from(Question question, int answerCount) {
return new QuestionResponseDto(
BookResponseDto.from(question.getBook()),
question.getUser().getUserId(),
question.getQuestionId(),
question.getUser().getUserNickname(),
question.getUser().getProfileUrl(),
question.getQuestionContent(),
answerCount,
question.getLikeCount(),
question.getScrapCount(),
question.getCreatedAt()
);
}
}
여기서 실시간은 답변 수를 받기위해서 question.getAnswerCount()가 아니라 answerCount를 사용하고 있다.
//질문 상세 조회
public QuestionDetailResponseDto getQuestionDetail(Long questionId, String sort) {
Question question = questionRepository.findById(Math.toIntExact(questionId))
.orElseThrow(QuestionNotFoundException::new);
List<Answer> answers = answerRepository.findByQuestion_QuestionId(questionId);
// 상태 우선순위 정의
Map<String, Integer> stateOrder = Map.of(
"BEFORE", 0,
"READING", 1,
"AFTER", 2
);
// 정렬 기준: 인기순 or 상태 + 최신순
Comparator<Answer> comparator = "popular".equals(sort)
? Comparator.comparing(Answer::getLikeCount, Comparator.nullsLast(Integer::compareTo)).reversed()
: Comparator
.comparing((Answer a) -> {
String state = a.getAnswerState();
return state != null ? stateOrder.getOrDefault(state.toUpperCase(), 99) : 99;
})
.thenComparing(Answer::getCreatedAt, Comparator.nullsLast(LocalDateTime::compareTo));
// 질문 정보 DTO로 변환
QuestionResponseDto questionDto = QuestionResponseDto.from(question, answers.size());
// 사용자 기준으로 묶기 (정렬 적용)
Map<Long, List<Answer>> grouped = answers.stream()
.filter(answer -> answer.getUser() != null)
.sorted(comparator)
.collect(Collectors.groupingBy(
answer -> answer.getUser().getUserId(),
LinkedHashMap::new,
Collectors.toList()
));
List<AnswersByUserDto> answersByUser = new ArrayList<>();
for (Map.Entry<Long, List<Answer>> entry : grouped.entrySet()) {
Long userId = entry.getKey();
User user = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);
List<AnswerResponseDto> answerDtos = entry.getValue().stream()
.sorted(
Comparator
.comparing((Answer a) -> {
String state = a.getAnswerState();
return state != null ? stateOrder.getOrDefault(state.toUpperCase(), 99) : 99;
})
.thenComparing(Answer::getCreatedAt, Comparator.nullsLast(LocalDateTime::compareTo))
)
.map(answer -> AnswerResponseDto.from(
answer,
user.getUserId().toString(),
user.getUserNickname() != null ? user.getUserNickname() : "알 수 없음",
user.getProfileUrl() != null ? user.getProfileUrl() : ""
))
.collect(Collectors.toList());
answersByUser.add(new AnswersByUserDto(user, answerDtos));
}
return new QuestionDetailResponseDto(questionDto, answersByUser);
}
// 질문 정보 DTO로 변환
QuestionResponseDto questionDto = QuestionResponseDto.from(question, answers.size());
- 이 부분에서 실시간으로 받기위해서 answers.size()를 사용하고 있다.
3. SearchService.java에서 질문 검색 결과 부분
//2. 질문 검색 결과
else if (type.equals("QUESTION")) {
//키워드 없을 때
if (keyword == null || keyword.trim().isEmpty()) {
QuestionPageResponseDto recentResult = questionService.getRecentQuestions(safePage, safeSize);
List<QuestionSearchOneDto> resultList = recentResult.getQuestions().stream()
.map(q -> new QuestionSearchOneDto(
q.getQuestionId().longValue(),
q.getQuestionContent(),
new BookSimpleDto( // BookResponseDto → BookSimpleDto 변환
q.getBook().getBookId(),
q.getBook().getTitle(),
q.getBook().getImageUrl(),
q.getBook().getAuthor(),
q.getBook().getPublisher(),
q.getBook().getPublishedYear()
),
q.getAnswerCount(),
q.getLikeCount(),
q.getScrapCount(),
q.getUserNickname(),
q.getProfileUrl()
))
.toList();
return new QuestionSearchResponseDto(
resultList,
recentResult.getPageInfoDto()
);
}
//키워드 존재할 때
else {
Page<Question> questions = questionRepository.searchQuestions(keyword, pageable);
return new QuestionSearchResponseDto(
questions.getContent().stream()
.map(q -> new QuestionSearchOneDto(
q.getQuestionId().longValue(),
q.getQuestionContent(),
new BookSimpleDto(
q.getBook().getBookId(),
q.getBook().getTitle(),
q.getBook().getImageUrl(),
q.getBook().getAuthor(),
q.getBook().getPublisher(),
q.getBook().getPublishedYear()
),
q.getAnswerCount(),
q.getLikeCount(),
q.getScrapCount(),
q.getUser().getUserNickname(),
q.getUser().getProfileUrl()
))
.toList(),
new PageInfoDto(
safePage,
questions.getTotalPages(),
(int) questions.getTotalElements())
);
}
}
여기서는 q.getAnswerCount()을 사용하고 있다.
// 최신 질문 조회 메소드
public QuestionPageResponseDto getRecentQuestions(int page, int size) {
// page가 1보다 작으면 1로 보정
int safePage = Math.max(page, 1);
int safeSize = Math.min(Math.max(size, 1), 50);
Pageable pageable = PageRequest.of(
safePage - 1, //스프링은 0부터 시작하니까
safeSize,
Sort.by(Sort.Direction.DESC, "createdAt") //최신순 정렬
);
Page<Question> questionPage = questionRepository.findAll(pageable);
//결과가 비었을 경우 처리
if (questionPage.isEmpty()) {
//빈 리스트로 반환
List<QuestionResponseDto> questions = Collections.emptyList();
PageInfoDto pageInfoDto = new PageInfoDto(
safePage, // 클라이언트가 요청한 페이지 그대로 사용
questionPage.getTotalPages(),
questionPage.getTotalElements()
);
return new QuestionPageResponseDto(questions, pageInfoDto);
}
List<QuestionResponseDto> questions = questionPage.getContent().stream()
.map(question -> QuestionResponseDto.from(question, 0))
// answerCount는 0으로 고정
.collect(Collectors.toList());
PageInfoDto pageInfoDto = new PageInfoDto(
// Spring의 0-based index → 1-based로 변환
questionPage.getNumber() + 1,
questionPage.getTotalPages(),
questionPage.getTotalElements()
);
return new QuestionPageResponseDto(questions, pageInfoDto);
}
.map(question -> QuestionResponseDto.from(question, 0)) 여기서는 이렇게 코드 사용했다.

DB에서 검색을 해보니 이 부분이 다르다. 현재 답변 개수는 3개이지만 answer_count는 5개다. 아마 삭제 메소드가 제대로 작동이 안되는 것 같다.
//답변 삭제
@Transactional
public void deleteAnswer(Long answerId, Long loginUserId) {
Answer answer = answerRepository.findById(answerId)
.orElseThrow(AnswerNotFoundException::new); // 404
if (!answer.getUser().getUserId().equals(loginUserId)) {
throw new UnauthorizedAccessException(); // 403
}
Question question = questionRepository.findById(answer.getQuestion().getQuestionId())
.orElseThrow(QuestionNotFoundException::new);
//질문 답변 수 감소
question.setAnswerCount(Math.max(0, question.getAnswerCount() - 1));
questionRepository.save(question);
//응답 반환
answerRepository.delete(answer);
}
지금 현재
//질문 답변 수 감소
question.setAnswerCount(Math.max(0, question.getAnswerCount() - 1));
questionRepository.save(question);
이 코드로 0 아래로 안떨어지게 설정해놨다.
SearchService.java에서 질문 검색 결과 부분에서 고쳐야할듯하다.
해당 코드 수정 버전
//2. 질문 검색 결과
else if (type.equals("QUESTION")) {
// 키워드 없을 때
if (keyword == null || keyword.trim().isEmpty()) {
QuestionPageResponseDto recentResult = questionService.getRecentQuestions(safePage, safeSize);
List<QuestionSearchOneDto> resultList = recentResult.getQuestions().stream()
.map(q -> {
int realAnswerCount = answerRepository.countByQuestion_QuestionId(q.getQuestionId().longValue());
return new QuestionSearchOneDto(
q.getQuestionId().longValue(),
q.getQuestionContent(),
new BookSimpleDto(
q.getBook().getBookId(),
q.getBook().getTitle(),
q.getBook().getImageUrl(),
q.getBook().getAuthor(),
q.getBook().getPublisher(),
q.getBook().getPublishedYear()
),
realAnswerCount, // 실시간 답변 수
q.getLikeCount(),
q.getScrapCount(),
q.getUserNickname(),
q.getProfileUrl()
);
})
.toList();
return new QuestionSearchResponseDto(
resultList,
recentResult.getPageInfoDto()
);
}
// 키워드 존재할 때
else {
Page<Question> questions = questionRepository.searchQuestions(keyword, pageable);
List<QuestionSearchOneDto> resultList = questions.getContent().stream()
.map(q -> {
int realAnswerCount = answerRepository.countByQuestion_QuestionId(q.getQuestionId().longValue());
return new QuestionSearchOneDto(
q.getQuestionId().longValue(),
q.getQuestionContent(),
new BookSimpleDto(
q.getBook().getBookId(),
q.getBook().getTitle(),
q.getBook().getImageUrl(),
q.getBook().getAuthor(),
q.getBook().getPublisher(),
q.getBook().getPublishedYear()
),
realAnswerCount, // 실시간 답변 수
q.getLikeCount(),
q.getScrapCount(),
q.getUser().getUserNickname(),
q.getUser().getProfileUrl()
);
})
.toList();
return new QuestionSearchResponseDto(
resultList,
new PageInfoDto(
safePage,
questions.getTotalPages(),
(int) questions.getTotalElements())
);
}
}
이제 정상 작동한다!