diff --git a/.github/workflows/backend-dev-cd.yml b/.github/workflows/backend-dev-cd.yml index c6c91e2d9..54e58aad0 100644 --- a/.github/workflows/backend-dev-cd.yml +++ b/.github/workflows/backend-dev-cd.yml @@ -11,11 +11,11 @@ on: env: ARTIFACT_NAME: review-me-dev ARTIFACT_DIRECTORY: ./backend/build/libs - APPLICATION_DIRECTORY: ~/review-me-app + APPLICATION_DIRECTORY: /home/ubuntu/review-me jobs: build: - name: Build Jar file and upload artifact + name: Build Dockerfile and push to DockerHub runs-on: ubuntu-latest steps: @@ -39,15 +39,22 @@ jobs: cd ./backend ./gradlew clean bootJar - - name: Rename artifact file - run: | - mv ${{ env.ARTIFACT_DIRECTORY }}/*.jar ${{ env.ARTIFACT_DIRECTORY }}/${{ env.ARTIFACT_NAME }}.jar + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_ID }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Upload created artifact - uses: actions/upload-artifact@v4 + - name: Build and push + uses: docker/build-push-action@v6 with: - name: ${{ env.ARTIFACT_NAME }} - path: ${{ env.ARTIFACT_DIRECTORY }}/${{ env.ARTIFACT_NAME }}.jar + context: ./backend + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ secrets.DOCKERHUB_ID }}/review-me-app:develop deploy: name: Deploy via self-hosted runner @@ -61,38 +68,20 @@ jobs: repository: ${{ secrets.PRIVATE_REPOSITORY_URL }} token: ${{ secrets.PRIVATE_REPOSITORY_TOKEN }} - - name: Download uploaded artifact - uses: actions/download-artifact@v4 - with: - name: ${{ env.ARTIFACT_NAME }} - - - name: Copy application related files to other directory + - name: Move application-related files to local run: | - sudo mv * ${{ env.APPLICATION_DIRECTORY }} + mkdir -p ${{ env.APPLICATION_DIRECTORY }}/app + mv ./app/* ./app/.* ${{ env.APPLICATION_DIRECTORY }}/app - - name: Find ${{ env.ARTIFACT_NAME }} process - run: | - echo "Checking processes..." - PID=$(pgrep -f ${{ env.ARTIFACT_NAME }}.jar -d " " || true) - if [ -n "$PID" ]; then - echo "Found processes: $PID" - echo "server_running=true" >> "$GITHUB_ENV" - echo "PID=$PID" >> "$GITHUB_ENV" - else - echo "Process not found!" - echo "server_running=false" >> "$GITHUB_ENV" - fi - - - name: Stop server if available (gracefully) - if: env.server_running == 'true' - run: | - echo "Gracefully shutting down process ${{ env.PID }}" - for PID in ${{ env.PID }}; do - sudo kill -15 $PID | true - tail --pid=$PID -f /dev/null | true - done + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_ID }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Start server + - name: Pull from DockerHub run: | - cd ${{ env.APPLICATION_DIRECTORY }} - sudo nohup java -jar ${{ env.ARTIFACT_NAME }}.jar --server.port=8080 --spring.config.location=application-dev.yml & + sudo docker-compose --env-file .env.dev down || true + sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:develop + sudo docker-compose --env-file .env.dev up -d + working-directory: ${{ env.APPLICATION_DIRECTORY }}/app diff --git a/.github/workflows/backend-prod-cd.yml b/.github/workflows/backend-prod-cd.yml index 4b3a6bcf6..3c79f4b85 100644 --- a/.github/workflows/backend-prod-cd.yml +++ b/.github/workflows/backend-prod-cd.yml @@ -11,11 +11,11 @@ on: env: ARTIFACT_NAME: review-me-prod ARTIFACT_DIRECTORY: ./backend/build/libs - APPLICATION_DIRECTORY: ~/review-me-app + APPLICATION_DIRECTORY: /home/ubuntu/review-me jobs: build: - name: Build Jar file and upload artifact + name: Build Dockerfile and push to DockerHub runs-on: ubuntu-latest steps: @@ -39,20 +39,30 @@ jobs: cd ./backend ./gradlew clean bootJar - - name: Rename artifact file - run: | - mv ${{ env.ARTIFACT_DIRECTORY }}/*.jar ${{ env.ARTIFACT_DIRECTORY }}/${{ env.ARTIFACT_NAME }}.jar + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_ID }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Upload created artifact - uses: actions/upload-artifact@v4 + - name: Build and push + uses: docker/build-push-action@v6 with: - name: ${{ env.ARTIFACT_NAME }} - path: ${{ env.ARTIFACT_DIRECTORY }}/${{ env.ARTIFACT_NAME }}.jar + context: ./backend + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ secrets.DOCKERHUB_ID }}/review-me-app:release deploy: name: Deploy via self-hosted runner needs: build - runs-on: [self-hosted, prod] + strategy: + matrix: + runner: [prod-a, prod-b] + runs-on: [ self-hosted, "${{ matrix.runner }}" ] steps: - name: Checkout to secret repository @@ -61,38 +71,20 @@ jobs: repository: ${{ secrets.PRIVATE_REPOSITORY_URL }} token: ${{ secrets.PRIVATE_REPOSITORY_TOKEN }} - - name: Download uploaded artifact - uses: actions/download-artifact@v4 - with: - name: ${{ env.ARTIFACT_NAME }} - - - name: Copy application related files to other directory + - name: Move application-related files to local run: | - sudo mv * ${{ env.APPLICATION_DIRECTORY }} + mkdir -p ${{ env.APPLICATION_DIRECTORY }}/app + mv ./app/* ./app/.* ${{ env.APPLICATION_DIRECTORY }}/app - - name: Find ${{ env.ARTIFACT_NAME }} process - run: | - echo "Checking processes..." - PID=$(pgrep -f ${{ env.ARTIFACT_NAME }}.jar -d " " || true) - if [ -n "$PID" ]; then - echo "Found processes: $PID" - echo "server_running=true" >> "$GITHUB_ENV" - echo "PID=$PID" >> "$GITHUB_ENV" - else - echo "Process not found!" - echo "server_running=false" >> "$GITHUB_ENV" - fi - - - name: Stop server if available (gracefully) - if: env.server_running == 'true' - run: | - echo "Gracefully shutting down process ${{ env.PID }}" - for PID in ${{ env.PID }}; do - sudo kill -15 $PID | true - tail --pid=$PID -f /dev/null | true - done + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_ID }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Start server + - name: Pull from DockerHub run: | - cd ${{ env.APPLICATION_DIRECTORY }} - sudo nohup java -jar ${{ env.ARTIFACT_NAME }}.jar --server.port=8080 --spring.config.location=application-prod.yml & + sudo docker-compose --env-file .env.prod down || true + sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:release + sudo docker-compose --env-file .env.prod up -d + working-directory: ${{ env.APPLICATION_DIRECTORY }}/app diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..7ca531f1c --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,6 @@ +FROM amazoncorretto:17-alpine-jdk + +ARG JAR_FILE=./build/libs/backend-0.0.1-SNAPSHOT.jar +COPY ${JAR_FILE} review-me-app.jar + +ENTRYPOINT ["java", "-jar", "/review-me-app.jar", "-Dspring.config.location=/application.yml"] diff --git a/backend/build.gradle b/backend/build.gradle index cfab09856..bd3a976cf 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -32,6 +32,8 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index d94d361b6..d45cb9711 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -24,8 +24,8 @@ include::create-review.adoc[] === 리뷰 단건 조회 -include::review-list.adoc[] +include::review-detail.adoc[] === 리뷰 목록 조회 -include::review-detail.adoc[] +include::review-list.adoc[] diff --git a/backend/src/docs/asciidoc/review-detail.adoc b/backend/src/docs/asciidoc/review-detail.adoc index 26317af36..42ad5cfe2 100644 --- a/backend/src/docs/asciidoc/review-detail.adoc +++ b/backend/src/docs/asciidoc/review-detail.adoc @@ -1,7 +1,3 @@ ==== 리뷰 단건 조회 -operation::review-detail[snippets="curl-request,request-headers,path-parameters,http-response,response-fields"] - -==== 접근 코드가 올바르지 않은 경우 - -operation::review-detail-invalid-group-access-code[snippets="http-response"] +operation::review-detail-with-session[snippets="curl-request,request-cookies,path-parameters,http-response,response-fields"] diff --git a/backend/src/docs/asciidoc/review-list.adoc b/backend/src/docs/asciidoc/review-list.adoc index 5c2694073..3d8648566 100644 --- a/backend/src/docs/asciidoc/review-list.adoc +++ b/backend/src/docs/asciidoc/review-list.adoc @@ -1,7 +1,3 @@ ==== 자신이 받은 리뷰 목록 조회 -operation::received-reviews[snippets="curl-request,request-headers,http-response,response-fields"] - -==== 접근 코드가 올바르지 않은 경우 - -operation::received-reviews-invalid-group-access-code[snippets="http-response"] +operation::received-review-list-with-pagination[snippets="curl-request,request-cookies,query-parameters,http-response,response-fields"] diff --git a/backend/src/docs/asciidoc/reviewgroup.adoc b/backend/src/docs/asciidoc/reviewgroup.adoc index 666d1e862..c3eb2f803 100644 --- a/backend/src/docs/asciidoc/reviewgroup.adoc +++ b/backend/src/docs/asciidoc/reviewgroup.adoc @@ -4,8 +4,8 @@ operation::review-group-create[snippets="curl-request,request-fields,http-respon ==== 리뷰 그룹 간단 정보 조회 -operation::review-group-summary[snippets="curl-request,request-headers,http-response,response-fields"] +operation::review-group-summary[snippets="curl-request,http-response,response-fields"] ==== 리뷰 요청 코드, 확인 코드 일치 여부 -operation::review-group-check-access[snippets="curl-request,request-fields,http-response,response-fields"] +operation::review-group-check-access[snippets="curl-request,request-fields,http-response,response-cookies"] diff --git a/backend/src/main/java/reviewme/DatabaseInitializer.java b/backend/src/main/java/reviewme/DatabaseInitializer.java index b8a6302bd..38d1e3496 100644 --- a/backend/src/main/java/reviewme/DatabaseInitializer.java +++ b/backend/src/main/java/reviewme/DatabaseInitializer.java @@ -1,10 +1,10 @@ package reviewme; import jakarta.annotation.PostConstruct; -import jakarta.transaction.Transactional; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import reviewme.question.domain.OptionGroup; import reviewme.question.domain.OptionItem; import reviewme.question.domain.OptionType; @@ -23,7 +23,7 @@ @RequiredArgsConstructor public class DatabaseInitializer { - private static final String CATEGORY_HEADER = "이제, 선택한 순간들을 바탕으로 {revieweeName}에 대한 리뷰를 작성해볼게요."; + private static final String CATEGORY_HEADER = "이제, 선택한 순간들을 바탕으로 ${revieweeName}에 대한 리뷰를 작성해볼게요."; private static final String CATEGORY_TEXT_QUESTION = "위에서 선택한 사항과 관련된 경험을 구체적으로 적어 주세요."; private static final int KEYWORD_CHECKBOX_MIN_COUNT = 1; private static final int KEYWORD_CHECKBOX_MAX_COUNT = 2; @@ -36,25 +36,25 @@ public class DatabaseInitializer { @PostConstruct @Transactional - void setup() { + public void setup() { // 템플릿이 이미 존재하면 종료 if (!templateRepository.findAll().isEmpty()) { return; } // 카테고리 선택 섹션 - long categoryQuestionId = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "프로젝트 기간 동안, {revieweeName}의 강점이 드러났던 순간을 선택해주세요.", null, 1)).getId(); - long categorySectionId = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(categoryQuestionId), null, "강점 발견", "{revieweeName}와 함께 한 기억을 떠올려볼게요.", 1)).getId(); + long categoryQuestionId = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "프로젝트 기간 동안, ${revieweeName}의 강점이 드러났던 순간을 선택해주세요.", null, 1)).getId(); + long categorySectionId = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(categoryQuestionId), null, "강점 발견", "${revieweeName/와:과} 함께 한 기억을 떠올려볼게요.", 1)).getId(); long categoryOptionGroupId = optionGroupRepository.save(new OptionGroup(categoryQuestionId, KEYWORD_CHECKBOX_MIN_COUNT, KEYWORD_CHECKBOX_MAX_COUNT)).getId(); - long communicationOptionId = optionItemRepository.save(new OptionItem("🗣️커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)", categoryOptionGroupId, 1, OptionType.CATEGORY)).getId(); - long problemSolvingOptionId = optionItemRepository.save(new OptionItem("💡문제 해결 능력 (ex: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)",categoryOptionGroupId,2, OptionType.CATEGORY )).getId(); - long timeManagingOptionId = optionItemRepository.save(new OptionItem("⏰시간 관리 능력 (ex: 일정과 마감 기한 준수, 업무의 우선 순위 분배)",categoryOptionGroupId,3, OptionType.CATEGORY )).getId(); - long technicalOptionId = optionItemRepository.save(new OptionItem("💻기술적 역량, 전문 지식 (ex: 요구 사항을 이해하고 이를 구현하는 능력)",categoryOptionGroupId,4, OptionType.CATEGORY )).getId(); - long growthOptionId = optionItemRepository.save(new OptionItem("🌱성장 마인드셋 (ex: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)",categoryOptionGroupId,5, OptionType.CATEGORY )).getId(); + long communicationOptionId = optionItemRepository.save(new OptionItem("🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)", categoryOptionGroupId, 1, OptionType.CATEGORY)).getId(); + long problemSolvingOptionId = optionItemRepository.save(new OptionItem("💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)",categoryOptionGroupId,2, OptionType.CATEGORY )).getId(); + long timeManagingOptionId = optionItemRepository.save(new OptionItem("⏰시간 관리 능력 (예: 일정과 마감 기한 준수, 업무의 우선 순위 분배)",categoryOptionGroupId,3, OptionType.CATEGORY )).getId(); + long technicalOptionId = optionItemRepository.save(new OptionItem("💻기술적 역량, 전문 지식 (예: 요구 사항을 이해하고 이를 구현하는 능력)",categoryOptionGroupId,4, OptionType.CATEGORY )).getId(); + long growthOptionId = optionItemRepository.save(new OptionItem("🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)",categoryOptionGroupId,5, OptionType.CATEGORY )).getId(); // 커뮤니케이션 능력 섹션 long checkBoxCommunicationQuestionId = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", null, 1)).getId(); - long textCommunicationQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 {revieweeName}에게 도움이 돼요. {revieweeName} 덕분에 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.", 2)).getId(); + long textCommunicationQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 ${revieweeName}에게 도움이 돼요. ${revieweeName} 덕분에 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.", 2)).getId(); long communicationSectionId = sectionRepository.save(new Section(VisibleType.CONDITIONAL, List.of(checkBoxCommunicationQuestionId, textCommunicationQuestionId), communicationOptionId, "커뮤니케이션 능력", CATEGORY_HEADER, 2)).getId(); long communicationOptionGroupId = optionGroupRepository.save(new OptionGroup(checkBoxCommunicationQuestionId, KEYWORD_CHECKBOX_MIN_COUNT, KEYWORD_CHECKBOX_MAX_COUNT)).getId(); optionItemRepository.save(new OptionItem("반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요.",communicationOptionGroupId,1, OptionType.KEYWORD )); @@ -67,7 +67,7 @@ void setup() { // 문제해결 능력 섹션 long checkBoxProblemSolvingQuestionId = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "문제해결 능력에서 어느 부분이 인상 깊었는지 선택해주세요.", null, 1)).getId(); - long textProblemSolvingQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 {revieweeName}에게 도움이 돼요. 어떤 문제 상황이 발생했고, {revieweeName}이/가 어떻게 해결했는지 그 과정을 떠올려 보세요.", 2)).getId(); + long textProblemSolvingQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 ${revieweeName}에게 도움이 돼요. 어떤 문제 상황이 발생했고, ${revieweeName/가:이} 어떻게 해결했는지 그 과정을 떠올려 보세요.", 2)).getId(); long problemSolvingSectionId = sectionRepository.save(new Section(VisibleType.CONDITIONAL, List.of(checkBoxProblemSolvingQuestionId, textProblemSolvingQuestionId), problemSolvingOptionId, "문제해결 능력", CATEGORY_HEADER, 3)).getId(); long problemSolvingOptionGroupId = optionGroupRepository.save(new OptionGroup(checkBoxProblemSolvingQuestionId, KEYWORD_CHECKBOX_MIN_COUNT, KEYWORD_CHECKBOX_MAX_COUNT)).getId(); optionItemRepository.save(new OptionItem("큰 문제를 작은 단위로 쪼개서 단계별로 해결해나가요.",problemSolvingOptionGroupId,1, OptionType.KEYWORD )); @@ -81,7 +81,7 @@ void setup() { // 시간 관리 능력 섹션 long checkBoxTimeManagingQuestionId = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "시간 관리 능력에서 어느 부분이 인상 깊었는지 선택해주세요.", null, 1)).getId(); - long textTimeManagingQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 {revieweeName}에게 도움이 돼요. {revieweeName} 덕분에 팀이 효율적으로 시간관리를 할 수 있었는지 떠올려 보세요.", 2)).getId(); + long textTimeManagingQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 ${revieweeName}에게 도움이 돼요. ${revieweeName} 덕분에 팀이 효율적으로 시간관리를 할 수 있었는지 떠올려 보세요.", 2)).getId(); long timeManagingSectionId = sectionRepository.save(new Section(VisibleType.CONDITIONAL, List.of(checkBoxTimeManagingQuestionId, textTimeManagingQuestionId), timeManagingOptionId, "시간관리 능력", CATEGORY_HEADER, 4)).getId(); long timeManagingOptionGroupId = optionGroupRepository.save(new OptionGroup(checkBoxTimeManagingQuestionId, KEYWORD_CHECKBOX_MIN_COUNT, KEYWORD_CHECKBOX_MAX_COUNT)).getId(); optionItemRepository.save(new OptionItem("프로젝트의 일정과 주요 마일스톤을 설정하여 체계적으로 일정을 관리해요.",timeManagingOptionGroupId,1, OptionType.KEYWORD )); @@ -92,7 +92,7 @@ void setup() { // 기술 역량 섹션 long checkBoxTechnicalQuestionId = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "기술 역량, 전문 지식에서 어떤 부분이 인상 깊었는지 선택해주세요.", null, 1)).getId(); - long textTechnicalQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 {revieweeName}에게 도움이 돼요. {revieweeName} 덕분에 기술적 역량, 전문 지식적으로 도움을 받은 경험을 떠올려 보세요.", 2)).getId(); + long textTechnicalQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 ${revieweeName}에게 도움이 돼요. ${revieweeName} 덕분에 기술적 역량, 전문 지식적으로 도움을 받은 경험을 떠올려 보세요.", 2)).getId(); long technicalSectionId = sectionRepository.save(new Section(VisibleType.CONDITIONAL, List.of(checkBoxTechnicalQuestionId, textTechnicalQuestionId), technicalOptionId, "기술 역량", CATEGORY_HEADER, 5)).getId(); long technicalOptionGroupId = optionGroupRepository.save(new OptionGroup(checkBoxTechnicalQuestionId, KEYWORD_CHECKBOX_MIN_COUNT, KEYWORD_CHECKBOX_MAX_COUNT)).getId(); optionItemRepository.save(new OptionItem("관련 언어 / 라이브러리 / 프레임워크 지식이 풍부해요.",technicalOptionGroupId,1, OptionType.KEYWORD )); @@ -110,7 +110,7 @@ void setup() { // 성장 마인드셋 섹션 long checkBoxGrowthQuestionId = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "성장 마인드셋에서 어떤 부분이 인상 깊었는지 선택해주세요.", null, 1)).getId(); - long textGrowthQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 {revieweeName}에게 도움이 돼요. 인상깊었던 {revieweeName}의 성장 마인드셋을 떠올려 보세요.", 2)).getId(); + long textGrowthQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, CATEGORY_TEXT_QUESTION, "상황을 자세하게 기록할수록 ${revieweeName}에게 도움이 돼요. 인상깊었던 ${revieweeName}의 성장 마인드셋을 떠올려 보세요.", 2)).getId(); long growthSectionId = sectionRepository.save(new Section(VisibleType.CONDITIONAL, List.of(checkBoxGrowthQuestionId, textGrowthQuestionId), growthOptionId, "성장 마인드셋", CATEGORY_HEADER, 6)).getId(); long growthOptionGroupId = optionGroupRepository.save(new OptionGroup(checkBoxGrowthQuestionId, KEYWORD_CHECKBOX_MIN_COUNT, KEYWORD_CHECKBOX_MAX_COUNT)).getId(); optionItemRepository.save(new OptionItem("어떤 상황에도 긍정적인 태도로 임해요.",growthOptionGroupId,1, OptionType.KEYWORD )); @@ -125,11 +125,11 @@ void setup() { optionItemRepository.save(new OptionItem("새로운 아이디어를 시도하고, 기존의 틀을 깨는 것을 두려워하지 않아요.",growthOptionGroupId,10, OptionType.KEYWORD )); // 성장 목표 설정 섹션 - long textGrowthGoalQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, "앞으로의 성장을 위해서 {revieweeName}이/가 어떤 목표를 설정하면 좋을까요?", "어떤 점을 보완하면 좋을지와 함께 '이렇게 해보면 어떨까?'하는 간단한 솔루션을 제안해봐요.", 1)).getId(); - long textGrowthGoalSectionId = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(textGrowthGoalQuestionId), null, "보완할 점", "{revieweeName}의 성장을 도와주세요!", 7)).getId(); + long textGrowthGoalQuestionId = questionRepository.save(new Question(true, QuestionType.TEXT, "앞으로의 성장을 위해서 ${revieweeName/가:이} 어떤 목표를 설정하면 좋을까요?", "어떤 점을 보완하면 좋을지와 함께 '이렇게 해보면 어떨까?'하는 간단한 솔루션을 제안해봐요.", 1)).getId(); + long textGrowthGoalSectionId = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(textGrowthGoalQuestionId), null, "보완할 점", "${revieweeName}의 성장을 도와주세요!", 7)).getId(); // 응원의 말 섹션 - long textCheerUpQuestionId = questionRepository.save(new Question(false, QuestionType.TEXT, "{revieweeName}에게 전하고 싶은 다른 리뷰가 있거나 응원의 말이 있다면 적어주세요.", null, 1)).getId(); + long textCheerUpQuestionId = questionRepository.save(new Question(false, QuestionType.TEXT, "${revieweeName}에게 전하고 싶은 다른 리뷰가 있거나 응원의 말이 있다면 적어주세요.", null, 1)).getId(); long cheerUpSectionId = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(textCheerUpQuestionId), null, "추가 리뷰/응원", "리뷰를 더 하고 싶은 리뷰어를 위한 추가 리뷰!", 8)).getId(); templateRepository.save(new Template(List.of( diff --git a/backend/src/main/java/reviewme/config/CorsConfig.java b/backend/src/main/java/reviewme/config/CorsConfig.java index 448e95eb7..2f51720ea 100644 --- a/backend/src/main/java/reviewme/config/CorsConfig.java +++ b/backend/src/main/java/reviewme/config/CorsConfig.java @@ -1,16 +1,48 @@ package reviewme.config; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -@Configuration -public class CorsConfig implements WebMvcConfigurer { +public class CorsConfig { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedMethods("*") - .allowedOrigins("*"); + private static final Logger log = LoggerFactory.getLogger(CorsConfig.class); + + private CorsConfig() { + } + + @Configuration + @Profile("local") + static class LocalCorsConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowCredentials(true); + } + } + + @Configuration + @RequiredArgsConstructor + @EnableConfigurationProperties(CorsProperties.class) + @Profile({"dev", "prod"}) + static class ExposedCorsConfig implements WebMvcConfigurer { + + private final CorsProperties corsProperties; + + @Override + public void addCorsMappings(CorsRegistry registry) { + log.info("Allowed origins: {}", corsProperties.allowedOrigins()); + registry.addMapping("/**") + .allowedOrigins(corsProperties.allowedOriginsAsArray()) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowCredentials(true); + } } } diff --git a/backend/src/main/java/reviewme/config/CorsProperties.java b/backend/src/main/java/reviewme/config/CorsProperties.java new file mode 100644 index 000000000..69a7d1c4d --- /dev/null +++ b/backend/src/main/java/reviewme/config/CorsProperties.java @@ -0,0 +1,12 @@ +package reviewme.config; + +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "cors") +public record CorsProperties(List allowedOrigins) { + + public String[] allowedOriginsAsArray() { + return allowedOrigins.toArray(new String[0]); + } +} diff --git a/backend/src/main/java/reviewme/config/DataSourceType.java b/backend/src/main/java/reviewme/config/DataSourceType.java new file mode 100644 index 000000000..c48080ab4 --- /dev/null +++ b/backend/src/main/java/reviewme/config/DataSourceType.java @@ -0,0 +1,7 @@ +package reviewme.config; + +public enum DataSourceType { + READ, + WRITE, + ; +} diff --git a/backend/src/main/java/reviewme/config/ReplicationDatasourceConfig.java b/backend/src/main/java/reviewme/config/ReplicationDatasourceConfig.java new file mode 100644 index 000000000..6a33a9e08 --- /dev/null +++ b/backend/src/main/java/reviewme/config/ReplicationDatasourceConfig.java @@ -0,0 +1,57 @@ +package reviewme.config; + +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +@Profile("prod") +@Configuration +public class ReplicationDatasourceConfig { + + public static final String WRITE_DATA_SOURCE_NAME = "writeDataSource"; + public static final String READ_DATA_SOURCE_NAME = "readDataSource"; + public static final String ROUTING_DATA_SOURCE_NAME = "routingDataSource"; + + @Bean(name = WRITE_DATA_SOURCE_NAME) + @ConfigurationProperties(prefix = "spring.datasource.write.hikari") + public DataSource writeDataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean(name = READ_DATA_SOURCE_NAME) + @ConfigurationProperties(prefix = "spring.datasource.read.hikari") + public DataSource readDataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean + DataSource routingDataSource( + @Qualifier(WRITE_DATA_SOURCE_NAME) DataSource writeDataSource, + @Qualifier(READ_DATA_SOURCE_NAME) DataSource readDataSource) { + AbstractRoutingDataSource routingDataSource = new ReplicationRoutingDataSource(); + Map dataSourceMap = new HashMap<>(); + dataSourceMap.put(DataSourceType.WRITE, writeDataSource); + dataSourceMap.put(DataSourceType.READ, readDataSource); + + routingDataSource.setTargetDataSources(dataSourceMap); + routingDataSource.setDefaultTargetDataSource(writeDataSource); + + return routingDataSource; + } + + @Primary + @Bean + public DataSource dataSource(@Qualifier(ROUTING_DATA_SOURCE_NAME) DataSource routingDataSource) { + return new LazyConnectionDataSourceProxy(routingDataSource); + } +} + diff --git a/backend/src/main/java/reviewme/config/ReplicationRoutingDataSource.java b/backend/src/main/java/reviewme/config/ReplicationRoutingDataSource.java new file mode 100644 index 000000000..49b7aa22b --- /dev/null +++ b/backend/src/main/java/reviewme/config/ReplicationRoutingDataSource.java @@ -0,0 +1,16 @@ +package reviewme.config; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class ReplicationRoutingDataSource extends AbstractRoutingDataSource { + + @Override + protected Object determineCurrentLookupKey() { + boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + if (readOnly) { + return DataSourceType.READ; + } + return DataSourceType.WRITE; + } +} diff --git a/backend/src/main/java/reviewme/config/SwaggerConfig.java b/backend/src/main/java/reviewme/config/SwaggerConfig.java deleted file mode 100644 index 896136561..000000000 --- a/backend/src/main/java/reviewme/config/SwaggerConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package reviewme.config; - -import io.swagger.v3.oas.models.OpenAPI; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import reviewme.config.properties.SwaggerProperties; - -@Profile("!prod") -@Configuration -@EnableConfigurationProperties(SwaggerProperties.class) -@RequiredArgsConstructor -public class SwaggerConfig { - - private final SwaggerProperties swaggerProperties; - - @Bean - public OpenAPI openAPI() { - return new OpenAPI() - .info(swaggerProperties.swaggerInfo()); - } -} diff --git a/backend/src/main/java/reviewme/config/properties/SwaggerProperties.java b/backend/src/main/java/reviewme/config/properties/SwaggerProperties.java deleted file mode 100644 index babdf727b..000000000 --- a/backend/src/main/java/reviewme/config/properties/SwaggerProperties.java +++ /dev/null @@ -1,19 +0,0 @@ -package reviewme.config.properties; - -import io.swagger.v3.oas.models.info.Info; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "docs.info") -public record SwaggerProperties( - String title, - String description, - String version -) { - - public Info swaggerInfo() { - return new Info() - .title(title) - .description(description) - .version(version); - } -} diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index e6fba936a..7724dd90e 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -25,7 +25,6 @@ import reviewme.global.exception.FieldErrorResponse; import reviewme.global.exception.NotFoundException; import reviewme.global.exception.UnauthorizedException; -import reviewme.global.exception.UnexpectedRequestException; @Slf4j @RestControllerAdvice @@ -41,11 +40,6 @@ public ProblemDetail handleBadRequestException(BadRequestException ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getErrorMessage()); } - @ExceptionHandler(UnexpectedRequestException.class) - public ProblemDetail handleUnexpectedRequestException(UnexpectedRequestException ex) { - return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getErrorMessage()); - } - @ExceptionHandler(UnauthorizedException.class) public ProblemDetail handleUnauthorizedException(UnauthorizedException ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getErrorMessage()); @@ -58,7 +52,7 @@ public ProblemDetail handleDataConsistencyException(DataInconsistencyException e @ExceptionHandler(Exception.class) public ProblemDetail handleException(Exception ex) { - log.error("Initial server error has occurred", ex); + log.error("Internal server error has occurred", ex); return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러가 발생했습니다."); } diff --git a/backend/src/main/java/reviewme/global/exception/DataInconsistencyException.java b/backend/src/main/java/reviewme/global/exception/DataInconsistencyException.java index 1f91caeff..41be0c067 100644 --- a/backend/src/main/java/reviewme/global/exception/DataInconsistencyException.java +++ b/backend/src/main/java/reviewme/global/exception/DataInconsistencyException.java @@ -7,6 +7,5 @@ public abstract class DataInconsistencyException extends ReviewMeException { protected DataInconsistencyException(String errorMessage) { super(errorMessage); - log.error("", this); } } diff --git a/backend/src/main/java/reviewme/global/exception/UnexpectedRequestException.java b/backend/src/main/java/reviewme/global/exception/UnexpectedRequestException.java deleted file mode 100644 index 1267cdc74..000000000 --- a/backend/src/main/java/reviewme/global/exception/UnexpectedRequestException.java +++ /dev/null @@ -1,12 +0,0 @@ -package reviewme.global.exception; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public abstract class UnexpectedRequestException extends ReviewMeException { - - protected UnexpectedRequestException(String errorMessage) { - super(errorMessage); - log.warn("", this); - } -} diff --git a/backend/src/main/java/reviewme/question/domain/Question.java b/backend/src/main/java/reviewme/question/domain/Question.java index 584f05215..f59e4ae87 100644 --- a/backend/src/main/java/reviewme/question/domain/Question.java +++ b/backend/src/main/java/reviewme/question/domain/Question.java @@ -55,15 +55,4 @@ public boolean isSelectable() { public boolean hasGuideline() { return guideline != null && !guideline.isEmpty(); } - - public String convertContent(String target, String replacement) { - return content.replace(target, replacement); - } - - public String convertGuideLine(String target, String replacement) { - if (guideline == null) { - return null; - } - return guideline.replace(target, replacement); - } } diff --git a/backend/src/main/java/reviewme/question/domain/exception/QuestionNotFoundException.java b/backend/src/main/java/reviewme/question/domain/exception/QuestionNotFoundException.java deleted file mode 100644 index a76e9e3ba..000000000 --- a/backend/src/main/java/reviewme/question/domain/exception/QuestionNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.question.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.NotFoundException; - -@Slf4j -public class QuestionNotFoundException extends NotFoundException { - - public QuestionNotFoundException(long questionId) { - super("질문이 존재하지 않아요."); - log.warn("Question not found - questionId: {}", questionId, this); - } -} diff --git a/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java b/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java index 1be923085..3935d6a8f 100644 --- a/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java +++ b/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java @@ -1,7 +1,9 @@ package reviewme.question.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import reviewme.question.domain.OptionGroup; @@ -9,4 +11,10 @@ public interface OptionGroupRepository extends JpaRepository { Optional findByQuestionId(long questionId); + + @Query(value = """ + SELECT og.* FROM option_group og + WHERE og.question_id IN (:questionIds) + """, nativeQuery = true) + List findAllByQuestionIds(List questionIds); } diff --git a/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java b/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java index 0b639b6fb..6466aa0bf 100644 --- a/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java +++ b/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java @@ -1,7 +1,6 @@ package reviewme.question.repository; import java.util.List; -import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -13,36 +12,17 @@ public interface OptionItemRepository extends JpaRepository { List findAllByOptionGroupId(long optionGroupId); - @Query(value = """ - SELECT o.id FROM option_item o - LEFT JOIN checkbox_answer_selected_option c - ON c.selected_option_id = o.id - LEFT JOIN checkbox_answer ca - ON c.checkbox_answer_id = ca.id - WHERE ca.review_id = :reviewId - """, nativeQuery = true) - Set findSelectedOptionItemIdsByReviewId(long reviewId); - @Query(value = """ SELECT o.* FROM option_item o - LEFT JOIN checkbox_answer_selected_option c - ON c.selected_option_id = o.id - LEFT JOIN checkbox_answer ca - ON c.checkbox_answer_id = ca.id - WHERE ca.review_id = :reviewId - AND ca.question_id = :questionId - ORDER BY o.position ASC + WHERE o.option_type = :#{#optionType.name()} """, nativeQuery = true) - List findSelectedOptionItemsByReviewIdAndQuestionId(long reviewId, long questionId); + List findAllByOptionType(OptionType optionType); @Query(value = """ SELECT o.* FROM option_item o - INNER JOIN checkbox_answer_selected_option cao - ON cao.selected_option_id = o.id - INNER JOIN checkbox_answer ca - ON cao.checkbox_answer_id = ca.id - WHERE ca.review_id = :reviewId - AND o.option_type = :#{#optionType.name()} + JOIN option_group og + ON o.option_group_id = og.id + WHERE og.question_id IN (:questionIds) """, nativeQuery = true) - List findByReviewIdAndOptionType(long reviewId, OptionType optionType); + List findAllByQuestionIds(List questionIds); } diff --git a/backend/src/main/java/reviewme/question/repository/QuestionRepository.java b/backend/src/main/java/reviewme/question/repository/QuestionRepository.java index d3e1816c9..fdeaea795 100644 --- a/backend/src/main/java/reviewme/question/repository/QuestionRepository.java +++ b/backend/src/main/java/reviewme/question/repository/QuestionRepository.java @@ -9,21 +9,22 @@ public interface QuestionRepository extends JpaRepository { @Query(value = """ - SELECT q.* FROM question q - LEFT JOIN section_question sq - ON sq.question_id = q.id - WHERE sq.section_id = :sectionId - ORDER BY q.position ASC + SELECT q.id FROM question q + JOIN section_question sq + ON q.id = sq.question_id + JOIN template_section ts + ON sq.section_id = ts.section_id + WHERE ts.template_id = :templateId """, nativeQuery = true) - List findAllBySectionId(long sectionId); + Set findAllQuestionIdByTemplateId(long templateId); @Query(value = """ - SELECT q.id FROM question q - LEFT JOIN section_question sq - ON sq.question_id = q.id - LEFT JOIN template_section ts + SELECT q.* FROM question q + JOIN section_question sq + ON q.id = sq.question_id + JOIN template_section ts ON sq.section_id = ts.section_id WHERE ts.template_id = :templateId """, nativeQuery = true) - Set findAllQuestionIdByTemplateId(long templateId); + List findAllByTemplatedId(long templateId); } diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index 4871f7377..c1485f678 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -10,12 +10,12 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import reviewme.global.HeaderProperty; -import reviewme.review.service.CreateReviewService; +import org.springframework.web.bind.annotation.SessionAttribute; import reviewme.review.service.ReviewDetailLookupService; -import reviewme.review.service.ReviewService; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; +import reviewme.review.service.ReviewListLookupService; +import reviewme.review.service.ReviewRegisterService; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; @RestController @@ -24,34 +24,33 @@ public class ReviewController { private static final String GROUP_ACCESS_CODE_HEADER = "GroupAccessCode"; - private final CreateReviewService createReviewService; - private final ReviewService reviewService; + private final ReviewRegisterService reviewRegisterService; + private final ReviewListLookupService reviewListLookupService; private final ReviewDetailLookupService reviewDetailLookupService; @PostMapping("/v2/reviews") - public ResponseEntity createReview(@Valid @RequestBody CreateReviewRequest request) { - long savedReviewId = createReviewService.createReview(request); + public ResponseEntity createReview(@Valid @RequestBody ReviewRegisterRequest request) { + long savedReviewId = reviewRegisterService.registerReview(request); return ResponseEntity.created(URI.create("/reviews/" + savedReviewId)).build(); } @GetMapping("/v2/reviews") public ResponseEntity findReceivedReviews( - @RequestParam String reviewRequestCode, - @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode + @RequestParam(required = false) Long lastReviewId, + @RequestParam(required = false) Integer size, + @SessionAttribute("reviewRequestCode") String reviewRequestCode ) { - ReceivedReviewsResponse response = reviewService.findReceivedReviews(reviewRequestCode, groupAccessCode); + ReceivedReviewsResponse response = reviewListLookupService.getReceivedReviews( + lastReviewId, size, reviewRequestCode); return ResponseEntity.ok(response); } @GetMapping("/v2/reviews/{id}") - public ResponseEntity findReceivedReviewDetail( + public ResponseEntity findReceivedReviewDetail( @PathVariable long id, - @RequestParam String reviewRequestCode, - @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode + @SessionAttribute("reviewRequestCode") String reviewRequestCode ) { - TemplateAnswerResponse response = reviewDetailLookupService.getReviewDetail( - id, reviewRequestCode, groupAccessCode - ); + ReviewDetailResponse response = reviewDetailLookupService.getReviewDetail(id, reviewRequestCode); return ResponseEntity.ok(response); } } diff --git a/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java b/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java index 6f6cf5aab..408b24076 100644 --- a/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java +++ b/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java @@ -15,6 +15,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import reviewme.review.domain.exception.QuestionNotAnsweredException; @Entity @Table(name = "checkbox_answer") @@ -38,9 +39,16 @@ public class CheckboxAnswer { private List selectedOptionIds; public CheckboxAnswer(long questionId, List selectedOptionIds) { + validateSelectedOptionIds(questionId, selectedOptionIds); this.questionId = questionId; this.selectedOptionIds = selectedOptionIds.stream() .map(CheckBoxAnswerSelectedOption::new) .toList(); } + + private void validateSelectedOptionIds(long questionId, List selectedOptionIds) { + if (selectedOptionIds == null || selectedOptionIds.isEmpty()) { + throw new QuestionNotAnsweredException(questionId); + } + } } diff --git a/backend/src/main/java/reviewme/review/domain/CheckboxAnswers.java b/backend/src/main/java/reviewme/review/domain/CheckboxAnswers.java deleted file mode 100644 index bc2447f5d..000000000 --- a/backend/src/main/java/reviewme/review/domain/CheckboxAnswers.java +++ /dev/null @@ -1,28 +0,0 @@ -package reviewme.review.domain; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import reviewme.review.domain.exception.MissingCheckboxAnswerForQuestionException; - -public class CheckboxAnswers { - - private final Map checkboxAnswers; - - public CheckboxAnswers(List checkboxAnswers) { - this.checkboxAnswers = checkboxAnswers.stream() - .collect(Collectors.toMap(CheckboxAnswer::getQuestionId, Function.identity())); - } - - public CheckboxAnswer getAnswerByQuestionId(long questionId) { - if (!checkboxAnswers.containsKey(questionId)) { - throw new MissingCheckboxAnswerForQuestionException(questionId); - } - return checkboxAnswers.get(questionId); - } - - public boolean hasAnswerByQuestionId(long questionId) { - return checkboxAnswers.containsKey(questionId); - } -} diff --git a/backend/src/main/java/reviewme/review/domain/Review.java b/backend/src/main/java/reviewme/review/domain/Review.java index 4bf4a6856..899a41391 100644 --- a/backend/src/main/java/reviewme/review/domain/Review.java +++ b/backend/src/main/java/reviewme/review/domain/Review.java @@ -13,6 +13,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -55,6 +58,24 @@ public Review(long templateId, long reviewGroupId, this.createdAt = LocalDateTime.now(); } + public Set getAnsweredQuestionIds() { + return Stream.concat( + textAnswers.stream().map(TextAnswer::getQuestionId), + checkboxAnswers.stream().map(CheckboxAnswer::getQuestionId) + ).collect(Collectors.toSet()); + } + + public Set getAllCheckBoxOptionIds() { + return checkboxAnswers.stream() + .flatMap(answer -> answer.getSelectedOptionIds().stream()) + .map(CheckBoxAnswerSelectedOption::getSelectedOptionId) + .collect(Collectors.toSet()); + } + + public boolean hasAnsweredQuestion(long questionId) { + return getAnsweredQuestionIds().contains(questionId); + } + public LocalDate getCreatedDate() { return createdAt.toLocalDate(); } diff --git a/backend/src/main/java/reviewme/review/domain/TextAnswer.java b/backend/src/main/java/reviewme/review/domain/TextAnswer.java index ac54530a9..a7ba8e5ff 100644 --- a/backend/src/main/java/reviewme/review/domain/TextAnswer.java +++ b/backend/src/main/java/reviewme/review/domain/TextAnswer.java @@ -10,6 +10,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import reviewme.review.domain.exception.QuestionNotAnsweredException; @Entity @Table(name = "text_answer") @@ -29,7 +30,14 @@ public class TextAnswer { private String content; public TextAnswer(long questionId, String content) { + validateContent(questionId, content); this.questionId = questionId; this.content = content; } + + private void validateContent(long questionId, String content) { + if (content == null || content.isEmpty()) { + throw new QuestionNotAnsweredException(questionId); + } + } } diff --git a/backend/src/main/java/reviewme/review/domain/exception/CategoryOptionByReviewNotFoundException.java b/backend/src/main/java/reviewme/review/domain/exception/CategoryOptionByReviewNotFoundException.java deleted file mode 100644 index bd50713ee..000000000 --- a/backend/src/main/java/reviewme/review/domain/exception/CategoryOptionByReviewNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.NotFoundException; - -@Slf4j -public class CategoryOptionByReviewNotFoundException extends NotFoundException { - - public CategoryOptionByReviewNotFoundException(long reviewId) { - super("리뷰에 선택한 카테고리가 없어요."); - log.warn("CategoryOptionNotFoundException is occured - reviewId: {}", reviewId, this); - } -} diff --git a/backend/src/main/java/reviewme/review/domain/exception/InvalidProjectNameLengthException.java b/backend/src/main/java/reviewme/review/domain/exception/InvalidProjectNameLengthException.java index 457cebfac..2e6386bb4 100644 --- a/backend/src/main/java/reviewme/review/domain/exception/InvalidProjectNameLengthException.java +++ b/backend/src/main/java/reviewme/review/domain/exception/InvalidProjectNameLengthException.java @@ -8,7 +8,7 @@ public class InvalidProjectNameLengthException extends BadRequestException { public InvalidProjectNameLengthException(int projectNameLength, int minLength, int maxLength) { super("프로젝트 이름은 %d글자 이상 %d글자 이하여야 해요.".formatted(minLength, maxLength)); - log.warn("ProjectName is out of bound - projectNameLength:{}, minLength:{}, maxLength: {}", - projectNameLength, minLength, maxLength, this); + log.info("ProjectName is out of bound - projectNameLength: {}, minLength: {}, maxLength: {}", + projectNameLength, minLength, maxLength); } } diff --git a/backend/src/main/java/reviewme/review/domain/exception/InvalidReviewAccessByReviewGroupException.java b/backend/src/main/java/reviewme/review/domain/exception/InvalidReviewAccessByReviewGroupException.java deleted file mode 100644 index 89b802fcf..000000000 --- a/backend/src/main/java/reviewme/review/domain/exception/InvalidReviewAccessByReviewGroupException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.UnexpectedRequestException; - -@Slf4j -public class InvalidReviewAccessByReviewGroupException extends UnexpectedRequestException { - - public InvalidReviewAccessByReviewGroupException(long reviewId, long reviewGroupId) { - super("리뷰가 존재하지 않아요."); - log.warn("Review is not in review group - reviewId: {}, reviewGroupId: {}", reviewId, reviewGroupId, this); - } -} diff --git a/backend/src/main/java/reviewme/review/domain/exception/InvalidRevieweeNameLengthException.java b/backend/src/main/java/reviewme/review/domain/exception/InvalidRevieweeNameLengthException.java index 0294685ef..27408d15b 100644 --- a/backend/src/main/java/reviewme/review/domain/exception/InvalidRevieweeNameLengthException.java +++ b/backend/src/main/java/reviewme/review/domain/exception/InvalidRevieweeNameLengthException.java @@ -8,7 +8,7 @@ public class InvalidRevieweeNameLengthException extends BadRequestException { public InvalidRevieweeNameLengthException(int revieweeNameLength, int minLength, int maxLength) { super("리뷰이 이름은 %d글자 이상 %d글자 이하여야 해요.".formatted(minLength, maxLength)); - log.warn("RevieweeName is out of bound - revieweeNameLength:{}, minLength:{}, maxLength: {}", - revieweeNameLength, minLength, maxLength, this); + log.info("RevieweeName is out of bound - revieweeNameLength: {}, minLength: {}, maxLength: {}", + revieweeNameLength, minLength, maxLength); } } diff --git a/backend/src/main/java/reviewme/review/domain/exception/InvalidTextAnswerLengthException.java b/backend/src/main/java/reviewme/review/domain/exception/InvalidTextAnswerLengthException.java deleted file mode 100644 index 236531179..000000000 --- a/backend/src/main/java/reviewme/review/domain/exception/InvalidTextAnswerLengthException.java +++ /dev/null @@ -1,14 +0,0 @@ -package reviewme.review.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.BadRequestException; - -@Slf4j -public class InvalidTextAnswerLengthException extends BadRequestException { - - public InvalidTextAnswerLengthException(int answerLength, int minLength, int maxLength) { - super("답변의 길이는 %d자 이상 %d자 이하여야 해요.".formatted(minLength, maxLength)); - log.warn("AnswerLength is out of bound - answerLength: {}, minLength: {}, maxLength: {}", - answerLength, minLength, maxLength, this); - } -} diff --git a/backend/src/main/java/reviewme/review/domain/exception/MissingCheckboxAnswerForQuestionException.java b/backend/src/main/java/reviewme/review/domain/exception/MissingCheckboxAnswerForQuestionException.java deleted file mode 100644 index f64df7f25..000000000 --- a/backend/src/main/java/reviewme/review/domain/exception/MissingCheckboxAnswerForQuestionException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.NotFoundException; - -@Slf4j -public class MissingCheckboxAnswerForQuestionException extends NotFoundException { - - public MissingCheckboxAnswerForQuestionException(long questionId) { - super("서버 내부에서 문제가 발생했어요. 서버에 문의해주세요."); - log.error("Checkbox Answer not found for questionId: {}", questionId); - } -} diff --git a/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java b/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java index 6ed567514..674dce41c 100644 --- a/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java +++ b/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java @@ -7,7 +7,7 @@ public class MissingTextAnswerForQuestionException extends DataInconsistencyException { public MissingTextAnswerForQuestionException(long questionId) { - super("질문에 해당하는 서술형 답변을 찾지 못했어요."); - log.error("The question is a text question but text answer not found for questionId: {}", questionId); + super("서버 내부에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."); + log.error("The question is a text question but text answer not found for questionId: {}", questionId, this); } } diff --git a/backend/src/main/java/reviewme/review/domain/exception/QuestionNotAnsweredException.java b/backend/src/main/java/reviewme/review/domain/exception/QuestionNotAnsweredException.java new file mode 100644 index 000000000..118a8e7b9 --- /dev/null +++ b/backend/src/main/java/reviewme/review/domain/exception/QuestionNotAnsweredException.java @@ -0,0 +1,13 @@ +package reviewme.review.domain.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.BadRequestException; + +@Slf4j +public class QuestionNotAnsweredException extends BadRequestException { + + public QuestionNotAnsweredException(long questionId) { + super("질문에 대한 답변을 작성하지 않았어요."); + log.info("question must be answered - questionId: {}", questionId); + } +} diff --git a/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundByGroupAccessCodeException.java b/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundByGroupAccessCodeException.java deleted file mode 100644 index 345fbe3a1..000000000 --- a/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundByGroupAccessCodeException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.NotFoundException; - -@Slf4j -public class ReviewGroupNotFoundByGroupAccessCodeException extends NotFoundException { - - public ReviewGroupNotFoundByGroupAccessCodeException(String groupAccessCode) { - super("리뷰 그룹을 찾을 수 없어요."); - log.info("ReviewGroup not found by groupAccessCode - groupAccessCode: {}", groupAccessCode); - } -} diff --git a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java index 1ab6a1cf3..5b175e081 100644 --- a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java +++ b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java @@ -1,5 +1,6 @@ package reviewme.review.repository; +import java.time.LocalDate; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,8 +11,33 @@ @Repository public interface ReviewRepository extends JpaRepository { - @Query("SELECT r FROM Review r WHERE r.reviewGroupId=:reviewGroupId ORDER BY r.createdAt DESC") - List findReceivedReviewsByGroupId(long reviewGroupId); + @Query(value = """ + SELECT r.* FROM review r + WHERE r.review_group_id = :reviewGroupId + ORDER BY r.created_at DESC + """, nativeQuery = true) + List findAllByGroupId(long reviewGroupId); + + @Query(value = """ + SELECT r.* FROM review r + WHERE r.review_group_id = :reviewGroupId + AND (:lastReviewId IS NULL OR r.id < :lastReviewId) + ORDER BY r.created_at DESC, r.id DESC + LIMIT :limit + """, nativeQuery = true) + List findByReviewGroupIdWithLimit(long reviewGroupId, Long lastReviewId, int limit); Optional findByIdAndReviewGroupId(long reviewId, long reviewGroupId); + + @Query(value = """ + SELECT COUNT(r.id) FROM review r + WHERE r.review_group_id = :reviewGroupId + AND r.id < :reviewId + AND CAST(r.created_at AS DATE) <= :createdDate + """, nativeQuery = true) + Long existsOlderReviewInGroupInLong(long reviewGroupId, long reviewId, LocalDate createdDate); + + default boolean existsOlderReviewInGroup(long reviewGroupId, long reviewId, LocalDate createdDate) { + return existsOlderReviewInGroupInLong(reviewGroupId, reviewId, createdDate) > 0; + } } diff --git a/backend/src/main/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidator.java b/backend/src/main/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidator.java deleted file mode 100644 index d9a10a434..000000000 --- a/backend/src/main/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidator.java +++ /dev/null @@ -1,77 +0,0 @@ -package reviewme.review.service; - -import java.util.HashSet; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import reviewme.question.domain.OptionGroup; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.Question; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; - -@Component -@RequiredArgsConstructor -public class CreateCheckBoxAnswerRequestValidator { - - private final QuestionRepository questionRepository; - private final OptionGroupRepository optionGroupRepository; - private final OptionItemRepository optionItemRepository; - - public void validate(CreateReviewAnswerRequest request) { - validateNotContainingText(request); - Question question = questionRepository.findById(request.questionId()) - .orElseThrow(() -> new SubmittedQuestionNotFoundException(request.questionId())); - OptionGroup optionGroup = optionGroupRepository.findByQuestionId(question.getId()) - .orElseThrow(() -> new OptionGroupNotFoundByQuestionIdException(question.getId())); - validateRequiredQuestion(request, question); - validateOnlyIncludingProvidedOptionItem(request, optionGroup); - validateCheckedOptionItemCount(request, optionGroup); - } - - private void validateNotContainingText(CreateReviewAnswerRequest request) { - if (request.text() != null) { - throw new CheckBoxAnswerIncludedTextException(); - } - } - - private void validateRequiredQuestion(CreateReviewAnswerRequest request, Question question) { - if (question.isRequired() && request.selectedOptionIds() == null) { - throw new RequiredQuestionNotAnsweredException(question.getId()); - } - } - - private void validateOnlyIncludingProvidedOptionItem(CreateReviewAnswerRequest request, OptionGroup optionGroup) { - List providedOptionItemIds = optionItemRepository.findAllByOptionGroupId(optionGroup.getId()) - .stream() - .map(OptionItem::getId) - .toList(); - List submittedOptionItemIds = request.selectedOptionIds(); - - if (!new HashSet<>(providedOptionItemIds).containsAll(submittedOptionItemIds)) { - throw new CheckBoxAnswerIncludedNotProvidedOptionItemException( - request.questionId(), providedOptionItemIds, submittedOptionItemIds - ); - } - } - - private void validateCheckedOptionItemCount(CreateReviewAnswerRequest request, OptionGroup optionGroup) { - if (request.selectedOptionIds().size() < optionGroup.getMinSelectionCount() - || request.selectedOptionIds().size() > optionGroup.getMaxSelectionCount()) { - throw new SelectedOptionItemCountOutOfRangeException( - request.questionId(), - request.selectedOptionIds().size(), - optionGroup.getMinSelectionCount(), - optionGroup.getMaxSelectionCount() - ); - } - } -} diff --git a/backend/src/main/java/reviewme/review/service/CreateReviewService.java b/backend/src/main/java/reviewme/review/service/CreateReviewService.java deleted file mode 100644 index d6225f133..000000000 --- a/backend/src/main/java/reviewme/review/service/CreateReviewService.java +++ /dev/null @@ -1,144 +0,0 @@ -package reviewme.review.service; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.CheckboxAnswer; -import reviewme.review.domain.Review; -import reviewme.review.domain.TextAnswer; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.exception.MissingRequiredQuestionException; -import reviewme.review.service.exception.SubmittedQuestionAndProvidedQuestionMismatchException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.review.service.exception.UnnecessaryQuestionIncludedException; -import reviewme.reviewgroup.domain.ReviewGroup; -import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.template.domain.SectionQuestion; -import reviewme.template.domain.Template; -import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException; -import reviewme.template.repository.SectionRepository; -import reviewme.template.repository.TemplateRepository; - -@Service -@RequiredArgsConstructor -public class CreateReviewService { - - private final ReviewRepository reviewRepository; - private final QuestionRepository questionRepository; - private final ReviewGroupRepository reviewGroupRepository; - private final CreateTextAnswerRequestValidator createTextAnswerRequestValidator; - private final CreateCheckBoxAnswerRequestValidator createCheckBoxAnswerRequestValidator; - private final TemplateRepository templateRepository; - private final SectionRepository sectionRepository; - - @Transactional - public long createReview(CreateReviewRequest request) { - ReviewGroup reviewGroup = validateReviewGroupByRequestCode(request.reviewRequestCode()); - Template template = templateRepository.findById(reviewGroup.getTemplateId()) - .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( - reviewGroup.getId(), reviewGroup.getTemplateId())); - validateSubmittedQuestionsContainedInTemplate(reviewGroup.getTemplateId(), request); - validateOnlyRequiredQuestionsSubmitted(template, request); - - return saveReview(request, reviewGroup); - } - - private ReviewGroup validateReviewGroupByRequestCode(String reviewRequestCode) { - return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - } - - private void validateSubmittedQuestionsContainedInTemplate(long templateId, CreateReviewRequest request) { - Set providedQuestionIds = questionRepository.findAllQuestionIdByTemplateId(templateId); - Set submittedQuestionIds = request.answers() - .stream() - .map(CreateReviewAnswerRequest::questionId) - .collect(Collectors.toSet()); - if (!providedQuestionIds.containsAll(submittedQuestionIds)) { - throw new SubmittedQuestionAndProvidedQuestionMismatchException(submittedQuestionIds, providedQuestionIds); - } - } - - private void validateOnlyRequiredQuestionsSubmitted(Template template, CreateReviewRequest request) { - // 제출된 리뷰의 옵션 아이템 ID 목록 - List selectedOptionItemIds = request.answers() - .stream() - .filter(answer -> answer.selectedOptionIds() != null) - .flatMap(answer -> answer.selectedOptionIds().stream()) - .toList(); - - // 제출된 리뷰의 질문 ID 목록 - List submittedQuestionIds = request.answers() - .stream() - .map(CreateReviewAnswerRequest::questionId) - .toList(); - - // 섹션에서 답해야 할 질문 ID 목록 - List requiredQuestionIdsCandidates = sectionRepository.findAllByTemplateId(template.getId()) - .stream() - // 선택된 optionItem 에 따라 required 를 다르게 책정해서 필터링 - .filter(section -> section.isVisibleBySelectedOptionIds(selectedOptionItemIds)) - .flatMap(section -> section.getQuestionIds().stream()) - .map(SectionQuestion::getQuestionId) - .toList(); - List requiredQuestionIds = questionRepository.findAllById(requiredQuestionIdsCandidates) - .stream() - .filter(Question::isRequired) - .map(Question::getId) - .toList(); - - // 제출된 리뷰의 질문 중에서 제출해야 할 질문이 모두 포함되었는지 검사 - Set submittedQuestionIds2 = new HashSet<>(submittedQuestionIds); - if (!submittedQuestionIds2.containsAll(requiredQuestionIds)) { - List missingRequiredQuestionIds = new ArrayList<>(requiredQuestionIds); - missingRequiredQuestionIds.removeAll(submittedQuestionIds2); - throw new MissingRequiredQuestionException(missingRequiredQuestionIds); - } - - // 제출된 리뷰의 질문 중에서 필수가 아닌 질문이 포함되었는지 검사 - requiredQuestionIds.forEach(submittedQuestionIds2::remove); - List unnecessaryQuestionIds = questionRepository.findAllById(submittedQuestionIds2) - .stream() - .filter(Question::isRequired) - .map(Question::getId) - .toList(); - if (!unnecessaryQuestionIds.isEmpty()) { - throw new UnnecessaryQuestionIncludedException(unnecessaryQuestionIds); - } - } - - private Long saveReview(CreateReviewRequest request, ReviewGroup reviewGroup) { - List textAnswers = new ArrayList<>(); - List checkboxAnswers = new ArrayList<>(); - for (CreateReviewAnswerRequest answerRequests : request.answers()) { - Question question = questionRepository.findById(answerRequests.questionId()) - .orElseThrow(() -> new SubmittedQuestionNotFoundException(answerRequests.questionId())); - QuestionType questionType = question.getQuestionType(); - if (questionType == QuestionType.TEXT && answerRequests.isNotBlank()) { - createTextAnswerRequestValidator.validate(answerRequests); - textAnswers.add(new TextAnswer(question.getId(), answerRequests.text())); - continue; - } - if (questionType == QuestionType.CHECKBOX) { - createCheckBoxAnswerRequestValidator.validate(answerRequests); - checkboxAnswers.add(new CheckboxAnswer(question.getId(), answerRequests.selectedOptionIds())); - } - } - - Review savedReview = reviewRepository.save( - new Review(reviewGroup.getTemplateId(), reviewGroup.getId(), textAnswers, checkboxAnswers) - ); - return savedReview.getId(); - } -} diff --git a/backend/src/main/java/reviewme/review/service/CreateTextAnswerRequestValidator.java b/backend/src/main/java/reviewme/review/service/CreateTextAnswerRequestValidator.java deleted file mode 100644 index f47d03852..000000000 --- a/backend/src/main/java/reviewme/review/service/CreateTextAnswerRequestValidator.java +++ /dev/null @@ -1,48 +0,0 @@ -package reviewme.review.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import reviewme.question.domain.Question; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.exception.InvalidTextAnswerLengthException; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; - -@Component -@RequiredArgsConstructor -public class CreateTextAnswerRequestValidator { - - private static final int MIN_LENGTH = 20; - private static final int MAX_LENGTH = 1_000; - - private final QuestionRepository questionRepository; - - public void validate(CreateReviewAnswerRequest request) { - Question question = questionRepository.findById(request.questionId()) - .orElseThrow(() -> new SubmittedQuestionNotFoundException(request.questionId())); - validateNotIncludingOptions(request); - validateQuestionRequired(question, request); - validateLength(request); - } - - private void validateNotIncludingOptions(CreateReviewAnswerRequest request) { - if (request.selectedOptionIds() != null) { - throw new TextAnswerIncludedOptionItemException(); - } - } - - private void validateQuestionRequired(Question question, CreateReviewAnswerRequest request) { - if (question.isRequired() && request.text() == null) { - throw new RequiredQuestionNotAnsweredException(question.getId()); - } - } - - private void validateLength(CreateReviewAnswerRequest request) { - int textLength = request.text().length(); - if (textLength < MIN_LENGTH || textLength > MAX_LENGTH) { - throw new InvalidTextAnswerLengthException(textLength, MIN_LENGTH, MAX_LENGTH); - } - } -} diff --git a/backend/src/main/java/reviewme/review/service/PageSize.java b/backend/src/main/java/reviewme/review/service/PageSize.java new file mode 100644 index 000000000..7c8d69b2f --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/PageSize.java @@ -0,0 +1,20 @@ +package reviewme.review.service; + +import lombok.Getter; + +@Getter +public class PageSize { + + private static final int DEFAULT_SIZE = 10; + private static final int MAX_SIZE = 50; + + private final int size; + + PageSize(Integer size) { + if (size == null || size < 1 || size > MAX_SIZE) { + this.size = DEFAULT_SIZE; + return; + } + this.size = size; + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java index eb9fb04f9..7356e7bd8 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java @@ -1,155 +1,35 @@ package reviewme.review.service; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import reviewme.question.domain.OptionGroup; -import reviewme.question.domain.Question; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.CheckboxAnswers; import reviewme.review.domain.Review; -import reviewme.review.domain.TextAnswer; -import reviewme.review.domain.TextAnswers; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.dto.response.detail.OptionGroupAnswerResponse; -import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse; -import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; -import reviewme.review.service.dto.response.detail.SectionAnswerResponse; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; -import reviewme.review.service.exception.ReviewGroupUnauthorizedException; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.review.service.exception.ReviewNotFoundByIdAndGroupException; +import reviewme.review.service.mapper.ReviewDetailMapper; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.template.domain.Section; -import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; -import reviewme.template.repository.SectionRepository; @Service @Transactional(readOnly = true) @AllArgsConstructor public class ReviewDetailLookupService { - private final SectionRepository sectionRepository; private final ReviewRepository reviewRepository; private final ReviewGroupRepository reviewGroupRepository; - private final QuestionRepository questionRepository; - private final OptionItemRepository optionItemRepository; - private final OptionGroupRepository optionGroupRepository; - public TemplateAnswerResponse getReviewDetail(long reviewId, String reviewRequestCode, String groupAccessCode) { + private final ReviewDetailMapper reviewDetailMapper; + + @Transactional(readOnly = true) + public ReviewDetailResponse getReviewDetail(long reviewId, String reviewRequestCode) { ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) { - throw new ReviewGroupUnauthorizedException(reviewGroup.getId()); - } + Review review = reviewRepository.findByIdAndReviewGroupId(reviewId, reviewGroup.getId()) .orElseThrow(() -> new ReviewNotFoundByIdAndGroupException(reviewId, reviewGroup.getId())); - long templateId = review.getTemplateId(); - - List
sections = sectionRepository.findAllByTemplateId(templateId); - List sectionResponses = new ArrayList<>(); - - for (Section section : sections) { - addSectionResponse(review, reviewGroup, section, sectionResponses); - } - - return new TemplateAnswerResponse( - templateId, - reviewGroup.getReviewee(), - reviewGroup.getProjectName(), - review.getCreatedDate(), - sectionResponses - ); - } - - private void addSectionResponse(Review review, ReviewGroup reviewGroup, - Section section, List sectionResponses) { - ArrayList questionResponses = new ArrayList<>(); - - for (Question question : questionRepository.findAllBySectionId(section.getId())) { - if (question.isSelectable()) { - addCheckboxQuestionResponse(review, reviewGroup, question, questionResponses); - } else { - addTextQuestionResponse(review, reviewGroup, question, questionResponses); - } - } - - if (!questionResponses.isEmpty()) { - sectionResponses.add(new SectionAnswerResponse( - section.getId(), - section.convertHeader("{revieweeName}", reviewGroup.getReviewee()), - questionResponses - )); - } - } - - private void addCheckboxQuestionResponse(Review review, ReviewGroup reviewGroup, - Question question, ArrayList questionResponses) { - CheckboxAnswers checkboxAnswers = new CheckboxAnswers(review.getCheckboxAnswers()); - - if (checkboxAnswers.hasAnswerByQuestionId(question.getId())) { - questionResponses.add(getCheckboxAnswerResponse(review, question, reviewGroup)); - } - - } - - private void addTextQuestionResponse(Review review, ReviewGroup reviewGroup, - Question question, ArrayList questionResponses) { - TextAnswers textAnswers = new TextAnswers(review.getTextAnswers()); - - if (textAnswers.hasAnswerByQuestionId(question.getId())) { - questionResponses.add(getTextAnswerResponse(textAnswers, question, reviewGroup)); - } - } - - private QuestionAnswerResponse getCheckboxAnswerResponse(Review review, Question question, - ReviewGroup reviewGroup) { - OptionGroup optionGroup = optionGroupRepository.findByQuestionId(question.getId()) - .orElseThrow(() -> new OptionGroupNotFoundByQuestionIdException(question.getId())); - Set selectedOptionItemIds = optionItemRepository.findSelectedOptionItemIdsByReviewId(review.getId()); - List optionItemResponse = - optionItemRepository.findSelectedOptionItemsByReviewIdAndQuestionId(review.getId(), question.getId()) - .stream() - .map(optionItem -> new OptionItemAnswerResponse( - optionItem.getId(), - optionItem.getContent(), - selectedOptionItemIds.contains(optionItem.getId())) - ).toList(); - - OptionGroupAnswerResponse optionGroupAnswerResponse = new OptionGroupAnswerResponse( - optionGroup.getId(), - optionGroup.getMinSelectionCount(), - optionGroup.getMaxSelectionCount(), - optionItemResponse - ); - - return new QuestionAnswerResponse( - question.getId(), - question.isRequired(), - question.getQuestionType(), - question.convertContent("{revieweeName}", reviewGroup.getReviewee()), - optionGroupAnswerResponse, - null - ); - } - - private QuestionAnswerResponse getTextAnswerResponse(TextAnswers textAnswers, Question question, - ReviewGroup reviewGroup) { - TextAnswer textAnswer = textAnswers.getAnswerByQuestionId(question.getId()); - return new QuestionAnswerResponse( - question.getId(), - question.isRequired(), - question.getQuestionType(), - question.convertContent("{revieweeName}", reviewGroup.getReviewee()), - null, - textAnswer.getContent() - ); + return reviewDetailMapper.mapToReviewDetailResponse(review, reviewGroup); } } diff --git a/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java new file mode 100644 index 000000000..39a4fdfb1 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java @@ -0,0 +1,54 @@ +package reviewme.review.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; +import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.mapper.ReviewListMapper; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; + +@Service +@RequiredArgsConstructor +public class ReviewListLookupService { + + private final ReviewGroupRepository reviewGroupRepository; + private final ReviewRepository reviewRepository; + private final ReviewListMapper reviewListMapper; + + @Transactional(readOnly = true) + public ReceivedReviewsResponse getReceivedReviews(Long lastReviewId, Integer size, String reviewRequestCode) { + ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); + + PageSize pageSize = new PageSize(size); + List reviewListResponse + = reviewListMapper.mapToReviewList(reviewGroup, lastReviewId, pageSize.getSize()); + long newLastReviewId = calculateLastReviewId(reviewListResponse); + boolean isLastPage = isLastPage(reviewListResponse, reviewGroup); + return new ReceivedReviewsResponse( + reviewGroup.getReviewee(), reviewGroup.getProjectName(), newLastReviewId, isLastPage, reviewListResponse + ); + } + + private long calculateLastReviewId(List elements) { + if (elements.isEmpty()) { + return 0; + } + return elements.get(elements.size() - 1).reviewId(); + } + + private boolean isLastPage(List elements, ReviewGroup reviewGroup) { + if (elements.isEmpty()) { + return true; + } + + ReviewListElementResponse lastReviewResponse = elements.get(elements.size() - 1); + return !reviewRepository.existsOlderReviewInGroup( + reviewGroup.getId(), lastReviewResponse.reviewId(), lastReviewResponse.createdAt()); + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java b/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java new file mode 100644 index 000000000..5b3d8291e --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java @@ -0,0 +1,28 @@ +package reviewme.review.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reviewme.review.domain.Review; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.mapper.ReviewMapper; +import reviewme.review.service.validator.ReviewValidator; + +@Service +@RequiredArgsConstructor +public class ReviewRegisterService { + + private final ReviewMapper reviewMapper; + private final ReviewValidator reviewValidator; + + private final ReviewRepository reviewRepository; + + @Transactional + public long registerReview(ReviewRegisterRequest request) { + Review review = reviewMapper.mapToReview(request); + reviewValidator.validate(review); + Review registeredReview = reviewRepository.save(review); + return registeredReview.getId(); + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewService.java b/backend/src/main/java/reviewme/review/service/ReviewService.java deleted file mode 100644 index 1ac3d0b3b..000000000 --- a/backend/src/main/java/reviewme/review/service/ReviewService.java +++ /dev/null @@ -1,63 +0,0 @@ -package reviewme.review.service; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; -import reviewme.question.repository.OptionItemRepository; -import reviewme.review.domain.Review; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.dto.response.list.ReceivedReviewCategoryResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; -import reviewme.review.service.exception.ReviewGroupUnauthorizedException; -import reviewme.reviewgroup.domain.ReviewGroup; -import reviewme.reviewgroup.repository.ReviewGroupRepository; - -@Service -@RequiredArgsConstructor -public class ReviewService { - - private final ReviewGroupRepository reviewGroupRepository; - private final OptionItemRepository optionItemRepository; - private final ReviewRepository reviewRepository; - - private final ReviewPreviewGenerator reviewPreviewGenerator = new ReviewPreviewGenerator(); - - @Transactional(readOnly = true) - public ReceivedReviewsResponse findReceivedReviews(String reviewRequestCode, String groupAccessCode) { - ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - - if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) { - throw new ReviewGroupUnauthorizedException(reviewGroup.getId()); - } - - List reviewResponses = - reviewRepository.findReceivedReviewsByGroupId(reviewGroup.getId()) - .stream() - .map(this::createReceivedReviewResponse) - .toList(); - - return new ReceivedReviewsResponse(reviewGroup.getReviewee(), reviewGroup.getProjectName(), reviewResponses); - } - - private ReceivedReviewResponse createReceivedReviewResponse(Review review) { - List categoryOptionItems = optionItemRepository.findByReviewIdAndOptionType(review.getId(), - OptionType.CATEGORY); - - List categoryResponses = categoryOptionItems.stream() - .map(optionItem -> new ReceivedReviewCategoryResponse(optionItem.getId(), optionItem.getContent())) - .toList(); - - return new ReceivedReviewResponse( - review.getId(), - review.getCreatedAt().toLocalDate(), - reviewPreviewGenerator.generatePreview(review.getTextAnswers()), - categoryResponses - ); - } -} diff --git a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewAnswerRequest.java b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java similarity index 59% rename from backend/src/main/java/reviewme/review/service/dto/request/CreateReviewAnswerRequest.java rename to backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java index 32ee6b238..60233be2d 100644 --- a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewAnswerRequest.java +++ b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java @@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotNull; import java.util.List; -public record CreateReviewAnswerRequest( +public record ReviewAnswerRequest( @NotNull(message = "질문 ID를 입력해주세요.") Long questionId, @@ -15,7 +15,11 @@ public record CreateReviewAnswerRequest( @Nullable String text ) { - public boolean isNotBlank() { - return text != null && !text.isBlank(); + public boolean hasTextAnswer() { + return text != null && !text.isEmpty(); + } + + public boolean hasCheckboxAnswer() { + return selectedOptionIds != null && !selectedOptionIds.isEmpty(); } } diff --git a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewRequest.java b/backend/src/main/java/reviewme/review/service/dto/request/ReviewRegisterRequest.java similarity index 80% rename from backend/src/main/java/reviewme/review/service/dto/request/CreateReviewRequest.java rename to backend/src/main/java/reviewme/review/service/dto/request/ReviewRegisterRequest.java index bfb47b769..1b7a6f896 100644 --- a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewRequest.java +++ b/backend/src/main/java/reviewme/review/service/dto/request/ReviewRegisterRequest.java @@ -4,12 +4,12 @@ import jakarta.validation.constraints.NotEmpty; import java.util.List; -public record CreateReviewRequest( +public record ReviewRegisterRequest( @NotBlank(message = "리뷰 요청 코드를 입력해주세요.") String reviewRequestCode, @NotEmpty(message = "답변 내용을 입력해주세요.") - List answers + List answers ) { } diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/TemplateAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/ReviewDetailResponse.java similarity index 87% rename from backend/src/main/java/reviewme/review/service/dto/response/detail/TemplateAnswerResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/detail/ReviewDetailResponse.java index 0e838236b..84e60cf97 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/TemplateAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/ReviewDetailResponse.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.util.List; -public record TemplateAnswerResponse( +public record ReviewDetailResponse( long formId, String revieweeName, String projectName, diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java index ad2887644..eb45bddb2 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java @@ -7,4 +7,8 @@ public record SectionAnswerResponse( String header, List questions ) { + + public boolean hasAnsweredQuestion() { + return !questions.isEmpty(); + } } diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java index 877b3a0de..eace5cd50 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java @@ -5,6 +5,8 @@ public record ReceivedReviewsResponse( String revieweeName, String projectName, - List reviews + long lastReviewId, + boolean isLastPage, + List reviews ) { } diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewCategoryResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewCategoryResponse.java similarity index 69% rename from backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewCategoryResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/list/ReviewCategoryResponse.java index 298e78faa..cb9d0cc7f 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewCategoryResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewCategoryResponse.java @@ -1,6 +1,6 @@ package reviewme.review.service.dto.response.list; -public record ReceivedReviewCategoryResponse( +public record ReviewCategoryResponse( long optionId, String content ) { diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java similarity index 67% rename from backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java index fa6804c18..07aa32c9f 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java @@ -3,10 +3,10 @@ import java.time.LocalDate; import java.util.List; -public record ReceivedReviewResponse( +public record ReviewListElementResponse( long reviewId, LocalDate createdAt, String contentPreview, - List categories + List categories ) { } diff --git a/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedNotProvidedOptionItemException.java b/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedNotProvidedOptionItemException.java index a7d7e04b0..e902d3e2f 100644 --- a/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedNotProvidedOptionItemException.java +++ b/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedNotProvidedOptionItemException.java @@ -2,16 +2,16 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.UnexpectedRequestException; +import reviewme.global.exception.BadRequestException; @Slf4j -public class CheckBoxAnswerIncludedNotProvidedOptionItemException extends UnexpectedRequestException { +public class CheckBoxAnswerIncludedNotProvidedOptionItemException extends BadRequestException { public CheckBoxAnswerIncludedNotProvidedOptionItemException(long questionId, List providedOptionIds, List submittedOptionIds) { super("제공되는 선택지에 없는 선택지를 응답했어요."); - log.warn("Answer included not provided options - questionId:{}, providedOptionIds: {}, submittedOptionIds: {}", + log.info("Answer included not provided options - questionId:{}, providedOptionIds: {}, submittedOptionIds: {}", questionId, providedOptionIds, submittedOptionIds, this); } } diff --git a/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedTextException.java b/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedTextException.java index f3a7843b6..a563acbf0 100644 --- a/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedTextException.java +++ b/backend/src/main/java/reviewme/review/service/exception/CheckBoxAnswerIncludedTextException.java @@ -6,8 +6,8 @@ @Slf4j public class CheckBoxAnswerIncludedTextException extends BadRequestException { - public CheckBoxAnswerIncludedTextException() { + public CheckBoxAnswerIncludedTextException(long questionId) { super("체크박스형 응답은 텍스트를 포함할 수 없어요."); - log.warn("CheckBox type answer cannot have option items"); + log.info("CheckBox type answer cannot have option items - questionId: {}", questionId); } } diff --git a/backend/src/main/java/reviewme/review/service/exception/InvalidTextAnswerLengthException.java b/backend/src/main/java/reviewme/review/service/exception/InvalidTextAnswerLengthException.java new file mode 100644 index 000000000..01c02ceb7 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/exception/InvalidTextAnswerLengthException.java @@ -0,0 +1,18 @@ +package reviewme.review.service.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.BadRequestException; + +@Slf4j +public class InvalidTextAnswerLengthException extends BadRequestException { + + public InvalidTextAnswerLengthException(long questionId, int answerLength, int minLength, int maxLength) { + super("답변의 길이는 %d자 이상 %d자 이하여야 해요.".formatted(minLength, maxLength)); + log.warn("AnswerLength is out of bound - questionId: {}, answerLength: {}, minLength: {}, maxLength: {}", + questionId, answerLength, minLength, maxLength, this); + } + + public InvalidTextAnswerLengthException(long questionId, int answerLength, int maxLength) { + this(questionId, answerLength, 0, maxLength); + } +} diff --git a/backend/src/main/java/reviewme/review/service/exception/MissingRequiredQuestionException.java b/backend/src/main/java/reviewme/review/service/exception/MissingRequiredQuestionException.java index efac7de80..a6a547acf 100644 --- a/backend/src/main/java/reviewme/review/service/exception/MissingRequiredQuestionException.java +++ b/backend/src/main/java/reviewme/review/service/exception/MissingRequiredQuestionException.java @@ -9,7 +9,6 @@ public class MissingRequiredQuestionException extends BadRequestException { public MissingRequiredQuestionException(List missingRequiredQuestionIds) { super("필수 질문을 제출하지 않았어요."); - log.warn("Required question is not submitted. Missing Required questionIds: {}", - missingRequiredQuestionIds, this); + log.info("Required question is not submitted. Missing Required questionIds: {}", missingRequiredQuestionIds); } } diff --git a/backend/src/main/java/reviewme/template/domain/exception/OptionGroupNotFoundByQuestionIdException.java b/backend/src/main/java/reviewme/review/service/exception/OptionGroupNotFoundByQuestionIdException.java similarity index 63% rename from backend/src/main/java/reviewme/template/domain/exception/OptionGroupNotFoundByQuestionIdException.java rename to backend/src/main/java/reviewme/review/service/exception/OptionGroupNotFoundByQuestionIdException.java index 88bcd7f3b..9039de3ae 100644 --- a/backend/src/main/java/reviewme/template/domain/exception/OptionGroupNotFoundByQuestionIdException.java +++ b/backend/src/main/java/reviewme/review/service/exception/OptionGroupNotFoundByQuestionIdException.java @@ -1,4 +1,4 @@ -package reviewme.template.domain.exception; +package reviewme.review.service.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.DataInconsistencyException; @@ -7,7 +7,7 @@ public class OptionGroupNotFoundByQuestionIdException extends DataInconsistencyException { public OptionGroupNotFoundByQuestionIdException(long questionId) { - super("응답한 질문과 대응하는 선택형 문항이 존재하지 않아요."); - log.error("User submitted checkBoxAnswer without provided options - questionId: {}", questionId); + super("서버 내부에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."); + log.error("User submitted checkBoxAnswer without provided options - questionId: {}", questionId, this); } } diff --git a/backend/src/main/java/reviewme/review/service/exception/OptionItemNotFoundBySelectedOptionId.java b/backend/src/main/java/reviewme/review/service/exception/OptionItemNotFoundBySelectedOptionId.java deleted file mode 100644 index 517354d35..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/OptionItemNotFoundBySelectedOptionId.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.service.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.DataInconsistencyException; - -@Slf4j -public class OptionItemNotFoundBySelectedOptionId extends DataInconsistencyException { - - public OptionItemNotFoundBySelectedOptionId(long selectedOptionId) { - super("서버 내부에서 문제가 발생했어요. 서버에 문의해주세요."); - log.error("Submitted checkBox's option item is not exist in database - selectedOptionId: {}", selectedOptionId); - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/RequiredQuestionNotAnsweredException.java b/backend/src/main/java/reviewme/review/service/exception/RequiredQuestionNotAnsweredException.java deleted file mode 100644 index 0367b93f6..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/RequiredQuestionNotAnsweredException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.service.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.BadRequestException; - -@Slf4j -public class RequiredQuestionNotAnsweredException extends BadRequestException { - - public RequiredQuestionNotAnsweredException(long questionId) { - super("필수 질문의 답변을 작성하지 않았어요."); - log.warn("Required question must be answered - questionId: {}", questionId, this); - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByCodesException.java b/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByCodesException.java deleted file mode 100644 index 7fa65044b..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByCodesException.java +++ /dev/null @@ -1,14 +0,0 @@ -package reviewme.review.service.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.BadRequestException; - -@Slf4j -public class ReviewGroupNotFoundByCodesException extends BadRequestException { - - public ReviewGroupNotFoundByCodesException(String reviewRequestCode, String groupAccessCode) { - super("인증 정보에 해당하는 리뷰 확인 코드와 리뷰 요청 코드를 통해 찾을 수 있는 리뷰 그룹이 없어요."); - log.info("ReviewGroup not found by codes - reviewRequestCode: {}, groupAccessCode: {}", - reviewRequestCode, groupAccessCode); - } -} diff --git a/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundByReviewRequestCodeException.java b/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java similarity index 91% rename from backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundByReviewRequestCodeException.java rename to backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java index 6b8cb64fe..121296482 100644 --- a/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundByReviewRequestCodeException.java +++ b/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java @@ -1,4 +1,4 @@ -package reviewme.review.domain.exception; +package reviewme.review.service.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.NotFoundException; diff --git a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupUnauthorizedException.java b/backend/src/main/java/reviewme/review/service/exception/ReviewGroupUnauthorizedException.java index e18bd7e34..125f2e7e9 100644 --- a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupUnauthorizedException.java +++ b/backend/src/main/java/reviewme/review/service/exception/ReviewGroupUnauthorizedException.java @@ -7,7 +7,8 @@ public class ReviewGroupUnauthorizedException extends UnauthorizedException { public ReviewGroupUnauthorizedException(long reviewGroupId) { - super("리뷰를 확인할 권한이 없어요."); + // 확인 코드가 올바르지 않은 경우에만 해당 예외가 발생하나, 보안상 사용자에게 정확한 이유를 알려주지 않습니다 + super("존재하지 않는 리뷰 그룹이거나, 확인 코드가 올바르지 않아요."); log.info("Group access code mismatch on review group: {}", reviewGroupId); } } diff --git a/backend/src/main/java/reviewme/review/service/exception/ReviewNotFoundException.java b/backend/src/main/java/reviewme/review/service/exception/ReviewNotFoundException.java deleted file mode 100644 index ed4d79c00..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/ReviewNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.service.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.NotFoundException; - -@Slf4j -public class ReviewNotFoundException extends NotFoundException { - - public ReviewNotFoundException(String reviewRequestCode, long reviewId) { - super("리뷰가 존재하지 않아요."); - log.info("Review not found: reviewRequestCode: {}, reviewId: {}", reviewRequestCode, reviewId); - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/SelectedOptionItemCountOutOfRangeException.java b/backend/src/main/java/reviewme/review/service/exception/SelectedOptionItemCountOutOfRangeException.java index a91cc26d0..6f775597e 100644 --- a/backend/src/main/java/reviewme/review/service/exception/SelectedOptionItemCountOutOfRangeException.java +++ b/backend/src/main/java/reviewme/review/service/exception/SelectedOptionItemCountOutOfRangeException.java @@ -10,9 +10,9 @@ public SelectedOptionItemCountOutOfRangeException(long questionId, int selectedC int minSelectionCount, int maxSelectionCount) { super("체크박스 응답 개수가 범위를 벗어났어요. (선택된 개수: %d, 최소 개수: %d, 최대 개수: %d)" .formatted(selectedCount, minSelectionCount, maxSelectionCount)); - log.warn( + log.info( "CheckBox answer count out of range - questionId: {}, selectedCount: {}, minSelectionCount: {}, maxSelectionCount: {}", - questionId, selectedCount, minSelectionCount, maxSelectionCount, this + questionId, selectedCount, minSelectionCount, maxSelectionCount ); } } diff --git a/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionAndProvidedQuestionMismatchException.java b/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionAndProvidedQuestionMismatchException.java index d2a981e12..97b0f77d9 100644 --- a/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionAndProvidedQuestionMismatchException.java +++ b/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionAndProvidedQuestionMismatchException.java @@ -2,15 +2,15 @@ import java.util.Collection; import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.UnexpectedRequestException; +import reviewme.global.exception.BadRequestException; @Slf4j -public class SubmittedQuestionAndProvidedQuestionMismatchException extends UnexpectedRequestException { +public class SubmittedQuestionAndProvidedQuestionMismatchException extends BadRequestException { public SubmittedQuestionAndProvidedQuestionMismatchException(Collection submittedQuestionIds, Collection providedQuestionIds) { super("제출된 응답이 제공된 질문과 매칭되지 않아요."); - log.warn( + log.info( "Submitted questions and provided questions mismatch. submittedQuestionIds: {}, providedQuestionIds: {}", submittedQuestionIds, providedQuestionIds, this ); diff --git a/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionNotFoundException.java b/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionNotFoundException.java index dc326ac32..ce037ed57 100644 --- a/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionNotFoundException.java +++ b/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionNotFoundException.java @@ -8,6 +8,6 @@ public class SubmittedQuestionNotFoundException extends NotFoundException { public SubmittedQuestionNotFoundException(long questionId) { super("제출된 질문이 존재하지 않아요."); - log.warn("Submitted question not found - questionId: {}", questionId, this); + log.info("Submitted question not found - questionId: {}", questionId, this); } } diff --git a/backend/src/main/java/reviewme/review/service/exception/TextAnswerIncludedOptionItemException.java b/backend/src/main/java/reviewme/review/service/exception/TextAnswerIncludedOptionItemException.java index 681af19a9..ba9310ee6 100644 --- a/backend/src/main/java/reviewme/review/service/exception/TextAnswerIncludedOptionItemException.java +++ b/backend/src/main/java/reviewme/review/service/exception/TextAnswerIncludedOptionItemException.java @@ -6,8 +6,8 @@ @Slf4j public class TextAnswerIncludedOptionItemException extends BadRequestException { - public TextAnswerIncludedOptionItemException() { + public TextAnswerIncludedOptionItemException(long questionId) { super("텍스트형 응답은 옵션 항목을 포함할 수 없어요."); - log.warn("Text type answer cannot have option items", this); + log.info("Text type answer cannot have option items - questionId: {}", questionId); } } diff --git a/backend/src/main/java/reviewme/review/service/exception/UnnecessaryQuestionIncludedException.java b/backend/src/main/java/reviewme/review/service/exception/UnnecessaryQuestionIncludedException.java deleted file mode 100644 index 2afeaf0b0..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/UnnecessaryQuestionIncludedException.java +++ /dev/null @@ -1,14 +0,0 @@ -package reviewme.review.service.exception; - -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.BadRequestException; - -@Slf4j -public class UnnecessaryQuestionIncludedException extends BadRequestException { - - public UnnecessaryQuestionIncludedException(List unnecessaryQuestionIds) { - super("제출해야 할 질문 이외의 질문에 응답했습니다."); - log.warn("Unnecessary question has submitted. unnecessaryQuestionIds: {}", unnecessaryQuestionIds, this); - } -} diff --git a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java new file mode 100644 index 000000000..ae444da6d --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java @@ -0,0 +1,28 @@ +package reviewme.review.service.mapper; + +import org.springframework.stereotype.Component; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; +import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; + +@Component +public class AnswerMapper { + + public TextAnswer mapToTextAnswer(ReviewAnswerRequest answerRequest) { + if (answerRequest.selectedOptionIds() != null) { + throw new TextAnswerIncludedOptionItemException(answerRequest.questionId()); + } + + return new TextAnswer(answerRequest.questionId(), answerRequest.text()); + } + + public CheckboxAnswer mapToCheckBoxAnswer(ReviewAnswerRequest answerRequest) { + if (answerRequest.text() != null) { + throw new CheckBoxAnswerIncludedTextException(answerRequest.questionId()); + } + + return new CheckboxAnswer(answerRequest.questionId(), answerRequest.selectedOptionIds()); + } +} diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java new file mode 100644 index 000000000..b4ee1f9ac --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java @@ -0,0 +1,141 @@ +package reviewme.review.service.mapper; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.response.detail.OptionGroupAnswerResponse; +import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse; +import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; +import reviewme.review.service.dto.response.detail.SectionAnswerResponse; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.template.domain.Section; +import reviewme.template.repository.SectionRepository; + +@Component +@RequiredArgsConstructor +public class ReviewDetailMapper { + + private final SectionRepository sectionRepository; + private final QuestionRepository questionRepository; + private final OptionGroupRepository optionGroupRepository; + private final OptionItemRepository optionItemRepository; + + public ReviewDetailResponse mapToReviewDetailResponse(Review review, ReviewGroup reviewGroup) { + long templateId = review.getTemplateId(); + + List
sections = sectionRepository.findAllByTemplateId(templateId); + List questions = questionRepository.findAllByTemplatedId(templateId); + List questionIds = questions.stream() + .map(Question::getId) + .toList(); + Map optionGroupsByQuestion = optionGroupRepository.findAllByQuestionIds(questionIds) + .stream() + .collect(Collectors.toMap(OptionGroup::getQuestionId, Function.identity())); + Map> optionItemsByOptionGroup = optionItemRepository.findAllByQuestionIds(questionIds) + .stream() + .collect(Collectors.groupingBy(OptionItem::getOptionGroupId)); + + List sectionResponses = sections.stream() + .map(section -> mapToSectionResponse(review, section, questions, + optionGroupsByQuestion, optionItemsByOptionGroup)) + .filter(sectionResponse -> !sectionResponse.questions().isEmpty()) + .toList(); + + return new ReviewDetailResponse( + templateId, + reviewGroup.getReviewee(), + reviewGroup.getProjectName(), + review.getCreatedDate(), + sectionResponses + ); + } + + private SectionAnswerResponse mapToSectionResponse(Review review, Section section, + List questions, + Map optionGroupsByQuestion, + Map> optionItemsByOptionGroup) { + List questionResponses = questions.stream() + .filter(question -> section.containsQuestionId(question.getId())) + .filter(question -> review.hasAnsweredQuestion(question.getId())) + .map(question -> mapToQuestionResponse( + review, question, optionGroupsByQuestion, optionItemsByOptionGroup) + ).toList(); + + return new SectionAnswerResponse( + section.getId(), + section.getHeader(), + questionResponses + ); + } + + private QuestionAnswerResponse mapToQuestionResponse(Review review, Question question, + Map optionGroupsByQuestion, + Map> optionItemsByOptionGroup) { + if (question.isSelectable()) { + return mapToCheckboxQuestionResponse(review, question, optionGroupsByQuestion, optionItemsByOptionGroup); + } else { + return mapToTextQuestionResponse(review, question); + } + } + + private QuestionAnswerResponse mapToCheckboxQuestionResponse(Review review, + Question question, + Map optionGroupsByQuestion, + Map> optionItemsByOptionGroup) { + OptionGroup optionGroup = optionGroupsByQuestion.get(question.getId()); + List optionItems = optionItemsByOptionGroup.get(optionGroup.getId()); + Set selectedOptionIds = review.getAllCheckBoxOptionIds(); + + List optionItemResponse = optionItems.stream() + .filter(optionItem -> selectedOptionIds.contains(optionItem.getId())) + .map(optionItem -> new OptionItemAnswerResponse(optionItem.getId(), optionItem.getContent(), true)) + .toList(); + + OptionGroupAnswerResponse optionGroupAnswerResponse = new OptionGroupAnswerResponse( + optionGroup.getId(), + optionGroup.getMinSelectionCount(), + optionGroup.getMaxSelectionCount(), + optionItemResponse + ); + + return new QuestionAnswerResponse( + question.getId(), + question.isRequired(), + question.getQuestionType(), + question.getContent(), + optionGroupAnswerResponse, + null + ); + } + + private QuestionAnswerResponse mapToTextQuestionResponse(Review review, + Question question) { + List textAnswers = review.getTextAnswers(); + TextAnswer textAnswer = textAnswers.stream() + .filter(answer -> answer.getQuestionId() == question.getId()) + .findFirst() + .orElseThrow(); + + return new QuestionAnswerResponse( + question.getId(), + question.isRequired(), + question.getQuestionType(), + question.getContent(), + null, + textAnswer.getContent() + ); + } +} diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java new file mode 100644 index 000000000..12eadf153 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java @@ -0,0 +1,53 @@ +package reviewme.review.service.mapper; + +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.OptionType; +import reviewme.question.repository.OptionItemRepository; +import reviewme.review.domain.Review; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.list.ReviewCategoryResponse; +import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.reviewgroup.domain.ReviewGroup; + +@Component +@RequiredArgsConstructor +public class ReviewListMapper { + + private final ReviewRepository reviewRepository; + private final OptionItemRepository optionItemRepository; + + private final ReviewPreviewGenerator reviewPreviewGenerator = new ReviewPreviewGenerator(); + + public List mapToReviewList(ReviewGroup reviewGroup, Long lastReviewId, int size) { + List categoryOptionItems = optionItemRepository.findAllByOptionType(OptionType.CATEGORY); + return reviewRepository.findByReviewGroupIdWithLimit(reviewGroup.getId(), lastReviewId, size) + .stream() + .map(review -> mapToReviewListElementResponse(review, categoryOptionItems)) + .toList(); + } + + private ReviewListElementResponse mapToReviewListElementResponse(Review review, + List categoryOptionItems) { + List categoryResponses = mapToCategoryOptionResponse(review, categoryOptionItems); + + return new ReviewListElementResponse( + review.getId(), + review.getCreatedDate(), + reviewPreviewGenerator.generatePreview(review.getTextAnswers()), + categoryResponses + ); + } + + private List mapToCategoryOptionResponse(Review review, + List categoryOptionItems) { + Set checkBoxOptionIds = review.getAllCheckBoxOptionIds(); + return categoryOptionItems.stream() + .filter(optionItem -> checkBoxOptionIds.contains(optionItem.getId())) + .map(optionItem -> new ReviewCategoryResponse(optionItem.getId(), optionItem.getContent())) + .toList(); + } +} diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java new file mode 100644 index 000000000..c5441fdf7 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java @@ -0,0 +1,96 @@ +package reviewme.review.service.mapper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.Question; +import reviewme.question.domain.QuestionType; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.template.domain.Template; +import reviewme.template.repository.TemplateRepository; +import reviewme.template.service.exception.TemplateNotFoundByReviewGroupException; + +@Component +@RequiredArgsConstructor +public class ReviewMapper { + + private final AnswerMapper answerMapper; + private final ReviewGroupRepository reviewGroupRepository; + private final QuestionRepository questionRepository; + private final TemplateRepository templateRepository; + + public Review mapToReview(ReviewRegisterRequest request) { + ReviewGroup reviewGroup = findReviewGroupByRequestCodeOrThrow(request.reviewRequestCode()); + Template template = findTemplateByReviewGroupOrThrow(reviewGroup); + + List textAnswers = new ArrayList<>(); + List checkboxAnswers = new ArrayList<>(); + addAnswersByQuestionType(request, textAnswers, checkboxAnswers); + + return new Review(template.getId(), reviewGroup.getId(), textAnswers, checkboxAnswers); + } + + private ReviewGroup findReviewGroupByRequestCodeOrThrow(String reviewRequestCode) { + return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); + } + + private Template findTemplateByReviewGroupOrThrow(ReviewGroup reviewGroup) { + return templateRepository.findById(reviewGroup.getTemplateId()) + .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( + reviewGroup.getId(), reviewGroup.getTemplateId())); + } + + private void addAnswersByQuestionType(ReviewRegisterRequest request, + List textAnswers, List checkboxAnswers) { + List questionIds = request.answers() + .stream() + .map(ReviewAnswerRequest::questionId) + .toList(); + + Map questionMap = questionRepository.findAllById(questionIds) + .stream() + .collect(Collectors.toMap(Question::getId, question -> question)); + + for (ReviewAnswerRequest answerRequest : request.answers()) { + Question question = questionMap.get(answerRequest.questionId()); + + if (question.getQuestionType() == QuestionType.TEXT) { + addIfTextAnswerExists(answerRequest, question, textAnswers); + } + + if (question.getQuestionType() == QuestionType.CHECKBOX) { + addIfCheckBoxAnswerExists(answerRequest, question, checkboxAnswers); + } + } + } + + private void addIfTextAnswerExists(ReviewAnswerRequest answerRequest, + Question question, + List textAnswers) { + if (question.isRequired() || answerRequest.hasTextAnswer()) { + TextAnswer textAnswer = answerMapper.mapToTextAnswer(answerRequest); + textAnswers.add(textAnswer); + } + } + + private void addIfCheckBoxAnswerExists(ReviewAnswerRequest answerRequest, + Question question, + List checkboxAnswers) { + if (question.isRequired() || answerRequest.hasCheckboxAnswer()) { + CheckboxAnswer checkboxAnswer = answerMapper.mapToCheckBoxAnswer(answerRequest); + checkboxAnswers.add(checkboxAnswer); + } + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewPreviewGenerator.java similarity index 74% rename from backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java rename to backend/src/main/java/reviewme/review/service/mapper/ReviewPreviewGenerator.java index d0d49781d..e4966910c 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewPreviewGenerator.java @@ -1,4 +1,4 @@ -package reviewme.review.service; +package reviewme.review.service.mapper; import java.util.List; import reviewme.review.domain.TextAnswer; @@ -6,6 +6,7 @@ public class ReviewPreviewGenerator { private static final int PREVIEW_LENGTH = 150; + private static final String ELLIPSIS = "..."; public String generatePreview(List reviewTextAnswers) { if (reviewTextAnswers.isEmpty()) { @@ -13,7 +14,7 @@ public String generatePreview(List reviewTextAnswers) { } String answer = reviewTextAnswers.get(0).getContent(); if (answer.length() > PREVIEW_LENGTH) { - return answer.substring(0, PREVIEW_LENGTH); + return answer.substring(0, PREVIEW_LENGTH) + ELLIPSIS; } return answer; } diff --git a/backend/src/main/java/reviewme/review/service/validator/CheckBoxAnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/CheckBoxAnswerValidator.java new file mode 100644 index 000000000..f906e7878 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/validator/CheckBoxAnswerValidator.java @@ -0,0 +1,73 @@ +package reviewme.review.service.validator; + +import java.util.HashSet; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckBoxAnswerSelectedOption; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.service.exception.OptionGroupNotFoundByQuestionIdException; +import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; +import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; + +@Component +@RequiredArgsConstructor +public class CheckBoxAnswerValidator { + + private final QuestionRepository questionRepository; + private final OptionGroupRepository optionGroupRepository; + private final OptionItemRepository optionItemRepository; + + public void validate(CheckboxAnswer checkboxAnswer) { + Question question = questionRepository.findById(checkboxAnswer.getQuestionId()) + .orElseThrow(() -> new SubmittedQuestionNotFoundException(checkboxAnswer.getQuestionId())); + + OptionGroup optionGroup = optionGroupRepository.findByQuestionId(question.getId()) + .orElseThrow(() -> new OptionGroupNotFoundByQuestionIdException(question.getId())); + + validateOnlyIncludingProvidedOptionItem(checkboxAnswer, optionGroup); + validateCheckedOptionItemCount(checkboxAnswer, optionGroup); + } + + private void validateOnlyIncludingProvidedOptionItem(CheckboxAnswer checkboxAnswer, OptionGroup optionGroup) { + List providedOptionItemIds = optionItemRepository.findAllByOptionGroupId(optionGroup.getId()) + .stream() + .map(OptionItem::getId) + .toList(); + List answeredOptionItemIds = extractAnsweredOptionItemIds(checkboxAnswer); + + if (!new HashSet<>(providedOptionItemIds).containsAll(answeredOptionItemIds)) { + throw new CheckBoxAnswerIncludedNotProvidedOptionItemException( + checkboxAnswer.getQuestionId(), providedOptionItemIds, answeredOptionItemIds + ); + } + } + + private void validateCheckedOptionItemCount(CheckboxAnswer checkboxAnswer, OptionGroup optionGroup) { + int answeredOptionItemCount = extractAnsweredOptionItemIds(checkboxAnswer).size(); + + if (answeredOptionItemCount < optionGroup.getMinSelectionCount() + || answeredOptionItemCount > optionGroup.getMaxSelectionCount()) { + throw new SelectedOptionItemCountOutOfRangeException( + checkboxAnswer.getQuestionId(), + answeredOptionItemCount, + optionGroup.getMinSelectionCount(), + optionGroup.getMaxSelectionCount() + ); + } + } + + private List extractAnsweredOptionItemIds(CheckboxAnswer checkboxAnswer) { + return checkboxAnswer.getSelectedOptionIds() + .stream() + .map(CheckBoxAnswerSelectedOption::getSelectedOptionId) + .toList(); + } +} diff --git a/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java b/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java new file mode 100644 index 000000000..df9ff96ab --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java @@ -0,0 +1,75 @@ +package reviewme.review.service.validator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.exception.MissingRequiredQuestionException; +import reviewme.review.service.exception.SubmittedQuestionAndProvidedQuestionMismatchException; +import reviewme.template.domain.Section; +import reviewme.template.domain.SectionQuestion; +import reviewme.template.repository.SectionRepository; + +@Component +@RequiredArgsConstructor +public class ReviewValidator { + + private final TextAnswerValidator textAnswerValidator; + private final CheckBoxAnswerValidator checkBoxAnswerValidator; + + private final SectionRepository sectionRepository; + private final QuestionRepository questionRepository; + + public void validate(Review review) { + validateAnswer(review.getTextAnswers(), review.getCheckboxAnswers()); + validateAllAnswersContainedInTemplate(review); + validateAllRequiredQuestionsAnswered(review); + } + + private void validateAnswer(List textAnswers, List checkboxAnswers) { + textAnswers.forEach(textAnswerValidator::validate); + checkboxAnswers.forEach(checkBoxAnswerValidator::validate); + } + + private void validateAllAnswersContainedInTemplate(Review review) { + Set providedQuestionIds = questionRepository.findAllQuestionIdByTemplateId(review.getTemplateId()); + Set reviewedQuestionIds = review.getAnsweredQuestionIds(); + if (!providedQuestionIds.containsAll(reviewedQuestionIds)) { + throw new SubmittedQuestionAndProvidedQuestionMismatchException(reviewedQuestionIds, providedQuestionIds); + } + } + + private void validateAllRequiredQuestionsAnswered(Review review) { + Set displayedQuestionIds = extractDisplayedQuestionIds(review); + Set requiredQuestionIds = questionRepository.findAllById(displayedQuestionIds) + .stream() + .filter(Question::isRequired) + .map(Question::getId) + .collect(Collectors.toSet()); + + Set reviewedQuestionIds = review.getAnsweredQuestionIds(); + if (!reviewedQuestionIds.containsAll(requiredQuestionIds)) { + List missingRequiredQuestionIds = new ArrayList<>(requiredQuestionIds); + missingRequiredQuestionIds.removeAll(reviewedQuestionIds); + throw new MissingRequiredQuestionException(missingRequiredQuestionIds); + } + } + + private Set extractDisplayedQuestionIds(Review review) { + Set selectedOptionIds = review.getAllCheckBoxOptionIds(); + List
sections = sectionRepository.findAllByTemplateId(review.getTemplateId()); + + return sections.stream() + .filter(section -> section.isVisibleBySelectedOptionIds(selectedOptionIds)) + .flatMap(section -> section.getQuestionIds().stream()) + .map(SectionQuestion::getQuestionId) + .collect(Collectors.toSet()); + } +} diff --git a/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java new file mode 100644 index 000000000..02e5e2abe --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java @@ -0,0 +1,38 @@ +package reviewme.review.service.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.exception.InvalidTextAnswerLengthException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; + +@Component +@RequiredArgsConstructor +public class TextAnswerValidator { + + private static final int MIN_LENGTH = 20; + private static final int MAX_LENGTH = 1_000; + + private final QuestionRepository questionRepository; + + public void validate(TextAnswer textAnswer) { + Question question = questionRepository.findById(textAnswer.getQuestionId()) + .orElseThrow(() -> new SubmittedQuestionNotFoundException(textAnswer.getQuestionId())); + + validateLength(textAnswer, question); + } + + private void validateLength(TextAnswer textAnswer, Question question) { + int answerLength = textAnswer.getContent().length(); + + if (question.isRequired() && (answerLength < MIN_LENGTH || answerLength > MAX_LENGTH)) { + throw new InvalidTextAnswerLengthException(question.getId(), answerLength, MIN_LENGTH, MAX_LENGTH); + } + + if (!question.isRequired() && answerLength > MAX_LENGTH) { + throw new InvalidTextAnswerLengthException(question.getId(), answerLength, MAX_LENGTH); + } + } +} diff --git a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java index 4bf631b98..3d1968bc8 100644 --- a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java +++ b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java @@ -1,5 +1,7 @@ package reviewme.reviewgroup.controller; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -11,7 +13,6 @@ import reviewme.reviewgroup.service.ReviewGroupLookupService; import reviewme.reviewgroup.service.ReviewGroupService; import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; -import reviewme.reviewgroup.service.dto.CheckValidAccessResponse; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; @@ -38,10 +39,13 @@ public ResponseEntity createReviewGroup( } @PostMapping("/v2/groups/check") - public ResponseEntity checkGroupAccessCode( - @RequestBody @Valid CheckValidAccessRequest request + public ResponseEntity checkGroupAccessCode( + @RequestBody @Valid CheckValidAccessRequest request, + HttpServletRequest httpRequest ) { - CheckValidAccessResponse response = reviewGroupService.checkGroupAccessCode(request); - return ResponseEntity.ok(response); + reviewGroupService.checkGroupAccessCode(request); + HttpSession session = httpRequest.getSession(); + session.setAttribute("reviewRequestCode", request.reviewRequestCode()); + return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidGroupAccessCodeFormatException.java b/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidGroupAccessCodeFormatException.java index e4425871b..416814e37 100644 --- a/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidGroupAccessCodeFormatException.java +++ b/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidGroupAccessCodeFormatException.java @@ -8,6 +8,6 @@ public class InvalidGroupAccessCodeFormatException extends BadRequestException { public InvalidGroupAccessCodeFormatException(String groupAccessCode) { super("그룹 액세스 코드 형식이 올바르지 않아요."); - log.warn("Invalid groupAccessCode format - groupAccessCode: {}", groupAccessCode); + log.info("Invalid groupAccessCode format - groupAccessCode: {}", groupAccessCode); } } diff --git a/backend/src/main/java/reviewme/reviewgroup/repository/ReviewGroupRepository.java b/backend/src/main/java/reviewme/reviewgroup/repository/ReviewGroupRepository.java index 5dd2d3ed8..37cce51d8 100644 --- a/backend/src/main/java/reviewme/reviewgroup/repository/ReviewGroupRepository.java +++ b/backend/src/main/java/reviewme/reviewgroup/repository/ReviewGroupRepository.java @@ -10,9 +10,5 @@ public interface ReviewGroupRepository extends JpaRepository Optional findByReviewRequestCode(String reviewRequestCode); - Optional findByReviewRequestCodeAndGroupAccessCode_Code( - String reviewRequestCode, String groupAccessCode - ); - boolean existsByReviewRequestCode(String reviewRequestCode); } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java index 324d56817..38bd711af 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java @@ -2,7 +2,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import org.springframework.transaction.annotation.Transactional; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; @@ -13,6 +14,7 @@ public class ReviewGroupLookupService { private final ReviewGroupRepository reviewGroupRepository; + @Transactional(readOnly = true) public ReviewGroupResponse getReviewGroupSummary(String reviewRequestCode) { ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java index fa197a3e7..c9f314b66 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java @@ -3,11 +3,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.exception.ReviewGroupUnauthorizedException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; -import reviewme.reviewgroup.service.dto.CheckValidAccessResponse; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; @@ -36,11 +36,11 @@ public ReviewGroupCreationResponse createReviewGroup(ReviewGroupCreationRequest } @Transactional(readOnly = true) - public CheckValidAccessResponse checkGroupAccessCode(CheckValidAccessRequest request) { + public void checkGroupAccessCode(CheckValidAccessRequest request) { ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(request.reviewRequestCode()) .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(request.reviewRequestCode())); - - boolean hasAccess = reviewGroup.matchesGroupAccessCode(request.groupAccessCode()); - return new CheckValidAccessResponse(hasAccess); + if (!reviewGroup.matchesGroupAccessCode(request.groupAccessCode())) { + throw new ReviewGroupUnauthorizedException(reviewGroup.getId()); + } } } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/CheckValidAccessRequest.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/CheckValidAccessRequest.java index 8a55df064..ad1d41a3c 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/dto/CheckValidAccessRequest.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/CheckValidAccessRequest.java @@ -1,13 +1,14 @@ package reviewme.reviewgroup.service.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; public record CheckValidAccessRequest( @NotBlank(message = "리뷰 요청 코드를 입력하세요.") String reviewRequestCode, - @NotBlank(message = "리뷰 확인 코드를 입력하세요.") + @NotEmpty(message = "리뷰 확인 코드를 입력하세요.") String groupAccessCode ) { } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupCreationRequest.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupCreationRequest.java index fdfe49dc5..c31a70f04 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupCreationRequest.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupCreationRequest.java @@ -1,13 +1,14 @@ package reviewme.reviewgroup.service.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; public record ReviewGroupCreationRequest( - @NotBlank(message = "리뷰이 이름을 입력해주세요.") + @NotEmpty(message = "리뷰이 이름을 입력해주세요.") String revieweeName, - @NotBlank(message = "프로젝트 이름을 입력해주세요.") + @NotEmpty(message = "프로젝트 이름을 입력해주세요.") String projectName, @NotBlank(message = "비밀번호를 입력해주세요.") diff --git a/backend/src/main/java/reviewme/template/domain/Section.java b/backend/src/main/java/reviewme/template/domain/Section.java index 696f39bc6..b9fa82b16 100644 --- a/backend/src/main/java/reviewme/template/domain/Section.java +++ b/backend/src/main/java/reviewme/template/domain/Section.java @@ -66,7 +66,8 @@ public boolean isVisibleBySelectedOptionIds(Collection selectedOptionIds) return visibleType == VisibleType.ALWAYS || selectedOptionIds.contains(onSelectedOptionId); } - public String convertHeader(String target, String replacement) { - return header.replace(target, replacement); + public boolean containsQuestionId(long questionId) { + return questionIds.stream() + .anyMatch(sectionQuestion -> sectionQuestion.hasQuestionId(questionId)); } } diff --git a/backend/src/main/java/reviewme/template/domain/SectionQuestion.java b/backend/src/main/java/reviewme/template/domain/SectionQuestion.java index eaac6e73e..36bed180c 100644 --- a/backend/src/main/java/reviewme/template/domain/SectionQuestion.java +++ b/backend/src/main/java/reviewme/template/domain/SectionQuestion.java @@ -29,4 +29,8 @@ public class SectionQuestion { public SectionQuestion(long questionId) { this.questionId = questionId; } + + public boolean hasQuestionId(long questionId) { + return this.questionId == questionId; + } } diff --git a/backend/src/main/java/reviewme/template/repository/SectionRepository.java b/backend/src/main/java/reviewme/template/repository/SectionRepository.java index 2b1babc98..bcb36c92f 100644 --- a/backend/src/main/java/reviewme/template/repository/SectionRepository.java +++ b/backend/src/main/java/reviewme/template/repository/SectionRepository.java @@ -11,8 +11,8 @@ public interface SectionRepository extends JpaRepository { @Query(value = """ SELECT s.* FROM section s - LEFT JOIN template_section ts - ON ts.section_id = s.id + JOIN template_section ts + ON s.id = ts.section_id WHERE ts.template_id = :templateId ORDER BY s.position ASC """, nativeQuery = true) diff --git a/backend/src/main/java/reviewme/template/service/TemplateService.java b/backend/src/main/java/reviewme/template/service/TemplateService.java index 905de7ce9..a49fc5160 100644 --- a/backend/src/main/java/reviewme/template/service/TemplateService.java +++ b/backend/src/main/java/reviewme/template/service/TemplateService.java @@ -3,32 +3,27 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.template.domain.Template; -import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException; -import reviewme.template.repository.TemplateRepository; import reviewme.template.service.dto.response.TemplateResponse; +import reviewme.template.service.mapper.TemplateMapper; @Service @RequiredArgsConstructor public class TemplateService { private final ReviewGroupRepository reviewGroupRepository; - private final TemplateRepository templateRepository; private final TemplateMapper templateMapper; @Transactional(readOnly = true) public TemplateResponse generateReviewForm(String reviewRequestCode) { - ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - - Template template = templateRepository.findById(reviewGroup.getTemplateId()) - .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( - reviewGroup.getId(), reviewGroup.getTemplateId() - )); + ReviewGroup reviewGroup = findReviewGroupByRequestCodeOrThrow(reviewRequestCode); + return templateMapper.mapToTemplateResponse(reviewGroup); + } - return templateMapper.mapToTemplateResponse(reviewGroup, template); + private ReviewGroup findReviewGroupByRequestCodeOrThrow(String reviewRequestCode) { + return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); } } diff --git a/backend/src/main/java/reviewme/question/domain/exception/MissingOptionItemsInOptionGroupException.java b/backend/src/main/java/reviewme/template/service/exception/MissingOptionItemsInOptionGroupException.java similarity index 85% rename from backend/src/main/java/reviewme/question/domain/exception/MissingOptionItemsInOptionGroupException.java rename to backend/src/main/java/reviewme/template/service/exception/MissingOptionItemsInOptionGroupException.java index a9a016b91..4d8ed8391 100644 --- a/backend/src/main/java/reviewme/question/domain/exception/MissingOptionItemsInOptionGroupException.java +++ b/backend/src/main/java/reviewme/template/service/exception/MissingOptionItemsInOptionGroupException.java @@ -1,4 +1,4 @@ -package reviewme.question.domain.exception; +package reviewme.template.service.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.DataInconsistencyException; @@ -8,6 +8,6 @@ public class MissingOptionItemsInOptionGroupException extends DataInconsistencyE public MissingOptionItemsInOptionGroupException(long optionGroupId) { super("서버 내부에서 문제가 발생했어요. 서버에 문의해주세요."); - log.error("OptionGroup has no OptionItems - optionGroupId: {}", optionGroupId); + log.error("OptionGroup has no OptionItems - optionGroupId: {}", optionGroupId, this); } } diff --git a/backend/src/main/java/reviewme/template/service/exception/QuestionInSectionNotFoundException.java b/backend/src/main/java/reviewme/template/service/exception/QuestionInSectionNotFoundException.java index 23b7130e3..d8dc6314b 100644 --- a/backend/src/main/java/reviewme/template/service/exception/QuestionInSectionNotFoundException.java +++ b/backend/src/main/java/reviewme/template/service/exception/QuestionInSectionNotFoundException.java @@ -7,7 +7,7 @@ public class QuestionInSectionNotFoundException extends DataInconsistencyException { public QuestionInSectionNotFoundException(long sectionId, long questionId) { - super("섹션에 질문이 존재하지 않아요."); - log.error("Question in section not found - sectionId: {}, questionId: {}", sectionId, questionId); + super("서버 내부에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."); + log.error("Question in section not found - sectionId: {}, questionId: {}", sectionId, questionId, this); } } diff --git a/backend/src/main/java/reviewme/template/domain/exception/SectionInTemplateNotFoundException.java b/backend/src/main/java/reviewme/template/service/exception/SectionInTemplateNotFoundException.java similarity index 69% rename from backend/src/main/java/reviewme/template/domain/exception/SectionInTemplateNotFoundException.java rename to backend/src/main/java/reviewme/template/service/exception/SectionInTemplateNotFoundException.java index 35d03ab73..d33430c2c 100644 --- a/backend/src/main/java/reviewme/template/domain/exception/SectionInTemplateNotFoundException.java +++ b/backend/src/main/java/reviewme/template/service/exception/SectionInTemplateNotFoundException.java @@ -1,4 +1,4 @@ -package reviewme.template.domain.exception; +package reviewme.template.service.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.DataInconsistencyException; @@ -8,6 +8,6 @@ public class SectionInTemplateNotFoundException extends DataInconsistencyExcepti public SectionInTemplateNotFoundException(long templateId, long sectionId) { super("서버 내부에서 문제가 발생했어요. 서버에 문의해주세요."); - log.warn("SectionNotFoundException has occurred - templateId: {}, sectionId: {}", templateId, sectionId); + log.error("SectionNotFoundException has occurred - templateId: {}, sectionId: {}", templateId, sectionId, this); } } diff --git a/backend/src/main/java/reviewme/template/domain/exception/TemplateNotFoundByReviewGroupException.java b/backend/src/main/java/reviewme/template/service/exception/TemplateNotFoundByReviewGroupException.java similarity index 92% rename from backend/src/main/java/reviewme/template/domain/exception/TemplateNotFoundByReviewGroupException.java rename to backend/src/main/java/reviewme/template/service/exception/TemplateNotFoundByReviewGroupException.java index 8380dc304..9e0d1e10f 100644 --- a/backend/src/main/java/reviewme/template/domain/exception/TemplateNotFoundByReviewGroupException.java +++ b/backend/src/main/java/reviewme/template/service/exception/TemplateNotFoundByReviewGroupException.java @@ -1,4 +1,4 @@ -package reviewme.template.domain.exception; +package reviewme.template.service.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.DataInconsistencyException; diff --git a/backend/src/main/java/reviewme/template/service/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java similarity index 80% rename from backend/src/main/java/reviewme/template/service/TemplateMapper.java rename to backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java index 15b5cb0a2..02b6084f1 100644 --- a/backend/src/main/java/reviewme/template/service/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java @@ -1,4 +1,4 @@ -package reviewme.template.service; +package reviewme.template.service.mapper; import java.util.List; import lombok.RequiredArgsConstructor; @@ -6,7 +6,6 @@ import reviewme.question.domain.OptionGroup; import reviewme.question.domain.OptionItem; import reviewme.question.domain.Question; -import reviewme.question.domain.exception.MissingOptionItemsInOptionGroupException; import reviewme.question.repository.OptionGroupRepository; import reviewme.question.repository.OptionItemRepository; import reviewme.question.repository.QuestionRepository; @@ -15,8 +14,11 @@ import reviewme.template.domain.SectionQuestion; import reviewme.template.domain.Template; import reviewme.template.domain.TemplateSection; -import reviewme.template.domain.exception.SectionInTemplateNotFoundException; +import reviewme.template.service.exception.MissingOptionItemsInOptionGroupException; +import reviewme.template.service.exception.SectionInTemplateNotFoundException; +import reviewme.template.service.exception.TemplateNotFoundByReviewGroupException; import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; import reviewme.template.service.dto.response.OptionGroupResponse; import reviewme.template.service.dto.response.OptionItemResponse; import reviewme.template.service.dto.response.QuestionResponse; @@ -28,15 +30,23 @@ @RequiredArgsConstructor public class TemplateMapper { + public static final String REVIEWEE_NAME_PLACEHOLDER = "{revieweeName}"; + + private final TemplateRepository templateRepository; private final SectionRepository sectionRepository; private final QuestionRepository questionRepository; private final OptionGroupRepository optionGroupRepository; private final OptionItemRepository optionItemRepository; - public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup, Template template) { + public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup) { + Template template = templateRepository.findById(reviewGroup.getTemplateId()) + .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( + reviewGroup.getId(), reviewGroup.getTemplateId() + )); + List sectionResponses = template.getSectionIds() .stream() - .map(templateSection -> mapToSectionResponse(templateSection, reviewGroup)) + .map(this::mapToSectionResponse) .toList(); return new TemplateResponse( @@ -47,14 +57,14 @@ public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup, Template ); } - private SectionResponse mapToSectionResponse(TemplateSection templateSection, ReviewGroup reviewGroup) { + private SectionResponse mapToSectionResponse(TemplateSection templateSection) { Section section = sectionRepository.findById(templateSection.getSectionId()) .orElseThrow(() -> new SectionInTemplateNotFoundException( templateSection.getTemplateId(), templateSection.getSectionId()) ); List questionResponses = section.getQuestionIds() .stream() - .map(sectionQuestion -> mapToQuestionResponse(sectionQuestion, reviewGroup)) + .map(this::mapToQuestionResponse) .toList(); return new SectionResponse( @@ -62,12 +72,12 @@ private SectionResponse mapToSectionResponse(TemplateSection templateSection, Re section.getSectionName(), section.getVisibleType().name(), section.getOnSelectedOptionId(), - section.convertHeader("{revieweeName}", reviewGroup.getReviewee()), + section.getHeader(), questionResponses ); } - private QuestionResponse mapToQuestionResponse(SectionQuestion sectionQuestion, ReviewGroup reviewGroup) { + private QuestionResponse mapToQuestionResponse(SectionQuestion sectionQuestion) { Question question = questionRepository.findById(sectionQuestion.getQuestionId()) .orElseThrow(() -> new QuestionInSectionNotFoundException( sectionQuestion.getSectionId(), sectionQuestion.getQuestionId()) @@ -79,11 +89,11 @@ private QuestionResponse mapToQuestionResponse(SectionQuestion sectionQuestion, return new QuestionResponse( question.getId(), question.isRequired(), - question.convertContent("{revieweeName}", reviewGroup.getReviewee()), + question.getContent(), question.getQuestionType().name(), optionGroupResponse, question.hasGuideline(), - question.convertGuideLine("{revieweeName}", reviewGroup.getReviewee()) + question.getGuideline() ); } diff --git a/backend/src/main/resources/api-docs.yml b/backend/src/main/resources/api-docs.yml deleted file mode 100644 index d267ece30..000000000 --- a/backend/src/main/resources/api-docs.yml +++ /dev/null @@ -1,11 +0,0 @@ -docs: - info: - title: "리뷰미 API" - description: "이 문서는 리뷰미 API 구현 방법을 소개합니다." - version: "0.0.1" - -springdoc: - swagger-ui: - path: /api-docs - operations-sorter: alpha - tags-sorter: alpha diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 3cc43c9a7..4181d5513 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -4,7 +4,6 @@ spring: config: import: - - classpath:api-docs.yml - classpath:logback.yml datasource: @@ -19,3 +18,19 @@ spring: show-sql: true hibernate: ddl-auto: update + open-in-view: false + flyway: + enabled: false + +server: + servlet: + session: + cookie: + same-site: strict + http-only: true + secure: true + +cors: + allowed-origins: + - http://localhost + - https://localhost diff --git a/backend/src/main/resources/db/migration/V1__add_index.sql b/backend/src/main/resources/db/migration/V1__add_index.sql new file mode 100644 index 000000000..61c2f174f --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__add_index.sql @@ -0,0 +1,3 @@ +-- 변경 사항 설명: https://github.com/woowacourse-teams/2024-review-me/issues/682 +CREATE INDEX review_idx_review_group_id ON review (review_group_id); +CREATE INDEX review_group_idx_review_request_code ON review_group (review_request_code); diff --git a/backend/src/main/resources/schema-doc.sql b/backend/src/main/resources/schema-doc.sql new file mode 100644 index 000000000..e23fcff79 --- /dev/null +++ b/backend/src/main/resources/schema-doc.sql @@ -0,0 +1,112 @@ +CREATE TABLE review_group +( + id BIGINT AUTO_INCREMENT, + group_access_code VARCHAR(255) NOT NULL, + project_name VARCHAR(255) NOT NULL, + review_request_code VARCHAR(255) NOT NULL, + reviewee VARCHAR(255) NOT NULL, + template_id BIGINT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE review +( + id BIGINT AUTO_INCREMENT, + created_at TIMESTAMP(6) NOT NULL, + review_group_id BIGINT NOT NULL, + template_id BIGINT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE template +( + id BIGINT AUTO_INCREMENT, + PRIMARY KEY (id) +); + +CREATE TABLE section +( + id BIGINT AUTO_INCREMENT, + header VARCHAR(1000) NOT NULL, + on_selected_option_id BIGINT, + position INTEGER NOT NULL, + section_name VARCHAR(255) NOT NULL, + visible_type VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE template_section +( + id BIGINT AUTO_INCREMENT, + section_id BIGINT NOT NULL, + template_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (template_id) REFERENCES template (id) +); + +CREATE TABLE question +( + id BIGINT AUTO_INCREMENT, + content VARCHAR(1000) NOT NULL, + guideline VARCHAR(1000), + position INTEGER NOT NULL, + question_type VARCHAR(255) NOT NULL, + required BOOLEAN NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE section_question +( + id BIGINT AUTO_INCREMENT, + question_id BIGINT NOT NULL, + section_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (section_id) REFERENCES section (id) +); + +CREATE TABLE text_answer +( + id BIGINT AUTO_INCREMENT, + content VARCHAR(5000) NOT NULL, + question_id BIGINT NOT NULL, + review_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (review_id) REFERENCES review (id) +); + +CREATE TABLE checkbox_answer +( + id BIGINT AUTO_INCREMENT, + question_id BIGINT NOT NULL, + review_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (review_id) REFERENCES review (id) +); + +CREATE TABLE checkbox_answer_selected_option +( + id BIGINT AUTO_INCREMENT, + checkbox_answer_id BIGINT NOT NULL, + selected_option_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (checkbox_answer_id) REFERENCES checkbox_answer (id) +); + +CREATE TABLE option_group +( + id BIGINT AUTO_INCREMENT, + max_selection_count INTEGER NOT NULL, + min_selection_count INTEGER NOT NULL, + question_id BIGINT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE option_item +( + id BIGINT AUTO_INCREMENT, + content VARCHAR(255) NOT NULL, + option_group_id BIGINT NOT NULL, + option_type VARCHAR(255) NOT NULL, + position INTEGER NOT NULL, + PRIMARY KEY (id) +); diff --git a/backend/src/main/resources/secret b/backend/src/main/resources/secret deleted file mode 160000 index 9e15707bb..000000000 --- a/backend/src/main/resources/secret +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9e15707bb4d91435d6c460b09343d2fb6e819fe1 diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index a0bf49b5d..e2b884706 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -7,6 +7,11 @@ import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification; +import jakarta.servlet.Filter; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import org.apache.http.HttpHeaders; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,9 +27,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import reviewme.review.controller.ReviewController; -import reviewme.review.service.CreateReviewService; import reviewme.review.service.ReviewDetailLookupService; -import reviewme.review.service.ReviewService; +import reviewme.review.service.ReviewListLookupService; +import reviewme.review.service.ReviewRegisterService; import reviewme.reviewgroup.controller.ReviewGroupController; import reviewme.reviewgroup.service.ReviewGroupLookupService; import reviewme.reviewgroup.service.ReviewGroupService; @@ -42,7 +47,7 @@ public abstract class ApiTest { private MockMvcRequestSpecification spec; @MockBean - protected ReviewService reviewService; + protected ReviewListLookupService reviewListLookupService; @MockBean protected ReviewGroupService reviewGroupService; @@ -51,7 +56,7 @@ public abstract class ApiTest { protected TemplateService templateService; @MockBean - protected CreateReviewService createReviewService; + protected ReviewRegisterService reviewRegisterService; @MockBean protected ReviewDetailLookupService reviewDetailLookupService; @@ -59,6 +64,18 @@ public abstract class ApiTest { @MockBean protected ReviewGroupLookupService reviewGroupLookupService; + Filter sessionCookieFilter = (request, response, chain) -> { + chain.doFilter(request, response); + HttpSession session = ((HttpServletRequest) request).getSession(false); + if (session != null) { + Cookie sessionCookie = new Cookie("JSESSIONID", session.getId()); + sessionCookie.setHttpOnly(true); + sessionCookie.setPath("/"); + sessionCookie.setSecure(true); + ((HttpServletResponse) response).addCookie(sessionCookie); + } + }; + @BeforeEach void setUpRestDocs(WebApplicationContext context, RestDocumentationContextProvider provider) { UriModifyingOperationPreprocessor uriModifier = modifyUris() @@ -82,9 +99,11 @@ void setUpRestDocs(WebApplicationContext context, RestDocumentationContextProvid MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context) .apply(configurer) + .addFilters(sessionCookieFilter) .build(); spec = RestAssuredMockMvc.given() + .sessionAttr("reviewRequestCode", "12341234") .mockMvc(mockMvc); } diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index 1d276aae1..46a6dace6 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -1,31 +1,32 @@ package reviewme.api; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; -import org.springframework.restdocs.headers.HeaderDescriptor; +import org.springframework.restdocs.cookies.CookieDescriptor; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.dto.response.list.ReceivedReviewCategoryResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewResponse; +import reviewme.review.service.dto.request.ReviewRegisterRequest; import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; -import reviewme.review.service.exception.ReviewGroupNotFoundByCodesException; +import reviewme.review.service.dto.response.list.ReviewCategoryResponse; +import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; class ReviewApiTest extends ApiTest { @@ -47,7 +48,7 @@ class ReviewApiTest extends ApiTest { @Test void 리뷰를_등록한다() { - BDDMockito.given(createReviewService.createReview(any(CreateReviewRequest.class))) + BDDMockito.given(reviewRegisterService.registerReview(any(ReviewRegisterRequest.class))) .willReturn(1L); FieldDescriptor[] requestFieldDescriptors = { @@ -74,7 +75,7 @@ class ReviewApiTest extends ApiTest { @Test void 리뷰_그룹_코드가_올바르지_않은_경우_예외가_발생한다() { - BDDMockito.given(createReviewService.createReview(any(CreateReviewRequest.class))) + BDDMockito.given(reviewRegisterService.registerReview(any(ReviewRegisterRequest.class))) .willThrow(new ReviewGroupNotFoundByReviewRequestCodeException(anyString())); FieldDescriptor[] requestFieldDescriptors = { @@ -100,18 +101,18 @@ class ReviewApiTest extends ApiTest { } @Test - void 자신이_받은_리뷰_한_개를_조회한다() { - BDDMockito.given(reviewDetailLookupService.getReviewDetail(anyLong(), anyString(), anyString())) + void 세션으로_자신이_받은_리뷰_한_개를_조회한다() { + BDDMockito.given(reviewDetailLookupService.getReviewDetail(anyLong(), anyString())) .willReturn(TemplateFixture.templateAnswerResponse()); - HeaderDescriptor[] requestHeaderDescriptors = { - headerWithName("groupAccessCode").description("그룹 접근 코드") - }; - ParameterDescriptor[] requestPathDescriptors = { parameterWithName("id").description("리뷰 ID") }; + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 쿠키") + }; + FieldDescriptor[] responseFieldDescriptors = { fieldWithPath("createdAt").description("리뷰 작성 날짜"), fieldWithPath("formId").description("폼 ID"), @@ -141,73 +142,49 @@ class ReviewApiTest extends ApiTest { }; RestDocumentationResultHandler handler = document( - "review-detail", - requestHeaders(requestHeaderDescriptors), + "review-detail-with-session", + requestCookies(cookieDescriptors), pathParameters(requestPathDescriptors), responseFields(responseFieldDescriptors) ); givenWithSpec().log().all() .pathParam("id", "1") - .queryParam("reviewRequestCode", "00001234") - .header("groupAccessCode", "abc12344") + .cookie("JSESSIONID", "AVEBNKLCL13TNVZ") .when().get("/v2/reviews/{id}") .then().log().all() .apply(handler) .statusCode(200); } - @Test - void 리뷰_단건_조회시_접근_코드가_올바르지_않은_경우_예외를_발생한다() { - long reviewId = 1L; - String reviewRequestCode = "00001234"; - String groupAccessCode = "43214321"; - BDDMockito.given(reviewDetailLookupService.getReviewDetail(reviewId, reviewRequestCode, groupAccessCode)) - .willThrow(new ReviewGroupNotFoundByCodesException(reviewRequestCode, groupAccessCode)); - - HeaderDescriptor[] requestHeaderDescriptors = { - headerWithName("groupAccessCode").description("그룹 접근 코드") - }; - - ParameterDescriptor[] requestPathDescriptors = { - parameterWithName("id").description("리뷰 ID") - }; - - RestDocumentationResultHandler handler = document( - "review-detail-invalid-group-access-code", - requestHeaders(requestHeaderDescriptors), - pathParameters(requestPathDescriptors) - ); - - givenWithSpec().log().all() - .pathParam("id", reviewId) - .queryParam("reviewRequestCode", reviewRequestCode) - .header("groupAccessCode", groupAccessCode) - .when().get("/v2/reviews/{id}") - .then().log().all() - .apply(handler) - .statusCode(400); - } - @Test void 자신이_받은_리뷰_목록을_조회한다() { - List receivedReviews = List.of( - new ReceivedReviewResponse(1L, LocalDate.of(2024, 8, 1), "(리뷰 미리보기 1)", - List.of(new ReceivedReviewCategoryResponse(1L, "카테고리 1"))), - new ReceivedReviewResponse(2L, LocalDate.of(2024, 8, 2), "(리뷰 미리보기 2)", - List.of(new ReceivedReviewCategoryResponse(2L, "카테고리 2"))) + List receivedReviews = List.of( + new ReviewListElementResponse(1L, LocalDate.of(2024, 8, 1), "(리뷰 미리보기 1)", + List.of(new ReviewCategoryResponse(1L, "카테고리 1"))), + new ReviewListElementResponse(2L, LocalDate.of(2024, 8, 2), "(리뷰 미리보기 2)", + List.of(new ReviewCategoryResponse(2L, "카테고리 2"))) ); - ReceivedReviewsResponse response = new ReceivedReviewsResponse("아루", "리뷰미", receivedReviews); - BDDMockito.given(reviewService.findReceivedReviews(anyString(), anyString())) + ReceivedReviewsResponse response = new ReceivedReviewsResponse( + "아루3", "리뷰미", 1L, true, receivedReviews); + BDDMockito.given(reviewListLookupService.getReceivedReviews(anyLong(), anyInt(), anyString())) .willReturn(response); - HeaderDescriptor[] requestHeaderDescriptors = { - headerWithName("groupAccessCode").description("그룹 접근 코드") + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 쿠키") + }; + + ParameterDescriptor[] queryParameter = { + parameterWithName("reviewRequestCode").description("리뷰 요청 코드"), + parameterWithName("lastReviewId").description("페이지의 마지막 리뷰 ID - 기본으로 최신순 첫번째 페이지 응답"), + parameterWithName("size").description("페이지의 크기 - 기본으로 10개씩 응답") }; FieldDescriptor[] responseFieldDescriptors = { fieldWithPath("revieweeName").description("리뷰이 이름"), fieldWithPath("projectName").description("프로젝트 이름"), + fieldWithPath("lastReviewId").description("페이지의 마지막 리뷰 ID"), + fieldWithPath("isLastPage").description("마지막 페이지 여부"), fieldWithPath("reviews[]").description("리뷰 목록"), fieldWithPath("reviews[].reviewId").description("리뷰 ID"), @@ -220,42 +197,20 @@ class ReviewApiTest extends ApiTest { }; RestDocumentationResultHandler handler = document( - "received-reviews", - requestHeaders(requestHeaderDescriptors), + "received-review-list-with-pagination", + requestCookies(cookieDescriptors), + queryParameters(queryParameter), responseFields(responseFieldDescriptors) ); givenWithSpec().log().all() - .queryParam("reviewRequestCode", "asdfasdf") - .header("groupAccessCode", "qwerqwer") + .cookie("JSESSIONID", "ASVNE1VAKDNV4") + .queryParam("reviewRequestCode", "hello!!") + .queryParam("lastReviewId", "2") + .queryParam("size", "5") .when().get("/v2/reviews") .then().log().all() .apply(handler) .statusCode(200); } - - @Test - void 자신이_받은_리뷰_조회시_접근_코드가_올바르지_않은_경우_예외를_발생한다() { - String reviewRequestCode = "43214321"; - String groupAccessCode = "00001234"; - BDDMockito.given(reviewService.findReceivedReviews(reviewRequestCode, groupAccessCode)) - .willThrow(new ReviewGroupNotFoundByCodesException(reviewRequestCode, groupAccessCode)); - - HeaderDescriptor[] requestHeaderDescriptors = { - headerWithName("groupAccessCode").description("그룹 접근 코드") - }; - - RestDocumentationResultHandler handler = document( - "received-reviews-invalid-group-access-code", - requestHeaders(requestHeaderDescriptors) - ); - - givenWithSpec().log().all() - .header("groupAccessCode", groupAccessCode) - .queryParam("reviewRequestCode", reviewRequestCode) - .when().get("/v2/reviews") - .then().log().all() - .apply(handler) - .statusCode(400); - } } diff --git a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java index 3e3df8e2a..e87cb8b5c 100644 --- a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java @@ -3,6 +3,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; @@ -12,11 +14,10 @@ import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; +import org.springframework.restdocs.cookies.CookieDescriptor; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; -import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; -import reviewme.reviewgroup.service.dto.CheckValidAccessResponse; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; @@ -90,9 +91,6 @@ class ReviewGroupApiTest extends ApiTest { @Test void 리뷰_그룹_코드와_액세스_코드로_일치_여부를_판단한다() { - BDDMockito.given(reviewGroupService.checkGroupAccessCode(any(CheckValidAccessRequest.class))) - .willReturn(new CheckValidAccessResponse(true)); - String request = """ { "reviewRequestCode": "ABCD1234", @@ -105,14 +103,14 @@ class ReviewGroupApiTest extends ApiTest { fieldWithPath("groupAccessCode").description("그룹 접근 코드 (비밀번호)") }; - FieldDescriptor[] responseFieldDescriptors = { - fieldWithPath("hasAccess").description("코드 일치 여부 (비밀번호 일치)") + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") }; RestDocumentationResultHandler handler = document( "review-group-check-access", requestFields(requestFieldDescriptors), - responseFields(responseFieldDescriptors) + responseCookies(cookieDescriptors) ); givenWithSpec().log().all() @@ -120,6 +118,7 @@ class ReviewGroupApiTest extends ApiTest { .when().post("/v2/groups/check") .then().log().all() .apply(handler) - .statusCode(200); + .cookie("JSESSIONID") + .statusCode(204); } } diff --git a/backend/src/test/java/reviewme/api/TemplateApiTest.java b/backend/src/test/java/reviewme/api/TemplateApiTest.java index 557e59e8a..510a894e0 100644 --- a/backend/src/test/java/reviewme/api/TemplateApiTest.java +++ b/backend/src/test/java/reviewme/api/TemplateApiTest.java @@ -12,7 +12,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; class TemplateApiTest extends ApiTest { diff --git a/backend/src/test/java/reviewme/api/TemplateFixture.java b/backend/src/test/java/reviewme/api/TemplateFixture.java index 1cee386d0..a4a941985 100644 --- a/backend/src/test/java/reviewme/api/TemplateFixture.java +++ b/backend/src/test/java/reviewme/api/TemplateFixture.java @@ -7,7 +7,7 @@ import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse; import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; import reviewme.review.service.dto.response.detail.SectionAnswerResponse; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.template.domain.VisibleType; import reviewme.template.service.dto.response.OptionGroupResponse; import reviewme.template.service.dto.response.OptionItemResponse; @@ -72,7 +72,7 @@ public static TemplateResponse templateResponse() { return new TemplateResponse(1, "아루", "리뷰미", List.of(firstSection, secondSection)); } - public static TemplateAnswerResponse templateAnswerResponse() { + public static ReviewDetailResponse templateAnswerResponse() { // Section 1 List firstOptionAnswers = List.of( new OptionItemAnswerResponse(1, "커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)", true), @@ -102,7 +102,7 @@ public static TemplateAnswerResponse templateAnswerResponse() { 2, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", List.of(secondQuestionAnswer) ); - return new TemplateAnswerResponse( + return new ReviewDetailResponse( 1, "아루", "리뷰미", LocalDate.of(2024, 8, 1), List.of(firstSectionAnswer, secondSectionAnswer) ); } diff --git a/backend/src/test/java/reviewme/config/CorsConfigTest.java b/backend/src/test/java/reviewme/config/CorsConfigTest.java new file mode 100644 index 000000000..c2ce590c1 --- /dev/null +++ b/backend/src/test/java/reviewme/config/CorsConfigTest.java @@ -0,0 +1,32 @@ +package reviewme.config; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.WebApplicationContext; + +@WebMvcTest(controllers = CorsConfigTest.TestController.class) +abstract class CorsConfigTest { + + @Autowired + private WebApplicationContext context; + + protected MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .build(); + } + + @Controller + static class TestController { + @RequestMapping("/test") + public void test() { + } + } +} diff --git a/backend/src/test/java/reviewme/config/ExternalCorsConfigTest.java b/backend/src/test/java/reviewme/config/ExternalCorsConfigTest.java new file mode 100644 index 000000000..81c04c76e --- /dev/null +++ b/backend/src/test/java/reviewme/config/ExternalCorsConfigTest.java @@ -0,0 +1,39 @@ +package reviewme.config; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("dev") +class ExternalCorsConfigTest extends CorsConfigTest { + + @Autowired + private CorsProperties corsProperties; + + @Test + void 로컬이_아닌_프로파일의_외부_요청은_허락하지_않는다() throws Exception { + String origin = "http://denied-domain.com"; + mockMvc.perform(options("/test") + .header(HttpHeaders.ORIGIN, origin) + .header(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "GET") + ).andDo(print()) + .andExpect(status().isForbidden()); + } + + @Test + void 로컬이_아닌_프로파일의_리뷰미_도메인_요청은_허락한다() throws Exception { + String origin = corsProperties.allowedOrigins().get(0); + mockMvc.perform(options("/test") + .header(HttpHeaders.ORIGIN, origin) + .header(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "GET") + ).andDo(print()) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin)) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/reviewme/config/LocalCorsConfigTest.java b/backend/src/test/java/reviewme/config/LocalCorsConfigTest.java new file mode 100644 index 000000000..f04698d3f --- /dev/null +++ b/backend/src/test/java/reviewme/config/LocalCorsConfigTest.java @@ -0,0 +1,26 @@ +package reviewme.config; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("local") +class LocalCorsConfigTest extends CorsConfigTest { + + @Test + void 로컬_프로파일에서는_외부_접근에_대해서도_허용한다() throws Exception { + String domain = "http://test-domain.com"; + mockMvc.perform(options("/test") + .header(HttpHeaders.ORIGIN, domain) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") + ).andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, domain)) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")); + } +} diff --git a/backend/src/test/java/reviewme/fixture/OptionGroupFixture.java b/backend/src/test/java/reviewme/fixture/OptionGroupFixture.java new file mode 100644 index 000000000..259a3ebcf --- /dev/null +++ b/backend/src/test/java/reviewme/fixture/OptionGroupFixture.java @@ -0,0 +1,10 @@ +package reviewme.fixture; + +import reviewme.question.domain.OptionGroup; + +public class OptionGroupFixture { + + public static OptionGroup 선택지_그룹(long questionId) { + return new OptionGroup(questionId, 1, 2); + } +} diff --git a/backend/src/test/java/reviewme/fixture/OptionItemFixture.java b/backend/src/test/java/reviewme/fixture/OptionItemFixture.java new file mode 100644 index 000000000..3b7e50725 --- /dev/null +++ b/backend/src/test/java/reviewme/fixture/OptionItemFixture.java @@ -0,0 +1,15 @@ +package reviewme.fixture; + +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.OptionType; + +public class OptionItemFixture { + + public static OptionItem 선택지(long optionGroupId) { + return 선택지(optionGroupId, 1); + } + + public static OptionItem 선택지(long optionGroupId, int position) { + return new OptionItem("선택지 본문", optionGroupId, position, OptionType.CATEGORY); + } +} diff --git a/backend/src/test/java/reviewme/fixture/QuestionFixture.java b/backend/src/test/java/reviewme/fixture/QuestionFixture.java new file mode 100644 index 000000000..f4ce28b88 --- /dev/null +++ b/backend/src/test/java/reviewme/fixture/QuestionFixture.java @@ -0,0 +1,39 @@ +package reviewme.fixture; + +import reviewme.question.domain.Question; +import reviewme.question.domain.QuestionType; + +public class QuestionFixture { + + public static Question 선택형_필수_질문() { + return 선택형_필수_질문(1); + } + + public static Question 선택형_필수_질문(int position) { + return new Question(true, QuestionType.CHECKBOX, "본문", null, position); + } + + public static Question 선택형_옵션_질문() { + return 선택형_옵션_질문(1); + } + + public static Question 선택형_옵션_질문(int position) { + return new Question(false, QuestionType.CHECKBOX, "본문", null, position); + } + + public static Question 서술형_필수_질문() { + return 서술형_필수_질문(1); + } + + public static Question 서술형_필수_질문(int position) { + return new Question(true, QuestionType.TEXT, "본문", null, position); + } + + public static Question 서술형_옵션_질문() { + return 서술형_옵션_질문(1); + } + + public static Question 서술형_옵션_질문(int position) { + return new Question(false, QuestionType.TEXT, "본문", null, position); + } +} diff --git a/backend/src/test/java/reviewme/fixture/ReviewGroupFixture.java b/backend/src/test/java/reviewme/fixture/ReviewGroupFixture.java new file mode 100644 index 000000000..5ae84fe0d --- /dev/null +++ b/backend/src/test/java/reviewme/fixture/ReviewGroupFixture.java @@ -0,0 +1,14 @@ +package reviewme.fixture; + +import reviewme.reviewgroup.domain.ReviewGroup; + +public class ReviewGroupFixture { + + public static ReviewGroup 리뷰_그룹() { + return 리뷰_그룹("reviewRequestCode", "groupAccessCode"); + } + + public static ReviewGroup 리뷰_그룹(String reviewRequestCode, String groupAccessCode) { + return new ReviewGroup("revieweeName", "projectName", reviewRequestCode, groupAccessCode); + } +} diff --git a/backend/src/test/java/reviewme/fixture/SectionFixture.java b/backend/src/test/java/reviewme/fixture/SectionFixture.java new file mode 100644 index 000000000..ce8d5d906 --- /dev/null +++ b/backend/src/test/java/reviewme/fixture/SectionFixture.java @@ -0,0 +1,24 @@ +package reviewme.fixture; + +import java.util.List; +import reviewme.template.domain.Section; +import reviewme.template.domain.VisibleType; + +public class SectionFixture { + + public static Section 항상_보이는_섹션(List questionIds) { + return 항상_보이는_섹션(questionIds, 1); + } + + public static Section 항상_보이는_섹션(List questionIds, int position) { + return new Section(VisibleType.ALWAYS, questionIds, null, "섹션명", "머릿말", position); + } + + public static Section 조건부로_보이는_섹션(List questionIds, long onSelectedOptionId) { + return 조건부로_보이는_섹션(questionIds, onSelectedOptionId, 1); + } + + public static Section 조건부로_보이는_섹션(List questionIds, long onSelectedOptionId, int position) { + return new Section(VisibleType.CONDITIONAL, questionIds, onSelectedOptionId, "섹션명", "머릿말", position); + } +} diff --git a/backend/src/test/java/reviewme/fixture/TemplateFixture.java b/backend/src/test/java/reviewme/fixture/TemplateFixture.java new file mode 100644 index 000000000..44826daee --- /dev/null +++ b/backend/src/test/java/reviewme/fixture/TemplateFixture.java @@ -0,0 +1,11 @@ +package reviewme.fixture; + +import java.util.List; +import reviewme.template.domain.Template; + +public class TemplateFixture { + + public static Template 템플릿(List sectionIds) { + return new Template(sectionIds); + } +} diff --git a/backend/src/test/java/reviewme/question/repository/OptionGroupRepositoryTest.java b/backend/src/test/java/reviewme/question/repository/OptionGroupRepositoryTest.java new file mode 100644 index 000000000..1bc3ea107 --- /dev/null +++ b/backend/src/test/java/reviewme/question/repository/OptionGroupRepositoryTest.java @@ -0,0 +1,46 @@ +package reviewme.question.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.Question; + +@DataJpaTest +class OptionGroupRepositoryTest { + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Test + void 질문_아이디_그룹에_포함되는_모든_옵션_그룹을_불러온다() { + // given + Question question1 = questionRepository.save(선택형_필수_질문()); + Question question2 = questionRepository.save(선택형_필수_질문()); + Question question3 = questionRepository.save(선택형_필수_질문()); + + OptionGroup optionGroup1 = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionGroup optionGroup2 = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionGroup optionGroup3 = optionGroupRepository.save(선택지_그룹(question2.getId())); + OptionGroup optionGroup4 = optionGroupRepository.save(선택지_그룹(question2.getId())); + OptionGroup optionGroup5 = optionGroupRepository.save(선택지_그룹(question3.getId())); + OptionGroup optionGroup6 = optionGroupRepository.save(선택지_그룹(question3.getId())); + + + // when + List actual = optionGroupRepository.findAllByQuestionIds( + List.of(question1.getId(), question2.getId())); + + // then + assertThat(actual).containsExactlyInAnyOrder(optionGroup1, optionGroup2, optionGroup3, optionGroup4); + } +} diff --git a/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java b/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java index 046eeeea9..5aebbf06b 100644 --- a/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java +++ b/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java @@ -1,9 +1,11 @@ package reviewme.question.repository; import static org.assertj.core.api.Assertions.assertThat; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; @@ -11,10 +13,6 @@ import reviewme.question.domain.OptionItem; import reviewme.question.domain.OptionType; import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.review.domain.CheckboxAnswer; -import reviewme.review.domain.Review; -import reviewme.review.repository.ReviewRepository; @DataJpaTest class OptionItemRepositoryTest { @@ -23,62 +21,47 @@ class OptionItemRepositoryTest { private OptionItemRepository optionItemRepository; @Autowired - private ReviewRepository reviewRepository; + private OptionGroupRepository optionGroupRepository; @Autowired private QuestionRepository questionRepository; - @Autowired - private OptionGroupRepository optionGroupRepository; - @Test - void 리뷰_아이디로_선택한_옵션_아이템_아이디를_불러온다() { + void 옵션_타입에_해당하는_모든_옵션_아이템을_불러온다() { // given - long optionId1 = optionItemRepository.save(new OptionItem("1", 0, 1, OptionType.KEYWORD)).getId(); - long optionId2 = optionItemRepository.save(new OptionItem("2", 0, 1, OptionType.KEYWORD)).getId(); - long optionId3 = optionItemRepository.save(new OptionItem("3", 0, 1, OptionType.KEYWORD)).getId(); - long optionId4 = optionItemRepository.save(new OptionItem("4", 0, 1, OptionType.KEYWORD)).getId(); - optionItemRepository.save(new OptionItem("5", 0, 1, OptionType.KEYWORD)); + Question question = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question.getId())); - List checkboxAnswers = List.of( - new CheckboxAnswer(1, List.of(optionId1, optionId2)), - new CheckboxAnswer(2, List.of(optionId3, optionId4)) - ); - Review review = reviewRepository.save(new Review(0, 0, List.of(), checkboxAnswers)); + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup.getId())); // when - Set actual = optionItemRepository.findSelectedOptionItemIdsByReviewId(review.getId()); + List actual = optionItemRepository.findAllByOptionType(OptionType.CATEGORY); // then - assertThat(actual).containsExactlyInAnyOrder(optionId1, optionId2, optionId3, optionId4); + assertThat(actual).containsExactlyInAnyOrder(optionItem1, optionItem2); } @Test - void 리뷰_아이디와_질문_아이디로_선택한_옵션_아이템을_순서대로_불러온다() { + void 질문_아이디_그룹에_포함되는_모든_옵션_아이템을_불러온다() { // given - Question question1 = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "질문1", null, 1)); - Question question2 = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "질문2", null, 2)); - - long optionGroupId = optionGroupRepository.save(new OptionGroup(question1.getId(), 1, 3)).getId(); - long optionId1 = optionItemRepository.save(new OptionItem("1", optionGroupId, 3, OptionType.KEYWORD)).getId(); - long optionId2 = optionItemRepository.save(new OptionItem("2", optionGroupId, 2, OptionType.KEYWORD)).getId(); - long optionId3 = optionItemRepository.save(new OptionItem("3", optionGroupId, 1, OptionType.KEYWORD)).getId(); - long optionId4 = optionItemRepository.save(new OptionItem("4", optionGroupId, 1, OptionType.KEYWORD)).getId(); - long optionId5 = optionItemRepository.save(new OptionItem("5", optionGroupId, 1, OptionType.KEYWORD)).getId(); - - List checkboxAnswers = List.of( - new CheckboxAnswer(question1.getId(), List.of(optionId1, optionId3)), - new CheckboxAnswer(question2.getId(), List.of(optionId4)) - ); - - Review review = reviewRepository.save(new Review(0, 0, List.of(), checkboxAnswers)); + Question question1 = questionRepository.save(선택형_필수_질문()); + Question question2 = questionRepository.save(선택형_필수_질문()); + Question question3 = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup1 = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionGroup optionGroup2 = optionGroupRepository.save(선택지_그룹(question2.getId())); + OptionGroup optionGroup3 = optionGroupRepository.save(선택지_그룹(question3.getId())); + + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup1.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup1.getId())); + OptionItem optionItem3 = optionItemRepository.save(선택지(optionGroup2.getId())); + OptionItem optionItem4 = optionItemRepository.save(선택지(optionGroup3.getId())); // when - List actual = optionItemRepository.findSelectedOptionItemsByReviewIdAndQuestionId( - review.getId(), question1.getId() - ); + List actual = optionItemRepository.findAllByQuestionIds( + List.of(question1.getId(), question2.getId())); // then - assertThat(actual).extracting(OptionItem::getId).containsExactly(optionId3, optionId1); + assertThat(actual).containsExactlyInAnyOrder(optionItem1, optionItem2, optionItem3); } } diff --git a/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java b/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java new file mode 100644 index 000000000..e0d427558 --- /dev/null +++ b/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java @@ -0,0 +1,74 @@ +package reviewme.question.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import reviewme.question.domain.Question; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@DataJpaTest +class QuestionRepositoryTest { + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Test + void 템플릿_아이디로_질문_목록_아이디를_모두_가져온다() { + // given + Question question1 = questionRepository.save(서술형_필수_질문(1)); + Question question2 = questionRepository.save(서술형_필수_질문(2)); + Question question3 = questionRepository.save(서술형_필수_질문(1)); + Question question4 = questionRepository.save(서술형_필수_질문(2)); + + List sectionQuestion1 = List.of(question1.getId(), question2.getId()); + List sectionQuestion2 = List.of(question3.getId(), question4.getId()); + Section section1 = sectionRepository.save(항상_보이는_섹션(sectionQuestion1)); + sectionRepository.save(항상_보이는_섹션(sectionQuestion2)); + List sectionIds = List.of(section1.getId()); + Template template = templateRepository.save(템플릿(sectionIds)); + + // when + Set actual = questionRepository.findAllQuestionIdByTemplateId(template.getId()); + + // then + assertThat(actual).containsExactlyInAnyOrder(question1.getId(), question2.getId()); + } + + @Test + void 템플릿_아이디로_질문_목록을_모두_가져온다() { + // given + Question question1 = questionRepository.save(서술형_필수_질문(1)); + Question question2 = questionRepository.save(서술형_필수_질문(2)); + Question question3 = questionRepository.save(서술형_필수_질문(1)); + Question question4 = questionRepository.save(서술형_필수_질문(2)); + + List sectionQuestion1 = List.of(question1.getId(), question2.getId()); + List sectionQuestion2 = List.of(question3.getId(), question4.getId()); + Section section1 = sectionRepository.save(항상_보이는_섹션(sectionQuestion1)); + sectionRepository.save(항상_보이는_섹션(sectionQuestion2)); + List sectionIds = List.of(section1.getId()); + Template template = templateRepository.save(템플릿(sectionIds)); + + // when + List actual = questionRepository.findAllByTemplatedId(template.getId()); + + // then + assertThat(actual).containsExactlyInAnyOrder(question1, question2); + } +} diff --git a/backend/src/test/java/reviewme/review/domain/CheckboxAnswerTest.java b/backend/src/test/java/reviewme/review/domain/CheckboxAnswerTest.java new file mode 100644 index 000000000..4581690ab --- /dev/null +++ b/backend/src/test/java/reviewme/review/domain/CheckboxAnswerTest.java @@ -0,0 +1,22 @@ +package reviewme.review.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.Test; +import reviewme.review.domain.exception.QuestionNotAnsweredException; + +class CheckboxAnswerTest { + + @Test + void 답변이_없는_경우_예외를_발생한다() { + // given, when, then + assertAll( + () -> assertThatThrownBy(() -> new CheckboxAnswer(1L, null)) + .isInstanceOf(QuestionNotAnsweredException.class), + () -> assertThatThrownBy(() -> new CheckboxAnswer(1L, List.of())) + .isInstanceOf(QuestionNotAnsweredException.class) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/domain/CheckboxAnswersTest.java b/backend/src/test/java/reviewme/review/domain/CheckboxAnswersTest.java deleted file mode 100644 index 504b10071..000000000 --- a/backend/src/test/java/reviewme/review/domain/CheckboxAnswersTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package reviewme.review.domain; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.util.List; -import org.junit.jupiter.api.Test; -import reviewme.review.domain.exception.MissingCheckboxAnswerForQuestionException; - -class CheckboxAnswersTest { - - @Test - void 질문에_해당하는_답변이_없으면_예외를_발생한다() { - // given - CheckboxAnswers checkboxAnswers = new CheckboxAnswers(List.of(new CheckboxAnswer(1, List.of(1L)))); - - // when, then - assertThatThrownBy(() -> checkboxAnswers.getAnswerByQuestionId(2)) - .isInstanceOf(MissingCheckboxAnswerForQuestionException.class); - } - - @Test - void 질문_ID로_선택형_답변을_반환한다() { - // given - CheckboxAnswers checkboxAnswers = new CheckboxAnswers(List.of(new CheckboxAnswer(1, List.of(1L)))); - - // when - CheckboxAnswer actual = checkboxAnswers.getAnswerByQuestionId(1); - - // then - assertThat(actual.getSelectedOptionIds()) - .extracting(CheckBoxAnswerSelectedOption::getSelectedOptionId) - .containsExactly(1L); - } - - @Test - void 질문_ID에_해당하는_답변이_있는지_확인한다() { - // given - CheckboxAnswers checkboxAnswers = new CheckboxAnswers(List.of(new CheckboxAnswer(1, List.of(1L)))); - - // when - boolean actual1 = checkboxAnswers.hasAnswerByQuestionId(1); - boolean actual2 = checkboxAnswers.hasAnswerByQuestionId(2); - - // then - assertAll( - () -> assertThat(actual1).isTrue(), - () -> assertThat(actual2).isFalse() - ); - } -} diff --git a/backend/src/test/java/reviewme/review/domain/ReviewTest.java b/backend/src/test/java/reviewme/review/domain/ReviewTest.java new file mode 100644 index 000000000..8caeceb27 --- /dev/null +++ b/backend/src/test/java/reviewme/review/domain/ReviewTest.java @@ -0,0 +1,56 @@ +package reviewme.review.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class ReviewTest { + + @Test + void 리뷰에_등록된_답변의_모든_질문들을_반환한다() { + // given + TextAnswer textAnswer = new TextAnswer(1L, "답변"); + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(2L, List.of(1L)); + Review review = new Review(1L, 1L, List.of(textAnswer), List.of(checkboxAnswer)); + + // when + Set allQuestionIdsFromAnswers = review.getAnsweredQuestionIds(); + + // then + assertThat(allQuestionIdsFromAnswers).containsAll(List.of(1L, 2L)); + } + + @Test + void 리뷰에_등록된_모든_선택형_답변의_옵션들을_반환환다() { + // given + CheckboxAnswer checkboxAnswer1 = new CheckboxAnswer(1L, List.of(1L, 2L)); + CheckboxAnswer checkboxAnswer2 = new CheckboxAnswer(1L, List.of(3L, 4L)); + Review review = new Review(1L, 1L, List.of(), List.of(checkboxAnswer1, checkboxAnswer2)); + + // when + Set allQuestionIdsFromAnswers = review.getAllCheckBoxOptionIds(); + + // then + assertThat(allQuestionIdsFromAnswers).containsAll(List.of(1L, 2L, 3L, 4L)); + } + + @Test + void 리뷰에_특정_질문에_대한_답변이_있는지_여부를_반환한다() { + // given + long textQuestionId = 1L; + long checkBoxQuestionId = 2L; + + TextAnswer textAnswer = new TextAnswer(textQuestionId, "답변"); + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(checkBoxQuestionId, List.of(1L)); + Review review = new Review(1L, 1L, List.of(textAnswer), List.of(checkboxAnswer)); + + // when, then + assertAll( + () -> assertThat(review.hasAnsweredQuestion(textQuestionId)).isTrue(), + () -> assertThat(review.hasAnsweredQuestion(checkBoxQuestionId)).isTrue() + ); + } +} diff --git a/backend/src/test/java/reviewme/review/domain/TextAnswerTest.java b/backend/src/test/java/reviewme/review/domain/TextAnswerTest.java new file mode 100644 index 000000000..bea1aa188 --- /dev/null +++ b/backend/src/test/java/reviewme/review/domain/TextAnswerTest.java @@ -0,0 +1,21 @@ +package reviewme.review.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.Test; +import reviewme.review.domain.exception.QuestionNotAnsweredException; + +class TextAnswerTest { + + @Test + void 답변이_없는_경우_예외를_발생한다() { + // given, when, then + assertAll( + () -> assertThatThrownBy(() -> new TextAnswer(1L, null)) + .isInstanceOf(QuestionNotAnsweredException.class), + () -> assertThatThrownBy(() -> new TextAnswer(1L, "")) + .isInstanceOf(QuestionNotAnsweredException.class) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/repository/QuestionRepositoryTest.java b/backend/src/test/java/reviewme/review/repository/QuestionRepositoryTest.java deleted file mode 100644 index 6ffc56eeb..000000000 --- a/backend/src/test/java/reviewme/review/repository/QuestionRepositoryTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package reviewme.review.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; -import java.util.Set; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.QuestionRepository; -import reviewme.template.domain.Section; -import reviewme.template.domain.Template; -import reviewme.template.domain.VisibleType; -import reviewme.template.repository.SectionRepository; -import reviewme.template.repository.TemplateRepository; - -@DataJpaTest -class QuestionRepositoryTest { - - @Autowired - private QuestionRepository questionRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TemplateRepository templateRepository; - - @Test - void 섹션_아이디로_질문_목록을_순서대로_가져온다() { - // given - Question question1 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문1", null, 1)); - Question question2 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문2", null, 2)); - Question question3 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문3", null, 3)); - questionRepository.save(new Question(true, QuestionType.TEXT, "질문4", null, 1)); - - List questionIds = List.of(question3.getId(), question1.getId(), question2.getId()); - Section section = sectionRepository.save(new Section(VisibleType.ALWAYS, questionIds, null, "sectionName", "header", 0)); - - // when - List actual = questionRepository.findAllBySectionId(section.getId()); - - // then - assertThat(actual).extracting(Question::getId) - .containsExactly(question1.getId(), question2.getId(), question3.getId()); - } - - @Test - void 템플릿_아이디로_질문_목록을_모두_가져온다() { - // given - Question question1 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문1", null, 1)); - Question question2 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문2", null, 2)); - Question question3 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문3", null, 1)); - Question question4 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문4", null, 2)); - - List sectionQuestion1 = List.of(question1.getId(), question2.getId()); - List sectionQuestion2 = List.of(question3.getId(), question4.getId()); - Section section1 = sectionRepository.save(new Section(VisibleType.ALWAYS, sectionQuestion1, null, "sectionName", "header", 0)); - sectionRepository.save(new Section(VisibleType.ALWAYS, sectionQuestion2, null, "sectionName", "header", 0)); - List sectionIds = List.of(section1.getId()); - Template template = templateRepository.save(new Template(sectionIds)); - - // when - Set actual = questionRepository.findAllQuestionIdByTemplateId(template.getId()); - - // then - assertThat(actual).containsExactlyInAnyOrder(question1.getId(), question2.getId()); - } -} diff --git a/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java b/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java new file mode 100644 index 000000000..51ea4c83f --- /dev/null +++ b/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java @@ -0,0 +1,200 @@ +package reviewme.review.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.Review; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@DataJpaTest +class ReviewRepositoryTest { + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Test + void 리뷰_그룹_아이디에_해당하는_모든_리뷰를_생성일_기준_내림차순으로_불러온다() { + // given + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Review review1 = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null)); + Review review2 = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null)); + + // when + List actual = reviewRepository.findAllByGroupId(reviewGroup.getId()); + + // then + assertThat(actual).containsExactly(review2, review1); + } + + @Nested + class 리뷰그룹_아이디에_해당하는_리뷰를_생성일_기준_내림차순으로_페이징하여_불러온다 { + + private final Question question = questionRepository.save(서술형_필수_질문()); + private final Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + private final Template template = templateRepository.save(템플릿(List.of(section.getId()))); + private final ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + private final Review review1 = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null)); + private final Review review2 = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null)); + private final Review review3 = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null)); + + @Test + void 페이징_크기보다_적은_수의_리뷰가_등록되었으면_그_크기만큼의_리뷰만_반환한다() { + // given + int limit = 5; + long lastReviewId = Long.MAX_VALUE; + + // when + List actual = reviewRepository.findByReviewGroupIdWithLimit( + reviewGroup.getId(), lastReviewId, limit); + + // then + assertThat(actual) + .hasSize(3) + .containsExactly(review3, review2, review1); + } + + @Test + void 페이징_크기보다_큰_수의_리뷰가_등록되었으면_페이징_크기만큼의_리뷰를_반환한다() { + // given + int limit = 2; + long lastReviewId = Long.MAX_VALUE; + + // when + List actual = reviewRepository.findByReviewGroupIdWithLimit( + reviewGroup.getId(), lastReviewId, limit); + + // then + assertThat(actual) + .hasSize(2) + .containsExactly(review3, review2); + } + + @Test + void 마지막_리뷰_아이디가_주어지지_않으면_가장_최신순으로_리뷰를_반환한다() { + // given + int limit = 5; + Long lastReviewId = null; + + // when + List actual = reviewRepository.findByReviewGroupIdWithLimit( + reviewGroup.getId(), lastReviewId, limit); + + // then + assertThat(actual) + .hasSize(3) + .containsExactly(review3, review2, review1); + } + + @Test + void 마지막_리뷰_아이디를_기준으로_그보다_전에_적힌_리뷰를_반환한다() { + // given + int limit = 5; + long lastReviewId = review3.getId(); + + // when + List actual = reviewRepository.findByReviewGroupIdWithLimit( + reviewGroup.getId(), lastReviewId, limit); + + // then + assertThat(actual) + .hasSize(2) + .containsExactly(review2, review1); + } + + @Test + void 마지막으로_온_리뷰_전에_작성된_리뷰가_없으면_빈_리스트를_반환한다() { + // given + int limit = 5; + long lastReviewId = review1.getId(); + + // when + List actual = reviewRepository.findByReviewGroupIdWithLimit( + reviewGroup.getId(), lastReviewId, limit); + + // then + assertThat(actual).isEmpty(); + } + } + + @Nested + class 주어진_리뷰보다_오래된_리뷰가_있는지_검사한다 { + + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Review firstReview = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null)); + Review secondReview = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null)); + + @Test + void 주어진_리뷰가_가장_오래된_경우() { + // given + long reviewGroupId = reviewGroup.getId(); + long reviewId = firstReview.getId(); + LocalDate createdAt = firstReview.getCreatedAt().toLocalDate(); + + // when + boolean isOlderExist = reviewRepository.existsOlderReviewInGroup(reviewGroupId, reviewId, createdAt); + + // then + assertThat(isOlderExist).isFalse(); + } + + @Test + void 주어진_리뷰가_가장_오래되지_않은_경우() { + // given + long reviewGroupId = reviewGroup.getId(); + long reviewId = secondReview.getId(); + LocalDate createdAt = secondReview.getCreatedAt().toLocalDate(); + + // when + boolean isOlderExist = reviewRepository.existsOlderReviewInGroup(reviewGroupId, reviewId, createdAt); + + // then + assertThat(isOlderExist).isTrue(); + } + } +} diff --git a/backend/src/test/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidatorTest.java b/backend/src/test/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidatorTest.java deleted file mode 100644 index f42bd2072..000000000 --- a/backend/src/test/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidatorTest.java +++ /dev/null @@ -1,160 +0,0 @@ -package reviewme.review.service; - -import static org.assertj.core.api.Assertions.assertThatCode; - -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import reviewme.question.domain.OptionGroup; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.support.ServiceTest; -import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; - -@ServiceTest -class CreateCheckBoxAnswerRequestValidatorTest { - - @Autowired - private CreateCheckBoxAnswerRequestValidator createCheckBoxAnswerRequestValidator; - - @Autowired - private QuestionRepository questionRepository; - - @Autowired - private OptionGroupRepository optionGroupRepository; - - @Autowired - private OptionItemRepository optionItemRepository; - - private Question savedQuestion; - - @BeforeEach - void setUp() { - savedQuestion = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "질문", null, 1)); - } - - @Test - void 저장되지_않은_질문에_대한_응답이면_예외가_발생한다() { - // given - long notSavedQuestionId = 100L; - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - notSavedQuestionId, List.of(1L), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(SubmittedQuestionNotFoundException.class); - } - - @Test - void 선택형_질문에_텍스트_응답을_하면_예외가_발생한다() { - // given - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(1L), "서술형 응답" - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(CheckBoxAnswerIncludedTextException.class); - } - - @Test - void 저장되지_않은_옵션그룹에_대해_응답하면_예외가_발생한다() { - // given - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(1L), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(OptionGroupNotFoundByQuestionIdException.class); - } - - @Test - void 필수_선택형_질문에_응답을_하지_않으면_예외가_발생한다() { - // given - optionGroupRepository.save( - new OptionGroup(savedQuestion.getId(), 1, 3) - ); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), - null, - null); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(RequiredQuestionNotAnsweredException.class); - } - - @Test - void 옵션그룹에서_제공하지_않은_옵션아이템을_응답하면_예외가_발생한다() { - // given - OptionGroup savedOptionGroup = optionGroupRepository.save( - new OptionGroup(savedQuestion.getId(), 1, 3) - ); - OptionItem savedOptionItem = optionItemRepository.save( - new OptionItem("옵션", savedOptionGroup.getId(), 1, OptionType.KEYWORD) - ); - - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(savedOptionItem.getId() + 1L), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(CheckBoxAnswerIncludedNotProvidedOptionItemException.class); - } - - @Test - void 옵션그룹에서_정한_최소_선택_수_보다_적게_선택하면_예외가_발생한다() { - // given - OptionGroup savedOptionGroup = optionGroupRepository.save( - new OptionGroup(savedQuestion.getId(), 2, 3) - ); - OptionItem savedOptionItem1 = optionItemRepository.save( - new OptionItem("옵션1", savedOptionGroup.getId(), 1, OptionType.KEYWORD) - ); - - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(savedOptionItem1.getId()), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); - } - - @Test - void 옵션그룹에서_정한_최대_선택_수_보다_많이_선택하면_예외가_발생한다() { - // given - OptionGroup savedOptionGroup = optionGroupRepository.save( - new OptionGroup(savedQuestion.getId(), 1, 1) - ); - OptionItem savedOptionItem1 = optionItemRepository.save( - new OptionItem("옵션1", savedOptionGroup.getId(), 1, OptionType.KEYWORD) - ); - OptionItem savedOptionItem2 = optionItemRepository.save( - new OptionItem("옵션2", savedOptionGroup.getId(), 2, OptionType.KEYWORD) - ); - - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(savedOptionItem1.getId(), savedOptionItem2.getId()), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); - } -} diff --git a/backend/src/test/java/reviewme/review/service/CreateReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/CreateReviewServiceTest.java deleted file mode 100644 index 8901b8d67..000000000 --- a/backend/src/test/java/reviewme/review/service/CreateReviewServiceTest.java +++ /dev/null @@ -1,264 +0,0 @@ -package reviewme.review.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import reviewme.question.domain.OptionGroup; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.repository.CheckboxAnswerRepository; -import reviewme.review.repository.ReviewRepository; -import reviewme.review.repository.TextAnswerRepository; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.exception.MissingRequiredQuestionException; -import reviewme.review.service.exception.UnnecessaryQuestionIncludedException; -import reviewme.reviewgroup.domain.ReviewGroup; -import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.support.ServiceTest; -import reviewme.template.domain.Section; -import reviewme.template.domain.Template; -import reviewme.template.domain.VisibleType; -import reviewme.template.repository.SectionRepository; -import reviewme.template.repository.TemplateRepository; - -@ServiceTest -class CreateReviewServiceTest { - - @Autowired - private CreateReviewService createReviewService; - - @Autowired - private QuestionRepository questionRepository; - - @Autowired - private OptionGroupRepository optionGroupRepository; - - @Autowired - private OptionItemRepository optionItemRepository; - - @Autowired - private ReviewGroupRepository reviewGroupRepository; - - @Autowired - private TemplateRepository templateRepository; - - @Autowired - private ReviewRepository reviewRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TextAnswerRepository textAnswerRepository; - - @Autowired - private CheckboxAnswerRepository checkboxAnswerRepository; - - @Test - void 필수_질문에_모두_응답하는_경우_예외가_발생하지_않는다() { - // 리뷰 그룹 저장 - String reviewRequestCode = "1234"; - reviewGroupRepository.save(new ReviewGroup("리뷰어", "프로젝트", reviewRequestCode, "12341234")); - - // 필수 선택형 질문, 섹션 저장 - Question alwaysRequiredQuestion = questionRepository.save( - new Question(true, QuestionType.CHECKBOX, "질문", "가이드라인", 1) - ); - OptionGroup alwaysRequiredOptionGroup = optionGroupRepository.save( - new OptionGroup(alwaysRequiredQuestion.getId(), 1, 2) - ); - OptionItem alwaysRequiredOptionItem1 = optionItemRepository.save( - new OptionItem("선택지", alwaysRequiredOptionGroup.getId(), 1, OptionType.KEYWORD) - ); - OptionItem alwaysRequiredOptionItem2 = optionItemRepository.save( - new OptionItem("선택지", alwaysRequiredOptionGroup.getId(), 2, OptionType.KEYWORD) - ); - Section alwaysRequiredSection = sectionRepository.save( - new Section(VisibleType.ALWAYS, List.of(alwaysRequiredQuestion.getId()), null, "섹션명", "말머리", 1) - ); - - // 필수가 아닌 서술형 질문 저장 - Question notRequiredQuestion = questionRepository.save( - new Question(false, QuestionType.TEXT, "질문", "가이드라인", 1) - ); - Section notRequiredSection = sectionRepository.save( - new Section(VisibleType.ALWAYS, List.of(notRequiredQuestion.getId()), null, "섹션명", "말머리", 1) - ); - - // optionItem 선택에 따라서 required 가 달라지는 섹션1 저장 - Question conditionalTextQuestion1 = questionRepository.save( - new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1) - ); - Question conditionalCheckQuestion = questionRepository.save( - new Question(true, QuestionType.CHECKBOX, "질문", "가이드라인", 1) - ); - OptionGroup conditionalOptionGroup = optionGroupRepository.save( - new OptionGroup(conditionalCheckQuestion.getId(), 1, 2) - ); - OptionItem conditionalOptionItem = optionItemRepository.save( - new OptionItem("선택지", conditionalOptionGroup.getId(), 1, OptionType.KEYWORD) - ); - Section conditionalSection1 = sectionRepository.save( - new Section(VisibleType.CONDITIONAL, - List.of(conditionalTextQuestion1.getId(), conditionalCheckQuestion.getId()), - alwaysRequiredOptionItem1.getId(), "섹션명", "말머리", 1) - ); - - // optionItem 선택에 따라서 required 가 달라지는 섹션2 저장 - Question conditionalQuestion2 = questionRepository.save( - new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1) - ); - Section conditionalSection2 = sectionRepository.save( - new Section(VisibleType.CONDITIONAL, List.of(conditionalQuestion2.getId()), - alwaysRequiredOptionItem2.getId(), "섹션명", "말머리", 1) - ); - - // 템플릿 저장 - templateRepository.save(new Template( - List.of(alwaysRequiredSection.getId(), conditionalSection1.getId(), - conditionalSection2.getId(), notRequiredSection.getId()) - )); - - // 각 질문에 대한 답변 생성 - CreateReviewAnswerRequest alwaysRequiredAnswer = new CreateReviewAnswerRequest( - alwaysRequiredQuestion.getId(), List.of(alwaysRequiredOptionItem1.getId()), null); - CreateReviewAnswerRequest conditionalTextAnswer1 = new CreateReviewAnswerRequest( - conditionalTextQuestion1.getId(), null, "답변".repeat(30)); - CreateReviewAnswerRequest conditionalCheckAnswer1 = new CreateReviewAnswerRequest( - conditionalCheckQuestion.getId(), List.of(conditionalOptionItem.getId()), null); - CreateReviewAnswerRequest conditionalTextAnswer2 = new CreateReviewAnswerRequest( - conditionalQuestion2.getId(), null, "답변".repeat(30)); - - // 상황별로 다르게 구성한 리뷰 생성 dto - CreateReviewRequest properRequest = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalTextAnswer1, conditionalCheckAnswer1)); - CreateReviewRequest selectedOptionIdQuestionMissingRequest1 = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer)); - CreateReviewRequest selectedOptionIdQuestionMissingRequest2 = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalTextAnswer1)); - CreateReviewRequest selectedOptionIdQuestionMissingRequest3 = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalCheckAnswer1)); - CreateReviewRequest unnecessaryQuestionIncludedRequest = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalTextAnswer1, - conditionalCheckAnswer1, conditionalTextAnswer2)); - - // when, then - assertThatCode(() -> createReviewService.createReview(properRequest)) - .doesNotThrowAnyException(); - assertThatCode(() -> createReviewService.createReview(selectedOptionIdQuestionMissingRequest1)) - .isInstanceOf(MissingRequiredQuestionException.class); - assertThatCode(() -> createReviewService.createReview(selectedOptionIdQuestionMissingRequest2)) - .isInstanceOf(MissingRequiredQuestionException.class); - assertThatCode(() -> createReviewService.createReview(selectedOptionIdQuestionMissingRequest3)) - .isInstanceOf(MissingRequiredQuestionException.class); - assertThatCode(() -> createReviewService.createReview(unnecessaryQuestionIncludedRequest)) - .isInstanceOf(UnnecessaryQuestionIncludedException.class); - } - - @Test - void 텍스트가_포함된_리뷰를_저장한다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(new ReviewGroup("리뷰어", "프로젝트", reviewRequestCode, "12341234")); - Section section = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(1L), 1L, "섹션명", "말머리", 1)); - templateRepository.save(new Template(List.of(section.getId()))); - - String expectedTextAnswer = "답".repeat(20); - Question savedQuestion = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest createReviewAnswerRequest = new CreateReviewAnswerRequest(savedQuestion.getId(), null, - expectedTextAnswer); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(createReviewAnswerRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(textAnswerRepository.findAll()).hasSize(1); - } - - @Test - void 필수가_아닌_텍스트형_응답에_빈문자열이_들어오면_저장하지_않는다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(new ReviewGroup("리뷰어", "프로젝트", reviewRequestCode, "12341234")); - Section section = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(1L), 1L, "섹션명", "말머리", 1)); - templateRepository.save(new Template(List.of(section.getId()))); - - Question savedQuestion = questionRepository.save( - new Question(false, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest emptyTextReviewRequest = new CreateReviewAnswerRequest( - savedQuestion.getId(), null, ""); - CreateReviewAnswerRequest validTextReviewRequest = new CreateReviewAnswerRequest( - savedQuestion.getId(), null, "질문 1 답변 (20자 이상 입력 적용)"); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(emptyTextReviewRequest, validTextReviewRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(textAnswerRepository.findAll()).hasSize(1); - } - - @Test - void 체크박스가_포함된_리뷰를_저장한다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(new ReviewGroup("리뷰어", "프로젝트", reviewRequestCode, "12341234")); - Section section = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(1L), 1L, "섹션명", "말머리", 1)); - templateRepository.save(new Template(List.of(section.getId()))); - - Question savedQuestion = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "질문", "가이드라인", 1)); - OptionGroup savedOptionGroup = optionGroupRepository.save(new OptionGroup(savedQuestion.getId(), 2, 2)); - OptionItem savedOptionItem1 = optionItemRepository.save( - new OptionItem("선택지1", savedOptionGroup.getId(), 1, OptionType.KEYWORD)); - OptionItem savedOptionItem2 = optionItemRepository.save( - new OptionItem("선택지2", savedOptionGroup.getId(), 2, OptionType.KEYWORD)); - CreateReviewAnswerRequest createReviewAnswerRequest = new CreateReviewAnswerRequest(savedQuestion.getId(), - List.of(savedOptionItem1.getId(), savedOptionItem2.getId()), null); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(createReviewAnswerRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(checkboxAnswerRepository.findAll()).hasSize(1); - } - - @Test - void 적정_글자수인_텍스트_응답인_경우_정상_저장된다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(new ReviewGroup("리뷰어", "프로젝트", reviewRequestCode, "12341234")); - Section section = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(1L), 1L, "섹션명", "말머리", 1)); - templateRepository.save(new Template(List.of(section.getId()))); - - String expectedTextAnswer = "답".repeat(1000); - Question savedQuestion = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest createReviewAnswerRequest = new CreateReviewAnswerRequest(savedQuestion.getId(), null, - expectedTextAnswer); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(createReviewAnswerRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(textAnswerRepository.findAll()).hasSize(1); - } -} diff --git a/backend/src/test/java/reviewme/review/service/CreateTextAnswerRequestValidatorTest.java b/backend/src/test/java/reviewme/review/service/CreateTextAnswerRequestValidatorTest.java deleted file mode 100644 index 8224d4acd..000000000 --- a/backend/src/test/java/reviewme/review/service/CreateTextAnswerRequestValidatorTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package reviewme.review.service; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.exception.InvalidTextAnswerLengthException; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; -import reviewme.support.ServiceTest; - -@ServiceTest -class CreateTextAnswerRequestValidatorTest { - - @Autowired - private CreateTextAnswerRequestValidator createTextAnswerRequestValidator; - - @Autowired - private QuestionRepository questionRepository; - - @Test - void 저장되지_않은_질문에_대한_대답이면_예외가_발생한다() { - // given - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(100L, null, "텍스트형 응답"); - - // when, then - assertThatCode(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(SubmittedQuestionNotFoundException.class); - } - - @Test - void 텍스트형_질문에_선택형_응답을_하면_예외가_발생한다() { - // given - Question savedQuestion - = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(savedQuestion.getId(), List.of(1L), "응답"); - - // when, then - assertThatCode(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(TextAnswerIncludedOptionItemException.class); - } - - @Test - void 필수_텍스트형_질문에_응답을_하지_않으면_예외가_발생한다() { - // given - Question savedQuestion - = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(savedQuestion.getId(), null, null); - - // when, then - assertThatCode(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(RequiredQuestionNotAnsweredException.class); - } - - @ParameterizedTest - @ValueSource(ints = {19, 10001}) - void 답변_길이가_유효하지_않으면_예외가_발생한다(int length) { - // given - String textAnswer = "답".repeat(length); - Question savedQuestion - = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(savedQuestion.getId(), null, textAnswer); - - // when, then - assertThatThrownBy(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(InvalidTextAnswerLengthException.class); - } -} diff --git a/backend/src/test/java/reviewme/review/service/PageSizeTest.java b/backend/src/test/java/reviewme/review/service/PageSizeTest.java new file mode 100644 index 000000000..c3bc4bae4 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/PageSizeTest.java @@ -0,0 +1,47 @@ +package reviewme.review.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PageSizeTest { + + @Test + void 유효한_값이_들어오면_그_값을_설정한다() { + // given + int size = 50; + + // when + PageSize pageSize = new PageSize(size); + + // then + assertEquals(size, pageSize.getSize()); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, 51}) + void 유효한_범위_외의_값이_들어오면_기본값으로_설정한다(Integer size) { + // given + int defaultSize = 10; + + // when + PageSize pageSize = new PageSize(size); + + // then + assertEquals(defaultSize, pageSize.getSize()); + } + + @Test + void null이_들어오면_기본값으로_설정한다() { + // given + int defaultSize = 10; + + // when + PageSize pageSize = new PageSize(null); + + // then + assertEquals(defaultSize, pageSize.getSize()); + } +} diff --git a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java index 84b54afb2..915693e8f 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java @@ -3,35 +3,39 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; -import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reviewme.question.domain.OptionGroup; import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; import reviewme.question.repository.OptionGroupRepository; import reviewme.question.repository.OptionItemRepository; import reviewme.question.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.review.repository.ReviewRepository; import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.review.service.dto.response.detail.SectionAnswerResponse; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; -import reviewme.review.service.exception.ReviewGroupUnauthorizedException; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.review.service.exception.ReviewNotFoundByIdAndGroupException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; import reviewme.template.domain.Section; import reviewme.template.domain.Template; -import reviewme.template.domain.VisibleType; import reviewme.template.repository.SectionRepository; import reviewme.template.repository.TemplateRepository; @@ -63,152 +67,145 @@ class ReviewDetailLookupServiceTest { private TemplateRepository templateRepository; @Test - void 잘못된_리뷰_요청_코드로_리뷰를_조회할_경우_예외를_발생한다() { + void 잘못된_리뷰_요청_코드로_리뷰를_조회할_경우_예외가_발생한다() { // given - String reviewRequestCode = "reviewRequestCode"; - String groupAccessCode = "groupAccessCode"; - ReviewGroup reviewGroup = reviewGroupRepository.save( - new ReviewGroup("테드", "리뷰미 프로젝트", reviewRequestCode, groupAccessCode)); - + String reviewRequestCode = "hello"; + String groupAccessCode = "goodBye"; + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); Review review = reviewRepository.save(new Review(0, reviewGroup.getId(), List.of(), List.of())); // when, then assertThatThrownBy(() -> reviewDetailLookupService.getReviewDetail( - review.getId(), "wrong" + reviewRequestCode, groupAccessCode + review.getId(), "wrong" + reviewRequestCode )).isInstanceOf(ReviewGroupNotFoundByReviewRequestCodeException.class); } @Test - void 잘못된_그룹_액세스_코드로_리뷰를_조회할_경우_예외를_발생한다() { - // given - String reviewRequestCode = "reviewRequestCode"; - String groupAccessCode = "groupAccessCode"; - ReviewGroup reviewGroup = reviewGroupRepository.save( - new ReviewGroup("테드", "리뷰미 프로젝트", reviewRequestCode, groupAccessCode)); - - Review review = reviewRepository.save(new Review(0, reviewGroup.getId(), List.of(), List.of())); - - // when, then - assertThatThrownBy(() -> reviewDetailLookupService.getReviewDetail( - review.getId(), reviewRequestCode, "wrong" + groupAccessCode - )).isInstanceOf(ReviewGroupUnauthorizedException.class); - } - - @Test - void 리뷰_그룹에_해당하지_않는_리뷰를_조회할_경우_예외를_발생한다() { + void 리뷰_그룹에_해당하지_않는_리뷰를_조회할_경우_예외가_발생한다() { // given - String reviewRequestCode1 = "reviewRequestCode1"; - String groupAccessCode1 = "groupAccessCode1"; - ReviewGroup reviewGroup1 = reviewGroupRepository.save( - new ReviewGroup("테드", "리뷰미 프로젝트", reviewRequestCode1, groupAccessCode1)); - ReviewGroup reviewGroup2 = reviewGroupRepository.save( - new ReviewGroup("테드", "리뷰미 프로젝트", "ABCD", "1234")); + String reviewRequestCode1 = "sancho"; + String groupAccessCode1 = "kirby"; + String reviewRequestCode2 = "aruru"; + String groupAccessCode2 = "tedChang"; + ReviewGroup reviewGroup1 = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode1, groupAccessCode1)); + ReviewGroup reviewGroup2 = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode2, groupAccessCode2)); Review review1 = reviewRepository.save(new Review(0, reviewGroup1.getId(), List.of(), List.of())); Review review2 = reviewRepository.save(new Review(0, reviewGroup2.getId(), List.of(), List.of())); // when, then - assertThatThrownBy(() -> reviewDetailLookupService.getReviewDetail( - review2.getId(), reviewRequestCode1, groupAccessCode1)) - .isInstanceOf(ReviewNotFoundByIdAndGroupException.class); + assertAll( + () -> assertThatThrownBy(() -> reviewDetailLookupService.getReviewDetail( + review2.getId(), reviewRequestCode1 + )).isInstanceOf(ReviewNotFoundByIdAndGroupException.class), + () -> assertThatThrownBy(() -> reviewDetailLookupService.getReviewDetail( + review1.getId(), reviewRequestCode2 + )).isInstanceOf(ReviewNotFoundByIdAndGroupException.class) + ); } @Test void 사용자가_작성한_리뷰를_확인한다() { - // given - String reviewRequestCode = "ABCD"; - String groupAccessCode = "0000"; - ReviewGroup reviewGroup = reviewGroupRepository.save(new ReviewGroup("aru", "reviewme", reviewRequestCode, groupAccessCode)); - Question question1 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", null, 1)); - Question question2 = questionRepository.save(new Question(true, QuestionType.CHECKBOX, "질문", null, 1)); - Question question3 = questionRepository.save(new Question(true, QuestionType.TEXT, "체크 1 조건", "가이드라인", 1)); - OptionGroup optionGroup = optionGroupRepository.save(new OptionGroup(question2.getId(), 1, 3)); - OptionItem optionItem1 = optionItemRepository.save( - new OptionItem("체크 1", optionGroup.getId(), 1, OptionType.KEYWORD)); - OptionItem optionItem2 = optionItemRepository.save( - new OptionItem("체크 2", optionGroup.getId(), 1, OptionType.KEYWORD)); - - Section section1 = sectionRepository.save( - new Section(VisibleType.ALWAYS, List.of(question1.getId(), question2.getId()), null, "1번 섹션", "말머리", 1) - ); - Section section2 = sectionRepository.save( - new Section(VisibleType.CONDITIONAL, List.of(question3.getId()), optionItem1.getId(), "2번 섹션", "말머리", 2) - ); - Template template = templateRepository.save(new Template(List.of(section1.getId(), section2.getId()))); - - List textAnswers = List.of( - new TextAnswer(1, "질문 1 답변 (20자 이상 입력 적용)"), - new TextAnswer(3, "질문 3 답변 (20자 이상 입력 적용)") - ); + // given - 리뷰 그룹 저장 + String reviewRequestCode = "1111"; + String groupAccessCode = "2222"; + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); + + // given - 질문 저장 + Question question1 = questionRepository.save(선택형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup.getId(), 1)); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup.getId(), 2)); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Section section2 = sectionRepository.save(항상_보이는_섹션(List.of(question2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId(), section2.getId()))); + + // given - 리뷰 답변 저장 + List textAnswers = List.of(new TextAnswer(question2.getId(), "답변".repeat(20))); List checkboxAnswers = List.of( - new CheckboxAnswer(2, List.of(optionItem1.getId(), optionItem2.getId())) + new CheckboxAnswer(question1.getId(), List.of(optionItem1.getId(), optionItem2.getId())) ); Review review = reviewRepository.save( new Review(template.getId(), reviewGroup.getId(), textAnswers, checkboxAnswers) ); // when - TemplateAnswerResponse reviewDetail = reviewDetailLookupService.getReviewDetail( - review.getId(), reviewRequestCode, groupAccessCode + ReviewDetailResponse reviewDetail = reviewDetailLookupService.getReviewDetail( + review.getId(), reviewRequestCode ); // then assertThat(reviewDetail.sections()).hasSize(2); } - @Test - void 답변이_있는_리뷰만_보여준다() { - // given - String reviewRequestCode = "ABCD"; - String groupAccessCode = "0000"; - ReviewGroup reviewGroup = reviewGroupRepository.save(new ReviewGroup("aru", "reviewme", reviewRequestCode, groupAccessCode)); - Question question1 = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", null, 1)); - Question question2 = questionRepository.save(new Question(false, QuestionType.CHECKBOX, "질문", null, 1)); - Question question3 = questionRepository.save(new Question(true, QuestionType.TEXT, "체크 1 조건", "가이드라인", 1)); - Question question4 = questionRepository.save(new Question(false, QuestionType.TEXT, "선택 질문", "가이드라인", 1)); - OptionGroup optionGroup = optionGroupRepository.save(new OptionGroup(question2.getId(), 1, 3)); - OptionItem optionItem1 = optionItemRepository.save( - new OptionItem("체크 1", optionGroup.getId(), 1, OptionType.KEYWORD)); - OptionItem optionItem2 = optionItemRepository.save( - new OptionItem("체크 2", optionGroup.getId(), 1, OptionType.KEYWORD)); - - Section section1 = sectionRepository.save( - new Section(VisibleType.ALWAYS, List.of(question1.getId(), question2.getId()), null, "1번 섹션", "말머리", 1) - ); - Section section2 = sectionRepository.save( - new Section(VisibleType.CONDITIONAL, List.of(question3.getId()), optionItem1.getId(), "2번 섹션", "말머리", 2) - ); - Section section3 = sectionRepository.save( - new Section(VisibleType.ALWAYS, List.of(question4.getId()), null, "3번 섹션", "말머리", 3) - ); - - Template template = templateRepository.save( - new Template(List.of(section1.getId(), section2.getId(), section3.getId()))); - - List textAnswers = List.of( - new TextAnswer(1, "질문 1 답변"), - new TextAnswer(3, "질문 3 답변") - ); - List checkboxAnswers = new ArrayList<>(); - Review review = reviewRepository.save( - new Review(template.getId(), reviewGroup.getId(), textAnswers, checkboxAnswers) - ); - - // when - TemplateAnswerResponse reviewDetail = reviewDetailLookupService.getReviewDetail( - review.getId(), reviewRequestCode, groupAccessCode - ); - - // then - List sections = reviewDetail.sections(); - - assertAll( - () -> assertThat(sections).extracting(SectionAnswerResponse::sectionId) - .containsExactly(section1.getId(), section2.getId()), - () -> assertThat(sections.get(0).questions()) - .extracting(QuestionAnswerResponse::questionId).containsExactly(question1.getId()), - () -> assertThat(sections.get(1).questions()) - .extracting(QuestionAnswerResponse::questionId).containsExactly(question3.getId()) - ); + @Nested + class 필수가_아닌_답변에_응답하지_않았을_때 { + + @Test + void 섹션에_필수가_아닌_질문만_있다면_섹션_자체를_반환하지_않는다() { + // given - 리뷰 그룹 저장 + String reviewRequestCode = "sancho"; + String groupAccessCode = "kirby"; + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); + + // given - 질문, 세션, 템플릿 저장 + Question question = questionRepository.save(서술형_옵션_질문(1)); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + // given - 아무것도 응답하지 않은 리뷰 답변 저장 + Review review = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), null, null) + ); + + // when + ReviewDetailResponse reviewDetail = reviewDetailLookupService.getReviewDetail( + review.getId(), reviewRequestCode + ); + + // then + assertThat(reviewDetail.sections()) + .extracting(SectionAnswerResponse::sectionId) + .isEmpty(); + } + + @Test + void 섹션의_다른_질문에_응답했다면_답하지_않은_질문만_반환하지_않는다() { + // given - 리뷰 그룹 저장 + String reviewRequestCode = "aruru"; + String groupAccessCode = "tedChang"; + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); + + // given - 질문, 세션, 템플릿 저장 + Question question1 = questionRepository.save(서술형_옵션_질문(1)); + Question question2 = questionRepository.save(서술형_옵션_질문(2)); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId(), question2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + // given - 질문 하나에만 응답한 리뷰 답변 저장 + TextAnswer textAnswer = new TextAnswer(question1.getId(), "답변".repeat(20)); + Review review = reviewRepository.save( + new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), null) + ); + + // when + ReviewDetailResponse reviewDetail = reviewDetailLookupService.getReviewDetail( + review.getId(), reviewRequestCode + ); + + // then + assertAll( + () -> assertThat(reviewDetail.sections()) + .extracting(SectionAnswerResponse::sectionId) + .containsExactly(section.getId()), + () -> assertThat(reviewDetail.sections()) + .flatExtracting(SectionAnswerResponse::questions) + .extracting(QuestionAnswerResponse::questionId) + .containsExactly(question1.getId()) + ); + } } } diff --git a/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java new file mode 100644 index 000000000..bf89caa10 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java @@ -0,0 +1,141 @@ +package reviewme.review.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewListLookupServiceTest { + + @Autowired + private ReviewListLookupService reviewListLookupService; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Test + void 리뷰_요청_코드가_존재하지_않는_경우_예외가_발생한다() { + assertThatThrownBy(() -> reviewListLookupService.getReceivedReviews(Long.MAX_VALUE, 5, "abc")) + .isInstanceOf(ReviewGroupNotFoundByReviewRequestCodeException.class); + } + + @Test + void 확인_코드에_해당하는_그룹이_존재하면_내가_받은_리뷰_목록을_반환한다() { + // given - 리뷰 그룹 저장 + String reviewRequestCode = "reviewRequestCode"; + String groupAccessCode = "groupAccessCode"; + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); + + // given - 질문 저장 + Question question = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question.getId())); + OptionItem categoryOption = optionItemRepository.save(선택지(optionGroup.getId(), 1)); + + // given - 섹션, 템플릿 저장 + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + // given - 리뷰 답변 저장 + CheckboxAnswer categoryAnswer = new CheckboxAnswer(question.getId(), List.of(categoryOption.getId())); + Review review1 = new Review(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer)); + TextAnswer textAnswer = new TextAnswer(question.getId(), "텍스트형 응답"); + Review review2 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + reviewRepository.saveAll(List.of(review1, review2)); + + // when + ReceivedReviewsResponse response = reviewListLookupService.getReceivedReviews( + Long.MAX_VALUE, 5, reviewRequestCode); + + // then + assertAll( + () -> assertThat(response.reviews()).hasSize(2), + () -> assertThat(response.lastReviewId()).isEqualTo(review1.getId()), + () -> assertThat(response.isLastPage()).isTrue() + ); + } + + @Test + void 내가_받은_리뷰_목록을_페이지네이션을_적용하여_반환한다() { + // given - 리뷰 그룹 저장 + String reviewRequestCode = "reviewRequestCode"; + String groupAccessCode = "groupAccessCode"; + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); + + // given - 질문 저장 + Question question = questionRepository.save(선택형_필수_질문()); + + // given - 섹션, 템플릿 저장 + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + // given - 리뷰 답변 저장 + TextAnswer textAnswer = new TextAnswer(question.getId(), "텍스트형 응답"); + Review review1 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review2 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review3 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + reviewRepository.saveAll(List.of(review1, review2, review3)); + + // when + ReceivedReviewsResponse response + = reviewListLookupService.getReceivedReviews(Long.MAX_VALUE, 2, reviewRequestCode); + + // then + assertAll( + () -> assertThat(response.reviews()) + .hasSize(2) + .extracting("reviewId") + .containsExactly(review3.getId(), review2.getId()), + () -> assertThat(response.lastReviewId()) + .isEqualTo(review2.getId()), + () -> assertThat(response.isLastPage()) + .isFalse() + ); + } +} diff --git a/backend/src/test/java/reviewme/review/service/ReviewPreviewGeneratorTest.java b/backend/src/test/java/reviewme/review/service/ReviewPreviewGeneratorTest.java index f63c47b5f..b017b0bac 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewPreviewGeneratorTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewPreviewGeneratorTest.java @@ -7,11 +7,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import reviewme.review.domain.TextAnswer; +import reviewme.review.service.mapper.ReviewPreviewGenerator; class ReviewPreviewGeneratorTest { @Test - void 답변_내용이_미리보기_최대_글자를_넘는_경우_미리보기_길이만큼_잘라서_반환한다() { + void 답변_내용이_미리보기_최대_글자를_넘는_경우_미리보기_길이만큼_자르고_말줄임표를_붙여_반환한다() { // given ReviewPreviewGenerator reviewPreviewGenerator = new ReviewPreviewGenerator(); String answer = "*".repeat(151); @@ -21,7 +22,7 @@ class ReviewPreviewGeneratorTest { String actual = reviewPreviewGenerator.generatePreview(List.of(textAnswer)); // then - assertThat(actual).hasSize(150); + assertThat(actual).isEqualTo("*".repeat(150) + "..."); } @ParameterizedTest diff --git a/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java new file mode 100644 index 000000000..7bba502ff --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java @@ -0,0 +1,118 @@ +package reviewme.review.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.조건부로_보이는_섹션; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewRegisterServiceTest { + + @Autowired + private ReviewRegisterService reviewRegisterService; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Test + void 요청한_내용으로_리뷰를_등록한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question requiredCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup requiredOptionGroup = optionGroupRepository.save(선택지_그룹(requiredCheckQuestion.getId())); + OptionItem requiredOptionItem1 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + OptionItem requiredOptionItem2 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + Section visibleSection = sectionRepository.save(항상_보이는_섹션(List.of(requiredCheckQuestion.getId()), 1)); + + Question requiredTextQuestion = questionRepository.save(서술형_필수_질문()); + Question conditionalCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup conditionalOptionGroup = optionGroupRepository.save(선택지_그룹(conditionalCheckQuestion.getId())); + OptionItem conditionalOptionItem1 = optionItemRepository.save(선택지(conditionalOptionGroup.getId())); + OptionItem conditionalOptionItem2 = optionItemRepository.save(선택지(conditionalOptionGroup.getId())); + Section conditionalSection = sectionRepository.save(조건부로_보이는_섹션( + List.of(requiredCheckQuestion.getId(), requiredTextQuestion.getId(), conditionalCheckQuestion.getId()), + requiredOptionItem1.getId(), 2) + ); + + Question optionalTextQuestion = questionRepository.save(서술형_옵션_질문()); + Section visibleOptionalSection = sectionRepository.save(항상_보이는_섹션( + List.of(optionalTextQuestion.getId()), 3) + ); + + Template template = templateRepository.save(템플릿( + List.of(visibleSection.getId(), conditionalSection.getId(), visibleOptionalSection.getId()))); + + ReviewAnswerRequest requiredCheckQuestionAnswer = new ReviewAnswerRequest( + requiredCheckQuestion.getId(), List.of(requiredOptionItem1.getId()), null); + ReviewAnswerRequest requiredTextQuestionAnswer = new ReviewAnswerRequest( + requiredTextQuestion.getId(), null, "답변".repeat(30)); + ReviewAnswerRequest conditionalCheckQuestionAnswer = new ReviewAnswerRequest( + conditionalCheckQuestion.getId(), List.of(conditionalOptionItem1.getId()), null); + ReviewAnswerRequest optionalTextQuestionAnswer = new ReviewAnswerRequest( + optionalTextQuestion.getId(), null, ""); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), + List.of(requiredCheckQuestionAnswer, requiredTextQuestionAnswer, conditionalCheckQuestionAnswer, + optionalTextQuestionAnswer)); + + // when + long registeredReviewId = reviewRegisterService.registerReview(reviewRegisterRequest); + + // when, then + Review review = reviewRepository.findById(registeredReviewId).orElseThrow(); + assertAll( + () -> assertThat(review.getTextAnswers()).extracting(TextAnswer::getQuestionId) + .containsExactly(requiredTextQuestion.getId()), + () -> assertThat(review.getCheckboxAnswers()).extracting(CheckboxAnswer::getQuestionId) + .containsAll(List.of(requiredCheckQuestion.getId(), conditionalCheckQuestion.getId())) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java deleted file mode 100644 index f5acf1634..000000000 --- a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package reviewme.review.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import reviewme.question.domain.OptionGroup; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.CheckboxAnswer; -import reviewme.review.domain.Review; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.repository.CheckboxAnswerRepository; -import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; -import reviewme.review.service.exception.ReviewGroupUnauthorizedException; -import reviewme.reviewgroup.domain.ReviewGroup; -import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.support.ServiceTest; -import reviewme.template.domain.Template; -import reviewme.template.repository.SectionRepository; -import reviewme.template.repository.TemplateRepository; - -@ServiceTest -class ReviewServiceTest { - - @Autowired - ReviewService reviewService; - - @Autowired - QuestionRepository questionRepository; - - @Autowired - ReviewGroupRepository reviewGroupRepository; - - @Autowired - OptionItemRepository optionItemRepository; - - @Autowired - OptionGroupRepository optionGroupRepository; - - @Autowired - SectionRepository sectionRepository; - - @Autowired - TemplateRepository templateRepository; - - @Autowired - CheckboxAnswerRepository checkboxAnswerRepository; - - @Autowired - ReviewRepository reviewRepository; - - @Test - void 리뷰_요청_코드가_존재하지_않는_경우_예외가_발생한다() { - assertThatThrownBy(() -> reviewService.findReceivedReviews("abc", "groupAccessCode")) - .isInstanceOf(ReviewGroupNotFoundByReviewRequestCodeException.class); - } - - @Test - void 그룹_액세스_코드가_일치하지_않는_경우_예외가_발생한다() { - // given - String reviewRequestCode = "code"; - String groupAccessCode = "1234"; - reviewGroupRepository.save(new ReviewGroup("커비", "리뷰미", reviewRequestCode, groupAccessCode)); - - // when, then - assertThatThrownBy(() -> reviewService.findReceivedReviews(reviewRequestCode, "5678")) - .isInstanceOf(ReviewGroupUnauthorizedException.class); - } - - @Test - void 확인_코드에_해당하는_그룹이_존재하면_리뷰_리스트를_반환한다() { - // given - String reviewRequestCode = "reviewRequestCode"; - String groupAccessCode = "groupAccessCode"; - Question question = questionRepository.save( - new Question(true, QuestionType.CHECKBOX, "프로젝트 기간 동안, 팀원의 강점이 드러났던 순간을 선택해주세요. (1~2개)", null, 1) - ); - OptionGroup categoryOptionGroup = optionGroupRepository.save(new OptionGroup(question.getId(), 1, 2)); - OptionItem categoryOption1 = new OptionItem("커뮤니케이션 능력 ", categoryOptionGroup.getId(), 1, OptionType.CATEGORY); - OptionItem categoryOption2 = new OptionItem("시간 관리 능력", categoryOptionGroup.getId(), 2, OptionType.CATEGORY); - optionItemRepository.saveAll(List.of(categoryOption1, categoryOption2)); - - Template template = templateRepository.save(new Template(List.of())); - - ReviewGroup reviewGroup = reviewGroupRepository.save( - new ReviewGroup("커비", "리뷰미", reviewRequestCode, groupAccessCode) - ); - CheckboxAnswer categoryAnswer1 = new CheckboxAnswer(question.getId(), List.of(categoryOption1.getId())); - CheckboxAnswer categoryAnswer2 = new CheckboxAnswer(question.getId(), List.of(categoryOption2.getId())); - Review review1 = new Review(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer1)); - Review review = new Review(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer2)); - reviewRepository.saveAll(List.of(review1, review)); - - // when - ReceivedReviewsResponse response = reviewService.findReceivedReviews(reviewRequestCode, groupAccessCode); - - // then - assertThat(response.reviews()).hasSize(2); - } -} diff --git a/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperTest.java new file mode 100644 index 000000000..f9557dde3 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperTest.java @@ -0,0 +1,85 @@ +package reviewme.review.service.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.review.domain.CheckBoxAnswerSelectedOption; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; +import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; +import reviewme.support.ServiceTest; + +@ServiceTest +class AnswerMapperTest { + + @Autowired + private AnswerMapper answerMapper; + + @Test + void 답변_요청을_서술형_답변으로_매핑한다() { + // given + long questionId = 1L; + String text = "답변"; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, null, text); + + // when + TextAnswer textAnswer = answerMapper.mapToTextAnswer(answerRequest); + + // then + assertAll( + () -> assertThat(textAnswer.getQuestionId()).isEqualTo(questionId), + () -> assertThat(textAnswer.getContent()).isEqualTo(text) + ); + } + + @Test + void 답변_요청을_선택형_답변으로_매핑한다() { + // given + long questionId = 1L; + long selectedOptionsId = 2L; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, List.of(selectedOptionsId), null); + + // when + CheckboxAnswer checkboxAnswer = answerMapper.mapToCheckBoxAnswer(answerRequest); + + // then + assertAll( + () -> assertThat(checkboxAnswer.getQuestionId()).isEqualTo(questionId), + () -> assertThat(checkboxAnswer.getSelectedOptionIds()) + .extracting(CheckBoxAnswerSelectedOption::getSelectedOptionId) + .containsOnly(selectedOptionsId) + ); + } + + @Test + void 서술형_답변_매핑시_선택형_답변이_존재할_경우_예외가_발생한다() { + // given + long questionId = 1L; + String text = "답변"; + long selectedOptionsId = 2L; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, List.of(selectedOptionsId), text); + + // when, then + assertThatThrownBy(() -> answerMapper.mapToTextAnswer(answerRequest)) + .isInstanceOf(TextAnswerIncludedOptionItemException.class); + } + + @Test + void 선택형_답변_매핑시_서술형_답변이_존재할_경우_예외가_발생한다() { + // given + long questionId = 1L; + String text = "답변"; + long selectedOptionsId = 2L; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, List.of(selectedOptionsId), text); + + // when, then + assertThatThrownBy(() -> answerMapper.mapToCheckBoxAnswer(answerRequest)) + .isInstanceOf(CheckBoxAnswerIncludedTextException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java new file mode 100644 index 000000000..a9237d211 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java @@ -0,0 +1,90 @@ +package reviewme.review.service.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewListMapperTest { + + @Autowired + private ReviewListMapper reviewListMapper; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Test + void 리뷰_그룹에_있는_리뷰를_반환한다() { + // given - 리뷰 그룹 + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // given - 질문 저장 + Question question = questionRepository.save(선택형_필수_질문()); + + // given - 섹션, 템플릿 저장 + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + // given - 리뷰 답변 저장 + TextAnswer textAnswer = new TextAnswer(question.getId(), "텍스트형 응답"); + Review review1 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review2 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review3 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review4 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review5 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review6 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review7 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review8 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review9 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + Review review10 = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), List.of()); + reviewRepository.saveAll( + List.of(review1, review2, review3, review4, review5, review6, review7, review8, review9, review10)); + + long lastReviewId = 8L; + int size = 5; + + // when + List responses = reviewListMapper.mapToReviewList( + reviewGroup, lastReviewId, size); + + // then + assertAll( + () -> assertThat(responses).hasSize(size), + () -> assertThat(responses).extracting(ReviewListElementResponse::reviewId) + .containsExactly( + review7.getId(), review6.getId(), review5.getId(), review4.getId(), review3.getId()) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java new file mode 100644 index 000000000..8566e8c20 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java @@ -0,0 +1,173 @@ +package reviewme.review.service.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.QuestionFixture.선택형_옵션_질문; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewMapperTest { + + @Autowired + private ReviewMapper reviewMapper; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Test + void 텍스트가_포함된_리뷰를_생성한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + String expectedTextAnswer = "답".repeat(20); + ReviewAnswerRequest reviewAnswerRequest = new ReviewAnswerRequest(question.getId(), null, expectedTextAnswer); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), + List.of(reviewAnswerRequest)); + + // when + Review review = reviewMapper.mapToReview(reviewRegisterRequest); + + // then + assertThat(review.getTextAnswers()).hasSize(1); + } + + @Test + void 체크박스가_포함된_리뷰를_생성한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question question = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question.getId())); + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup.getId())); + + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewAnswerRequest reviewAnswerRequest = new ReviewAnswerRequest(question.getId(), + List.of(optionItem1.getId()), null); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), + List.of(reviewAnswerRequest)); + + // when + Review review = reviewMapper.mapToReview(reviewRegisterRequest); + + // then + assertThat(review.getCheckboxAnswers()).hasSize(1); + } + + @Test + void 필수가_아닌_질문에_답변이_없을_경우_답변을_생성하지_않는다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question requiredTextQuestion = questionRepository.save(서술형_필수_질문()); + Question optionalTextQuestion = questionRepository.save(서술형_옵션_질문()); + + Question requeiredCheckBoxQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup1 = optionGroupRepository.save(선택지_그룹(requeiredCheckBoxQuestion.getId())); + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup1.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup1.getId())); + + Question optionalCheckBoxQuestion = questionRepository.save(선택형_옵션_질문()); + OptionGroup optionGroup2 = optionGroupRepository.save(선택지_그룹(optionalCheckBoxQuestion.getId())); + OptionItem optionItem3 = optionItemRepository.save(선택지(optionGroup2.getId())); + OptionItem optionItem4 = optionItemRepository.save(선택지(optionGroup2.getId())); + + Section section = sectionRepository.save(항상_보이는_섹션( + List.of(requiredTextQuestion.getId(), optionalTextQuestion.getId(), + requeiredCheckBoxQuestion.getId(), optionalCheckBoxQuestion.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + String textAnswer = "답".repeat(20); + ReviewAnswerRequest requiredTextAnswerRequest = new ReviewAnswerRequest( + requiredTextQuestion.getId(), null, textAnswer); + ReviewAnswerRequest optionalTextAnswerRequest = new ReviewAnswerRequest( + optionalTextQuestion.getId(), null, ""); + ReviewAnswerRequest requiredCheckBoxAnswerRequest = new ReviewAnswerRequest( + requeiredCheckBoxQuestion.getId(), List.of(optionItem1.getId()), null); + ReviewAnswerRequest optionalCheckBoxAnswerRequest = new ReviewAnswerRequest( + optionalCheckBoxQuestion.getId(), List.of(), null); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), + List.of(requiredTextAnswerRequest, optionalTextAnswerRequest, + requiredCheckBoxAnswerRequest, optionalCheckBoxAnswerRequest)); + + // when + Review review = reviewMapper.mapToReview(reviewRegisterRequest); + + // then + assertAll( + () -> assertThat(review.getTextAnswers()) + .extracting(TextAnswer::getQuestionId) + .containsExactly(requiredTextQuestion.getId()), + () -> assertThat(review.getCheckboxAnswers()) + .extracting(CheckboxAnswer::getQuestionId) + .containsExactly(requeiredCheckBoxQuestion.getId()) + ); + } + + @Test + void 잘못된_리뷰_요청_코드로_리뷰를_생성할_경우_예외가_발생한다() { + // given + String reviewRequestCode = "notExistCode"; + Question savedQuestion = questionRepository.save(서술형_필수_질문()); + ReviewAnswerRequest emptyTextReviewRequest = new ReviewAnswerRequest( + savedQuestion.getId(), null, ""); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest( + reviewRequestCode, List.of(emptyTextReviewRequest)); + + // when, then + assertThatThrownBy(() -> reviewMapper.mapToReview( + reviewRegisterRequest)) + .isInstanceOf(ReviewGroupNotFoundByReviewRequestCodeException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/validator/CheckBoxAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/CheckBoxAnswerValidatorTest.java new file mode 100644 index 000000000..d3aa2798f --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/validator/CheckBoxAnswerValidatorTest.java @@ -0,0 +1,109 @@ +package reviewme.review.service.validator; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.service.exception.OptionGroupNotFoundByQuestionIdException; +import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; +import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; +import reviewme.support.ServiceTest; + +@ServiceTest +class CheckBoxAnswerValidatorTest { + + @Autowired + private CheckBoxAnswerValidator checkBoxAnswerValidator; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Test + void 저장되지_않은_질문에_대한_답변이면_예외가_발생한다() { + // given + long notSavedQuestionId = 100L; + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(notSavedQuestionId, List.of(1L)); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(SubmittedQuestionNotFoundException.class); + } + + @Test + void 옵션_그룹이_지정되지_않은_질문에_대한_답변이면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(savedQuestion.getId(), List.of(1L)); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(OptionGroupNotFoundByQuestionIdException.class); + } + + @Test + void 옵션그룹에서_제공하지_않은_옵션아이템을_응답하면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup savedOptionGroup = optionGroupRepository.save(선택지_그룹(savedQuestion.getId())); + OptionItem savedOptionItem = optionItemRepository.save(선택지(savedOptionGroup.getId())); + + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(savedQuestion.getId(), + List.of(savedOptionItem.getId() + 1L)); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(CheckBoxAnswerIncludedNotProvidedOptionItemException.class); + } + + @Test + void 옵션그룹에서_정한_최소_선택_수_보다_적게_선택하면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup savedOptionGroup = optionGroupRepository.save( + new OptionGroup(savedQuestion.getId(), 2, 3) + ); + OptionItem savedOptionItem1 = optionItemRepository.save(선택지(savedOptionGroup.getId())); + + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(savedQuestion.getId(), List.of(savedOptionItem1.getId())); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); + } + + @Test + void 옵션그룹에서_정한_최대_선택_수_보다_많이_선택하면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup savedOptionGroup = optionGroupRepository.save( + new OptionGroup(savedQuestion.getId(), 1, 1) + ); + OptionItem savedOptionItem1 = optionItemRepository.save(선택지(savedOptionGroup.getId(), 1)); + OptionItem savedOptionItem2 = optionItemRepository.save(선택지(savedOptionGroup.getId(), 2)); + + CheckboxAnswer checkboxAnswer = new CheckboxAnswer( + savedQuestion.getId(), List.of(savedOptionItem1.getId(), savedOptionItem2.getId())); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/validator/ReviewValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/ReviewValidatorTest.java new file mode 100644 index 000000000..ef3c54a1b --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/validator/ReviewValidatorTest.java @@ -0,0 +1,154 @@ +package reviewme.review.service.validator; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.조건부로_보이는_섹션; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.exception.MissingRequiredQuestionException; +import reviewme.review.service.exception.SubmittedQuestionAndProvidedQuestionMismatchException; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewValidatorTest { + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private ReviewValidator reviewValidator; + + @Test + void 템플릿에_있는_질문에_대한_답과_필수_질문에_모두_응답하는_경우_예외가_발생하지_않는다() { + // 리뷰 그룹 저장 + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // 필수가 아닌 서술형 질문 저장 + Question notRequiredTextQuestion = questionRepository.save(서술형_옵션_질문()); + Section visibleSection1 = sectionRepository.save(항상_보이는_섹션(List.of(notRequiredTextQuestion.getId()), 1)); + + // 필수 선택형 질문, 섹션 저장 + Question requiredCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup requiredOptionGroup = optionGroupRepository.save(선택지_그룹(requiredCheckQuestion.getId())); + OptionItem requiredOptionItem1 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + OptionItem requiredOptionItem2 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + Section visibleSection2 = sectionRepository.save(항상_보이는_섹션(List.of(requiredCheckQuestion.getId()), 2)); + + // optionItem 선택에 따라서 required 가 달라지는 섹션1 저장 + Question conditionalTextQuestion1 = questionRepository.save(서술형_필수_질문()); + Question conditionalCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup conditionalOptionGroup = optionGroupRepository.save(선택지_그룹(conditionalCheckQuestion.getId())); + OptionItem conditionalOptionItem = optionItemRepository.save(선택지(conditionalOptionGroup.getId())); + Section conditionalSection1 = sectionRepository.save(조건부로_보이는_섹션( + List.of(conditionalTextQuestion1.getId(), conditionalCheckQuestion.getId()), + requiredOptionItem1.getId(), 3) + ); + + // optionItem 선택에 따라서 required 가 달라지는 섹션2 저장 + Question conditionalQuestion2 = questionRepository.save(서술형_필수_질문()); + Section conditionalSection2 = sectionRepository.save(조건부로_보이는_섹션( + List.of(conditionalQuestion2.getId()), requiredOptionItem2.getId(), 3) + ); + + // 템플릿 저장 + Template template = templateRepository.save(템플릿( + List.of(visibleSection1.getId(), visibleSection2.getId(), + conditionalSection1.getId(), conditionalSection2.getId()) + )); + + // 각 질문에 대한 답변 생성 + TextAnswer notRequiredlTextAnswer = new TextAnswer(notRequiredTextQuestion.getId(), "답변".repeat(30)); + CheckboxAnswer alwaysRequiredCheckAnswer = new CheckboxAnswer(requiredCheckQuestion.getId(), + List.of(requiredOptionItem1.getId())); + TextAnswer conditionalTextAnswer1 = new TextAnswer(conditionalTextQuestion1.getId(), "답변".repeat(30)); + CheckboxAnswer conditionalCheckAnswer1 = new CheckboxAnswer(conditionalCheckQuestion.getId(), + List.of(conditionalOptionItem.getId())); + + // 리뷰 생성 + Review review = new Review(template.getId(), reviewGroup.getId(), + List.of(notRequiredlTextAnswer, conditionalTextAnswer1), + List.of(alwaysRequiredCheckAnswer, conditionalCheckAnswer1)); + + // when, then + assertThatCode(() -> reviewValidator.validate(review)) + .doesNotThrowAnyException(); + } + + @Test + void 제공된_템플릿에_없는_질문에_대한_답변이_있을_경우_예외가_발생한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + TextAnswer textAnswer = new TextAnswer(question2.getId(), "답변".repeat(20)); + Review review = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), new ArrayList<>()); + + // when, then + assertThatThrownBy(() -> reviewValidator.validate(review)) + .isInstanceOf(SubmittedQuestionAndProvidedQuestionMismatchException.class); + } + + @Test + void 필수_질문에_답변하지_않은_경우_예외가_발생한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question requiredQuestion = questionRepository.save(서술형_필수_질문()); + Question optionalQuestion = questionRepository.save(서술형_옵션_질문()); + Section section = sectionRepository.save( + 항상_보이는_섹션(List.of(requiredQuestion.getId(), optionalQuestion.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + TextAnswer optionalTextAnswer = new TextAnswer(optionalQuestion.getId(), "답변".repeat(20)); + Review review = new Review(template.getId(), reviewGroup.getId(), List.of(optionalTextAnswer), List.of()); + + // when, then + assertThatThrownBy(() -> reviewValidator.validate(review)) + .isInstanceOf(MissingRequiredQuestionException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java new file mode 100644 index 000000000..e9a28db91 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java @@ -0,0 +1,74 @@ +package reviewme.review.service.validator; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.exception.InvalidTextAnswerLengthException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; +import reviewme.support.ServiceTest; + +@ServiceTest +class TextAnswerValidatorTest { + + @Autowired + private TextAnswerValidator textAnswerValidator; + + @Autowired + private QuestionRepository questionRepository; + + @Test + void 저장되지_않은_질문에_대한_대답이면_예외가_발생한다() { + // given + long notSavedQuestionId = 100L; + TextAnswer textAnswer = new TextAnswer(notSavedQuestionId, "텍스트형 응답"); + + // when, then + assertThatCode(() -> textAnswerValidator.validate(textAnswer)) + .isInstanceOf(SubmittedQuestionNotFoundException.class); + } + + @ParameterizedTest + @ValueSource(ints = {19, 10001}) + void 필수_질문의_답변_길이가_유효하지_않으면_예외가_발생한다(int length) { + // given + String content = "답".repeat(length); + Question savedQuestion = questionRepository.save(서술형_필수_질문()); + TextAnswer textAnswer = new TextAnswer(savedQuestion.getId(), content); + + // when, then + assertThatThrownBy(() -> textAnswerValidator.validate(textAnswer)) + .isInstanceOf(InvalidTextAnswerLengthException.class); + } + + @Test + void 선택_질문의_답변_길이가_유효하지_않으면_예외가_발생한다() { + // given + String content = "답".repeat(10001); + Question savedQuestion = questionRepository.save(서술형_옵션_질문()); + TextAnswer textAnswer = new TextAnswer(savedQuestion.getId(), content); + + // when, then + assertThatThrownBy(() -> textAnswerValidator.validate(textAnswer)) + .isInstanceOf(InvalidTextAnswerLengthException.class); + } + + @Test + void 선택_질문은_최소_글자수_제한을_받지_않는다() { + // given + String content = "답".repeat(1); + Question savedQuestion = questionRepository.save(서술형_옵션_질문()); + TextAnswer textAnswer = new TextAnswer(savedQuestion.getId(), content); + + // when, then + assertThatCode(() -> textAnswerValidator.validate(textAnswer)).doesNotThrowAnyException(); + } +} diff --git a/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupLookupServiceTest.java b/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupLookupServiceTest.java index 6764e8586..1e905dc95 100644 --- a/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupLookupServiceTest.java +++ b/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupLookupServiceTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; diff --git a/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupServiceTest.java b/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupServiceTest.java index d7f693de8..d1c5a54e5 100644 --- a/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupServiceTest.java +++ b/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupServiceTest.java @@ -1,21 +1,23 @@ package reviewme.reviewgroup.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; -import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.review.service.exception.ReviewGroupUnauthorizedException; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; -import reviewme.reviewgroup.service.dto.CheckValidAccessResponse; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; import reviewme.support.ServiceTest; @@ -36,7 +38,7 @@ class ReviewGroupServiceTest { @Test void 코드가_중복되는_경우_다시_생성한다() { // given - reviewGroupRepository.save(new ReviewGroup("reviewee", "project", "0000", "1111")); + reviewGroupRepository.save(리뷰_그룹("0000", "1111")); given(randomCodeGenerator.generate(anyInt())) .willReturn("0000") // ReviewRequestCode .willReturn("AAAA"); @@ -56,19 +58,16 @@ class ReviewGroupServiceTest { // given String reviewRequestCode = "reviewRequestCode"; String groupAccessCode = "groupAccessCode"; - reviewGroupRepository.save(new ReviewGroup("reviewee", "project", reviewRequestCode, groupAccessCode)); + reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); CheckValidAccessRequest request = new CheckValidAccessRequest(reviewRequestCode, groupAccessCode); CheckValidAccessRequest wrongRequest = new CheckValidAccessRequest(reviewRequestCode, groupAccessCode + "!"); // when - CheckValidAccessResponse expected1 = reviewGroupService.checkGroupAccessCode(request); - CheckValidAccessResponse expected2 = reviewGroupService.checkGroupAccessCode(wrongRequest); - - // then assertAll( - () -> assertThat(expected1.hasAccess()).isTrue(), - () -> assertThat(expected2.hasAccess()).isFalse() + () -> assertDoesNotThrow(() -> reviewGroupService.checkGroupAccessCode(request)), + () -> assertThatThrownBy(() -> reviewGroupService.checkGroupAccessCode(wrongRequest)) + .isInstanceOf(ReviewGroupUnauthorizedException.class) ); } } diff --git a/backend/src/test/java/reviewme/support/DatabaseCleaner.java b/backend/src/test/java/reviewme/support/DatabaseCleaner.java index b90427980..50801b02f 100644 --- a/backend/src/test/java/reviewme/support/DatabaseCleaner.java +++ b/backend/src/test/java/reviewme/support/DatabaseCleaner.java @@ -5,10 +5,10 @@ import jakarta.persistence.PersistenceContext; import jakarta.persistence.Table; import jakarta.persistence.metamodel.EntityType; -import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.springframework.transaction.annotation.Transactional; public class DatabaseCleaner { diff --git a/backend/src/test/java/reviewme/template/domain/SectionTest.java b/backend/src/test/java/reviewme/template/domain/SectionTest.java index af307e7bd..0e3f1339b 100644 --- a/backend/src/test/java/reviewme/template/domain/SectionTest.java +++ b/backend/src/test/java/reviewme/template/domain/SectionTest.java @@ -1,6 +1,8 @@ package reviewme.template.domain; import static org.assertj.core.api.Assertions.assertThat; +import static reviewme.fixture.SectionFixture.조건부로_보이는_섹션; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; import java.util.List; import org.junit.jupiter.api.Test; @@ -10,10 +12,15 @@ class SectionTest { @Test void 조건_옵션을_선택하면_섹션이_보인다() { // given - Section section = new Section(VisibleType.CONDITIONAL, List.of(), 1L, "섹션명", "말머리", 1); + List questionIds = List.of(1L); + long optionId1 = 1L; + long optionId2 = 2L; + long optionId3 = 3L; + + Section section = 조건부로_보이는_섹션(questionIds, optionId2); // when - boolean actual = section.isVisibleBySelectedOptionIds(List.of(1L, 2L, 3L)); + boolean actual = section.isVisibleBySelectedOptionIds(List.of(optionId1, optionId2, optionId3)); // then assertThat(actual).isTrue(); @@ -22,10 +29,15 @@ class SectionTest { @Test void 조건_옵션을_선택하지_않으면_섹션이_보이지_않는다() { // given - Section section = new Section(VisibleType.CONDITIONAL, List.of(), 1L, "섹션명", "말머리", 1); + List questionIds = List.of(1L); + long optionId1 = 1L; + long optionId2 = 2L; + long optionId3 = 3L; + + Section section = 조건부로_보이는_섹션(questionIds, optionId2); // when - boolean actual = section.isVisibleBySelectedOptionIds(List.of(4L, 5L, 6L)); + boolean actual = section.isVisibleBySelectedOptionIds(List.of(optionId1, optionId3)); // then assertThat(actual).isFalse(); @@ -34,7 +46,8 @@ class SectionTest { @Test void 타입이_ALWAYS라면_조건과_상관없이_모두_보인다() { // given - Section section = new Section(VisibleType.ALWAYS, List.of(), null, "섹션명", "말머리", 1); + List questionIds = List.of(1L); + Section section = 항상_보이는_섹션(questionIds); // when boolean actual = section.isVisibleBySelectedOptionIds(List.of()); diff --git a/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java b/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java index 0154c40f7..8bfa41dca 100644 --- a/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java +++ b/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java @@ -1,6 +1,8 @@ package reviewme.template.repository; import static org.assertj.core.api.Assertions.assertThat; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; import java.util.List; import org.junit.jupiter.api.Test; @@ -8,26 +10,27 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import reviewme.template.domain.Section; import reviewme.template.domain.Template; -import reviewme.template.domain.VisibleType; @DataJpaTest class SectionRepositoryTest { @Autowired private SectionRepository sectionRepository; + @Autowired private TemplateRepository templateRepository; @Test void 템플릿_아이디로_섹션을_불러온다() { // given - Section section1 = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "1","말머리", 1)); - Section section2 = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "2","말머리", 1)); - Section section3 = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "3","말머리", 1)); - sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "4","말머리", 1)); - Template template = templateRepository.save( - new Template(List.of(section1.getId(), section2.getId(), section3.getId())) - ); + List questionIds = List.of(1L); + Section section1 = sectionRepository.save(항상_보이는_섹션(questionIds)); + Section section2 = sectionRepository.save(항상_보이는_섹션(questionIds)); + Section section3 = sectionRepository.save(항상_보이는_섹션(questionIds)); + sectionRepository.save(항상_보이는_섹션(questionIds)); + List sectionIds = List.of(section1.getId(), section2.getId(), section3.getId()); + + Template template = templateRepository.save(템플릿(sectionIds)); // when List
actual = sectionRepository.findAllByTemplateId(template.getId()); diff --git a/backend/src/test/java/reviewme/template/service/TemplateMapperTest.java b/backend/src/test/java/reviewme/template/service/TemplateMapperTest.java deleted file mode 100644 index 984df25df..000000000 --- a/backend/src/test/java/reviewme/template/service/TemplateMapperTest.java +++ /dev/null @@ -1,210 +0,0 @@ -package reviewme.template.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import reviewme.question.domain.OptionGroup; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.domain.exception.MissingOptionItemsInOptionGroupException; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; -import reviewme.reviewgroup.domain.ReviewGroup; -import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.support.ServiceTest; -import reviewme.template.domain.Section; -import reviewme.template.domain.Template; -import reviewme.template.domain.VisibleType; -import reviewme.template.domain.exception.SectionInTemplateNotFoundException; -import reviewme.template.repository.SectionRepository; -import reviewme.template.repository.TemplateRepository; -import reviewme.template.service.dto.response.QuestionResponse; -import reviewme.template.service.dto.response.SectionResponse; -import reviewme.template.service.dto.response.TemplateResponse; - -@ServiceTest -class TemplateMapperTest { - - @Autowired - TemplateMapper templateMapper; - - @Autowired - TemplateRepository templateRepository; - - @Autowired - SectionRepository sectionRepository; - - @Autowired - QuestionRepository questionRepository; - - @Autowired - OptionGroupRepository optionGroupRepository; - - @Autowired - OptionItemRepository optionItemRepository; - - @Autowired - ReviewGroupRepository reviewGroupRepository; - - @Test - void 리뷰_그룹과_템플릿으로_템플릿_응답을_매핑한다() { - // given - Question question1 = new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1); - Question question2 = new Question(true, QuestionType.CHECKBOX, "질문", "가이드라인", 1); - questionRepository.saveAll(List.of(question1, question2)); - - OptionGroup optionGroup = new OptionGroup(question2.getId(), 1, 2); - optionGroupRepository.save(optionGroup); - - OptionItem optionItem = new OptionItem("선택지", optionGroup.getId(), 1, OptionType.CATEGORY); - optionItemRepository.save(optionItem); - - Section section1 = new Section(VisibleType.ALWAYS, List.of(question1.getId()), null, "섹션명", "말머리1", 1); - Section section2 = new Section(VisibleType.ALWAYS, List.of(question2.getId()), null, "섹션명", "말머리2", 2); - sectionRepository.saveAll(List.of(section1, section2)); - - Template template = new Template(List.of(section1.getId(), section2.getId())); - templateRepository.save(template); - - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); - - // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); - - // then - assertAll( - () -> assertThat(templateResponse.revieweeName()).isEqualTo(reviewGroup.getReviewee()), - () -> assertThat(templateResponse.projectName()).isEqualTo(reviewGroup.getProjectName()), - () -> assertThat(templateResponse.sections()).hasSize(2), - () -> assertThat(templateResponse.sections().get(0).header()).isEqualTo(section1.getHeader()), - () -> assertThat(templateResponse.sections().get(0).questions()).hasSize(1), - () -> assertThat(templateResponse.sections().get(1).header()).isEqualTo(section2.getHeader()), - () -> assertThat(templateResponse.sections().get(1).questions()).hasSize(1) - ); - } - - @Test - void 섹션의_선택된_옵션이_필요없는_경우_제공하지_않는다() { - // given - Question question = new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1); - questionRepository.save(question); - - Section section = new Section(VisibleType.ALWAYS, List.of(question.getId()), null, "섹션명", "말머리", 1); - sectionRepository.save(section); - - Template template = new Template(List.of(section.getId())); - templateRepository.save(template); - - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); - - // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); - - // then - SectionResponse sectionResponse = templateResponse.sections().get(0); - assertThat(sectionResponse.onSelectedOptionId()).isNull(); - } - - @Test - void 가이드라인이_없는_경우_가이드_라인을_제공하지_않는다() { - // given - Question question = new Question(true, QuestionType.TEXT, "질문", null, 1); - questionRepository.save(question); - - OptionGroup optionGroup = new OptionGroup(question.getId(), 1, 2); - optionGroupRepository.save(optionGroup); - - OptionItem optionItem = new OptionItem("선택지", optionGroup.getId(), 1, OptionType.CATEGORY); - optionItemRepository.save(optionItem); - - Section section = new Section(VisibleType.ALWAYS, List.of(question.getId()), null, "섹션명", "말머리", 1); - sectionRepository.save(section); - - Template template = new Template(List.of(section.getId())); - templateRepository.save(template); - - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); - - // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); - - // then - QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); - assertAll( - () -> assertThat(questionResponse.hasGuideline()).isFalse(), - () -> assertThat(questionResponse.guideline()).isNull() - ); - } - - @Test - void 옵션_그룹이_없는_질문의_경우_옵션_그룹을_제공하지_않는다() { - // given - Question question = new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1); - questionRepository.save(question); - - Section section = new Section(VisibleType.ALWAYS, List.of(question.getId()), null, "섹션명", "말머리", 1); - sectionRepository.save(section); - - Template template = new Template(List.of(section.getId())); - templateRepository.save(template); - - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); - - // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); - - // then - QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); - assertThat(questionResponse.optionGroup()).isNull(); - } - - @Test - void 템플릿_매핑_시_템플릿에_제공할_섹션이_없을_경우_예외가_발생한다() { - // given - Template template = new Template(List.of(1L)); - templateRepository.save(template); - - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); - - // when, then - assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup, template)) - .isInstanceOf(SectionInTemplateNotFoundException.class); - } - - @Test - void 템플릿_매핑_시_옵션_그룹에_해당하는_옵션_아이템이_없을_경우_예외가_발생한다() { - // given - Question question1 = new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1); - Question question2 = new Question(true, QuestionType.CHECKBOX, "질문", "가이드라인", 1); - questionRepository.saveAll(List.of(question1, question2)); - - OptionGroup optionGroup = new OptionGroup(question2.getId(), 1, 2); - optionGroupRepository.save(optionGroup); - - Section section1 = new Section(VisibleType.ALWAYS, List.of(question1.getId()), null, "섹션명", "말머리", 1); - Section section2 = new Section(VisibleType.ALWAYS, List.of(question2.getId()), null, "섹션명", "말머리", 2); - sectionRepository.saveAll(List.of(section1, section2)); - - Template template = new Template(List.of(section1.getId(), section2.getId())); - templateRepository.save(template); - - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); - - // when, then - assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup, template)) - .isInstanceOf(MissingOptionItemsInOptionGroupException.class); - } -} diff --git a/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java b/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java index 8d84d6f82..7c512d99f 100644 --- a/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java +++ b/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java @@ -1,29 +1,29 @@ package reviewme.template.service; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; -import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException; +import reviewme.template.service.exception.TemplateNotFoundByReviewGroupException; @ServiceTest class TemplateServiceTest { @Autowired - TemplateService templateService; + private TemplateService templateService; @Autowired - ReviewGroupRepository reviewGroupRepository; + private ReviewGroupRepository reviewGroupRepository; @Test void 잘못된_리뷰_요청_코드로_리뷰_작성폼을_요청할_경우_예외가_발생한다() { // given - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when, then assertThatThrownBy(() -> templateService.generateReviewForm(reviewGroup.getReviewRequestCode() + " ")) @@ -33,8 +33,7 @@ class TemplateServiceTest { @Test void 리뷰이에게_작성될_리뷰_양식_생성_시_저장된_템플릿이_없을_경우_예외가_발생한다() { // given - ReviewGroup reviewGroup = new ReviewGroup("리뷰이명", "프로젝트명", "reviewRequestCode", "groupAccessCode"); - reviewGroupRepository.save(reviewGroup); + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when, then assertThatThrownBy(() -> templateService.generateReviewForm(reviewGroup.getReviewRequestCode())) diff --git a/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java b/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java new file mode 100644 index 000000000..e1a0f1853 --- /dev/null +++ b/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java @@ -0,0 +1,169 @@ +package reviewme.template.service.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.service.exception.MissingOptionItemsInOptionGroupException; +import reviewme.template.service.exception.SectionInTemplateNotFoundException; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; +import reviewme.template.service.dto.response.QuestionResponse; +import reviewme.template.service.dto.response.SectionResponse; +import reviewme.template.service.dto.response.TemplateResponse; + +@ServiceTest +class TemplateMapperTest { + + @Autowired + private TemplateMapper templateMapper; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Test + void 리뷰_그룹과_템플릿으로_템플릿_응답을_매핑한다() { + // given + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question1.getId())); + optionItemRepository.save(선택지(optionGroup.getId())); + + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Section section2 = sectionRepository.save(항상_보이는_섹션(List.of(question2.getId()))); + + templateRepository.save(템플릿(List.of(section1.getId(), section2.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // when + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + + // then + assertAll( + () -> assertThat(templateResponse.revieweeName()).isEqualTo(reviewGroup.getReviewee()), + () -> assertThat(templateResponse.projectName()).isEqualTo(reviewGroup.getProjectName()), + () -> assertThat(templateResponse.sections()).hasSize(2), + () -> assertThat(templateResponse.sections().get(0).header()).isEqualTo(section1.getHeader()), + () -> assertThat(templateResponse.sections().get(0).questions()).hasSize(1), + () -> assertThat(templateResponse.sections().get(1).header()).isEqualTo(section2.getHeader()), + () -> assertThat(templateResponse.sections().get(1).questions()).hasSize(1) + ); + } + + @Test + void 섹션의_선택된_옵션이_필요없는_경우_제공하지_않는다() { + // given + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // when + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + + // then + SectionResponse sectionResponse = templateResponse.sections().get(0); + assertThat(sectionResponse.onSelectedOptionId()).isNull(); + } + + @Test + void 가이드라인이_없는_경우_가이드_라인을_제공하지_않는다() { + // given + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // when + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + + // then + QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); + assertAll( + () -> assertThat(questionResponse.hasGuideline()).isFalse(), + () -> assertThat(questionResponse.guideline()).isNull() + ); + } + + @Test + void 옵션_그룹이_없는_질문의_경우_옵션_그룹을_제공하지_않는다() { + // given + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // when + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + + // then + QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); + assertThat(questionResponse.optionGroup()).isNull(); + } + + @Test + void 템플릿_매핑_시_템플릿에_제공할_섹션이_없을_경우_예외가_발생한다() { + // given + templateRepository.save(템플릿(List.of(1L))); + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // when, then + assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup)) + .isInstanceOf(SectionInTemplateNotFoundException.class); + } + + @Test + void 템플릿_매핑_시_옵션_그룹에_해당하는_옵션_아이템이_없을_경우_예외가_발생한다() { + // given + Question question = questionRepository.save(선택형_필수_질문()); + optionGroupRepository.save(선택지_그룹(question.getId())); + + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // when, then + assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup)) + .isInstanceOf(MissingOptionItemsInOptionGroupException.class); + } +} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index b941b41ba..0c5a19c1e 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -11,6 +11,8 @@ spring: show-sql: true hibernate: ddl-auto: update + flyway: + enabled: false springdoc: swagger-ui: @@ -32,3 +34,7 @@ logging: pattern: console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx" file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } [%15.15t] %-40.40logger{39} : %m%n%wEx" + +cors: + allowed-origins: + - https://allowed-domain.com diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index b6d283909..dd825e3f4 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { 'jest.polyfills.js', 'jest.setup.js', 'tsconfig.json', + 'svgTransformer.js', ], parser: '@typescript-eslint/parser', plugins: ['react-refresh'], diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 3891f259d..cadb0f0c4 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -10,4 +10,11 @@ module.exports = { testEnvironmentOptions: { customExportConditions: [''], }, + preset: 'ts-jest', + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + '^.+\\.js$': 'babel-jest', + '^.+\\.svg$': '/svgTransform.js', + }, + transformIgnorePatterns: ['/node_modules/'], }; diff --git a/frontend/jest.polyfills.js b/frontend/jest.polyfills.js index 03a705a59..872888baa 100644 --- a/frontend/jest.polyfills.js +++ b/frontend/jest.polyfills.js @@ -1,4 +1,4 @@ -import 'dotenv/config'; +require('dotenv/config'); const { TextDecoder, TextEncoder, ReadableStream } = require('node:util'); Object.defineProperties(globalThis, { diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js index ed5421416..b37498d81 100644 --- a/frontend/jest.setup.js +++ b/frontend/jest.setup.js @@ -1,5 +1,9 @@ import server from './src/mocks/server'; +import { matchers } from '@emotion/jest'; +import '@testing-library/jest-dom'; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); + +expect.extend(matchers); diff --git a/frontend/package.json b/frontend/package.json index 7c1e38e29..a8b4b52d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,10 +4,11 @@ "main": "index.js", "license": "MIT", "scripts": { - "dev": "webpack-dev-server --mode=development --open --hot --progress", - "start": "webpack serve --open --config webpack.config.js", - "build": "webpack --config webpack.config.js", - "serve": "http-server ./dist", + "dev": "webpack serve --mode=development --open --hot --progress", + "prebuild": "svgo -rf ./src/assets -o ./dist ", + "build": "webpack --mode=production --node-env=production", + "serve-dist": "http-server ./dist", + "serve-prod": " webpack serve --mode=production --node-env=production --open", "lint:styles": "stylelint \"src/**/styles.ts\" --fix", "test": "jest" }, @@ -32,6 +33,7 @@ "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", "@emotion/babel-plugin": "^11.11.0", + "@emotion/jest": "^11.13.0", "@stylelint/postcss-css-in-js": "^0.38.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", @@ -42,8 +44,10 @@ "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.16.0", "@typescript-eslint/parser": "^7.16.0", + "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", "clean-webpack-plugin": "^4.0.0", + "compression-webpack-plugin": "^11.1.0", "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -64,12 +68,15 @@ "stylelint": "^16.7.0", "stylelint-config-clean-order": "^6.1.0", "stylelint-order": "^6.0.4", + "svgo": "^3.3.2", "ts-jest": "^29.2.4", "typescript": "^5.5.3", "undici": "5.0.0", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.4" + "webpack-dev-server": "^5.1.0", + "zlib": "^1.0.5" }, "msw": { "workerDirectory": [ diff --git a/frontend/public/index.html b/frontend/public/index.html index ee0cb1c0b..2f05f7266 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -16,12 +16,14 @@ + - - + + + + REVIEW ME diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bb182911e..58a3a8e68 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,28 +1,11 @@ import { Outlet } from 'react-router'; -import { PageLayout, Sidebar, Topbar, SideModal, Footer, Main } from './components'; -import Breadcrumb from './components/common/Breadcrumb'; -import { useSidebar } from './hooks'; -import useBreadcrumbPaths from './hooks/useBreadcrumbPaths'; +import { PageLayout } from './components'; const App = () => { - const { isSidebarHidden, isSidebarModalOpen, closeSidebar, openSidebar } = useSidebar(); - - const breadcrumbPathList = useBreadcrumbPaths(); - return ( - {/* {isSidebarModalOpen && ( - - - - )} */} - - {breadcrumbPathList.length > 1 && } -
1)}> - -
-