diff --git a/src/main/java/org/depromeet/sambad/moring/meeting/answer/application/MeetingAnswerService.java b/src/main/java/org/depromeet/sambad/moring/meeting/answer/application/MeetingAnswerService.java index 9df0d861..c0db23b5 100644 --- a/src/main/java/org/depromeet/sambad/moring/meeting/answer/application/MeetingAnswerService.java +++ b/src/main/java/org/depromeet/sambad/moring/meeting/answer/application/MeetingAnswerService.java @@ -4,9 +4,9 @@ import java.time.Clock; import java.time.LocalDateTime; +import java.util.List; import org.depromeet.sambad.moring.answer.application.AnswerService; -import org.depromeet.sambad.moring.answer.domain.Answer; import org.depromeet.sambad.moring.event.application.EventService; import org.depromeet.sambad.moring.meeting.answer.domain.MeetingAnswer; import org.depromeet.sambad.moring.meeting.answer.presentation.exception.DuplicateMeetingAnswerException; @@ -41,17 +41,18 @@ public class MeetingAnswerService { public void save(Long userId, Long meetingId, Long meetingQuestionId, MeetingAnswerRequest request) { MeetingMember loginMember = meetingMemberService.getByUserIdAndMeetingId(userId, meetingId); MeetingQuestion meetingQuestion = meetingQuestionService.getById(meetingId, meetingQuestionId); + Long questionId = meetingQuestion.getQuestion().getId(); + meetingQuestion.validateNotFinished(LocalDateTime.now(clock)); validateNonDuplicateMeetingAnswer(meetingQuestion.getId(), loginMember.getId()); + meetingQuestion.validateMeetingAnswerCount(request.answerIds().size()); - Answer selectedAnswer = answerService.getById(meetingQuestion.getQuestion().getId(), request.answerId()); - MeetingAnswer meetingAnswer = MeetingAnswer - .builder() - .meetingMember(loginMember) - .meetingQuestion(meetingQuestion) - .answer(selectedAnswer) - .build(); - meetingAnswerRepository.save(meetingAnswer); + List answerIds = request.answerIds(); + List meetingAnswers = answerIds.stream() + .map(answerId -> answerService.getById(questionId, answerId)) + .map(answer -> new MeetingAnswer(meetingQuestion, answer, loginMember)) + .toList(); + meetingAnswers.forEach(meetingAnswerRepository::save); eventService.inactivateLastEventByType(userId, meetingId, QUESTION_REGISTERED); advanceToNextQuestionIfAllAnswered(meetingId, meetingQuestion); diff --git a/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/MeetingAnswerQueryRepository.java b/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/MeetingAnswerQueryRepository.java index 6b5dd62d..7df28e63 100644 --- a/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/MeetingAnswerQueryRepository.java +++ b/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/MeetingAnswerQueryRepository.java @@ -1,25 +1,24 @@ package org.depromeet.sambad.moring.meeting.answer.infrastructure; import static com.querydsl.core.types.dsl.Expressions.*; -import static com.querydsl.jpa.JPAExpressions.*; import static org.depromeet.sambad.moring.meeting.answer.domain.QMeetingAnswer.*; import static org.depromeet.sambad.moring.meeting.comment.domain.comment.QMeetingQuestionComment.*; import static org.depromeet.sambad.moring.meeting.member.domain.QMeetingMember.*; +import static org.depromeet.sambad.moring.meeting.question.domain.QMeetingQuestion.*; import java.util.List; import java.util.Objects; +import org.depromeet.sambad.moring.answer.domain.Answer; import org.depromeet.sambad.moring.meeting.answer.domain.MeetingAnswer; import org.depromeet.sambad.moring.meeting.answer.infrastructure.dto.MyMeetingAnswerResponseCustom; import org.depromeet.sambad.moring.meeting.answer.presentation.response.MyMeetingAnswerListResponse; -import org.depromeet.sambad.moring.meeting.comment.domain.comment.MeetingQuestionComment; import org.depromeet.sambad.moring.meeting.member.domain.MeetingMember; +import org.depromeet.sambad.moring.meeting.question.domain.MeetingQuestion; import org.springframework.stereotype.Repository; import com.querydsl.core.Tuple; -import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.StringExpression; -import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -86,23 +85,36 @@ public List findMeetingMembersSelectWith(Long meetingQuestionId, } public MyMeetingAnswerListResponse findAllByMeetingMemberId(Long meetingMemberId) { - List responseCustoms = queryFactory.select(Projections.constructor( - MyMeetingAnswerResponseCustom.class, - meetingAnswer.meetingQuestion.as("meetingQuestion"), - meetingAnswer.as("meetingAnswer"), - as(getMyComment(meetingMemberId), "comment") - )) - .from(meetingAnswer) - .where(meetingAnswer.meetingMember.id.eq(meetingMemberId)) + List meetingQuestions = queryFactory.select(meetingQuestion) + .from(meetingQuestion) + .join(meetingAnswer).on(meetingQuestion.eq(meetingAnswer.meetingQuestion)).fetchJoin() + .join(meetingMember).on(meetingMember.eq(meetingAnswer.meetingMember)).fetchJoin() + .where(meetingMember.id.eq(meetingMemberId)) + .orderBy(meetingQuestion.createdAt.asc()) .fetch(); + List responseCustoms = meetingQuestions.stream() + .map(question -> new MyMeetingAnswerResponseCustom(question.getTitle(), + getMyAnswers(meetingMemberId, question), + getMyComment(meetingMemberId, question))) + .toList(); + return MyMeetingAnswerListResponse.from(responseCustoms); } - private JPQLQuery getMyComment(Long memberId) { - return select(meetingQuestionComment) + private List getMyAnswers(Long memberId, MeetingQuestion meetingQuestion) { + return queryFactory.select(meetingAnswer.answer) + .from(meetingAnswer) + .where(meetingAnswer.meetingQuestion.id.eq(meetingQuestion.getId()), + meetingAnswer.meetingMember.id.eq(memberId)) + .fetch(); + } + + private String getMyComment(Long memberId, MeetingQuestion meetingQuestion) { + return queryFactory.select(meetingQuestionComment.content) .from(meetingQuestionComment) .where(meetingQuestionComment.meetingMember.id.eq(memberId), - meetingQuestionComment.meetingQuestion.eq(meetingAnswer.meetingQuestion)); + meetingQuestionComment.meetingQuestion.eq(meetingQuestion)) + .fetchFirst(); } } diff --git a/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/dto/MyMeetingAnswerResponseCustom.java b/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/dto/MyMeetingAnswerResponseCustom.java index cb2326a3..9c185b2b 100644 --- a/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/dto/MyMeetingAnswerResponseCustom.java +++ b/src/main/java/org/depromeet/sambad/moring/meeting/answer/infrastructure/dto/MyMeetingAnswerResponseCustom.java @@ -1,18 +1,24 @@ package org.depromeet.sambad.moring.meeting.answer.infrastructure.dto; -import org.depromeet.sambad.moring.meeting.answer.domain.MeetingAnswer; -import org.depromeet.sambad.moring.meeting.comment.domain.comment.MeetingQuestionComment; -import org.depromeet.sambad.moring.meeting.question.domain.MeetingQuestion; +import java.util.List; + +import org.depromeet.sambad.moring.answer.domain.Answer; import com.querydsl.core.annotations.QueryProjection; public record MyMeetingAnswerResponseCustom( - MeetingQuestion meetingQuestion, - MeetingAnswer meetingAnswer, - MeetingQuestionComment comment + String meetingQuestionTitle, + List meetingAnswers, + String comment ) { @QueryProjection public MyMeetingAnswerResponseCustom { } + + public List getMeetingAnswers() { + return meetingAnswers.stream() + .map(Answer::getContent) + .toList(); + } } diff --git a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/MeetingAnswerController.java b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/MeetingAnswerController.java index 8a54284e..e92d55e5 100644 --- a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/MeetingAnswerController.java +++ b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/MeetingAnswerController.java @@ -35,7 +35,8 @@ public class MeetingAnswerController { @Operation(summary = "모임원 답변 등록") @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "meetingQuestion(모임원 답변) 등록 성공"), + @ApiResponse(responseCode = "201", description = "모임원 답변 등록 성공"), + @ApiResponse(responseCode = "400", description = "ANSWER_COUNT_OUT_OF_RANGE"), @ApiResponse(responseCode = "404", description = "MEETING_MEMBER_NOT_FOUND / NOT_FOUND_MEETING_QUESTION / " + "NOT_FOUND_ANSWER"), @ApiResponse(responseCode = "409", description = "DUPLICATE_MEETING_ANSWER / FINISHED_MEETING_QUESTION") diff --git a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/request/MeetingAnswerRequest.java b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/request/MeetingAnswerRequest.java index d85bf068..9cde66b9 100644 --- a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/request/MeetingAnswerRequest.java +++ b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/request/MeetingAnswerRequest.java @@ -1,11 +1,17 @@ package org.depromeet.sambad.moring.meeting.answer.presentation.request; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.*; + +import java.util.List; + import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record MeetingAnswerRequest( - @Schema(example = "1", description = "선택한 답변 식별자") + @Schema(description = "선택한 Answer ID 리스트", example = "[1, 2, 3]", requiredMode = REQUIRED) + @Size(max = 9) @NotNull - Long answerId + List answerIds ) { } \ No newline at end of file diff --git a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponse.java b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponse.java index 2297d183..b737e212 100644 --- a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponse.java +++ b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponse.java @@ -1,6 +1,6 @@ package org.depromeet.sambad.moring.meeting.answer.presentation.response; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.*; import java.util.List; @@ -9,11 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema; public record MyMeetingAnswerListResponse( - @Schema( - description = "내가 작성한 답변 목록", - example = "[{\"idx\":1,\"title\":\"갖고 싶은 초능력은?\",\"content\":\"분신술\",\"commentContent\":\"요새 할 일이 너무 많아요ㅠ 분신술로 시간 단축!!\"}]", - requiredMode = REQUIRED - ) + @Schema(description = "내가 작성한 답변 목록", requiredMode = REQUIRED) List contents ) { diff --git a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponseDetail.java b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponseDetail.java index bdfe7e0e..3b00191c 100644 --- a/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponseDetail.java +++ b/src/main/java/org/depromeet/sambad/moring/meeting/answer/presentation/response/MyMeetingAnswerListResponseDetail.java @@ -16,8 +16,8 @@ public record MyMeetingAnswerListResponseDetail( @Schema(title = "릴레이 질문 제목", example = "갖고 싶은 초능력은?", requiredMode = REQUIRED) String title, - @Schema(title = "유저가 선택한 답변", example = "분신술", requiredMode = REQUIRED) - String content, + @Schema(description = "유저가 선택한 답변 리스트", example = "[\"토마토\"]", requiredMode = REQUIRED) + List content, @Schema(title = "유저가 단 댓글", example = "요새 할 일이 너무 많아요ㅠ 분신술로 시간 단축!!", description = "댓글이 없으면 null 응답합니다.", requiredMode = NOT_REQUIRED) @@ -30,9 +30,9 @@ public static List from(List answerContents) { - validateAnswerCount(answerContents); + QuestionType.validateAnswerCount(questionType, answerContents.size()); this.questionType = questionType; this.title = title; this.questionImageFile = questionImageFile; @@ -86,10 +85,4 @@ public String getQuestionImageUrl() { public QuestionType getQuestionType() { return questionType; } - - private void validateAnswerCount(List answerContents) { - if (answerContents.size() < MIN_ANSWER_COUNT || answerContents.size() > MAX_ANSWER_COUNT) { - throw new AnswerCountOutOfRangeException(); - } - } } diff --git a/src/main/java/org/depromeet/sambad/moring/question/domain/QuestionType.java b/src/main/java/org/depromeet/sambad/moring/question/domain/QuestionType.java index 04598f67..c6075e90 100644 --- a/src/main/java/org/depromeet/sambad/moring/question/domain/QuestionType.java +++ b/src/main/java/org/depromeet/sambad/moring/question/domain/QuestionType.java @@ -1,5 +1,22 @@ package org.depromeet.sambad.moring.question.domain; +import org.depromeet.sambad.moring.question.presentation.exception.AnswerCountOutOfRangeException; + public enum QuestionType { SINGLE_CHOICE, MULTIPLE_SHORT_CHOICE, MULTIPLE_DESCRIPTIVE_CHOICE; + + private static final int MIN_ANSWER_COUNT = 1; + private static final int MULTI_CHOICE_MAX_ANSWER_COUNT = 9; + + public static void validateAnswerCount(QuestionType questionType, int answerCount) { + if (SINGLE_CHOICE.equals(questionType)) { + if (answerCount != MIN_ANSWER_COUNT) { + throw new AnswerCountOutOfRangeException(); + } + return; + } + if (answerCount < MIN_ANSWER_COUNT || answerCount > MULTI_CHOICE_MAX_ANSWER_COUNT) { + throw new AnswerCountOutOfRangeException(); + } + } }