From 30340cbd63642840b4826ce2c3c70914985c9f7c Mon Sep 17 00:00:00 2001 From: KIMGYUTAE Date: Sat, 12 Oct 2024 22:40:37 +0900 Subject: [PATCH 01/60] =?UTF-8?q?refactor:=20response=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=8E=81=EC=8A=A4=20=EB=B0=8F=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/detail/OptionGroupAnswerResponse.java | 11 ----------- .../dto/response/detail/OptionItemAnswerResponse.java | 3 +-- .../dto/response/detail/QuestionAnswerResponse.java | 3 ++- .../dto/response/detail/SectionAnswerResponse.java | 4 ---- 4 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 backend/src/main/java/reviewme/review/service/dto/response/detail/OptionGroupAnswerResponse.java diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionGroupAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionGroupAnswerResponse.java deleted file mode 100644 index 894dbaae8..000000000 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionGroupAnswerResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package reviewme.review.service.dto.response.detail; - -import java.util.List; - -public record OptionGroupAnswerResponse( - long optionGroupId, - long minCount, - long maxCount, - List options -) { -} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionItemAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionItemAnswerResponse.java index 6bd424f5f..d5c9ae174 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionItemAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionItemAnswerResponse.java @@ -2,7 +2,6 @@ public record OptionItemAnswerResponse( long optionId, - String content, - boolean isChecked + String content ) { } diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java index 000eb83c8..59203939e 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java @@ -1,6 +1,7 @@ package reviewme.review.service.dto.response.detail; import jakarta.annotation.Nullable; +import java.util.List; import reviewme.question.domain.QuestionType; public record QuestionAnswerResponse( @@ -8,7 +9,7 @@ public record QuestionAnswerResponse( boolean required, QuestionType questionType, String content, - @Nullable OptionGroupAnswerResponse optionGroup, + @Nullable List options, @Nullable String answer ) { } 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 eb45bddb2..ad2887644 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,8 +7,4 @@ public record SectionAnswerResponse( String header, List questions ) { - - public boolean hasAnsweredQuestion() { - return !questions.isEmpty(); - } } From 79e74d6cbad8c81245fea52a7db4ecf5c74d821f Mon Sep 17 00:00:00 2001 From: KIMGYUTAE Date: Sat, 12 Oct 2024 22:44:55 +0900 Subject: [PATCH 02/60] =?UTF-8?q?refactor:=20ReviewDetailMapper=20response?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/service/mapper/ReviewDetailMapper.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java index 7121d99b5..921f5364e 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java @@ -17,7 +17,6 @@ import reviewme.review.domain.CheckboxAnswerSelectedOption; 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; @@ -107,22 +106,15 @@ private QuestionAnswerResponse mapToCheckboxQuestionResponse(Review review, List optionItemResponse = optionItems.stream() .filter(optionItem -> selectedOptionIds.contains(optionItem.getId())) - .map(optionItem -> new OptionItemAnswerResponse(optionItem.getId(), optionItem.getContent(), true)) + .map(optionItem -> new OptionItemAnswerResponse(optionItem.getId(), optionItem.getContent())) .toList(); - OptionGroupAnswerResponse optionGroupAnswerResponse = new OptionGroupAnswerResponse( - optionGroup.getId(), - optionGroup.getMinSelectionCount(), - optionGroup.getMaxSelectionCount(), - optionItemResponse - ); - return new QuestionAnswerResponse( question.getId(), question.isRequired(), question.getQuestionType(), question.getContent(), - optionGroupAnswerResponse, + optionItemResponse, null ); } From 2971e1c416b0a433822ae162b003810e3ae28e36 Mon Sep 17 00:00:00 2001 From: KIMGYUTAE Date: Sat, 12 Oct 2024 22:45:09 +0900 Subject: [PATCH 03/60] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/java/reviewme/api/ReviewApiTest.java | 12 +++-------- .../java/reviewme/api/TemplateFixture.java | 21 ++++++++----------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index 06a39f63b..b4eef16af 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -138,15 +138,9 @@ class ReviewApiTest extends ApiTest { fieldWithPath("sections[].questions[].content").description("질문 내용"), fieldWithPath("sections[].questions[].questionType").description("질문 타입"), - fieldWithPath("sections[].questions[].optionGroup").description("옵션 그룹").optional(), - fieldWithPath("sections[].questions[].optionGroup.optionGroupId").description("옵션 그룹 ID"), - fieldWithPath("sections[].questions[].optionGroup.minCount").description("최소 선택 개수"), - fieldWithPath("sections[].questions[].optionGroup.maxCount").description("최대 선택 개수"), - - fieldWithPath("sections[].questions[].optionGroup.options[]").description("선택 항목 목록"), - fieldWithPath("sections[].questions[].optionGroup.options[].optionId").description("선택 항목 ID"), - fieldWithPath("sections[].questions[].optionGroup.options[].content").description("선택 항목 내용"), - fieldWithPath("sections[].questions[].optionGroup.options[].isChecked").description("선택 여부"), + fieldWithPath("sections[].questions[].options[]").description("선택 항목 목록"), + fieldWithPath("sections[].questions[].options[].optionId").description("선택 항목 ID"), + fieldWithPath("sections[].questions[].options[].content").description("선택 항목 내용"), fieldWithPath("sections[].questions[].answer").description("서술형 답변").optional(), }; diff --git a/backend/src/test/java/reviewme/api/TemplateFixture.java b/backend/src/test/java/reviewme/api/TemplateFixture.java index aba719fbb..5c562ae98 100644 --- a/backend/src/test/java/reviewme/api/TemplateFixture.java +++ b/backend/src/test/java/reviewme/api/TemplateFixture.java @@ -3,11 +3,10 @@ import java.time.LocalDate; import java.util.List; import reviewme.question.domain.QuestionType; -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.ReviewDetailResponse; +import reviewme.review.service.dto.response.detail.SectionAnswerResponse; import reviewme.template.domain.VisibleType; import reviewme.template.service.dto.response.OptionGroupResponse; import reviewme.template.service.dto.response.OptionItemResponse; @@ -75,13 +74,12 @@ public static TemplateResponse templateResponse() { public static ReviewDetailResponse templateAnswerResponse() { // Section 1 List firstOptionAnswers = List.of( - new OptionItemAnswerResponse(1, "커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)", true), - new OptionItemAnswerResponse(2, "문제 해결 능력 (ex: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)", false), - new OptionItemAnswerResponse(3, "시간 관리 능력 (ex: 일정과 마감 기한 준수, 업무의 우선 순위 분배)", false) + new OptionItemAnswerResponse(1, "커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)"), + new OptionItemAnswerResponse(2, "문제 해결 능력 (ex: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)"), + new OptionItemAnswerResponse(3, "시간 관리 능력 (ex: 일정과 마감 기한 준수, 업무의 우선 순위 분배)") ); - OptionGroupAnswerResponse firstOptionGroupAnswer = new OptionGroupAnswerResponse(1, 1, 2, firstOptionAnswers); QuestionAnswerResponse firstQuestionAnswer = new QuestionAnswerResponse( - 1, true, QuestionType.CHECKBOX, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", firstOptionGroupAnswer, null + 1, true, QuestionType.CHECKBOX, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", firstOptionAnswers, null ); SectionAnswerResponse firstSectionAnswer = new SectionAnswerResponse( 1, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", List.of(firstQuestionAnswer) @@ -89,13 +87,12 @@ public static ReviewDetailResponse templateAnswerResponse() { // Section 2 List secondOptionAnswers = List.of( - new OptionItemAnswerResponse(4, "반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요.", true), - new OptionItemAnswerResponse(5, "팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요.", false), - new OptionItemAnswerResponse(6, "팀의 분위기를 주도해요.", true) + new OptionItemAnswerResponse(4, "반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요."), + new OptionItemAnswerResponse(5, "팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요."), + new OptionItemAnswerResponse(6, "팀의 분위기를 주도해요.") ); - OptionGroupAnswerResponse secondOptionGroupAnswer = new OptionGroupAnswerResponse(2, 1, 3, secondOptionAnswers); QuestionAnswerResponse secondQuestionAnswer = new QuestionAnswerResponse( - 2, true, QuestionType.CHECKBOX, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", secondOptionGroupAnswer, + 2, true, QuestionType.CHECKBOX, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", secondOptionAnswers, null ); SectionAnswerResponse secondSectionAnswer = new SectionAnswerResponse( From 0d5b242b15ac82b61d5f5de09b4152e8b65c92da Mon Sep 17 00:00:00 2001 From: KIMGYUTAE Date: Mon, 14 Oct 2024 17:58:47 +0900 Subject: [PATCH 04/60] =?UTF-8?q?refactor:=20dto=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/QuestionAnswerResponse.java | 2 +- .../detail/SectionAnswerResponse.java | 2 +- .../service/mapper/ReviewDetailMapper.java | 2 +- .../test/java/reviewme/api/ReviewApiTest.java | 20 +++++++++---------- .../ReviewDetailLookupServiceTest.java | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java index 59203939e..75985ffad 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java @@ -8,7 +8,7 @@ public record QuestionAnswerResponse( long questionId, boolean required, QuestionType questionType, - String content, + String questionContents, @Nullable List options, @Nullable String answer ) { 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..ec330b76f 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 @@ -5,6 +5,6 @@ public record SectionAnswerResponse( long sectionId, String header, - List questions + List reviews ) { } diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java index 921f5364e..7c3ab4336 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java @@ -52,7 +52,7 @@ public ReviewDetailResponse mapToReviewDetailResponse(Review review, ReviewGroup List sectionResponses = sections.stream() .map(section -> mapToSectionResponse(review, section, questions, optionGroupsByQuestion, optionItemsByOptionGroup)) - .filter(sectionResponse -> !sectionResponse.questions().isEmpty()) + .filter(sectionResponse -> !sectionResponse.reviews().isEmpty()) .toList(); return new ReviewDetailResponse( diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index b4eef16af..a5ebea440 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -132,16 +132,16 @@ class ReviewApiTest extends ApiTest { fieldWithPath("sections[].sectionId").description("섹션 ID"), fieldWithPath("sections[].header").description("섹션 제목"), - fieldWithPath("sections[].questions[]").description("질문 목록"), - fieldWithPath("sections[].questions[].questionId").description("질문 ID"), - fieldWithPath("sections[].questions[].required").description("필수 여부"), - fieldWithPath("sections[].questions[].content").description("질문 내용"), - fieldWithPath("sections[].questions[].questionType").description("질문 타입"), - - fieldWithPath("sections[].questions[].options[]").description("선택 항목 목록"), - fieldWithPath("sections[].questions[].options[].optionId").description("선택 항목 ID"), - fieldWithPath("sections[].questions[].options[].content").description("선택 항목 내용"), - fieldWithPath("sections[].questions[].answer").description("서술형 답변").optional(), + fieldWithPath("sections[].reviews[]").description("리뷰 목록"), + fieldWithPath("sections[].reviews[].questionId").description("질문 ID"), + fieldWithPath("sections[].reviews[].required").description("필수 여부"), + fieldWithPath("sections[].reviews[].questionContents").description("질문 내용"), + fieldWithPath("sections[].reviews[].questionType").description("질문 타입"), + + fieldWithPath("sections[].reviews[].options[]").description("선택 항목 목록"), + fieldWithPath("sections[].reviews[].options[].optionId").description("선택 항목 ID"), + fieldWithPath("sections[].reviews[].options[].content").description("선택 항목 내용"), + fieldWithPath("sections[].reviews[].answer").description("서술형 답변").optional(), }; RestDocumentationResultHandler handler = document( diff --git a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java index 94f281594..6d5dcf861 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java @@ -205,7 +205,7 @@ class NotAnsweredOptionalQuestion { .extracting(SectionAnswerResponse::sectionId) .containsExactly(section.getId()), () -> assertThat(reviewDetail.sections()) - .flatExtracting(SectionAnswerResponse::questions) + .flatExtracting(SectionAnswerResponse::reviews) .extracting(QuestionAnswerResponse::questionId) .containsExactly(question1.getId()) ); From 14f6846c64546dc922396cf7fca26d765620c82e Mon Sep 17 00:00:00 2001 From: KIMGYUTAE Date: Mon, 14 Oct 2024 21:48:58 +0900 Subject: [PATCH 05/60] =?UTF-8?q?docs:=20=EB=A6=AC=EB=B7=B0=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A1=B0=ED=9A=8C=20api=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/java/reviewme/api/ReviewApiTest.java | 1 + .../java/reviewme/api/TemplateFixture.java | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index a5ebea440..8bd604f92 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -138,6 +138,7 @@ class ReviewApiTest extends ApiTest { fieldWithPath("sections[].reviews[].questionContents").description("질문 내용"), fieldWithPath("sections[].reviews[].questionType").description("질문 타입"), + fieldWithPath("sections[].reviews[].options").description("선택 항목 목록").optional(), fieldWithPath("sections[].reviews[].options[]").description("선택 항목 목록"), fieldWithPath("sections[].reviews[].options[].optionId").description("선택 항목 ID"), fieldWithPath("sections[].reviews[].options[].content").description("선택 항목 내용"), diff --git a/backend/src/test/java/reviewme/api/TemplateFixture.java b/backend/src/test/java/reviewme/api/TemplateFixture.java index 5c562ae98..ab6980a3b 100644 --- a/backend/src/test/java/reviewme/api/TemplateFixture.java +++ b/backend/src/test/java/reviewme/api/TemplateFixture.java @@ -73,34 +73,41 @@ public static TemplateResponse templateResponse() { public static ReviewDetailResponse templateAnswerResponse() { // Section 1 - List firstOptionAnswers = List.of( + List optionAnswer = List.of( new OptionItemAnswerResponse(1, "커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)"), new OptionItemAnswerResponse(2, "문제 해결 능력 (ex: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)"), new OptionItemAnswerResponse(3, "시간 관리 능력 (ex: 일정과 마감 기한 준수, 업무의 우선 순위 분배)") ); QuestionAnswerResponse firstQuestionAnswer = new QuestionAnswerResponse( - 1, true, QuestionType.CHECKBOX, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", firstOptionAnswers, null - ); - SectionAnswerResponse firstSectionAnswer = new SectionAnswerResponse( - 1, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", List.of(firstQuestionAnswer) + 1, true, QuestionType.CHECKBOX, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", optionAnswer, null ); - // Section 2 - List secondOptionAnswers = List.of( - new OptionItemAnswerResponse(4, "반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요."), - new OptionItemAnswerResponse(5, "팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요."), - new OptionItemAnswerResponse(6, "팀의 분위기를 주도해요.") - ); - QuestionAnswerResponse secondQuestionAnswer = new QuestionAnswerResponse( - 2, true, QuestionType.CHECKBOX, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", secondOptionAnswers, - null - ); - SectionAnswerResponse secondSectionAnswer = new SectionAnswerResponse( - 2, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", List.of(secondQuestionAnswer) + QuestionAnswerResponse secondQuestionAnswer = new QuestionAnswerResponse(2, true, QuestionType.TEXT, "위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요.", null, "나산초의 답변"); + + SectionAnswerResponse sectionAnswer = new SectionAnswerResponse( + 1, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", List.of(firstQuestionAnswer, secondQuestionAnswer) ); return new ReviewDetailResponse( - 1, "아루", "리뷰미", LocalDate.of(2024, 8, 1), List.of(firstSectionAnswer, secondSectionAnswer) + 1, "아루", "리뷰미", LocalDate.of(2024, 8, 1), List.of(sectionAnswer) ); + + // Section 2 +// List secondOptionAnswers = List.of( +// new OptionItemAnswerResponse(4, "반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요."), +// new OptionItemAnswerResponse(5, "팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요."), +// new OptionItemAnswerResponse(6, "팀의 분위기를 주도해요.") +// ); +// QuestionAnswerResponse secondQuestionAnswer = new QuestionAnswerResponse( +// 2, true, QuestionType.CHECKBOX, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", secondOptionAnswers, +// null +// ); +// SectionAnswerResponse secondSectionAnswer = new SectionAnswerResponse( +// 2, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", List.of(secondQuestionAnswer) +// ); + +// return new ReviewDetailResponse( +// 1, "아루", "리뷰미", LocalDate.of(2024, 8, 1), List.of(sectionAnswer, secondSectionAnswer) +// ); } } From 64359f548a86391336a64dbb7d066b097e65f7d5 Mon Sep 17 00:00:00 2001 From: Fe <64690761+chysis@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:14:55 +0900 Subject: [PATCH 06/60] =?UTF-8?q?design:=20=EC=95=84=EC=BD=94=EB=94=94?= =?UTF-8?q?=EC=96=B8=20header=EC=9D=98=20=EB=AA=A8=EB=93=A0=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=EC=9D=84=20=ED=81=B4=EB=A6=AD=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#939)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Accordion/styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/common/Accordion/styles.ts b/frontend/src/components/common/Accordion/styles.ts index b701f6c60..49a5a4836 100644 --- a/frontend/src/components/common/Accordion/styles.ts +++ b/frontend/src/components/common/Accordion/styles.ts @@ -24,7 +24,6 @@ export const AccordionContainer = styled.div` export const AccordionHeader = styled.div` display: flex; - padding: 1rem; border-bottom: ${({ $isOpened, theme }) => $isOpened && `0.1rem solid ${theme.colors.placeholder}`}; `; @@ -36,7 +35,8 @@ export const AccordionButton = styled.button` width: 100%; height: fit-content; - min-height: 3rem; + min-height: 5rem; + padding: 1rem; `; export const AccordionTitle = styled.p` From 6142cbde0bd6986347948000be6b2bdb09551a1c Mon Sep 17 00:00:00 2001 From: Fe <64690761+chysis@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:23:30 +0900 Subject: [PATCH 07/60] =?UTF-8?q?[All]=20README=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20(#927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * fix: 이미지 정렬 및 변경, 이모지 추가 * fix: 형광펜 이미지 교체 --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c7e1b40ba..7f807fd6a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,61 @@ -# 리뷰미 +

리뷰미 로고

-> 🤔 우리 팀원은 나를 어떻게 생각할까? -> 🫂 나와 팀이 함께 성장하려면 어떻게 해야 할까? -> 🤨 팀원에게 하고 싶은 말이 있는데, 대면으로 하기가 민망하네.. -> 🥹 기능 구현 하기에도 바빠서 문화를 챙길 시간도 없고, 팀원들한테 이런거 하자고 하기도 부담스러워... +🔗[리뷰미 바로가기](https://review-me.page) -저희도 스스로가 팀에서 어떤 존재인지 고민될 때가 있습니다. +# 🔎 리뷰미 -동료의 피드백을 통해 저희는 자신의 강점과 팀에 어떻게 기여할 수 있는지를 알게 되었습니다. -지칠 때 받은 동료의 리뷰가 큰 힘이 되었어요. 팀원 모두가 서로를 응원하니 자연스럽게 팀워크도 향상됐습니다. -리뷰미는 동료로부터 기술뿐만 아니라 소프트 스킬, 나의 특징 등을 다방면으로 리뷰 받을 수 있는 서비스입니다. -리뷰미를 통해 협업하는 내 모습을 알아갈 수 있고, 나아가 함께 성장하는 방식을 고민할 수 있습니다. -어쩌면 내가 몰랐던 내 모습을 발견할 수도 있겠죠? +## 프로젝트 소개 +프로젝트를 함께한 동료들에게 받은 리뷰를 통해 자신이 어떤 개발자인지 파악하고 표현하는 데 도움을 주는 서비스입니다. +기술뿐만 아니라 소프트 스킬, 나의 강점 등을 다방면으로 리뷰 받을 수 있어요. +어쩌면 내가 몰랐던 내 모습을 발견할 수도 있겠죠? -여러분들도 리뷰를 통한 좋은 경험을 해보고 싶으시다면, -리뷰를 통해 누군가에게 응원을 전달하고 싶으시다면, -리뷰미와 함께하세요! +## 리뷰미가 세상에 나온 이유✨ +> 🤔 나는 무엇을 잘하는 개발자일까? +📚 어떤 점을 보완하면 내가 더 성장할 수 있을까? +🫂 우리 팀원은 나를 어떻게 생각할까? + +프로젝트를 하다보면 이런 고민이 들 때가 있지 않나요? +우리는 이 고민의 답을 `동료들의 피드백`에서 찾았어요. +동료들과 피드백을 주고받으며 `내가 팀에서 어떤 사람`이었고 `무엇을 잘하는지` 알 수 있었기 때문이에요. + +그렇게 동료들과 피드백을 주고받을 수 있는 서비스, `리뷰미`가 탄생하였습니다. + +## 주요 기능 소개 + +### 리뷰를 작성해보세요 +뭐라고 리뷰를 써야할지 막막한가요? 리뷰미를 통해 그 때의 기억을 떠올리며 리뷰를 작성해보세요. + +

+ +### 리뷰를 확인해보세요 +팀원들이 보는 내 모습은 어땠을까요? 작성한 리뷰를 확인해보세요! + +

+ +### 리뷰로 나를 파악해보세요 +받은 리뷰를 모아보고, 나를 파악하는데 도움이 된 부분을 형광펜으로 표시할 수 있어요. +

+ +## 😮 리뷰미 서비스 사용 후기 +

+ + +## ⚙️ 기술 스택 +### 프론트엔드 +

+ +### 백엔드 +

+ +## 🧑‍💻 팀원 소개 + +### 프론트엔드 +| bada | soosoo | fe | ollie | +| :---: | :---: | :---: | :---: | +| [🐋 바다](https://github.com/badahertz52) | [😍 쑤쑤](https://github.com/soosoo22) | [🔥 에프이](https://github.com/chysis) | [👾 올리](https://github.com/ImxYJL) | + +### 백엔드 +| | | | | +| :---: | :---: | :---: | :---: | +| [🦧 산초](https://github.com/nayonsoso) | [🤸🏻‍♂️ 아루](https://github.com/donghoony) | [💃 커비](https://github.com/skylar1220) | [🐻 테드](https://github.com/Kimprodp) | From afa104f4df9dc88a8a24448f6966f0a4acf32465 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 25 Oct 2024 10:50:35 +0900 Subject: [PATCH 08/60] =?UTF-8?q?[BE]=20cd-fix:=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=8B=9C=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20pull=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#947)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 새로운 이미지 다운 명령어 추가 * chore: 새로운 이미지 다운 명령어 위치 변경 - shell script 가 아니라 github actions 으로 이동 --- .github/workflows/backend-dev-cd.yml | 2 +- .github/workflows/backend-prod-cd.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend-dev-cd.yml b/.github/workflows/backend-dev-cd.yml index d0d2c39b2..7d7459d62 100644 --- a/.github/workflows/backend-dev-cd.yml +++ b/.github/workflows/backend-dev-cd.yml @@ -81,7 +81,7 @@ jobs: env: PROFILE_VAR: "dev" run: | + sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:$PROFILE_VAR chmod +x ./deploy.sh sudo -E ./deploy.sh - working-directory: ${{ env.APPLICATION_DIRECTORY }}/app diff --git a/.github/workflows/backend-prod-cd.yml b/.github/workflows/backend-prod-cd.yml index c010d96e3..158d3e308 100644 --- a/.github/workflows/backend-prod-cd.yml +++ b/.github/workflows/backend-prod-cd.yml @@ -79,6 +79,7 @@ jobs: env: PROFILE_VAR: "prod" run: | + sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:$PROFILE_VAR chmod +x ./deploy.sh sudo -E ./deploy.sh working-directory: ${{ env.APPLICATION_DIRECTORY }}/app From 253ec8ace3ca44df3b9d9237e229b2eafde01c06 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 25 Oct 2024 10:59:14 +0900 Subject: [PATCH 09/60] =?UTF-8?q?chore:=20=EB=8F=84=EC=BB=A4=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A7=80=EC=A0=95=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#948)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 도커 태그를 하드코딩으로 설정했다. - profile 과 docker tag 가 다른 이슈가 있었다. --- .github/workflows/backend-dev-cd.yml | 2 +- .github/workflows/backend-prod-cd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend-dev-cd.yml b/.github/workflows/backend-dev-cd.yml index 7d7459d62..eec962f53 100644 --- a/.github/workflows/backend-dev-cd.yml +++ b/.github/workflows/backend-dev-cd.yml @@ -81,7 +81,7 @@ jobs: env: PROFILE_VAR: "dev" run: | - sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:$PROFILE_VAR + sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:develop chmod +x ./deploy.sh sudo -E ./deploy.sh working-directory: ${{ env.APPLICATION_DIRECTORY }}/app diff --git a/.github/workflows/backend-prod-cd.yml b/.github/workflows/backend-prod-cd.yml index 158d3e308..41050eb22 100644 --- a/.github/workflows/backend-prod-cd.yml +++ b/.github/workflows/backend-prod-cd.yml @@ -79,7 +79,7 @@ jobs: env: PROFILE_VAR: "prod" run: | - sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:$PROFILE_VAR + sudo docker pull ${{ secrets.DOCKERHUB_ID }}/review-me-app:release chmod +x ./deploy.sh sudo -E ./deploy.sh working-directory: ${{ env.APPLICATION_DIRECTORY }}/app From af5c0202ee2ab54741070df7c48b99983f5d83a0 Mon Sep 17 00:00:00 2001 From: Hyeonji <110809927+skylar1220@users.noreply.github.com> Date: Sun, 27 Oct 2024 15:32:27 +0900 Subject: [PATCH 10/60] =?UTF-8?q?[BE]=20=EC=98=88=EC=99=B8=EC=9D=98=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=EB=90=9C=20=EB=A1=9C=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#951)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 로그 레벨 info로 조정 * refactor: 사용하지 않는 예외 삭제 --- .../HighlightStartIndexExceedEndIndexException.java | 13 ------------- .../MissingTextAnswerForQuestionException.java | 13 ------------- .../exception/AnswerNotFoundByIdException.java | 13 ------------- .../exception/InvalidTextAnswerLengthException.java | 2 +- ...uestionAndProvidedQuestionMismatchException.java | 6 ------ 5 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 backend/src/main/java/reviewme/highlight/domain/exception/HighlightStartIndexExceedEndIndexException.java delete mode 100644 backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java delete mode 100644 backend/src/main/java/reviewme/review/service/exception/AnswerNotFoundByIdException.java diff --git a/backend/src/main/java/reviewme/highlight/domain/exception/HighlightStartIndexExceedEndIndexException.java b/backend/src/main/java/reviewme/highlight/domain/exception/HighlightStartIndexExceedEndIndexException.java deleted file mode 100644 index 38c99ac9a..000000000 --- a/backend/src/main/java/reviewme/highlight/domain/exception/HighlightStartIndexExceedEndIndexException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.highlight.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.BadRequestException; - -@Slf4j -public class HighlightStartIndexExceedEndIndexException extends BadRequestException { - - public HighlightStartIndexExceedEndIndexException(int startIndex, int endIndex) { - super("하이라이트 끝 위치는 시작 위치보다 같거나 커야 해요."); - log.info("Highlight start index exceed end index - startIndex: {}, endIndex: {}", startIndex, endIndex); - } -} diff --git a/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java b/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java deleted file mode 100644 index 674dce41c..000000000 --- a/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.domain.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.DataInconsistencyException; - -@Slf4j -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, this); - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/AnswerNotFoundByIdException.java b/backend/src/main/java/reviewme/review/service/exception/AnswerNotFoundByIdException.java deleted file mode 100644 index aef381ffc..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/AnswerNotFoundByIdException.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 AnswerNotFoundByIdException extends NotFoundException { - - public AnswerNotFoundByIdException(long answerId) { - super("답변을 찾을 수 없어요."); - log.info("Answer not found by id - answerId: {}", answerId); - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/InvalidTextAnswerLengthException.java b/backend/src/main/java/reviewme/review/service/exception/InvalidTextAnswerLengthException.java index 01c02ceb7..314f72673 100644 --- a/backend/src/main/java/reviewme/review/service/exception/InvalidTextAnswerLengthException.java +++ b/backend/src/main/java/reviewme/review/service/exception/InvalidTextAnswerLengthException.java @@ -8,7 +8,7 @@ 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: {}", + log.info("AnswerLength is out of bound - questionId: {}, answerLength: {}, minLength: {}, maxLength: {}", questionId, answerLength, minLength, maxLength, this); } 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 1924b1cf5..97b0f77d9 100644 --- a/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionAndProvidedQuestionMismatchException.java +++ b/backend/src/main/java/reviewme/review/service/exception/SubmittedQuestionAndProvidedQuestionMismatchException.java @@ -1,7 +1,6 @@ package reviewme.review.service.exception; import java.util.Collection; -import java.util.List; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.BadRequestException; @@ -16,9 +15,4 @@ public SubmittedQuestionAndProvidedQuestionMismatchException(Collection su submittedQuestionIds, providedQuestionIds, this ); } - - public SubmittedQuestionAndProvidedQuestionMismatchException(long submittedQuestionId, - Collection providedQuestionIds) { - this(List.of(submittedQuestionId), providedQuestionIds); - } } From 28ee86dbc5f0ca1d66c806e3e84a5954828bf436 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Thu, 31 Oct 2024 19:46:12 +0900 Subject: [PATCH 11/60] =?UTF-8?q?[BE]=20docs:=20=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=EB=AF=B8=EC=97=90=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=B6=94=EA=B0=80=20(#952)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7f807fd6a..63ae0b310 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ ### 백엔드

+### Infrastructure +

+ ## 🧑‍💻 팀원 소개 ### 프론트엔드 @@ -59,3 +62,4 @@ | | | | | | :---: | :---: | :---: | :---: | | [🦧 산초](https://github.com/nayonsoso) | [🤸🏻‍♂️ 아루](https://github.com/donghoony) | [💃 커비](https://github.com/skylar1220) | [🐻 테드](https://github.com/Kimprodp) | + From cfdaeac92e093f61127fabe46f18dc9ae4a97f2f Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Tue, 5 Nov 2024 16:29:36 +0900 Subject: [PATCH 12/60] =?UTF-8?q?[BE]=20Question=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20Template=20=ED=95=98=EC=9C=84=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20(#956)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Question 도메인을 Template 아래로 이동 - 전체적인 경계를 아래와 같이 나눔: 1. 템플릿 (질문, 옵션, 섹션, 템플릿) 2. 리뷰 (답변) 3. 하이라이트 4. 리뷰 그룹 * refactor: TemplateService에서 리뷰 그룹 Repository 계층을 참조하지 않도록 수정 --- .../main/java/reviewme/DatabaseInitializer.java | 16 ++++++++-------- .../service/ReviewGatheredLookupService.java | 4 ++-- .../response/detail/QuestionAnswerResponse.java | 2 +- .../gathered/SimpleQuestionResponse.java | 2 +- .../review/service/mapper/AnswerMapper.java | 2 +- .../service/mapper/AnswerMapperFactory.java | 2 +- .../service/mapper/CheckboxAnswerMapper.java | 2 +- .../service/mapper/ReviewDetailMapper.java | 12 ++++++------ .../service/mapper/ReviewGatherMapper.java | 6 +++--- .../review/service/mapper/ReviewListMapper.java | 6 +++--- .../review/service/mapper/ReviewMapper.java | 4 ++-- .../review/service/mapper/TextAnswerMapper.java | 2 +- .../mapper/UnsupportedQuestionTypeException.java | 2 +- .../validator/CheckboxAnswerValidator.java | 12 ++++++------ .../service/validator/ReviewValidator.java | 4 ++-- .../service/validator/TextAnswerValidator.java | 4 ++-- .../domain/OptionGroup.java | 2 +- .../domain/OptionItem.java | 2 +- .../domain/OptionType.java | 2 +- .../{question => template}/domain/Question.java | 2 +- .../domain/QuestionType.java | 2 +- .../repository/OptionGroupRepository.java | 4 ++-- .../repository/OptionItemRepository.java | 6 +++--- .../repository/QuestionRepository.java | 6 +++--- .../template/service/TemplateService.java | 12 +++--------- .../template/service/mapper/TemplateMapper.java | 12 ++++++------ .../test/java/reviewme/api/ReviewApiTest.java | 3 +-- .../test/java/reviewme/api/TemplateFixture.java | 2 +- .../reviewme/fixture/OptionGroupFixture.java | 2 +- .../java/reviewme/fixture/OptionItemFixture.java | 4 ++-- .../java/reviewme/fixture/QuestionFixture.java | 4 ++-- .../highlight/service/HighlightServiceTest.java | 2 +- .../service/mapper/HighlightMapperTest.java | 2 +- .../validator/HighlightValidatorTest.java | 2 +- .../review/repository/AnswerRepositoryTest.java | 4 ++-- .../review/repository/ReviewRepositoryTest.java | 4 ++-- .../service/ReviewDetailLookupServiceTest.java | 12 ++++++------ .../service/ReviewGatheredLookupServiceTest.java | 16 ++++++++-------- .../service/ReviewListLookupServiceTest.java | 12 ++++++------ .../service/ReviewRegisterServiceTest.java | 12 ++++++------ .../review/service/ReviewSummaryServiceTest.java | 4 ++-- .../service/mapper/AnswerMapperFactoryTest.java | 2 +- .../service/mapper/ReviewGatherMapperTest.java | 12 ++++++------ .../service/mapper/ReviewListMapperTest.java | 4 ++-- .../review/service/mapper/ReviewMapperTest.java | 12 ++++++------ .../validator/CheckboxAnswerValidatorTest.java | 12 ++++++------ .../service/validator/ReviewValidatorTest.java | 12 ++++++------ .../validator/TextAnswerValidatorTest.java | 4 ++-- .../repository/OptionGroupRepositoryTest.java | 9 +++++---- .../repository/OptionItemRepositoryTest.java | 13 ++++++++----- .../repository/QuestionRepositoryTest.java | 11 +++++++---- .../template/service/TemplateServiceTest.java | 14 ++------------ .../service/mapper/TemplateMapperTest.java | 10 +++++----- 53 files changed, 161 insertions(+), 171 deletions(-) rename backend/src/main/java/reviewme/{question => template}/domain/OptionGroup.java (96%) rename backend/src/main/java/reviewme/{question => template}/domain/OptionItem.java (97%) rename backend/src/main/java/reviewme/{question => template}/domain/OptionType.java (61%) rename backend/src/main/java/reviewme/{question => template}/domain/Question.java (97%) rename backend/src/main/java/reviewme/{question => template}/domain/QuestionType.java (64%) rename backend/src/main/java/reviewme/{question => template}/repository/OptionGroupRepository.java (87%) rename backend/src/main/java/reviewme/{question => template}/repository/OptionItemRepository.java (86%) rename backend/src/main/java/reviewme/{question => template}/repository/QuestionRepository.java (92%) rename backend/src/test/java/reviewme/{question => template}/repository/OptionGroupRepositoryTest.java (88%) rename backend/src/test/java/reviewme/{question => template}/repository/OptionItemRepositoryTest.java (87%) rename backend/src/test/java/reviewme/{question => template}/repository/QuestionRepositoryTest.java (94%) diff --git a/backend/src/main/java/reviewme/DatabaseInitializer.java b/backend/src/main/java/reviewme/DatabaseInitializer.java index 38d1e3496..71c18843b 100644 --- a/backend/src/main/java/reviewme/DatabaseInitializer.java +++ b/backend/src/main/java/reviewme/DatabaseInitializer.java @@ -5,14 +5,14 @@ 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; -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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.OptionType; +import reviewme.template.domain.Question; +import reviewme.template.domain.QuestionType; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.template.domain.Section; import reviewme.template.domain.Template; import reviewme.template.domain.VisibleType; diff --git a/backend/src/main/java/reviewme/review/service/ReviewGatheredLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewGatheredLookupService.java index 703348c9e..82de0ab16 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewGatheredLookupService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewGatheredLookupService.java @@ -9,8 +9,8 @@ import org.springframework.transaction.annotation.Transactional; import reviewme.highlight.domain.Highlight; import reviewme.highlight.repository.HighlightRepository; -import reviewme.question.domain.Question; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.repository.AnswerRepository; import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java index 000eb83c8..fccc3c752 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java @@ -1,7 +1,7 @@ package reviewme.review.service.dto.response.detail; import jakarta.annotation.Nullable; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; public record QuestionAnswerResponse( long questionId, diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java index e16df25e6..d7f1647d9 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java @@ -1,6 +1,6 @@ package reviewme.review.service.dto.response.gathered; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; public record SimpleQuestionResponse( long id, diff --git a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java index 7b3cbb631..1181808a5 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java @@ -1,6 +1,6 @@ package reviewme.review.service.mapper; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; import reviewme.review.domain.Answer; import reviewme.review.service.dto.request.ReviewAnswerRequest; diff --git a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapperFactory.java b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapperFactory.java index 6dc804547..624c3ba81 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapperFactory.java +++ b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapperFactory.java @@ -3,7 +3,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; @Component @RequiredArgsConstructor diff --git a/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java index 3648e32f6..7fb87b0dc 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java @@ -1,7 +1,7 @@ package reviewme.review.service.mapper; import org.springframework.stereotype.Component; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java index 7121d99b5..c2874c1c1 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewDetailMapper.java @@ -7,12 +7,12 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.domain.Review; diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewGatherMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewGatherMapper.java index 2a1f4e135..d53fd0905 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewGatherMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewGatherMapper.java @@ -7,9 +7,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import reviewme.highlight.domain.Highlight; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.Question; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.CheckboxAnswerSelectedOption; diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java index aa882802a..ab5ec4327 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java @@ -5,9 +5,9 @@ import java.util.stream.Collectors; 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.template.domain.OptionItem; +import reviewme.template.domain.OptionType; +import reviewme.template.repository.OptionItemRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.domain.Review; diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java index 68ee776b9..58d0c6a6f 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java @@ -8,8 +8,8 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import reviewme.question.domain.Question; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.domain.Review; import reviewme.review.service.dto.request.ReviewAnswerRequest; diff --git a/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java index afd47ac97..48bd55789 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java @@ -1,7 +1,7 @@ package reviewme.review.service.mapper; import org.springframework.stereotype.Component; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; import reviewme.review.domain.TextAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; diff --git a/backend/src/main/java/reviewme/review/service/mapper/UnsupportedQuestionTypeException.java b/backend/src/main/java/reviewme/review/service/mapper/UnsupportedQuestionTypeException.java index b08870515..26a22f0fd 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/UnsupportedQuestionTypeException.java +++ b/backend/src/main/java/reviewme/review/service/mapper/UnsupportedQuestionTypeException.java @@ -2,7 +2,7 @@ import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.DataInconsistencyException; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; @Slf4j public class UnsupportedQuestionTypeException extends DataInconsistencyException { diff --git a/backend/src/main/java/reviewme/review/service/validator/CheckboxAnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/CheckboxAnswerValidator.java index 62d39728b..ab57ead79 100644 --- a/backend/src/main/java/reviewme/review/service/validator/CheckboxAnswerValidator.java +++ b/backend/src/main/java/reviewme/review/service/validator/CheckboxAnswerValidator.java @@ -5,12 +5,12 @@ import lombok.AccessLevel; 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.domain.CheckboxAnswer; diff --git a/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java b/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java index 2906a8507..9c68894c9 100644 --- a/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java +++ b/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java @@ -7,8 +7,8 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import reviewme.question.domain.Question; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.domain.CheckboxAnswer; diff --git a/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java index 78a0701dd..59d9d476b 100644 --- a/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java +++ b/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java @@ -3,8 +3,8 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import reviewme.question.domain.Question; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.domain.TextAnswer; import reviewme.review.service.exception.InvalidTextAnswerLengthException; diff --git a/backend/src/main/java/reviewme/question/domain/OptionGroup.java b/backend/src/main/java/reviewme/template/domain/OptionGroup.java similarity index 96% rename from backend/src/main/java/reviewme/question/domain/OptionGroup.java rename to backend/src/main/java/reviewme/template/domain/OptionGroup.java index 61aa3d23a..a5ee115ca 100644 --- a/backend/src/main/java/reviewme/question/domain/OptionGroup.java +++ b/backend/src/main/java/reviewme/template/domain/OptionGroup.java @@ -1,4 +1,4 @@ -package reviewme.question.domain; +package reviewme.template.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/backend/src/main/java/reviewme/question/domain/OptionItem.java b/backend/src/main/java/reviewme/template/domain/OptionItem.java similarity index 97% rename from backend/src/main/java/reviewme/question/domain/OptionItem.java rename to backend/src/main/java/reviewme/template/domain/OptionItem.java index 59b29bc3b..7232dd3df 100644 --- a/backend/src/main/java/reviewme/question/domain/OptionItem.java +++ b/backend/src/main/java/reviewme/template/domain/OptionItem.java @@ -1,4 +1,4 @@ -package reviewme.question.domain; +package reviewme.template.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/backend/src/main/java/reviewme/question/domain/OptionType.java b/backend/src/main/java/reviewme/template/domain/OptionType.java similarity index 61% rename from backend/src/main/java/reviewme/question/domain/OptionType.java rename to backend/src/main/java/reviewme/template/domain/OptionType.java index dfa86920b..80e4e2c1b 100644 --- a/backend/src/main/java/reviewme/question/domain/OptionType.java +++ b/backend/src/main/java/reviewme/template/domain/OptionType.java @@ -1,4 +1,4 @@ -package reviewme.question.domain; +package reviewme.template.domain; public enum OptionType { CATEGORY, diff --git a/backend/src/main/java/reviewme/question/domain/Question.java b/backend/src/main/java/reviewme/template/domain/Question.java similarity index 97% rename from backend/src/main/java/reviewme/question/domain/Question.java rename to backend/src/main/java/reviewme/template/domain/Question.java index f59e4ae87..f4384d854 100644 --- a/backend/src/main/java/reviewme/question/domain/Question.java +++ b/backend/src/main/java/reviewme/template/domain/Question.java @@ -1,4 +1,4 @@ -package reviewme.question.domain; +package reviewme.template.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/backend/src/main/java/reviewme/question/domain/QuestionType.java b/backend/src/main/java/reviewme/template/domain/QuestionType.java similarity index 64% rename from backend/src/main/java/reviewme/question/domain/QuestionType.java rename to backend/src/main/java/reviewme/template/domain/QuestionType.java index 863ba56e5..78b84fb58 100644 --- a/backend/src/main/java/reviewme/question/domain/QuestionType.java +++ b/backend/src/main/java/reviewme/template/domain/QuestionType.java @@ -1,4 +1,4 @@ -package reviewme.question.domain; +package reviewme.template.domain; public enum QuestionType { CHECKBOX, diff --git a/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java b/backend/src/main/java/reviewme/template/repository/OptionGroupRepository.java similarity index 87% rename from backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java rename to backend/src/main/java/reviewme/template/repository/OptionGroupRepository.java index ad2994537..ce57a64e7 100644 --- a/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java +++ b/backend/src/main/java/reviewme/template/repository/OptionGroupRepository.java @@ -1,11 +1,11 @@ -package reviewme.question.repository; +package reviewme.template.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; +import reviewme.template.domain.OptionGroup; @Repository public interface OptionGroupRepository extends JpaRepository { diff --git a/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java b/backend/src/main/java/reviewme/template/repository/OptionItemRepository.java similarity index 86% rename from backend/src/main/java/reviewme/question/repository/OptionItemRepository.java rename to backend/src/main/java/reviewme/template/repository/OptionItemRepository.java index e42274c33..305df2ef1 100644 --- a/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java +++ b/backend/src/main/java/reviewme/template/repository/OptionItemRepository.java @@ -1,11 +1,11 @@ -package reviewme.question.repository; +package reviewme.template.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.OptionType; @Repository public interface OptionItemRepository extends JpaRepository { diff --git a/backend/src/main/java/reviewme/question/repository/QuestionRepository.java b/backend/src/main/java/reviewme/template/repository/QuestionRepository.java similarity index 92% rename from backend/src/main/java/reviewme/question/repository/QuestionRepository.java rename to backend/src/main/java/reviewme/template/repository/QuestionRepository.java index 9db137d25..aa21a0167 100644 --- a/backend/src/main/java/reviewme/question/repository/QuestionRepository.java +++ b/backend/src/main/java/reviewme/template/repository/QuestionRepository.java @@ -1,12 +1,12 @@ -package reviewme.question.repository; +package reviewme.template.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; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.Question; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; @Repository public interface QuestionRepository extends JpaRepository { diff --git a/backend/src/main/java/reviewme/template/service/TemplateService.java b/backend/src/main/java/reviewme/template/service/TemplateService.java index a49fc5160..1b6370878 100644 --- a/backend/src/main/java/reviewme/template/service/TemplateService.java +++ b/backend/src/main/java/reviewme/template/service/TemplateService.java @@ -3,9 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; 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.ReviewGroupService; import reviewme.template.service.dto.response.TemplateResponse; import reviewme.template.service.mapper.TemplateMapper; @@ -13,17 +12,12 @@ @RequiredArgsConstructor public class TemplateService { - private final ReviewGroupRepository reviewGroupRepository; + private final ReviewGroupService reviewGroupService; private final TemplateMapper templateMapper; @Transactional(readOnly = true) public TemplateResponse generateReviewForm(String reviewRequestCode) { - ReviewGroup reviewGroup = findReviewGroupByRequestCodeOrThrow(reviewRequestCode); + ReviewGroup reviewGroup = reviewGroupService.getReviewGroupByReviewRequestCode(reviewRequestCode); return templateMapper.mapToTemplateResponse(reviewGroup); } - - private ReviewGroup findReviewGroupByRequestCodeOrThrow(String reviewRequestCode) { - return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - } } diff --git a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java index 02b6084f1..7151003d5 100644 --- a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java @@ -3,12 +3,12 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.template.domain.Section; import reviewme.template.domain.SectionQuestion; diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index 5add4cfbd..4552b02d5 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -3,7 +3,6 @@ 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.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -22,7 +21,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; import reviewme.review.service.dto.request.ReviewRegisterRequest; import reviewme.review.service.dto.response.gathered.HighlightResponse; import reviewme.review.service.dto.response.gathered.RangeResponse; diff --git a/backend/src/test/java/reviewme/api/TemplateFixture.java b/backend/src/test/java/reviewme/api/TemplateFixture.java index aba719fbb..7a8f3c194 100644 --- a/backend/src/test/java/reviewme/api/TemplateFixture.java +++ b/backend/src/test/java/reviewme/api/TemplateFixture.java @@ -2,7 +2,7 @@ import java.time.LocalDate; import java.util.List; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; import reviewme.review.service.dto.response.detail.OptionGroupAnswerResponse; import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse; import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; diff --git a/backend/src/test/java/reviewme/fixture/OptionGroupFixture.java b/backend/src/test/java/reviewme/fixture/OptionGroupFixture.java index 259a3ebcf..f880cbfb7 100644 --- a/backend/src/test/java/reviewme/fixture/OptionGroupFixture.java +++ b/backend/src/test/java/reviewme/fixture/OptionGroupFixture.java @@ -1,6 +1,6 @@ package reviewme.fixture; -import reviewme.question.domain.OptionGroup; +import reviewme.template.domain.OptionGroup; public class OptionGroupFixture { diff --git a/backend/src/test/java/reviewme/fixture/OptionItemFixture.java b/backend/src/test/java/reviewme/fixture/OptionItemFixture.java index 3b7e50725..c076cb205 100644 --- a/backend/src/test/java/reviewme/fixture/OptionItemFixture.java +++ b/backend/src/test/java/reviewme/fixture/OptionItemFixture.java @@ -1,7 +1,7 @@ package reviewme.fixture; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.OptionType; public class OptionItemFixture { diff --git a/backend/src/test/java/reviewme/fixture/QuestionFixture.java b/backend/src/test/java/reviewme/fixture/QuestionFixture.java index f4ce28b88..3484116af 100644 --- a/backend/src/test/java/reviewme/fixture/QuestionFixture.java +++ b/backend/src/test/java/reviewme/fixture/QuestionFixture.java @@ -1,7 +1,7 @@ package reviewme.fixture; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.Question; +import reviewme.template.domain.QuestionType; public class QuestionFixture { diff --git a/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java b/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java index 32eed36b8..207a68d29 100644 --- a/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java +++ b/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java @@ -17,7 +17,7 @@ import reviewme.highlight.service.dto.HighlightRequest; import reviewme.highlight.service.dto.HighlightedLineRequest; import reviewme.highlight.service.dto.HighlightsRequest; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; import reviewme.review.repository.ReviewRepository; diff --git a/backend/src/test/java/reviewme/highlight/service/mapper/HighlightMapperTest.java b/backend/src/test/java/reviewme/highlight/service/mapper/HighlightMapperTest.java index 14a6639f9..66101cf31 100644 --- a/backend/src/test/java/reviewme/highlight/service/mapper/HighlightMapperTest.java +++ b/backend/src/test/java/reviewme/highlight/service/mapper/HighlightMapperTest.java @@ -17,7 +17,7 @@ import reviewme.highlight.service.dto.HighlightRequest; import reviewme.highlight.service.dto.HighlightedLineRequest; import reviewme.highlight.service.dto.HighlightsRequest; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; import reviewme.review.repository.ReviewRepository; diff --git a/backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java b/backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java index 84bf793d2..4c98fa75b 100644 --- a/backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java +++ b/backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java @@ -12,7 +12,7 @@ import reviewme.highlight.service.dto.HighlightRequest; import reviewme.highlight.service.dto.HighlightsRequest; import reviewme.highlight.service.exception.SubmittedAnswerAndProvidedAnswerMismatchException; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; import reviewme.review.repository.ReviewRepository; diff --git a/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java b/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java index e13ce1427..9d2ad447d 100644 --- a/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java +++ b/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java @@ -11,8 +11,8 @@ 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.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java b/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java index 2149c7ed9..3855c1520 100644 --- a/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java +++ b/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java @@ -13,8 +13,8 @@ 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.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Review; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; diff --git a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java index ab296d796..236143e75 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java @@ -17,12 +17,12 @@ 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.Question; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; +import reviewme.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; diff --git a/backend/src/test/java/reviewme/review/service/ReviewGatheredLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewGatheredLookupServiceTest.java index 141992950..f6c8c4690 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewGatheredLookupServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewGatheredLookupServiceTest.java @@ -18,14 +18,14 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.OptionType; +import reviewme.template.domain.Question; +import reviewme.template.domain.QuestionType; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java index d8384afe5..ebc559924 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java @@ -12,12 +12,12 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java index 73ab64897..5852dae7c 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java @@ -15,12 +15,12 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/test/java/reviewme/review/service/ReviewSummaryServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewSummaryServiceTest.java index 2a2ffa7a5..0f83f4c3f 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewSummaryServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewSummaryServiceTest.java @@ -10,8 +10,8 @@ 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.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Review; import reviewme.review.repository.ReviewRepository; import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; diff --git a/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java index 25d1b5018..bdf37e905 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java @@ -8,7 +8,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import reviewme.question.domain.QuestionType; +import reviewme.template.domain.QuestionType; import reviewme.review.domain.Answer; import reviewme.review.service.dto.request.ReviewAnswerRequest; diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewGatherMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewGatherMapperTest.java index 1e411f13e..63fcd7615 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/ReviewGatherMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewGatherMapperTest.java @@ -12,12 +12,12 @@ import org.assertj.core.groups.Tuple; 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java index 0cdfe0a32..0032b7cb2 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java @@ -10,8 +10,8 @@ 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.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; import reviewme.review.repository.ReviewRepository; diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java index 8ca15f312..4065c63de 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java @@ -16,12 +16,12 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/test/java/reviewme/review/service/validator/CheckboxAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/CheckboxAnswerValidatorTest.java index 5c64c2503..c405ab4ba 100644 --- a/backend/src/test/java/reviewme/review/service/validator/CheckboxAnswerValidatorTest.java +++ b/backend/src/test/java/reviewme/review/service/validator/CheckboxAnswerValidatorTest.java @@ -8,12 +8,12 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; import reviewme.review.service.exception.OptionGroupNotFoundByQuestionIdException; diff --git a/backend/src/test/java/reviewme/review/service/validator/ReviewValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/ReviewValidatorTest.java index dca5dd59d..dd8ebfc94 100644 --- a/backend/src/test/java/reviewme/review/service/validator/ReviewValidatorTest.java +++ b/backend/src/test/java/reviewme/review/service/validator/ReviewValidatorTest.java @@ -15,12 +15,12 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java index 0e8265bb6..27854e02c 100644 --- a/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java +++ b/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java @@ -9,8 +9,8 @@ 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.template.domain.Question; +import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.TextAnswer; import reviewme.review.service.exception.InvalidTextAnswerLengthException; import reviewme.review.service.exception.SubmittedQuestionNotFoundException; diff --git a/backend/src/test/java/reviewme/question/repository/OptionGroupRepositoryTest.java b/backend/src/test/java/reviewme/template/repository/OptionGroupRepositoryTest.java similarity index 88% rename from backend/src/test/java/reviewme/question/repository/OptionGroupRepositoryTest.java rename to backend/src/test/java/reviewme/template/repository/OptionGroupRepositoryTest.java index 1bc3ea107..d121c3688 100644 --- a/backend/src/test/java/reviewme/question/repository/OptionGroupRepositoryTest.java +++ b/backend/src/test/java/reviewme/template/repository/OptionGroupRepositoryTest.java @@ -1,7 +1,6 @@ -package reviewme.question.repository; +package reviewme.template.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.선택형_필수_질문; @@ -9,8 +8,10 @@ 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; +import reviewme.template.domain.OptionGroup; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.QuestionRepository; @DataJpaTest class OptionGroupRepositoryTest { diff --git a/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java b/backend/src/test/java/reviewme/template/repository/OptionItemRepositoryTest.java similarity index 87% rename from backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java rename to backend/src/test/java/reviewme/template/repository/OptionItemRepositoryTest.java index 5aebbf06b..5e1c7a288 100644 --- a/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java +++ b/backend/src/test/java/reviewme/template/repository/OptionItemRepositoryTest.java @@ -1,4 +1,4 @@ -package reviewme.question.repository; +package reviewme.template.repository; import static org.assertj.core.api.Assertions.assertThat; import static reviewme.fixture.OptionGroupFixture.선택지_그룹; @@ -9,10 +9,13 @@ 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.OptionItem; -import reviewme.question.domain.OptionType; -import reviewme.question.domain.Question; +import reviewme.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.OptionType; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; @DataJpaTest class OptionItemRepositoryTest { diff --git a/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java b/backend/src/test/java/reviewme/template/repository/QuestionRepositoryTest.java similarity index 94% rename from backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java rename to backend/src/test/java/reviewme/template/repository/QuestionRepositoryTest.java index da694e335..584057104 100644 --- a/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java +++ b/backend/src/test/java/reviewme/template/repository/QuestionRepositoryTest.java @@ -1,4 +1,4 @@ -package reviewme.question.repository; +package reviewme.template.repository; import static org.assertj.core.api.Assertions.assertThat; import static reviewme.fixture.OptionGroupFixture.선택지_그룹; @@ -13,13 +13,16 @@ 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.OptionItem; -import reviewme.question.domain.Question; +import reviewme.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.template.domain.Section; import reviewme.template.domain.Template; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.template.repository.SectionRepository; import reviewme.template.repository.TemplateRepository; diff --git a/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java b/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java index 7c512d99f..9f372e1d8 100644 --- a/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java +++ b/backend/src/test/java/reviewme/template/service/TemplateServiceTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; @@ -20,23 +19,14 @@ class TemplateServiceTest { @Autowired private ReviewGroupRepository reviewGroupRepository; - @Test - void 잘못된_리뷰_요청_코드로_리뷰_작성폼을_요청할_경우_예외가_발생한다() { - // given - ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); - - // when, then - assertThatThrownBy(() -> templateService.generateReviewForm(reviewGroup.getReviewRequestCode() + " ")) - .isInstanceOf(ReviewGroupNotFoundByReviewRequestCodeException.class); - } - @Test void 리뷰이에게_작성될_리뷰_양식_생성_시_저장된_템플릿이_없을_경우_예외가_발생한다() { // given ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + String reviewRequestCode = reviewGroup.getReviewRequestCode(); // when, then - assertThatThrownBy(() -> templateService.generateReviewForm(reviewGroup.getReviewRequestCode())) + assertThatThrownBy(() -> templateService.generateReviewForm(reviewRequestCode)) .isInstanceOf(TemplateNotFoundByReviewGroupException.class); } } diff --git a/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java b/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java index e1a0f1853..0814b4400 100644 --- a/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java +++ b/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java @@ -14,11 +14,11 @@ 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.template.domain.OptionGroup; +import reviewme.template.domain.Question; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; From 7cd031ccf32e0be7f982be3451718962dfaef6c7 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sun, 10 Nov 2024 12:09:07 +0900 Subject: [PATCH 13/60] =?UTF-8?q?[All]=20fix:=20`[Release]`=20PR=20?= =?UTF-8?q?=EB=A8=B8=EB=A6=BF=EB=A7=90=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=EC=B2=98=EB=A6=AC=20(#940)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: [Release] 머릿말에도 디스코드 알림 처리 * refactor: `Release`인 경우 `All`로 처리 --- .github/workflows/discord-pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/discord-pull-request.yml b/.github/workflows/discord-pull-request.yml index 52b446ebb..9f9919b42 100644 --- a/.github/workflows/discord-pull-request.yml +++ b/.github/workflows/discord-pull-request.yml @@ -31,7 +31,7 @@ jobs: elif [ "$PR_PREFIX" = '[FE]' ]; then echo Frontend PR Found! echo "PR_PREFIX=FE" >> $GITHUB_ENV - elif [ "$PR_PREFIX" = '[All]' ]; then + elif [ "$PR_PREFIX" = '[All]' ] || [ "$PR_PREFIX" = '[Release]' ]; then echo All PR Found! echo "PR_PREFIX=All" >> $GITHUB_ENV fi From 11a2b05f18a54836bbdc90e9035b4ff67047d68c Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sun, 10 Nov 2024 13:29:04 +0900 Subject: [PATCH 14/60] =?UTF-8?q?[BE]=20refactor:=20Config=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC,=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=81=EC=9A=A9=20(#957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: use `@Slf4j` instead of `LoggerFactory.getLogger` * refactor: `WebConfig`가 하나의 역할만 하도록 수정 * style: dto 불필요한 개행 제거 * refactor: Config는 상위 패키지, Resolver/Interceptor는 각 도메인에 두기 --- .../main/java/reviewme/config/WebConfig.java | 10 ---------- .../config/{ => cors}/CorsConfig.java | 8 +++----- .../config/{ => cors}/CorsProperties.java | 2 +- .../{ => datasource}/DataSourceType.java | 2 +- .../ReplicationDatasourceConfig.java | 3 +-- .../ReplicationRoutingDataSource.java | 2 +- .../RequestLimitInterceptor.java | 4 +--- .../RequestLimitProperties.java | 9 ++------- .../RequestLimitRedisConfig.java | 2 +- .../requestlimit/RequestLimitWebConfig.java | 20 +++++++++++++++++++ .../TooManyRequestException.java | 3 ++- .../global/GlobalExceptionHandler.java | 2 +- .../global/exception/FieldErrorResponse.java | 6 +----- .../review/service/ReviewRegisterService.java | 4 ---- .../config/{ => cors}/CorsConfigTest.java | 3 ++- .../{ => cors}/ExternalCorsConfigTest.java | 2 +- .../{ => cors}/LocalCorsConfigTest.java | 2 +- .../RequestLimitInterceptorTest.java | 5 +---- 18 files changed, 40 insertions(+), 49 deletions(-) rename backend/src/main/java/reviewme/config/{ => cors}/CorsConfig.java (90%) rename backend/src/main/java/reviewme/config/{ => cors}/CorsProperties.java (91%) rename backend/src/main/java/reviewme/config/{ => datasource}/DataSourceType.java (62%) rename backend/src/main/java/reviewme/config/{ => datasource}/ReplicationDatasourceConfig.java (98%) rename backend/src/main/java/reviewme/config/{ => datasource}/ReplicationRoutingDataSource.java (93%) rename backend/src/main/java/reviewme/{global => config/requestlimit}/RequestLimitInterceptor.java (93%) rename backend/src/main/java/reviewme/config/{ => requestlimit}/RequestLimitProperties.java (50%) rename backend/src/main/java/reviewme/config/{ => requestlimit}/RequestLimitRedisConfig.java (97%) create mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java rename backend/src/main/java/reviewme/{global/exception => config/requestlimit}/TooManyRequestException.java (80%) rename backend/src/test/java/reviewme/config/{ => cors}/CorsConfigTest.java (93%) rename backend/src/test/java/reviewme/config/{ => cors}/ExternalCorsConfigTest.java (98%) rename backend/src/test/java/reviewme/config/{ => cors}/LocalCorsConfigTest.java (97%) rename backend/src/test/java/reviewme/{global => config/requestlimit}/RequestLimitInterceptorTest.java (93%) diff --git a/backend/src/main/java/reviewme/config/WebConfig.java b/backend/src/main/java/reviewme/config/WebConfig.java index 916ea5a41..d855040f0 100644 --- a/backend/src/main/java/reviewme/config/WebConfig.java +++ b/backend/src/main/java/reviewme/config/WebConfig.java @@ -3,11 +3,8 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import reviewme.global.RequestLimitInterceptor; import reviewme.reviewgroup.controller.ReviewGroupSessionResolver; import reviewme.reviewgroup.service.ReviewGroupService; @@ -16,16 +13,9 @@ public class WebConfig implements WebMvcConfigurer { private final ReviewGroupService reviewGroupService; - private final RedisTemplate redisTemplate; - private final RequestLimitProperties requestLimitProperties; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(new ReviewGroupSessionResolver(reviewGroupService)); } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new RequestLimitInterceptor(redisTemplate, requestLimitProperties)); - } } diff --git a/backend/src/main/java/reviewme/config/CorsConfig.java b/backend/src/main/java/reviewme/config/cors/CorsConfig.java similarity index 90% rename from backend/src/main/java/reviewme/config/CorsConfig.java rename to backend/src/main/java/reviewme/config/cors/CorsConfig.java index 2f51720ea..ff1a483a3 100644 --- a/backend/src/main/java/reviewme/config/CorsConfig.java +++ b/backend/src/main/java/reviewme/config/cors/CorsConfig.java @@ -1,18 +1,16 @@ -package reviewme.config; +package reviewme.config.cors; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; 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; +@Slf4j public class CorsConfig { - private static final Logger log = LoggerFactory.getLogger(CorsConfig.class); - private CorsConfig() { } diff --git a/backend/src/main/java/reviewme/config/CorsProperties.java b/backend/src/main/java/reviewme/config/cors/CorsProperties.java similarity index 91% rename from backend/src/main/java/reviewme/config/CorsProperties.java rename to backend/src/main/java/reviewme/config/cors/CorsProperties.java index 69a7d1c4d..e11a93667 100644 --- a/backend/src/main/java/reviewme/config/CorsProperties.java +++ b/backend/src/main/java/reviewme/config/cors/CorsProperties.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.cors; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/backend/src/main/java/reviewme/config/DataSourceType.java b/backend/src/main/java/reviewme/config/datasource/DataSourceType.java similarity index 62% rename from backend/src/main/java/reviewme/config/DataSourceType.java rename to backend/src/main/java/reviewme/config/datasource/DataSourceType.java index c48080ab4..b40750df2 100644 --- a/backend/src/main/java/reviewme/config/DataSourceType.java +++ b/backend/src/main/java/reviewme/config/datasource/DataSourceType.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.datasource; public enum DataSourceType { READ, diff --git a/backend/src/main/java/reviewme/config/ReplicationDatasourceConfig.java b/backend/src/main/java/reviewme/config/datasource/ReplicationDatasourceConfig.java similarity index 98% rename from backend/src/main/java/reviewme/config/ReplicationDatasourceConfig.java rename to backend/src/main/java/reviewme/config/datasource/ReplicationDatasourceConfig.java index 6a33a9e08..fb59b2498 100644 --- a/backend/src/main/java/reviewme/config/ReplicationDatasourceConfig.java +++ b/backend/src/main/java/reviewme/config/datasource/ReplicationDatasourceConfig.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.datasource; import java.util.HashMap; import java.util.Map; @@ -54,4 +54,3 @@ public DataSource dataSource(@Qualifier(ROUTING_DATA_SOURCE_NAME) DataSource rou return new LazyConnectionDataSourceProxy(routingDataSource); } } - diff --git a/backend/src/main/java/reviewme/config/ReplicationRoutingDataSource.java b/backend/src/main/java/reviewme/config/datasource/ReplicationRoutingDataSource.java similarity index 93% rename from backend/src/main/java/reviewme/config/ReplicationRoutingDataSource.java rename to backend/src/main/java/reviewme/config/datasource/ReplicationRoutingDataSource.java index 49b7aa22b..f8a802467 100644 --- a/backend/src/main/java/reviewme/config/ReplicationRoutingDataSource.java +++ b/backend/src/main/java/reviewme/config/datasource/ReplicationRoutingDataSource.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.datasource; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.transaction.support.TransactionSynchronizationManager; diff --git a/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java similarity index 93% rename from backend/src/main/java/reviewme/global/RequestLimitInterceptor.java rename to backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java index b5747dfd1..ef25b711e 100644 --- a/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java +++ b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java @@ -1,4 +1,4 @@ -package reviewme.global; +package reviewme.config.requestlimit; import static org.springframework.http.HttpHeaders.USER_AGENT; @@ -11,8 +11,6 @@ import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import reviewme.config.RequestLimitProperties; -import reviewme.global.exception.TooManyRequestException; @Component @EnableConfigurationProperties(RequestLimitProperties.class) diff --git a/backend/src/main/java/reviewme/config/RequestLimitProperties.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java similarity index 50% rename from backend/src/main/java/reviewme/config/RequestLimitProperties.java rename to backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java index efea3b4f8..558378094 100644 --- a/backend/src/main/java/reviewme/config/RequestLimitProperties.java +++ b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java @@ -1,13 +1,8 @@ -package reviewme.config; +package reviewme.config.requestlimit; import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "request-limit") -public record RequestLimitProperties( - long threshold, - Duration duration, - String host, - int port -) { +public record RequestLimitProperties(long threshold, Duration duration, String host, int port) { } diff --git a/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java similarity index 97% rename from backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java rename to backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java index a8307db5f..d8bb458a9 100644 --- a/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java +++ b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.requestlimit; import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.EnableConfigurationProperties; diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java new file mode 100644 index 000000000..19f3b2fe4 --- /dev/null +++ b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java @@ -0,0 +1,20 @@ +package reviewme.config.requestlimit; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class RequestLimitWebConfig implements WebMvcConfigurer { + + private final RedisTemplate redisTemplate; + private final RequestLimitProperties requestLimitProperties; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new RequestLimitInterceptor(redisTemplate, requestLimitProperties)); + } +} diff --git a/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java b/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java similarity index 80% rename from backend/src/main/java/reviewme/global/exception/TooManyRequestException.java rename to backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java index 4f26fee3e..544fb5885 100644 --- a/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java +++ b/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java @@ -1,6 +1,7 @@ -package reviewme.global.exception; +package reviewme.config.requestlimit; import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.ReviewMeException; @Slf4j public class TooManyRequestException extends ReviewMeException { diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index 9d4511618..161e43172 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -22,7 +22,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import reviewme.global.exception.BadRequestException; import reviewme.global.exception.DataInconsistencyException; -import reviewme.global.exception.TooManyRequestException; +import reviewme.config.requestlimit.TooManyRequestException; import reviewme.global.exception.FieldErrorResponse; import reviewme.global.exception.NotFoundException; import reviewme.global.exception.UnauthorizedException; diff --git a/backend/src/main/java/reviewme/global/exception/FieldErrorResponse.java b/backend/src/main/java/reviewme/global/exception/FieldErrorResponse.java index e44edf619..ae0c678a4 100644 --- a/backend/src/main/java/reviewme/global/exception/FieldErrorResponse.java +++ b/backend/src/main/java/reviewme/global/exception/FieldErrorResponse.java @@ -1,8 +1,4 @@ package reviewme.global.exception; -public record FieldErrorResponse( - String field, - Object value, - String message -) { +public record FieldErrorResponse(String field, Object value, String message) { } diff --git a/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java b/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java index 966eaa602..87e5f8538 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java @@ -1,8 +1,6 @@ package reviewme.review.service; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reviewme.review.domain.Review; @@ -15,8 +13,6 @@ @RequiredArgsConstructor public class ReviewRegisterService { - private static final Logger log = LoggerFactory.getLogger(ReviewRegisterService.class); - private final ReviewMapper reviewMapper; private final ReviewValidator reviewValidator; private final ReviewRepository reviewRepository; diff --git a/backend/src/test/java/reviewme/config/CorsConfigTest.java b/backend/src/test/java/reviewme/config/cors/CorsConfigTest.java similarity index 93% rename from backend/src/test/java/reviewme/config/CorsConfigTest.java rename to backend/src/test/java/reviewme/config/cors/CorsConfigTest.java index 90af4a342..d7f20bfd2 100644 --- a/backend/src/test/java/reviewme/config/CorsConfigTest.java +++ b/backend/src/test/java/reviewme/config/cors/CorsConfigTest.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.cors; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; @@ -32,6 +32,7 @@ void setUp() { static class TestController { @RequestMapping("/test") public void test() { + // Testing controller calls, no-op } } } diff --git a/backend/src/test/java/reviewme/config/ExternalCorsConfigTest.java b/backend/src/test/java/reviewme/config/cors/ExternalCorsConfigTest.java similarity index 98% rename from backend/src/test/java/reviewme/config/ExternalCorsConfigTest.java rename to backend/src/test/java/reviewme/config/cors/ExternalCorsConfigTest.java index 095bb1bc7..39445b70f 100644 --- a/backend/src/test/java/reviewme/config/ExternalCorsConfigTest.java +++ b/backend/src/test/java/reviewme/config/cors/ExternalCorsConfigTest.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.cors; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; diff --git a/backend/src/test/java/reviewme/config/LocalCorsConfigTest.java b/backend/src/test/java/reviewme/config/cors/LocalCorsConfigTest.java similarity index 97% rename from backend/src/test/java/reviewme/config/LocalCorsConfigTest.java rename to backend/src/test/java/reviewme/config/cors/LocalCorsConfigTest.java index cd050b988..214de0857 100644 --- a/backend/src/test/java/reviewme/config/LocalCorsConfigTest.java +++ b/backend/src/test/java/reviewme/config/cors/LocalCorsConfigTest.java @@ -1,4 +1,4 @@ -package reviewme.config; +package reviewme.config.cors; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; diff --git a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java b/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java similarity index 93% rename from backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java rename to backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java index 998639691..969040683 100644 --- a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java +++ b/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java @@ -1,4 +1,4 @@ -package reviewme.global; +package reviewme.config.requestlimit; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -14,8 +14,6 @@ import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; -import reviewme.config.RequestLimitProperties; -import reviewme.global.exception.TooManyRequestException; class RequestLimitInterceptorTest { @@ -41,7 +39,6 @@ void setUp() { @Test void POST_요청이_아니면_통과한다() { // given - HttpServletRequest request = mock(HttpServletRequest.class); given(request.getMethod()).willReturn("GET"); // when From 55db0f6ebb512f783876d528097d1f27c4f7e43d Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Tue, 12 Nov 2024 16:32:18 +0900 Subject: [PATCH 15/60] [All] chore: add CODEOWNERS (#959) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..06032ab2f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +* @chysis @BadaHertz52 @soosoo22 @ImxYJL @donghoony @Kimprodp @nayonsoso @skylar1220 +/frontend/ @chysis @BadaHertz52 @soosoo22 @ImxYJL +/backend/ @donghoony @Kimprodp @nayonsoso @skylar1220 From 8858e0221de07aad22015be37802fba73309bbb2 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sun, 17 Nov 2024 16:00:26 +0900 Subject: [PATCH 16/60] =?UTF-8?q?[BE]=20feat:=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EB=B2=8C=ED=81=AC=20=EC=82=BD?= =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80=20(#961)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 하이라이트 벌크 삽입 추가 * refactor: 불필요한 `@Repository` 제거 * fix: `findAll` 사용하지 않도록 수정 * fix: 테스트 수정 * refactor: `NamedParameterJdbcTemplate` 사용하도록 수정 --- .../repository/HighlightJdbcRepository.java | 9 +++++++ .../HighlightJdbcRepositoryImpl.java | 24 +++++++++++++++++++ .../repository/HighlightRepository.java | 8 +++++-- .../repository/HighlightRepositoryTest.java | 18 ++++++++++++++ .../service/HighlightServiceTest.java | 2 +- 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepository.java create mode 100644 backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepositoryImpl.java diff --git a/backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepository.java b/backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepository.java new file mode 100644 index 000000000..b1e20ef36 --- /dev/null +++ b/backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepository.java @@ -0,0 +1,9 @@ +package reviewme.highlight.repository; + +import java.util.Collection; +import reviewme.highlight.domain.Highlight; + +public interface HighlightJdbcRepository { + + void saveAll(Collection highlights); +} diff --git a/backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepositoryImpl.java b/backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepositoryImpl.java new file mode 100644 index 000000000..077f6a531 --- /dev/null +++ b/backend/src/main/java/reviewme/highlight/repository/HighlightJdbcRepositoryImpl.java @@ -0,0 +1,24 @@ +package reviewme.highlight.repository; + +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import reviewme.highlight.domain.Highlight; + +@RequiredArgsConstructor +public class HighlightJdbcRepositoryImpl implements HighlightJdbcRepository { + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @Override + public void saveAll(Collection highlights) { + SqlParameterSource[] parameterSources = SqlParameterSourceUtils.createBatch(highlights.toArray()); + String insertSql = """ + INSERT INTO highlight (answer_id, line_index, start_index, end_index) + VALUES (:answerId, :lineIndex, :highlightRange.startIndex, :highlightRange.endIndex) + """; + namedParameterJdbcTemplate.batchUpdate(insertSql, parameterSources); + } +} diff --git a/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java b/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java index 74760e09c..104e4cfc1 100644 --- a/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java +++ b/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java @@ -2,12 +2,16 @@ import java.util.Collection; import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; import reviewme.highlight.domain.Highlight; -public interface HighlightRepository extends JpaRepository { +public interface HighlightRepository extends Repository, HighlightJdbcRepository { + + Highlight save(Highlight highlight); + + boolean existsById(long id); @Modifying @Query(""" diff --git a/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java b/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java index 40b584f47..6e46ed088 100644 --- a/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java +++ b/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java @@ -16,6 +16,24 @@ class HighlightRepositoryTest { @Autowired private HighlightRepository highlightRepository; + @Test + void 한_번에_여러_하이라이트를_벌크_삽입한다() { + // given + List highlights = List.of( + new Highlight(1L, 1, new HighlightRange(1, 2)), + new Highlight(1L, 1, new HighlightRange(3, 5)) + ); + + // when + highlightRepository.saveAll(highlights); + + // then + List actual = highlightRepository.findAllByAnswerIdsOrderedAsc(List.of(1L)); + assertThat(actual) + .extracting(Highlight::getHighlightRange) + .containsExactly(new HighlightRange(1, 2), new HighlightRange(3, 5)); + } + @Test void 하이라이트를_줄번호_시작_인덱스_순서대로_정렬해서_가져온다() { // given diff --git a/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java b/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java index 207a68d29..c4ea9f3bb 100644 --- a/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java +++ b/backend/src/test/java/reviewme/highlight/service/HighlightServiceTest.java @@ -102,7 +102,7 @@ class HighlightServiceTest { highlightService.editHighlight(highlightsRequest, reviewGroup); // then - List highlights = highlightRepository.findAll(); + List highlights = highlightRepository.findAllByAnswerIdsOrderedAsc(List.of(textAnswer.getId())); assertAll( () -> assertThat(highlights.get(0).getAnswerId()).isEqualTo(textAnswer.getId()), () -> assertThat(highlights.get(0).getHighlightRange()).isEqualTo( From 9b59f8d58b1766af0263d4c19289392f55b157b6 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sun, 17 Nov 2024 16:01:21 +0900 Subject: [PATCH 17/60] =?UTF-8?q?[BE]=20refactor:=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EB=A7=A4=ED=95=91=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=84=EC=86=8C=ED=99=94=20(#962)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 하이라이트 매핑 로직 간소화 * refactor: `Fragments` 변환 로직 dto 내부로 이동 * refactor: 내부 값 사용하도록 변경 * refactor: Stream 가독성 향상 * style: 개행 삭제 * test: 하이라이트가 안 쳐지는 라인이 잘 변환되는지 테스트 Co-authored-by: Hyeonji <110809927+skylar1220@users.noreply.github.com> * refactor: 메서드 네임 확실하게 수정 --------- Co-authored-by: Hyeonji <110809927+skylar1220@users.noreply.github.com> --- .../highlight/domain/HighlightedLines.java | 10 +++ .../highlight/service/HighlightService.java | 1 - .../service/dto/HighlightRequest.java | 13 ++++ .../service/dto/HighlightsRequest.java | 7 +++ .../service/mapper/HighlightFragment.java | 4 ++ .../service/mapper/HighlightMapper.java | 63 ++++--------------- .../domain/HighlightedLinesTest.java | 17 +++++ 7 files changed, 62 insertions(+), 53 deletions(-) create mode 100644 backend/src/main/java/reviewme/highlight/service/mapper/HighlightFragment.java diff --git a/backend/src/main/java/reviewme/highlight/domain/HighlightedLines.java b/backend/src/main/java/reviewme/highlight/domain/HighlightedLines.java index f7000ecb2..f24827d30 100644 --- a/backend/src/main/java/reviewme/highlight/domain/HighlightedLines.java +++ b/backend/src/main/java/reviewme/highlight/domain/HighlightedLines.java @@ -2,6 +2,8 @@ import java.util.Arrays; import java.util.List; +import java.util.function.Function; +import java.util.stream.IntStream; import lombok.Getter; import reviewme.highlight.domain.exception.InvalidHighlightLineIndexException; import reviewme.highlight.domain.exception.NegativeHighlightLineIndexException; @@ -37,4 +39,12 @@ private void validateLineIndexRange(int lineIndex) { throw new InvalidHighlightLineIndexException(lineIndex, lines.size()); } } + + public List toHighlights(long answerId) { + return IntStream.range(0, lines.size()) + .mapToObj(lineIndex -> lines.get(lineIndex).getRanges().stream() + .map(range -> new Highlight(answerId, lineIndex, range))) + .flatMap(Function.identity()) + .toList(); + } } diff --git a/backend/src/main/java/reviewme/highlight/service/HighlightService.java b/backend/src/main/java/reviewme/highlight/service/HighlightService.java index 7cb9f9c70..cff5063a5 100644 --- a/backend/src/main/java/reviewme/highlight/service/HighlightService.java +++ b/backend/src/main/java/reviewme/highlight/service/HighlightService.java @@ -30,7 +30,6 @@ public void editHighlight(HighlightsRequest highlightsRequest, ReviewGroup revie Set answerIds = answerRepository.findIdsByQuestionId(highlightsRequest.questionId()); highlightRepository.deleteAllByAnswerIds(answerIds); - highlightRepository.saveAll(highlights); } } diff --git a/backend/src/main/java/reviewme/highlight/service/dto/HighlightRequest.java b/backend/src/main/java/reviewme/highlight/service/dto/HighlightRequest.java index 673cc8e6a..1371c6959 100644 --- a/backend/src/main/java/reviewme/highlight/service/dto/HighlightRequest.java +++ b/backend/src/main/java/reviewme/highlight/service/dto/HighlightRequest.java @@ -4,6 +4,8 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; +import java.util.stream.Stream; +import reviewme.highlight.service.mapper.HighlightFragment; public record HighlightRequest( @@ -13,4 +15,15 @@ public record HighlightRequest( @Valid @NotEmpty(message = "하이라이트 된 라인을 입력해주세요.") List lines ) { + public List toFragments() { + return lines.stream() + .flatMap(this::mapRangesToFragment) + .toList(); + } + + private Stream mapRangesToFragment(HighlightedLineRequest line) { + return line.ranges() + .stream() + .map(range -> new HighlightFragment(answerId, line.index(), range.startIndex(), range.endIndex())); + } } diff --git a/backend/src/main/java/reviewme/highlight/service/dto/HighlightsRequest.java b/backend/src/main/java/reviewme/highlight/service/dto/HighlightsRequest.java index b8f26cba6..b1f7f6de3 100644 --- a/backend/src/main/java/reviewme/highlight/service/dto/HighlightsRequest.java +++ b/backend/src/main/java/reviewme/highlight/service/dto/HighlightsRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; +import reviewme.highlight.service.mapper.HighlightFragment; public record HighlightsRequest( @@ -20,4 +21,10 @@ public List getUniqueAnswerIds() { .distinct() .toList(); } + + public List toFragments() { + return highlights.stream() + .flatMap(request -> request.toFragments().stream()) + .toList(); + } } diff --git a/backend/src/main/java/reviewme/highlight/service/mapper/HighlightFragment.java b/backend/src/main/java/reviewme/highlight/service/mapper/HighlightFragment.java new file mode 100644 index 000000000..33e08f56e --- /dev/null +++ b/backend/src/main/java/reviewme/highlight/service/mapper/HighlightFragment.java @@ -0,0 +1,4 @@ +package reviewme.highlight.service.mapper; + +public record HighlightFragment(long answerId, int lineIndex, int startIndex, int endIndex) { +} diff --git a/backend/src/main/java/reviewme/highlight/service/mapper/HighlightMapper.java b/backend/src/main/java/reviewme/highlight/service/mapper/HighlightMapper.java index edbec9013..512546030 100644 --- a/backend/src/main/java/reviewme/highlight/service/mapper/HighlightMapper.java +++ b/backend/src/main/java/reviewme/highlight/service/mapper/HighlightMapper.java @@ -1,21 +1,14 @@ package reviewme.highlight.service.mapper; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import reviewme.highlight.domain.HighlightedLines; -import reviewme.highlight.domain.HighlightedLine; import reviewme.highlight.domain.Highlight; -import reviewme.highlight.domain.HighlightRange; -import reviewme.highlight.service.dto.HighlightIndexRangeRequest; -import reviewme.highlight.service.dto.HighlightRequest; -import reviewme.highlight.service.dto.HighlightedLineRequest; +import reviewme.highlight.domain.HighlightedLines; import reviewme.highlight.service.dto.HighlightsRequest; -import reviewme.review.domain.Answer; +import reviewme.review.domain.TextAnswer; import reviewme.review.repository.TextAnswerRepository; @Component @@ -25,53 +18,19 @@ public class HighlightMapper { private final TextAnswerRepository textAnswerRepository; public List mapToHighlights(HighlightsRequest highlightsRequest) { - Map answerHighlightLines = textAnswerRepository + Map answerIdHighlightedLines = textAnswerRepository .findAllById(highlightsRequest.getUniqueAnswerIds()) .stream() - .collect(Collectors.toMap(Answer::getId, answer -> new HighlightedLines(answer.getContent()))); - addIndexRanges(highlightsRequest, answerHighlightLines); - return mapLinesToHighlights(answerHighlightLines); - } - - private void addIndexRanges(HighlightsRequest highlightsRequest, Map answerHighlightLines) { - for (HighlightRequest highlightRequest : highlightsRequest.highlights()) { - HighlightedLines highlightedLines = answerHighlightLines.get(highlightRequest.answerId()); - addIndexRangesForAnswer(highlightRequest, highlightedLines); - } - } - - private void addIndexRangesForAnswer(HighlightRequest highlightRequest, HighlightedLines highlightedLines) { - for (HighlightedLineRequest lineRequest : highlightRequest.lines()) { - int lineIndex = lineRequest.index(); - for (HighlightIndexRangeRequest rangeRequest : lineRequest.ranges()) { - highlightedLines.addRange(lineIndex, rangeRequest.startIndex(), rangeRequest.endIndex()); - } - } - } - - private List mapLinesToHighlights(Map answerHighlightLines) { - List highlights = new ArrayList<>(); - for (Entry answerHighlightLine : answerHighlightLines.entrySet()) { - createHighlightsForAnswer(answerHighlightLine, highlights); - } - return highlights; - } + .collect(Collectors.toMap(TextAnswer::getId, answer -> new HighlightedLines(answer.getContent()))); - private void createHighlightsForAnswer(Entry answerHighlightLine, - List highlights) { - long answerId = answerHighlightLine.getKey(); - List highlightedLines = answerHighlightLine.getValue().getLines(); - - for (int lineIndex = 0; lineIndex < highlightedLines.size(); lineIndex++) { - createHighlightForLine(highlightedLines, lineIndex, answerId, highlights); + for (HighlightFragment fragment : highlightsRequest.toFragments()) { + HighlightedLines highlightedLines = answerIdHighlightedLines.get(fragment.answerId()); + highlightedLines.addRange(fragment.lineIndex(), fragment.startIndex(), fragment.endIndex()); } - } - private void createHighlightForLine(List highlightedLines, int lineIndex, long answerId, - List highlights) { - for (HighlightRange range : highlightedLines.get(lineIndex).getRanges()) { - Highlight highlight = new Highlight(answerId, lineIndex, range); - highlights.add(highlight); - } + return answerIdHighlightedLines.entrySet() + .stream() + .flatMap(entry -> entry.getValue().toHighlights(entry.getKey()).stream()) + .toList(); } } diff --git a/backend/src/test/java/reviewme/highlight/domain/HighlightedLinesTest.java b/backend/src/test/java/reviewme/highlight/domain/HighlightedLinesTest.java index 53d81c209..d3e4a443c 100644 --- a/backend/src/test/java/reviewme/highlight/domain/HighlightedLinesTest.java +++ b/backend/src/test/java/reviewme/highlight/domain/HighlightedLinesTest.java @@ -84,4 +84,21 @@ class HighlightedLinesTest { assertThatCode(() -> highlightedLines.addRange(invalidLineIndex, 0, 1)) .isInstanceOf(InvalidHighlightLineIndexException.class); } + + @Test + void 하이라이트가_존재하는_부분만_엔티티로_변환한다() { + // given + HighlightedLines lines = new HighlightedLines("0\n11\n222"); + lines.addRange(0, 0, 0); + lines.addRange(2, 2, 2); + + // when + List highlights = lines.toHighlights(1L); + + // then + assertThat(highlights).containsExactly( + new Highlight(1L, 0, new HighlightRange(0, 0)), + new Highlight(1L, 2, new HighlightRange(2, 2)) + ); + } } From 9e017a19ba06fb887ea305dd02a0a9fcb4295570 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Mon, 18 Nov 2024 01:12:21 +0900 Subject: [PATCH 18/60] =?UTF-8?q?[BE]=20refactor/fix:=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EA=B2=80=EC=A6=9D=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=B1=85=EC=9E=84=20=EC=9D=B4=EB=8F=99,=20?= =?UTF-8?q?=ED=95=98=EC=9D=B4=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: `HighlightValidator` 삭제 * chore: add todos * refactor: 타입에 따라 검증하는 객체 이름에 `Typed-`를 접두사로 추가 * feat: `AnswerValidator`에서 정답과 관련된 검증 진행 * refactor: `List` -> `Collection` 받도록 수정 * fix: 리뷰 그룹에 해당하는 하이라이트만 삭제하도록 수정 * refactor: 사용하지 않는 상수 삭제 * refactor: `TypedAnswerValidatorFactory`로 수정 * refactor: 쿼리 수정, 테스트 추가 --- .../repository/HighlightRepository.java | 19 +-- .../highlight/service/HighlightService.java | 16 ++- ...werAndProvidedAnswerMismatchException.java | 16 --- .../service/validator/HighlightValidator.java | 41 ------- ...QuestionNotContainingAnswersException.java | 15 +++ ...iewGroupNotContainingAnswersException.java | 15 +++ .../service/validator/AnswerValidator.java | 29 ++++- ...java => CheckboxTypedAnswerValidator.java} | 2 +- .../service/validator/ReviewValidator.java | 12 +- ...tor.java => TextTypedAnswerValidator.java} | 3 +- .../validator/TypedAnswerValidator.java | 10 ++ ....java => TypedAnswerValidatorFactory.java} | 8 +- .../repository/HighlightRepositoryTest.java | 53 +++++++++ .../validator/HighlightValidatorTest.java | 109 ------------------ .../validator/AnswerValidatorTest.java | 90 +++++++++++++++ ... => CheckboxTypedAnswerValidatorTest.java} | 4 +- ...java => TextTypedAnswerValidatorTest.java} | 4 +- ...TypedTypedAnswerValidatorFactoryTest.java} | 13 ++- 18 files changed, 249 insertions(+), 210 deletions(-) delete mode 100644 backend/src/main/java/reviewme/highlight/service/exception/SubmittedAnswerAndProvidedAnswerMismatchException.java delete mode 100644 backend/src/main/java/reviewme/highlight/service/validator/HighlightValidator.java create mode 100644 backend/src/main/java/reviewme/review/service/exception/QuestionNotContainingAnswersException.java create mode 100644 backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotContainingAnswersException.java rename backend/src/main/java/reviewme/review/service/validator/{CheckboxAnswerValidator.java => CheckboxTypedAnswerValidator.java} (97%) rename backend/src/main/java/reviewme/review/service/validator/{TextAnswerValidator.java => TextTypedAnswerValidator.java} (94%) create mode 100644 backend/src/main/java/reviewme/review/service/validator/TypedAnswerValidator.java rename backend/src/main/java/reviewme/review/service/validator/{AnswerValidatorFactory.java => TypedAnswerValidatorFactory.java} (68%) delete mode 100644 backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java create mode 100644 backend/src/test/java/reviewme/review/service/validator/AnswerValidatorTest.java rename backend/src/test/java/reviewme/review/service/validator/{CheckboxAnswerValidatorTest.java => CheckboxTypedAnswerValidatorTest.java} (97%) rename backend/src/test/java/reviewme/review/service/validator/{TextAnswerValidatorTest.java => TextTypedAnswerValidatorTest.java} (96%) rename backend/src/test/java/reviewme/review/service/validator/{AnswerValidatorFactoryTest.java => TypedTypedAnswerValidatorFactoryTest.java} (68%) diff --git a/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java b/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java index 104e4cfc1..2733b2027 100644 --- a/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java +++ b/backend/src/main/java/reviewme/highlight/repository/HighlightRepository.java @@ -13,18 +13,21 @@ public interface HighlightRepository extends Repository, Highli boolean existsById(long id); - @Modifying @Query(""" - DELETE FROM Highlight h + SELECT h FROM Highlight h WHERE h.answerId IN :answerIds + ORDER BY h.lineIndex, h.highlightRange.startIndex ASC """) - void deleteAllByAnswerIds(Collection answerIds); + List findAllByAnswerIdsOrderedAsc(Collection answerIds); + @Modifying @Query(""" - SELECT h - FROM Highlight h - WHERE h.answerId IN :answerIds - ORDER BY h.lineIndex, h.highlightRange.startIndex ASC + DELETE FROM Highlight h + WHERE h.answerId IN ( + SELECT a.id FROM Answer a + JOIN Review r ON a.reviewId = r.id + WHERE r.reviewGroupId = :reviewGroupId AND a.questionId = :questionId + ) """) - List findAllByAnswerIdsOrderedAsc(Collection answerIds); + void deleteByReviewGroupIdAndQuestionId(long reviewGroupId, long questionId); } diff --git a/backend/src/main/java/reviewme/highlight/service/HighlightService.java b/backend/src/main/java/reviewme/highlight/service/HighlightService.java index cff5063a5..8651bca86 100644 --- a/backend/src/main/java/reviewme/highlight/service/HighlightService.java +++ b/backend/src/main/java/reviewme/highlight/service/HighlightService.java @@ -1,7 +1,6 @@ package reviewme.highlight.service; import java.util.List; -import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,8 +8,7 @@ import reviewme.highlight.repository.HighlightRepository; import reviewme.highlight.service.dto.HighlightsRequest; import reviewme.highlight.service.mapper.HighlightMapper; -import reviewme.highlight.service.validator.HighlightValidator; -import reviewme.review.repository.AnswerRepository; +import reviewme.review.service.validator.AnswerValidator; import reviewme.reviewgroup.domain.ReviewGroup; @Service @@ -18,18 +16,18 @@ public class HighlightService { private final HighlightRepository highlightRepository; - private final AnswerRepository answerRepository; - private final HighlightValidator highlightValidator; private final HighlightMapper highlightMapper; + private final AnswerValidator answerValidator; @Transactional public void editHighlight(HighlightsRequest highlightsRequest, ReviewGroup reviewGroup) { - highlightValidator.validate(highlightsRequest, reviewGroup); - List highlights = highlightMapper.mapToHighlights(highlightsRequest); + List requestedAnswerIds = highlightsRequest.getUniqueAnswerIds(); + answerValidator.validateQuestionContainsAnswers(highlightsRequest.questionId(), requestedAnswerIds); + answerValidator.validateReviewGroupContainsAnswers(reviewGroup, requestedAnswerIds); - Set answerIds = answerRepository.findIdsByQuestionId(highlightsRequest.questionId()); - highlightRepository.deleteAllByAnswerIds(answerIds); + List highlights = highlightMapper.mapToHighlights(highlightsRequest); + highlightRepository.deleteByReviewGroupIdAndQuestionId(reviewGroup.getId(), highlightsRequest.questionId()); highlightRepository.saveAll(highlights); } } diff --git a/backend/src/main/java/reviewme/highlight/service/exception/SubmittedAnswerAndProvidedAnswerMismatchException.java b/backend/src/main/java/reviewme/highlight/service/exception/SubmittedAnswerAndProvidedAnswerMismatchException.java deleted file mode 100644 index 0282bd983..000000000 --- a/backend/src/main/java/reviewme/highlight/service/exception/SubmittedAnswerAndProvidedAnswerMismatchException.java +++ /dev/null @@ -1,16 +0,0 @@ -package reviewme.highlight.service.exception; - -import java.util.Collection; -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.BadRequestException; - -@Slf4j -public class SubmittedAnswerAndProvidedAnswerMismatchException extends BadRequestException { - - public SubmittedAnswerAndProvidedAnswerMismatchException(Collection providedAnswerIds, - Collection submittedAnswerIds) { - super("제출된 응답이 제공된 응답과 일치하지 않아요."); - log.info("SubmittedAnswer and providedAnswer mismatch - providedAnswerIds: {}, submittedAnswerIds: {}", - providedAnswerIds, submittedAnswerIds); - } -} diff --git a/backend/src/main/java/reviewme/highlight/service/validator/HighlightValidator.java b/backend/src/main/java/reviewme/highlight/service/validator/HighlightValidator.java deleted file mode 100644 index e05f0f9df..000000000 --- a/backend/src/main/java/reviewme/highlight/service/validator/HighlightValidator.java +++ /dev/null @@ -1,41 +0,0 @@ - -package reviewme.highlight.service.validator; - -import java.util.List; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import reviewme.highlight.service.dto.HighlightsRequest; -import reviewme.highlight.service.exception.SubmittedAnswerAndProvidedAnswerMismatchException; -import reviewme.review.repository.AnswerRepository; -import reviewme.reviewgroup.domain.ReviewGroup; - -@Component -@RequiredArgsConstructor -public class HighlightValidator { - - private final AnswerRepository answerRepository; - - public void validate(HighlightsRequest request, ReviewGroup reviewGroup) { - validateQuestionContainsAnswer(request); - validateReviewGroupContainsAnswer(request, reviewGroup); - } - - private void validateQuestionContainsAnswer(HighlightsRequest request) { - Set providedAnswerIds = answerRepository.findIdsByQuestionId(request.questionId()); - List submittedAnswerIds = request.getUniqueAnswerIds(); - - if (!providedAnswerIds.containsAll(submittedAnswerIds)) { - throw new SubmittedAnswerAndProvidedAnswerMismatchException(providedAnswerIds, submittedAnswerIds); - } - } - - private void validateReviewGroupContainsAnswer(HighlightsRequest request, ReviewGroup reviewGroup) { - Set providedAnswerIds = answerRepository.findIdsByReviewGroupId(reviewGroup.getId()); - List submittedAnswerIds = request.getUniqueAnswerIds(); - - if (!providedAnswerIds.containsAll(submittedAnswerIds)) { - throw new SubmittedAnswerAndProvidedAnswerMismatchException(providedAnswerIds, submittedAnswerIds); - } - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/QuestionNotContainingAnswersException.java b/backend/src/main/java/reviewme/review/service/exception/QuestionNotContainingAnswersException.java new file mode 100644 index 000000000..3a7740787 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/exception/QuestionNotContainingAnswersException.java @@ -0,0 +1,15 @@ +package reviewme.review.service.exception; + +import java.util.Collection; +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.ReviewMeException; + +@Slf4j +public class QuestionNotContainingAnswersException extends ReviewMeException { + + public QuestionNotContainingAnswersException(long questionId, Collection providedAnswerIds) { + super("질문에 속하지 않는 답변이예요."); + log.info("Question not containing provided answers - questionId: {}, providedAnswerIds: {}", + questionId, providedAnswerIds); + } +} diff --git a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotContainingAnswersException.java b/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotContainingAnswersException.java new file mode 100644 index 000000000..7f641512f --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotContainingAnswersException.java @@ -0,0 +1,15 @@ +package reviewme.review.service.exception; + +import java.util.Collection; +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.ReviewMeException; + +@Slf4j +public class ReviewGroupNotContainingAnswersException extends ReviewMeException { + + public ReviewGroupNotContainingAnswersException(long reviewGroupId, Collection providedAnswerIds) { + super("리뷰 그룹에 속하지 않는 답변이예요."); + log.info("ReviewGroup not containing provided answers - reviewGroupId: {}, providedAnswerIds: {}", + reviewGroupId, providedAnswerIds); + } +} diff --git a/backend/src/main/java/reviewme/review/service/validator/AnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/AnswerValidator.java index 11162cc26..bb9f85434 100644 --- a/backend/src/main/java/reviewme/review/service/validator/AnswerValidator.java +++ b/backend/src/main/java/reviewme/review/service/validator/AnswerValidator.java @@ -1,10 +1,31 @@ package reviewme.review.service.validator; -import reviewme.review.domain.Answer; +import java.util.Collection; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.review.repository.AnswerRepository; +import reviewme.review.service.exception.QuestionNotContainingAnswersException; +import reviewme.review.service.exception.ReviewGroupNotContainingAnswersException; +import reviewme.reviewgroup.domain.ReviewGroup; -public interface AnswerValidator { +@Component +@RequiredArgsConstructor +public class AnswerValidator { - boolean supports(Class answerClass); + private final AnswerRepository answerRepository; - void validate(Answer answer); + public void validateQuestionContainsAnswers(long questionId, Collection answerIds) { + Set receivedAnswerIds = answerRepository.findIdsByQuestionId(questionId); + if (!receivedAnswerIds.containsAll(answerIds)) { + throw new QuestionNotContainingAnswersException(questionId, answerIds); + } + } + + public void validateReviewGroupContainsAnswers(ReviewGroup reviewGroup, Collection answerIds) { + Set receivedAnswerIds = answerRepository.findIdsByReviewGroupId(reviewGroup.getId()); + if (!receivedAnswerIds.containsAll(answerIds)) { + throw new ReviewGroupNotContainingAnswersException(reviewGroup.getId(), answerIds); + } + } } diff --git a/backend/src/main/java/reviewme/review/service/validator/CheckboxAnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/CheckboxTypedAnswerValidator.java similarity index 97% rename from backend/src/main/java/reviewme/review/service/validator/CheckboxAnswerValidator.java rename to backend/src/main/java/reviewme/review/service/validator/CheckboxTypedAnswerValidator.java index ab57ead79..d6cd50eec 100644 --- a/backend/src/main/java/reviewme/review/service/validator/CheckboxAnswerValidator.java +++ b/backend/src/main/java/reviewme/review/service/validator/CheckboxTypedAnswerValidator.java @@ -21,7 +21,7 @@ @Component @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class CheckboxAnswerValidator implements AnswerValidator { +public class CheckboxTypedAnswerValidator implements TypedAnswerValidator { private final QuestionRepository questionRepository; private final OptionGroupRepository optionGroupRepository; diff --git a/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java b/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java index 9c68894c9..96fb55ec4 100644 --- a/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java +++ b/backend/src/main/java/reviewme/review/service/validator/ReviewValidator.java @@ -7,23 +7,23 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import reviewme.template.domain.Question; -import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.Answer; -import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.domain.Review; import reviewme.review.service.exception.MissingRequiredQuestionException; import reviewme.review.service.exception.SubmittedQuestionAndProvidedQuestionMismatchException; +import reviewme.template.domain.Question; import reviewme.template.domain.Section; import reviewme.template.domain.SectionQuestion; +import reviewme.template.repository.QuestionRepository; import reviewme.template.repository.SectionRepository; @Component @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public class ReviewValidator { - private final AnswerValidatorFactory answerValidatorFactory; + private final TypedAnswerValidatorFactory typedAnswerValidatorFactory; private final SectionRepository sectionRepository; private final QuestionRepository questionRepository; @@ -36,8 +36,8 @@ public void validate(Review review) { private void validateAnswer(List answers) { for (Answer answer : answers) { - AnswerValidator validator = answerValidatorFactory.getAnswerValidator(answer.getClass()); - validator.validate(answer); + typedAnswerValidatorFactory.getAnswerValidator(answer.getClass()) + .validate(answer); } } diff --git a/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/TextTypedAnswerValidator.java similarity index 94% rename from backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java rename to backend/src/main/java/reviewme/review/service/validator/TextTypedAnswerValidator.java index 59d9d476b..0dffa56c1 100644 --- a/backend/src/main/java/reviewme/review/service/validator/TextAnswerValidator.java +++ b/backend/src/main/java/reviewme/review/service/validator/TextTypedAnswerValidator.java @@ -12,9 +12,8 @@ @Component @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class TextAnswerValidator implements AnswerValidator { +public class TextTypedAnswerValidator implements TypedAnswerValidator { - private static final int ZERO_LENGTH = 0; private static final int MIN_LENGTH = 20; private static final int MAX_LENGTH = 1_000; diff --git a/backend/src/main/java/reviewme/review/service/validator/TypedAnswerValidator.java b/backend/src/main/java/reviewme/review/service/validator/TypedAnswerValidator.java new file mode 100644 index 000000000..2bc060c52 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/validator/TypedAnswerValidator.java @@ -0,0 +1,10 @@ +package reviewme.review.service.validator; + +import reviewme.review.domain.Answer; + +public interface TypedAnswerValidator { + + boolean supports(Class answerClass); + + void validate(Answer answer); +} diff --git a/backend/src/main/java/reviewme/review/service/validator/AnswerValidatorFactory.java b/backend/src/main/java/reviewme/review/service/validator/TypedAnswerValidatorFactory.java similarity index 68% rename from backend/src/main/java/reviewme/review/service/validator/AnswerValidatorFactory.java rename to backend/src/main/java/reviewme/review/service/validator/TypedAnswerValidatorFactory.java index b1adc5933..a0ff54733 100644 --- a/backend/src/main/java/reviewme/review/service/validator/AnswerValidatorFactory.java +++ b/backend/src/main/java/reviewme/review/service/validator/TypedAnswerValidatorFactory.java @@ -8,12 +8,12 @@ @Component @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class AnswerValidatorFactory { +public class TypedAnswerValidatorFactory { - private final List answerValidators; + private final List validators; - public AnswerValidator getAnswerValidator(Class answerClass) { - return answerValidators.stream() + public TypedAnswerValidator getAnswerValidator(Class answerClass) { + return validators.stream() .filter(validator -> validator.supports(answerClass)) .findFirst() .orElseThrow(() -> new UnsupportedAnswerTypeException(answerClass)); diff --git a/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java b/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java index 6e46ed088..528ccbfff 100644 --- a/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java +++ b/backend/src/test/java/reviewme/highlight/repository/HighlightRepositoryTest.java @@ -2,13 +2,21 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import java.util.ArrayList; 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.highlight.domain.Highlight; import reviewme.highlight.domain.HighlightRange; +import reviewme.review.domain.Answer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.ReviewRepository; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; @DataJpaTest class HighlightRepositoryTest { @@ -16,6 +24,12 @@ class HighlightRepositoryTest { @Autowired private HighlightRepository highlightRepository; + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + @Test void 한_번에_여러_하이라이트를_벌크_삽입한다() { // given @@ -62,4 +76,43 @@ class HighlightRepositoryTest { .containsExactly(1, 4, 2, 6, 3) ); } + + @Test + void 그룹_아이디와_질문_아이디로_하이라이트를_삭제한다() { + // given + ReviewGroup reviewGroup1 = reviewGroupRepository.save(리뷰_그룹()); + ReviewGroup reviewGroup2 = reviewGroupRepository.save(리뷰_그룹()); + + List answers1 = List.of( + new TextAnswer(1L, "A1"), + new TextAnswer(2L, "A2"), + new TextAnswer(3L, "A3") + ); + List answers2 = List.of( + new TextAnswer(1L, "B1"), + new TextAnswer(2L, "B2"), + new TextAnswer(3L, "B3") + ); + reviewRepository.save(new Review(1L, reviewGroup1.getId(), answers1)); + reviewRepository.save(new Review(2L, reviewGroup2.getId(), answers2)); + + List answerIds = new ArrayList<>(); + answerIds.addAll(answers1.stream().map(Answer::getId).toList()); + answerIds.addAll(answers2.stream().map(Answer::getId).toList()); + + HighlightRange range = new HighlightRange(0, 1); + answerIds.stream() + .map(answerId -> new Highlight(answerId, 0, range)) + .forEach(highlightRepository::save); + + // when + highlightRepository.deleteByReviewGroupIdAndQuestionId(reviewGroup1.getId(), 1L); + + // then + List actual = highlightRepository.findAllByAnswerIdsOrderedAsc(answerIds); + assertAll( + () -> assertThat(actual).hasSize(5), + () -> assertThat(actual).extracting(Highlight::getAnswerId).doesNotContain(answers1.get(0).getId()) + ); + } } diff --git a/backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java b/backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java deleted file mode 100644 index 4c98fa75b..000000000 --- a/backend/src/test/java/reviewme/highlight/service/validator/HighlightValidatorTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package reviewme.highlight.service.validator; - -import static org.assertj.core.api.Assertions.assertThatCode; -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.highlight.service.dto.HighlightRequest; -import reviewme.highlight.service.dto.HighlightsRequest; -import reviewme.highlight.service.exception.SubmittedAnswerAndProvidedAnswerMismatchException; -import reviewme.template.repository.QuestionRepository; -import reviewme.review.domain.Review; -import reviewme.review.domain.TextAnswer; -import reviewme.review.repository.ReviewRepository; -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 HighlightValidatorTest { - - @Autowired - private HighlightValidator highlightValidator; - - @Autowired - private ReviewGroupRepository reviewGroupRepository; - - @Autowired - private ReviewRepository reviewRepository; - - @Autowired - private QuestionRepository questionRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TemplateRepository templateRepository; - - @Test - void 하이라이트의_답변_id가_하이라이트의_질문_id에_해당하는_답변이_아니면_예외를_발생한다() { - // given - long questionId1 = questionRepository.save(서술형_필수_질문()).getId(); - long questionId2 = questionRepository.save(서술형_필수_질문()).getId(); - Section section = sectionRepository.save(항상_보이는_섹션(List.of(questionId1, questionId2))); - Template template = templateRepository.save(템플릿(List.of(section.getId()))); - - ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); - TextAnswer textAnswer_Q1 = new TextAnswer(questionId1, "text answer 1"); - - HighlightRequest highlightRequest = new HighlightRequest(textAnswer_Q1.getId(), List.of()); - HighlightsRequest highlightsRequest = new HighlightsRequest(questionId2, List.of(highlightRequest)); - - // when && then - assertThatCode(() -> highlightValidator.validate(highlightsRequest, reviewGroup)) - .isInstanceOf(SubmittedAnswerAndProvidedAnswerMismatchException.class); - } - - @Test - void 하이라이트의_답변_id가_리뷰_그룹에_달린_답변이_아니면_예외를_발생한다() { - // given - long questionId = questionRepository.save(서술형_필수_질문()).getId(); - Section section = sectionRepository.save(항상_보이는_섹션(List.of(questionId))); - Template template = templateRepository.save(템플릿(List.of(section.getId()))); - - ReviewGroup reviewGroup1 = reviewGroupRepository.save(리뷰_그룹()); - ReviewGroup reviewGroup2 = reviewGroupRepository.save(리뷰_그룹()); - TextAnswer textAnswer1 = new TextAnswer(questionId, "text answer1"); - TextAnswer textAnswer2 = new TextAnswer(questionId, "text answer2"); - reviewRepository.saveAll(List.of( - new Review(template.getId(), reviewGroup1.getId(), List.of(textAnswer1)), - new Review(template.getId(), reviewGroup2.getId(), List.of(textAnswer2)) - )); - - HighlightRequest highlightRequest = new HighlightRequest(textAnswer2.getId(), List.of()); - HighlightsRequest highlightsRequest = new HighlightsRequest(1L, List.of(highlightRequest)); - - // when && then - assertThatCode(() -> highlightValidator.validate(highlightsRequest, reviewGroup1)) - .isInstanceOf(SubmittedAnswerAndProvidedAnswerMismatchException.class); - } - - @Test - void 하이라이트의_질문_id가_리뷰_그룹의_템플릿에_속한_질문이_아니면_예외를_발생한다() { - // given - long questionId1 = questionRepository.save(서술형_필수_질문()).getId(); - long questionId2 = questionRepository.save(서술형_필수_질문()).getId(); - Section section = sectionRepository.save(항상_보이는_섹션(List.of(questionId1))); - Template template = templateRepository.save(템플릿(List.of(section.getId()))); - - ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); - TextAnswer textAnswer_Q1 = new TextAnswer(questionId1, "text answer 1"); - - HighlightRequest highlightRequest = new HighlightRequest(textAnswer_Q1.getId(), List.of()); - HighlightsRequest highlightsRequest = new HighlightsRequest(questionId2, List.of(highlightRequest)); - - // when && then - assertThatCode(() -> highlightValidator.validate(highlightsRequest, reviewGroup)) - .isInstanceOf(SubmittedAnswerAndProvidedAnswerMismatchException.class); - } -} diff --git a/backend/src/test/java/reviewme/review/service/validator/AnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/AnswerValidatorTest.java new file mode 100644 index 000000000..9cce52a1b --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/validator/AnswerValidatorTest.java @@ -0,0 +1,90 @@ +package reviewme.review.service.validator; + +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 reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.review.domain.Answer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.exception.QuestionNotContainingAnswersException; +import reviewme.review.service.exception.ReviewGroupNotContainingAnswersException; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Question; +import reviewme.template.repository.QuestionRepository; + +@ServiceTest +class AnswerValidatorTest { + + @Autowired + private AnswerValidator answerValidator; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Test + void 답변이_질문에_속하는지_검증한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + List answers = List.of( + new TextAnswer(question1.getId(), "답변1"), + new TextAnswer(question2.getId(), "답변2") + ); + Review review = reviewRepository.save(new Review(1L, reviewGroup.getId(), answers)); + Set answerIds = review.getAnsweredQuestionIds(); + List firstAnswerId = List.of(answers.get(0).getId()); + + // when, then + assertAll( + () -> assertDoesNotThrow( + () -> answerValidator.validateQuestionContainsAnswers(question1.getId(), firstAnswerId)), + () -> assertThatThrownBy( + () -> answerValidator.validateQuestionContainsAnswers(question1.getId(), answerIds)) + .isInstanceOf(QuestionNotContainingAnswersException.class) + ); + } + + @Test + void 답변이_리뷰그룹에_속하는지_검증한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + List answers = List.of( + new TextAnswer(question1.getId(), "답변1"), + new TextAnswer(question2.getId(), "답변2") + ); + Review review = reviewRepository.save(new Review(1L, reviewGroup.getId(), answers)); + + List answerIds = review.getAnswers().stream().map(Answer::getQuestionId).toList(); + List wrongAnswerIds = new ArrayList<>(answerIds); + wrongAnswerIds.add(Long.MAX_VALUE); + + // when, then + assertAll( + () -> assertDoesNotThrow( + () -> answerValidator.validateReviewGroupContainsAnswers(reviewGroup, answerIds)), + () -> assertThatThrownBy( + () -> answerValidator.validateReviewGroupContainsAnswers(reviewGroup, wrongAnswerIds)) + .isInstanceOf(ReviewGroupNotContainingAnswersException.class) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/service/validator/CheckboxAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/CheckboxTypedAnswerValidatorTest.java similarity index 97% rename from backend/src/test/java/reviewme/review/service/validator/CheckboxAnswerValidatorTest.java rename to backend/src/test/java/reviewme/review/service/validator/CheckboxTypedAnswerValidatorTest.java index c405ab4ba..57e7dfd4b 100644 --- a/backend/src/test/java/reviewme/review/service/validator/CheckboxAnswerValidatorTest.java +++ b/backend/src/test/java/reviewme/review/service/validator/CheckboxTypedAnswerValidatorTest.java @@ -22,10 +22,10 @@ import reviewme.support.ServiceTest; @ServiceTest -class CheckboxAnswerValidatorTest { +class CheckboxTypedAnswerValidatorTest { @Autowired - private CheckboxAnswerValidator checkBoxAnswerValidator; + private CheckboxTypedAnswerValidator checkBoxAnswerValidator; @Autowired private QuestionRepository questionRepository; diff --git a/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/validator/TextTypedAnswerValidatorTest.java similarity index 96% rename from backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java rename to backend/src/test/java/reviewme/review/service/validator/TextTypedAnswerValidatorTest.java index 27854e02c..9773112e8 100644 --- a/backend/src/test/java/reviewme/review/service/validator/TextAnswerValidatorTest.java +++ b/backend/src/test/java/reviewme/review/service/validator/TextTypedAnswerValidatorTest.java @@ -17,10 +17,10 @@ import reviewme.support.ServiceTest; @ServiceTest -class TextAnswerValidatorTest { +class TextTypedAnswerValidatorTest { @Autowired - private TextAnswerValidator textAnswerValidator; + private TextTypedAnswerValidator textAnswerValidator; @Autowired private QuestionRepository questionRepository; diff --git a/backend/src/test/java/reviewme/review/service/validator/AnswerValidatorFactoryTest.java b/backend/src/test/java/reviewme/review/service/validator/TypedTypedAnswerValidatorFactoryTest.java similarity index 68% rename from backend/src/test/java/reviewme/review/service/validator/AnswerValidatorFactoryTest.java rename to backend/src/test/java/reviewme/review/service/validator/TypedTypedAnswerValidatorFactoryTest.java index 0a6e75db5..d808c53d3 100644 --- a/backend/src/test/java/reviewme/review/service/validator/AnswerValidatorFactoryTest.java +++ b/backend/src/test/java/reviewme/review/service/validator/TypedTypedAnswerValidatorFactoryTest.java @@ -8,9 +8,9 @@ import reviewme.review.domain.Answer; import reviewme.review.domain.CheckboxAnswer; -class AnswerValidatorFactoryTest { +class TypedTypedAnswerValidatorFactoryTest { - private final AnswerValidator validator = new AnswerValidator() { + private final TypedAnswerValidator validator = new TypedAnswerValidator() { @Override public boolean supports(Class answerClass) { @@ -19,17 +19,18 @@ public boolean supports(Class answerClass) { @Override public void validate(Answer answer) { + // no-op } }; @Test void 지원하는_타입에_따른_밸리데이터를_가져온다() { // given - List validators = List.of(validator); - AnswerValidatorFactory factory = new AnswerValidatorFactory(validators); + List validators = List.of(validator); + TypedAnswerValidatorFactory factory = new TypedAnswerValidatorFactory(validators); // when - AnswerValidator actual = factory.getAnswerValidator(CheckboxAnswer.class); + TypedAnswerValidator actual = factory.getAnswerValidator(CheckboxAnswer.class); // then assertThat(actual).isEqualTo(validator); @@ -38,7 +39,7 @@ public void validate(Answer answer) { @Test void 지원하지_않는_타입에_대한_밸리데이터_요청_시_예외가_발생한다() { // given - AnswerValidatorFactory factory = new AnswerValidatorFactory(List.of()); + TypedAnswerValidatorFactory factory = new TypedAnswerValidatorFactory(List.of()); // when, then assertThatThrownBy(() -> factory.getAnswerValidator(CheckboxAnswer.class)) From 1b8799328a550a2174b6104bfb7c6d6f4664ee25 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Mon, 18 Nov 2024 14:23:01 +0900 Subject: [PATCH 19/60] =?UTF-8?q?[All]=20fix:=20Release=20PR=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A6=AC=EB=B7=B0=20=EC=8B=9C=20=EB=94=94=EC=8A=A4?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=95=8C=EB=A6=BC=20(#969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/discord-pull-request-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/discord-pull-request-comment.yml b/.github/workflows/discord-pull-request-comment.yml index e1a3fc36b..d3594088f 100644 --- a/.github/workflows/discord-pull-request-comment.yml +++ b/.github/workflows/discord-pull-request-comment.yml @@ -31,7 +31,7 @@ jobs: elif [ "$PR_PREFIX" = '[FE]' ]; then echo Frontend PR Found! echo "PR_PREFIX=FE" >> $GITHUB_ENV - elif [ "$PR_PREFIX" = '[All]' ]; then + elif [ "$PR_PREFIX" = '[All]' ] || [ "$PR_PREFIX" = '[Release]' ]; then echo All PR Found! echo "PR_PREFIX=All" >> $GITHUB_ENV fi From 90646b60946aa52bd9aea5befbc0ba5bc41a7d4f Mon Sep 17 00:00:00 2001 From: badahertz52 Date: Tue, 19 Nov 2024 16:17:39 +0900 Subject: [PATCH 20/60] =?UTF-8?q?[FE]=20fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=AA=A8=EC=95=84=EB=B3=B4=EA=B8=B0=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20=ED=98=95=EA=B4=91=ED=8E=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B0=98=EC=98=81=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 질문별 모아보기 목 데이터 주관식 답변 변경 * feat : 현재 sectionId sessionSotrage에 저장하고, 모아보기 페이지 언마운트 시 삭제하는 기능 추가 * feat : 형광펜 API 요청 성공 후, 해당 질문 쿼리 무효화하는 기능 추가 * feat : 형광펜 데이터 로컬 스토리지에 저장한 코드 삭제 * feat : ReviewCollectionPage 에서 clearEditorAnswerMapStorage 코드 삭제 --- .../HighlightEditor/hooks/useHighlight.ts | 19 ++--------------- .../hooks/useMutateHighlight/index.ts | 21 +++++++++++++++++-- frontend/src/constants/storageKey.ts | 2 +- .../src/mocks/mockData/reviewCollection.ts | 12 +++++------ .../ReviewCollectionPageContents/index.tsx | 11 ++++++++-- .../hooks/useGetGroupedReviews.ts | 3 ++- .../src/pages/ReviewCollectionPage/index.tsx | 21 ------------------- 7 files changed, 39 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/highlight/components/HighlightEditor/hooks/useHighlight.ts b/frontend/src/components/highlight/components/HighlightEditor/hooks/useHighlight.ts index 703f8acd7..2b91ac8e3 100644 --- a/frontend/src/components/highlight/components/HighlightEditor/hooks/useHighlight.ts +++ b/frontend/src/components/highlight/components/HighlightEditor/hooks/useHighlight.ts @@ -1,11 +1,6 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; -import { - EDITOR_ANSWER_CLASS_NAME, - HIGHLIGHT_EVENT_NAME, - HIGHLIGHT_SPAN_CLASS_NAME, - SESSION_STORAGE_KEY, -} from '@/constants'; +import { EDITOR_ANSWER_CLASS_NAME, HIGHLIGHT_EVENT_NAME, HIGHLIGHT_SPAN_CLASS_NAME } from '@/constants'; import { EditorAnswerMap, EditorLine, HighlightResponseData, ReviewAnswerResponseData } from '@/types'; import { getEndLineOffset, @@ -76,14 +71,6 @@ const useHighlight = ({ handleModalMessage, }: UseHighlightProps) => { const [editorAnswerMap, setEditorAnswerMap] = useState(makeInitialEditorAnswerMap(answerList)); - const storageKey = `${SESSION_STORAGE_KEY.editorAnswerMap}-${questionId}`; - - useEffect(() => { - const item = localStorage.getItem(storageKey); - if (item) { - setEditorAnswerMap(new Map(JSON.parse(item)) as EditorAnswerMap); - } - }, []); // span 클릭 시, 제공되는 형광펜 삭제 기능 타겟 const [longPressRemovalTarget, setLongPressRemovalTarget] = useState(null); @@ -92,8 +79,6 @@ const useHighlight = ({ const updateEditorAnswerMap = (newEditorAnswerMap: EditorAnswerMap) => { setEditorAnswerMap(newEditorAnswerMap); - // editorAnswerMap이 변경될 때 새로운 값을 로컬 스토리지에 저장 - localStorage.setItem(storageKey, JSON.stringify(Array.from(newEditorAnswerMap))); }; const resetHighlightMenu = () => { diff --git a/frontend/src/components/highlight/components/HighlightEditor/hooks/useMutateHighlight/index.ts b/frontend/src/components/highlight/components/HighlightEditor/hooks/useMutateHighlight/index.ts index 56681e41e..d607901cc 100644 --- a/frontend/src/components/highlight/components/HighlightEditor/hooks/useMutateHighlight/index.ts +++ b/frontend/src/components/highlight/components/HighlightEditor/hooks/useMutateHighlight/index.ts @@ -1,7 +1,7 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { postHighlight } from '@/apis/highlight'; -import { LOCAL_STORAGE_KEY } from '@/constants'; +import { LOCAL_STORAGE_KEY, REVIEW_QUERY_KEY, SESSION_STORAGE_KEY } from '@/constants'; import { EditorAnswerMap } from '@/types'; export interface UseMutateHighlightProps { @@ -17,6 +17,21 @@ const useMutateHighlight = ({ updateEditorAnswerMap, resetHighlightMenu, }: UseMutateHighlightProps) => { + const queryClient = useQueryClient(); + /** + * 형광펜 API 성공 후, 현재 질문에 대한 쿼리 캐시 무효화해서, 변경된 형광펜 데이터 불러오도록 함 + */ + const invalidateCurrentSectionQuery = () => { + const sectionId = sessionStorage.getItem(SESSION_STORAGE_KEY.currentReviewCollectionSectionId); + + if (sectionId) { + queryClient.invalidateQueries({ + predicate: (query) => + query.queryKey[0] === REVIEW_QUERY_KEY.groupedReviews && query.queryKey[1] === Number(sectionId), + }); + } + }; + const mutation = useMutation({ mutationFn: (newEditorAnswerMap: EditorAnswerMap) => postHighlight(newEditorAnswerMap, questionId), onMutate: () => { @@ -28,6 +43,8 @@ const useMutateHighlight = ({ // 토스트 모달 지우기 handleErrorModal(false); localStorage.removeItem(LOCAL_STORAGE_KEY.isHighlightError); + // 해당 질문 쿼리 캐시 무효화 + invalidateCurrentSectionQuery(); }, onError: (error) => { //토스트 모달 띄움 diff --git a/frontend/src/constants/storageKey.ts b/frontend/src/constants/storageKey.ts index f25f48312..5fb04d984 100644 --- a/frontend/src/constants/storageKey.ts +++ b/frontend/src/constants/storageKey.ts @@ -4,5 +4,5 @@ export const LOCAL_STORAGE_KEY = { }; export const SESSION_STORAGE_KEY = { - editorAnswerMap: 'editorAnswerMap-question', + currentReviewCollectionSectionId: 'currentReviewCollectionSectionId', }; diff --git a/frontend/src/mocks/mockData/reviewCollection.ts b/frontend/src/mocks/mockData/reviewCollection.ts index c2946f3bf..4b8b5df43 100644 --- a/frontend/src/mocks/mockData/reviewCollection.ts +++ b/frontend/src/mocks/mockData/reviewCollection.ts @@ -115,7 +115,7 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews[] = [ { id: 2, content: - 'http://localhost:3000/user/review-zone/5WkYQLqW1http://localhost:3000/user/review-zone/5WkYQLqW2http://localhost:3000/user/review-zone/5WkYQLqW3http://localhost:3000/user/review-zone/5WkYQLqW4http://localhost:3000/user/review-zone/5WkYQLqW5http://localhost:3000/user/review-zone/5WkYQLqW6http://localhost:3000/user/review-zone/5WkYQLqW7http://localhost:3000/user/review-zone/5WkYQLqW8http://localhost:3000/user/review-zone/5WkYQLqW9http://localhost:3000/user/review-zone/5WkYQLqW10', + ' 복잡한 문제를 체계적으로 분석하고, 창의적인 해결책을 제안하며 이를 실행하는 데 뛰어난 역량을 보여줍니다. 특히, 제한된 시간과 자원 속에서도 효과적으로 우선순위를 정하고 문제를 해결하는 모습을 통해 팀에 큰 신뢰를 주었습니다. 이러한 능력은 팀의 목표 달성과 성장에 큰 기여를 하며, 앞으로도 더 많은 성과를 낼 수 있을 것으로 기대됩니다.!!!!!', highlights: [ { lineIndex: 0, @@ -132,13 +132,13 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews[] = [ { id: 3, content: - '장의 시작부분은 짧고 직접적이며, 뒤따라 나올 복잡한 정보를 어떻게 해석해야 할 것인지 프레임을 짜주는 역할을 해야 한다. 그러면 아무리 긴 문장이라도 쉽게 읽힌다.', + '문제의 핵심 원인을 빠르게 파악하고, 이를 바탕으로 실행 가능한 솔루션을 제시하며 팀의 목표를 달성하는 데 큰 기여를 했습니다. 특히, 예상치 못한 상황에서도 냉철한 판단과 적극적인 태도로 해결책을 찾아가는 모습은 팀원들에게 좋은 자극이 되었습니다.', highlights: [], }, { id: 4, content: - '고액공제건강보험과 건강저축계좌를 만들어 노동자와 고용주가 세금공제를 받을 수 있도록 하면 결과적으로 노동자의 의료보험 부담이 커진다. 세금공제를 받을 수 있도록 하면------------------------------------------- 결과적으로 노동자의 의료보험 부담이 커진다.', + '문제를 다양한 관점에서 바라보며 가장 적합한 해결책을 찾아내는 능력이 뛰어납니다. 특히, 여러 이해관계자 간의 의견을 조율하며 모두가 만족할 수 있는 방안을 제안한 점이 돋보였습니다. 이 과정에서 보여준 적극적인 소통과 논리적인 접근법은 팀의 신뢰를 더욱 높였고, 어려운 과제를 성공적으로 마무리할 수 있는 원동력이 되었습니다.', highlights: [], }, ], @@ -181,7 +181,7 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews[] = [ { id: 2, content: - 'http://localhost:3000/user/review-zone/5WkYQLqW1http://localhost:3000/user/review-zone/5WkYQLqW2http://localhost:3000/user/review-zone/5WkYQLqW3http://localhost:3000/user/review-zone/5WkYQLqW4http://localhost:3000/user/review-zone/5WkYQLqW5http://localhost:3000/user/review-zone/5WkYQLqW6http://localhost:3000/user/review-zone/5WkYQLqW7http://localhost:3000/user/review-zone/5WkYQLqW8http://localhost:3000/user/review-zone/5WkYQLqW9http://localhost:3000/user/review-zone/5WkYQLqW10', + '효율적인 시간 관리 능력을 통해 중요한 작업을 기한 내에 완수하는 모습이 매우 인상적이었습니다. 특히, 작업의 우선순위를 명확히 구분하고 이를 기반으로 체계적으로 계획을 세워 진행하는 점이 돋보였습니다. 이러한 능력 덕분에 팀 전체의 생산성이 향상되었고, 예상치 못한 문제가 발생했을 때도 유연하게 대처하며 프로젝트를 성공적으로 이끌었습니다.', highlights: [ { lineIndex: 0, @@ -198,13 +198,13 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews[] = [ { id: 3, content: - '장의 시작부분은 짧고 직접적이며, 뒤따라 나올 복잡한 정보를 어떻게 해석해야 할 것인지 프레임을 짜주는 역할을 해야 한다. 그러면 아무리 긴 문장이라도 쉽게 읽힌다.', + '시간을 효율적으로 활용하는 뛰어난 능력을 보여주셨습니다. 작업 초기부터 명확한 계획을 수립하고 이를 끝까지 유지하는 모습이 인상적이었으며, 예상치 못한 변수에도 침착하게 대처하며 프로젝트의 일정과 품질을 모두 충족시켰습니다. 이러한 점은 팀에 큰 안정감을 주었고, 함께 일하는 사람들에게도 좋은 본보기가 되었습니다.', highlights: [], }, { id: 4, content: - '고액공제건강보험과 건강저축계좌를 만들어 노동자와 고용주가 세금공제를 받을 수 있도록 하면 결과적으로 노동자의 의료보험 부담이 커진다. 세금공제를 받을 수 있도록 하면------------------------------------------- 결과적으로 노동자의 의료보험 부담이 커진다.', + '타이트한 일정 속에서도 주어진 목표를 체계적으로 달성하며, 동시에 세부적인 디테일까지 놓치지 않는 모습을 보여주셨습니다. 특히, 작업 과정에서 우선순위를 명확히 설정하고, 불필요한 시간 낭비를 줄이는 효율적인 접근 방식은 팀의 전반적인 속도와 성과에 크게 기여했습니다. 앞으로도 이런 시간 관리 능력을 통해 더 많은 성과를 이루시리라 믿습니다.', highlights: [], }, ], diff --git a/frontend/src/pages/ReviewCollectionPage/components/ReviewCollectionPageContents/index.tsx b/frontend/src/pages/ReviewCollectionPage/components/ReviewCollectionPageContents/index.tsx index 5b8f77b54..44cd6c5e0 100644 --- a/frontend/src/pages/ReviewCollectionPage/components/ReviewCollectionPageContents/index.tsx +++ b/frontend/src/pages/ReviewCollectionPage/components/ReviewCollectionPageContents/index.tsx @@ -1,10 +1,10 @@ -import React, { useContext, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { Accordion, Dropdown, HighlightEditorContainer } from '@/components'; import { DropdownItem } from '@/components/common/Dropdown'; import ReviewEmptySection from '@/components/common/ReviewEmptySection'; import { ReviewInfoDataContext } from '@/components/layouts/ReviewDisplayLayout/ReviewInfoDataProvider'; -import { REVIEW_EMPTY } from '@/constants'; +import { REVIEW_EMPTY, SESSION_STORAGE_KEY } from '@/constants'; import { GroupedReview } from '@/types'; import { substituteString } from '@/utils'; @@ -18,6 +18,7 @@ const ReviewCollectionPageContents = () => { const { revieweeName, projectName, totalReviewCount } = useContext(ReviewInfoDataContext); const { data: reviewSectionList } = useGetSectionList(); + const dropdownSectionList = reviewSectionList.sections.map((section) => { return { text: section.name, value: section.id }; }); @@ -29,6 +30,12 @@ const ReviewCollectionPageContents = () => { review.votes?.sort((voteA, voteB) => voteB.count - voteA.count); }); + useEffect(() => { + return () => { + sessionStorage.removeItem(SESSION_STORAGE_KEY.currentReviewCollectionSectionId); + }; + }, []); + const renderContent = (review: GroupedReview) => { if (review.question.type === 'CHECKBOX') { const hasNoCheckboxAnswer = review.votes?.every((vote) => vote.count === 0); diff --git a/frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts b/frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts index be16a1427..27b2d687f 100644 --- a/frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts +++ b/frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts @@ -1,7 +1,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { getGroupedReviews } from '@/apis/review'; -import { REVIEW_QUERY_KEY } from '@/constants'; +import { REVIEW_QUERY_KEY, SESSION_STORAGE_KEY } from '@/constants'; import { GroupedReviews } from '@/types'; interface UseGetGroupedReviewsProps { @@ -11,6 +11,7 @@ interface UseGetGroupedReviewsProps { const useGetGroupedReviews = ({ sectionId }: UseGetGroupedReviewsProps) => { const fetchGroupedReviews = async () => { const result = await getGroupedReviews({ sectionId }); + sessionStorage.setItem(SESSION_STORAGE_KEY.currentReviewCollectionSectionId, sectionId.toString()); return result; }; diff --git a/frontend/src/pages/ReviewCollectionPage/index.tsx b/frontend/src/pages/ReviewCollectionPage/index.tsx index 9b838b0d0..c582d30e6 100644 --- a/frontend/src/pages/ReviewCollectionPage/index.tsx +++ b/frontend/src/pages/ReviewCollectionPage/index.tsx @@ -1,30 +1,9 @@ -import { useEffect } from 'react'; - import { AuthAndServerErrorFallback, ErrorSuspenseContainer, TopButton } from '@/components'; import ReviewDisplayLayout from '@/components/layouts/ReviewDisplayLayout'; -import { SESSION_STORAGE_KEY } from '@/constants'; import ReviewCollectionPageContents from './components/ReviewCollectionPageContents'; const ReviewCollectionPage = () => { - const clearEditorAnswerMapStorage = () => { - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - - // 키에 특정 문자열이 포함되어 있는지 확인 - if (key?.includes(SESSION_STORAGE_KEY.editorAnswerMap)) { - localStorage.removeItem(key); // 해당 키 삭제 - i--; // removeItem 후에 인덱스가 변경되므로 i를 감소시켜야 함 - } - } - }; - - useEffect(() => { - return () => { - clearEditorAnswerMapStorage(); - }; - }, []); - return ( From 418382c405d329ff70379f52155124c7da35e8aa Mon Sep 17 00:00:00 2001 From: Kimprodp Date: Wed, 20 Nov 2024 23:32:03 +0900 Subject: [PATCH 21/60] =?UTF-8?q?[BE]=20refactor:=202=EC=B0=A8=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EC=B6=94=EA=B0=80=20(#972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 2차 정렬 기준 추가 * refactor: 2차 정렬 기준 추가 --- .../main/java/reviewme/review/repository/AnswerRepository.java | 2 +- .../main/java/reviewme/review/repository/ReviewRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/reviewme/review/repository/AnswerRepository.java b/backend/src/main/java/reviewme/review/repository/AnswerRepository.java index ea793623a..5b18ab9f2 100644 --- a/backend/src/main/java/reviewme/review/repository/AnswerRepository.java +++ b/backend/src/main/java/reviewme/review/repository/AnswerRepository.java @@ -15,7 +15,7 @@ public interface AnswerRepository extends JpaRepository { SELECT a FROM Answer a JOIN Review r ON a.reviewId = r.id WHERE r.reviewGroupId = :reviewGroupId AND a.questionId IN :questionIds - ORDER BY r.createdAt DESC + ORDER BY r.createdAt DESC, r.id DESC LIMIT :limit """) List findReceivedAnswersByQuestionIds(long reviewGroupId, Collection questionIds, int limit); diff --git a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java index 3a0600ad9..90119fa0b 100644 --- a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java +++ b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java @@ -12,7 +12,7 @@ public interface ReviewRepository extends JpaRepository { @Query(""" SELECT r FROM Review r WHERE r.reviewGroupId = :reviewGroupId - ORDER BY r.createdAt DESC + ORDER BY r.createdAt DESC, r.id DESC """) List findAllByGroupId(long reviewGroupId); From fa7d3d304f6963fee8af30587f0dc0b4b5740ad8 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Thu, 21 Nov 2024 00:52:01 +0900 Subject: [PATCH 22/60] =?UTF-8?q?[BE]=20feat:=20Spring=20Cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#792)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 1 + .../reviewme/config/CacheManagerConfig.java | 19 +++++++++++++++++++ .../service/mapper/TemplateMapper.java | 8 +++++--- backend/src/test/resources/application.yml | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/reviewme/config/CacheManagerConfig.java diff --git a/backend/build.gradle b/backend/build.gradle index cea87d9cf..99bb632fb 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-cache' 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' diff --git a/backend/src/main/java/reviewme/config/CacheManagerConfig.java b/backend/src/main/java/reviewme/config/CacheManagerConfig.java new file mode 100644 index 000000000..21151523f --- /dev/null +++ b/backend/src/main/java/reviewme/config/CacheManagerConfig.java @@ -0,0 +1,19 @@ +package reviewme.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@EnableCaching +public class CacheManagerConfig { + + @Profile({"local", "dev", "prod"}) + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } +} diff --git a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java index 7151003d5..5225bd7e6 100644 --- a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java @@ -2,6 +2,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; import reviewme.template.domain.OptionGroup; import reviewme.template.domain.OptionItem; @@ -14,9 +15,6 @@ import reviewme.template.domain.SectionQuestion; import reviewme.template.domain.Template; import reviewme.template.domain.TemplateSection; -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; @@ -24,7 +22,10 @@ import reviewme.template.service.dto.response.QuestionResponse; import reviewme.template.service.dto.response.SectionResponse; import reviewme.template.service.dto.response.TemplateResponse; +import reviewme.template.service.exception.MissingOptionItemsInOptionGroupException; import reviewme.template.service.exception.QuestionInSectionNotFoundException; +import reviewme.template.service.exception.SectionInTemplateNotFoundException; +import reviewme.template.service.exception.TemplateNotFoundByReviewGroupException; @Component @RequiredArgsConstructor @@ -38,6 +39,7 @@ public class TemplateMapper { private final OptionGroupRepository optionGroupRepository; private final OptionItemRepository optionItemRepository; + @Cacheable(value = "template_response", key = "#reviewGroup.templateId") public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup) { Template template = templateRepository.findById(reviewGroup.getTemplateId()) .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index f18542246..9b8436a86 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -13,6 +13,8 @@ spring: ddl-auto: update flyway: enabled: false + cache: + type: none springdoc: swagger-ui: From 0522c5fc2c7322787e3feaa259385a02be8c17cf Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sat, 23 Nov 2024 00:32:24 +0900 Subject: [PATCH 23/60] =?UTF-8?q?[BE]=20refactor:=20question=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EA=B3=BC=20=EB=8B=B5=EB=B3=80=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20(#958)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 타입에 대한 분기를 answerMapper 안에서 처리할 수 있도록 * refactor: 사용하지 않는 코드 제거 * refactor: 하나의 테스트에 한가지를 테스트하도록 * refactor: 추상화를 더욱 활용하도록 * style: 개행 통일 * refactor: getQuestionType 없애고 바로 supports를 사용하도록 * refactor: 다른 유형의 답변이 동시에 입력되는지에 대한 검증 삭제 * refactor: 함수 이름 변경 * style: 띄어쓰기 추가 * refactor: 접근제한자 변경 * refactor: 접근제한자 변경 - 팀의 컨벤션을 위해 & 유지보수 편한 코드를 위해 접근 제한자를 public & private 으로 통일 * refactor: AnswerMapper 변경 - abstract class -> interface * fix: 컴파일 에러 해결 - abstract class -> interface 변환 하면서 extends -> implements 변경되지 않은 부분 변경 --- .../dto/request/ReviewAnswerRequest.java | 9 +- .../review/service/mapper/AnswerMapper.java | 2 +- .../service/mapper/CheckboxAnswerMapper.java | 7 +- .../review/service/mapper/ReviewMapper.java | 10 --- .../service/mapper/TextAnswerMapper.java | 8 +- .../mapper/AnswerMapperFactoryTest.java | 10 +-- .../mapper/CheckboxAnswerMapperTest.java | 17 ++-- .../service/mapper/ReviewMapperTest.java | 83 ++++++++----------- .../service/mapper/TextAnswerMapperTest.java | 25 +++--- 9 files changed, 71 insertions(+), 100 deletions(-) diff --git a/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java index 60233be2d..85d89cd7e 100644 --- a/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java +++ b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java @@ -15,11 +15,12 @@ public record ReviewAnswerRequest( @Nullable String text ) { - public boolean hasTextAnswer() { - return text != null && !text.isEmpty(); + + public boolean hasNoText() { + return text == null || text.isBlank(); } - public boolean hasCheckboxAnswer() { - return selectedOptionIds != null && !selectedOptionIds.isEmpty(); + public boolean hasNoSelectedOptions() { + return selectedOptionIds == null || selectedOptionIds.isEmpty(); } } diff --git a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java index 1181808a5..87ee4c511 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java @@ -1,8 +1,8 @@ package reviewme.review.service.mapper; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.Answer; import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.template.domain.QuestionType; public interface AnswerMapper { diff --git a/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java index 7fb87b0dc..2829890cd 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java @@ -1,10 +1,9 @@ package reviewme.review.service.mapper; import org.springframework.stereotype.Component; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; +import reviewme.template.domain.QuestionType; @Component public class CheckboxAnswerMapper implements AnswerMapper { @@ -16,8 +15,8 @@ public boolean supports(QuestionType questionType) { @Override public CheckboxAnswer mapToAnswer(ReviewAnswerRequest answerRequest) { - if (answerRequest.text() != null) { - throw new CheckBoxAnswerIncludedTextException(answerRequest.questionId()); + if (answerRequest.hasNoSelectedOptions()) { + return null; } return new CheckboxAnswer(answerRequest.questionId(), answerRequest.selectedOptionIds()); } diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java index 58d0c6a6f..499a5ea19 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java @@ -62,20 +62,10 @@ private List getAnswersByQuestionType(ReviewRegisterRequest request) { private Answer mapRequestToAnswer(Map questions, ReviewAnswerRequest answerRequest) { Question question = questions.get(answerRequest.questionId()); - if (question == null) { throw new SubmittedQuestionNotFoundException(answerRequest.questionId()); } - // TODO: 아래 코드를 삭제해야 한다 - if (question.isSelectable() && answerRequest.selectedOptionIds() != null && answerRequest.selectedOptionIds().isEmpty()) { - return null; - } - if (!question.isSelectable() && answerRequest.text() != null && answerRequest.text().isEmpty()) { - return null; - } - // END - AnswerMapper answerMapper = answerMapperFactory.getAnswerMapper(question.getQuestionType()); return answerMapper.mapToAnswer(answerRequest); } diff --git a/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java index 48bd55789..6f28faedd 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java @@ -1,10 +1,9 @@ package reviewme.review.service.mapper; import org.springframework.stereotype.Component; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.TextAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; +import reviewme.template.domain.QuestionType; @Component public class TextAnswerMapper implements AnswerMapper { @@ -16,12 +15,9 @@ public boolean supports(QuestionType questionType) { @Override public TextAnswer mapToAnswer(ReviewAnswerRequest answerRequest) { - if (!answerRequest.hasTextAnswer()) { + if (answerRequest.hasNoText()) { return null; } - if (answerRequest.selectedOptionIds() != null) { - throw new TextAnswerIncludedOptionItemException(answerRequest.questionId()); - } return new TextAnswer(answerRequest.questionId(), answerRequest.text()); } } diff --git a/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java index bdf37e905..f18dc74f3 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java @@ -8,9 +8,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.Answer; import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.template.domain.QuestionType; @ExtendWith(OutputCaptureExtension.class) class AnswerMapperFactoryTest { @@ -18,13 +18,13 @@ class AnswerMapperFactoryTest { private final AnswerMapper answerMapper = new AnswerMapper() { @Override - public boolean supports(QuestionType questionType) { - return questionType == QuestionType.CHECKBOX; + public Answer mapToAnswer(ReviewAnswerRequest answerRequest) { + return null; } @Override - public Answer mapToAnswer(ReviewAnswerRequest answerRequest) { - return null; + public boolean supports(QuestionType questionType) { + return questionType == QuestionType.CHECKBOX; } }; diff --git a/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java index eb2d96f98..c05b4553f 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java @@ -1,14 +1,14 @@ package reviewme.review.service.mapper; 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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; class CheckboxAnswerMapperTest { @@ -28,16 +28,17 @@ class CheckboxAnswerMapperTest { .containsExactly(1L, 2L, 3L); } - @Test - void 체크박스_답변_요청에_텍스트가_포함되어_있으면_예외를_발생시킨다() { + @ParameterizedTest + @NullAndEmptySource + void 체크박스_답변이_비어있는_경우_null로_매핑한다(List selectedOptionIds) { // given - ReviewAnswerRequest request = new ReviewAnswerRequest(1L, List.of(1L, 2L, 3L), "text"); + ReviewAnswerRequest request = new ReviewAnswerRequest(1L, selectedOptionIds, null); + CheckboxAnswerMapper mapper = new CheckboxAnswerMapper(); // when - CheckboxAnswerMapper mapper = new CheckboxAnswerMapper(); + CheckboxAnswer actual = mapper.mapToAnswer(request); // then - assertThatThrownBy(() -> mapper.mapToAnswer(request)) - .isInstanceOf(CheckBoxAnswerIncludedTextException.class); + assertThat(actual).isNull(); } } diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java index 4065c63de..2a624ccf5 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java @@ -2,7 +2,6 @@ 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.서술형_옵션_질문; @@ -16,12 +15,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import reviewme.template.domain.OptionGroup; -import reviewme.template.domain.OptionItem; -import reviewme.template.domain.Question; -import reviewme.template.repository.OptionGroupRepository; -import reviewme.template.repository.OptionItemRepository; -import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; @@ -31,7 +24,13 @@ import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; +import reviewme.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; import reviewme.template.domain.Section; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.template.repository.SectionRepository; import reviewme.template.repository.TemplateRepository; @@ -60,7 +59,7 @@ class ReviewMapperTest { private TemplateRepository templateRepository; @Test - void 텍스트가_포함된_리뷰를_생성한다() { + void 서술형_답변을_매핑한다() { // given ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); @@ -81,7 +80,7 @@ class ReviewMapperTest { } @Test - void 체크박스가_포함된_리뷰를_생성한다() { + void 선택형_답변을_매핑한다() { // given ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); @@ -106,57 +105,47 @@ class ReviewMapperTest { } @Test - void 필수가_아닌_질문에_답변이_없을_경우_답변을_생성하지_않는다() { + void 필수가_아닌_서술형_질문에_답변이_없으면_매핑하지_않는다() { // given ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + Question question = questionRepository.save(서술형_옵션_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); - Question requiredTextQuestion = questionRepository.save(서술형_필수_질문()); - Question optionalTextQuestion = questionRepository.save(서술형_옵션_질문()); + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(question.getId(), null, ""); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest( + reviewGroup.getReviewRequestCode(), List.of(answerRequest)); - Question requeiredCheckBoxQuestion = questionRepository.save(선택형_필수_질문()); - OptionGroup optionGroup1 = optionGroupRepository.save(선택지_그룹(requeiredCheckBoxQuestion.getId())); - OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup1.getId())); - OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup1.getId())); + // when + Review review = reviewMapper.mapToReview(reviewRegisterRequest); - Question optionalCheckBoxQuestion = questionRepository.save(선택형_옵션_질문()); - OptionGroup optionGroup2 = optionGroupRepository.save(선택지_그룹(optionalCheckBoxQuestion.getId())); - OptionItem optionItem3 = optionItemRepository.save(선택지(optionGroup2.getId())); - OptionItem optionItem4 = optionItemRepository.save(선택지(optionGroup2.getId())); + // then + assertThat(review.getAnswersByType(TextAnswer.class)) + .extracting(TextAnswer::getQuestionId) + .isEmpty(); + } - Section section = sectionRepository.save(항상_보이는_섹션( - List.of(requiredTextQuestion.getId(), optionalTextQuestion.getId(), - requeiredCheckBoxQuestion.getId(), optionalCheckBoxQuestion.getId()))); + @Test + void 필수가_아닌_선택형_질문에_답변이_없으면_매핑하지_않는다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + Question question = questionRepository.save(선택형_옵션_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question.getId())); + + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.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)); + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(question.getId(), List.of(), null); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest( + reviewGroup.getReviewRequestCode(), List.of(answerRequest)); // when Review review = reviewMapper.mapToReview(reviewRegisterRequest); // then - assertAll( - () -> assertThat(review.getAnswersByType(TextAnswer.class)) - .extracting(TextAnswer::getQuestionId) - .containsExactly(requiredTextQuestion.getId()), - () -> assertThat(review.getAnswersByType(CheckboxAnswer.class)) - .extracting(CheckboxAnswer::getQuestionId) - .containsExactly(requeiredCheckBoxQuestion.getId()) - ); + assertThat(review.getAnswersByType(CheckboxAnswer.class)) + .extracting(CheckboxAnswer::getQuestionId) + .isEmpty(); } @Test diff --git a/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java index 841e2d5a3..b7fc960cf 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java @@ -1,23 +1,16 @@ package reviewme.review.service.mapper; 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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import reviewme.review.domain.TextAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; class TextAnswerMapperTest { - /* - TODO: Request를 추상화해야 할까요? - 떠오르는 방법은 아래와 같습니다. - 1: static factory method를 사용 -> 걷잡을 수 없어지지 않을까요? - 2: 다른 방식으로 추상화 ? - */ - @Test void 텍스트_답변을_요청으로부터_매핑한다() { // given @@ -31,16 +24,18 @@ class TextAnswerMapperTest { assertThat(actual.getContent()).isEqualTo("text"); } - @Test - void 텍스트_답변_요청에_옵션이_포함되어_있으면_예외를_발생시킨다() { + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " "}) + void 텍스트_답변이_비어있는_경우_null로_매핑한다(String text) { // given - ReviewAnswerRequest request = new ReviewAnswerRequest(1L, List.of(1L), "text"); + ReviewAnswerRequest request = new ReviewAnswerRequest(1L, null, text); // when TextAnswerMapper mapper = new TextAnswerMapper(); + TextAnswer actual = mapper.mapToAnswer(request); // then - assertThatThrownBy(() -> mapper.mapToAnswer(request)) - .isInstanceOf(TextAnswerIncludedOptionItemException.class); + assertThat(actual).isNull(); } } From 1c99dd839f3c08eb2606cab87c7d1e614867ca35 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 28 Nov 2024 22:30:55 +0900 Subject: [PATCH 24/60] =?UTF-8?q?[BE]=20refactor:=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#983)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: request limit 코드 제거 * refactor: 로컬, 테스트의 request limit 변수 제거 --- .../requestlimit/RequestLimitInterceptor.java | 48 ------------ .../requestlimit/RequestLimitProperties.java | 8 -- .../requestlimit/RequestLimitRedisConfig.java | 34 --------- .../requestlimit/RequestLimitWebConfig.java | 20 ----- .../requestlimit/TooManyRequestException.java | 13 ---- .../global/GlobalExceptionHandler.java | 6 -- backend/src/main/resources/application.yml | 6 -- .../RequestLimitInterceptorTest.java | 75 ------------------- backend/src/test/resources/application.yml | 6 -- 9 files changed, 216 deletions(-) delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java delete mode 100644 backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java deleted file mode 100644 index ef25b711e..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java +++ /dev/null @@ -1,48 +0,0 @@ -package reviewme.config.requestlimit; - -import static org.springframework.http.HttpHeaders.USER_AGENT; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.http.HttpMethod; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -@Component -@EnableConfigurationProperties(RequestLimitProperties.class) -@RequiredArgsConstructor -public class RequestLimitInterceptor implements HandlerInterceptor { - - private final RedisTemplate redisTemplate; - private final RequestLimitProperties requestLimitProperties; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - if (!HttpMethod.POST.matches(request.getMethod())) { - return true; - } - - String key = generateRequestKey(request); - ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.setIfAbsent(key, 0L, requestLimitProperties.duration()); - redisTemplate.expire(key, requestLimitProperties.duration()); - - long requestCount = valueOperations.increment(key); - if (requestCount > requestLimitProperties.threshold()) { - throw new TooManyRequestException(key); - } - return true; - } - - private String generateRequestKey(HttpServletRequest request) { - String requestURI = request.getRequestURI(); - String remoteAddr = request.getRemoteAddr(); - String userAgent = request.getHeader(USER_AGENT); - - return String.format("RequestURI: %s, RemoteAddr: %s, UserAgent: %s", requestURI, remoteAddr, userAgent); - } -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java deleted file mode 100644 index 558378094..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java +++ /dev/null @@ -1,8 +0,0 @@ -package reviewme.config.requestlimit; - -import java.time.Duration; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "request-limit") -public record RequestLimitProperties(long threshold, Duration duration, String host, int port) { -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java deleted file mode 100644 index d8bb458a9..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package reviewme.config.requestlimit; - -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.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.GenericToStringSerializer; - -@Configuration -@EnableConfigurationProperties(RequestLimitProperties.class) -@RequiredArgsConstructor -public class RequestLimitRedisConfig { - - private final RequestLimitProperties requestLimitProperties; - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory( - requestLimitProperties.host(), requestLimitProperties.port() - ); - } - - @Bean - public RedisTemplate requestLimitRedisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory()); - redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); - - return redisTemplate; - } -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java deleted file mode 100644 index 19f3b2fe4..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package reviewme.config.requestlimit; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@RequiredArgsConstructor -public class RequestLimitWebConfig implements WebMvcConfigurer { - - private final RedisTemplate redisTemplate; - private final RequestLimitProperties requestLimitProperties; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new RequestLimitInterceptor(redisTemplate, requestLimitProperties)); - } -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java b/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java deleted file mode 100644 index 544fb5885..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.config.requestlimit; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.ReviewMeException; - -@Slf4j -public class TooManyRequestException extends ReviewMeException { - - public TooManyRequestException(String requestKey) { - super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요. 잠시 후 다시 시도해주세요."); - log.warn("Too many request received - request: {}", requestKey); - } -} diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index 161e43172..7724dd90e 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -22,7 +22,6 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import reviewme.global.exception.BadRequestException; import reviewme.global.exception.DataInconsistencyException; -import reviewme.config.requestlimit.TooManyRequestException; import reviewme.global.exception.FieldErrorResponse; import reviewme.global.exception.NotFoundException; import reviewme.global.exception.UnauthorizedException; @@ -51,11 +50,6 @@ public ProblemDetail handleDataConsistencyException(DataInconsistencyException e return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getErrorMessage()); } - @ExceptionHandler(TooManyRequestException.class) - public ProblemDetail handleDuplicateRequestException(TooManyRequestException ex) { - return ProblemDetail.forStatusAndDetail(HttpStatus.TOO_MANY_REQUESTS, ex.getErrorMessage()); - } - @ExceptionHandler(Exception.class) public ProblemDetail handleException(Exception ex) { log.error("Internal server error has occurred", ex); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index aa0160b1f..45df6e2cb 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -37,9 +37,3 @@ cors: allowed-origins: - http://localhost - https://localhost - -request-limit: - threshold: 3 - duration: 1s - host: localhost - port: 6379 diff --git a/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java b/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java deleted file mode 100644 index 969040683..000000000 --- a/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package reviewme.config.requestlimit; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.springframework.http.HttpHeaders.USER_AGENT; - -import jakarta.servlet.http.HttpServletRequest; -import java.time.Duration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; - -class RequestLimitInterceptorTest { - - private final HttpServletRequest request = mock(HttpServletRequest.class); - private final RedisTemplate redisTemplate = mock(RedisTemplate.class); - private final ValueOperations valueOperations = mock(ValueOperations.class); - private final RequestLimitProperties requestLimitProperties = mock(RequestLimitProperties.class); - private final RequestLimitInterceptor interceptor = new RequestLimitInterceptor(redisTemplate, requestLimitProperties); - private final String requestKey = "RequestURI: /api/v2/reviews, RemoteAddr: localhost, UserAgent: Postman"; - - @BeforeEach - void setUp() { - given(request.getMethod()).willReturn("POST"); - given(request.getRequestURI()).willReturn("/api/v2/reviews"); - given(request.getRemoteAddr()).willReturn("localhost"); - given(request.getHeader(USER_AGENT)).willReturn("Postman"); - - given(redisTemplate.opsForValue()).willReturn(valueOperations); - given(requestLimitProperties.duration()).willReturn(Duration.ofSeconds(1)); - given(requestLimitProperties.threshold()).willReturn(3L); - } - - @Test - void POST_요청이_아니면_통과한다() { - // given - given(request.getMethod()).willReturn("GET"); - - // when - boolean result = interceptor.preHandle(request, null, null); - - // then - assertThat(result).isTrue(); - } - - @Test - void 특정_POST_요청이_처음이_아니며_최대_빈도보다_작을_경우_빈도를_1증가시킨다() { - // given - long requestCount = 1; - given(valueOperations.get(anyString())).willReturn(requestCount); - - // when - boolean result = interceptor.preHandle(request, null, null); - - // then - assertThat(result).isTrue(); - verify(valueOperations).increment(requestKey); - } - - @Test - void 특정_POST_요청이_처음이_아니며_최대_빈도보다_클_경우_예외를_발생시킨다() { - // given - long maxRequestCount = 3; - given(valueOperations.increment(anyString())).willReturn(maxRequestCount + 1); - - // when & then - assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) - .isInstanceOf(TooManyRequestException.class); - } -} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 9b8436a86..ccbe2e2ff 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -40,9 +40,3 @@ logging: cors: allowed-origins: - https://allowed-domain.com - -request-limit: - threshold: 3 - duration: 1s - host: localhost - port: 6379 From c07afbb00c3349ed22c8caa3e3ce1aa40f70926c Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Fri, 29 Nov 2024 11:30:57 +0900 Subject: [PATCH 25/60] =?UTF-8?q?[BE]=20fix:=20redis=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20(#985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 1 - .../src/test/java/reviewme/api/ApiTest.java | 19 +------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index 99bb632fb..4981440a7 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index 682a2ea18..20d57db83 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -1,7 +1,5 @@ package reviewme.api; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; @@ -17,11 +15,8 @@ import org.apache.http.HttpHeaders; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.MediaType; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; @@ -34,8 +29,8 @@ import reviewme.highlight.controller.HighlightController; import reviewme.highlight.service.HighlightService; import reviewme.review.controller.ReviewController; -import reviewme.review.service.ReviewGatheredLookupService; import reviewme.review.service.ReviewDetailLookupService; +import reviewme.review.service.ReviewGatheredLookupService; import reviewme.review.service.ReviewListLookupService; import reviewme.review.service.ReviewRegisterService; import reviewme.review.service.ReviewSummaryService; @@ -78,12 +73,6 @@ public abstract class ApiTest { @MockBean protected ReviewGroupLookupService reviewGroupLookupService; - @MockBean - protected RedisTemplate redisTemplate; - - @Mock - protected ValueOperations valueOperations; - @MockBean protected ReviewSummaryService reviewSummaryService; @@ -111,12 +100,6 @@ public abstract class ApiTest { } }; - @BeforeEach - void setUpRedisConfig() { - given(redisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.increment(anyString())).willReturn(1L); - } - @BeforeEach void setUpRestDocs(WebApplicationContext context, RestDocumentationContextProvider provider) { UriModifyingOperationPreprocessor uriModifier = modifyUris() From 89080698b9bebef59e79a076527b1a3c6c645611 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sat, 30 Nov 2024 14:53:34 +0900 Subject: [PATCH 26/60] =?UTF-8?q?cd:=20develop=20=EC=98=A4=EB=9D=BC?= =?UTF-8?q?=ED=81=B4=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=8B=A4=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#984)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/backend-dev-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-dev-cd.yml b/.github/workflows/backend-dev-cd.yml index eec962f53..6a10d7c00 100644 --- a/.github/workflows/backend-dev-cd.yml +++ b/.github/workflows/backend-dev-cd.yml @@ -57,7 +57,7 @@ jobs: deploy: name: Deploy via self-hosted runner needs: build - runs-on: [self-hosted, dev] + runs-on: [self-hosted, dev, oracle] steps: - name: Checkout to secret repository From 6e7a72af670b33f788aa3c9df1ef72499c525762 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Mon, 2 Dec 2024 14:22:10 +0900 Subject: [PATCH 27/60] =?UTF-8?q?[BE]=20refactor:=20port=20config=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Related to 1b6ee74 --- backend/src/main/resources/ports.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 backend/src/main/resources/ports.yml diff --git a/backend/src/main/resources/ports.yml b/backend/src/main/resources/ports.yml deleted file mode 100644 index 8b8093829..000000000 --- a/backend/src/main/resources/ports.yml +++ /dev/null @@ -1,6 +0,0 @@ -server: - port: ${SERVER_PORT} - -management: - server: - port: ${ACTUATOR_PORT} From 35d7530cf473f7f6e48b555ca119c02beda6ba1f Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Wed, 4 Dec 2024 21:20:28 +0900 Subject: [PATCH 28/60] =?UTF-8?q?chore:=20prod=20cd=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=A0=20runner=20=EB=B3=80=EA=B2=BD=20(#992)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 오라클 러너를 사용하도록 변경 --- .github/workflows/backend-prod-cd.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/backend-prod-cd.yml b/.github/workflows/backend-prod-cd.yml index 41050eb22..f3958ba9e 100644 --- a/.github/workflows/backend-prod-cd.yml +++ b/.github/workflows/backend-prod-cd.yml @@ -52,10 +52,7 @@ jobs: deploy: name: Deploy via self-hosted runner needs: build - strategy: - matrix: - runner: [prod-a, prod-b] - runs-on: [ self-hosted, "${{ matrix.runner }}" ] + runs-on: [self-hosted, prod, oracle] steps: - name: Checkout to secret repository From b7b88acd8831b6d155b49b08c572fc9e0a7fd81e Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sun, 15 Dec 2024 18:04:07 +0900 Subject: [PATCH 29/60] =?UTF-8?q?chore:=20release=20cd=EB=A5=BC=20?= =?UTF-8?q?=EB=8F=8C=EB=A6=B4=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20=ED=95=9C=EC=A0=95=20(#1004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/backend-prod-cd.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/backend-prod-cd.yml b/.github/workflows/backend-prod-cd.yml index f3958ba9e..2033f023c 100644 --- a/.github/workflows/backend-prod-cd.yml +++ b/.github/workflows/backend-prod-cd.yml @@ -7,6 +7,17 @@ env: APPLICATION_DIRECTORY: /home/ubuntu/review-me jobs: + check-branch: + name: Check branch name + runs-on: ubuntu-latest + steps: + - name: Check branch name + if: ${{ github.ref_name != 'release' && !startsWith(github.ref_name, 'hotfix/') }} + run: | + echo "This workflow can only run on 'release' branch or branches starting with 'hotfix/'" + echo "Current branch: ${{ github.ref_name }}" + exit 1 + build: name: Build Dockerfile and push to DockerHub runs-on: ubuntu-latest From 6b8d31370160feee0b1e540acafca6e6e4887332 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Tue, 17 Dec 2024 14:57:13 +0900 Subject: [PATCH 30/60] =?UTF-8?q?[BE]=20refactor:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=ED=95=98=EC=97=AC=20=ED=99=98=EA=B2=BD=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20(#1007)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/java/reviewme/config/TestConfig.java | 7 ++++++ .../java/reviewme/support/CacheCleaner.java | 22 +++++++++++++++++++ .../support/CacheCleanerExtension.java | 15 +++++++++++++ .../java/reviewme/support/ServiceTest.java | 2 +- backend/src/test/resources/application.yml | 2 +- 5 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 backend/src/test/java/reviewme/support/CacheCleaner.java create mode 100644 backend/src/test/java/reviewme/support/CacheCleanerExtension.java diff --git a/backend/src/test/java/reviewme/config/TestConfig.java b/backend/src/test/java/reviewme/config/TestConfig.java index f339dd641..e3a05bb95 100644 --- a/backend/src/test/java/reviewme/config/TestConfig.java +++ b/backend/src/test/java/reviewme/config/TestConfig.java @@ -1,7 +1,9 @@ package reviewme.config; import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; +import reviewme.support.CacheCleaner; import reviewme.support.DatabaseCleaner; @TestConfiguration @@ -11,4 +13,9 @@ public class TestConfig { public DatabaseCleaner databaseCleaner() { return new DatabaseCleaner(); } + + @Bean + public CacheCleaner cacheCleaner(CacheManager cacheManager) { + return new CacheCleaner(cacheManager); + } } diff --git a/backend/src/test/java/reviewme/support/CacheCleaner.java b/backend/src/test/java/reviewme/support/CacheCleaner.java new file mode 100644 index 000000000..7c96acfe6 --- /dev/null +++ b/backend/src/test/java/reviewme/support/CacheCleaner.java @@ -0,0 +1,22 @@ +package reviewme.support; + +import java.util.Objects; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +public class CacheCleaner { + + private final CacheManager cacheManager; + + public CacheCleaner(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + public void execute() { + cacheManager.getCacheNames() + .stream() + .map(cacheManager::getCache) + .filter(Objects::nonNull) + .forEach(Cache::clear); + } +} diff --git a/backend/src/test/java/reviewme/support/CacheCleanerExtension.java b/backend/src/test/java/reviewme/support/CacheCleanerExtension.java new file mode 100644 index 000000000..e941b6e83 --- /dev/null +++ b/backend/src/test/java/reviewme/support/CacheCleanerExtension.java @@ -0,0 +1,15 @@ +package reviewme.support; + +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +public class CacheCleanerExtension implements BeforeEachCallback { + + @Override + public void beforeEach(ExtensionContext extensionContext) { + SpringExtension.getApplicationContext(extensionContext) + .getBean(CacheCleaner.class) + .execute(); + } +} diff --git a/backend/src/test/java/reviewme/support/ServiceTest.java b/backend/src/test/java/reviewme/support/ServiceTest.java index 34ae4b4fd..c3838a35f 100644 --- a/backend/src/test/java/reviewme/support/ServiceTest.java +++ b/backend/src/test/java/reviewme/support/ServiceTest.java @@ -12,6 +12,6 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = TestConfig.class) -@ExtendWith(DatabaseCleanerExtension.class) +@ExtendWith({DatabaseCleanerExtension.class, CacheCleanerExtension.class}) public @interface ServiceTest { } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index ccbe2e2ff..541b9ff06 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -14,7 +14,7 @@ spring: flyway: enabled: false cache: - type: none + type: simple springdoc: swagger-ui: From 1306c7b6bb56ce39c40413d148c3ce815b9615e7 Mon Sep 17 00:00:00 2001 From: Hyeonji <110809927+skylar1220@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:43:58 +0900 Subject: [PATCH 31/60] =?UTF-8?q?refactor:=20review=20group=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?(#999)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - review 패키지에서 사용되는 예외도 있으나, review가 reviewGroup을 의존하는 것이 반대의 경우보다 자연스럽기때문에 이동 --- .../java/reviewme/review/service/mapper/ReviewMapper.java | 2 +- .../main/java/reviewme/reviewgroup/domain/ReviewGroup.java | 4 ++-- .../domain/exception/InvalidProjectNameLengthException.java | 2 +- .../domain/exception/InvalidRevieweeNameLengthException.java | 2 +- .../reviewgroup/service/ReviewGroupLookupService.java | 2 +- .../java/reviewme/reviewgroup/service/ReviewGroupService.java | 4 ++-- .../ReviewGroupNotFoundByReviewRequestCodeException.java | 2 +- .../service/exception/ReviewGroupUnauthorizedException.java | 2 +- backend/src/test/java/reviewme/api/ReviewApiTest.java | 2 +- backend/src/test/java/reviewme/api/TemplateApiTest.java | 2 +- .../java/reviewme/review/service/mapper/ReviewMapperTest.java | 2 +- .../reviewgroup/service/ReviewGroupLookupServiceTest.java | 2 +- .../reviewme/reviewgroup/service/ReviewGroupServiceTest.java | 4 ++-- 13 files changed, 16 insertions(+), 16 deletions(-) rename backend/src/main/java/reviewme/{review => reviewgroup}/domain/exception/InvalidProjectNameLengthException.java (92%) rename backend/src/main/java/reviewme/{review => reviewgroup}/domain/exception/InvalidRevieweeNameLengthException.java (92%) rename backend/src/main/java/reviewme/{review => reviewgroup}/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java (90%) rename backend/src/main/java/reviewme/{review => reviewgroup}/service/exception/ReviewGroupUnauthorizedException.java (92%) diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java index 499a5ea19..0db3362ae 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java @@ -14,7 +14,7 @@ import reviewme.review.domain.Review; import reviewme.review.service.dto.request.ReviewAnswerRequest; import reviewme.review.service.dto.request.ReviewRegisterRequest; -import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.review.service.exception.SubmittedQuestionNotFoundException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; diff --git a/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java b/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java index dcc97fefe..6e95967bd 100644 --- a/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java +++ b/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java @@ -11,8 +11,8 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import reviewme.review.domain.exception.InvalidProjectNameLengthException; -import reviewme.review.domain.exception.InvalidRevieweeNameLengthException; +import reviewme.reviewgroup.domain.exception.InvalidProjectNameLengthException; +import reviewme.reviewgroup.domain.exception.InvalidRevieweeNameLengthException; @Entity @Table(name = "review_group") diff --git a/backend/src/main/java/reviewme/review/domain/exception/InvalidProjectNameLengthException.java b/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidProjectNameLengthException.java similarity index 92% rename from backend/src/main/java/reviewme/review/domain/exception/InvalidProjectNameLengthException.java rename to backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidProjectNameLengthException.java index 2e6386bb4..75b56ba43 100644 --- a/backend/src/main/java/reviewme/review/domain/exception/InvalidProjectNameLengthException.java +++ b/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidProjectNameLengthException.java @@ -1,4 +1,4 @@ -package reviewme.review.domain.exception; +package reviewme.reviewgroup.domain.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.BadRequestException; diff --git a/backend/src/main/java/reviewme/review/domain/exception/InvalidRevieweeNameLengthException.java b/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidRevieweeNameLengthException.java similarity index 92% rename from backend/src/main/java/reviewme/review/domain/exception/InvalidRevieweeNameLengthException.java rename to backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidRevieweeNameLengthException.java index 27408d15b..e77562a23 100644 --- a/backend/src/main/java/reviewme/review/domain/exception/InvalidRevieweeNameLengthException.java +++ b/backend/src/main/java/reviewme/reviewgroup/domain/exception/InvalidRevieweeNameLengthException.java @@ -1,4 +1,4 @@ -package reviewme.review.domain.exception; +package reviewme.reviewgroup.domain.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.BadRequestException; diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java index 38bd711af..0567f6344 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java index 1ae76f6a0..86303197c 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java @@ -3,8 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.service.exception.ReviewGroupUnauthorizedException; +import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.service.exception.ReviewGroupUnauthorizedException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; diff --git a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java b/backend/src/main/java/reviewme/reviewgroup/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java similarity index 90% rename from backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java rename to backend/src/main/java/reviewme/reviewgroup/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java index 121296482..5761e3c8d 100644 --- a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/exception/ReviewGroupNotFoundByReviewRequestCodeException.java @@ -1,4 +1,4 @@ -package reviewme.review.service.exception; +package reviewme.reviewgroup.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/reviewgroup/service/exception/ReviewGroupUnauthorizedException.java similarity index 92% rename from backend/src/main/java/reviewme/review/service/exception/ReviewGroupUnauthorizedException.java rename to backend/src/main/java/reviewme/reviewgroup/service/exception/ReviewGroupUnauthorizedException.java index 125f2e7e9..64c106e84 100644 --- a/backend/src/main/java/reviewme/review/service/exception/ReviewGroupUnauthorizedException.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/exception/ReviewGroupUnauthorizedException.java @@ -1,4 +1,4 @@ -package reviewme.review.service.exception; +package reviewme.reviewgroup.service.exception; import lombok.extern.slf4j.Slf4j; import reviewme.global.exception.UnauthorizedException; diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index 4552b02d5..e05935d43 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -34,7 +34,7 @@ import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; import reviewme.review.service.dto.response.list.ReviewCategoryResponse; import reviewme.review.service.dto.response.list.ReviewListElementResponse; -import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; class ReviewApiTest extends ApiTest { diff --git a/backend/src/test/java/reviewme/api/TemplateApiTest.java b/backend/src/test/java/reviewme/api/TemplateApiTest.java index 932039bac..e41e746cd 100644 --- a/backend/src/test/java/reviewme/api/TemplateApiTest.java +++ b/backend/src/test/java/reviewme/api/TemplateApiTest.java @@ -17,7 +17,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; -import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.template.service.dto.response.SectionNameResponse; import reviewme.template.service.dto.response.SectionNamesResponse; diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java index 2a624ccf5..146c6d7bb 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java @@ -20,7 +20,7 @@ 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.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; diff --git a/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupLookupServiceTest.java b/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupLookupServiceTest.java index a7719e52f..4c553a9a3 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.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.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 6bdc22f44..83b399682 100644 --- a/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupServiceTest.java +++ b/backend/src/test/java/reviewme/reviewgroup/service/ReviewGroupServiceTest.java @@ -17,8 +17,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; -import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.service.exception.ReviewGroupUnauthorizedException; +import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.service.exception.ReviewGroupUnauthorizedException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; From 788dc656b99ddcf62d045388216dd05d7f35487a Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Wed, 18 Dec 2024 16:44:20 +0900 Subject: [PATCH 32/60] =?UTF-8?q?[BE]=20fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20=EC=BA=90=EC=8B=9C=20=ED=82=A4=20=EC=A0=95=EC=A0=95=20(#1003?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 잘못된 캐시 키 정정 * fix: 잘못된 캐시 키 정정 - reviewGroupId 로 * refactor: cache 삭제 --- .../template/service/mapper/TemplateMapper.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java index 5225bd7e6..41f003e8c 100644 --- a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java @@ -2,19 +2,18 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; +import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.template.domain.OptionGroup; import reviewme.template.domain.OptionItem; import reviewme.template.domain.Question; -import reviewme.template.repository.OptionGroupRepository; -import reviewme.template.repository.OptionItemRepository; -import reviewme.template.repository.QuestionRepository; -import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.template.domain.Section; import reviewme.template.domain.SectionQuestion; import reviewme.template.domain.Template; import reviewme.template.domain.TemplateSection; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.template.repository.SectionRepository; import reviewme.template.repository.TemplateRepository; import reviewme.template.service.dto.response.OptionGroupResponse; @@ -39,7 +38,6 @@ public class TemplateMapper { private final OptionGroupRepository optionGroupRepository; private final OptionItemRepository optionItemRepository; - @Cacheable(value = "template_response", key = "#reviewGroup.templateId") public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup) { Template template = templateRepository.findById(reviewGroup.getTemplateId()) .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( From 69b507a6d024d1be4419de18f77586a107adfce8 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Wed, 18 Dec 2024 16:46:00 +0900 Subject: [PATCH 33/60] =?UTF-8?q?[BE]=20chore:=20H2=EB=A5=BC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=EB=A7=8C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20(#1?= =?UTF-8?q?010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: H2 TestRuntimeOnly 설정, Swagger 의존성 삭제 * chore: 외부 의존성 버전 명시 * chore: 외부 의존성 버전 명시 제거 --- backend/build.gradle | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index 4981440a7..f5457c003 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -30,26 +30,30 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-cache' - 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' - annotationProcessor 'org.projectlombok:lombok' + + // ConfigurationProperties annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + // Test dependencies + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'com.h2database:h2' + + // Lombok + annotationProcessor 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testImplementation 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' // RestDocs - asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.0.1' - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:3.0.1' - testImplementation 'io.rest-assured:spring-mock-mvc:5.4.0' - testImplementation 'io.rest-assured:rest-assured:5.4.0' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'io.rest-assured:spring-mock-mvc' + testImplementation 'io.rest-assured:rest-assured' } ext { From 77d08632085c5b5469f2053eeaee913f468930e9 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sat, 28 Dec 2024 21:00:02 +0900 Subject: [PATCH 34/60] =?UTF-8?q?[BE]=20refactor:=20TemplateMapper?= =?UTF-8?q?=EC=99=80=20ReviewGroup=20=EA=B2=B0=ED=95=A9=20=EB=8A=90?= =?UTF-8?q?=EC=8A=A8=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95=20(#1011)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/service/TemplateService.java | 9 ++- .../service/mapper/TemplateMapper.java | 19 ++---- .../service/mapper/TemplateMapperTest.java | 65 ++++++++++--------- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/backend/src/main/java/reviewme/template/service/TemplateService.java b/backend/src/main/java/reviewme/template/service/TemplateService.java index 1b6370878..0a19289d9 100644 --- a/backend/src/main/java/reviewme/template/service/TemplateService.java +++ b/backend/src/main/java/reviewme/template/service/TemplateService.java @@ -1,10 +1,12 @@ package reviewme.template.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.service.ReviewGroupService; +import reviewme.template.service.dto.response.SectionResponse; import reviewme.template.service.dto.response.TemplateResponse; import reviewme.template.service.mapper.TemplateMapper; @@ -18,6 +20,11 @@ public class TemplateService { @Transactional(readOnly = true) public TemplateResponse generateReviewForm(String reviewRequestCode) { ReviewGroup reviewGroup = reviewGroupService.getReviewGroupByReviewRequestCode(reviewRequestCode); - return templateMapper.mapToTemplateResponse(reviewGroup); + List sectionResponses = templateMapper.mapSectionResponses( + reviewGroup.getId(), reviewGroup.getTemplateId() + ); + return new TemplateResponse( + reviewGroup.getTemplateId(), reviewGroup.getReviewee(), reviewGroup.getProjectName(), sectionResponses + ); } } diff --git a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java index 41f003e8c..951459e8e 100644 --- a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java @@ -3,7 +3,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.template.domain.OptionGroup; import reviewme.template.domain.OptionItem; import reviewme.template.domain.Question; @@ -20,7 +19,6 @@ import reviewme.template.service.dto.response.OptionItemResponse; import reviewme.template.service.dto.response.QuestionResponse; import reviewme.template.service.dto.response.SectionResponse; -import reviewme.template.service.dto.response.TemplateResponse; import reviewme.template.service.exception.MissingOptionItemsInOptionGroupException; import reviewme.template.service.exception.QuestionInSectionNotFoundException; import reviewme.template.service.exception.SectionInTemplateNotFoundException; @@ -38,23 +36,14 @@ public class TemplateMapper { private final OptionGroupRepository optionGroupRepository; private final OptionItemRepository optionItemRepository; - public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup) { - Template template = templateRepository.findById(reviewGroup.getTemplateId()) - .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( - reviewGroup.getId(), reviewGroup.getTemplateId() - )); + public List mapSectionResponses(long templateId, long reviewGroupId) { + Template template = templateRepository.findById(templateId) + .orElseThrow(() -> new TemplateNotFoundByReviewGroupException(reviewGroupId, templateId)); - List sectionResponses = template.getSectionIds() + return template.getSectionIds() .stream() .map(this::mapToSectionResponse) .toList(); - - return new TemplateResponse( - template.getId(), - reviewGroup.getReviewee(), - reviewGroup.getProjectName(), - sectionResponses - ); } private SectionResponse mapToSectionResponse(TemplateSection templateSection) { diff --git a/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java b/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java index 0814b4400..9669113de 100644 --- a/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java +++ b/backend/src/test/java/reviewme/template/service/mapper/TemplateMapperTest.java @@ -14,22 +14,22 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; import reviewme.template.domain.OptionGroup; import reviewme.template.domain.Question; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; import reviewme.template.repository.OptionGroupRepository; import reviewme.template.repository.OptionItemRepository; import reviewme.template.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; +import reviewme.template.service.exception.MissingOptionItemsInOptionGroupException; +import reviewme.template.service.exception.SectionInTemplateNotFoundException; @ServiceTest class TemplateMapperTest { @@ -67,22 +67,22 @@ class TemplateMapperTest { Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); Section section2 = sectionRepository.save(항상_보이는_섹션(List.of(question2.getId()))); - templateRepository.save(템플릿(List.of(section1.getId(), section2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId(), section2.getId()))); ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + List sectionResponses = templateMapper.mapSectionResponses( + template.getId(), reviewGroup.getId() + ); // 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) + () -> assertThat(sectionResponses).hasSize(2), + () -> assertThat(sectionResponses.get(0).header()).isEqualTo(section1.getHeader()), + () -> assertThat(sectionResponses.get(0).questions()).hasSize(1), + () -> assertThat(sectionResponses.get(1).header()).isEqualTo(section2.getHeader()), + () -> assertThat(sectionResponses.get(1).questions()).hasSize(1) ); } @@ -91,16 +91,17 @@ class TemplateMapperTest { // given Question question = questionRepository.save(서술형_필수_질문()); Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); - templateRepository.save(템플릿(List.of(section.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + List sectionResponses = templateMapper.mapSectionResponses( + template.getId(), reviewGroup.getId() + ); // then - SectionResponse sectionResponse = templateResponse.sections().get(0); - assertThat(sectionResponse.onSelectedOptionId()).isNull(); + assertThat(sectionResponses.get(0).onSelectedOptionId()).isNull(); } @Test @@ -108,15 +109,17 @@ class TemplateMapperTest { // given Question question = questionRepository.save(서술형_필수_질문()); Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); - templateRepository.save(템플릿(List.of(section.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + List sectionResponses = templateMapper.mapSectionResponses( + template.getId(), reviewGroup.getId() + ); // then - QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); + QuestionResponse questionResponse = sectionResponses.get(0).questions().get(0); assertAll( () -> assertThat(questionResponse.hasGuideline()).isFalse(), () -> assertThat(questionResponse.guideline()).isNull() @@ -128,26 +131,28 @@ class TemplateMapperTest { // given Question question = questionRepository.save(서술형_필수_질문()); Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); - templateRepository.save(템플릿(List.of(section.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); + List sectionResponses = templateMapper.mapSectionResponses( + template.getId(), reviewGroup.getId() + ); // then - QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); + QuestionResponse questionResponse = sectionResponses.get(0).questions().get(0); assertThat(questionResponse.optionGroup()).isNull(); } @Test void 템플릿_매핑_시_템플릿에_제공할_섹션이_없을_경우_예외가_발생한다() { // given - templateRepository.save(템플릿(List.of(1L))); + Template template = templateRepository.save(템플릿(List.of(1L))); ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when, then - assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup)) + assertThatThrownBy(() -> templateMapper.mapSectionResponses(template.getId(), reviewGroup.getId())) .isInstanceOf(SectionInTemplateNotFoundException.class); } @@ -158,12 +163,12 @@ class TemplateMapperTest { optionGroupRepository.save(선택지_그룹(question.getId())); Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); - templateRepository.save(템플릿(List.of(section.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); // when, then - assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup)) + assertThatThrownBy(() -> templateMapper.mapSectionResponses(template.getId(), reviewGroup.getId())) .isInstanceOf(MissingOptionItemsInOptionGroupException.class); } } From e6292ec3a8c176fb7d8167d2f3e87daa85493c20 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sat, 28 Dec 2024 21:00:26 +0900 Subject: [PATCH 35/60] =?UTF-8?q?[BE]=20refactor:=20DTO=EC=97=90=EC=84=9C?= =?UTF-8?q?=20Enum=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#1012)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: 개행 수정 * refactor: Dto에 Enum 직접 사용하도록 수정 * style: 개행 수정 --- .../main/java/reviewme/template/domain/OptionType.java | 2 ++ .../java/reviewme/template/domain/QuestionType.java | 2 +- .../service/dto/response/QuestionResponse.java | 3 ++- .../template/service/dto/response/SectionResponse.java | 3 ++- .../template/service/mapper/TemplateMapper.java | 4 ++-- .../src/test/java/reviewme/api/TemplateFixture.java | 10 +++++----- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/reviewme/template/domain/OptionType.java b/backend/src/main/java/reviewme/template/domain/OptionType.java index 80e4e2c1b..cf20adc26 100644 --- a/backend/src/main/java/reviewme/template/domain/OptionType.java +++ b/backend/src/main/java/reviewme/template/domain/OptionType.java @@ -1,6 +1,8 @@ package reviewme.template.domain; public enum OptionType { + CATEGORY, KEYWORD, + ; } diff --git a/backend/src/main/java/reviewme/template/domain/QuestionType.java b/backend/src/main/java/reviewme/template/domain/QuestionType.java index 78b84fb58..4213ce925 100644 --- a/backend/src/main/java/reviewme/template/domain/QuestionType.java +++ b/backend/src/main/java/reviewme/template/domain/QuestionType.java @@ -1,8 +1,8 @@ package reviewme.template.domain; public enum QuestionType { + CHECKBOX, TEXT, ; - } diff --git a/backend/src/main/java/reviewme/template/service/dto/response/QuestionResponse.java b/backend/src/main/java/reviewme/template/service/dto/response/QuestionResponse.java index 90d1fb45e..d03da4d57 100644 --- a/backend/src/main/java/reviewme/template/service/dto/response/QuestionResponse.java +++ b/backend/src/main/java/reviewme/template/service/dto/response/QuestionResponse.java @@ -1,12 +1,13 @@ package reviewme.template.service.dto.response; import jakarta.annotation.Nullable; +import reviewme.template.domain.QuestionType; public record QuestionResponse( long questionId, boolean required, String content, - String questionType, + QuestionType questionType, @Nullable OptionGroupResponse optionGroup, boolean hasGuideline, @Nullable String guideline diff --git a/backend/src/main/java/reviewme/template/service/dto/response/SectionResponse.java b/backend/src/main/java/reviewme/template/service/dto/response/SectionResponse.java index 31ae9d849..bb59001fc 100644 --- a/backend/src/main/java/reviewme/template/service/dto/response/SectionResponse.java +++ b/backend/src/main/java/reviewme/template/service/dto/response/SectionResponse.java @@ -2,11 +2,12 @@ import jakarta.annotation.Nullable; import java.util.List; +import reviewme.template.domain.VisibleType; public record SectionResponse( long sectionId, String sectionName, - String visible, + VisibleType visible, @Nullable Long onSelectedOptionId, String header, List questions diff --git a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java index 951459e8e..363b61e7e 100644 --- a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java @@ -59,7 +59,7 @@ private SectionResponse mapToSectionResponse(TemplateSection templateSection) { return new SectionResponse( section.getId(), section.getSectionName(), - section.getVisibleType().name(), + section.getVisibleType(), section.getOnSelectedOptionId(), section.getHeader(), questionResponses @@ -79,7 +79,7 @@ private QuestionResponse mapToQuestionResponse(SectionQuestion sectionQuestion) question.getId(), question.isRequired(), question.getContent(), - question.getQuestionType().name(), + question.getQuestionType(), optionGroupResponse, question.hasGuideline(), question.getGuideline() diff --git a/backend/src/test/java/reviewme/api/TemplateFixture.java b/backend/src/test/java/reviewme/api/TemplateFixture.java index 7a8f3c194..a1a05349c 100644 --- a/backend/src/test/java/reviewme/api/TemplateFixture.java +++ b/backend/src/test/java/reviewme/api/TemplateFixture.java @@ -29,14 +29,14 @@ public static TemplateResponse templateResponse() { 1, true, "프로젝트 기간 동안, 아루의 강점이 드러났던 순간을 선택해주세요.", - QuestionType.CHECKBOX.name(), + QuestionType.CHECKBOX, new OptionGroupResponse(1, 1, 2, firstSectionOptions), false, null ) ); SectionResponse firstSection = new SectionResponse( - 1, "카테고리 선택", VisibleType.ALWAYS.name(), null, "아루와 함께 한 기억을 떠올려볼게요.", firstSectionQuestions + 1, "카테고리 선택", VisibleType.ALWAYS, null, "아루와 함께 한 기억을 떠올려볼게요.", firstSectionQuestions ); // Section 2 @@ -50,7 +50,7 @@ public static TemplateResponse templateResponse() { 2, true, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", - QuestionType.CHECKBOX.name(), + QuestionType.CHECKBOX, new OptionGroupResponse(2, 1, 3, secondSectionOptions), false, null @@ -59,14 +59,14 @@ public static TemplateResponse templateResponse() { 3, true, "위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요.", - QuestionType.TEXT.name(), + QuestionType.TEXT, null, true, "상황을 자세하게 기록할수록 아루에게 도움이 돼요. 아루 덕분에 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요." ) ); SectionResponse secondSection = new SectionResponse( - 2, "커뮤니케이션 능력", VisibleType.ALWAYS.name(), 1L, "아루의 커뮤니케이션, 협업 능력을 평가해주세요.", secondSectionQuestions + 2, "커뮤니케이션 능력", VisibleType.ALWAYS, 1L, "아루의 커뮤니케이션, 협업 능력을 평가해주세요.", secondSectionQuestions ); return new TemplateResponse(1, "아루", "리뷰미", List.of(firstSection, secondSection)); From 9ef8b71ec63931818590e473761edcd883cd269b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yejin=20Lee=28=EC=9D=B4=EC=98=88=EC=A7=84=29?= <111052302+ImxYJL@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:38:39 +0900 Subject: [PATCH 36/60] =?UTF-8?q?chore:=20=ED=83=80=EC=9E=85,=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=EC=9D=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20PascalCase=EB=A1=9C=20=EA=B0=95=EC=A0=9C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20eslint=20=EA=B7=9C=EC=B9=99=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#1034)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.eslintrc.cjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index dd825e3f4..ea0dcdbdb 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -26,6 +26,17 @@ module.exports = { rules: { 'react/react-in-jsx-scope': 'off', 'react/no-unknown-property': ['error', { ignore: ['css'] }], + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'interface', + format: ['PascalCase'], + }, + { + selector: 'typeAlias', + format: ['PascalCase'], + }, + ], 'import/order': [ 'error', { From 4d0fe93f3595ed25b94c22c01160819720c9a891 Mon Sep 17 00:00:00 2001 From: badahertz52 Date: Sun, 5 Jan 2025 10:42:59 +0900 Subject: [PATCH 37/60] =?UTF-8?q?feat=20:=20ErrorSuspense=EC=97=90=20fallb?= =?UTF-8?q?ack=20=EC=88=98=EC=A0=95=20(#1027)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Suspense fallback을 props로 받을 수 있게 하면서, 기존의 ErrorBoundary fallback props명 수장 --- .../error/ErrorSuspenseContainer/index.tsx | 12 +++++++----- frontend/src/pages/DetailedReviewPage/index.tsx | 2 +- frontend/src/pages/ReviewCollectionPage/index.tsx | 2 +- frontend/src/pages/ReviewListPage/index.tsx | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/error/ErrorSuspenseContainer/index.tsx b/frontend/src/components/error/ErrorSuspenseContainer/index.tsx index 5752adb6b..36f2e8b93 100644 --- a/frontend/src/components/error/ErrorSuspenseContainer/index.tsx +++ b/frontend/src/components/error/ErrorSuspenseContainer/index.tsx @@ -1,5 +1,5 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query'; -import { lazy, Suspense } from 'react'; +import { lazy, ReactNode, Suspense } from 'react'; import { EssentialPropsWithChildren } from '@/types'; @@ -9,18 +9,20 @@ import ErrorFallback from '../ErrorFallback'; const LoadingPage = lazy(() => import('@/pages/LoadingPage')); interface ErrorSuspenseContainerProps { - fallback?: React.ComponentType; + errorFallback?: React.ComponentType; + suspenseFallback?: ReactNode; } const ErrorSuspenseContainer = ({ children, - fallback = ErrorFallback, + errorFallback = ErrorFallback, + suspenseFallback = , }: EssentialPropsWithChildren) => { return ( {({ reset }) => ( - - }>{children} + + {children} )} diff --git a/frontend/src/pages/DetailedReviewPage/index.tsx b/frontend/src/pages/DetailedReviewPage/index.tsx index fd316a205..8b4701c92 100644 --- a/frontend/src/pages/DetailedReviewPage/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/index.tsx @@ -4,7 +4,7 @@ import { DetailedReviewPageContents } from './components'; const DetailedReviewPage = () => { return ( - + diff --git a/frontend/src/pages/ReviewCollectionPage/index.tsx b/frontend/src/pages/ReviewCollectionPage/index.tsx index c582d30e6..d603f91b8 100644 --- a/frontend/src/pages/ReviewCollectionPage/index.tsx +++ b/frontend/src/pages/ReviewCollectionPage/index.tsx @@ -5,7 +5,7 @@ import ReviewCollectionPageContents from './components/ReviewCollectionPageConte const ReviewCollectionPage = () => { return ( - + diff --git a/frontend/src/pages/ReviewListPage/index.tsx b/frontend/src/pages/ReviewListPage/index.tsx index a2aec6cdf..3924129ef 100644 --- a/frontend/src/pages/ReviewListPage/index.tsx +++ b/frontend/src/pages/ReviewListPage/index.tsx @@ -5,7 +5,7 @@ import ReviewListPageContents from './components/ReviewListPageContents'; const ReviewListPage = () => { return ( - + From 67c887c1f5734f04033b789201377aeb8e860562 Mon Sep 17 00:00:00 2001 From: badahertz52 Date: Sun, 5 Jan 2025 10:43:53 +0900 Subject: [PATCH 38/60] =?UTF-8?q?[FE]=20feat=20:=20=EB=B0=9B=EC=9D=80,=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=20=EB=A6=AC=EB=B7=B0=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EB=8F=8B=EB=B3=B4=EA=B8=B0=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20(#1024)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : EmptyContent 컴포넌트 생성 * refactor: width, height 조건문 리팩토링 --- frontend/src/assets/emptyContentIcon.svg | 12 ++++++++ .../components/common/EmptyContent/index.tsx | 28 +++++++++++++++++++ .../components/common/EmptyContent/styles.ts | 23 +++++++++++++++ frontend/src/components/common/index.tsx | 2 +- frontend/src/styles/theme.ts | 3 +- 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/emptyContentIcon.svg create mode 100644 frontend/src/components/common/EmptyContent/index.tsx create mode 100644 frontend/src/components/common/EmptyContent/styles.ts diff --git a/frontend/src/assets/emptyContentIcon.svg b/frontend/src/assets/emptyContentIcon.svg new file mode 100644 index 000000000..1c1d3e1d9 --- /dev/null +++ b/frontend/src/assets/emptyContentIcon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/src/components/common/EmptyContent/index.tsx b/frontend/src/components/common/EmptyContent/index.tsx new file mode 100644 index 000000000..4bad95f0d --- /dev/null +++ b/frontend/src/components/common/EmptyContent/index.tsx @@ -0,0 +1,28 @@ +import Icon from '@/assets/emptyContentIcon.svg'; +import { EssentialPropsWithChildren } from '@/types'; + +import * as S from './styles'; + +interface EmptyContentProps { + iconHeight?: string; + iconWidth?: string; + iconMessageGap?: string; + messageFontSize?: string; +} + +const EmptyContent = ({ + iconHeight, + iconWidth, + iconMessageGap, + messageFontSize, + children, +}: EssentialPropsWithChildren) => { + return ( + + + {children} + + ); +}; + +export default EmptyContent; diff --git a/frontend/src/components/common/EmptyContent/styles.ts b/frontend/src/components/common/EmptyContent/styles.ts new file mode 100644 index 000000000..3a53166c2 --- /dev/null +++ b/frontend/src/components/common/EmptyContent/styles.ts @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +export const EmptyContent = styled.div<{ $iconMessageGap?: string }>` + display: flex; + flex-direction: column; + gap: ${(props) => props.$iconMessageGap ?? '3.2rem'}; + align-items: center; +`; +interface ImgProps { + $height?: string; + $width?: string; +} +export const Img = styled.img` + aspect-ratio: 39/25; + width: ${(props) => props.$width || 'auto'}; + height: ${(props) => props.$height || (props.$width ? 'auto' : '19.7rem')}; +`; + +export const MessageContainer = styled.div<{ $messageFontSize?: string }>` + font-size: ${(props) => props.$messageFontSize ?? props.theme.fontSize.medium}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + color: ${({ theme }) => theme.colors.emptyContentText}; +`; diff --git a/frontend/src/components/common/index.tsx b/frontend/src/components/common/index.tsx index 2a15f749f..6dda8b053 100644 --- a/frontend/src/components/common/index.tsx +++ b/frontend/src/components/common/index.tsx @@ -10,7 +10,7 @@ export { default as Carousel } from './Carousel'; export { default as Accordion } from './Accordion'; export { default as Dropdown } from './Dropdown'; export { default as Toast } from './Toast'; - +export { default as EmptyContent } from './EmptyContent'; export { default as OptionSwitch } from './OptionSwitch'; export { default as ReviewEmptySection } from './ReviewEmptySection'; export * from './modals'; diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts index d3370a64e..c12147f14 100644 --- a/frontend/src/styles/theme.ts +++ b/frontend/src/styles/theme.ts @@ -36,6 +36,7 @@ export const breakpoint = { medium: 1024, large: 1025, }; + // NOTE: 1rem = 10px export const fontSize: ThemeProperty = { small: '1.4rem', @@ -45,7 +46,6 @@ export const fontSize: ThemeProperty = { large: '3.2rem', h2: '4.8rem', }; - export const borderRadius: ThemeProperty = { basic: '0.8rem', }; @@ -72,6 +72,7 @@ export const colors: ThemeProperty = { sidebarBackground: `rgba(0, 0, 0, 0.25)`, disabled: '#D8D8D8', disabledText: '#7F7F7F', + emptyContentText: '#CBD6DE', red: '#FF0000', }; From 8eca61a5b520a7b071262a71710a1124e6ae9313 Mon Sep 17 00:00:00 2001 From: badahertz52 Date: Sun, 5 Jan 2025 10:45:24 +0900 Subject: [PATCH 39/60] =?UTF-8?q?[FE]=20feat=20:=20=ED=9A=8C=EC=9B=90/?= =?UTF-8?q?=EB=B9=84=ED=9A=8C=EC=9B=90=20=EB=A6=AC=EB=B7=B0=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=83=9D=EC=84=B1=20=ED=8F=BC=20=20(#1031)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : 회원 여부에 따른 리뷰 링크 생성폼 변경 * fix : 공백 검사 대상에 공백으로 이루어진 경우도 추가 * feat : ErrorSuespenseContainer에 Suespen fallback props 추가 * refactor: constant/index.ts 에 route 추가 * feat: 리뷰 링크 생성 버튼 컴포넌트로 분리 및 로딩,에러 처리 추가 * feat : 토스트 메세지 핸들러 props 옵셔널로 변경 * feat : 리뷰 생성 API 오류 시, 토스트 띄우는 것으로 변경 * refactor: useURLGeneratorState 생성 - 리뷰 링크 input 상태 관리 훅 * feat : ErrorSuspense에 fallback 수정 - Suspense fallback을 props로 받을 수 있게 하면서, 기존의 ErrorBoundary fallback props명 수장 * fix : isPending을 사용해, 리뷰 생성 중 보여주기 * refactor: InputField setValue props 변경 - setValue -> updateValue * refactor: 유효성 검사 관련 상수 constants/review 로 이동 * refactor: ReviewNameField -> ReviewGroupDataField 이름 변경 * test : usePostDataForReviewRequestCode 목 핸들러에 props 추가 * chore: 사용하지 않는 주석 삭제 * style : 토스트 위치 하단으로 변경 * feat: ReviewGroupDataField 에러 메세지 표시, 초기화 시점 변경 - blur 시 에러 메세지 표시 - change 시 에러 메세지 초기화 * feat : 비밀번호 입력 시 에러 메세지 초기화 * feat : 공백만 입력한 경우에 대한 오류 메세지 추가 * refactor: props 타입명 앞에 대문자 변경 * chore : 링크 생성 실패 시 오류 실험 시 만든 코드 복구 - 리뷰 링크 생성 실패 시 ErrorBoundary가 아닌 토스트로 에러 표시하기 위해 했던 실험 관련 코드들 이전으로 복구 * fix : 토스트 duration 오류 수정 --- frontend/src/apis/group.ts | 7 +- .../src/components/common/Toast/index.tsx | 6 +- .../HighlightEditorContainer/index.tsx | 2 +- frontend/src/constants/index.ts | 1 + frontend/src/constants/review.ts | 5 + frontend/src/hooks/usePasswordValidation.ts | 17 +-- frontend/src/mocks/handlers/group.ts | 7 - .../HomePage/components/Inputs/InputField.tsx | 2 +- .../components/Inputs/PasswordField.tsx | 14 +- .../components/Inputs/ProjectNameField.tsx | 33 ----- .../Inputs/ReviewGroupDataField.tsx | 44 +++++++ .../components/Inputs/RevieweeNameField.tsx | 33 ----- .../pages/HomePage/components/Inputs/index.ts | 3 +- .../components/URLGeneratorButton/index.tsx | 49 +++++++ .../hooks/useURLGeneratorState.ts | 39 ++++++ .../components/URLGeneratorForm/index.tsx | 120 ++++++++++-------- .../usePostDataForReviewRequestCode/index.ts | 27 ++-- .../usePostDataForReviewRequestCode/test.tsx | 11 +- .../HomePage/utils/validateInput/index.ts | 19 +-- .../HomePage/utils/validateInput/test.ts | 1 + 20 files changed, 265 insertions(+), 175 deletions(-) delete mode 100644 frontend/src/pages/HomePage/components/Inputs/ProjectNameField.tsx create mode 100644 frontend/src/pages/HomePage/components/Inputs/ReviewGroupDataField.tsx delete mode 100644 frontend/src/pages/HomePage/components/Inputs/RevieweeNameField.tsx create mode 100644 frontend/src/pages/HomePage/components/URLGeneratorForm/components/URLGeneratorButton/index.tsx create mode 100644 frontend/src/pages/HomePage/components/URLGeneratorForm/hooks/useURLGeneratorState.ts diff --git a/frontend/src/apis/group.ts b/frontend/src/apis/group.ts index 2f7c2ed7e..6cc3374c6 100644 --- a/frontend/src/apis/group.ts +++ b/frontend/src/apis/group.ts @@ -1,4 +1,4 @@ -import { INVALID_REVIEW_PASSWORD_MESSAGE } from '@/constants'; +import { ERROR_BOUNDARY_IGNORE_ERROR, INVALID_REVIEW_PASSWORD_MESSAGE } from '@/constants'; import { PasswordResponse, ReviewGroupData } from '@/types'; import createApiErrorMessage from './apiErrorMessageCreator'; @@ -7,7 +7,7 @@ import endPoint from './endpoints'; export interface DataForReviewRequestCode { revieweeName: string; projectName: string; - groupAccessCode: string; + groupAccessCode?: string; } export const postDataForReviewRequestCodeApi = async (dataForReviewRequestCode: DataForReviewRequestCode) => { @@ -16,11 +16,12 @@ export const postDataForReviewRequestCodeApi = async (dataForReviewRequestCode: headers: { 'Content-Type': 'application/json', }, + // TODO : 회원 리뷰 링크 API 문서 나오면 비밀번호 관련해 변경해야함 body: JSON.stringify(dataForReviewRequestCode), }); if (!response.ok) { - throw new Error(createApiErrorMessage(response.status)); + throw new Error(`${createApiErrorMessage(response.status)} ${ERROR_BOUNDARY_IGNORE_ERROR}`); } const data = await response.json(); diff --git a/frontend/src/components/common/Toast/index.tsx b/frontend/src/components/common/Toast/index.tsx index d9219477e..ffa621dfb 100644 --- a/frontend/src/components/common/Toast/index.tsx +++ b/frontend/src/components/common/Toast/index.tsx @@ -17,15 +17,15 @@ interface ToastProps { duration: number; position: ToastPositionType; handleOpenModal: (isOpen: boolean) => void; - handleModalMessage: (message: string) => void; + handleModalMessage?: (message: string) => void; } const Toast = ({ icon, message, duration, position, handleOpenModal, handleModalMessage }: ToastProps) => { useEffect(() => { const timer = setTimeout(() => { handleOpenModal(false); - handleModalMessage(''); - }, duration * 1000); + if (handleModalMessage) handleModalMessage(''); + }, duration); return () => clearTimeout(timer); }, [handleOpenModal]); diff --git a/frontend/src/components/highlight/components/HighlightEditorContainer/index.tsx b/frontend/src/components/highlight/components/HighlightEditorContainer/index.tsx index a280fc534..ddfd7034f 100644 --- a/frontend/src/components/highlight/components/HighlightEditorContainer/index.tsx +++ b/frontend/src/components/highlight/components/HighlightEditorContainer/index.tsx @@ -32,7 +32,7 @@ const HighlightEditorContainer = (props: HighlightEditorContainerProps) => { { const [passwordErrorMessage, setPasswordErrorMessage] = useState(''); @@ -19,7 +17,7 @@ const usePasswordValidation = (password: string) => { }; const validatePassword = () => { - if (!isWithinLengthRange(password, MAX_PASSWORD_INPUT, MIN_PASSWORD_INPUT)) { + if (!isWithinLengthRange(password, max, min)) { return setPasswordErrorMessage(PASSWORD_LENGTH_ERROR_MESSAGE); } if (!isAlphanumeric(password)) { @@ -33,12 +31,15 @@ const usePasswordValidation = (password: string) => { validatePassword(); }; + const handlePasswordErrorMessage = (errorMessage: string) => setPasswordErrorMessage(errorMessage); + useEffect(() => { if (isBlurredOnce) validatePassword(); }, [password, isBlurredOnce]); return { passwordErrorMessage, + handlePasswordErrorMessage, handlePasswordBlur, initializeIsBlurredOnce, }; diff --git a/frontend/src/mocks/handlers/group.ts b/frontend/src/mocks/handlers/group.ts index 50e12b17b..b01bfd2a5 100644 --- a/frontend/src/mocks/handlers/group.ts +++ b/frontend/src/mocks/handlers/group.ts @@ -18,13 +18,6 @@ const postDataForReviewRequestCode = () => { }); }; -// NOTE: reviewRequestCode 생성 에러 응답 -// const postDataForReviewRequestCode = () => { -// return http.post(endPoint.postingDataForReviewRequestCode, async () => { -// return HttpResponse.json({ error: '서버 에러 테스트' }, { status: 500 }); -// }); -// }; - const postPassWordValidation = () => { return http.post(endPoint.checkingPassword, async ({ request, cookies }) => { // request body의 존재 검증 diff --git a/frontend/src/pages/HomePage/components/Inputs/InputField.tsx b/frontend/src/pages/HomePage/components/Inputs/InputField.tsx index 4be9564b2..c18808735 100644 --- a/frontend/src/pages/HomePage/components/Inputs/InputField.tsx +++ b/frontend/src/pages/HomePage/components/Inputs/InputField.tsx @@ -14,7 +14,7 @@ interface InputFieldProps { export interface InputValueProps { id: string; value: string; - setValue: Dispatch>; + updateValue: (newValue: string) => void; } const InputField = ({ diff --git a/frontend/src/pages/HomePage/components/Inputs/PasswordField.tsx b/frontend/src/pages/HomePage/components/Inputs/PasswordField.tsx index a8bc9082d..be7f859e9 100644 --- a/frontend/src/pages/HomePage/components/Inputs/PasswordField.tsx +++ b/frontend/src/pages/HomePage/components/Inputs/PasswordField.tsx @@ -1,8 +1,8 @@ import { useEffect } from 'react'; import { EyeButton, Input } from '@/components'; +import { REVIEW_URL_GENERATOR_FORM_VALIDATION } from '@/constants'; import { useEyeButton, usePasswordValidation } from '@/hooks'; -import { MAX_PASSWORD_INPUT, MIN_PASSWORD_INPUT } from '@/pages/HomePage/utils/validateInput'; import * as S from '../URLGeneratorForm/styles'; @@ -10,9 +10,12 @@ import { InputValueProps } from './InputField'; import { InputField } from '.'; -const PasswordField = ({ id, value: password, setValue: setPassword }: InputValueProps) => { +const PasswordField = ({ id, value: password, updateValue: updatePassword }: InputValueProps) => { const { isOff, handleEyeButtonToggle } = useEyeButton(); - const { passwordErrorMessage, handlePasswordBlur, initializeIsBlurredOnce } = usePasswordValidation(password); + const { passwordErrorMessage, handlePasswordErrorMessage, handlePasswordBlur, initializeIsBlurredOnce } = + usePasswordValidation(password); + + const { min, max } = REVIEW_URL_GENERATOR_FORM_VALIDATION.password; useEffect(() => { initializeIsBlurredOnce(); @@ -22,7 +25,7 @@ const PasswordField = ({ id, value: password, setValue: setPassword }: InputValu @@ -33,7 +36,8 @@ const PasswordField = ({ id, value: password, setValue: setPassword }: InputValu type={isOff ? 'password' : 'text'} $style={{ width: '100%', paddingRight: '3rem' }} onChange={(event) => { - setPassword(event.target.value); + updatePassword(event.target.value); + handlePasswordErrorMessage(''); }} /> diff --git a/frontend/src/pages/HomePage/components/Inputs/ProjectNameField.tsx b/frontend/src/pages/HomePage/components/Inputs/ProjectNameField.tsx deleted file mode 100644 index 433683907..000000000 --- a/frontend/src/pages/HomePage/components/Inputs/ProjectNameField.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { Input } from '@/components'; -import { isWithinLengthRange, MAX_VALID_REVIEW_GROUP_DATA_INPUT } from '@/pages/HomePage/utils/validateInput'; - -import { InputValueProps } from './InputField'; - -import { InputField } from './'; - -const ProjectNameField = ({ id, value: revieweeName, setValue: setRevieweeName }: InputValueProps) => { - const [errorMessage, setErrorMessage] = useState(''); - - useEffect(() => { - isWithinLengthRange(revieweeName, MAX_VALID_REVIEW_GROUP_DATA_INPUT) - ? setErrorMessage('') - : setErrorMessage(`최대 ${MAX_VALID_REVIEW_GROUP_DATA_INPUT}자까지 입력할 수 있어요`); - }, [revieweeName]); - - return ( - - { - setRevieweeName(event.target.value); - }} - /> - - ); -}; - -export default ProjectNameField; diff --git a/frontend/src/pages/HomePage/components/Inputs/ReviewGroupDataField.tsx b/frontend/src/pages/HomePage/components/Inputs/ReviewGroupDataField.tsx new file mode 100644 index 000000000..a9dd0e7cb --- /dev/null +++ b/frontend/src/pages/HomePage/components/Inputs/ReviewGroupDataField.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react'; + +import { Input } from '@/components'; +import { REVIEW_URL_GENERATOR_FORM_VALIDATION } from '@/constants'; +import { isNotEmptyInput, isValidReviewGroupDataInput } from '@/pages/HomePage/utils/validateInput'; + +import { InputValueProps } from './InputField'; + +import { InputField } from '.'; + +const EMPTY_ERROR_MESSAGE = '공백이 아닌 내용을 입력해주세요'; +const { min, max } = REVIEW_URL_GENERATOR_FORM_VALIDATION.groupData; +const WRONG_LENGTH_ERROR_MESSAGE = `${min}자부터 ${max}자까지 입력할 수 있어요`; +interface ReviewGroupDataFieldProps extends InputValueProps { + labelText: string; +} +const ReviewGroupDataField = ({ id, labelText, value: data, updateValue: updateData }: ReviewGroupDataFieldProps) => { + const [errorMessage, setErrorMessage] = useState(''); + + const handleBlur = () => { + if (isValidReviewGroupDataInput(data)) return setErrorMessage(''); + // 공백으로만 이루어진 경우 + if (!isNotEmptyInput(data)) return setErrorMessage(EMPTY_ERROR_MESSAGE); + // 글자 수 초과 + setErrorMessage(WRONG_LENGTH_ERROR_MESSAGE); + }; + + return ( + + { + updateData(event.target.value); + setErrorMessage(''); + }} + onBlur={handleBlur} + /> + + ); +}; + +export default ReviewGroupDataField; diff --git a/frontend/src/pages/HomePage/components/Inputs/RevieweeNameField.tsx b/frontend/src/pages/HomePage/components/Inputs/RevieweeNameField.tsx deleted file mode 100644 index 8b128c575..000000000 --- a/frontend/src/pages/HomePage/components/Inputs/RevieweeNameField.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { Input } from '@/components'; -import { isWithinLengthRange, MAX_VALID_REVIEW_GROUP_DATA_INPUT } from '@/pages/HomePage/utils/validateInput'; - -import { InputValueProps } from './InputField'; - -import { InputField } from '.'; - -const RevieweeNameField = ({ id, value: projectName, setValue: setProjectName }: InputValueProps) => { - const [errorMessage, setErrorMessage] = useState(''); - - useEffect(() => { - isWithinLengthRange(projectName, MAX_VALID_REVIEW_GROUP_DATA_INPUT) - ? setErrorMessage('') - : setErrorMessage(`최대 ${MAX_VALID_REVIEW_GROUP_DATA_INPUT}자까지 입력할 수 있어요`); - }, [projectName]); - - return ( - - { - setProjectName(event.target.value); - }} - /> - - ); -}; - -export default RevieweeNameField; diff --git a/frontend/src/pages/HomePage/components/Inputs/index.ts b/frontend/src/pages/HomePage/components/Inputs/index.ts index 93305867d..4da203eab 100644 --- a/frontend/src/pages/HomePage/components/Inputs/index.ts +++ b/frontend/src/pages/HomePage/components/Inputs/index.ts @@ -1,4 +1,3 @@ -export { default as ProjectNameField } from './ProjectNameField'; -export { default as RevieweeNameField } from './RevieweeNameField'; +export { default as ReviewGroupDataField } from './ReviewGroupDataField'; export { default as PasswordField } from './PasswordField'; export { default as InputField } from './InputField'; diff --git a/frontend/src/pages/HomePage/components/URLGeneratorForm/components/URLGeneratorButton/index.tsx b/frontend/src/pages/HomePage/components/URLGeneratorForm/components/URLGeneratorButton/index.tsx new file mode 100644 index 000000000..e0e8217de --- /dev/null +++ b/frontend/src/pages/HomePage/components/URLGeneratorForm/components/URLGeneratorButton/index.tsx @@ -0,0 +1,49 @@ +import { DataForReviewRequestCode } from '@/apis/group'; +import { Button } from '@/components'; +import { HOM_EVENT_NAME } from '@/constants'; +import usePostDataForReviewRequestCode, { + UsePostDataForReviewRequestCodeProps, +} from '@/pages/HomePage/hooks/usePostDataForReviewRequestCode'; +import { debounce, trackEventInAmplitude } from '@/utils'; + +const DEBOUNCE_TIME = 300; + +interface URLGeneratorButtonProps extends UsePostDataForReviewRequestCodeProps { + isFormValid: boolean; + dataForReviewRequestCode: DataForReviewRequestCode; +} +const URLGeneratorButton = ({ + isFormValid, + dataForReviewRequestCode, + handleAPIError, + handleAPISuccess, +}: URLGeneratorButtonProps) => { + const mutation = usePostDataForReviewRequestCode({ handleAPIError, handleAPISuccess }); + + const postDataForURL = () => { + trackEventInAmplitude(HOM_EVENT_NAME.generateReviewURL); + + mutation.mutate(dataForReviewRequestCode, { + onSuccess: handleAPISuccess, + onError: handleAPIError, + }); + }; + + const handleUrlCreationButtonClick = debounce((event: React.MouseEvent) => { + event.preventDefault(); + postDataForURL(); + }, DEBOUNCE_TIME); + + return ( + + ); +}; + +export default URLGeneratorButton; diff --git a/frontend/src/pages/HomePage/components/URLGeneratorForm/hooks/useURLGeneratorState.ts b/frontend/src/pages/HomePage/components/URLGeneratorForm/hooks/useURLGeneratorState.ts new file mode 100644 index 000000000..caf2c92f3 --- /dev/null +++ b/frontend/src/pages/HomePage/components/URLGeneratorForm/hooks/useURLGeneratorState.ts @@ -0,0 +1,39 @@ +import { useState } from 'react'; + +import { isValidPasswordInput, isValidReviewGroupDataInput } from '@/pages/HomePage/utils/validateInput'; + +interface UseURLGeneratorStateProps { + isMember?: boolean; +} +const useURLGeneratorState = ({ isMember }: UseURLGeneratorStateProps) => { + const [revieweeName, setRevieweeName] = useState(''); + const [projectName, setProjectName] = useState(''); + const [password, setPassword] = useState(''); + + const isCommonFormValid = isValidReviewGroupDataInput(revieweeName) && isValidReviewGroupDataInput(projectName); + + const isFormValid = isMember ? isCommonFormValid : isCommonFormValid && isValidPasswordInput(password); + + const resetForm = () => { + setRevieweeName(''); + setProjectName(''); + !isMember && setPassword(''); + }; + + const urlGeneratorStateUpdater = { + revieweeName: (value: string) => setRevieweeName(value), + projectName: (value: string) => setProjectName(value), + password: (value: string) => setPassword(value), + }; + + return { + revieweeName, + projectName, + password, + isFormValid, + resetForm, + urlGeneratorStateUpdater, + }; +}; + +export default useURLGeneratorState; diff --git a/frontend/src/pages/HomePage/components/URLGeneratorForm/index.tsx b/frontend/src/pages/HomePage/components/URLGeneratorForm/index.tsx index 63a18c6c6..572e5e644 100644 --- a/frontend/src/pages/HomePage/components/URLGeneratorForm/index.tsx +++ b/frontend/src/pages/HomePage/components/URLGeneratorForm/index.tsx @@ -1,92 +1,104 @@ -import { useId, useState } from 'react'; +import React, { useId, useState } from 'react'; -import { DataForReviewRequestCode } from '@/apis/group'; -import { Button } from '@/components'; -import { HOM_EVENT_NAME } from '@/constants'; +import AlertIcon from '@/assets/alertTriangle.svg'; +import { ErrorSuspenseContainer, Toast } from '@/components'; import { ROUTE } from '@/constants/route'; import { useModals } from '@/hooks'; -import { isValidPasswordInput, isValidReviewGroupDataInput } from '@/pages/HomePage/utils/validateInput'; -import { debounce, trackEventInAmplitude } from '@/utils'; -import usePostDataForReviewRequestCode from '../../hooks/usePostDataForReviewRequestCode'; import { FormLayout, ReviewZoneURLModal } from '../index'; -import { ProjectNameField, RevieweeNameField, PasswordField } from '../Inputs'; +import { PasswordField, ReviewGroupDataField } from '../Inputs'; +import URLGeneratorButton from './components/URLGeneratorButton'; +import useURLGeneratorState from './hooks/useURLGeneratorState'; import * as S from './styles'; -const DEBOUNCE_TIME = 300; - const MODAL_KEYS = { confirm: 'CONFIRM', }; -const URLGeneratorForm = () => { - const [revieweeName, setRevieweeName] = useState(''); - const [projectName, setProjectName] = useState(''); - const [password, setPassword] = useState(''); +const TOAST_INFORM = { + icon: { src: AlertIcon, alt: '' }, + message: '리뷰 링크 생성에 실패했어요. 다시 시도해 보세요.', + duration: 1000 * 3, +}; +interface URLGeneratorFormProps { + isMember?: boolean; +} +const URLGeneratorForm = ({ isMember = false }: URLGeneratorFormProps) => { + const { revieweeName, projectName, password, isFormValid, resetForm, urlGeneratorStateUpdater } = + useURLGeneratorState({ isMember }); const [reviewZoneURL, setReviewZoneURL] = useState(''); + const [isOpenToast, setIsOpenToast] = useState(false); const { isOpen, openModal, closeModal } = useModals(); + const handleOpenToast = (isOpen: boolean) => setIsOpenToast(isOpen); + const useInputId = useId(); + const INPUT_ID = { revieweeName: `reviewee-name-input-${useInputId}`, projectName: `project-name-input-${useInputId}`, password: `password-input-${useInputId}`, }; - const mutation = usePostDataForReviewRequestCode(); - - const isFormValid = - isValidReviewGroupDataInput(revieweeName) && - isValidReviewGroupDataInput(projectName) && - isValidPasswordInput(password); + const getCompleteReviewZoneURL = (reviewRequestCode: string) => { + return `${window.location.origin}/${ROUTE.reviewZone}/${reviewRequestCode}`; + }; - const postDataForURL = () => { - trackEventInAmplitude(HOM_EVENT_NAME.generateReviewURL); + const handleAPISuccess = (data: any) => { + const completeReviewZoneURL = getCompleteReviewZoneURL(data.reviewRequestCode); + setReviewZoneURL(completeReviewZoneURL); - const dataForReviewRequestCode: DataForReviewRequestCode = { revieweeName, projectName, groupAccessCode: password }; - mutation.mutate(dataForReviewRequestCode, { - onSuccess: (data) => { - const completeReviewZoneURL = getCompleteReviewZoneURL(data.reviewRequestCode); - setReviewZoneURL(completeReviewZoneURL); + resetForm(); - resetForm(); - }, - }); + handleOpenToast(false); + openModal(MODAL_KEYS.confirm); }; - const resetForm = () => { - setRevieweeName(''); - setProjectName(''); - setPassword(''); - }; + const handleAPIError = (error: Error) => { + console.error(error.message); - const getCompleteReviewZoneURL = (reviewRequestCode: string) => { - return `${window.location.origin}/${ROUTE.reviewZone}/${reviewRequestCode}`; + handleOpenToast(true); + closeModal(MODAL_KEYS.confirm); }; - const handleUrlCreationButtonClick = debounce((event: React.MouseEvent) => { - event.preventDefault(); - postDataForURL(); - openModal(MODAL_KEYS.confirm); - }, DEBOUNCE_TIME); - return ( - - - - + + + {!isMember && ( + + )} + + + + {isOpenToast && ( + + )} {isOpen(MODAL_KEYS.confirm) && ( closeModal(MODAL_KEYS.confirm)} /> )} diff --git a/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/index.ts b/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/index.ts index 3dd6298bc..92f7a24f1 100644 --- a/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/index.ts +++ b/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/index.ts @@ -2,29 +2,32 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DataForReviewRequestCode, postDataForReviewRequestCodeApi } from '@/apis/group'; import { GROUP_QUERY_KEY } from '@/constants'; +export interface UsePostDataForReviewRequestCodeProps { + handleAPISuccess: (data: any) => void; + handleAPIError: (error: Error) => void; +} -const usePostDataForReviewRequestCode = () => { +const usePostDataForReviewRequestCode = ({ + handleAPIError, + handleAPISuccess, +}: UsePostDataForReviewRequestCodeProps) => { const queryClient = useQueryClient(); - const { mutate, isSuccess, isPending, data } = useMutation({ + const mutation = useMutation({ mutationFn: (dataForReviewRequestCode: DataForReviewRequestCode) => postDataForReviewRequestCodeApi(dataForReviewRequestCode), + onMutate: () => { - if (isPending) return; + if (mutation.isPending) return; }, - onSuccess: () => { + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: [GROUP_QUERY_KEY.dataForReviewRequestCode] }); + handleAPISuccess(data); }, - onError: (error) => { - console.error(error.message); - }, + onError: handleAPIError, }); - return { - mutate, - isSuccess, - data, - }; + return mutation; }; export default usePostDataForReviewRequestCode; diff --git a/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/test.tsx b/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/test.tsx index 8956d47e7..f8374ce07 100644 --- a/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/test.tsx +++ b/frontend/src/pages/HomePage/hooks/usePostDataForReviewRequestCode/test.tsx @@ -14,7 +14,16 @@ describe('usePostDataForReviewRequestCode', () => { groupAccessCode: '1234', }; - const { result } = renderHook(() => usePostDataForReviewRequestCode(), { wrapper: QueryClientWrapper }); + const { result } = renderHook( + () => + usePostDataForReviewRequestCode({ + handleAPIError: (error: Error) => { + console.error(error); + }, + handleAPISuccess: (data: any) => {}, + }), + { wrapper: QueryClientWrapper }, + ); // when act(() => { diff --git a/frontend/src/pages/HomePage/utils/validateInput/index.ts b/frontend/src/pages/HomePage/utils/validateInput/index.ts index b09cf3dea..a891fe694 100644 --- a/frontend/src/pages/HomePage/utils/validateInput/index.ts +++ b/frontend/src/pages/HomePage/utils/validateInput/index.ts @@ -1,5 +1,7 @@ +import { REVIEW_URL_GENERATOR_FORM_VALIDATION } from '@/constants'; + export const isNotEmptyInput = (input: string) => { - return input !== ''; + return input.trim() !== ''; }; export const isAlphanumeric = (input: string) => { @@ -12,19 +14,12 @@ export const isWithinLengthRange = (input: string, end: number, start: number = return length >= start && length <= end; }; -export const MAX_VALID_REVIEW_GROUP_DATA_INPUT = 50; - export const isValidReviewGroupDataInput = (input: string) => { - return isNotEmptyInput(input) && isWithinLengthRange(input, MAX_VALID_REVIEW_GROUP_DATA_INPUT); + const { min, max } = REVIEW_URL_GENERATOR_FORM_VALIDATION.groupData; + return isNotEmptyInput(input) && isWithinLengthRange(input, max, min); }; -export const MIN_PASSWORD_INPUT = 4; -export const MAX_PASSWORD_INPUT = 20; - export const isValidPasswordInput = (input: string) => { - return ( - isNotEmptyInput(input) && - isAlphanumeric(input) && - isWithinLengthRange(input, MAX_PASSWORD_INPUT, MIN_PASSWORD_INPUT) - ); + const { min, max } = REVIEW_URL_GENERATOR_FORM_VALIDATION.password; + return isNotEmptyInput(input) && isAlphanumeric(input) && isWithinLengthRange(input, max, min); }; diff --git a/frontend/src/pages/HomePage/utils/validateInput/test.ts b/frontend/src/pages/HomePage/utils/validateInput/test.ts index 720dc2b37..cb07b0188 100644 --- a/frontend/src/pages/HomePage/utils/validateInput/test.ts +++ b/frontend/src/pages/HomePage/utils/validateInput/test.ts @@ -7,6 +7,7 @@ describe('isNotEmptyInput', () => { test('빈 문자열인 경우 false를 반환한다.', () => { expect(isNotEmptyInput('')).toBe(false); + expect(isNotEmptyInput(' ')).toBe(false); }); }); From 8107b1dfb9592f380d1e02b2f58b9a27cc00cba6 Mon Sep 17 00:00:00 2001 From: Fe <64690761+chysis@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:02:31 +0900 Subject: [PATCH 40/60] =?UTF-8?q?[FE]=20feat:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=84=EB=A1=9C=ED=95=84=20=ED=83=AD=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#1035)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 사용자 프로필 컴포넌트 구현 * feat: 프로필 탭 컴포넌트 구현 및 관련 타입 작성 * chore: 공통 theme의 zIndex 값 수정 * feat: 프로필 탭 상태 및 외부 요소 클릭 시 닫히게 하는 로직 구현 * feat: 로그인 탭에 들어가는 요소를 관리하는 훅 구현 * chore: 커스텀 훅 적용 및 프로필 요소 드래그 불가능하도록 수정 * feat: 프로필 정보 및 프로필 탭 반응형 구현 * chore: 프로필 사진 src가 없는 경우 회색 배경을 보여주도록 수정 * chore: 프로필 탭의 모든 요소가 드래그 할 수 없도록 수정 * refactor: 시맨틱 태그 적용 * refactor: 프로필 탭의 element를 별도의 컴포넌트로 분리 * refactor: 프로필 탭 element의 content 타입 변경 및 elementId 속성 추가 * refactor: 프로필 탭 element 렌더링 함수 분리 및 리팩토링 * design: 프로필 사진 크기 수정 * chore: UndraggableWrapper의 min-width 속성 제거 * feat: 프로필 탭 element가 길어질 경우 말 줄임표 처리 * design: 프로필 탭 최대 width 설정 * chore: topbar에 z-index 설정 * fix: click 이벤트 핸들러가 전달되지 않는 문제 수정 * refactor: 프로필 탭 element 정보를 ProfileTab에서 불러와 사용하도록 수정 --- frontend/src/assets/github.svg | 3 + frontend/src/assets/logout.svg | 3 + frontend/src/assets/menu.svg | 3 + frontend/src/assets/openedBook.svg | 3 + frontend/src/assets/user.svg | 3 + .../common/UndraggableWrapper/styles.ts | 2 - .../src/components/layouts/Topbar/styles.ts | 2 + .../components/profile/ProfileInfo/index.tsx | 35 ++++++++ .../components/profile/ProfileInfo/styles.ts | 57 +++++++++++++ .../components/ActionItem/index.tsx | 20 +++++ .../components/ActionItem/styles.ts | 39 +++++++++ .../ProfileTab/components/Divider/index.tsx | 11 +++ .../ProfileTab/components/Divider/styles.ts | 22 +++++ .../components/ReadonlyItem/index.tsx | 19 +++++ .../components/ReadonlyItem/styles.ts | 33 ++++++++ .../profile/ProfileTab/hooks/useProfile.tsx | 28 +++++++ .../hooks/useProfileTabElements.tsx | 81 +++++++++++++++++++ .../components/profile/ProfileTab/index.tsx | 49 +++++++++++ .../components/profile/ProfileTab/styles.ts | 22 +++++ frontend/src/styles/theme.ts | 3 +- frontend/src/types/profile.ts | 19 +++++ 21 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 frontend/src/assets/github.svg create mode 100644 frontend/src/assets/logout.svg create mode 100644 frontend/src/assets/menu.svg create mode 100644 frontend/src/assets/openedBook.svg create mode 100644 frontend/src/assets/user.svg create mode 100644 frontend/src/components/profile/ProfileInfo/index.tsx create mode 100644 frontend/src/components/profile/ProfileInfo/styles.ts create mode 100644 frontend/src/components/profile/ProfileTab/components/ActionItem/index.tsx create mode 100644 frontend/src/components/profile/ProfileTab/components/ActionItem/styles.ts create mode 100644 frontend/src/components/profile/ProfileTab/components/Divider/index.tsx create mode 100644 frontend/src/components/profile/ProfileTab/components/Divider/styles.ts create mode 100644 frontend/src/components/profile/ProfileTab/components/ReadonlyItem/index.tsx create mode 100644 frontend/src/components/profile/ProfileTab/components/ReadonlyItem/styles.ts create mode 100644 frontend/src/components/profile/ProfileTab/hooks/useProfile.tsx create mode 100644 frontend/src/components/profile/ProfileTab/hooks/useProfileTabElements.tsx create mode 100644 frontend/src/components/profile/ProfileTab/index.tsx create mode 100644 frontend/src/components/profile/ProfileTab/styles.ts create mode 100644 frontend/src/types/profile.ts diff --git a/frontend/src/assets/github.svg b/frontend/src/assets/github.svg new file mode 100644 index 000000000..518b64d8a --- /dev/null +++ b/frontend/src/assets/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/logout.svg b/frontend/src/assets/logout.svg new file mode 100644 index 000000000..4cb4c48a4 --- /dev/null +++ b/frontend/src/assets/logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/menu.svg b/frontend/src/assets/menu.svg new file mode 100644 index 000000000..8c279f0a2 --- /dev/null +++ b/frontend/src/assets/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/openedBook.svg b/frontend/src/assets/openedBook.svg new file mode 100644 index 000000000..233c9d6fe --- /dev/null +++ b/frontend/src/assets/openedBook.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/user.svg b/frontend/src/assets/user.svg new file mode 100644 index 000000000..9a80b77a0 --- /dev/null +++ b/frontend/src/assets/user.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/common/UndraggableWrapper/styles.ts b/frontend/src/components/common/UndraggableWrapper/styles.ts index 7781b2819..8defcac00 100644 --- a/frontend/src/components/common/UndraggableWrapper/styles.ts +++ b/frontend/src/components/common/UndraggableWrapper/styles.ts @@ -5,6 +5,4 @@ export const Wrapper = styled.div` -moz-user-select: none; -ms-user-select: none; user-select: none; - - min-width: fit-content; `; diff --git a/frontend/src/components/layouts/Topbar/styles.ts b/frontend/src/components/layouts/Topbar/styles.ts index 01974ea6e..80f6ff698 100644 --- a/frontend/src/components/layouts/Topbar/styles.ts +++ b/frontend/src/components/layouts/Topbar/styles.ts @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; export const Layout = styled.section` + z-index: ${({ theme }) => theme.zIndex.topbar}; + display: flex; justify-content: space-between; diff --git a/frontend/src/components/profile/ProfileInfo/index.tsx b/frontend/src/components/profile/ProfileInfo/index.tsx new file mode 100644 index 000000000..d9c0cdc21 --- /dev/null +++ b/frontend/src/components/profile/ProfileInfo/index.tsx @@ -0,0 +1,35 @@ +import DownArrowIcon from '@/assets/downArrow.svg'; +import UndraggableWrapper from '@/components/common/UndraggableWrapper'; +import { SocialType } from '@/types/profile'; + +import ProfileTab from '../ProfileTab'; +import useProfile from '../ProfileTab/hooks/useProfile'; + +import * as S from './styles'; + +interface ProfileInfoProps { + profileImageSrc?: string; + profileId: string; + socialType: SocialType; +} + +const ProfileInfo = ({ profileImageSrc, profileId, socialType }: ProfileInfoProps) => { + const { isOpened, containerRef, handleContainerClick } = useProfile(); + + return ( + + + + + {profileImageSrc && 프로필 사진} + + {profileId} + + + + {isOpened && } + + ); +}; + +export default ProfileInfo; diff --git a/frontend/src/components/profile/ProfileInfo/styles.ts b/frontend/src/components/profile/ProfileInfo/styles.ts new file mode 100644 index 000000000..c059f86b6 --- /dev/null +++ b/frontend/src/components/profile/ProfileInfo/styles.ts @@ -0,0 +1,57 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +interface DropdownStyleProps { + $isOpened: boolean; +} + +export const ProfileSection = styled.section` + cursor: pointer; + position: relative; + width: fit-content; +`; + +export const ProfileContainer = styled.div` + display: flex; + gap: 1rem; + align-items: center; + padding: 0 1rem; +`; + +export const ProfileImageWrapper = styled.div` + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + + width: 2.8rem; + height: 2.8rem; + + background-color: ${({ theme }) => theme.colors.gray}; + border-radius: 50%; + + ${media.small} { + width: 2.6rem; + height: 2.6rem; + } +`; + +export const ProfileId = styled.p` + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + + ${media.small} { + display: none; + } +`; + +export const ArrowIcon = styled.img` + transform: ${({ $isOpened }) => ($isOpened ? 'rotate(180deg)' : 'rotate(0deg)')}; + width: 2rem; + height: 2rem; + transition: transform 0.3s ease-in-out; + + ${media.small} { + display: none; + } +`; diff --git a/frontend/src/components/profile/ProfileTab/components/ActionItem/index.tsx b/frontend/src/components/profile/ProfileTab/components/ActionItem/index.tsx new file mode 100644 index 000000000..5c21153a5 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/components/ActionItem/index.tsx @@ -0,0 +1,20 @@ +import { ProfileTabElementContent } from '@/types/profile'; + +import * as S from './styles'; + +interface ActionItemProps { + isDisplayedOnlyMobile: boolean; + content: ProfileTabElementContent; + handleItemClick: () => void; +} + +const ActionItem = ({ isDisplayedOnlyMobile, content, handleItemClick }: ActionItemProps) => { + return ( + + {content.icon.alt} + {content.text} + + ); +}; + +export default ActionItem; diff --git a/frontend/src/components/profile/ProfileTab/components/ActionItem/styles.ts b/frontend/src/components/profile/ProfileTab/components/ActionItem/styles.ts new file mode 100644 index 000000000..6026315d4 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/components/ActionItem/styles.ts @@ -0,0 +1,39 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +interface ActionItemStyleProps { + $isDisplayedOnlyMobile: boolean; +} + +export const ActionItemContainer = styled.div` + cursor: pointer; + + display: ${({ $isDisplayedOnlyMobile }) => ($isDisplayedOnlyMobile ? 'none' : 'flex')}; + gap: 1rem; + align-items: center; + + width: 100%; + height: 3rem; + padding: 1rem; + + border-radius: 0.8rem; + + :hover { + background-color: ${({ theme }) => theme.colors.lightGray}; + } + + ${media.small} { + display: ${({ $isDisplayedOnlyMobile }) => $isDisplayedOnlyMobile && 'flex'}; + } +`; + +export const ItemText = styled.p` + overflow: hidden; + display: block; + + width: 100%; + + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/frontend/src/components/profile/ProfileTab/components/Divider/index.tsx b/frontend/src/components/profile/ProfileTab/components/Divider/index.tsx new file mode 100644 index 000000000..11198c0b4 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/components/Divider/index.tsx @@ -0,0 +1,11 @@ +import * as S from './styles'; + +interface DividerProps { + isDisplayedOnlyMobile: boolean; +} + +const Divider = ({ isDisplayedOnlyMobile }: DividerProps) => { + return ; +}; + +export default Divider; diff --git a/frontend/src/components/profile/ProfileTab/components/Divider/styles.ts b/frontend/src/components/profile/ProfileTab/components/Divider/styles.ts new file mode 100644 index 000000000..b4cc27451 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/components/Divider/styles.ts @@ -0,0 +1,22 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +interface DividerStyleProps { + $isDisplayedOnlyMobile: boolean; +} + +export const Divider = styled.hr` + display: ${({ $isDisplayedOnlyMobile }) => ($isDisplayedOnlyMobile ? 'none' : 'block')}; + + width: 100%; + height: 0; + margin: 0.5rem 0; + padding: 0; + + border: 0.1rem solid ${({ theme }) => theme.colors.placeholder}; + + ${media.small} { + display: ${({ $isDisplayedOnlyMobile }) => $isDisplayedOnlyMobile && 'block'}; + } +`; diff --git a/frontend/src/components/profile/ProfileTab/components/ReadonlyItem/index.tsx b/frontend/src/components/profile/ProfileTab/components/ReadonlyItem/index.tsx new file mode 100644 index 000000000..62920db7a --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/components/ReadonlyItem/index.tsx @@ -0,0 +1,19 @@ +import { ProfileTabElementContent } from '@/types/profile'; + +import * as S from './styles'; + +interface ReadonlyItemProps { + isDisplayedOnlyMobile: boolean; + content: ProfileTabElementContent; +} + +const ReadonlyItem = ({ isDisplayedOnlyMobile, content }: ReadonlyItemProps) => { + return ( + + {content.icon.alt} + {content.text} + + ); +}; + +export default ReadonlyItem; diff --git a/frontend/src/components/profile/ProfileTab/components/ReadonlyItem/styles.ts b/frontend/src/components/profile/ProfileTab/components/ReadonlyItem/styles.ts new file mode 100644 index 000000000..cd5588922 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/components/ReadonlyItem/styles.ts @@ -0,0 +1,33 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +interface ReadonlyItemStyleProps { + $isDisplayedOnlyMobile: boolean; +} + +export const ReadonlyItemContainer = styled.div` + cursor: default; + + display: ${({ $isDisplayedOnlyMobile }) => ($isDisplayedOnlyMobile ? 'none' : 'flex')}; + gap: 1rem; + align-items: center; + + width: 100%; + height: 3rem; + padding: 1rem; + + ${media.small} { + display: ${({ $isDisplayedOnlyMobile }) => $isDisplayedOnlyMobile && 'flex'}; + } +`; + +export const ItemText = styled.p` + overflow: hidden; + display: block; + + width: 100%; + + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/frontend/src/components/profile/ProfileTab/hooks/useProfile.tsx b/frontend/src/components/profile/ProfileTab/hooks/useProfile.tsx new file mode 100644 index 000000000..a96347ca8 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/hooks/useProfile.tsx @@ -0,0 +1,28 @@ +import { useEffect, useRef, useState } from 'react'; + +const useProfile = () => { + const [isOpened, setIsOpened] = useState(false); + const containerRef = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsOpened(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [containerRef]); + + const handleContainerClick = () => { + setIsOpened((prev) => !prev); + }; + + return { isOpened, containerRef, handleContainerClick }; +}; + +export default useProfile; diff --git a/frontend/src/components/profile/ProfileTab/hooks/useProfileTabElements.tsx b/frontend/src/components/profile/ProfileTab/hooks/useProfileTabElements.tsx new file mode 100644 index 000000000..1f50ed2a7 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/hooks/useProfileTabElements.tsx @@ -0,0 +1,81 @@ +import { useNavigate } from 'react-router'; + +import GitHubIcon from '@/assets/github.svg'; +import LogoutIcon from '@/assets/logout.svg'; +import MenuIcon from '@/assets/menu.svg'; +import OpenedBookIcon from '@/assets/openedBook.svg'; +import UserIcon from '@/assets/user.svg'; +import { ProfileTabElement, SocialType } from '@/types/profile'; + +interface UseProfileTabElementsProps { + profileId: string; + socialType: SocialType; +} + +const useProfileTabElements = ({ profileId, socialType }: UseProfileTabElementsProps) => { + const navigate = useNavigate(); + + const handleReviewLinkControl = () => { + // 리뷰 링크 관리 페이지로 이동 + console.log('리뷰 링크 관리 클릭'); + }; + + const handleCheckWrittenReviews = () => { + // 작성한 리뷰 확인 페이지로 이동 + console.log('작성한 리뷰 확인 클릭'); + }; + + const handleLogout = () => { + // 로그아웃 로직 + console.log('로그아웃 클릭'); + }; + + const profileTabElements: ProfileTabElement[] = [ + { + elementId: 'socialType', + elementType: 'readonly', + isDisplayedOnlyMobile: false, + content: + // 다른 소셜 타입 추가 시 리팩토링 + socialType === 'github' + ? { icon: { src: GitHubIcon, alt: '' }, text: 'GitHub 계정' } + : { icon: { src: '', alt: '' }, text: '' }, + }, + { + elementId: 'profileId', + elementType: 'readonly', + isDisplayedOnlyMobile: true, + content: { icon: { src: UserIcon, alt: '' }, text: profileId }, + }, + { + elementId: 'reviewLinkControlButton', + elementType: 'action', + isDisplayedOnlyMobile: false, + content: { icon: { src: MenuIcon, alt: '' }, text: '리뷰 링크 관리' }, + handleClick: handleReviewLinkControl, + }, + { + elementId: 'checkWrittenReviewsButton', + elementType: 'action', + isDisplayedOnlyMobile: false, + content: { icon: { src: OpenedBookIcon, alt: '' }, text: '작성한 리뷰 확인' }, + handleClick: handleCheckWrittenReviews, + }, + { + elementId: 'divider', + elementType: 'divider', + isDisplayedOnlyMobile: false, + }, + { + elementId: 'logoutButton', + elementType: 'action', + isDisplayedOnlyMobile: false, + content: { icon: { src: LogoutIcon, alt: '' }, text: '로그아웃' }, + handleClick: handleLogout, + }, + ]; + + return { profileTabElements }; +}; + +export default useProfileTabElements; diff --git a/frontend/src/components/profile/ProfileTab/index.tsx b/frontend/src/components/profile/ProfileTab/index.tsx new file mode 100644 index 000000000..5289ffb07 --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/index.tsx @@ -0,0 +1,49 @@ +import UndraggableWrapper from '@/components/common/UndraggableWrapper'; +import { ProfileTabElement, SocialType } from '@/types/profile'; + +import ActionItem from './components/ActionItem'; +import Divider from './components/Divider'; +import ReadonlyItem from './components/ReadonlyItem'; +import useProfileTabElements from './hooks/useProfileTabElements'; +import * as S from './styles'; + +interface ProfileTabProps { + profileId: string; + socialType: SocialType; +} + +const ProfileTab = ({ socialType, profileId }: ProfileTabProps) => { + const { profileTabElements } = useProfileTabElements({ profileId, socialType }); + + const renderProfileTabItem = (item: ProfileTabElement) => { + switch (item.elementType) { + case 'readonly': + return ( + + ); + case 'action': + return ( + + ); + case 'divider': + return ; + } + }; + + return ( + + {profileTabElements.map((element) => renderProfileTabItem(element))} + + ); +}; + +export default ProfileTab; diff --git a/frontend/src/components/profile/ProfileTab/styles.ts b/frontend/src/components/profile/ProfileTab/styles.ts new file mode 100644 index 000000000..4e6b3650f --- /dev/null +++ b/frontend/src/components/profile/ProfileTab/styles.ts @@ -0,0 +1,22 @@ +import styled from '@emotion/styled'; + +export const ProfileTabContainer = styled.section` + position: absolute; + top: 5rem; + right: 0; + + display: flex; + flex-direction: column; + + width: max-content; + max-width: 25rem; + height: fit-content; + padding: 1rem; + + background-color: ${({ theme }) => theme.colors.white}; + border-radius: 0.8rem; + box-shadow: + 0 0.5rem 0.5rem -0.3rem rgba(0, 0, 0, 0.2), + 0 0.8rem 1rem 0.1rem rgba(0, 0, 0, 0.14), + 0 0.3rem 1.4rem 0.2rem rgba(0, 0, 0, 0.12); +`; diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts index c12147f14..2d83a68e1 100644 --- a/frontend/src/styles/theme.ts +++ b/frontend/src/styles/theme.ts @@ -78,7 +78,8 @@ export const colors: ThemeProperty = { export const zIndex: ThemeProperty = { main: 1, - dropdown: 998, + dropdown: 997, + topbar: 998, modal: 999, }; diff --git a/frontend/src/types/profile.ts b/frontend/src/types/profile.ts new file mode 100644 index 000000000..b85e23b46 --- /dev/null +++ b/frontend/src/types/profile.ts @@ -0,0 +1,19 @@ +export type SocialType = 'github'; + +export type ProfileTabElementType = 'readonly' | 'action' | 'divider'; + +interface ContentIcon { + src: string; + alt: string; +} +export interface ProfileTabElementContent { + icon: ContentIcon; + text: string; +} +export interface ProfileTabElement { + elementId: string; + elementType: ProfileTabElementType; + isDisplayedOnlyMobile: boolean; // true: 모바일만, false: 전체 + content?: ProfileTabElementContent; // divider 제외 지정 + handleClick?: () => void; // action일 때 지정 +} From 62fb09562d2a743ae7d6aef8cd4c1c1f276b50ee Mon Sep 17 00:00:00 2001 From: sooyeon Date: Tue, 7 Jan 2025 12:09:43 +0900 Subject: [PATCH 41/60] =?UTF-8?q?[FE]=20refactor:=20ReviewCard=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=95=EC=A0=90=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=91=9C=EC=8B=9C=ED=95=98=EB=8A=94=20ReviewKeywor?= =?UTF-8?q?d=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=9C=EA=B1=B0=20(#1039)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 강점 키워드 데이터 서버 응답 형식과 통일 * feat: StrengthKeywordList 컴포넌트 생성 * refactor: ReviewCard 폴더 위치 이동 및 디자인 수정 * refactor: formattedCategories 함수 유틸로 분리 및 목록, 상세 페이지에 적용 * refactor: 작성일 표시 방식을 ReviewDate 컴포넌트를 사용하도록 변경 * design: 상세 페이지 디자인 변경 * chore: 작성일 형식을 '-'에서 '.'으로 변경 * refactor: 배열을 처리하던 함수를 단일 문자열을 포맷팅하는 formatKeyword 함수로 변경 * refactor: ReviewCard에서 강점 키워드를 표시하는 ReviewKeyword 컴포넌트 분리 * refactor: KeywordSection에서 MultipleChoiceAnswer 컴포넌트명으로 변경 * refactor: formatKeyword 함수에서 반환 타입을 명시하지 않도록 변경 * style: css 속성 정렬 * refactor: QuestionAnswerSection을 질문과 답변(객관식/주관식) 구조로 변경 * refactor: MultilineTextViewer 상세 페이지 관련 컴포넌트로 폴더 위치 변경 * refactor: 예시 포함 여부에 따라 포맷팅된 답변 또는 원본 답변 표시 * refactor: MultilineTextViewer 컴포넌트 위치를 src/components/common/으로 복원 --- .../components/common/OptionSwitch/styles.ts | 3 +- .../{ => common}/ReviewCard/index.tsx | 15 +- .../{ => common}/ReviewCard/styles.ts | 36 ++--- .../components/common/ReviewDate/index.tsx | 9 +- .../components/common/ReviewDate/styles.ts | 22 +-- .../components/common/ReviewKeyword/index.tsx | 11 ++ .../components/common/ReviewKeyword/styles.ts | 11 ++ .../mocks/mockData/detailedReviewMockData.ts | 12 +- .../src/mocks/mockData/reviewListMockData.ts | 144 ++++++++++++------ .../DetailedReviewPageContents/index.tsx | 25 +-- .../DetailedReviewPageContents/styles.ts | 20 ++- .../components/KeywordSection/index.tsx | 21 --- .../components/KeywordSection/styles.ts | 15 -- .../components/MultipleChoiceAnswer/index.tsx | 11 ++ .../MultipleChoiceAnswerList/index.tsx | 21 +++ .../MultipleChoiceAnswerList/styles.ts | 11 ++ .../QuestionAnswerSection/index.tsx | 28 ++++ .../styles.ts | 10 +- .../components/QuestionTitle/index.tsx | 11 ++ .../components/QuestionTitle/styles.ts | 5 + .../components/ReviewDescription/index.tsx | 2 +- .../components/ReviewDescription/styles.ts | 4 - .../components/ReviewSection/index.tsx | 25 --- .../components/ReviewSectionHeader/index.tsx | 11 -- .../components/ReviewSectionHeader/styles.ts | 7 - .../DetailedReviewPage/components/index.tsx | 5 +- .../ReviewListPageContents/index.tsx | 2 +- frontend/src/utils/formatKeyword.ts | 9 ++ 28 files changed, 295 insertions(+), 211 deletions(-) rename frontend/src/components/{ => common}/ReviewCard/index.tsx (61%) rename frontend/src/components/{ => common}/ReviewCard/styles.ts (61%) create mode 100644 frontend/src/components/common/ReviewKeyword/index.tsx create mode 100644 frontend/src/components/common/ReviewKeyword/styles.ts delete mode 100644 frontend/src/pages/DetailedReviewPage/components/KeywordSection/index.tsx delete mode 100644 frontend/src/pages/DetailedReviewPage/components/KeywordSection/styles.ts create mode 100644 frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswer/index.tsx create mode 100644 frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/index.tsx create mode 100644 frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/styles.ts create mode 100644 frontend/src/pages/DetailedReviewPage/components/QuestionAnswerSection/index.tsx rename frontend/src/pages/DetailedReviewPage/components/{ReviewSection => QuestionAnswerSection}/styles.ts (59%) create mode 100644 frontend/src/pages/DetailedReviewPage/components/QuestionTitle/index.tsx create mode 100644 frontend/src/pages/DetailedReviewPage/components/QuestionTitle/styles.ts delete mode 100644 frontend/src/pages/DetailedReviewPage/components/ReviewSection/index.tsx delete mode 100644 frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/index.tsx delete mode 100644 frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/styles.ts create mode 100644 frontend/src/utils/formatKeyword.ts diff --git a/frontend/src/components/common/OptionSwitch/styles.ts b/frontend/src/components/common/OptionSwitch/styles.ts index b2be3f531..2e2bf729f 100644 --- a/frontend/src/components/common/OptionSwitch/styles.ts +++ b/frontend/src/components/common/OptionSwitch/styles.ts @@ -10,13 +10,12 @@ export const OptionSwitchContainer = styled.ul` width: 20rem; height: 4.4rem; + margin-top: 0.9rem; padding: 0.7rem; background-color: ${({ theme }) => theme.colors.lightGray}; border-radius: ${({ theme }) => theme.borderRadius.basic}; - margin-top: 0.9rem; - @media screen and (max-width: 530px) { width: 100%; } diff --git a/frontend/src/components/ReviewCard/index.tsx b/frontend/src/components/common/ReviewCard/index.tsx similarity index 61% rename from frontend/src/components/ReviewCard/index.tsx rename to frontend/src/components/common/ReviewCard/index.tsx index 92baf19a9..57b7de1e6 100644 --- a/frontend/src/components/ReviewCard/index.tsx +++ b/frontend/src/components/common/ReviewCard/index.tsx @@ -1,5 +1,8 @@ import { Category } from '@/types'; +import ReviewDate from '../ReviewDate'; +import ReviewKeyword from '../ReviewKeyword'; + import * as S from './styles'; interface ReviewCardProps { @@ -10,19 +13,21 @@ interface ReviewCardProps { } const ReviewCard = ({ createdAt, contentPreview, categories, handleClick }: ReviewCardProps) => { + const date = new Date(createdAt); + return ( - {createdAt} + {contentPreview} - - {categories.map((category) => ( -
{category.content}
+ + {categories.map(({ optionId, content }) => ( + ))} -
+
diff --git a/frontend/src/components/ReviewCard/styles.ts b/frontend/src/components/common/ReviewCard/styles.ts similarity index 61% rename from frontend/src/components/ReviewCard/styles.ts rename to frontend/src/components/common/ReviewCard/styles.ts index 5d333823e..93786a694 100644 --- a/frontend/src/components/ReviewCard/styles.ts +++ b/frontend/src/components/common/ReviewCard/styles.ts @@ -5,15 +5,15 @@ import media from '@/utils/media'; export const Layout = styled.div` display: flex; flex-direction: column; - border: 0.1rem solid ${({ theme }) => theme.colors.lightGray}; + border: 0.2rem solid ${({ theme }) => theme.colors.disabled}; border-radius: 1rem; &:hover { cursor: pointer; - border: 0.15rem solid ${({ theme }) => theme.colors.primaryHover}; + border: 0.2rem solid ${({ theme }) => theme.colors.primaryHover}; - & > div:first-of-type { - background-color: ${({ theme }) => theme.colors.lightPurple}; + & > div { + background-color: ${({ theme }) => theme.colors.palePurple}; } } `; @@ -23,27 +23,22 @@ export const Header = styled.div` align-items: center; width: 100%; - height: 3.8rem; + padding: 2rem 0 0 2.5rem; - background-color: ${({ theme }) => theme.colors.lightGray}; border-radius: 1rem 1rem 0 0; `; -export const Date = styled.p` - height: fit-content; - padding: 0 3rem; - font-size: 1.3rem; -`; - export const Main = styled.div` display: flex; flex-direction: column; gap: 2rem; width: 100%; - padding: 2rem 3rem; + padding: 2rem 2.5rem; font-size: 1.6rem; + + border-radius: 0 0 1rem 1rem; `; export const ContentPreview = styled.p` @@ -52,10 +47,10 @@ export const ContentPreview = styled.p` -webkit-box-orient: vertical; -webkit-line-clamp: 3; - height: 6rem; + height: 7.5rem; padding-right: 2rem; - line-height: 2rem; + line-height: 2.5rem; text-overflow: ellipsis; overflow-wrap: break-word; `; @@ -73,21 +68,16 @@ export const Footer = styled.div` } `; -export const Keyword = styled.div` +export const ReviewKeywordList = styled.ul` display: flex; flex-wrap: wrap; gap: 2.5rem; align-items: center; - font-size: 1.4rem; + font-size: 1.2rem; + list-style-type: none; ${media.small} { gap: 1.2rem; } - - div { - padding: 0.5rem 3rem; - background-color: ${({ theme }) => theme.colors.lightPurple}; - border-radius: 0.8rem; - } `; diff --git a/frontend/src/components/common/ReviewDate/index.tsx b/frontend/src/components/common/ReviewDate/index.tsx index 8c86290cc..921d3e9b5 100644 --- a/frontend/src/components/common/ReviewDate/index.tsx +++ b/frontend/src/components/common/ReviewDate/index.tsx @@ -1,4 +1,3 @@ -import ClockIcon from '@/assets/clock.svg'; import { formatDate } from '@/utils'; import * as S from './styles'; @@ -10,15 +9,11 @@ export interface ReviewDateProps { const ReviewDate = ({ date, dateTitle }: ReviewDateProps) => { const { year, month, day } = formatDate(date); + return ( - - - {dateTitle} - : - - {year}-{month}-{day} + {dateTitle} | {year}.{month}.{day} ); diff --git a/frontend/src/components/common/ReviewDate/styles.ts b/frontend/src/components/common/ReviewDate/styles.ts index 6af512685..dc65be47b 100644 --- a/frontend/src/components/common/ReviewDate/styles.ts +++ b/frontend/src/components/common/ReviewDate/styles.ts @@ -2,31 +2,13 @@ import styled from '@emotion/styled'; import media from '@/utils/media'; -export const ReviewDateText = styled.div` - display: inline-flex; - align-items: center; - justify-content: center; - - ${media.small} { - display: none; - } -`; - -export const ClockImg = styled.img` - width: auto; - height: 1.6rem; - margin-right: 0.8rem; -`; - export const ReviewDate = styled.div` display: flex; align-items: center; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + color: ${({ theme }) => theme.colors.gray}; ${media.xSmall} { font-size: ${({ theme }) => theme.fontSize.small}; } `; - -export const Colon = styled.span` - margin: 0 1rem; -`; diff --git a/frontend/src/components/common/ReviewKeyword/index.tsx b/frontend/src/components/common/ReviewKeyword/index.tsx new file mode 100644 index 000000000..8b77f0c22 --- /dev/null +++ b/frontend/src/components/common/ReviewKeyword/index.tsx @@ -0,0 +1,11 @@ +import formatKeyword from '@/utils/formatKeyword'; + +import * as S from './styles'; + +const ReviewKeyword = ({ content }: { content: string }) => { + const formattedKeyword = formatKeyword(content); + + return {formattedKeyword}; +}; + +export default ReviewKeyword; diff --git a/frontend/src/components/common/ReviewKeyword/styles.ts b/frontend/src/components/common/ReviewKeyword/styles.ts new file mode 100644 index 000000000..7c5345aae --- /dev/null +++ b/frontend/src/components/common/ReviewKeyword/styles.ts @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +export const ReviewKeyword = styled.li` + padding: 0.5rem 2rem; + + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + color: ${({ theme }) => theme.colors.primary}; + + background-color: ${({ theme }) => theme.colors.lightPurple}; + border-radius: 1.4rem; +`; diff --git a/frontend/src/mocks/mockData/detailedReviewMockData.ts b/frontend/src/mocks/mockData/detailedReviewMockData.ts index 919428f5c..db8e92858 100644 --- a/frontend/src/mocks/mockData/detailedReviewMockData.ts +++ b/frontend/src/mocks/mockData/detailedReviewMockData.ts @@ -26,8 +26,16 @@ export const DETAILED_REVIEW_MOCK_DATA: DetailReviewData = { minCount: 1, maxCount: 2, options: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력', isChecked: true }, - { optionId: 2, content: '💡 문제 해결 능력', isChecked: false }, + { + optionId: 1, + content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)', + isChecked: true, + }, + { + optionId: 2, + content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)', + isChecked: true, + }, ], }, }, diff --git a/frontend/src/mocks/mockData/reviewListMockData.ts b/frontend/src/mocks/mockData/reviewListMockData.ts index 8f9986c3d..7d73abd0c 100644 --- a/frontend/src/mocks/mockData/reviewListMockData.ts +++ b/frontend/src/mocks/mockData/reviewListMockData.ts @@ -10,8 +10,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2024-07-24', contentPreview: `1. 나는 짧은 데이터`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 5, content: '🌱 성장 마인드셋' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, ], }, { @@ -19,8 +23,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2023-08-29', contentPreview: `2. 전해주고 싶어 슬픈 시간이 다 흩어진 후에야 들리지만 눈을 감고 느껴봐 움직이는 마음 너를 향한 내 눈빛을 특별한 기적을 기다리지마 눈 앞에선 우리의 거친 길은 알 수 없는 미래와 벽 바꾸지 않아 포기할 수 없어 변치 않을 사랑으로 지켜줘 상처 입은 내 맘까지 시선 속에서 말은 필요 없어 멈춰져 버린 이 시간 사랑해 널 이 느낌 이대로 그려왔던 헤매임의 끝 이 세상 속에서 반복되는 슬픔 이젠 안녕 수많은 알 수 없는 길 속에 희미한 빛을 난 쫓아가 언제까지라도 함께 하는거야 다시 만난 나의 세계`, categories: [ - { optionId: 3, content: '⏰ 시간 관리 능력' }, - { optionId: 4, content: '🤓 기술적 역량, 전문 지식' }, + { optionId: 3, content: '⏰시간 관리 능력 (예: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, + { optionId: 4, content: '💻기술적 역량, 전문 지식 (예: 요구 사항을 이해하고 이를 구현하는 능력)' }, ], }, { @@ -41,8 +45,12 @@ export const REVIEW_LIST: ReviewList = { Disco overload I'm into that I'm good to go `, categories: [ - { optionId: 5, content: '🌱 성장 마인드셋' }, - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, ], }, { @@ -61,8 +69,8 @@ export const REVIEW_LIST: ReviewList = { 아무 걱정도 하지는 마, 나에게 다 맡겨 봐 `, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -80,8 +88,8 @@ export const REVIEW_LIST: ReviewList = { That tick, that tick, tick bomb `, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -95,8 +103,12 @@ export const REVIEW_LIST: ReviewList = { Ooh-ooh, ooh-ooh, lalalala-lalala `, categories: [ - { optionId: 5, content: '🌱 성장 마인드셋' }, - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, ], }, { @@ -104,8 +116,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `7. 나는 짧은 데이터`, categories: [ - { optionId: 3, content: '⏰ 시간 관리 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 3, content: '⏰시간 관리 능력 (예: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -113,8 +125,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `8. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 4, content: '🤓 기술적 역량, 전문 지식' }, - { optionId: 5, content: '🌱 성장 마인드셋' }, + { optionId: 4, content: '💻기술적 역량, 전문 지식 (예: 요구 사항을 이해하고 이를 구현하는 능력)' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, ], }, { @@ -122,8 +138,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `9. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 5, content: '🌱 성장 마인드셋' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -131,8 +151,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `10. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 3, content: '⏰ 시간 관리 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 3, content: '⏰시간 관리 능력 (예: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, ], }, { @@ -140,8 +160,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `11. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -149,8 +169,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `12. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 4, content: '🌱 성장 마인드셋' }, - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, + { + optionId: 4, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, ], }, { @@ -158,8 +182,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2024-07-24', contentPreview: `13. 물론 시중에 출간되어 있는 책들로 공부하는 것도 큰 장점이지만 더 깊은 공부를 하고 싶을 때 공식 문서를 확인해보는 것이 좋기 때문에, 저 개인적인 생각으로는 언어 공부를 아예 처음 입문하시는 분들은 한국에서 출간된 개발 서적으로 공부를 시작하시다가 모르는 부분이.....`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 5, content: '🌱 성장 마인드셋' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, ], }, { @@ -167,8 +195,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2023-08-29', contentPreview: `14. 하루스터디는 효율적인 공부 방법을 제공하는 학습 진행 도구 서비스입니다. 하루스터디는 목표 설정 단계, 학습 단계, 회고 단계를 반복하는 학습 사이클을 통해 학습 효율을 끌어올립니다. 하루스터디를 사용하게 되면 '학습을 잘 하는 방법'에 대해서...`, categories: [ - { optionId: 3, content: '⏰ 시간 관리 능력' }, - { optionId: 4, content: '🤓 기술적 역량, 전문 지식' }, + { optionId: 3, content: '⏰시간 관리 능력 (예: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, + { optionId: 4, content: '💻기술적 역량, 전문 지식 (예: 요구 사항을 이해하고 이를 구현하는 능력)' }, ], }, { @@ -176,8 +204,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `15. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 5, content: '🌱 성장 마인드셋' }, - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, ], }, { @@ -185,8 +217,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `16. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -194,8 +226,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `17. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -203,8 +235,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `18. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 5, content: '🌱 성장 마인드셋' }, - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, ], }, { @@ -212,8 +248,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `19. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 3, content: '⏰ 시간 관리 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 3, content: '⏰시간 관리 능력 (예: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -221,8 +257,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `20. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 4, content: '🤓 기술적 역량, 전문 지식' }, - { optionId: 5, content: '🌱 성장 마인드셋' }, + { optionId: 4, content: '💻기술적 역량, 전문 지식 (예: 요구 사항을 이해하고 이를 구현하는 능력)' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, ], }, { @@ -230,8 +270,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `21. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 5, content: '🌱 성장 마인드셋' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { + optionId: 5, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -239,8 +283,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `22. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 3, content: '⏰ 시간 관리 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 3, content: '⏰시간 관리 능력 (예: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, ], }, { @@ -248,8 +292,8 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `23. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, - { optionId: 2, content: '💡 문제 해결 능력' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 2, content: '💡문제 해결 능력 (예: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)' }, ], }, { @@ -257,8 +301,12 @@ export const REVIEW_LIST: ReviewList = { createdAt: '2021-08-01', contentPreview: `24. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`, categories: [ - { optionId: 4, content: '🌱 성장 마인드셋' }, - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' }, + { + optionId: 4, + content: + '🌱성장 마인드셋 (예: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (예: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, ], }, ], diff --git a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx index cef4b9e72..0141be1c4 100644 --- a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { ROUTE_PARAM } from '@/constants'; import { useGetDetailedReview, useSearchParamAndQuery } from '@/hooks'; -import { ReviewDescription, ReviewSection, KeywordSection } from '@/pages/DetailedReviewPage/components'; +import { ReviewDescription, QuestionAnswerSection } from '@/pages/DetailedReviewPage/components'; import { substituteString } from '@/utils'; import * as S from './styles'; @@ -56,14 +56,21 @@ const DetailedReviewPageContents = () => { isPublic={true} handleClickToggleButton={() => console.log('click toggle ')} /> - {parsedDetailedReview.sections.map((section) => - section.questions.map((question) => ( - - - {question.questionType === 'CHECKBOX' && } - - )), - )} + + + {parsedDetailedReview.sections.map((section) => + section.questions.map((question) => ( + + + + )), + )} + ); }; diff --git a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/styles.ts b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/styles.ts index 6acc796b6..74eb3bac8 100644 --- a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/styles.ts +++ b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/styles.ts @@ -5,7 +5,9 @@ import media from '@/utils/media'; export const DetailedReviewPageContents = styled.div` width: 70%; margin-top: 2rem; - border: 0.1rem solid ${({ theme }) => theme.colors.lightPurple}; + padding: 2rem 3rem; + + border: 0.2rem solid ${({ theme }) => theme.colors.disabled}; border-radius: ${({ theme }) => theme.borderRadius.basic}; ${media.medium} { @@ -17,10 +19,20 @@ export const DetailedReviewPageContents = styled.div` } `; -export const ReviewContentContainer = styled.div` - margin-bottom: 7rem; - padding: 0 4rem; +export const Separator = styled.div` + width: 100%; + height: 0.3rem; + margin: 3rem 0; + background-color: ${({ theme }) => theme.colors.disabled}; +`; +export const DetailedReviewContainer = styled.div` + display: flex; + flex-direction: column; + gap: 4rem; +`; + +export const ReviewContentContainer = styled.div` ${media.xSmall} { padding: 0 2rem; } diff --git a/frontend/src/pages/DetailedReviewPage/components/KeywordSection/index.tsx b/frontend/src/pages/DetailedReviewPage/components/KeywordSection/index.tsx deleted file mode 100644 index 7c9572616..000000000 --- a/frontend/src/pages/DetailedReviewPage/components/KeywordSection/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Options } from '@/types'; - -import * as S from './styles'; - -interface KeywordSectionProps { - options: Options[]; -} - -const KeywordSection = ({ options }: KeywordSectionProps) => { - return ( - - - {options.map(({ optionId, content }) => ( - {content} - ))} - - - ); -}; - -export default KeywordSection; diff --git a/frontend/src/pages/DetailedReviewPage/components/KeywordSection/styles.ts b/frontend/src/pages/DetailedReviewPage/components/KeywordSection/styles.ts deleted file mode 100644 index 09f7c695e..000000000 --- a/frontend/src/pages/DetailedReviewPage/components/KeywordSection/styles.ts +++ /dev/null @@ -1,15 +0,0 @@ -import styled from '@emotion/styled'; - -export const KeywordSection = styled.section` - width: 100%; - margin-top: 2rem; -`; - -export const KeywordList = styled.ul` - padding-left: 2rem; - list-style-type: disc; -`; - -export const KeywordItem = styled.li` - margin-bottom: 0.5rem; -`; diff --git a/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswer/index.tsx b/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswer/index.tsx new file mode 100644 index 000000000..2c0dbd4f7 --- /dev/null +++ b/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswer/index.tsx @@ -0,0 +1,11 @@ +import formatKeyword from '@/utils/formatKeyword'; + +const MultipleChoiceAnswer = ({ selectedOption }: { selectedOption: string }) => { + const isExampleIncluded = selectedOption.includes('예'); + + const formattedAnswer = formatKeyword(selectedOption); + + return
  • {isExampleIncluded ? formattedAnswer : selectedOption}
  • ; +}; + +export default MultipleChoiceAnswer; diff --git a/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/index.tsx b/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/index.tsx new file mode 100644 index 000000000..da9a4d5d1 --- /dev/null +++ b/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/index.tsx @@ -0,0 +1,21 @@ +import { Options } from '@/types'; + +import MultipleChoiceAnswer from '../MultipleChoiceAnswer'; + +import * as S from './styles'; + +interface MultipleChoiceAnswerListProps { + selectedOptionList: Options[]; +} + +const MultipleChoiceAnswerList = ({ selectedOptionList }: MultipleChoiceAnswerListProps) => { + return ( + + {selectedOptionList.map(({ optionId, content }) => ( + + ))} + + ); +}; + +export default MultipleChoiceAnswerList; diff --git a/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/styles.ts b/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/styles.ts new file mode 100644 index 000000000..db52191cf --- /dev/null +++ b/frontend/src/pages/DetailedReviewPage/components/MultipleChoiceAnswerList/styles.ts @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +export const MultipleChoiceAnswerList = styled.ul` + display: flex; + flex-direction: column; + gap: 0.5rem; + + padding-left: 4rem; + + list-style-type: disc; +`; diff --git a/frontend/src/pages/DetailedReviewPage/components/QuestionAnswerSection/index.tsx b/frontend/src/pages/DetailedReviewPage/components/QuestionAnswerSection/index.tsx new file mode 100644 index 000000000..a1606b0a1 --- /dev/null +++ b/frontend/src/pages/DetailedReviewPage/components/QuestionAnswerSection/index.tsx @@ -0,0 +1,28 @@ +import { MultilineTextViewer } from '@/components'; +import { MultipleChoiceAnswerList, QuestionTitle } from '@/pages/DetailedReviewPage/components'; +import { Options, QuestionType } from '@/types'; + +import * as S from './styles'; + +interface QuestionAnswerProps { + question: string; + questionType: QuestionType; + answer?: string; + options?: Options[]; +} + +const QuestionAnswerSection = ({ question, questionType, answer, options }: QuestionAnswerProps) => { + return ( + + + {questionType === 'CHECKBOX' && options && } + {questionType === 'TEXT' && answer && ( + + + + )} + + ); +}; + +export default QuestionAnswerSection; diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSection/styles.ts b/frontend/src/pages/DetailedReviewPage/components/QuestionAnswerSection/styles.ts similarity index 59% rename from frontend/src/pages/DetailedReviewPage/components/ReviewSection/styles.ts rename to frontend/src/pages/DetailedReviewPage/components/QuestionAnswerSection/styles.ts index eb8917aa8..59e13d1b2 100644 --- a/frontend/src/pages/DetailedReviewPage/components/ReviewSection/styles.ts +++ b/frontend/src/pages/DetailedReviewPage/components/QuestionAnswerSection/styles.ts @@ -1,16 +1,18 @@ import styled from '@emotion/styled'; -export const ReviewSection = styled.section` +export const QuestionAnswerSection = styled.section` + display: flex; + flex-direction: column; + gap: 2rem; width: 100%; - margin-top: 3.2rem; `; -export const Answer = styled.div` +export const TextAnswerWrapper = styled.div` overflow-y: auto; box-sizing: border-box; width: 100%; - height: 23rem; + height: 20rem; padding: 1rem 1.5rem; font-size: 1.6rem; diff --git a/frontend/src/pages/DetailedReviewPage/components/QuestionTitle/index.tsx b/frontend/src/pages/DetailedReviewPage/components/QuestionTitle/index.tsx new file mode 100644 index 000000000..65fdd38d1 --- /dev/null +++ b/frontend/src/pages/DetailedReviewPage/components/QuestionTitle/index.tsx @@ -0,0 +1,11 @@ +import * as S from './styles'; + +interface QuestionProps { + text: string; +} + +const QuestionTitle = ({ text }: QuestionProps) => { + return {text}; +}; + +export default QuestionTitle; diff --git a/frontend/src/pages/DetailedReviewPage/components/QuestionTitle/styles.ts b/frontend/src/pages/DetailedReviewPage/components/QuestionTitle/styles.ts new file mode 100644 index 000000000..f29376283 --- /dev/null +++ b/frontend/src/pages/DetailedReviewPage/components/QuestionTitle/styles.ts @@ -0,0 +1,5 @@ +import styled from '@emotion/styled'; + +export const QuestionTitle = styled.p` + font-weight: ${({ theme }) => theme.fontWeight.bold}; +`; diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx index ed4e721b8..72dc2f745 100644 --- a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx @@ -2,7 +2,7 @@ import ReviewDate, { ReviewDateProps } from '@/components/common/ReviewDate'; import * as S from './styles'; -const DATE_TITLE = '리뷰 작성일'; +const DATE_TITLE = '작성일'; interface ReviewDescriptionProps extends Omit { projectName: string; diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts index e6cfa2323..52ee76dff 100644 --- a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts +++ b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts @@ -9,9 +9,7 @@ export const Description = styled.section` width: 100%; margin: 0; - padding: 1rem 3rem; - background-color: ${({ theme }) => theme.colors.lightPurple}; border-radius: ${({ theme }) => theme.borderRadius.basic} ${({ theme }) => theme.borderRadius.basic} 0 0; ${media.xSmall} { @@ -29,9 +27,7 @@ export const ProjectInfoContainer = styled.div` display: flex; flex-direction: column; justify-content: flex-start; - width: 100%; - margin: 0 1rem; `; export const ProjectName = styled.p` diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSection/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewSection/index.tsx deleted file mode 100644 index c059715b6..000000000 --- a/frontend/src/pages/DetailedReviewPage/components/ReviewSection/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { MultilineTextViewer } from '@/components'; - -import ReviewSectionHeader from '../ReviewSectionHeader'; - -import * as S from './styles'; - -interface ReviewSectionProps { - question: string; - answer: string; -} - -const ReviewSection = ({ question, answer }: ReviewSectionProps) => { - return ( - - - {answer && ( - - - - )} - - ); -}; - -export default ReviewSection; diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/index.tsx deleted file mode 100644 index 50fa0dab5..000000000 --- a/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as S from './styles'; - -interface ReviewSectionHeaderProps { - text: string; -} - -const ReviewSectionHeader = ({ text }: ReviewSectionHeaderProps) => { - return {text}; -}; - -export default ReviewSectionHeader; diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/styles.ts deleted file mode 100644 index 517c62e27..000000000 --- a/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/styles.ts +++ /dev/null @@ -1,7 +0,0 @@ -import styled from '@emotion/styled'; - -export const ReviewSectionHeader = styled.p` - margin-bottom: 1rem; - font-size: 1.6rem; - font-weight: bold; -`; diff --git a/frontend/src/pages/DetailedReviewPage/components/index.tsx b/frontend/src/pages/DetailedReviewPage/components/index.tsx index 4bfb172bd..a455c2400 100644 --- a/frontend/src/pages/DetailedReviewPage/components/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/components/index.tsx @@ -1,4 +1,5 @@ -export { default as KeywordSection } from './KeywordSection'; +export { default as QuestionAnswerSection } from './QuestionAnswerSection'; +export { default as QuestionTitle } from './QuestionTitle'; +export { default as MultipleChoiceAnswerList } from './MultipleChoiceAnswerList'; export { default as ReviewDescription } from './ReviewDescription'; -export { default as ReviewSection } from './ReviewSection'; export { default as DetailedReviewPageContents } from './DetailedReviewPageContents'; diff --git a/frontend/src/pages/ReviewListPage/components/ReviewListPageContents/index.tsx b/frontend/src/pages/ReviewListPage/components/ReviewListPageContents/index.tsx index 172242ad0..3d8f11597 100644 --- a/frontend/src/pages/ReviewListPage/components/ReviewListPageContents/index.tsx +++ b/frontend/src/pages/ReviewListPage/components/ReviewListPageContents/index.tsx @@ -2,9 +2,9 @@ import { useContext } from 'react'; import { useNavigate } from 'react-router'; import { ReviewEmptySection } from '@/components'; +import ReviewCard from '@/components/common/ReviewCard'; import UndraggableWrapper from '@/components/common/UndraggableWrapper'; import { ReviewInfoDataContext } from '@/components/layouts/ReviewDisplayLayout/ReviewInfoDataProvider'; -import ReviewCard from '@/components/ReviewCard'; import { REVIEW_EMPTY } from '@/constants'; import { ROUTE } from '@/constants/route'; import { useGetReviewList, useSearchParamAndQuery } from '@/hooks'; diff --git a/frontend/src/utils/formatKeyword.ts b/frontend/src/utils/formatKeyword.ts new file mode 100644 index 000000000..7e2a046e5 --- /dev/null +++ b/frontend/src/utils/formatKeyword.ts @@ -0,0 +1,9 @@ +const formatKeyword = (content: string) => { + const contentWithoutExample = Array.from(content.split(' (예: ')[0]); + const emoji = contentWithoutExample.shift(); + const keyword = contentWithoutExample.join(''); + + return `${emoji} ${keyword}`; +}; + +export default formatKeyword; From b033992b8bbcf942002d4a98518a8d7686d0b628 Mon Sep 17 00:00:00 2001 From: Fe <64690761+chysis@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:13:03 +0900 Subject: [PATCH 42/60] =?UTF-8?q?[FE]=20feat:=20=EB=92=A4=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#1040)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 상단 이동 버튼이 중복으로 들어간 문제 해결 * feat: 뒤로 가기 버튼 구현 * chore: 불필요한 주석 제거 * refactor: 뒤로 가기 버튼을 페이지에 배치할 때 필요한 공통 스타일링 처리 * chore: 누락된 스타일 코드 추가 * refactor: props 네이밍 수정 * chore: BackButton의 type 속성 지정 --- frontend/src/assets/backButton.svg | 3 ++ .../components/common/BackButton/index.tsx | 31 +++++++++++++++++++ .../components/common/BackButton/styles.ts | 20 ++++++++++++ .../DetailedReviewPageContents/index.tsx | 1 - .../src/pages/ReviewCollectionPage/index.tsx | 3 +- frontend/src/pages/ReviewListPage/index.tsx | 3 +- 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 frontend/src/assets/backButton.svg create mode 100644 frontend/src/components/common/BackButton/index.tsx create mode 100644 frontend/src/components/common/BackButton/styles.ts diff --git a/frontend/src/assets/backButton.svg b/frontend/src/assets/backButton.svg new file mode 100644 index 000000000..3693f7b99 --- /dev/null +++ b/frontend/src/assets/backButton.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/common/BackButton/index.tsx b/frontend/src/components/common/BackButton/index.tsx new file mode 100644 index 000000000..c2e295c27 --- /dev/null +++ b/frontend/src/components/common/BackButton/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { NavigateOptions, useNavigate } from 'react-router'; + +import BackButtonIcon from '@/assets/backButton.svg'; + +import * as S from './styles'; + +interface BackButtonProps { + prevPath: string; + navigateOptions?: NavigateOptions; + buttonStyle?: React.CSSProperties; + wrapperStyle?: React.CSSProperties; +} + +const BackButton = ({ prevPath, navigateOptions, buttonStyle, wrapperStyle }: BackButtonProps) => { + const navigate = useNavigate(); + + const handleBackButtonClick = () => { + navigate(prevPath, navigateOptions); + }; + + return ( + + + 뒤로가기 버튼 + + + ); +}; + +export default BackButton; diff --git a/frontend/src/components/common/BackButton/styles.ts b/frontend/src/components/common/BackButton/styles.ts new file mode 100644 index 000000000..46a909b04 --- /dev/null +++ b/frontend/src/components/common/BackButton/styles.ts @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; + +interface BackButtonStyleProps { + $style?: React.CSSProperties; +} + +export const BackButtonWrapper = styled.div` + display: flex; + justify-content: flex-start; + width: 100%; + + ${({ $style }) => $style && { ...$style }} +`; + +export const BackButton = styled.button` + width: 3.5rem; + height: 3.5rem; + + ${({ $style }) => $style && { ...$style }} +`; diff --git a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx index 0141be1c4..01b01c9e7 100644 --- a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx @@ -46,7 +46,6 @@ const DetailedReviewPageContents = () => { }; }, [detailedReview]); - // TODO: 리뷰 공개/비공개 토글 버튼 기능 return ( { - ); diff --git a/frontend/src/pages/ReviewListPage/index.tsx b/frontend/src/pages/ReviewListPage/index.tsx index 3924129ef..318b6bfcc 100644 --- a/frontend/src/pages/ReviewListPage/index.tsx +++ b/frontend/src/pages/ReviewListPage/index.tsx @@ -1,4 +1,4 @@ -import { ErrorSuspenseContainer, AuthAndServerErrorFallback, TopButton } from '@/components'; +import { ErrorSuspenseContainer, AuthAndServerErrorFallback } from '@/components'; import ReviewDisplayLayout from '@/components/layouts/ReviewDisplayLayout'; import ReviewListPageContents from './components/ReviewListPageContents'; @@ -8,7 +8,6 @@ const ReviewListPage = () => { - ); From eaad162c72625996c668ad9c5a52977b29f453c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yejin=20Lee=28=EC=9D=B4=EC=98=88=EC=A7=84=29?= <111052302+ImxYJL@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:05:03 +0900 Subject: [PATCH 43/60] =?UTF-8?q?[FE]=20feat:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=AA=A8=EB=8B=AC=20=20(#1022)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: LoginButton 컴포넌트트 * feat: GithubLoginButton 컴포넌트트 * chore: component 파일의 index에 GithubLoginButton, LoginButton 추가가 * refactor: style 전용 인터페이스 분리 및 공통 컴포넌트 import 경로 수정정 * chore: modals의 index에 ContentModal 추가가 * feat: LoginRequestModal 컴포넌트트 * refactor: 클릭 핸들러 네이밍 간소화 * chore: components 하위에 login 폴더 생성 및 관련 컴포넌트 이동 * chore: login 폴더 추가 이동 * chore: 주석 삭제 * chore: self-closing 적용 * chore: LoginRequestTitle 타입에서 -type 접미사 삭제 * refactor: useTheme 사용 및 스타일 인터페이스에 Omit 대신 Pick 적용 * feat: 로그인 요청 모달에 에러 메세지 구역 추가 * refactor: LoginButton 문구 수정 및 그에 따른 컴포넌트 구조 수정 * refactor: 스타일 객체의 이름을 명확하게 수정 * refactor: LoginRequestTitleMap 생성 * fix: LoginRequestTitleMap에 readonly 추가 * refactor: LoginRequestTitle 관리 방식을 객체로 변경 * fix: LOGIN_REQUEST_TITLE value를 한국어 안내로 변경 --- frontend/src/assets/githubWhiteLogo.svg | 9 ++++ .../src/components/common/modals/index.tsx | 1 + frontend/src/components/index.tsx | 1 + .../login/GithubLoginButton/index.tsx | 21 ++++++++ .../components/login/LoginButton/index.tsx | 27 ++++++++++ .../components/login/LoginButton/styles.ts | 16 ++++++ .../login/LoginRequestModal/index.tsx | 51 +++++++++++++++++++ .../login/LoginRequestModal/styles.ts | 18 +++++++ frontend/src/components/login/index.ts | 3 ++ frontend/src/pages/ReviewZonePage/index.tsx | 1 - 10 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 frontend/src/assets/githubWhiteLogo.svg create mode 100644 frontend/src/components/login/GithubLoginButton/index.tsx create mode 100644 frontend/src/components/login/LoginButton/index.tsx create mode 100644 frontend/src/components/login/LoginButton/styles.ts create mode 100644 frontend/src/components/login/LoginRequestModal/index.tsx create mode 100644 frontend/src/components/login/LoginRequestModal/styles.ts create mode 100644 frontend/src/components/login/index.ts diff --git a/frontend/src/assets/githubWhiteLogo.svg b/frontend/src/assets/githubWhiteLogo.svg new file mode 100644 index 000000000..dc79940e2 --- /dev/null +++ b/frontend/src/assets/githubWhiteLogo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/common/modals/index.tsx b/frontend/src/components/common/modals/index.tsx index e6c52e598..9918f5d51 100644 --- a/frontend/src/components/common/modals/index.tsx +++ b/frontend/src/components/common/modals/index.tsx @@ -1,3 +1,4 @@ export { default as ConfirmModal } from './ConfirmModal'; export { default as AlertModal } from './AlertModal'; export { default as ErrorAlertModal } from './ErrorAlertModal'; +export { default as ContentModal } from './ContentModal'; diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx index 34bf9c0c8..9c0ce42cc 100644 --- a/frontend/src/components/index.tsx +++ b/frontend/src/components/index.tsx @@ -2,3 +2,4 @@ export * from './layouts'; export * from './common'; export * from './error'; export * from './highlight/components'; +export * from './login'; diff --git a/frontend/src/components/login/GithubLoginButton/index.tsx b/frontend/src/components/login/GithubLoginButton/index.tsx new file mode 100644 index 000000000..db6905b00 --- /dev/null +++ b/frontend/src/components/login/GithubLoginButton/index.tsx @@ -0,0 +1,21 @@ +import GithubWhiteLogoIcon from '@/assets/githubWhiteLogo.svg'; +import { LoginButton } from '@/components/login'; +import { LoginButtonStyleProps } from '@/components/login/LoginButton'; + +interface GithubLoginButtonProps extends LoginButtonStyleProps { + handleClick: () => void; +} + +const GithubLoginButton = ({ handleClick, $logoImgStyle, $buttonStyle }: GithubLoginButtonProps) => { + return ( + + ); +}; + +export default GithubLoginButton; diff --git a/frontend/src/components/login/LoginButton/index.tsx b/frontend/src/components/login/LoginButton/index.tsx new file mode 100644 index 000000000..366f055cc --- /dev/null +++ b/frontend/src/components/login/LoginButton/index.tsx @@ -0,0 +1,27 @@ +import { Button } from '@/components/index'; + +import * as S from './styles'; + +interface LoginButtonProps extends LoginButtonStyleProps { + platform: string; + logoSrc: string; + handleClick: () => void; +} + +export interface LoginButtonStyleProps { + $logoImgStyle?: React.CSSProperties; + $buttonStyle?: React.CSSProperties; +} + +const LoginButton = ({ platform, logoSrc, handleClick, $logoImgStyle, $buttonStyle }: LoginButtonProps) => { + return ( + + ); +}; + +export default LoginButton; diff --git a/frontend/src/components/login/LoginButton/styles.ts b/frontend/src/components/login/LoginButton/styles.ts new file mode 100644 index 000000000..b774a615e --- /dev/null +++ b/frontend/src/components/login/LoginButton/styles.ts @@ -0,0 +1,16 @@ +import styled from '@emotion/styled'; + +import { LoginButtonStyleProps } from './index'; + +export const ButtonLabelContainer = styled.div` + display: flex; + gap: 0.8rem; + align-items: center; +`; + +export const LogoImg = styled.img>` + width: 3.2rem; + height: 3.2rem; + + ${({ $logoImgStyle }) => $logoImgStyle && { ...$logoImgStyle }}; +`; diff --git a/frontend/src/components/login/LoginRequestModal/index.tsx b/frontend/src/components/login/LoginRequestModal/index.tsx new file mode 100644 index 000000000..bd518d200 --- /dev/null +++ b/frontend/src/components/login/LoginRequestModal/index.tsx @@ -0,0 +1,51 @@ +import { useTheme } from '@emotion/react'; +import { useState } from 'react'; + +import { ContentModal, GithubLoginButton } from '@/components'; + +import * as S from './styles'; + +const LOGIN_REQUEST_TITLE = { + loginIntent: '로그인하시겠어요?', + membershipCheck: '회원이신가요?', +} as const; + +type LoginRequestTitle = keyof typeof LOGIN_REQUEST_TITLE; + +interface LoginRequestModalProps { + titleType: LoginRequestTitle; + closeModal: () => void; +} + +const LoginRequestModal = ({ titleType, closeModal }: LoginRequestModalProps) => { + const [errorMessage, setErrorMessage] = useState(''); + const theme = useTheme(); + + // 에러 메세지 확인용. 추후 API 호출로 변경 + const handleClickLoginButton = () => { + setErrorMessage('에러 메세지'); + }; + + return ( + + + 로그인 후 간편하게 받은 리뷰를 확인하세요! + + {errorMessage && {errorMessage}} + + + ); +}; + +export default LoginRequestModal; diff --git a/frontend/src/components/login/LoginRequestModal/styles.ts b/frontend/src/components/login/LoginRequestModal/styles.ts new file mode 100644 index 000000000..674f69f21 --- /dev/null +++ b/frontend/src/components/login/LoginRequestModal/styles.ts @@ -0,0 +1,18 @@ +import styled from '@emotion/styled'; + +export const LoginRequestModal = styled.div` + display: flex; + flex-direction: column; + height: 9.5rem; +`; + +export const LoginRequestLabel = styled.p` + margin-bottom: 1rem; + font-size: 1.4rem; +`; + +export const ErrorMessage = styled.p` + margin-top: 0.6rem; + font-size: 1.2rem; + color: ${({ theme }) => theme.colors.red}; +`; diff --git a/frontend/src/components/login/index.ts b/frontend/src/components/login/index.ts new file mode 100644 index 000000000..1f41e25f0 --- /dev/null +++ b/frontend/src/components/login/index.ts @@ -0,0 +1,3 @@ +export { default as GithubLoginButton } from '../login/GithubLoginButton'; +export { default as LoginButton } from '../login/LoginButton'; +export { default as LoginRequestModal } from '../login/LoginRequestModal'; diff --git a/frontend/src/pages/ReviewZonePage/index.tsx b/frontend/src/pages/ReviewZonePage/index.tsx index 790431550..89b6cb35a 100644 --- a/frontend/src/pages/ReviewZonePage/index.tsx +++ b/frontend/src/pages/ReviewZonePage/index.tsx @@ -4,7 +4,6 @@ import { useRecoilState } from 'recoil'; import ReviewZoneIcon from '@/assets/reviewZone.svg'; import { Button } from '@/components'; -// TODO: ROUTE 상수명을 단수로 고치기 import { ROUTE } from '@/constants/route'; import { useGetReviewGroupData, useSearchParamAndQuery, useModals } from '@/hooks'; import { reviewRequestCodeAtom } from '@/recoil'; From 2f94d3edf27608cc395e8474ea605d21e90a8f8d Mon Sep 17 00:00:00 2001 From: sooyeon Date: Fri, 10 Jan 2025 14:23:11 +0900 Subject: [PATCH 44/60] =?UTF-8?q?[FE]=20feat:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=99=80=20=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#1032)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 리뷰미 로고 글자 간격 및 크기 조정 * refactor: 푸터에 border 추가 * feat: 리뷰 링크 관리 페이지 레이아웃 구현 * feat: 리뷰 링크 관리 페이지 구현 및 레이아웃 적용 * feat: 라우터 설정 * fix: Amplitude 방문 이벤트에 리뷰 링크 관리 페이지 추가 * refactor: ErrorSuspenseContainer로 감싼 후, ReviewLinkManagementPage에서 ReviewLinkDashboard로 분리 * feat: ReviewLinkLayout 컴포넌트 추가 및 반응형 스타일 적용 * feat: 모바일 환경에서 CardList의 max-height를 none으로 설정 및 반응형 세부 수정 * fix: ReviewCard 컴포넌트 flex-start로 상단 정렬 및 위치 비워두기 * refactor: 리뷰 링크 관리 페이지 경로명 및 폴더명 변경 --- .../src/components/layouts/Footer/style.ts | 2 + .../layouts/Topbar/components/Logo/styles.ts | 8 +- frontend/src/constants/amplitudeEventName.ts | 1 + frontend/src/constants/route.ts | 1 + .../components/URLGeneratorForm/styles.ts | 2 +- .../components/ReviewLinkDashboard/index.tsx | 27 +++++++ .../components/ReviewLinkDashboard/styles.ts | 78 +++++++++++++++++++ .../layouts/ReviewLinkLayout/index.tsx | 22 ++++++ .../layouts/ReviewLinkLayout/styles.ts | 58 ++++++++++++++ frontend/src/pages/ReviewLinkPage/index.tsx | 14 ++++ frontend/src/pages/index.tsx | 1 + frontend/src/router.tsx | 2 + 12 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx create mode 100644 frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts create mode 100644 frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/index.tsx create mode 100644 frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/styles.ts create mode 100644 frontend/src/pages/ReviewLinkPage/index.tsx diff --git a/frontend/src/components/layouts/Footer/style.ts b/frontend/src/components/layouts/Footer/style.ts index 51c79fd4c..394029288 100644 --- a/frontend/src/components/layouts/Footer/style.ts +++ b/frontend/src/components/layouts/Footer/style.ts @@ -21,6 +21,8 @@ export const Footer = styled.footer` background-color: ${({ theme }) => theme.colors.white}; + border-top: 0.1rem solid ${({ theme }) => theme.colors.lightGray}; + ${media.xSmall} { flex-direction: column; gap: 0.2rem; diff --git a/frontend/src/components/layouts/Topbar/components/Logo/styles.ts b/frontend/src/components/layouts/Topbar/components/Logo/styles.ts index 682e0908d..860d6abea 100644 --- a/frontend/src/components/layouts/Topbar/components/Logo/styles.ts +++ b/frontend/src/components/layouts/Topbar/components/Logo/styles.ts @@ -3,17 +3,11 @@ import styled from '@emotion/styled'; import media from '@/utils/media'; export const Logo = styled.div` - line-height: 8rem; text-align: center; span { - font-size: 3rem; + font-size: 2.8rem; font-weight: ${({ theme }) => theme.fontWeight.bolder}; - letter-spacing: 0.7rem; - - ${media.small} { - font-size: 2.8rem; - } ${media.xSmall} { font-size: 2.6rem; diff --git a/frontend/src/constants/amplitudeEventName.ts b/frontend/src/constants/amplitudeEventName.ts index c610d53b7..a90ae22e5 100644 --- a/frontend/src/constants/amplitudeEventName.ts +++ b/frontend/src/constants/amplitudeEventName.ts @@ -24,6 +24,7 @@ export const PAGE_VISITED_EVENT_NAME: { [key in Exclude]: s detailedReview: '[page] 리뷰 상세 보기 페이지', reviewWriting: '[page] 리뷰 작성 페이지', reviewWritingComplete: '[page] 리뷰 작성 완료 페이지', + reviewLinks: '[page] 리뷰 링크 관리 페이지', }; export const REVIEW_WRITING_EVENT_NAME = { diff --git a/frontend/src/constants/route.ts b/frontend/src/constants/route.ts index 0826e9ff8..df2fc17fe 100644 --- a/frontend/src/constants/route.ts +++ b/frontend/src/constants/route.ts @@ -7,4 +7,5 @@ export const ROUTE = { detailedReview: 'user/detailed-review', reviewZone: 'user/review-zone', reviewCollection: 'user/review-collection', + reviewLinks: 'user/review-links', }; diff --git a/frontend/src/pages/HomePage/components/URLGeneratorForm/styles.ts b/frontend/src/pages/HomePage/components/URLGeneratorForm/styles.ts index 80fcda2fa..a15d87396 100644 --- a/frontend/src/pages/HomePage/components/URLGeneratorForm/styles.ts +++ b/frontend/src/pages/HomePage/components/URLGeneratorForm/styles.ts @@ -8,10 +8,10 @@ export const URLGeneratorForm = styled.section` justify-content: center; width: 40%; - padding: 0 9rem; ${media.medium} { width: 45%; + padding: 0 9rem; h2 { font-size: 2rem; diff --git a/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx new file mode 100644 index 000000000..aaf99ed0c --- /dev/null +++ b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx @@ -0,0 +1,27 @@ +import { URLGeneratorForm } from '@/pages/HomePage/components'; + +import ReviewLinkLayout from '../layouts/ReviewLinkLayout'; + +import * as S from './styles'; + +const ReviewLinkDashboard = () => { + return ( + + + + + + + + {/* TODO: ReviewCard 컴포넌트 추가 및 생성한 리뷰 링크가 없을 경우, 돋보기 컴포넌트 추가 */} + <> + + + + ); +}; + +export default ReviewLinkDashboard; diff --git a/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts new file mode 100644 index 000000000..8d4ab58ae --- /dev/null +++ b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts @@ -0,0 +1,78 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +export const ReviewLinkDashboardContainer = styled.div` + display: flex; + justify-content: center; + gap: 7rem; + + width: 100%; + + ${media.medium} { + gap: 4rem; + } + + @media screen and (max-width: 900px) { + gap: 2rem; + } + + ${media.small} { + flex-direction: column; + align-items: center; + } +`; + +export const FormSection = styled.section` + section { + width: auto; + } + + padding: 5rem 0; + + ${media.medium} { + section { + padding: 0; + } + } + + ${media.small} { + width: 100%; + padding: 0; + } +`; + +export const Separator = styled.div` + width: 0.1rem; + // 전체 영역에서 헤더(7rem)와 푸터(6rem) 영역 제외하고, 추후 네비게이션 탭이 추가되면 해당 영역도 제외 + min-height: calc(100vh - 13rem); + + background-color: ${({ theme }) => theme.colors.lightGray}; + + ${media.small} { + display: none; + } +`; + +export const LinkSection = styled.section` + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 3rem; + + width: 100%; + + padding: 5rem 0; + + ${media.medium} { + width: 50%; + } + + ${media.small} { + width: 85%; + } + + ${media.xSmall} { + width: 90%; + } +`; diff --git a/frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/index.tsx b/frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/index.tsx new file mode 100644 index 000000000..b6ca6fee1 --- /dev/null +++ b/frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/index.tsx @@ -0,0 +1,22 @@ +import { EssentialPropsWithChildren } from '@/types'; + +import * as S from './styles'; + +interface ReviewLinkListLayoutProps { + title: string; + subTitle: string; +} + +const ReviewLinkLayout = ({ title, subTitle, children }: EssentialPropsWithChildren) => { + return ( + + + {title} + {subTitle} + + {children} + + ); +}; + +export default ReviewLinkLayout; diff --git a/frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/styles.ts b/frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/styles.ts new file mode 100644 index 000000000..ed8ec571f --- /dev/null +++ b/frontend/src/pages/ReviewLinkPage/components/layouts/ReviewLinkLayout/styles.ts @@ -0,0 +1,58 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +export const ReviewLinkLayout = styled.div` + display: flex; + flex-direction: column; + + gap: 4rem; +`; + +export const TitleWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; +`; + +export const Title = styled.h2` + ${media.medium} { + font-size: 2rem; + } + + ${media.xSmall} { + font-size: 1.8rem; + } + + ${media.xxSmall} { + font-size: 1.6rem; + } +`; + +export const SubTitle = styled.span` + color: ${({ theme }) => theme.colors.gray}; + + ${media.xSmall} { + font-size: 1.5rem; + } + + ${media.xxSmall} { + font-size: 1.3rem; + } +`; + +export const CardList = styled.ul` + display: flex; + flex-direction: column; + gap: 4rem; + + max-height: calc(100vh - 34rem); + overflow-y: auto; + + padding-right: 2rem; + + ${media.small} { + max-height: none; + padding-right: 0; + } +`; diff --git a/frontend/src/pages/ReviewLinkPage/index.tsx b/frontend/src/pages/ReviewLinkPage/index.tsx new file mode 100644 index 000000000..f9ecf4719 --- /dev/null +++ b/frontend/src/pages/ReviewLinkPage/index.tsx @@ -0,0 +1,14 @@ +import { ErrorSuspenseContainer } from '@/components'; + +import ReviewLinkDashboard from './components/ReviewLinkDashboard'; + +const ReviewLinkPage = () => { + return ( + + {/* TODO: 네비게이션 탭 추가 */} + + + ); +}; + +export default ReviewLinkPage; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 4dc586d19..28b98890c 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -7,3 +7,4 @@ export { default as ReviewWritingPage } from './ReviewWritingPage'; export { default as ReviewWritingCompletePage } from './ReviewWritingCompletePage'; export { default as ReviewZonePage } from './ReviewZonePage'; export { default as ReviewCollectionPage } from './ReviewCollectionPage'; +export { default as ReviewLinkPage } from './ReviewLinkPage'; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 0fd64b227..a8abb7137 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -10,6 +10,7 @@ const ReviewWritingPage = lazy(() => import('@/pages/ReviewWritingPage')); const ReviewZonePage = lazy(() => import('@/pages/ReviewZonePage')); const ReviewCollectionPage = lazy(() => import('@/pages/ReviewCollectionPage')); const LoadingPage = lazy(() => import('@/pages/LoadingPage')); +const ReviewLinkPage = lazy(() => import('@/pages/ReviewLinkPage')); import App from './App'; import { ErrorSuspenseContainer } from './components'; @@ -52,6 +53,7 @@ const router = createBrowserRouter([ ), }, { path: `${ROUTE.reviewCollection}/:${ROUTE_PARAM.reviewRequestCode}`, element: }, + { path: `${ROUTE.reviewLinks}`, element: }, ], }, ]); From 08a5cafb7ce90139404a3ac82002f88f04d1473e Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sun, 12 Jan 2025 17:59:49 +0900 Subject: [PATCH 45/60] =?UTF-8?q?[BE]=20docs:=20GitHub=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99,=20=EC=9E=90=EC=8B=A0=EC=9D=B4=20=EB=A7=8C=EB=93=A0?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EA=B7=B8=EB=A3=B9=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=20=EB=AC=B8=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#1014)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/docs/asciidoc/auth.adoc | 7 +++ backend/src/docs/asciidoc/index.adoc | 8 +++ backend/src/docs/asciidoc/member.adoc | 3 + backend/src/docs/asciidoc/reviewgroup.adoc | 4 ++ .../auth/controller/AuthController.java | 33 +++++++++++ .../reviewme/auth/service/AuthService.java | 7 +++ .../auth/service/dto/GithubCodeRequest.java | 8 +++ .../member/controller/MemberController.java | 21 +++++++ .../member/service/MemberService.java | 12 ++++ .../member/service/dto/ProfileResponse.java | 7 +++ .../review/controller/ReviewController.java | 6 +- .../service/ReviewListLookupService.java | 16 ++--- ...=> ReceivedReviewPageElementResponse.java} | 2 +- ...e.java => ReceivedReviewPageResponse.java} | 4 +- .../service/mapper/ReviewListMapper.java | 10 ++-- .../controller/ReviewGroupController.java | 10 +++- .../service/ReviewGroupLookupService.java | 6 ++ .../dto/ReviewGroupPageElementResponse.java | 12 ++++ .../service/dto/ReviewGroupPageResponse.java | 10 ++++ .../src/test/java/reviewme/api/ApiTest.java | 14 ++++- .../test/java/reviewme/api/AuthApiTest.java | 59 +++++++++++++++++++ .../test/java/reviewme/api/MemberApiTest.java | 45 ++++++++++++++ .../test/java/reviewme/api/ReviewApiTest.java | 12 ++-- .../java/reviewme/api/ReviewGroupApiTest.java | 47 ++++++++++++++- .../service/ReviewListLookupServiceTest.java | 6 +- .../service/mapper/ReviewListMapperTest.java | 6 +- 26 files changed, 341 insertions(+), 34 deletions(-) create mode 100644 backend/src/docs/asciidoc/auth.adoc create mode 100644 backend/src/docs/asciidoc/member.adoc create mode 100644 backend/src/main/java/reviewme/auth/controller/AuthController.java create mode 100644 backend/src/main/java/reviewme/auth/service/AuthService.java create mode 100644 backend/src/main/java/reviewme/auth/service/dto/GithubCodeRequest.java create mode 100644 backend/src/main/java/reviewme/member/controller/MemberController.java create mode 100644 backend/src/main/java/reviewme/member/service/MemberService.java create mode 100644 backend/src/main/java/reviewme/member/service/dto/ProfileResponse.java rename backend/src/main/java/reviewme/review/service/dto/response/list/{ReviewListElementResponse.java => ReceivedReviewPageElementResponse.java} (83%) rename backend/src/main/java/reviewme/review/service/dto/response/list/{ReceivedReviewsResponse.java => ReceivedReviewPageResponse.java} (66%) create mode 100644 backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageElementResponse.java create mode 100644 backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageResponse.java create mode 100644 backend/src/test/java/reviewme/api/AuthApiTest.java create mode 100644 backend/src/test/java/reviewme/api/MemberApiTest.java diff --git a/backend/src/docs/asciidoc/auth.adoc b/backend/src/docs/asciidoc/auth.adoc new file mode 100644 index 000000000..17900fe72 --- /dev/null +++ b/backend/src/docs/asciidoc/auth.adoc @@ -0,0 +1,7 @@ +==== 깃허브로 로그인/회원가입 + +operation::github-auth[snippets="curl-request,request-fields,http-response"] + +==== 로그아웃 + +operation::logout[snippets="curl-request,request-cookies,http-response"] diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 4d67754a7..0575b8a24 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -40,3 +40,11 @@ include::review-gather.adoc[] === 답변 하이라이트 include::highlight-answers.adoc[] + +== 인증 + +include::auth.adoc[] + +== 사용자 + +include::member.adoc[] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc new file mode 100644 index 000000000..9a51e2c94 --- /dev/null +++ b/backend/src/docs/asciidoc/member.adoc @@ -0,0 +1,3 @@ +==== 내 프로필 정보 + +operation::my-profile[snippets="curl-request,request-cookies,http-response,response-fields"] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/reviewgroup.adoc b/backend/src/docs/asciidoc/reviewgroup.adoc index c3eb2f803..79d70a9f5 100644 --- a/backend/src/docs/asciidoc/reviewgroup.adoc +++ b/backend/src/docs/asciidoc/reviewgroup.adoc @@ -9,3 +9,7 @@ operation::review-group-summary[snippets="curl-request,http-response,response-fi ==== 리뷰 요청 코드, 확인 코드 일치 여부 operation::review-group-check-access[snippets="curl-request,request-fields,http-response,response-cookies"] + +==== 자신이 만든 리뷰 그룹 목록 조회 + +operation::review-group-list[snippets="curl-request,request-cookies,http-response,response-fields"] diff --git a/backend/src/main/java/reviewme/auth/controller/AuthController.java b/backend/src/main/java/reviewme/auth/controller/AuthController.java new file mode 100644 index 000000000..14573f2fe --- /dev/null +++ b/backend/src/main/java/reviewme/auth/controller/AuthController.java @@ -0,0 +1,33 @@ +package reviewme.auth.controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import reviewme.auth.service.AuthService; +import reviewme.auth.service.dto.GithubCodeRequest; + +@RestController +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/v2/auth/github") + public ResponseEntity authWithGithub( + @Valid @RequestBody GithubCodeRequest request, + HttpServletRequest httpRequest + ) { + return ResponseEntity.ok().build(); + } + + @PostMapping("/v2/auth/logout") + public ResponseEntity logout( + HttpServletRequest httpRequest + ) { + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/reviewme/auth/service/AuthService.java b/backend/src/main/java/reviewme/auth/service/AuthService.java new file mode 100644 index 000000000..5458807af --- /dev/null +++ b/backend/src/main/java/reviewme/auth/service/AuthService.java @@ -0,0 +1,7 @@ +package reviewme.auth.service; + +import org.springframework.stereotype.Service; + +@Service +public class AuthService { +} diff --git a/backend/src/main/java/reviewme/auth/service/dto/GithubCodeRequest.java b/backend/src/main/java/reviewme/auth/service/dto/GithubCodeRequest.java new file mode 100644 index 000000000..b26511917 --- /dev/null +++ b/backend/src/main/java/reviewme/auth/service/dto/GithubCodeRequest.java @@ -0,0 +1,8 @@ +package reviewme.auth.service.dto; + +import jakarta.validation.constraints.NotBlank; + +public record GithubCodeRequest( + @NotBlank(message = "깃허브 임시 코드를 입력해주세요.") + String code) { +} diff --git a/backend/src/main/java/reviewme/member/controller/MemberController.java b/backend/src/main/java/reviewme/member/controller/MemberController.java new file mode 100644 index 000000000..662ccf88c --- /dev/null +++ b/backend/src/main/java/reviewme/member/controller/MemberController.java @@ -0,0 +1,21 @@ +package reviewme.member.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reviewme.member.service.MemberService; +import reviewme.member.service.dto.ProfileResponse; + +@RestController +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @GetMapping("/v2/members/profile") + public ResponseEntity getProfile() { + ProfileResponse response = memberService.getProfile(); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/reviewme/member/service/MemberService.java b/backend/src/main/java/reviewme/member/service/MemberService.java new file mode 100644 index 000000000..ef828b848 --- /dev/null +++ b/backend/src/main/java/reviewme/member/service/MemberService.java @@ -0,0 +1,12 @@ +package reviewme.member.service; + +import org.springframework.stereotype.Service; +import reviewme.member.service.dto.ProfileResponse; + +@Service +public class MemberService { + + public ProfileResponse getProfile() { + return null; + } +} diff --git a/backend/src/main/java/reviewme/member/service/dto/ProfileResponse.java b/backend/src/main/java/reviewme/member/service/dto/ProfileResponse.java new file mode 100644 index 000000000..5ec6900cd --- /dev/null +++ b/backend/src/main/java/reviewme/member/service/dto/ProfileResponse.java @@ -0,0 +1,7 @@ +package reviewme.member.service.dto; + +public record ProfileResponse( + String nickname, + String profileImageUrl +) { +} diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index 1b31af214..4e5903f21 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -18,7 +18,7 @@ import reviewme.review.service.dto.request.ReviewRegisterRequest; import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; +import reviewme.review.service.dto.response.list.ReceivedReviewPageResponse; import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; import reviewme.reviewgroup.controller.ReviewGroupSession; import reviewme.reviewgroup.domain.ReviewGroup; @@ -40,12 +40,12 @@ public ResponseEntity createReview(@Valid @RequestBody ReviewRegisterReque } @GetMapping("/v2/reviews") - public ResponseEntity findReceivedReviews( + public ResponseEntity findReceivedReviews( @RequestParam(required = false) Long lastReviewId, @RequestParam(required = false) Integer size, @ReviewGroupSession ReviewGroup reviewGroup ) { - ReceivedReviewsResponse response = reviewListLookupService.getReceivedReviews(lastReviewId, size, reviewGroup); + ReceivedReviewPageResponse response = reviewListLookupService.getReceivedReviews(lastReviewId, size, reviewGroup); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java index d576e9eeb..6c75ce480 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java @@ -5,8 +5,8 @@ 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.dto.response.list.ReceivedReviewPageResponse; +import reviewme.review.service.dto.response.list.ReceivedReviewPageElementResponse; import reviewme.review.service.mapper.ReviewListMapper; import reviewme.reviewgroup.domain.ReviewGroup; @@ -18,30 +18,30 @@ public class ReviewListLookupService { private final ReviewListMapper reviewListMapper; @Transactional(readOnly = true) - public ReceivedReviewsResponse getReceivedReviews(Long lastReviewId, Integer size, ReviewGroup reviewGroup) { + public ReceivedReviewPageResponse getReceivedReviews(Long lastReviewId, Integer size, ReviewGroup reviewGroup) { PageSize pageSize = new PageSize(size); - List reviewListResponse + List reviewListResponse = reviewListMapper.mapToReviewList(reviewGroup, lastReviewId, pageSize.getSize()); long newLastReviewId = calculateLastReviewId(reviewListResponse); boolean isLastPage = isLastPage(reviewListResponse, reviewGroup); - return new ReceivedReviewsResponse( + return new ReceivedReviewPageResponse( reviewGroup.getReviewee(), reviewGroup.getProjectName(), newLastReviewId, isLastPage, reviewListResponse ); } - private long calculateLastReviewId(List elements) { + private long calculateLastReviewId(List elements) { if (elements.isEmpty()) { return 0; } return elements.get(elements.size() - 1).reviewId(); } - private boolean isLastPage(List elements, ReviewGroup reviewGroup) { + private boolean isLastPage(List elements, ReviewGroup reviewGroup) { if (elements.isEmpty()) { return true; } - ReviewListElementResponse lastReviewResponse = elements.get(elements.size() - 1); + ReceivedReviewPageElementResponse 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/dto/response/list/ReviewListElementResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewPageElementResponse.java similarity index 83% rename from backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewPageElementResponse.java index 07aa32c9f..5aa7c36e5 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewPageElementResponse.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.util.List; -public record ReviewListElementResponse( +public record ReceivedReviewPageElementResponse( long reviewId, LocalDate createdAt, String contentPreview, 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/ReceivedReviewPageResponse.java similarity index 66% rename from backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewPageResponse.java index eace5cd50..35042e0d5 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/ReceivedReviewPageResponse.java @@ -2,11 +2,11 @@ import java.util.List; -public record ReceivedReviewsResponse( +public record ReceivedReviewPageResponse( String revieweeName, String projectName, long lastReviewId, boolean isLastPage, - List reviews + List reviews ) { } diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java index ab5ec4327..de46e8537 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewListMapper.java @@ -14,7 +14,7 @@ import reviewme.review.domain.TextAnswer; import reviewme.review.repository.ReviewRepository; import reviewme.review.service.dto.response.list.ReviewCategoryResponse; -import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.review.service.dto.response.list.ReceivedReviewPageElementResponse; import reviewme.reviewgroup.domain.ReviewGroup; @Component @@ -26,7 +26,7 @@ public class ReviewListMapper { private final ReviewPreviewGenerator reviewPreviewGenerator = new ReviewPreviewGenerator(); - public List mapToReviewList(ReviewGroup reviewGroup, Long lastReviewId, int size) { + public List mapToReviewList(ReviewGroup reviewGroup, Long lastReviewId, int size) { List categoryOptionItems = optionItemRepository.findAllByOptionType(OptionType.CATEGORY); return reviewRepository.findByReviewGroupIdWithLimit(reviewGroup.getId(), lastReviewId, size) .stream() @@ -34,11 +34,11 @@ public List mapToReviewList(ReviewGroup reviewGroup, .toList(); } - private ReviewListElementResponse mapToReviewListElementResponse(Review review, - List categoryOptionItems) { + private ReceivedReviewPageElementResponse mapToReviewListElementResponse(Review review, + List categoryOptionItems) { List categoryResponses = mapToCategoryOptionResponse(review, categoryOptionItems); - return new ReviewListElementResponse( + return new ReceivedReviewPageElementResponse( review.getId(), review.getCreatedDate(), reviewPreviewGenerator.generatePreview(review.getAnswersByType(TextAnswer.class)), diff --git a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java index b6c7a973c..be0935c3f 100644 --- a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java +++ b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java @@ -15,6 +15,7 @@ import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; +import reviewme.reviewgroup.service.dto.ReviewGroupPageResponse; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; @RestController @@ -24,7 +25,7 @@ public class ReviewGroupController { private final ReviewGroupService reviewGroupService; private final ReviewGroupLookupService reviewGroupLookupService; - @GetMapping("/v2/groups") + @GetMapping("/v2/groups/summary") public ResponseEntity getReviewGroupSummary(@RequestParam String reviewRequestCode) { ReviewGroupResponse response = reviewGroupLookupService.getReviewGroupSummary(reviewRequestCode); return ResponseEntity.ok(response); @@ -48,4 +49,11 @@ public ResponseEntity checkGroupAccessCode( session.setAttribute("reviewRequestCode", request.reviewRequestCode()); return ResponseEntity.noContent().build(); } + + @GetMapping("/v2/groups") + public ResponseEntity getMyReviewGroups() { + // TODO: 세션을 활용한 권한 체계에 따른 추가 조치 필요 + ReviewGroupPageResponse response = reviewGroupLookupService.getMyReviewGroups(); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java index 0567f6344..44c8402a2 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import reviewme.reviewgroup.service.dto.ReviewGroupPageResponse; import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; @@ -21,4 +22,9 @@ public ReviewGroupResponse getReviewGroupSummary(String reviewRequestCode) { return new ReviewGroupResponse(reviewGroup.getReviewee(), reviewGroup.getProjectName()); } + + public ReviewGroupPageResponse getMyReviewGroups() { + // TODO: 생성일자 최신순 정렬 + return null; + } } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageElementResponse.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageElementResponse.java new file mode 100644 index 000000000..f5a7aab78 --- /dev/null +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageElementResponse.java @@ -0,0 +1,12 @@ +package reviewme.reviewgroup.service.dto; + +import java.time.LocalDate; + +public record ReviewGroupPageElementResponse( + String revieweeName, + String projectName, + LocalDate createdAt, + String reviewRequestCode, + int reviewCount +) { +} diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageResponse.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageResponse.java new file mode 100644 index 000000000..ef6c250e9 --- /dev/null +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupPageResponse.java @@ -0,0 +1,10 @@ +package reviewme.reviewgroup.service.dto; + +import java.util.List; + +public record ReviewGroupPageResponse( + long lastReviewGroupId, + boolean isLastPage, + List reviewGroups +) { +} diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index 20d57db83..a656602bb 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -26,8 +26,12 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import reviewme.auth.controller.AuthController; +import reviewme.auth.service.AuthService; import reviewme.highlight.controller.HighlightController; import reviewme.highlight.service.HighlightService; +import reviewme.member.controller.MemberController; +import reviewme.member.service.MemberService; import reviewme.review.controller.ReviewController; import reviewme.review.service.ReviewDetailLookupService; import reviewme.review.service.ReviewGatheredLookupService; @@ -48,7 +52,9 @@ ReviewController.class, TemplateController.class, SectionController.class, - HighlightController.class + HighlightController.class, + MemberController.class, + AuthController.class }) @ExtendWith(RestDocumentationExtension.class) public abstract class ApiTest { @@ -85,6 +91,12 @@ public abstract class ApiTest { @MockBean protected HighlightService highlightService; + @MockBean + protected MemberService memberService; + + @MockBean + protected AuthService authService; + @MockBean private ReviewGroupSessionResolver reviewGroupSessionResolver; diff --git a/backend/src/test/java/reviewme/api/AuthApiTest.java b/backend/src/test/java/reviewme/api/AuthApiTest.java new file mode 100644 index 000000000..3bf6c61c9 --- /dev/null +++ b/backend/src/test/java/reviewme/api/AuthApiTest.java @@ -0,0 +1,59 @@ +package reviewme.api; + +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 org.junit.jupiter.api.Test; +import org.springframework.restdocs.cookies.CookieDescriptor; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; + +public class AuthApiTest extends ApiTest { + + @Test + void 깃허브로_인증한다() { + String request = """ + { + "code": "github_auth_code" + } + """; + + FieldDescriptor[] requestFieldDescriptors = { + fieldWithPath("code").description("깃허브 임시 인증 코드"), + }; + + RestDocumentationResultHandler handler = document( + "github-auth", + requestFields(requestFieldDescriptors) + ); + + givenWithSpec().log().all() + .body(request) + .when().post("/v2/auth/github") + .then().log().all() + .apply(handler) + .statusCode(200); + } + + @Test + void 로그아웃한다() { + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + RestDocumentationResultHandler handler = document( + "logout", + requestCookies(cookieDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "SESSION12345678") + .when().post("/v2/auth/logout") + .then().log().all() + .apply(handler) + .statusCode(204); + } +} diff --git a/backend/src/test/java/reviewme/api/MemberApiTest.java b/backend/src/test/java/reviewme/api/MemberApiTest.java new file mode 100644 index 000000000..d8b4d4191 --- /dev/null +++ b/backend/src/test/java/reviewme/api/MemberApiTest.java @@ -0,0 +1,45 @@ +package reviewme.api; + +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.responseFields; + +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 reviewme.member.service.dto.ProfileResponse; + +public class MemberApiTest extends ApiTest { + + @Test + void 내_프로필을_불러온다() { + BDDMockito.given(memberService.getProfile()) + .willReturn(new ProfileResponse("donghoony", "https://aru.image")); + + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("nickname").description("닉네임"), + fieldWithPath("profileImageUrl").description("프로필 이미지 URL") + }; + + RestDocumentationResultHandler handler = document( + "my-profile", + requestCookies(cookieDescriptors), + responseFields(responseFieldDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "SESSION12345678") + .when().get("/v2/members/profile") + .then().log().all() + .apply(handler) + .statusCode(200); + } +} diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index e05935d43..eeea31f7c 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -30,10 +30,10 @@ import reviewme.review.service.dto.response.gathered.SimpleQuestionResponse; import reviewme.review.service.dto.response.gathered.TextResponse; import reviewme.review.service.dto.response.gathered.VoteResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; +import reviewme.review.service.dto.response.list.ReceivedReviewPageResponse; import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; import reviewme.review.service.dto.response.list.ReviewCategoryResponse; -import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.review.service.dto.response.list.ReceivedReviewPageElementResponse; import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; class ReviewApiTest extends ApiTest { @@ -167,13 +167,13 @@ class ReviewApiTest extends ApiTest { @Test void 자신이_받은_리뷰_목록을_조회한다() { - List receivedReviews = List.of( - new ReviewListElementResponse(1L, LocalDate.of(2024, 8, 1), "(리뷰 미리보기 1)", + List receivedReviews = List.of( + new ReceivedReviewPageElementResponse(1L, LocalDate.of(2024, 8, 1), "(리뷰 미리보기 1)", List.of(new ReviewCategoryResponse(1L, "카테고리 1"))), - new ReviewListElementResponse(2L, LocalDate.of(2024, 8, 2), "(리뷰 미리보기 2)", + new ReceivedReviewPageElementResponse(2L, LocalDate.of(2024, 8, 2), "(리뷰 미리보기 2)", List.of(new ReviewCategoryResponse(2L, "카테고리 2"))) ); - ReceivedReviewsResponse response = new ReceivedReviewsResponse( + ReceivedReviewPageResponse response = new ReceivedReviewPageResponse( "아루3", "리뷰미", 1L, true, receivedReviews); BDDMockito.given(reviewListLookupService.getReceivedReviews(anyLong(), anyInt(), any())) .willReturn(response); diff --git a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java index e87cb8b5c..fe3c8e5ff 100644 --- a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java @@ -4,6 +4,7 @@ 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.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -12,6 +13,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; 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.cookies.CookieDescriptor; @@ -20,6 +23,8 @@ import org.springframework.restdocs.request.ParameterDescriptor; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; +import reviewme.reviewgroup.service.dto.ReviewGroupPageElementResponse; +import reviewme.reviewgroup.service.dto.ReviewGroupPageResponse; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; class ReviewGroupApiTest extends ApiTest { @@ -83,7 +88,7 @@ class ReviewGroupApiTest extends ApiTest { givenWithSpec().log().all() .queryParam("reviewRequestCode", "ABCD1234") - .when().get("/v2/groups") + .when().get("/v2/groups/summary") .then().log().all() .apply(handler) .statusCode(200); @@ -121,4 +126,44 @@ class ReviewGroupApiTest extends ApiTest { .cookie("JSESSIONID") .statusCode(204); } + + @Test + void 회원이_생성한_프로젝트_목록을_반환한다() { + ReviewGroupPageResponse response = new ReviewGroupPageResponse(2L, true, + List.of( + new ReviewGroupPageElementResponse("이동훈", "우테코", LocalDate.of(2024, 1, 30), "WOOTECO1", 1), + new ReviewGroupPageElementResponse("아루", "리뷰미", LocalDate.of(2024, 1, 5), "ABCD1234", 2) + ) + ); + BDDMockito.given(reviewGroupLookupService.getMyReviewGroups()) + .willReturn(response); + + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("lastReviewGroupId").description("해당 페이지의 마지막 리뷰 그룹 ID"), + fieldWithPath("isLastPage").description("마지막 페이지 여부"), + fieldWithPath("reviewGroups[]").description("리뷰 그룹 목록 (생성일 기준 내림차순 정렬)"), + fieldWithPath("reviewGroups[].revieweeName").description("리뷰이 이름"), + fieldWithPath("reviewGroups[].projectName").description("프로젝트 이름"), + fieldWithPath("reviewGroups[].createdAt").description("생성일"), + fieldWithPath("reviewGroups[].reviewRequestCode").description("리뷰 요청 코드"), + fieldWithPath("reviewGroups[].reviewCount").description("작성된 리뷰 수") + }; + + RestDocumentationResultHandler handler = document( + "review-group-list", + responseFields(responseFieldDescriptors), + requestCookies(cookieDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "ABCDEFGHI1234") + .when().get("/v2/groups") + .then().log().all() + .apply(handler) + .statusCode(200); + } } diff --git a/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java index ebc559924..06c311a2a 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java @@ -22,7 +22,7 @@ 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.dto.response.list.ReceivedReviewPageResponse; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; @@ -82,7 +82,7 @@ class ReviewListLookupServiceTest { reviewRepository.saveAll(List.of(review1, review2)); // when - ReceivedReviewsResponse response = reviewListLookupService.getReceivedReviews( + ReceivedReviewPageResponse response = reviewListLookupService.getReceivedReviews( Long.MAX_VALUE, 5, reviewGroup ); @@ -116,7 +116,7 @@ class ReviewListLookupServiceTest { reviewRepository.saveAll(List.of(review1, review2, review3)); // when - ReceivedReviewsResponse response + ReceivedReviewPageResponse response = reviewListLookupService.getReceivedReviews(Long.MAX_VALUE, 2, reviewGroup); // then diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java index 0032b7cb2..eb5ace5c0 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewListMapperTest.java @@ -15,7 +15,7 @@ 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.review.service.dto.response.list.ReceivedReviewPageElementResponse; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; @@ -76,13 +76,13 @@ class ReviewListMapperTest { int size = 5; // when - List responses = reviewListMapper.mapToReviewList( + List responses = reviewListMapper.mapToReviewList( reviewGroup, lastReviewId, size); // then assertAll( () -> assertThat(responses).hasSize(size), - () -> assertThat(responses).extracting(ReviewListElementResponse::reviewId) + () -> assertThat(responses).extracting(ReceivedReviewPageElementResponse::reviewId) .containsExactly( review7.getId(), review6.getId(), review5.getId(), review4.getId(), review3.getId()) ); From 29b65c9128aa29f3f0b22e0f7381c300aa9c1448 Mon Sep 17 00:00:00 2001 From: Kimprodp Date: Tue, 14 Jan 2025 10:28:00 +0900 Subject: [PATCH 46/60] =?UTF-8?q?[BE]=20docs:=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=20=EB=A6=AC=EB=B7=B0=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=ED=9A=8C=EC=9B=90=EC=9A=A9=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EA=B7=B8=EB=A3=B9=20=EC=83=9D=EC=84=B1,=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EA=B7=B8=EB=A3=B9=20=EC=A0=95=EB=B3=B4,=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=93=B1=EB=A1=9D=20API=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.=20(#?= =?UTF-8?q?1017)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내가 쓴 리뷰 목록 DTO, 컨트롤러 구현 * docs: 내가 쓴 리뷰 목록 api 테스트 문서 작성 * refactor: 기존 리뷰 그룹 생성을 비회원 리뷰 그룹 생성으로 변경 * feat: 회원용 그룹 생성 DTO, 컨트롤러 구현 * docs: 회원용 그룹 생성 api 테스트 문서 작성 * refactor: 리뷰 그룹 정보 조회 api를 회원, 비회원용으로 수정 * refactor: 리뷰 생성 api를 생성자가 회원, 비회원인 경우로 수정 * fix: 생성일자 최신순 정렬 반영 * test: api 테스트에 쿠키 추가 * refactor: 내가 쓴 리뷰 목록 응답에서 member id 제거 * refactor: 회원 리뷰 작성을 비회원과 통합 * refactor: 리뷰 그룹 생성 api를 회원, 비회원 통합 * refactor: 받은/작성한 리뷰 목록 uri 구체화 * refactor: 내가 만든 그룹 uri 수정 * refactor: 내가 작성한 리뷰에 대해 written -> authored로 표현 수정 --------- Co-authored-by: hyeonjilee --- backend/src/docs/asciidoc/create-review.adoc | 8 +- backend/src/docs/asciidoc/review-list.adoc | 4 + backend/src/docs/asciidoc/reviewgroup.adoc | 16 ++- .../review/controller/ReviewController.java | 15 ++- .../service/ReviewListLookupService.java | 6 ++ .../list/AuthoredReviewElementResponse.java | 14 +++ .../list/AuthoredReviewsResponse.java | 10 ++ .../controller/ReviewGroupController.java | 1 + .../service/ReviewGroupLookupService.java | 2 +- .../service/ReviewGroupService.java | 5 +- .../dto/MemberReviewGroupCreationRequest.java | 13 +++ .../dto/ReviewGroupCreationRequest.java | 4 +- .../service/dto/ReviewGroupResponse.java | 3 + .../test/java/reviewme/api/ReviewApiTest.java | 98 ++++++++++++++++++- .../java/reviewme/api/ReviewGroupApiTest.java | 81 ++++++++++++++- 15 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewElementResponse.java create mode 100644 backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewsResponse.java create mode 100644 backend/src/main/java/reviewme/reviewgroup/service/dto/MemberReviewGroupCreationRequest.java diff --git a/backend/src/docs/asciidoc/create-review.adoc b/backend/src/docs/asciidoc/create-review.adoc index 7b3464613..e05ef1b90 100644 --- a/backend/src/docs/asciidoc/create-review.adoc +++ b/backend/src/docs/asciidoc/create-review.adoc @@ -1,6 +1,10 @@ -==== 리뷰 생성 +==== 비회원이 리뷰 생성 -operation::create-review[snippets="curl-request,request-fields,http-response"] +operation::create-review-by-guest[snippets="curl-request,request-fields,http-response"] + +==== 회원이 리뷰 생성 + +operation::create-review-by-member[snippets="curl-request,request-fields,http-response"] ==== 그룹 코드가 올바르지 않은 경우 diff --git a/backend/src/docs/asciidoc/review-list.adoc b/backend/src/docs/asciidoc/review-list.adoc index 3d8648566..d0cf479ba 100644 --- a/backend/src/docs/asciidoc/review-list.adoc +++ b/backend/src/docs/asciidoc/review-list.adoc @@ -1,3 +1,7 @@ ==== 자신이 받은 리뷰 목록 조회 operation::received-review-list-with-pagination[snippets="curl-request,request-cookies,query-parameters,http-response,response-fields"] + +==== 자신이 작성한 리뷰 목록 조회 + +operation::authored-review-list-with-pagination[snippets="curl-request,query-parameters,http-response,response-fields"] diff --git a/backend/src/docs/asciidoc/reviewgroup.adoc b/backend/src/docs/asciidoc/reviewgroup.adoc index 79d70a9f5..3b47c4228 100644 --- a/backend/src/docs/asciidoc/reviewgroup.adoc +++ b/backend/src/docs/asciidoc/reviewgroup.adoc @@ -1,10 +1,18 @@ -==== 리뷰 그룹 생성 +==== 비회원용 리뷰 그룹 생성 -operation::review-group-create[snippets="curl-request,request-fields,http-response,response-fields"] +operation::guest-review-group-create[snippets="curl-request,request-fields,http-response,response-fields"] -==== 리뷰 그룹 간단 정보 조회 +==== 회원용 리뷰 그룹 생성 -operation::review-group-summary[snippets="curl-request,http-response,response-fields"] +operation::member-review-group-create[snippets="curl-request,request-fields,http-response,response-fields"] + +==== 회원이 만든 리뷰 그룹 간단 정보 조회 + +operation::member-review-group-summary[snippets="curl-request,http-response,response-fields"] + +==== 비회원이 만든 리뷰 그룹 간단 정보 조회 + +operation::guest-review-group-summary[snippets="curl-request,http-response,response-fields"] ==== 리뷰 요청 코드, 확인 코드 일치 여부 diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index 4e5903f21..0689e6fa0 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -20,6 +20,7 @@ import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; import reviewme.review.service.dto.response.list.ReceivedReviewPageResponse; import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; +import reviewme.review.service.dto.response.list.AuthoredReviewsResponse; import reviewme.reviewgroup.controller.ReviewGroupSession; import reviewme.reviewgroup.domain.ReviewGroup; @@ -35,11 +36,12 @@ public class ReviewController { @PostMapping("/v2/reviews") public ResponseEntity createReview(@Valid @RequestBody ReviewRegisterRequest request) { + // 회원 세션 추후 추가해야 함 long savedReviewId = reviewRegisterService.registerReview(request); return ResponseEntity.created(URI.create("/reviews/" + savedReviewId)).build(); } - @GetMapping("/v2/reviews") + @GetMapping("/v2/reviews/received") public ResponseEntity findReceivedReviews( @RequestParam(required = false) Long lastReviewId, @RequestParam(required = false) Integer size, @@ -75,4 +77,15 @@ public ResponseEntity getReceivedReviewsBySect reviewGatheredLookupService.getReceivedReviewsBySectionId(reviewGroup, sectionId); return ResponseEntity.ok(response); } + + @GetMapping("/v2/reviews/authored") + public ResponseEntity findAuthoredReviews( + @RequestParam(required = false) Long lastReviewId, + @RequestParam(required = false) Integer size +// @MemberSession Member member + // TODO: 세션을 활용한 권한 체계에 따른 추가 조치 필요 + ) { + AuthoredReviewsResponse response = reviewListLookupService.getAuthoredReviews(lastReviewId, size); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java index 6c75ce480..933e5a219 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.list.AuthoredReviewsResponse; import reviewme.review.service.dto.response.list.ReceivedReviewPageResponse; import reviewme.review.service.dto.response.list.ReceivedReviewPageElementResponse; import reviewme.review.service.mapper.ReviewListMapper; @@ -29,6 +30,11 @@ public ReceivedReviewPageResponse getReceivedReviews(Long lastReviewId, Integer ); } + public AuthoredReviewsResponse getAuthoredReviews(Long lastReviewId, Integer size) { + // TODO: 생성일자 최신순 정렬 + return null; + } + private long calculateLastReviewId(List elements) { if (elements.isEmpty()) { return 0; diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewElementResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewElementResponse.java new file mode 100644 index 000000000..282f6dbd3 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewElementResponse.java @@ -0,0 +1,14 @@ +package reviewme.review.service.dto.response.list; + +import java.time.LocalDate; +import java.util.List; + +public record AuthoredReviewElementResponse( + long reviewId, + String revieweeName, + String projectName, + LocalDate createdAt, + String contentPreview, + List categories +) { +} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewsResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewsResponse.java new file mode 100644 index 000000000..7d712a31c --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/AuthoredReviewsResponse.java @@ -0,0 +1,10 @@ +package reviewme.review.service.dto.response.list; + +import java.util.List; + +public record AuthoredReviewsResponse( + List reviews, + long lastReviewId, + boolean isLastPage +) { +} diff --git a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java index be0935c3f..fa82ba70d 100644 --- a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java +++ b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java @@ -35,6 +35,7 @@ public ResponseEntity getReviewGroupSummary(@RequestParam S public ResponseEntity createReviewGroup( @Valid @RequestBody ReviewGroupCreationRequest request ) { + // 회원 세션 추후 추가해야 함 ReviewGroupCreationResponse response = reviewGroupService.createReviewGroup(request); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java index 44c8402a2..479b59116 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java @@ -20,7 +20,7 @@ public ReviewGroupResponse getReviewGroupSummary(String reviewRequestCode) { ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - return new ReviewGroupResponse(reviewGroup.getReviewee(), reviewGroup.getProjectName()); + return new ReviewGroupResponse(null, reviewGroup.getReviewee(), reviewGroup.getProjectName()); } public ReviewGroupPageResponse getMyReviewGroups() { diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java index 86303197c..fb09f6cb0 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupService.java @@ -3,13 +3,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.reviewgroup.service.exception.ReviewGroupUnauthorizedException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; +import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.reviewgroup.service.exception.ReviewGroupUnauthorizedException; import reviewme.template.domain.Template; import reviewme.template.repository.TemplateRepository; import reviewme.template.service.exception.TemplateNotFoundException; @@ -27,6 +27,7 @@ public class ReviewGroupService { @Transactional public ReviewGroupCreationResponse createReviewGroup(ReviewGroupCreationRequest request) { + // 회원, 비회원 분기 처리 필요 String reviewRequestCode; do { reviewRequestCode = randomCodeGenerator.generate(REVIEW_REQUEST_CODE_LENGTH); diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/MemberReviewGroupCreationRequest.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/MemberReviewGroupCreationRequest.java new file mode 100644 index 000000000..c76c3c19c --- /dev/null +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/MemberReviewGroupCreationRequest.java @@ -0,0 +1,13 @@ +package reviewme.reviewgroup.service.dto; + +import jakarta.validation.constraints.NotEmpty; + +public record MemberReviewGroupCreationRequest( + + @NotEmpty(message = "리뷰이 이름을 입력해주세요.") + String revieweeName, + + @NotEmpty(message = "프로젝트 이름을 입력해주세요.") + String projectName +) { +} 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 c31a70f04..d8c1a0a0b 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupCreationRequest.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupCreationRequest.java @@ -1,6 +1,6 @@ package reviewme.reviewgroup.service.dto; -import jakarta.validation.constraints.NotBlank; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotEmpty; public record ReviewGroupCreationRequest( @@ -11,7 +11,7 @@ public record ReviewGroupCreationRequest( @NotEmpty(message = "프로젝트 이름을 입력해주세요.") String projectName, - @NotBlank(message = "비밀번호를 입력해주세요.") + @Nullable String groupAccessCode ) { } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupResponse.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupResponse.java index ea6f12a29..4d1accfbf 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupResponse.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupResponse.java @@ -1,7 +1,10 @@ package reviewme.reviewgroup.service.dto; +import jakarta.annotation.Nullable; + public record ReviewGroupResponse( + @Nullable Long revieweeId, String revieweeName, String projectName ) { diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index eeea31f7c..d6d5fd293 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -21,7 +21,6 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; -import reviewme.template.domain.QuestionType; import reviewme.review.service.dto.request.ReviewRegisterRequest; import reviewme.review.service.dto.response.gathered.HighlightResponse; import reviewme.review.service.dto.response.gathered.RangeResponse; @@ -34,7 +33,10 @@ import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; import reviewme.review.service.dto.response.list.ReviewCategoryResponse; import reviewme.review.service.dto.response.list.ReceivedReviewPageElementResponse; +import reviewme.review.service.dto.response.list.AuthoredReviewElementResponse; +import reviewme.review.service.dto.response.list.AuthoredReviewsResponse; import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.template.domain.QuestionType; class ReviewApiTest extends ApiTest { @@ -55,10 +57,41 @@ class ReviewApiTest extends ApiTest { """; @Test - void 리뷰를_등록한다() { + void 비회원이_리뷰를_등록한다() { + BDDMockito.given(reviewRegisterService.registerReview(any(ReviewRegisterRequest.class))) + .willReturn(1L); + + FieldDescriptor[] requestFieldDescriptors = { + fieldWithPath("reviewRequestCode").description("리뷰 요청 코드"), + + fieldWithPath("answers[]").description("답변 목록"), + fieldWithPath("answers[].questionId").description("질문 ID"), + fieldWithPath("answers[].selectedOptionIds").description("선택한 옵션 ID 목록").optional(), + fieldWithPath("answers[].text").description("서술 답변").optional() + }; + + RestDocumentationResultHandler handler = document( + "create-review-by-guest", + requestFields(requestFieldDescriptors) + ); + + givenWithSpec().log().all() + .body(request) + .when().post("/v2/reviews") + .then().log().all() + .apply(handler) + .statusCode(201); + } + + @Test + void 회원이_리뷰를_등록한다() { BDDMockito.given(reviewRegisterService.registerReview(any(ReviewRegisterRequest.class))) .willReturn(1L); + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + FieldDescriptor[] requestFieldDescriptors = { fieldWithPath("reviewRequestCode").description("리뷰 요청 코드"), @@ -69,11 +102,13 @@ class ReviewApiTest extends ApiTest { }; RestDocumentationResultHandler handler = document( - "create-review", + "create-review-by-member", + requestCookies(cookieDescriptors), requestFields(requestFieldDescriptors) ); givenWithSpec().log().all() + .cookie("JSESSIONID", "ASVNE1VAKDNV4") .body(request) .when().post("/v2/reviews") .then().log().all() @@ -216,7 +251,7 @@ class ReviewApiTest extends ApiTest { .queryParam("reviewRequestCode", "hello!!") .queryParam("lastReviewId", "2") .queryParam("size", "5") - .when().get("/v2/reviews") + .when().get("/v2/reviews/received") .then().log().all() .apply(handler) .statusCode(200); @@ -314,4 +349,59 @@ class ReviewApiTest extends ApiTest { .apply(handler) .statusCode(200); } + + @Test + void 자신이_작성한_리뷰_목록을_조회한다() { + List authoredReviews = List.of( + new AuthoredReviewElementResponse(1L, "테드1", "리뷰미", LocalDate.of(2024, 8, 2), "(리뷰 미리보기 1)", + List.of(new ReviewCategoryResponse(1L, "카테고리 1"))), + new AuthoredReviewElementResponse(2L, "테드2", "리뷰미", LocalDate.of(2024, 8, 1), "(리뷰 미리보기 2)", + List.of(new ReviewCategoryResponse(2L, "카테고리 2"))) + ); + AuthoredReviewsResponse response = new AuthoredReviewsResponse(authoredReviews, 1L, true); + BDDMockito.given(reviewListLookupService.getAuthoredReviews(anyLong(), anyInt())) + .willReturn(response); + + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + ParameterDescriptor[] queryParameter = { + parameterWithName("lastReviewId").description("페이지의 마지막 리뷰 ID - 기본으로 최신순 첫번째 페이지 응답"), + parameterWithName("size").description("페이지의 크기 - 기본으로 10개씩 응답") + }; + + FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("lastReviewId").description("페이지의 마지막 리뷰 ID"), + fieldWithPath("isLastPage").description("마지막 페이지 여부"), + + fieldWithPath("reviews[]").description("리뷰 목록 (생성일 기준 내림차순 정렬)"), + fieldWithPath("reviews[].reviewId").description("리뷰 ID"), + fieldWithPath("reviews[].createdAt").description("리뷰 작성 날짜"), + fieldWithPath("reviews[].contentPreview").description("리뷰 미리보기"), + fieldWithPath("reviews[].revieweeName").description("리뷰이 이름"), + fieldWithPath("reviews[].projectName").description("프로젝트명"), + + fieldWithPath("reviews[].categories[]").description("카테고리 목록"), + fieldWithPath("reviews[].categories[].optionId").description("카테고리 ID"), + fieldWithPath("reviews[].categories[].content").description("카테고리 내용") + }; + + RestDocumentationResultHandler handler = document( + "authored-review-list-with-pagination", + requestCookies(cookieDescriptors), + queryParameters(queryParameter), + responseFields(responseFieldDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "ASVNE1VAKDNV4") +// .queryParam("reviewRequestCode", "hello!!") + .queryParam("lastReviewId", "2") + .queryParam("size", "5") + .when().get("/v2/reviews/authored") + .then().log().all() + .apply(handler) + .statusCode(200); + } } diff --git a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java index fe3c8e5ff..7de76c4b0 100644 --- a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java @@ -30,7 +30,7 @@ class ReviewGroupApiTest extends ApiTest { @Test - void 리뷰_그룹을_생성한다() { + void 비회원용_리뷰_그룹을_생성한다() { BDDMockito.given(reviewGroupService.createReviewGroup(any(ReviewGroupCreationRequest.class))) .willReturn(new ReviewGroupCreationResponse("ABCD1234")); @@ -53,7 +53,7 @@ class ReviewGroupApiTest extends ApiTest { }; RestDocumentationResultHandler handler = document( - "review-group-create", + "guest-review-group-create", requestFields(requestFieldDescriptors), responseFields(responseFieldDescriptors) ); @@ -67,21 +67,92 @@ class ReviewGroupApiTest extends ApiTest { } @Test - void 리뷰_요청_코드로_리뷰_그룹_정보를_반환한다() { + void 회원용_리뷰_그룹을_생성한다() { + BDDMockito.given(reviewGroupService.createReviewGroup(any(ReviewGroupCreationRequest.class))) + .willReturn(new ReviewGroupCreationResponse("ABCD1234")); + + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + String request = """ + { + "revieweeName": "아루", + "projectName": "리뷰미" + } + """; + + FieldDescriptor[] requestFieldDescriptors = { + fieldWithPath("revieweeName").description("리뷰이 이름"), + fieldWithPath("projectName").description("프로젝트 이름") + }; + + FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("reviewRequestCode").description("리뷰 요청 코드") + }; + + RestDocumentationResultHandler handler = document( + "member-review-group-create", + requestCookies(cookieDescriptors), + requestFields(requestFieldDescriptors), + responseFields(responseFieldDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "ASVNE1VAKDNV4") + .body(request) + .when().post("/v2/groups") + .then().log().all() + .apply(handler) + .statusCode(200); + } + + @Test + void 리뷰_요청_코드로_회원이_만든_리뷰_그룹_정보를_반환한다() { + BDDMockito.given(reviewGroupLookupService.getReviewGroupSummary(anyString())) + .willReturn(new ReviewGroupResponse(1L,"아루", "리뷰미")); + + ParameterDescriptor[] parameterDescriptors = { + parameterWithName("reviewRequestCode").description("리뷰 요청 코드") + }; + + FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("revieweeId").description("리뷰이 ID"), + fieldWithPath("revieweeName").description("리뷰이 이름"), + fieldWithPath("projectName").description("프로젝트 이름") + }; + + RestDocumentationResultHandler handler = document( + "member-review-group-summary", + queryParameters(parameterDescriptors), + responseFields(responseFieldDescriptors) + ); + + givenWithSpec().log().all() + .queryParam("reviewRequestCode", "ABCD1234") + .when().get("/v2/groups/summary") + .then().log().all() + .apply(handler) + .statusCode(200); + } + + @Test + void 리뷰_요청_코드로_비회원이_만든_리뷰_그룹_정보를_반환한다() { BDDMockito.given(reviewGroupLookupService.getReviewGroupSummary(anyString())) - .willReturn(new ReviewGroupResponse("아루", "리뷰미")); + .willReturn(new ReviewGroupResponse(null, "아루", "리뷰미")); ParameterDescriptor[] parameterDescriptors = { parameterWithName("reviewRequestCode").description("리뷰 요청 코드") }; FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("revieweeId").description("리뷰이 ID"), fieldWithPath("revieweeName").description("리뷰이 이름"), fieldWithPath("projectName").description("프로젝트 이름") }; RestDocumentationResultHandler handler = document( - "review-group-summary", + "guest-review-group-summary", queryParameters(parameterDescriptors), responseFields(responseFieldDescriptors) ); From ca790e4eff4380bf98275674dd16b837550d22f9 Mon Sep 17 00:00:00 2001 From: badahertz52 Date: Wed, 15 Jan 2025 21:28:53 +0900 Subject: [PATCH 47/60] =?UTF-8?q?ci=20:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80,=20=ED=94=84=EB=A6=AC=ED=94=8C?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#1052)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sentry, amplitude 환경변수 추가 - 환경변수 확인하는 프리플라이트 추가 --- .github/workflows/frontend-ci.yml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index 689733362..a64b9a52f 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -10,9 +10,9 @@ permissions: contents: read pages: write id-token: write -concurrency: +concurrency: group: "ci-group" - cancel-in-progress: false # NOTE: 기존 CI가 돌고 있는 상황에서 새 작업이 추가돼도 기존 작업 계속 수행 + cancel-in-progress: false # 기존 작업 계속 수행 jobs: build: runs-on: ubuntu-latest @@ -28,11 +28,31 @@ jobs: cache-dependency-path: ./frontend/yarn.lock - name: Create .env file - run: echo "API_BASE_URL=${{ secrets.API_BASE_URL }}" > ./frontend/.env + run: | + echo "API_BASE_URL=${{ secrets.API_BASE_URL }}" > ./frontend/.env + echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> ./frontend/.env + echo "AMPLITUDE_KEY=${{ secrets.AMPLITUDE_KEY }}" >> ./frontend/.env - name: Set environment file permissions run: chmod 644 ./frontend/.env + # 프리플라이트 체크 + - name: Preflight Check for Environment Variables + run: | + if [ -z "${{ secrets.API_BASE_URL }}" ]; then + echo "Error: API_BASE_URL is not set" + exit 1 + fi + if [ -z "${{ secrets.SENTRY_AUTH_TOKEN }}" ]; then + echo "Error: SENTRY_AUTH_TOKEN is not set" + exit 1 + fi + if [ -z "${{ secrets.AMPLITUDE_KEY }}" ]; then + echo "Error: AMPLITUDE_KEY is not set" + exit 1 + fi + shell: bash + - name: Install dependencies run: yarn install --frozen-lockfile working-directory: frontend @@ -45,4 +65,6 @@ jobs: run: yarn build env: API_BASE_URL: ${{ secrets.API_BASE_URL }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + AMPLITUDE_KEY: ${{ secrets.AMPLITUDE_KEY }} working-directory: frontend From 9e09b048b14377debbee7363ae16bda703c30c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yejin=20Lee=28=EC=9D=B4=EC=98=88=EC=A7=84=29?= <111052302+ImxYJL@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:20:29 +0900 Subject: [PATCH 48/60] =?UTF-8?q?[FE]=20=EC=9E=91=EC=84=B1=ED=95=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=EB=A5=BC=20=ED=99=95=EC=9D=B8=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20(#1038)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 현재 미디어 쿼리 상태와 디바이스 종류(boolean)를 리턴하는 훅 * feat: 작성한 리뷰 페이지의 분할 레이아웃을 담당하는 WrittenReviewItem 레이아웃 컴포넌트 * feat: 임시 WrittenReviewList 컴포넌트 * feat: 임시 DetailedWrittenReview 컴포넌트 * feat: 임시 작성한 리뷰 확인 페이지 * feat: 작성한 리뷰 페이지에 대한 임시 라우팅 * feat: 임시 레이아웃, 반응형 적용 * feat: 선택한 리뷰가 없을 때의 컴포넌트 추가 * refactor: 페이지 레이아웃 이름을 더 직관적이고 단순하게 수정 * chore: WrittenReviewPage의 layout 폴더 위치를 component 하위로 변경 * refactor: 작성한 리뷰 확인 페이지의 이름을 WrittenReviewPage로 간략하게 변경 * refactor: 반응형 레이아웃을 위해 queryString 도입 (+변경된 페이지명에 따른 추가 변경사항) * chore: amplitude 페이지 정보에 작성한 리뷰 확인 페이지 추가 * refactor: 작성한 리뷰 확인 페이지에 early return 스타일 적용 * refactor: useSearchParamAndQuery의 매개변수 paramKey를 optional로 변경 * refactor: 미디어 쿼리 관련 훅 리팩토링 - mediaType 대신 breakpoint로 명시 1. 훅 이름 변경 2. 변수명 변경 * chore: 간단한 변수명 수정 * chore: Breakpoints 타입 분리 * chore: 경로 수정 * refactor: resize 함수에 debounce 추가 * refactor: xSmall 사이즈를 430으로 변경(일단 아이폰 프로 맥스를 위해 430으로 맞춤) * chore: 에러 바운더리에 적용된 속성 이름 변경 * fix: useDeviceBreakpoints훅에서 large 크기일 때 undefined가 나오던 문제 * refactor: ReviewList와 ReviewListItem 스타일 조정 - large 사이즈에서만 2분할되도록 변경 * refactor: 작성한 리뷰 확인 페이지 반응형 조절 - 2분할은 large 사이즈에서만 이루어지도록 변경 * fix: WrittenReviewList에 고정 width값(Detailed의 Outline과 동일) 부여 --- frontend/src/assets/slideArrows.svg | 4 ++ .../src/components/ReviewListItem/index.tsx | 13 +++++ .../src/components/ReviewListItem/styles.ts | 31 +++++++++++ frontend/src/constants/amplitudeEventName.ts | 1 + frontend/src/constants/route.ts | 1 + frontend/src/hooks/useSearchParamAndQuery.ts | 4 +- .../DetailedWrittenReview/index.tsx | 23 ++++++++ .../DetailedWrittenReview/styles.ts | 47 ++++++++++++++++ .../NoSelectedReviewGuide/index.tsx | 14 +++++ .../NoSelectedReviewGuide/styles.ts | 31 +++++++++++ .../components/WrittenReviewList/index.tsx | 29 ++++++++++ .../components/WrittenReviewList/styles.ts | 32 +++++++++++ .../WrittenReviewPage/components/index.tsx | 3 ++ .../layouts/PageContentLayout/index.tsx | 18 +++++++ .../layouts/PageContentLayout/styles.ts | 23 ++++++++ .../components/layouts/index.tsx | 1 + .../pages/WrittenReviewPage/hooks/index.ts | 1 + .../hooks/useDeviceBreakpoints/index.ts | 53 +++++++++++++++++++ .../src/pages/WrittenReviewPage/index.tsx | 51 ++++++++++++++++++ .../src/pages/WrittenReviewPage/styles.ts | 17 ++++++ frontend/src/pages/index.tsx | 1 + frontend/src/router.tsx | 2 + frontend/src/styles/theme.ts | 11 +++- frontend/src/types/emotion.ts | 3 ++ frontend/src/types/media.ts | 3 ++ frontend/src/utils/media.ts | 6 +-- 26 files changed, 417 insertions(+), 6 deletions(-) create mode 100644 frontend/src/assets/slideArrows.svg create mode 100644 frontend/src/components/ReviewListItem/index.tsx create mode 100644 frontend/src/components/ReviewListItem/styles.ts create mode 100644 frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/index.tsx create mode 100644 frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/styles.ts create mode 100644 frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/index.tsx create mode 100644 frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/styles.ts create mode 100644 frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/index.tsx create mode 100644 frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/styles.ts create mode 100644 frontend/src/pages/WrittenReviewPage/components/index.tsx create mode 100644 frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/index.tsx create mode 100644 frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/styles.ts create mode 100644 frontend/src/pages/WrittenReviewPage/components/layouts/index.tsx create mode 100644 frontend/src/pages/WrittenReviewPage/hooks/index.ts create mode 100644 frontend/src/pages/WrittenReviewPage/hooks/useDeviceBreakpoints/index.ts create mode 100644 frontend/src/pages/WrittenReviewPage/index.tsx create mode 100644 frontend/src/pages/WrittenReviewPage/styles.ts create mode 100644 frontend/src/types/media.ts diff --git a/frontend/src/assets/slideArrows.svg b/frontend/src/assets/slideArrows.svg new file mode 100644 index 000000000..acf1609d9 --- /dev/null +++ b/frontend/src/assets/slideArrows.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/components/ReviewListItem/index.tsx b/frontend/src/components/ReviewListItem/index.tsx new file mode 100644 index 000000000..75dacaf8f --- /dev/null +++ b/frontend/src/components/ReviewListItem/index.tsx @@ -0,0 +1,13 @@ +// 임시 컴포넌트! 작성한 리뷰 확인 && 받은 리뷰 확인 아이템 + +import * as S from './styles'; + +interface ReviewListItemProps { + handleClick: () => void; +} + +const ReviewListItem = ({ handleClick }: ReviewListItemProps) => { + return 리뷰 목록 아이템입니다; +}; + +export default ReviewListItem; diff --git a/frontend/src/components/ReviewListItem/styles.ts b/frontend/src/components/ReviewListItem/styles.ts new file mode 100644 index 000000000..4d1408aa0 --- /dev/null +++ b/frontend/src/components/ReviewListItem/styles.ts @@ -0,0 +1,31 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +export const ReviewListItem = styled.li` + display: flex; + flex-direction: column; + + min-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMinWidth}; + max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth}; + min-height: 20rem; + max-height: 24rem; + + border: 0.2rem solid ${({ theme }) => theme.colors.placeholder}; + border-radius: ${({ theme }) => theme.borderRadius.basic}; + + ${media.medium} { + min-width: 62vw; + min-height: 18vh; + } + + ${media.small} { + min-width: 65vw; + min-height: 14vh; + } + + ${media.xSmall} { + min-width: 70vw; + min-height: 14vh; + } +`; diff --git a/frontend/src/constants/amplitudeEventName.ts b/frontend/src/constants/amplitudeEventName.ts index a90ae22e5..a9b406408 100644 --- a/frontend/src/constants/amplitudeEventName.ts +++ b/frontend/src/constants/amplitudeEventName.ts @@ -25,6 +25,7 @@ export const PAGE_VISITED_EVENT_NAME: { [key in Exclude]: s reviewWriting: '[page] 리뷰 작성 페이지', reviewWritingComplete: '[page] 리뷰 작성 완료 페이지', reviewLinks: '[page] 리뷰 링크 관리 페이지', + writtenReview: '[page] 작성한 리뷰 확인 페이지', }; export const REVIEW_WRITING_EVENT_NAME = { diff --git a/frontend/src/constants/route.ts b/frontend/src/constants/route.ts index df2fc17fe..b8b220cfa 100644 --- a/frontend/src/constants/route.ts +++ b/frontend/src/constants/route.ts @@ -8,4 +8,5 @@ export const ROUTE = { reviewZone: 'user/review-zone', reviewCollection: 'user/review-collection', reviewLinks: 'user/review-links', + writtenReview: 'user/written-review', }; diff --git a/frontend/src/hooks/useSearchParamAndQuery.ts b/frontend/src/hooks/useSearchParamAndQuery.ts index 349d9a6e5..898f1942f 100644 --- a/frontend/src/hooks/useSearchParamAndQuery.ts +++ b/frontend/src/hooks/useSearchParamAndQuery.ts @@ -1,13 +1,13 @@ import { useLocation, useParams } from 'react-router'; interface UseSearchParamAndQueryProps { - paramKey: string; + paramKey?: string; queryStringKey?: string; } /** * url에서 원하는 param, queryString의 값을 가져온다. * @param paramKey: 가져오고 싶은 param의 key - * @param queryStringKey: 가져오고 싶은 queryString의 key (옵셔널) + * @param queryStringKey: 가져오고 싶은 queryString의 key */ const useSearchParamAndQuery = ({ paramKey, queryStringKey }: UseSearchParamAndQueryProps) => { const location = useLocation(); diff --git a/frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/index.tsx b/frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/index.tsx new file mode 100644 index 000000000..5b8e92297 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/index.tsx @@ -0,0 +1,23 @@ +import { NoSelectedReviewGuide } from '../index'; +import { PageContentLayout } from '../layouts'; + +import * as S from './styles'; + +export interface DetailedWrittenReviewProps { + $isDisplayable: boolean; + selectedReviewId: number | null; +} + +const DetailedWrittenReview = ({ $isDisplayable, selectedReviewId }: DetailedWrittenReviewProps) => { + // 추후 이곳에서 직접 상세 리뷰 데이터 호출 + + return ( + + + {selectedReviewId ?
    {selectedReviewId} 선택함
    : }
    +
    +
    + ); +}; + +export default DetailedWrittenReview; diff --git a/frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/styles.ts b/frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/styles.ts new file mode 100644 index 000000000..f928196c0 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/DetailedWrittenReview/styles.ts @@ -0,0 +1,47 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +import { DetailedWrittenReviewProps } from '.'; + +export interface StyleProps extends Pick {} + +export const DetailedWrittenReview = styled.div` + display: block; + max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth}; + + ${media.medium} { + ${({ $isDisplayable }) => + $isDisplayable + ? ` + display: block; + ` + : ` + display: none; + `} + } +`; + +export const Outline = styled.div` + display: flex; + align-items: center; + + width: 100%; + min-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMinWidth}; + max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth}; + height: 100%; + min-height: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxHeight}; + + border: 0.2rem solid ${({ theme }) => theme.colors.lightGray}; + border-radius: ${({ theme }) => theme.borderRadius.basic}; + + ${media.medium} { + min-width: 65vw; + max-width: 65vw; + } + + ${media.small} { + min-width: 75vw; + max-width: 75vw; + } +`; diff --git a/frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/index.tsx b/frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/index.tsx new file mode 100644 index 000000000..f9105028a --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/index.tsx @@ -0,0 +1,14 @@ +import SlideArrowsIcon from '@/assets/slideArrows.svg'; + +import * as S from './styles'; + +const NoSelectedReviewGuide = () => { + return ( + + +

    확인할 리뷰를 선택해주세요!

    +
    + ); +}; + +export default NoSelectedReviewGuide; diff --git a/frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/styles.ts b/frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/styles.ts new file mode 100644 index 000000000..45985eaba --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/NoSelectedReviewGuide/styles.ts @@ -0,0 +1,31 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +export const NoSelectedReview = styled.section` + display: flex; + gap: 2rem; + align-items: center; + justify-content: center; + + margin: 0 auto; + + img { + height: 3rem; + + ${media.medium} { + height: 2.8rem; + margin-left: 2.5rem; + } + } + + p { + font-size: ${({ theme }) => theme.fontSize.mediumSmall}; + font-weight: bold; + color: ${({ theme }) => theme.colors.disabled}; + + ${media.medium} { + font-size: ${({ theme }) => theme.fontSize.basic}; + } + } +`; diff --git a/frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/index.tsx b/frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/index.tsx new file mode 100644 index 000000000..f8fa96677 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/index.tsx @@ -0,0 +1,29 @@ +import ReviewListItem from '@/components/ReviewListItem'; + +import { PageContentLayout } from '../layouts'; + +import * as S from './styles'; + +interface WrittenReviewListProps { + handleClick: (reviewId: number) => void; +} + +const WrittenReviewList = ({ handleClick }: WrittenReviewListProps) => { + // 리뷰 리스트 받아오기 + const reviewIdList = [5, 1, 2, 3, 4]; + + return ( + + + {/** 추후 이벤트 위임 형식으로 변경 가능 */} + + {/** TODO: 작성한 리뷰 없을 때의 컴포넌트 추가*/} + {reviewIdList.map((reviewId) => ( + handleClick(reviewId)} /> + ))} + + + ); +}; + +export default WrittenReviewList; diff --git a/frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/styles.ts b/frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/styles.ts new file mode 100644 index 000000000..16660d103 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/WrittenReviewList/styles.ts @@ -0,0 +1,32 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +export const WrittenReviewList = styled.ul` + overflow-x: hidden; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1.7rem; + + width: 100%; + min-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMinWidth}; + max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth}; + height: 100%; + min-height: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxHeight}; + max-height: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxHeight}; + + ${media.medium} { + min-width: 65vw; + max-width: 65vw; + } + + ${media.small} { + min-width: 75vw; + max-width: 75vw; + } + + & > li { + margin-right: 0.5rem; + } +`; diff --git a/frontend/src/pages/WrittenReviewPage/components/index.tsx b/frontend/src/pages/WrittenReviewPage/components/index.tsx new file mode 100644 index 000000000..44f66add5 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/index.tsx @@ -0,0 +1,3 @@ +export { default as NoSelectedReviewGuide } from './NoSelectedReviewGuide'; +export { default as DetailedWrittenReview } from './DetailedWrittenReview'; +export { default as WrittenReviewList } from './WrittenReviewList'; diff --git a/frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/index.tsx b/frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/index.tsx new file mode 100644 index 000000000..ce61b04d9 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/index.tsx @@ -0,0 +1,18 @@ +import { EssentialPropsWithChildren } from '@/types'; + +import * as S from './styles'; + +interface WrittenReviewItemProps { + title: string; +} + +const PageContentLayout = ({ title, children }: EssentialPropsWithChildren) => { + return ( + + {title} + {children} + + ); +}; + +export default PageContentLayout; diff --git a/frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/styles.ts b/frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/styles.ts new file mode 100644 index 000000000..246f76354 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/layouts/PageContentLayout/styles.ts @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +export const PageContentLayout = styled.article` + display: flex; + flex-direction: column; + justify-content: center; + + height: 100%; + margin: 0 auto; +`; + +export const Title = styled.h2` + align-self: flex-start; + + margin-top: 4.7rem; + margin-bottom: 2.4rem; + + font-size: 1.8rem; + font-weight: bold; + text-align: left; +`; + +export const Content = styled.section``; diff --git a/frontend/src/pages/WrittenReviewPage/components/layouts/index.tsx b/frontend/src/pages/WrittenReviewPage/components/layouts/index.tsx new file mode 100644 index 000000000..fe53259cd --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/components/layouts/index.tsx @@ -0,0 +1 @@ +export { default as PageContentLayout } from './PageContentLayout'; diff --git a/frontend/src/pages/WrittenReviewPage/hooks/index.ts b/frontend/src/pages/WrittenReviewPage/hooks/index.ts new file mode 100644 index 000000000..f74b15f07 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/hooks/index.ts @@ -0,0 +1 @@ +export { default as useDeviceBreakpoints } from './useDeviceBreakpoints'; diff --git a/frontend/src/pages/WrittenReviewPage/hooks/useDeviceBreakpoints/index.ts b/frontend/src/pages/WrittenReviewPage/hooks/useDeviceBreakpoints/index.ts new file mode 100644 index 000000000..1089d63de --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/hooks/useDeviceBreakpoints/index.ts @@ -0,0 +1,53 @@ +import { useState, useLayoutEffect } from 'react'; + +import { breakpoint } from '@/styles/theme'; +import { Breakpoints } from '@/types/media'; +import { debounce } from '@/utils'; + +interface CurrentDevice { + isMobile: boolean; + isTablet: boolean; + isDesktop: boolean; +} + +const DEBOUNCE_TIME = 100; + +/** + 현재 미디어 쿼리 상태와 디바이스 종류(boolean)를 리턴하는 훅 + */ +const useDeviceBreakpoints = () => { + const [breakpointType, setBreakPointType] = useState(null); + const breakpointsArray = Object.entries(breakpoint); + + const getDeviceType = (breakpointType: Breakpoints | null): CurrentDevice => ({ + isMobile: breakpointType === 'xSmall' || breakpointType === 'xxSmall', + isTablet: breakpointType === 'small' || breakpointType === 'medium', + isDesktop: breakpointType === 'large', + }); + + const handleResize = debounce(() => { + const currentWidth = window.innerWidth; + + // 마지막 breakpoint만 특정 범위 사이의 width 값이 아닌, 해당 기준 이상인 값이므로 따로 처리 + const inRangeBreakpoint = breakpointsArray.find(([, width]) => currentWidth <= width); + const upperBoundBreakpoint = breakpointsArray[breakpointsArray.length - 1]; + + const finalBreakpoint = inRangeBreakpoint || upperBoundBreakpoint; + + setBreakPointType((finalBreakpoint[0] as Breakpoints) ?? null); + }, DEBOUNCE_TIME); + + useLayoutEffect(() => { + handleResize(); + window.addEventListener('resize', handleResize); + + return () => window.removeEventListener('resize', handleResize); + }, []); + + return { + breakpointType, + deviceType: getDeviceType(breakpointType), + }; +}; + +export default useDeviceBreakpoints; diff --git a/frontend/src/pages/WrittenReviewPage/index.tsx b/frontend/src/pages/WrittenReviewPage/index.tsx new file mode 100644 index 000000000..c17bd1f7a --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/index.tsx @@ -0,0 +1,51 @@ +import { useLocation, useNavigate } from 'react-router-dom'; + +import { ErrorSuspenseContainer, AuthAndServerErrorFallback } from '@/components'; +import { useSearchParamAndQuery } from '@/hooks'; + +import DetailedWrittenReview from './components/DetailedWrittenReview'; +import WrittenReviewList from './components/WrittenReviewList'; +import { useDeviceBreakpoints } from './hooks'; +import * as S from './styles'; + +const WrittenReviewPage = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { deviceType } = useDeviceBreakpoints(); + + const { queryString: reviewIdString } = useSearchParamAndQuery({ + queryStringKey: 'reviewId', + }); + + const selectedReviewId = reviewIdString ? Number(reviewIdString) : null; + + const handleReviewItemClick = (reviewId: number) => { + const params = new URLSearchParams(); + params.set('reviewId', reviewId.toString()); + + navigate(`${location.pathname}?${params.toString()}`); + }; + + const renderContent = () => { + // 노트북, 보통 사이즈 이상의 태블릿 가로모드: 목록, 상세 모두 렌더링 + if (deviceType.isDesktop) { + return ( + + + + + ); + } + + // 이외의 경우: queryString 없으면 목록, 있으면 상세보기 렌더링 + return selectedReviewId ? ( + + ) : ( + + ); + }; + + return {renderContent()}; +}; + +export default WrittenReviewPage; diff --git a/frontend/src/pages/WrittenReviewPage/styles.ts b/frontend/src/pages/WrittenReviewPage/styles.ts new file mode 100644 index 000000000..4de7d51b0 --- /dev/null +++ b/frontend/src/pages/WrittenReviewPage/styles.ts @@ -0,0 +1,17 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +export const PageContainer = styled.div` + display: flex; + gap: 6rem; + justify-content: center; + + ${media.medium} { + gap: 4rem; + } + + ${media.small} { + margin: 0 2rem; + } +`; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 28b98890c..f9d1841e3 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -8,3 +8,4 @@ export { default as ReviewWritingCompletePage } from './ReviewWritingCompletePag export { default as ReviewZonePage } from './ReviewZonePage'; export { default as ReviewCollectionPage } from './ReviewCollectionPage'; export { default as ReviewLinkPage } from './ReviewLinkPage'; +export { default as WrittenReviewPage } from './WrittenReviewPage'; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index a8abb7137..934668035 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -11,6 +11,7 @@ const ReviewZonePage = lazy(() => import('@/pages/ReviewZonePage')); const ReviewCollectionPage = lazy(() => import('@/pages/ReviewCollectionPage')); const LoadingPage = lazy(() => import('@/pages/LoadingPage')); const ReviewLinkPage = lazy(() => import('@/pages/ReviewLinkPage')); +const WrittenReviewPage = lazy(() => import('@/pages/WrittenReviewPage')); import App from './App'; import { ErrorSuspenseContainer } from './components'; @@ -54,6 +55,7 @@ const router = createBrowserRouter([ }, { path: `${ROUTE.reviewCollection}/:${ROUTE_PARAM.reviewRequestCode}`, element: }, { path: `${ROUTE.reviewLinks}`, element: }, + { path: `${ROUTE.writtenReview}`, element: }, ], }, ]); diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts index 2d83a68e1..956a98fa4 100644 --- a/frontend/src/styles/theme.ts +++ b/frontend/src/styles/theme.ts @@ -9,9 +9,11 @@ export const scrollbarWidth = { basic: '1.2rem', small: '0.5rem', }; + export const breadcrumbSize = { paddingLeft: '2rem', }; + export const confirmModalSize = { maxWidth: '90vw', padding: '3.2rem', @@ -23,6 +25,12 @@ export const contentModalSize = { smallPadding: '2rem', }; +export const writtenReviewLayoutSize = { + largeMinWidth: '45rem', + largeMaxHeight: '90rem', + largeMaxWidth: '90rem', +}; + export const componentHeight = { footer: '6rem', topbar: '7rem', @@ -31,7 +39,7 @@ export const componentHeight = { export const breakpoint = { xxSmall: 320, - xSmall: 425, + xSmall: 430, small: 768, medium: 1024, large: 1025, @@ -104,6 +112,7 @@ const theme: Theme = { confirmModalSize, contentModalSize, breadcrumbSize, + writtenReviewLayoutSize, }; export default theme; diff --git a/frontend/src/types/emotion.ts b/frontend/src/types/emotion.ts index 61055fcd4..554083b9e 100644 --- a/frontend/src/types/emotion.ts +++ b/frontend/src/types/emotion.ts @@ -12,6 +12,7 @@ import { confirmModalSize, contentModalSize, breadcrumbSize, + writtenReviewLayoutSize, } from '../styles/theme'; export type Color = typeof colors; @@ -25,6 +26,7 @@ export type ComponentHeight = typeof componentHeight; export type ConfirmModalSize = typeof confirmModalSize; export type ContentModalSize = typeof contentModalSize; export type BreadcrumbSize = typeof breadcrumbSize; +export type WrittenReviewLayoutSize = typeof writtenReviewLayoutSize; type ThemeType = { fontSize: FontSize; @@ -39,6 +41,7 @@ type ThemeType = { confirmModalSize: ConfirmModalSize; contentModalSize: ContentModalSize; breadcrumbSize: BreadcrumbSize; + writtenReviewLayoutSize: WrittenReviewLayoutSize; }; declare module '@emotion/react' { diff --git a/frontend/src/types/media.ts b/frontend/src/types/media.ts new file mode 100644 index 000000000..67b13e5f4 --- /dev/null +++ b/frontend/src/types/media.ts @@ -0,0 +1,3 @@ +import { breakpoint } from '@/styles/theme'; + +export type Breakpoints = keyof typeof breakpoint; diff --git a/frontend/src/utils/media.ts b/frontend/src/utils/media.ts index ab61e7285..e4030cee4 100644 --- a/frontend/src/utils/media.ts +++ b/frontend/src/utils/media.ts @@ -1,10 +1,10 @@ import theme from '@/styles/theme'; +import { Breakpoints } from '@/types/media'; -const { breakpoint } = theme; - -export type Breakpoints = keyof typeof breakpoint; type Media = { [key in Breakpoints]: string }; +const { breakpoint } = theme; + const breakpointsKeyList = Object.keys(breakpoint) as Breakpoints[]; const media = breakpointsKeyList.reduce((prev, key, index) => { From 7b0c8653c7e37881bc928be3ebffdc503722b0aa Mon Sep 17 00:00:00 2001 From: sooyeon Date: Thu, 16 Jan 2025 01:24:46 +0900 Subject: [PATCH 49/60] =?UTF-8?q?[FE]=20feat:=20NavigationTab=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#1037)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: NavigationTab 컴포넌트 구현 * feat: NavigationTab 반응형 구현 * feat: URL 쿼리 파라미터 값에 따라 탭 인덱스 업데이트 * feat: 세션 스토리지를 활용해서 선택한 탭 상태 유지 * feat: 상태를 추가하여 transition 활성화 여부 제어 * style: css 속성 정렬 * fix: sessionStorage에 activeTab이 없을 경우 기본값 0 설정 * refactor: useState와 세션 스토리지 로직 제거 및 URL 기반으로 활성화 탭 관리 * feat: NavigationTab 존재 시 헤더 border-bottom none 처리 * feat: ROUTE 상수에 리뷰 링크 관리, 작성한 리뷰 확인 페이지 경로 추가 * feat: 작성한 리뷰 확인 페이지 amplitude 방문 이벤트 이름 추가 * feat: 리뷰 링크 페이지에 NavigationTab 적용 * refactor: min-height을 ReviewLinkDashboardContainer로 이동 및 flex-start 적용 --- .../common/NavigationTab/NavItem/index.tsx | 17 +++++++++ .../common/NavigationTab/NavItem/styles.ts | 33 +++++++++++++++++ .../components/common/NavigationTab/index.tsx | 27 ++++++++++++++ .../components/common/NavigationTab/styles.ts | 29 +++++++++++++++ .../src/components/layouts/Topbar/index.tsx | 8 ++++- .../src/components/layouts/Topbar/styles.ts | 5 +-- frontend/src/hooks/useNavigationTabs.ts | 35 +++++++++++++++++++ .../components/ReviewLinkDashboard/index.tsx | 13 +++++-- .../components/ReviewLinkDashboard/styles.ts | 5 +-- frontend/src/pages/ReviewLinkPage/index.tsx | 3 +- 10 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/common/NavigationTab/NavItem/index.tsx create mode 100644 frontend/src/components/common/NavigationTab/NavItem/styles.ts create mode 100644 frontend/src/components/common/NavigationTab/index.tsx create mode 100644 frontend/src/components/common/NavigationTab/styles.ts create mode 100644 frontend/src/hooks/useNavigationTabs.ts diff --git a/frontend/src/components/common/NavigationTab/NavItem/index.tsx b/frontend/src/components/common/NavigationTab/NavItem/index.tsx new file mode 100644 index 000000000..88d4f9682 --- /dev/null +++ b/frontend/src/components/common/NavigationTab/NavItem/index.tsx @@ -0,0 +1,17 @@ +import * as S from './styles'; + +interface NavItemProps { + label: string; + $isSelected: boolean; + onClick: () => void; +} + +const NavItem = ({ label, $isSelected, onClick }: NavItemProps) => { + return ( + + + + ); +}; + +export default NavItem; diff --git a/frontend/src/components/common/NavigationTab/NavItem/styles.ts b/frontend/src/components/common/NavigationTab/NavItem/styles.ts new file mode 100644 index 000000000..f7f144cd2 --- /dev/null +++ b/frontend/src/components/common/NavigationTab/NavItem/styles.ts @@ -0,0 +1,33 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +interface NavItemProps { + $isSelected: boolean; +} + +export const NavItem = styled.li` + border-bottom: 0.3rem solid ${({ theme, $isSelected }) => ($isSelected ? theme.colors.primary : 'none')}; + padding: 0 1rem 1.3rem 1rem; + + button { + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + color: ${({ theme, $isSelected }) => ($isSelected ? theme.colors.black : theme.colors.disabled)}; + + &:hover { + color: ${({ theme }) => theme.colors.black}; + } + } + + ${media.xSmall} { + display: flex; + flex: 1; + justify-content: center; + + margin: 0 2rem; + } + + ${media.xxSmall} { + margin: 0 1.6rem; + } +`; diff --git a/frontend/src/components/common/NavigationTab/index.tsx b/frontend/src/components/common/NavigationTab/index.tsx new file mode 100644 index 000000000..2ad073e60 --- /dev/null +++ b/frontend/src/components/common/NavigationTab/index.tsx @@ -0,0 +1,27 @@ +import useNavigationTabs from '@/hooks/useNavigationTabs'; + +import NavItem from './NavItem'; +import * as S from './styles'; + +const NavigationTab = () => { + const { currentTabIndex, tabList } = useNavigationTabs(); + + return ( + + + {tabList.map((tab, index) => { + return ( + + ); + })} + + + ); +}; + +export default NavigationTab; diff --git a/frontend/src/components/common/NavigationTab/styles.ts b/frontend/src/components/common/NavigationTab/styles.ts new file mode 100644 index 000000000..6b2472ae2 --- /dev/null +++ b/frontend/src/components/common/NavigationTab/styles.ts @@ -0,0 +1,29 @@ +import styled from '@emotion/styled'; + +import media from '@/utils/media'; + +export const NavContainer = styled.nav` + position: relative; + display: flex; + width: calc(100vw - ${({ theme }) => theme.scrollbarWidth.basic}); + height: 4rem; + + border-bottom: 0.1rem solid ${({ theme }) => theme.colors.lightGray}; + + ${media.small} { + width: calc(100vw - ${({ theme }) => theme.scrollbarWidth.small}); + } +`; + +export const NavList = styled.ul` + display: flex; + gap: 3rem; + padding: 0 2.5rem; + list-style-type: none; + + ${media.xSmall} { + gap: 0; + width: 100%; + padding: 0; + } +`; diff --git a/frontend/src/components/layouts/Topbar/index.tsx b/frontend/src/components/layouts/Topbar/index.tsx index 61fd22b88..af4710013 100644 --- a/frontend/src/components/layouts/Topbar/index.tsx +++ b/frontend/src/components/layouts/Topbar/index.tsx @@ -1,11 +1,17 @@ +import { useLocation } from 'react-router'; + import UndraggableWrapper from '@/components/common/UndraggableWrapper'; +import { ROUTE } from '@/constants'; import Logo from './components/Logo'; import * as S from './styles'; const Topbar = () => { + const { pathname } = useLocation(); + const $hasNavigationTab = [ROUTE.reviewLinks, ROUTE.writtenReview].includes(pathname); + return ( - + diff --git a/frontend/src/components/layouts/Topbar/styles.ts b/frontend/src/components/layouts/Topbar/styles.ts index 80f6ff698..ea32b903b 100644 --- a/frontend/src/components/layouts/Topbar/styles.ts +++ b/frontend/src/components/layouts/Topbar/styles.ts @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -export const Layout = styled.section` +export const Layout = styled.section<{ $hasNavigationTab: boolean }>` z-index: ${({ theme }) => theme.zIndex.topbar}; display: flex; @@ -11,7 +11,8 @@ export const Layout = styled.section` height: ${({ theme }) => theme.componentHeight.topbar}; padding: 2rem 2.5rem; - border-bottom: 0.1rem solid ${({ theme }) => theme.colors.lightGray}; + border-bottom: ${({ theme, $hasNavigationTab }) => + $hasNavigationTab ? `0.1rem solid ${theme.colors.lightGray}` : 'none'}; `; export const Container = styled.div` diff --git a/frontend/src/hooks/useNavigationTabs.ts b/frontend/src/hooks/useNavigationTabs.ts new file mode 100644 index 000000000..57f416023 --- /dev/null +++ b/frontend/src/hooks/useNavigationTabs.ts @@ -0,0 +1,35 @@ +import { useLocation, useNavigate } from 'react-router'; + +import { ROUTE } from '@/constants'; + +const useNavigationTabs = () => { + const { pathname } = useLocation(); + const navigate = useNavigate(); + + const navigateReviewLinkManagementPage = () => { + navigate(`/${ROUTE.reviewLinks}`); + }; + + const navigateWrittenReviewConfirmPage = () => { + navigate(`/${ROUTE.writtenReview}`); + }; + + const tabList = [ + { + label: '리뷰 링크 관리', + path: `/${ROUTE.reviewLinks}`, + handleTabClick: navigateReviewLinkManagementPage, + }, + { + label: '작성한 리뷰 확인', + path: `/${ROUTE.writtenReview}`, + handleTabClick: navigateWrittenReviewConfirmPage, + }, + ]; + + const currentTabIndex = tabList.findIndex((tab) => tab.path === pathname); + + return { currentTabIndex, tabList }; +}; + +export default useNavigationTabs; diff --git a/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx index aaf99ed0c..4341f2e60 100644 --- a/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx +++ b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/index.tsx @@ -1,3 +1,4 @@ +import ReviewCard from '@/components/common/ReviewCard'; import { URLGeneratorForm } from '@/pages/HomePage/components'; import ReviewLinkLayout from '../layouts/ReviewLinkLayout'; @@ -16,8 +17,16 @@ const ReviewLinkDashboard = () => { title="생성한 리뷰 링크를 확인해보세요" subTitle="클릭하면 해당 프로젝트의 리뷰 목록으로 이동해요" > - {/* TODO: ReviewCard 컴포넌트 추가 및 생성한 리뷰 링크가 없을 경우, 돋보기 컴포넌트 추가 */} - <> + {/* TODO: 생성한 리뷰 링크가 없을 경우, 돋보기 컴포넌트 추가 */} + {}} + /> diff --git a/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts index 8d4ab58ae..ecf34b973 100644 --- a/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts +++ b/frontend/src/pages/ReviewLinkPage/components/ReviewLinkDashboard/styles.ts @@ -8,6 +8,8 @@ export const ReviewLinkDashboardContainer = styled.div` gap: 7rem; width: 100%; + // 전체 영역에서 헤더(7rem), 푸터(6rem), 네비게이션 탭(4rem) 영역 제외 + min-height: calc(100vh - 17rem); ${media.medium} { gap: 4rem; @@ -19,6 +21,7 @@ export const ReviewLinkDashboardContainer = styled.div` ${media.small} { flex-direction: column; + justify-content: flex-start; align-items: center; } `; @@ -44,8 +47,6 @@ export const FormSection = styled.section` export const Separator = styled.div` width: 0.1rem; - // 전체 영역에서 헤더(7rem)와 푸터(6rem) 영역 제외하고, 추후 네비게이션 탭이 추가되면 해당 영역도 제외 - min-height: calc(100vh - 13rem); background-color: ${({ theme }) => theme.colors.lightGray}; diff --git a/frontend/src/pages/ReviewLinkPage/index.tsx b/frontend/src/pages/ReviewLinkPage/index.tsx index f9ecf4719..a7b1761df 100644 --- a/frontend/src/pages/ReviewLinkPage/index.tsx +++ b/frontend/src/pages/ReviewLinkPage/index.tsx @@ -1,11 +1,12 @@ import { ErrorSuspenseContainer } from '@/components'; +import NavigationTab from '@/components/common/NavigationTab'; import ReviewLinkDashboard from './components/ReviewLinkDashboard'; const ReviewLinkPage = () => { return ( - {/* TODO: 네비게이션 탭 추가 */} + ); From 4887154663f657b2637a0ac7510a96eda88536ab Mon Sep 17 00:00:00 2001 From: badahertz52 Date: Thu, 16 Jan 2025 14:59:03 +0900 Subject: [PATCH 50/60] =?UTF-8?q?[FE]=20feat=20:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#1055)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : 이미지 스켈레톤 컴포넌트 구현 - 이미지 컴포넌트를 children으로 받아서, 로드 전에는 스켈레톤을 띄우는 컴포넌트 구현 * feat : 리뷰 연결 페이지 이미지에 스켈레톤 적용 * style : 이미지 스켈레톤 스타일 변경 - 스켈레톤 배경색 변경 - 오타 수정 * feat : 홈페이지 캐러셀에 이미지 스켈레톤 적용 - 슬라이드 너비에 따라 이미지 사이즈 구하는 훅(useSlideImgSize) 생성 - 슬라이드 이미지 스타일 컴포넌트 분리 * chore : 불필요한 주석 삭제 및 경로 수정 * refactor : 가독성을 위해 줄 바꿈 추가 * feat : ImageSkeleton에서 img onLoad 확장할 수 있게 변경 * fix : 스켈레톤 event 타입으로 인한 테스트 오류 수정 - 테스트 시, event타입이 any로 명시되어서 오류가 나는 것으로 추측 -> 타입을 명시해 오류 해결 --- frontend/src/components/index.tsx | 1 + .../skeleton/ImgWithSkeleton/index.tsx | 33 +++++++++++++ .../skeleton/ImgWithSkeleton/style.ts | 47 +++++++++++++++++++ frontend/src/components/skeleton/index.tsx | 1 + .../components/InfinityCarousel/index.tsx | 8 +++- .../components/InfinityCarousel/styles.ts | 6 +-- .../pages/HomePage/hooks/useSlideImgSize.tsx | 42 +++++++++++++++++ frontend/src/pages/ReviewZonePage/index.tsx | 27 +++++------ frontend/src/pages/ReviewZonePage/styles.ts | 5 +- 9 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/skeleton/ImgWithSkeleton/index.tsx create mode 100644 frontend/src/components/skeleton/ImgWithSkeleton/style.ts create mode 100644 frontend/src/components/skeleton/index.tsx create mode 100644 frontend/src/pages/HomePage/hooks/useSlideImgSize.tsx diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx index 9c0ce42cc..207227698 100644 --- a/frontend/src/components/index.tsx +++ b/frontend/src/components/index.tsx @@ -3,3 +3,4 @@ export * from './common'; export * from './error'; export * from './highlight/components'; export * from './login'; +export * from './skeleton'; diff --git a/frontend/src/components/skeleton/ImgWithSkeleton/index.tsx b/frontend/src/components/skeleton/ImgWithSkeleton/index.tsx new file mode 100644 index 000000000..b89c74af3 --- /dev/null +++ b/frontend/src/components/skeleton/ImgWithSkeleton/index.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; + +import * as S from './style'; + +interface ImgWithSkeletonProps { + children: React.ReactElement>; + imgWidth: string; + imgHeight: string; +} + +const ImgWithSkeleton = ({ children, imgWidth, imgHeight }: ImgWithSkeletonProps) => { + const [isLoaded, setIsLoaded] = useState(false); + + const handleImgLoad = (event: React.SyntheticEvent) => { + if (children.props.onLoad) { + children.props.onLoad(event); + } + setIsLoaded(true); + }; + + return ( + + {!isLoaded && } + + {React.cloneElement(children, { + onLoad: (event: React.SyntheticEvent) => handleImgLoad(event), + })} + + + ); +}; + +export default ImgWithSkeleton; diff --git a/frontend/src/components/skeleton/ImgWithSkeleton/style.ts b/frontend/src/components/skeleton/ImgWithSkeleton/style.ts new file mode 100644 index 000000000..5c0f5c372 --- /dev/null +++ b/frontend/src/components/skeleton/ImgWithSkeleton/style.ts @@ -0,0 +1,47 @@ +import styled from '@emotion/styled'; + +interface ContainerProps { + $width: string; + $height: string; +} +export const Container = styled.div` + position: relative; + width: ${(props) => props.$width}; + height: ${(props) => props.$height}; +`; +export const ImgWrapper = styled.div<{ $isLoaded: boolean }>` + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + opacity: ${(props) => (props.$isLoaded ? 1 : 0)}; + + transition: opacity 300ms; +`; +export const ImgSkeleton = styled.div` + width: 100%; + height: 100%; + + background-image: linear-gradient( + 135deg, + ${({ theme }) => theme.colors.lightGray} 40%, + rgba(246, 246, 246, 0.89) 50%, + ${({ theme }) => theme.colors.lightGray} 85% + ); + background-size: 200% 100%; + border-radius: ${({ theme }) => theme.borderRadius.basic}; + + animation: skeleton-animation 1.5s infinite linear; + + @keyframes skeleton-animation { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } +`; diff --git a/frontend/src/components/skeleton/index.tsx b/frontend/src/components/skeleton/index.tsx new file mode 100644 index 000000000..08e501619 --- /dev/null +++ b/frontend/src/components/skeleton/index.tsx @@ -0,0 +1 @@ +export { default as ImgWithSkeleton } from './ImgWithSkeleton'; diff --git a/frontend/src/pages/HomePage/components/InfinityCarousel/index.tsx b/frontend/src/pages/HomePage/components/InfinityCarousel/index.tsx index d5a9cc5fe..ce6153278 100644 --- a/frontend/src/pages/HomePage/components/InfinityCarousel/index.tsx +++ b/frontend/src/pages/HomePage/components/InfinityCarousel/index.tsx @@ -2,8 +2,11 @@ import { useRef, useState, useEffect } from 'react'; import nextArrowIcon from '@/assets/nextArrow.svg'; import prevArrowIcon from '@/assets/prevArrow.svg'; +import { ImgWithSkeleton } from '@/components'; import { breakpoints } from '@/styles/theme'; +import useSlideImgSize from '../../hooks/useSlideImgSize'; + import * as S from './styles'; export interface Slide { @@ -37,6 +40,7 @@ const InfinityCarousel = ({ slideList }: InfinityCarouselProps) => { const [deltaX, setDeltaX] = useState(0); // 현재 드래그 중인 위치와 시작 위치 사이의 차이 const slideRef = useRef(null); + const { imgSize } = useSlideImgSize({ slideRef }); const slideLength = slideList.length; // 첫 슬라이드와 마지막 슬라이드의 복제본을 각각 맨 뒤, 맨 처음에 추가 @@ -151,7 +155,9 @@ const InfinityCarousel = ({ slideList }: InfinityCarouselProps) => { {clonedSlideList.map((slide, index) => ( - {slide.alt} + + + ))} diff --git a/frontend/src/pages/HomePage/components/InfinityCarousel/styles.ts b/frontend/src/pages/HomePage/components/InfinityCarousel/styles.ts index 9d17162cc..90e59eac6 100644 --- a/frontend/src/pages/HomePage/components/InfinityCarousel/styles.ts +++ b/frontend/src/pages/HomePage/components/InfinityCarousel/styles.ts @@ -46,10 +46,10 @@ export const SlideContent = styled.div` justify-content: space-between; width: 100%; +`; - img { - width: 80%; - } +export const SlideContentImg = styled.img` + width: 100%; `; export const PrevButton = styled.button` diff --git a/frontend/src/pages/HomePage/hooks/useSlideImgSize.tsx b/frontend/src/pages/HomePage/hooks/useSlideImgSize.tsx new file mode 100644 index 000000000..bbf3d74d2 --- /dev/null +++ b/frontend/src/pages/HomePage/hooks/useSlideImgSize.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; + +import { debounce } from '@/utils'; + +const DEBOUNCE_TIME = 300; + +interface UseSlideImgSizeProps { + slideRef: React.RefObject; +} +const useSlideImgSize = ({ slideRef }: UseSlideImgSizeProps) => { + interface ImgSize { + width: string; + height: string; + } + const [imgSize, setImgSize] = useState({ width: '', height: '' }); + + const updateImgSize = () => { + if (!slideRef.current) return; + + const slideDomRect = slideRef.current.getBoundingClientRect(); + const width = Math.ceil(slideDomRect.width * 0.8 * 0.1); + const height = width * 0.61; + + setImgSize({ width: `${width}rem`, height: `${height}rem` }); + }; + + const debouncedUpdateImgSize = debounce(updateImgSize, DEBOUNCE_TIME); + + useEffect(() => { + updateImgSize(); + + document.addEventListener('resize', debouncedUpdateImgSize); + + return () => { + document.removeEventListener('resize', debouncedUpdateImgSize); + }; + }, [slideRef]); + + return { imgSize }; +}; + +export default useSlideImgSize; diff --git a/frontend/src/pages/ReviewZonePage/index.tsx b/frontend/src/pages/ReviewZonePage/index.tsx index 89b6cb35a..aa6523668 100644 --- a/frontend/src/pages/ReviewZonePage/index.tsx +++ b/frontend/src/pages/ReviewZonePage/index.tsx @@ -3,8 +3,8 @@ import { useNavigate } from 'react-router'; import { useRecoilState } from 'recoil'; import ReviewZoneIcon from '@/assets/reviewZone.svg'; -import { Button } from '@/components'; -import { ROUTE } from '@/constants/route'; +import { Button, ImgWithSkeleton } from '@/components'; +import { ROUTE } from '@/constants'; import { useGetReviewGroupData, useSearchParamAndQuery, useModals } from '@/hooks'; import { reviewRequestCodeAtom } from '@/recoil'; import { calculateParticle } from '@/utils'; @@ -15,6 +15,11 @@ import * as S from './styles'; const MODAL_KEYS = { content: 'CONTENT_MODAL', }; +const BUTTON_SIZE = { + width: '28rem', + height: '8.5rem', +}; +const IMG_HEIGHT = '15rem'; const ReviewZonePage = () => { const { isOpen, openModal, closeModal } = useModals(); @@ -46,29 +51,21 @@ const ReviewZonePage = () => { return ( - + + + {`${reviewGroupData.projectName}${calculateParticle({ target: reviewGroupData.projectName, particles: { withFinalConsonant: '을', withoutFinalConsonant: '를' } })} 함께한`} {`${reviewGroupData.revieweeName}의 리뷰 공간이에요`} - -