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
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ public class AnswerController {
})
@GetMapping
public ApiResponse<AnswerListResponse> getAnswers(
@AuthenticationPrincipal UUID userId,
@Parameter(description = "게시글 ID (UUID)", required = true) @PathVariable UUID postId) {
return ApiResponse.ok(answerService.getAnswers(postId));
return ApiResponse.ok(answerService.getAnswers(postId, userId));
}

@Operation(summary = "답변 작성", description = "게시글에 답변을 작성합니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ public record AnswerWithCommentsResponse(
String authorProfileImage,
Boolean isAdopted,
Boolean isEdited,
Boolean canAdopt,
Instant createdAt,
Instant updatedAt,
List<CommentResponse> comments
) {
public static AnswerWithCommentsResponse of(Answer answer, List<Comment> comments) {
public static AnswerWithCommentsResponse of(Answer answer, List<Comment> comments,
UUID currentUserId, UUID postAuthorId, boolean anyAdopted) {
boolean canAdopt = postAuthorId.equals(currentUserId)
&& !anyAdopted
&& !Boolean.TRUE.equals(answer.getIsAdopted())
&& !answer.getUser().getId().equals(currentUserId);
return new AnswerWithCommentsResponse(
answer.getId(),
answer.getContent(),
Expand All @@ -35,6 +41,7 @@ public static AnswerWithCommentsResponse of(Answer answer, List<Comment> comment
answer.getUser().getProfileImage(),
answer.getIsAdopted(),
answer.getIsEdited(),
canAdopt,
answer.getCreatedAt() != null ? answer.getCreatedAt().toInstant(ZoneOffset.UTC) : null,
answer.getUpdatedAt() != null ? answer.getUpdatedAt().toInstant(ZoneOffset.UTC) : null,
comments.stream().map(CommentResponse::of).toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,18 @@ public class AnswerService {
private final AnswerLikeRepository answerLikeRepository;

@Transactional(readOnly = true)
public AnswerListResponse getAnswers(UUID postId) {
postRepository.findById(postId)
public AnswerListResponse getAnswers(UUID postId, UUID currentUserId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new DevpickException(ErrorCode.COMMUNITY_POST_NOT_FOUND));

List<Answer> answers = answerRepository.findByPost_IdOrderByCreatedAtAsc(postId);
boolean anyAdopted = answers.stream().anyMatch(a -> Boolean.TRUE.equals(a.getIsAdopted()));
UUID postAuthorId = post.getUser().getId();

List<AnswerWithCommentsResponse> result = answers.stream()
.map(answer -> {
List<Comment> comments = commentRepository.findByAnswer_IdOrderByCreatedAtAsc(answer.getId());
return AnswerWithCommentsResponse.of(answer, comments);
return AnswerWithCommentsResponse.of(answer, comments, currentUserId, postAuthorId, anyAdopted);
})
.toList();
return new AnswerListResponse(result);
Expand Down Expand Up @@ -137,6 +140,10 @@ public AnswerResponse adoptAnswer(UUID userId, UUID postId, UUID answerId) {
throw new DevpickException(ErrorCode.COMMUNITY_ANSWER_NOT_FOUND);
}

if (answer.getUser().getId().equals(userId)) {
throw new DevpickException(ErrorCode.COMMUNITY_CANNOT_ADOPT_OWN_ANSWER);
}

answer.adopt();
pointService.earn(answer.getUser(), PointAction.ANSWER_ADOPTED);
return AnswerResponse.of(answer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public enum ErrorCode {
COMMUNITY_ANSWER_NOT_LIKED(HttpStatus.NOT_FOUND, "COMMUNITY_012", "좋아요하지 않은 답변입니다."),
COMMUNITY_DUPLICATE_POST(HttpStatus.CONFLICT, "COMMUNITY_013", "잠시 후 다시 시도해 주세요."),
COMMUNITY_AI_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "COMMUNITY_014", "커리어 게시글은 AI 기능을 지원하지 않습니다."),
COMMUNITY_CANNOT_ADOPT_OWN_ANSWER(HttpStatus.FORBIDDEN, "COMMUNITY_015", "본인이 작성한 답변은 채택할 수 없습니다."),

// Report
REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "REPORT_001", "주간 리포트를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,28 +190,40 @@ void adoptAnswer_alreadyAdopted_returns409() throws Exception {
void getAnswers_success_returns200() throws Exception {
AnswerWithCommentsResponse answerWithComments = new AnswerWithCommentsResponse(
answerId, "Test Answer", userId, "tester", null, null, null,
false, false, Instant.now(), Instant.now(), List.of()
false, false, false, Instant.now(), Instant.now(), List.of()
);
AnswerListResponse listResponse = new AnswerListResponse(List.of(answerWithComments));
given(answerService.getAnswers(postId)).willReturn(listResponse);
given(answerService.getAnswers(postId, userId)).willReturn(listResponse);

mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders
.get("/posts/" + postId + "/answers"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.answers[0].content").value("Test Answer"))
.andExpect(jsonPath("$.data.answers[0].comments").isArray());
.andExpect(jsonPath("$.data.answers[0].comments").isArray())
.andExpect(jsonPath("$.data.answers[0].canAdopt").value(false));
}

@Test
@DisplayName("GET /posts/{postId}/answers - 게시글 없으면 404 반환")
void getAnswers_postNotFound_returns404() throws Exception {
given(answerService.getAnswers(postId))
given(answerService.getAnswers(postId, userId))
.willThrow(new DevpickException(ErrorCode.COMMUNITY_POST_NOT_FOUND));

mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders
.get("/posts/" + postId + "/answers"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false));
}

@Test
@DisplayName("POST /posts/{postId}/answers/{answerId}/adopt - 본인 답변 채택 시 403 반환")
void adoptAnswer_ownAnswer_returns403() throws Exception {
given(answerService.adoptAnswer(userId, postId, answerId))
.willThrow(new DevpickException(ErrorCode.COMMUNITY_CANNOT_ADOPT_OWN_ANSWER));

mockMvc.perform(post("/posts/" + postId + "/answers/" + answerId + "/adopt"))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.success").value(false));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,89 @@ void getAnswers_success_returnsAnswerListWithComments() {
given(answerRepository.findByPost_IdOrderByCreatedAtAsc(postId)).willReturn(List.of(answer));
given(commentRepository.findByAnswer_IdOrderByCreatedAtAsc(answerId)).willReturn(List.of());

AnswerListResponse response = answerService.getAnswers(postId);
AnswerListResponse response = answerService.getAnswers(postId, userId);

assertThat(response.answers()).hasSize(1);
assertThat(response.answers().get(0).id()).isEqualTo(answerId);
assertThat(response.answers().get(0).comments()).isEmpty();
assertThat(response.answers().get(0).canAdopt()).isFalse();
}

@Test
@DisplayName("getAnswers — 게시글 없으면 COMMUNITY_POST_NOT_FOUND 예외")
void getAnswers_postNotFound_throwsException() {
given(postRepository.findById(postId)).willReturn(Optional.empty());

assertThatThrownBy(() -> answerService.getAnswers(postId))
assertThatThrownBy(() -> answerService.getAnswers(postId, userId))
.isInstanceOf(DevpickException.class)
.satisfies(e -> assertThat(((DevpickException) e).getErrorCode())
.isEqualTo(ErrorCode.COMMUNITY_POST_NOT_FOUND));
}

@Test
@DisplayName("getAnswers — 게시글 작성자가 타인의 미채택 답변 조회 시 canAdopt=true")
void getAnswers_postAuthor_othersAnswer_canAdoptTrue() {
UUID otherUserId = UUID.randomUUID();
User otherUser = User.builder().email("other@devpick.kr").nickname("other").job(Job.FRONTEND).level(Level.JUNIOR).build();
ReflectionTestUtils.setField(otherUser, "id", otherUserId);

Answer otherAnswer = Answer.builder().post(post).user(otherUser).content("Other Answer").build();
ReflectionTestUtils.setField(otherAnswer, "id", UUID.randomUUID());

given(postRepository.findById(postId)).willReturn(Optional.of(post));
given(answerRepository.findByPost_IdOrderByCreatedAtAsc(postId)).willReturn(List.of(otherAnswer));
given(commentRepository.findByAnswer_IdOrderByCreatedAtAsc(any())).willReturn(List.of());

AnswerListResponse response = answerService.getAnswers(postId, userId);

assertThat(response.answers().get(0).canAdopt()).isTrue();
}

@Test
@DisplayName("getAnswers — 게시글 작성자가 본인 답변 조회 시 canAdopt=false (본인 답변 채택 불가)")
void getAnswers_postAuthor_ownAnswer_canAdoptFalse() {
given(postRepository.findById(postId)).willReturn(Optional.of(post));
given(answerRepository.findByPost_IdOrderByCreatedAtAsc(postId)).willReturn(List.of(answer));
given(commentRepository.findByAnswer_IdOrderByCreatedAtAsc(answerId)).willReturn(List.of());

AnswerListResponse response = answerService.getAnswers(postId, userId);

assertThat(response.answers().get(0).canAdopt()).isFalse();
}

@Test
@DisplayName("getAnswers — 게시글 작성자가 아닌 유저는 canAdopt=false")
void getAnswers_notPostAuthor_canAdoptFalse() {
UUID otherUserId = UUID.randomUUID();
given(postRepository.findById(postId)).willReturn(Optional.of(post));
given(answerRepository.findByPost_IdOrderByCreatedAtAsc(postId)).willReturn(List.of(answer));
given(commentRepository.findByAnswer_IdOrderByCreatedAtAsc(answerId)).willReturn(List.of());

AnswerListResponse response = answerService.getAnswers(postId, otherUserId);

assertThat(response.answers().get(0).canAdopt()).isFalse();
}

@Test
@DisplayName("getAnswers — 이미 채택된 답변이 있으면 모든 답변에 canAdopt=false")
void getAnswers_anyAnswerAlreadyAdopted_canAdoptFalse() {
UUID otherUserId = UUID.randomUUID();
User otherUser = User.builder().email("other@devpick.kr").nickname("other").job(Job.FRONTEND).level(Level.JUNIOR).build();
ReflectionTestUtils.setField(otherUser, "id", otherUserId);

Answer adoptedAnswer = Answer.builder().post(post).user(otherUser).content("Adopted Answer").build();
ReflectionTestUtils.setField(adoptedAnswer, "id", UUID.randomUUID());
adoptedAnswer.adopt();

given(postRepository.findById(postId)).willReturn(Optional.of(post));
given(answerRepository.findByPost_IdOrderByCreatedAtAsc(postId)).willReturn(List.of(adoptedAnswer));
given(commentRepository.findByAnswer_IdOrderByCreatedAtAsc(any())).willReturn(List.of());

AnswerListResponse response = answerService.getAnswers(postId, userId);

assertThat(response.answers().get(0).canAdopt()).isFalse();
}

@Test
@DisplayName("createAnswer — 성공 시 답변 저장 후 반환") void createAnswer_success_savesAndReturns() {
AnswerCreateRequest request = new AnswerCreateRequest("Test Answer");
Expand Down Expand Up @@ -231,9 +296,15 @@ void deleteAnswer_unauthorized_throwsException() {
@Test
@DisplayName("adoptAnswer — 성공 시 isAdopted=true, isEdited는 변경되지 않는다")
void adoptAnswer_success_adoptsAnswer() {
UUID otherUserId = UUID.randomUUID();
User otherUser = User.builder().email("other@devpick.kr").nickname("other").job(Job.BACKEND).level(Level.JUNIOR).build();
ReflectionTestUtils.setField(otherUser, "id", otherUserId);
Answer otherAnswer = Answer.builder().post(post).user(otherUser).content("Other Answer").build();
ReflectionTestUtils.setField(otherAnswer, "id", answerId);

given(postRepository.findById(postId)).willReturn(Optional.of(post));
given(answerRepository.findAdoptedByPostIdForUpdate(postId)).willReturn(List.of());
given(answerRepository.findById(answerId)).willReturn(Optional.of(answer));
given(answerRepository.findById(answerId)).willReturn(Optional.of(otherAnswer));

AnswerResponse response = answerService.adoptAnswer(userId, postId, answerId);

Expand Down Expand Up @@ -279,4 +350,17 @@ void adoptAnswer_answerNotFound_throwsException() {
.satisfies(e -> assertThat(((DevpickException) e).getErrorCode())
.isEqualTo(ErrorCode.COMMUNITY_ANSWER_NOT_FOUND));
}

@Test
@DisplayName("adoptAnswer — 본인 답변 채택 시 COMMUNITY_CANNOT_ADOPT_OWN_ANSWER 예외")
void adoptAnswer_ownAnswer_throwsException() {
given(postRepository.findById(postId)).willReturn(Optional.of(post));
given(answerRepository.findAdoptedByPostIdForUpdate(postId)).willReturn(List.of());
given(answerRepository.findById(answerId)).willReturn(Optional.of(answer)); // answer.user == post.user == userId

assertThatThrownBy(() -> answerService.adoptAnswer(userId, postId, answerId))
.isInstanceOf(DevpickException.class)
.satisfies(e -> assertThat(((DevpickException) e).getErrorCode())
.isEqualTo(ErrorCode.COMMUNITY_CANNOT_ADOPT_OWN_ANSWER));
}
}
Loading