diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/GroupMemberStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/GroupMemberStepDefinitions.java index 1747c700e..c329e46c9 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/GroupMemberStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/GroupMemberStepDefinitions.java @@ -2,37 +2,39 @@ import io.cucumber.java.en.Given; import wooteco.prolog.AcceptanceSteps; -import wooteco.prolog.member.domain.GroupMember; -import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroup; -import wooteco.prolog.member.domain.repository.GroupMemberRepository; -import wooteco.prolog.member.domain.repository.MemberGroupRepository; +import wooteco.prolog.member.domain.*; +import wooteco.prolog.member.domain.repository.DepartmentMemberRepository; +import wooteco.prolog.member.domain.repository.DepartmentRepository; import wooteco.prolog.member.domain.repository.MemberRepository; +import static wooteco.prolog.member.domain.Part.*; +import static wooteco.prolog.member.domain.Term.*; + public class GroupMemberStepDefinitions extends AcceptanceSteps { private final MemberRepository memberRepository; - private final MemberGroupRepository memberGroupRepository; - private final GroupMemberRepository groupMemberRepository; + private final DepartmentRepository departmentRepository; + private final DepartmentMemberRepository departmentMemberRepository; public GroupMemberStepDefinitions(MemberRepository memberRepository, - MemberGroupRepository memberGroupRepository, - GroupMemberRepository groupMemberRepository) { + DepartmentRepository departmentRepository, + DepartmentMemberRepository departmentMemberRepository) { this.memberRepository = memberRepository; - this.memberGroupRepository = memberGroupRepository; - this.groupMemberRepository = groupMemberRepository; + this.departmentRepository = departmentRepository; + this.departmentMemberRepository = departmentMemberRepository; } @Given("{string}을 멤버그룹과 그룹멤버에 등록하고") public void 그룹멤버를_생성하고(String title) { Member member = memberRepository.findById(1L).get(); - MemberGroup 프론트엔드 = memberGroupRepository.save( - new MemberGroup(null, "4기 프론트엔드", "4기 프론트엔드 설명")); - MemberGroup 백엔드 = memberGroupRepository.save(new MemberGroup(null, "4기 백엔드", "4기 백엔드 설명")); - MemberGroup 안드로이드 = memberGroupRepository.save( - new MemberGroup(null, "4기 안드로이드", "4기 안드로이드 설명")); - groupMemberRepository.save(new GroupMember(null, member, 백엔드)); - groupMemberRepository.save(new GroupMember(null, member, 프론트엔드)); - groupMemberRepository.save(new GroupMember(null, member, 안드로이드)); + Department 프론트엔드 = departmentRepository.save( + new Department(null, FRONTEND, FOURTH)); + Department 백엔드 = departmentRepository.save(new Department(null, BACKEND, FOURTH)); + Department 안드로이드 = departmentRepository.save( + new Department(null, ANDROID, FOURTH)); + departmentMemberRepository.save(new DepartmentMember(null, member, 백엔드)); + departmentMemberRepository.save(new DepartmentMember(null, member, 프론트엔드)); + departmentMemberRepository.save(new DepartmentMember(null, member, 안드로이드)); } + } diff --git a/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java index 96e3341c5..e88cc6334 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java @@ -1,6 +1,8 @@ package wooteco.prolog.docu; import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.prolog.member.domain.Part.*; +import static wooteco.prolog.member.domain.Term.*; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; @@ -16,11 +18,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import wooteco.prolog.Documentation; -import wooteco.prolog.member.domain.GroupMember; -import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroup; -import wooteco.prolog.member.domain.repository.GroupMemberRepository; -import wooteco.prolog.member.domain.repository.MemberGroupRepository; +import wooteco.prolog.member.domain.*; +import wooteco.prolog.member.domain.repository.DepartmentMemberRepository; +import wooteco.prolog.member.domain.repository.DepartmentRepository; import wooteco.prolog.member.domain.repository.MemberRepository; import wooteco.prolog.session.application.dto.MissionRequest; import wooteco.prolog.session.application.dto.MissionResponse; @@ -37,9 +37,9 @@ class StudylogDocumentation extends Documentation { @Autowired private MemberRepository memberRepository; @Autowired - private MemberGroupRepository memberGroupRepository; + private DepartmentRepository departmentRepository; @Autowired - private GroupMemberRepository groupMemberRepository; + private DepartmentMemberRepository departmentMemberRepository; @Test void 스터디로그를_생성한다() { @@ -312,14 +312,14 @@ private StudylogRequest createStudylogRequest3() { private void 회원과_멤버그룹_그룹멤버를_등록함() { Member member = memberRepository.findById(1L).get(); - MemberGroup 프론트엔드 = memberGroupRepository.save( - new MemberGroup(null, "4기 프론트엔드", "4기 프론트엔드 설명")); - MemberGroup 백엔드 = memberGroupRepository.save(new MemberGroup(null, "4기 백엔드", "4기 백엔드 설명")); - MemberGroup 안드로이드 = memberGroupRepository.save( - new MemberGroup(null, "4기 안드로이드", "4기 안드로이드 설명")); - groupMemberRepository.save(new GroupMember(null, member, 백엔드)); - groupMemberRepository.save(new GroupMember(null, member, 프론트엔드)); - groupMemberRepository.save(new GroupMember(null, member, 안드로이드)); + Department 프론트엔드 = departmentRepository.save( + new Department(null, FRONTEND, FOURTH)); + Department 백엔드 = departmentRepository.save(new Department(null, BACKEND, FOURTH)); + Department 안드로이드 = departmentRepository.save( + new Department(null, ANDROID, FOURTH)); + departmentMemberRepository.save(new DepartmentMember(null, member, 백엔드)); + departmentMemberRepository.save(new DepartmentMember(null, member, 프론트엔드)); + departmentMemberRepository.save(new DepartmentMember(null, member, 안드로이드)); } private Long 미션_등록함(MissionRequest request) { diff --git a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java index 4dd8f2945..82ea51cff 100644 --- a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java +++ b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java @@ -86,13 +86,13 @@ public List getFilteredArticles(final LoginMember member, final ArticleFilterType course, final boolean onlyBookmarked) { if (member.isMember()) { - return articleRepository.findArticlesByCourseAndMember(course.getGroupName(), + return articleRepository.findArticlesByCourseAndMember(course.getPartName(), member.getId(), onlyBookmarked).stream() .map(article -> ArticleResponse.of(article, member.getId())) .collect(toList()); } - return articleRepository.findArticlesByCourse(course.getGroupName()).stream() + return articleRepository.findArticlesByCourse(course.getPartName()).stream() .map(article -> ArticleResponse.of(article, member.getId())) .collect(toList()); } diff --git a/backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java b/backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java index 8eddb65f6..94913a001 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java @@ -9,9 +9,9 @@ public enum ArticleFilterType { BACKEND("백엔드"), FRONTEND("프론트엔드"); - private final String groupName; + private final String partName; - ArticleFilterType(String groupName) { - this.groupName = groupName; + ArticleFilterType(String partName) { + this.partName = partName; } } diff --git a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java index 2e02f23ea..69360be64 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java @@ -18,18 +18,18 @@ public interface ArticleRepository extends JpaRepository { Optional
findFetchLikeById(@Param("id") final Long id); @Query("SELECT DISTINCT a FROM Article a " + - "JOIN GroupMember gm ON a.member.id = gm.member.id " + - "JOIN gm.group mg " + - "WHERE mg.name LIKE %:course " + + "JOIN DepartmentMember dm ON dm.member.id = dm.member.id " + + "JOIN dm.department d " + + "WHERE d.part = :course " + "ORDER by a.createdAt desc") List
findArticlesByCourse(@Param("course") String course); @Query("SELECT DISTINCT a FROM Article a " + - "JOIN GroupMember gm ON a.member.id = gm.member.id " + - "JOIN gm.group mg " + + "JOIN DepartmentMember dm ON a.member.id = dm.member.id " + + "JOIN dm.department d " + "LEFT JOIN a.articleBookmarks.articleBookmarks ab " + "LEFT JOIN a.articleLikes.articleLikes al " + - "WHERE mg.name LIKE %:course AND (:onlyBookmarked = false OR (:onlyBookmarked = true and ab.memberId = :memberId))" + + "WHERE d.part = :course AND (:onlyBookmarked = false OR (:onlyBookmarked = true and ab.memberId = :memberId))" + "ORDER by a.createdAt desc") List
findArticlesByCourseAndMember(@Param("course") String course, @Param("memberId") Long memberId, diff --git a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java index cafc1458f..25a47e25a 100644 --- a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java +++ b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java @@ -94,7 +94,7 @@ public enum BadRequestCode { ARTICLE_IMAGE_URL_OVER_LENGTH_EXCEPTION(12007, "ARTICLE_IMAGE_URL_OVER_LENGTH_EXCEPTION"), ARTICLE_INVALID_URL_EXCEPTION(12008, "ARTICLE_INVALID_URL_EXCEPTION"), UNVALIDATED_MEMBER_EXCEPTION(12009, "UNVALIDATED_MEMBER_EXCEPTION"); - + private int code; private String message; } diff --git a/backend/src/main/java/wooteco/prolog/member/application/DepartmentMemberService.java b/backend/src/main/java/wooteco/prolog/member/application/DepartmentMemberService.java new file mode 100644 index 000000000..80fd7d323 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/application/DepartmentMemberService.java @@ -0,0 +1,21 @@ +package wooteco.prolog.member.application; + +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.member.domain.DepartmentMember; +import wooteco.prolog.member.domain.repository.DepartmentMemberRepository; + +@Service +@AllArgsConstructor +@Transactional(readOnly = true) +public class DepartmentMemberService { + + private DepartmentMemberRepository departmentMemberRepository; + + public List findDepartmentMemberByDepartmentId(Long departmentId) { + return departmentMemberRepository.findByDepartmentId(departmentId); + } + +} diff --git a/backend/src/main/java/wooteco/prolog/member/application/GroupMemberService.java b/backend/src/main/java/wooteco/prolog/member/application/GroupMemberService.java deleted file mode 100644 index cd50ced04..000000000 --- a/backend/src/main/java/wooteco/prolog/member/application/GroupMemberService.java +++ /dev/null @@ -1,20 +0,0 @@ -package wooteco.prolog.member.application; - -import java.util.List; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import wooteco.prolog.member.domain.GroupMember; -import wooteco.prolog.member.domain.repository.GroupMemberRepository; - -@Service -@AllArgsConstructor -@Transactional(readOnly = true) -public class GroupMemberService { - - private GroupMemberRepository groupMemberRepository; - - public List findGroupMemberByGroupId(Long groupId) { - return groupMemberRepository.findByGroupId(groupId); - } -} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/Department.java b/backend/src/main/java/wooteco/prolog/member/domain/Department.java new file mode 100644 index 000000000..23fe87c6a --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/Department.java @@ -0,0 +1,40 @@ +package wooteco.prolog.member.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Department { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(value = EnumType.STRING) + private Part part; + + @Enumerated(value = EnumType.STRING) + private Term term; + + public Department(Long id, Part part, Term term) { + this.id = id; + this.part = part; + this.term = term; + } + + public Department(Long id, String part, String term) { + this.id = id; + this.part = Part.getPartByName(part); + this.term = Term.getTermByName(term); + } + + public Part getPart() { + return part; + } + +} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/GroupMember.java b/backend/src/main/java/wooteco/prolog/member/domain/DepartmentMember.java similarity index 75% rename from backend/src/main/java/wooteco/prolog/member/domain/GroupMember.java rename to backend/src/main/java/wooteco/prolog/member/domain/DepartmentMember.java index 3c6797a56..690ecad41 100644 --- a/backend/src/main/java/wooteco/prolog/member/domain/GroupMember.java +++ b/backend/src/main/java/wooteco/prolog/member/domain/DepartmentMember.java @@ -14,7 +14,7 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class GroupMember { +public class DepartmentMember { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -25,12 +25,12 @@ public class GroupMember { private Member member; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "group_id", nullable = false) - private MemberGroup group; + @JoinColumn(name = "department_id", nullable = false) + private Department department; - public GroupMember(Long id, Member member, MemberGroup group) { + public DepartmentMember(Long id, Member member, Department department) { this.id = id; this.member = member; - this.group = group; + this.department = department; } } diff --git a/backend/src/main/java/wooteco/prolog/member/domain/Departments.java b/backend/src/main/java/wooteco/prolog/member/domain/Departments.java new file mode 100644 index 000000000..04ab1103e --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/Departments.java @@ -0,0 +1,16 @@ +package wooteco.prolog.member.domain; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class Departments { + + private List values; + + public boolean isContainsDepartments(Department department) { + return values.contains(department); + } +} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroup.java b/backend/src/main/java/wooteco/prolog/member/domain/MemberGroup.java deleted file mode 100644 index f45b9dc3f..000000000 --- a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroup.java +++ /dev/null @@ -1,47 +0,0 @@ -package wooteco.prolog.member.domain; - -import static wooteco.prolog.common.exception.BadRequestCode.CANT_FIND_GROUP_TYPE; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import wooteco.prolog.common.exception.BadRequestCode; -import wooteco.prolog.common.exception.BadRequestException; - -@Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -public class MemberGroup { - - private static final String BACKEND = "백엔드"; - private static final String FRONTEND = "프론트엔드"; - private static final String ANDROID = "안드로이드"; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private String description; - - public MemberGroup(Long id, String name, String description) { - this.id = id; - this.name = name; - this.description = description; - } - - public MemberGroupType groupType() { - for (MemberGroupType groupType : MemberGroupType.values()) { - if (groupType.isContainedBy(this.name)) { - return groupType; - } - } - throw new BadRequestException(CANT_FIND_GROUP_TYPE); - } - -} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroupType.java b/backend/src/main/java/wooteco/prolog/member/domain/MemberGroupType.java deleted file mode 100644 index c17b9e9ae..000000000 --- a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroupType.java +++ /dev/null @@ -1,23 +0,0 @@ -package wooteco.prolog.member.domain; - -import lombok.Getter; - -@Getter -public enum MemberGroupType { - ANDROID("안드로이드"), - BACKEND("백엔드"), - FRONTEND("프론트엔드"); - - private final String groupName; - - MemberGroupType(String groupName) { - this.groupName = groupName; - } - - public boolean isContainedBy(String value) { - if (value == null) { - return false; - } - return value.contains(this.groupName); - } -} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroups.java b/backend/src/main/java/wooteco/prolog/member/domain/MemberGroups.java deleted file mode 100644 index 9c9424779..000000000 --- a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroups.java +++ /dev/null @@ -1,16 +0,0 @@ -package wooteco.prolog.member.domain; - -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public class MemberGroups { - - private List values; - - public boolean isContainsMemberGroups(GroupMember groupMember) { - return values.contains(groupMember.getGroup()); - } -} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/Part.java b/backend/src/main/java/wooteco/prolog/member/domain/Part.java new file mode 100644 index 000000000..7f6960af6 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/Part.java @@ -0,0 +1,24 @@ +package wooteco.prolog.member.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum Part { + + BACKEND("백엔드"), + FRONTEND("프론트엔드"), + ANDROID("안드로이드"); + + private final String name; + + public static Part getPartByName(String name) { + return Arrays.stream(values()) + .filter(part -> part.name.equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("name과 일치하는 part가 존재하지 않습니다.")); + } +} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/Term.java b/backend/src/main/java/wooteco/prolog/member/domain/Term.java new file mode 100644 index 000000000..162b67402 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/Term.java @@ -0,0 +1,27 @@ +package wooteco.prolog.member.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum Term { + + FIRST("1기"), + SECOND("2기"), + THIRD("3기"), + FOURTH("4기"), + FIFTH("5기"), + SIXTH("6기"); + + private final String name; + + public static Term getTermByName(String name) { + return Arrays.stream(values()) + .filter(term -> term.name.equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("name과 일치하는 term이 존재하지 않습니다.")); + } +} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/repository/DepartmentMemberRepository.java b/backend/src/main/java/wooteco/prolog/member/domain/repository/DepartmentMemberRepository.java new file mode 100644 index 000000000..b574f4386 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/repository/DepartmentMemberRepository.java @@ -0,0 +1,14 @@ +package wooteco.prolog.member.domain.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.member.domain.DepartmentMember; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Department; + +public interface DepartmentMemberRepository extends JpaRepository { + + List findByDepartmentId(Long departmentId); + + boolean existsDepartmentMemberByMemberAndDepartment(Member member, Department department); +} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/repository/DepartmentRepository.java b/backend/src/main/java/wooteco/prolog/member/domain/repository/DepartmentRepository.java new file mode 100644 index 000000000..08314f19d --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/repository/DepartmentRepository.java @@ -0,0 +1,8 @@ +package wooteco.prolog.member.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.member.domain.Department; + +public interface DepartmentRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/repository/GroupMemberRepository.java b/backend/src/main/java/wooteco/prolog/member/domain/repository/GroupMemberRepository.java deleted file mode 100644 index 5b2c76f1d..000000000 --- a/backend/src/main/java/wooteco/prolog/member/domain/repository/GroupMemberRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package wooteco.prolog.member.domain.repository; - -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; -import wooteco.prolog.member.domain.GroupMember; -import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroup; - -public interface GroupMemberRepository extends JpaRepository { - - List findByGroupId(Long groupId); - - boolean existsGroupMemberByMemberAndGroup(Member member, MemberGroup memberGroup); -} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/repository/MemberGroupRepository.java b/backend/src/main/java/wooteco/prolog/member/domain/repository/MemberGroupRepository.java deleted file mode 100644 index c98165eb2..000000000 --- a/backend/src/main/java/wooteco/prolog/member/domain/repository/MemberGroupRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package wooteco.prolog.member.domain.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import wooteco.prolog.member.domain.MemberGroup; - -public interface MemberGroupRepository extends JpaRepository { - -} diff --git a/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java b/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java index b7635b1b3..9bf2df56c 100644 --- a/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java +++ b/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import wooteco.prolog.common.exception.BadRequestException; -import wooteco.prolog.member.application.GroupMemberService; +import wooteco.prolog.member.application.DepartmentMemberService; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.member.domain.Member; @@ -25,7 +25,7 @@ public class SessionMemberService { private SessionMemberRepository sessionMemberRepository; private SessionRepository sessionRepository; private MemberService memberService; - private GroupMemberService groupMemberService; + private DepartmentMemberService departmentMemberService; @Transactional public void registerMember(Long sessionId, Long memberId) { @@ -42,7 +42,7 @@ public void registerMembersByGroupId(Long sessionId, List alreadySessionMembers = sessionMemberRepository.findAllBySessionId( sessionId); - List members = groupMemberService.findGroupMemberByGroupId( + List members = departmentMemberService.findDepartmentMemberByDepartmentId( sessionGroupMemberRequest.getGroupId()).stream() .map(it -> it.getMember()) .collect(toList()); diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java b/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java index cbe1a5ade..aadf4d314 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java @@ -13,13 +13,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import wooteco.prolog.member.domain.GroupMember; -import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroup; -import wooteco.prolog.member.domain.MemberGroupType; -import wooteco.prolog.member.domain.MemberGroups; -import wooteco.prolog.member.domain.repository.GroupMemberRepository; -import wooteco.prolog.member.domain.repository.MemberGroupRepository; +import wooteco.prolog.member.domain.*; +import wooteco.prolog.member.domain.repository.DepartmentMemberRepository; +import wooteco.prolog.member.domain.repository.DepartmentRepository; import wooteco.prolog.studylog.application.dto.PopularStudylogsResponse; import wooteco.prolog.studylog.application.dto.StudylogResponse; import wooteco.prolog.studylog.application.dto.StudylogsResponse; @@ -39,26 +35,26 @@ public class PopularStudylogService { private final StudylogService studylogService; private final StudylogRepository studylogRepository; private final PopularStudylogRepository popularStudylogRepository; - private final MemberGroupRepository memberGroupRepository; - private final GroupMemberRepository groupMemberRepository; + private final DepartmentRepository departmentRepository; + private final DepartmentMemberRepository departmentMemberRepository; @Transactional public void updatePopularStudylogs(Pageable pageable) { deleteAllLegacyPopularStudylogs(); - List groupMembers = groupMemberRepository.findAll(); - Map> memberGroupsBygroupType = memberGroupRepository.findAll() + List departmentMembers = departmentMemberRepository.findAll(); + Map> DepartmetsBygroupType = departmentRepository.findAll() .stream() - .collect(Collectors.groupingBy(MemberGroup::groupType)); + .collect(Collectors.groupingBy(Department::getPart)); final List recentStudylogs = findRecentStudylogs(LocalDateTime.now(), pageable.getPageSize()); List popularStudylogs = new ArrayList<>(); - for (MemberGroupType groupType : MemberGroupType.values()) { - popularStudylogs.addAll(filterStudylogsByMemberGroups(recentStudylogs, - new MemberGroups(memberGroupsBygroupType.get(groupType)), groupMembers).stream() + for (Part partType : Part.values()) { + popularStudylogs.addAll(filterStudylogsByDepartmets(recentStudylogs, + new Departments(DepartmetsBygroupType.get(partType)), departmentMembers).stream() .sorted(Comparator.comparing(Studylog::getPopularScore).reversed()) .limit(pageable.getPageSize()).collect(toList())); } @@ -89,43 +85,43 @@ private List findRecentStudylogs(final LocalDateTime dateTime, return recentStudylogs; } - private List filterStudylogsByMemberGroups(final List studylogs, - final MemberGroups memberGroups, - final List groupMembers) { + private List filterStudylogsByDepartmets(final List studylogs, + final Departments departments, + final List departmentMembers) { return studylogs.stream() .filter( - studylog -> checkMemberAssignedInMemberGroups(memberGroups, studylog.getMember(), - groupMembers)) + studylog -> checkMemberAssignedInDepartmets(departments, studylog.getMember(), + departmentMembers)) .collect(toList()); } - private boolean checkMemberAssignedInMemberGroups(MemberGroups memberGroups, Member member, - List groupMembers) { - return groupMembers.stream().anyMatch( - it -> it.getMember().equals(member) && memberGroups.isContainsMemberGroups(it)); + private boolean checkMemberAssignedInDepartmets(Departments departments, Member member, + List departmentMembers) { + return departmentMembers.stream().anyMatch( + it -> it.getMember().equals(member) && departments.isContainsDepartments(it.getDepartment())); } public PopularStudylogsResponse findPopularStudylogs(Pageable pageable, Long memberId, boolean isAnonymousMember) { List allPopularStudylogs = getSortedPopularStudyLogs(pageable); - List groupedMembers = groupMemberRepository.findAll(); - Map> memberGroupsBygroupType = memberGroupRepository.findAll() - .stream().collect(Collectors.groupingBy(MemberGroup::groupType)); + List groupedMembers = departmentMemberRepository.findAll(); + Map> DepartmetsBygroupType = departmentRepository.findAll() + .stream().collect(Collectors.groupingBy(Department::getPart)); return PopularStudylogsResponse.of( studylogsResponse(allPopularStudylogs, pageable, memberId), studylogsResponse( - filterStudylogsByMemberGroups(allPopularStudylogs, new MemberGroups(memberGroupsBygroupType.get(MemberGroupType.FRONTEND)), groupedMembers), + filterStudylogsByDepartmets(allPopularStudylogs, new Departments(DepartmetsBygroupType.get(Part.FRONTEND)), groupedMembers), pageable, memberId), studylogsResponse( - filterStudylogsByMemberGroups(allPopularStudylogs, new MemberGroups(memberGroupsBygroupType.get(MemberGroupType.BACKEND)), groupedMembers), + filterStudylogsByDepartmets(allPopularStudylogs, new Departments(DepartmetsBygroupType.get(Part.BACKEND)), groupedMembers), pageable, memberId), studylogsResponse( - filterStudylogsByMemberGroups(allPopularStudylogs, new MemberGroups(memberGroupsBygroupType.get(MemberGroupType.ANDROID)), groupedMembers), + filterStudylogsByDepartmets(allPopularStudylogs, new Departments(DepartmetsBygroupType.get(Part.ANDROID)), groupedMembers), pageable, memberId)); } diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocument.java b/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocument.java index 7a74c40c3..526d41ab3 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocument.java +++ b/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocument.java @@ -41,6 +41,6 @@ public class StudylogDocument { @Field(type = FieldType.Text) private String username; - @Field(type = FieldType.Text) + @Field(type = FieldType.Date, format = DateFormat.date_optional_time) private LocalDateTime dateTime; } diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocumentQueryBuilder.java b/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocumentQueryBuilder.java index 46d20d8d0..6358bcdff 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocumentQueryBuilder.java +++ b/backend/src/main/java/wooteco/prolog/studylog/domain/StudylogDocumentQueryBuilder.java @@ -66,7 +66,7 @@ private static void makeBoolQuery( .must(defaultField(username, "username")) // .must(defaultField(levels, "levelId")) // .must(defaultField(missions, "missionId")) - .filter(rangeQuery(start, end)) +// .filter(rangeQuery(start, end)) ); } @@ -96,7 +96,6 @@ private static RangeQueryBuilder rangeQuery(LocalDate start, LocalDate end) { if (Objects.isNull(end)) { end = LocalDate.parse("99991231", DateTimeFormatter.BASIC_ISO_DATE); } - return QueryBuilders.rangeQuery("dateTime") .from(start) .to(end); diff --git a/backend/src/main/resources/db/migration/prod/V9__alter_table_group_and_member.sql b/backend/src/main/resources/db/migration/prod/V9__alter_table_group_and_member.sql new file mode 100644 index 000000000..6766b3a8e --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V9__alter_table_group_and_member.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS department +( + id bigint auto_increment primary key, + part varchar(50) not null, + term varchar(50) not null +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +create table if not exists department_member +( + id bigint auto_increment primary key, + member_id bigint not null, + department_id bigint not null, + constraint FK_DEPARTMENT_MEMBER_ON_MEMBERㅇ + foreign key (member_id) references prolog.member (id), + constraint FK_DEPARTMENT_MEMBER_ON_DEPARTMENT + foreign key (department_id) references prolog.department (id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +insert into department(id, part, term) values (1, '백엔드', '3기'); +insert into department(id, part, term) values (2, '프론트엔드', '3기'); +insert into department(id, part, term) values (3, '백엔드', '4기'); +insert into department(id, part, term) values (4, '프론트엔드', '4기'); +insert into department(id, part, term) values (5, '백엔드', '5기'); +insert into department(id, part, term) values (6, '프론트엔드', '5기'); +insert into department(id, part, term) values (7, '안드로이드', '5기'); + +insert into department_member (id, member_id, department_id) + (select id, member_id, group_id from group_member); diff --git a/backend/src/test/java/wooteco/prolog/member/application/DepartmentMemberServiceTest.java b/backend/src/test/java/wooteco/prolog/member/application/DepartmentMemberServiceTest.java new file mode 100644 index 000000000..f95ed8e80 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/member/application/DepartmentMemberServiceTest.java @@ -0,0 +1,50 @@ +package wooteco.prolog.member.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static wooteco.prolog.member.domain.Part.*; +import static wooteco.prolog.member.domain.Term.*; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import wooteco.prolog.member.domain.*; +import wooteco.prolog.member.domain.repository.DepartmentMemberRepository; + +@ExtendWith(MockitoExtension.class) +class DepartmentMemberServiceTest { + + @Mock + private DepartmentMemberRepository departmentMemberRepository; + + @InjectMocks + private DepartmentMemberService departmentMemberService; + + @DisplayName("GroupId로 GroupMember를 찾는다.") + @Test + void findDepartmentMemberByDepartmentId() { + //given + final Long memberId = 1L; + final Long DepartmentId = 2L; + final Long groupMemberId = 3L; + + final Member member = new Member(memberId, "송세연", "아마란스", Role.CREW, 1523L, "image"); + final Department department = new Department(DepartmentId, BACKEND, FIFTH); + final DepartmentMember departmentMember = new DepartmentMember(groupMemberId, member, department); + + when(departmentMemberRepository.findByDepartmentId(any())).thenReturn(ImmutableList.of(departmentMember)); + + //when + final List departmentMembers = departmentMemberService.findDepartmentMemberByDepartmentId( + DepartmentId); + + //then + assertThat(departmentMembers).containsExactly(departmentMember); + } +} diff --git a/backend/src/test/java/wooteco/prolog/member/application/GroupMemberServiceTest.java b/backend/src/test/java/wooteco/prolog/member/application/GroupMemberServiceTest.java deleted file mode 100644 index a15d4ff53..000000000 --- a/backend/src/test/java/wooteco/prolog/member/application/GroupMemberServiceTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package wooteco.prolog.member.application; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import com.google.common.collect.ImmutableList; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import wooteco.prolog.member.domain.GroupMember; -import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroup; -import wooteco.prolog.member.domain.Role; -import wooteco.prolog.member.domain.repository.GroupMemberRepository; - -@ExtendWith(MockitoExtension.class) -class GroupMemberServiceTest { - - @Mock - private GroupMemberRepository groupMemberRepository; - - @InjectMocks - private GroupMemberService groupMemberService; - - @DisplayName("GroupId로 GroupMember를 찾는다.") - @Test - void findGroupMemberByGroupId() { - //given - final Long memberId = 1L; - final Long memberGroupId = 2L; - final Long groupMemberId = 3L; - - final Member member = new Member(memberId, "송세연", "아마란스", Role.CREW, 1523L, "image"); - final MemberGroup memberGroup = new MemberGroup(memberGroupId, "백엔드", "2023 백엔드"); - final GroupMember groupMember = new GroupMember(groupMemberId, member, memberGroup); - - when(groupMemberRepository.findByGroupId(any())).thenReturn(ImmutableList.of(groupMember)); - - //when - final List groupMembers = groupMemberService.findGroupMemberByGroupId( - memberGroupId); - - //then - assertThat(groupMembers).containsExactly(groupMember); - } -} diff --git a/backend/src/test/java/wooteco/prolog/member/domain/DepartmentTest.java b/backend/src/test/java/wooteco/prolog/member/domain/DepartmentTest.java new file mode 100644 index 000000000..638cb356e --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/member/domain/DepartmentTest.java @@ -0,0 +1,31 @@ +package wooteco.prolog.member.domain; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static wooteco.prolog.member.domain.Part.*; +import static wooteco.prolog.member.domain.Term.*; + +import org.junit.jupiter.api.Test; + +class DepartmentTest { + + private static final Department ANDROID_DEPARTMENT = new Department(null, ANDROID, FIFTH); + private static final Department BACKEND_DEPARTMENT = new Department(null, BACKEND, FIFTH); + private static final Department FRONTEND_DEPARTMENT = new Department(null, FRONTEND, FOURTH); + + @Test + void getPartType_이름이_그룹명을_포함하면_그룹을_반환한다() { + assertThat(ANDROID_DEPARTMENT.getPart()).isEqualTo(ANDROID); + assertThat(BACKEND_DEPARTMENT.getPart()).isEqualTo(BACKEND); + assertThat(FRONTEND_DEPARTMENT.getPart()).isEqualTo(FRONTEND); + } + + @Test + void getPartType_이름이_포함하는_그룹명이_없으면_예외가_발생한다() { + + assertThatThrownBy(() -> new Department(null, "테스트", "test")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("name과 일치하는 part가 존재하지 않습니다."); + } + +} diff --git a/backend/src/test/java/wooteco/prolog/member/domain/MemberGroupTest.java b/backend/src/test/java/wooteco/prolog/member/domain/MemberGroupTest.java deleted file mode 100644 index 702640ade..000000000 --- a/backend/src/test/java/wooteco/prolog/member/domain/MemberGroupTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package wooteco.prolog.member.domain; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -import org.junit.jupiter.api.Test; -import wooteco.prolog.common.exception.BadRequestCode; -import wooteco.prolog.common.exception.BadRequestException; - -class MemberGroupTest { - - private static final MemberGroup ANDROID_GROUP = new MemberGroup(null, " 안드로이드 5기", "A"); - private static final MemberGroup BACKEND_GROUP = new MemberGroup(null, " 백엔드 5기", "B"); - private static final MemberGroup FRONTEND_GROUP = new MemberGroup(null, " 프론트엔드 5기", "F"); - - @Test - void getGroupType_이름이_그룹명을_포함하면_그룹을_반환한다() { - assertThat(ANDROID_GROUP.groupType()).isEqualTo(MemberGroupType.ANDROID); - assertThat(BACKEND_GROUP.groupType()).isEqualTo(MemberGroupType.BACKEND); - assertThat(FRONTEND_GROUP.groupType()).isEqualTo(MemberGroupType.FRONTEND); - } - - @Test - void getGroupType_이름이_포함하는_그룹명이_없으면_예외가_발생한다() { - MemberGroup memberGroup = new MemberGroup(null, "테스트", "test"); - - assertThatThrownBy(memberGroup::groupType) - .isInstanceOf(BadRequestException.class) - .hasMessage("해당 그룹의 타입을 결정할 수 없습니다."); - } - -} diff --git a/backend/src/test/java/wooteco/prolog/member/domain/MemberGroupTypeTest.java b/backend/src/test/java/wooteco/prolog/member/domain/MemberGroupTypeTest.java deleted file mode 100644 index 102652e26..000000000 --- a/backend/src/test/java/wooteco/prolog/member/domain/MemberGroupTypeTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package wooteco.prolog.member.domain; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; - -class MemberGroupTypeTest { - - @ParameterizedTest - @EnumSource(value = MemberGroupType.class) - void isContainedBy_null은_그룹명을_포함하지_않는다(MemberGroupType memberGroupType) { - assertThat(memberGroupType.isContainedBy(null)).isFalse(); - } - - @Test - void isContainedBy() { - assertThat(MemberGroupType.ANDROID.isContainedBy("안드로이드 5기")).isTrue(); - assertThat(MemberGroupType.ANDROID.isContainedBy("프론트엔드 5기")).isFalse(); - assertThat(MemberGroupType.ANDROID.isContainedBy("백엔드 5기")).isFalse(); - - assertThat(MemberGroupType.FRONTEND.isContainedBy("안드로이드 5기")).isFalse(); - assertThat(MemberGroupType.FRONTEND.isContainedBy("프론트엔드 5기")).isTrue(); - assertThat(MemberGroupType.FRONTEND.isContainedBy("백엔드 5기")).isFalse(); - - assertThat(MemberGroupType.BACKEND.isContainedBy("안드로이드 5기")).isFalse(); - assertThat(MemberGroupType.BACKEND.isContainedBy("프론트엔드 5기")).isFalse(); - assertThat(MemberGroupType.BACKEND.isContainedBy("백엔드 5기")).isTrue(); - } -} diff --git a/backend/src/test/java/wooteco/prolog/member/domain/repository/DepartmentMemberRepositoryTest.java b/backend/src/test/java/wooteco/prolog/member/domain/repository/DepartmentMemberRepositoryTest.java new file mode 100644 index 000000000..ae789f84e --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/member/domain/repository/DepartmentMemberRepositoryTest.java @@ -0,0 +1,43 @@ +package wooteco.prolog.member.domain.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.prolog.member.domain.Part.*; +import static wooteco.prolog.member.domain.Term.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import wooteco.prolog.member.domain.*; +import wooteco.support.utils.RepositoryTest; + +@RepositoryTest +class DepartmentMemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + @Autowired + private DepartmentMemberRepository departmentMemberRepository; + @Autowired + private DepartmentRepository departmentRepository; + + @Test + @DisplayName("작성된 studylog의 Member가 DepartmentMember의 Department에 포함되는 경우 true를 반환한다.") + void existsDepartmentMemberByMemberAndDepartment() { + // given + Member saveMember = memberRepository.save( + new Member("username", "nickname", Role.CREW, 1L, "imageUrl")); + Department saveDepartment = departmentRepository.save( + new Department(null, FRONTEND, FIFTH) + ); + departmentMemberRepository.save( + new DepartmentMember(null, saveMember, saveDepartment) + ); + + // when + boolean extract = departmentMemberRepository.existsDepartmentMemberByMemberAndDepartment( + saveMember, saveDepartment); + + // then + assertThat(extract).isTrue(); + } +} diff --git a/backend/src/test/java/wooteco/prolog/member/domain/repository/GroupMemberRepositoryTest.java b/backend/src/test/java/wooteco/prolog/member/domain/repository/GroupMemberRepositoryTest.java deleted file mode 100644 index ef64b761d..000000000 --- a/backend/src/test/java/wooteco/prolog/member/domain/repository/GroupMemberRepositoryTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package wooteco.prolog.member.domain.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import wooteco.prolog.member.domain.GroupMember; -import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroup; -import wooteco.prolog.member.domain.Role; -import wooteco.support.utils.RepositoryTest; - -@RepositoryTest -class GroupMemberRepositoryTest { - - @Autowired - private MemberRepository memberRepository; - @Autowired - private GroupMemberRepository groupMemberRepository; - @Autowired - private MemberGroupRepository memberGroupRepository; - - @Test - @DisplayName("작성된 studylog의 Member가 GroupMember의 MemberGroup에 포함되는 경우 true를 반환한다.") - void existsGroupMemberByMemberAndMemberGroup() { - // given - Member saveMember = memberRepository.save( - new Member("username", "nickname", Role.CREW, 1L, "imageUrl")); - MemberGroup saveMemberGroup = memberGroupRepository.save( - new MemberGroup(null, "프론트엔드", "프론트엔드 설명") - ); - groupMemberRepository.save( - new GroupMember(null, saveMember, saveMemberGroup) - ); - - // when - boolean extract = groupMemberRepository.existsGroupMemberByMemberAndGroup( - saveMember, saveMemberGroup); - - // then - assertThat(extract).isTrue(); - } -} diff --git a/backend/src/test/java/wooteco/prolog/session/application/SessionMemberServiceTest.java b/backend/src/test/java/wooteco/prolog/session/application/SessionMemberServiceTest.java index 50ee17c60..5327feb44 100644 --- a/backend/src/test/java/wooteco/prolog/session/application/SessionMemberServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/session/application/SessionMemberServiceTest.java @@ -20,7 +20,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import wooteco.prolog.common.exception.BadRequestException; -import wooteco.prolog.member.application.GroupMemberService; +import wooteco.prolog.member.application.DepartmentMemberService; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.member.domain.Member; @@ -45,7 +45,7 @@ class SessionMemberServiceTest { private MemberService memberService; @Mock - private GroupMemberService groupMemberService; + private DepartmentMemberService departmentMemberService; @DisplayName("Member가 회원가입을 할 수 있어야 한다.") @Test @@ -89,7 +89,7 @@ void registerMembersByGroupId() { // then verify(sessionMemberRepository, atMostOnce()).findAllBySessionId(1L); - verify(groupMemberService, atMostOnce()).findGroupMemberByGroupId(request.getGroupId()); + verify(departmentMemberService, atMostOnce()).findDepartmentMemberByDepartmentId(request.getGroupId()); verify(sessionMemberRepository, atMostOnce()).saveAll(null); } diff --git a/backend/src/test/java/wooteco/prolog/studylog/application/PopularStudylogServiceTest.java b/backend/src/test/java/wooteco/prolog/studylog/application/PopularStudylogServiceTest.java index 0dedf34bd..86e338c64 100644 --- a/backend/src/test/java/wooteco/prolog/studylog/application/PopularStudylogServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/studylog/application/PopularStudylogServiceTest.java @@ -20,12 +20,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; -import wooteco.prolog.member.domain.GroupMember; +import wooteco.prolog.member.domain.DepartmentMember; import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroup; +import wooteco.prolog.member.domain.Department; import wooteco.prolog.member.domain.Role; -import wooteco.prolog.member.domain.repository.GroupMemberRepository; -import wooteco.prolog.member.domain.repository.MemberGroupRepository; +import wooteco.prolog.member.domain.repository.DepartmentMemberRepository; +import wooteco.prolog.member.domain.repository.DepartmentRepository; import wooteco.prolog.session.domain.Mission; import wooteco.prolog.session.domain.Session; import wooteco.prolog.studylog.application.dto.PopularStudylogsResponse; @@ -46,9 +46,9 @@ class PopularStudylogServiceTest { @Mock private PopularStudylogRepository popularStudylogRepository; @Mock - private MemberGroupRepository memberGroupRepository; + private DepartmentRepository departmentRepository; @Mock - private GroupMemberRepository groupMemberRepository; + private DepartmentMemberRepository departmentMemberRepository; @InjectMocks private PopularStudylogService popularStudylogService; @@ -56,15 +56,15 @@ class PopularStudylogServiceTest { @Test void enoughWhileTwoCycle() { //given - final MemberGroup frontend = setUpMemberGroup("5기 프론트엔드", "5기 프론트엔드 설명"); - final MemberGroup backend = setUpMemberGroup("5기 백엔드", "5기 백엔드 설명"); - final MemberGroup android = setUpMemberGroup("5기 안드로이드", "5기 안드로이드 설명"); + final Department frontend = setUpDepartment("프론트엔드", "5기"); + final Department backend = setUpDepartment("백엔드", "5기"); + final Department android = setUpDepartment("안드로이드", "5기"); final Member split = setUpMember(1L, "박상현", "스플릿", 1L); final Member journey = setUpMember(2L, "이지원", "져니", 2L); - final GroupMember splitGroupMember = setUpGroupMember(split, backend); - final GroupMember journeyGroupMember = setUpGroupMember(journey, backend); + final DepartmentMember splitDepartmentMember = setUpDepartmentMember(split, backend); + final DepartmentMember journeyDepartmentMember = setUpDepartmentMember(journey, backend); final Studylog splitStudyLog = setUpStudyLog(split); final Studylog journeyStudylog = setUpStudyLog(journey); @@ -75,16 +75,16 @@ void enoughWhileTwoCycle() { journeyStudylog.getId()); final PageRequest pageRequest = PageRequest.of(1, 2); - final List memberGroups = Arrays.asList(frontend, backend, android); - final List groupMembers = Arrays.asList(splitGroupMember, - journeyGroupMember); + final List departments = Arrays.asList(frontend, backend, android); + final List departmentMembers = Arrays.asList(splitDepartmentMember, + journeyDepartmentMember); final List studylogs = Arrays.asList(splitStudyLog, journeyStudylog); final List popularStudylogs = Arrays.asList(splitPopularStudylog, journeyPopularStudylog); - when(groupMemberRepository.findAll()).thenReturn(groupMembers); - when(memberGroupRepository.findAll()).thenReturn(memberGroups); + when(departmentMemberRepository.findAll()).thenReturn(departmentMembers); + when(departmentRepository.findAll()).thenReturn(departments); when(studylogRepository.findByPastDays(any())).thenReturn(studylogs); when(popularStudylogRepository.findAllByDeletedFalse()).thenReturn(popularStudylogs); when(popularStudylogRepository.saveAll(any())).thenReturn(popularStudylogs); @@ -100,15 +100,15 @@ void enoughWhileTwoCycle() { @Test void notEnoughWhileTwoCycle() { //given - final MemberGroup frontend = setUpMemberGroup("5기 프론트엔드", "5기 프론트엔드 설명"); - final MemberGroup backend = setUpMemberGroup("5기 백엔드", "5기 백엔드 설명"); - final MemberGroup android = setUpMemberGroup("5기 안드로이드", "5기 안드로이드 설명"); + final Department frontend = setUpDepartment("프론트엔드", "5기"); + final Department backend = setUpDepartment("백엔드", "5기"); + final Department android = setUpDepartment("안드로이드", "5기"); final Member split = setUpMember(1L, "박상현", "스플릿", 1L); final Member journey = setUpMember(2L, "이지원", "져니", 2L); - final GroupMember splitGroupMember = setUpGroupMember(split, backend); - final GroupMember journeyGroupMember = setUpGroupMember(journey, backend); + final DepartmentMember splitDepartmentMember = setUpDepartmentMember(split, backend); + final DepartmentMember journeyDepartmentMember = setUpDepartmentMember(journey, backend); final Studylog splitStudylog = setUpStudyLog(split); final Studylog journeyStudylog = setUpStudyLog(journey); @@ -119,15 +119,15 @@ void notEnoughWhileTwoCycle() { journeyStudylog.getId()); final PageRequest pageRequest = PageRequest.of(1, 5); - final List memberGroups = Arrays.asList(frontend, backend, android); - final List groupMembers = Arrays.asList(splitGroupMember, - journeyGroupMember); + final List departments = Arrays.asList(frontend, backend, android); + final List departmentMembers = Arrays.asList(splitDepartmentMember, + journeyDepartmentMember); final List studylogs = Arrays.asList(splitStudylog, journeyStudylog); final List popularStudylogs = Arrays.asList(splitPopularStudylog, journeyPopularStudylog); - when(groupMemberRepository.findAll()).thenReturn(groupMembers); - when(memberGroupRepository.findAll()).thenReturn(memberGroups); + when(departmentMemberRepository.findAll()).thenReturn(departmentMembers); + when(departmentRepository.findAll()).thenReturn(departments); when(studylogRepository.findByPastDays(any())).thenReturn(studylogs); when(popularStudylogRepository.findAllByDeletedFalse()).thenReturn(popularStudylogs); when(popularStudylogRepository.saveAll(any())).thenReturn(popularStudylogs); @@ -142,15 +142,15 @@ void notEnoughWhileTwoCycle() { @DisplayName("익명의 사용자일 경우 스크랩과 읽음 여부가 표시하지 않고 학습로그를 조회한다.") @Test void findPopularStudylogs_IsAnonymousMemberTrue() { - final MemberGroup frontend = setUpMemberGroup("5기 프론트엔드", "5기 프론트엔드 설명"); - final MemberGroup backend = setUpMemberGroup("5기 백엔드", "5기 백엔드 설명"); - final MemberGroup android = setUpMemberGroup("5기 안드로이드", "5기 안드로이드 설명"); + final Department frontend = setUpDepartment("프론트엔드", "5기"); + final Department backend = setUpDepartment("백엔드", "5기"); + final Department android = setUpDepartment("안드로이드", "5기"); final Member split = setUpMember(1L, "박상현", "스플릿", 1L); final Member journey = setUpMember(2L, "이지원", "져니", 2L); - final GroupMember splitGroupMember = setUpGroupMember(split, backend); - final GroupMember journeyGroupMember = setUpGroupMember(journey, backend); + final DepartmentMember splitDepartmentMember = setUpDepartmentMember(split, backend); + final DepartmentMember journeyDepartmentMember = setUpDepartmentMember(journey, backend); final Studylog splitStudylog = setUpStudyLog(split); final Studylog journeyStudylog = setUpStudyLog(journey); @@ -163,14 +163,14 @@ void findPopularStudylogs_IsAnonymousMemberTrue() { final List studylogs = Arrays.asList(splitStudylog, journeyStudylog); final PageRequest pageRequest = PageRequest.of(0, 1); final Page pages = new PageImpl<>(studylogs, pageRequest, 2); - final List memberGroups = Arrays.asList(frontend, backend, android); - final List groupMembers = Arrays.asList(splitGroupMember, - journeyGroupMember); + final List departments = Arrays.asList(frontend, backend, android); + final List departmentMembers = Arrays.asList(splitDepartmentMember, + journeyDepartmentMember); final List popularStudylogs = Arrays.asList(splitPopularStudylog, journeyPopularStudylog); - when(groupMemberRepository.findAll()).thenReturn(groupMembers); - when(memberGroupRepository.findAll()).thenReturn(memberGroups); + when(departmentMemberRepository.findAll()).thenReturn(departmentMembers); + when(departmentRepository.findAll()).thenReturn(departments); when(studylogRepository.findAllByIdIn(any(), any())).thenReturn(pages); when(popularStudylogRepository.findAllByDeletedFalse()).thenReturn(popularStudylogs); @@ -218,15 +218,15 @@ void findPopularStudylogs_IsAnonymousMemberTrue() { void findPopularStudylogs_IsAnonymousMemberFalse() { { //given - final MemberGroup frontend = setUpMemberGroup("5기 프론트엔드", "5기 프론트엔드 설명"); - final MemberGroup backend = setUpMemberGroup("5기 백엔드", "5기 백엔드 설명"); - final MemberGroup android = setUpMemberGroup("5기 안드로이드", "5기 안드로이드 설명"); + final Department frontend = setUpDepartment("프론트엔드", "5기"); + final Department backend = setUpDepartment("백엔드", "5기"); + final Department android = setUpDepartment("안드로이드", "5기"); final Member split = setUpMember(1L, "박상현", "스플릿", 1L); final Member journey = setUpMember(2L, "이지원", "져니", 2L); - final GroupMember splitGroupMember = setUpGroupMember(split, backend); - final GroupMember journeyGroupMember = setUpGroupMember(journey, backend); + final DepartmentMember splitDepartmentMember = setUpDepartmentMember(split, backend); + final DepartmentMember journeyDepartmentMember = setUpDepartmentMember(journey, backend); final Studylog splitStudylog = setUpStudyLog(split); final Studylog journeyStudylog = setUpStudyLog(journey); @@ -234,12 +234,12 @@ void findPopularStudylogs_IsAnonymousMemberFalse() { final List studylogs = Arrays.asList(splitStudylog, journeyStudylog); final PageRequest pageRequest = PageRequest.of(0, 1); final Page pages = new PageImpl<>(studylogs, pageRequest, 2); - final List memberGroups = Arrays.asList(frontend, backend, android); - final List groupMembers = Arrays.asList(splitGroupMember, - journeyGroupMember); + final List departments = Arrays.asList(frontend, backend, android); + final List departmentMembers = Arrays.asList(splitDepartmentMember, + journeyDepartmentMember); - when(groupMemberRepository.findAll()).thenReturn(groupMembers); - when(memberGroupRepository.findAll()).thenReturn(memberGroups); + when(departmentMemberRepository.findAll()).thenReturn(departmentMembers); + when(departmentRepository.findAll()).thenReturn(departments); when(studylogRepository.findAllByIdIn(any(), any())).thenReturn(pages); when(studylogService.findScrapIds(any())).thenReturn(Arrays.asList(1L, 2L)); doNothing().when(studylogService).updateScrap(any(), any()); @@ -288,20 +288,20 @@ void findPopularStudylogs_IsAnonymousMemberFalse() { @DisplayName("인기학습 로그를 분야별로 나누어서 반한한다.") @Test - void findPopularStudylogs_filterGroupType() { + void findPopularStudylogs_filterDepartmentType() { { //given - final MemberGroup frontend = setUpMemberGroup("5기 프론트엔드", "5기 프론트엔드 설명"); - final MemberGroup backend = setUpMemberGroup("5기 백엔드", "5기 백엔드 설명"); - final MemberGroup android = setUpMemberGroup("5기 안드로이드", "5기 안드로이드 설명"); + final Department frontend = setUpDepartment("프론트엔드", "5기"); + final Department backend = setUpDepartment("백엔드", "5기"); + final Department android = setUpDepartment("안드로이드", "5기"); final Member split = setUpMember(1L, "박상현", "스플릿", 1L); final Member journey = setUpMember(2L, "이지원", "져니", 2L); final Member pooh = setUpMember(3L, "백승준", "푸우", 3L); - final GroupMember splitGroupMember = setUpGroupMember(split, frontend); - final GroupMember journeyGroupMember = setUpGroupMember(journey, backend); - final GroupMember poohGroupMember = setUpGroupMember(pooh, android); + final DepartmentMember splitDepartmentMember = setUpDepartmentMember(split, frontend); + final DepartmentMember journeyDepartmentMember = setUpDepartmentMember(journey, backend); + final DepartmentMember poohDepartmentMember = setUpDepartmentMember(pooh, android); final Studylog splitStudylog = setUpStudyLog(split); final Studylog journeyStudylog = setUpStudyLog(journey); @@ -310,8 +310,8 @@ void findPopularStudylogs_filterGroupType() { final List studylogs = Arrays.asList(splitStudylog, journeyStudylog, poohStudylog); final PageRequest pageRequest = PageRequest.of(0, 3); final Page pages = new PageImpl<>(studylogs, pageRequest, 1); - final List memberGroups = Arrays.asList(frontend, backend, android); - final List groupMembers = Arrays.asList(splitGroupMember, journeyGroupMember, poohGroupMember); + final List departments = Arrays.asList(frontend, backend, android); + final List departmentMembers = Arrays.asList(splitDepartmentMember, journeyDepartmentMember, poohDepartmentMember); final PopularStudylog splitPopularStudylog = setUpPopularStudylog(1L, splitStudylog.getId()); @@ -324,8 +324,8 @@ void findPopularStudylogs_filterGroupType() { journeyPopularStudylog, poohPopularStudylog); when(popularStudylogRepository.findAllByDeletedFalse()).thenReturn(popularStudylogs); - when(groupMemberRepository.findAll()).thenReturn(groupMembers); - when(memberGroupRepository.findAll()).thenReturn(memberGroups); + when(departmentMemberRepository.findAll()).thenReturn(departmentMembers); + when(departmentRepository.findAll()).thenReturn(departments); when(studylogRepository.findAllByIdIn(any(), any())).thenReturn(pages); //when @@ -366,8 +366,8 @@ void findPopularStudylogs_filterGroupType() { } - private MemberGroup setUpMemberGroup(final String name, final String description) { - return new MemberGroup(null, name, description); + private Department setUpDepartment(final String part, final String term) { + return new Department(null, part, term); } private Member setUpMember(final Long id, final String userName, @@ -375,8 +375,8 @@ private Member setUpMember(final Long id, final String userName, return new Member(id, userName, nickname, Role.CREW, githubId, "image url"); } - private GroupMember setUpGroupMember(final Member member, final MemberGroup memberGroup) { - return new GroupMember(null, member, memberGroup); + private DepartmentMember setUpDepartmentMember(final Member member, final Department department) { + return new DepartmentMember(null, member, department); } private Studylog setUpStudyLog(final Member member) { diff --git a/frontend/src/apis/essayanswers.ts b/frontend/src/apis/essayanswers.ts index cab66b4e4..6ba6b8cde 100644 --- a/frontend/src/apis/essayanswers.ts +++ b/frontend/src/apis/essayanswers.ts @@ -1,12 +1,34 @@ import { AxiosPromise, AxiosResponse } from 'axios'; import { client } from '.'; -import { EssayAnswerRequest, EssayEditRequest } from '../models/EssayAnswers'; +import { + EssayAnswerListRequest, + EssayAnswerRequest, + EssayEditRequest, +} from '../models/EssayAnswers'; import { Quiz } from '../models/Keywords'; export const createNewEssayAnswerRequest = (body: EssayAnswerRequest) => client.post(`/essay-answers`, body); -export const requestGetEssayAnswer = async (essayAnswerId) => { +export const requestGetEssayAnswers = async (params: EssayAnswerListRequest) => { + const { quizIds, memberIds, ...otherParams } = params; + + const axiosParams: Omit & { + quizIds?: string; + memberIds?: string; + } = { ...otherParams }; + + if (quizIds) axiosParams.quizIds = quizIds.join(','); + if (memberIds) axiosParams.memberIds = memberIds.join(','); + + const { data } = await client.get('/essay-answers', { + params: axiosParams, + }); + + return data; +}; + +export const requestGetEssayAnswer = async (essayAnswerId: number) => { const { data } = await client.get(`/essay-answers/${essayAnswerId}`); return data; }; @@ -15,17 +37,17 @@ export const requestEditEssayAnswer = async (essayAnswerId: number, body: EssayE await client.patch(`/essay-answers/${essayAnswerId}`, body); }; -export const requestDeleteEssayAnswer = async (essayAnswerId) => { +export const requestDeleteEssayAnswer = async (essayAnswerId: number) => { await client.delete(`/essay-answers/${essayAnswerId}`); }; -export const requestGetEssayAnswerList = async (quizId) => { +export const requestGetQuizAnswers = async (quizId: number) => { const { data } = await client.get(`/quizzes/${quizId}/essay-answers`); return data; }; -export const requestGetQuizAsync = async (quizId) => { +export const requestGetQuizAsync = async (quizId: number) => { const { data } = await client.get(`/quizzes/${quizId}`); return data; @@ -33,3 +55,10 @@ export const requestGetQuizAsync = async (quizId) => { export const requestGetQuiz = (quizId: Number): AxiosPromise> => client.get>(`/quizzes/${quizId}`); + +export const requestGetQuizzes = async (curriculumId: number) => { + const { data } = await client.get<{ id: number; question: string }[]>( + `/curriculums/${curriculumId}/quizzes` + ); + return data; +}; diff --git a/frontend/src/apis/filter.ts b/frontend/src/apis/filter.ts new file mode 100644 index 000000000..330950aba --- /dev/null +++ b/frontend/src/apis/filter.ts @@ -0,0 +1,11 @@ +import { client } from '.'; +import { FilterResponse } from '../models/filter'; +import { Author } from '../models/Studylogs'; + +export const getMembersForFilter = async (): Promise => { + const { + data: { members }, + } = await client.get(`/filters`); + + return members; +}; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.stories.js b/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx similarity index 56% rename from frontend/src/components/DropdownMenu/DropdownMenu.stories.js rename to frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx index 784c59559..fcc5e656a 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.stories.js +++ b/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx @@ -1,12 +1,16 @@ -import DropdownMenu from './DropdownMenu'; +/** @jsxImportSource @emotion/react */ + +import DropdownMenu, { DropdownMenuProps } from './DropdownMenu'; +import { Story, Meta } from '@storybook/react'; +import { css } from '@emotion/react'; export default { title: 'Component/DropdownMenu', component: DropdownMenu, argTypes: { children: { control: 'text' } }, -}; +} as Meta; -const Template = (args) => ; +const Template: Story> = (args) => ; export const Basic = Template.bind({}); @@ -24,5 +28,5 @@ Basic.args = { ), - onLogoClick: () => {}, + css: css``, }; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts b/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts index 2fb8589dc..0e59d6838 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts +++ b/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts @@ -1,11 +1,9 @@ +import { InterpolationWithTheme } from '@emotion/core'; +import { Theme } from '@emotion/react'; import styled from '@emotion/styled'; import COLOR from '../../constants/color'; -import { css } from '@emotion/react'; -import MEDIA_QUERY from '../../constants/mediaQuery'; -const Container = styled.div<{ - css: ReturnType; -}>` +const Container = styled.div<{ css?: InterpolationWithTheme }>` height: fit-content; max-height: 32rem; white-space: nowrap; @@ -13,60 +11,15 @@ const Container = styled.div<{ background-color: ${COLOR.WHITE}; border-radius: 1.2rem; box-shadow: 0px 0px 6px ${COLOR.BLACK_OPACITY_300}; + margin-top: 1rem; padding: 1rem 1.2rem; position: absolute; z-index: 100; - right: 30px; - top: 50px; - - ${MEDIA_QUERY.xs} { - right: 10px; - top: 40px; - } - /* transform: translateY(30%); */ && { - ${(props) => props.css} - } - - /* &:before { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: -1; - content: ' '; - background-color: rgba(0, 0, 0, 0.4); - } */ - - /* 삼각형 입니다 ^^ - &:before { - content: ''; - position: absolute; - border-style: solid; - border-width: 0 14px 15px; - border-color: #ffffff transparent; - display: block; - width: 0; - z-index: 0; - top: -14px; - right: 16px; + ${({ css }) => css} } - &:after { - content: ''; - position: absolute; - border-style: solid; - border-width: 0 14px 15px; - border-color: #ffffff transparent; - display: block; - width: 0; - z-index: 0; - top: -14px; - right: 16px; - } */ - & li { height: 4rem; display: flex; @@ -87,12 +40,6 @@ const Container = styled.div<{ & li:not(:last-child) { border-bottom: 0.7px solid ${COLOR.LIGHT_GRAY_700}; } - - /* & li:hover { - & > * { - font-size: 2.2rem; - } - } */ `; export { Container }; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.styles.tsx b/frontend/src/components/DropdownMenu/DropdownMenu.styles.tsx deleted file mode 100644 index 499ae1e5a..000000000 --- a/frontend/src/components/DropdownMenu/DropdownMenu.styles.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import styled from '@emotion/styled'; -import COLOR from '../../constants/color'; - -const Container = styled.div<{ css: {} }>` - height: fit-content; - max-height: 32rem; - white-space: nowrap; - overflow-y: auto; - background-color: ${COLOR.WHITE}; - border-radius: 1.2rem; - box-shadow: 0px 0px 6px ${COLOR.BLACK_OPACITY_300}; - padding: 1rem 1.2rem; - position: absolute; - z-index: 100; - - && { - ${(props) => props.css} - } - - & li { - height: 4rem; - display: flex; - align-items: center; - padding: 0 0.8rem; - width: 100%; - - & > * { - width: 100%; - font-size: 1.4rem; - font-weight: 500; - color: ${COLOR.DARK_GRAY_900}; - transition: font-size 0.1s ease; - text-align: left; - } - } - - & li:not(:last-child) { - border-bottom: 0.7px solid ${COLOR.LIGHT_GRAY_700}; - } -`; - -export { Container }; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.tsx b/frontend/src/components/DropdownMenu/DropdownMenu.tsx index dd834dfb7..4e0aa7c5d 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.tsx +++ b/frontend/src/components/DropdownMenu/DropdownMenu.tsx @@ -1,12 +1,15 @@ -import PropTypes from 'prop-types'; +/** @jsxImportSource @emotion/react */ + +// import PropTypes from 'prop-types'; import { Container } from './DropdownMenu.styles'; +import { css } from '@emotion/react'; -const DropdownMenu = ({ children, css }) => { - return {children}; -}; +export interface DropdownMenuProps { + css?: ReturnType; +} -DropdownMenu.propTypes = { - children: PropTypes.node, +const DropdownMenu = ({ children, css }: React.PropsWithChildren) => { + return {children}; }; export default DropdownMenu; diff --git a/frontend/src/components/FilterList/FilterList.tsx b/frontend/src/components/FilterList/FilterList.jsx similarity index 96% rename from frontend/src/components/FilterList/FilterList.tsx rename to frontend/src/components/FilterList/FilterList.jsx index 0c2269c84..925d12b6d 100644 --- a/frontend/src/components/FilterList/FilterList.tsx +++ b/frontend/src/components/FilterList/FilterList.jsx @@ -1,3 +1,5 @@ +/** @jsxImportSource @emotion/react */ + import { useState } from 'react'; import { DropdownMenu } from '..'; import SearchBar from '../SearchBar/SearchBar'; @@ -81,7 +83,7 @@ const FilterList = ({ setSearchKeyword(target.value)} + onChange={(value) => setSearchKeyword(value)} value={searchKeyword} /> diff --git a/frontend/src/components/FilterList/FilterList.styles.ts b/frontend/src/components/FilterList/FilterList.styles.ts index 64232d11d..36721f98f 100644 --- a/frontend/src/components/FilterList/FilterList.styles.ts +++ b/frontend/src/components/FilterList/FilterList.styles.ts @@ -18,10 +18,7 @@ const DropdownToggledStyle = css` } `; -const Container = styled.div<{ - isDropdownToggled: string; - css: ReturnType | null; -}>` +const Container = styled.div<{ isDropdownToggled: boolean }>` background-color: ${COLOR.LIGHT_GRAY_50}; border: 1px solid ${COLOR.DARK_GRAY_400}; @@ -33,7 +30,7 @@ const Container = styled.div<{ align-items: center; ${({ isDropdownToggled }) => isDropdownToggled && DropdownToggledStyle} - ${({ css }) => css && css} + ${({ css }) => css} & > div:not(:last-child) { margin-right: 3.2rem; @@ -148,9 +145,7 @@ const ResetFilter = styled.div` flex-shrink: 0; `; -const CheckIcon = styled.img<{ - checked: boolean; -}>` +const CheckIcon = styled.img<{ checked: boolean }>` ${({ checked }) => !checked && 'visibility: hidden;'} `; diff --git a/frontend/src/components/FilterList/FilterList.styles.tsx b/frontend/src/components/FilterList/FilterList.styles.tsx new file mode 100644 index 000000000..ac82f8880 --- /dev/null +++ b/frontend/src/components/FilterList/FilterList.styles.tsx @@ -0,0 +1,240 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import COLOR from '../../constants/color'; +import MEDIA_QUERY from '../../constants/mediaQuery'; + +const DropdownToggledStyle = css` + &:before { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 80; + display: block; + cursor: default; + content: ' '; + background: transparent; + } +`; + +const Container = styled.div<{ + isDropdownToggled: string; + css: ReturnType | null; +}>` + background-color: ${COLOR.LIGHT_GRAY_50}; + + border: 1px solid ${COLOR.DARK_GRAY_400}; + border-radius: 1.4rem; + padding: 0 3.2rem; + display: flex; + font-size: 1.4rem; + height: inherit; + align-items: center; + + ${({ isDropdownToggled }) => isDropdownToggled && DropdownToggledStyle} + ${({ css }) => css} + + & > div:not(:last-child) { + margin-right: 3.2rem; + + ${MEDIA_QUERY.xs} { + margin-right: 2rem; + } + } + + ${MEDIA_QUERY.xs} { + font-size: 1rem; + } + + & > div { + input[type='search'] { + font-weight: 500; + padding: 1rem; + font-size: 1.4rem; + border: 1px solid ${COLOR.LIGHT_GRAY_700}; + border-radius: 1rem; + outline: none; + + :focus { + border-color: ${COLOR.LIGHT_GRAY_700}; + } + } + + & > button { + display: flex; + align-items: center; + height: 100%; + font-size: 1.6rem; + text-align: center; + color: ${COLOR.DARK_GRAY_500}; + + ::after { + content: ''; + width: 0; + height: 0; + transform: translateY(50%); + margin-left: 0.2rem; + border-top: 0.5rem solid ${COLOR.DARK_GRAY_500}; + border-bottom: 0.5rem solid transparent; + border-left: 0.5rem solid transparent; + border-right: 0.5rem solid transparent; + } + } + + :hover { + color: ${COLOR.BLACK_600}; + + ::after { + border-top-color: ${COLOR.BLACK_600}; + } + } + } +`; + +const FilterContainer = styled.div` + display: flex; + + & > div:not(:last-child) { + margin-right: 3.2rem; + + ${MEDIA_QUERY.xs} { + margin-right: 2rem; + } + } + + ${MEDIA_QUERY.sm} { + display: grid; + grid-template-columns: repeat(2, 1fr); + } + + & button { + display: flex; + align-items: center; + height: 100%; + text-align: center; + color: ${COLOR.DARK_GRAY_500}; + + ::after { + content: ''; + width: 0; + height: 0; + transform: translateY(50%); + margin-left: 0.2rem; + border-top: 0.5rem solid ${COLOR.DARK_GRAY_500}; + border-bottom: 0.5rem solid transparent; + border-left: 0.5rem solid transparent; + border-right: 0.5rem solid transparent; + } + } +`; + +const FilterDetail = styled.button` + display: flex; + align-items: center; + gap: 0.4rem; + + & > img { + width: 1.6rem; + height: 1.6rem; + } +`; + +const ResetFilter = styled.div` + margin-left: auto; + color: ${COLOR.DARK_GRAY_500}; + cursor: pointer; + + flex-shrink: 0; +`; + +const CheckIcon = styled.img<{ checked: boolean }>` + ${({ checked }) => !checked && 'visibility: hidden;'} +`; + +const DropdownStyle = css` + padding-top: 0; +`; + +const SearchBarWrapper = styled.div` + position: sticky; + top: 0; + padding-top: 1rem; + background-color: white; +`; + +const SearchBarStyle = css` + width: 100%; + height: 100%; + background-color: white; + margin-right: 1rem; + padding: 0.5rem 0; + + & > input { + margin-right: 0; + } +`; + +const MemberWrapper = styled.div` + display: flex; + align-items: center; +`; + +const MemberImage = styled.img` + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + margin-right: 0.7rem; +`; + +const Nickname = styled.span` + margin-left: 0.7rem; + color: ${COLOR.DARK_GRAY_500}; + font-weight: 400; +`; + +export const NoContent = styled.div` + width: 100%; + height: 8rem; + padding: 1rem; + color: ${COLOR.LIGHT_GRAY_600}; + + display: flex; + justify-content: center; + align-items: center; +`; + +const SessionInMission = styled.div` + background-color: aliceblue; + border-radius: 1rem; + padding-left: 0.3rem; + padding-right: 0.3rem; + font-size: xx-small; +`; + +const MissionName = styled.div` + margin-top: 0.2rem; + margin-left: 0.5rem; +`; + +const MissionWrapper = styled.div` + margin-top: 0.2rem; + margin-left: 0.5rem; +`; + +export { + Container, + FilterDetail, + ResetFilter, + CheckIcon, + DropdownStyle, + SearchBarWrapper, + SearchBarStyle, + MemberWrapper, + MemberImage, + Nickname, + SessionInMission, + MissionName, + MissionWrapper, + FilterContainer, +}; diff --git a/frontend/src/components/Items/EssayAnswerItem.tsx b/frontend/src/components/Items/EssayAnswerItem.tsx index f823c5f8a..0b2d055b0 100644 --- a/frontend/src/components/Items/EssayAnswerItem.tsx +++ b/frontend/src/components/Items/EssayAnswerItem.tsx @@ -10,13 +10,20 @@ import { import { AlignItemsEndStyle, FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; import Card from '../Card/Card'; import ProfileChip from '../ProfileChip/ProfileChip'; +import { EssayAnswer } from '../../models/EssayAnswers'; -const EssayAnswerItem = ({ essayAnswer }) => { - const { author, answer } = essayAnswer; +type EssayAnswerItemProps = ( + Pick + & { title?: string; showTitle?: boolean } +); + +const EssayAnswerItem = (props: EssayAnswerItemProps) => { + const { author, answer, title, showTitle } = props; return (
+ { showTitle &&

{title}

}
{answer}
diff --git a/frontend/src/components/Lists/EssayAnswerList.tsx b/frontend/src/components/Lists/EssayAnswerList.tsx deleted file mode 100644 index 5838d5a80..000000000 --- a/frontend/src/components/Lists/EssayAnswerList.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import {css} from '@emotion/react'; -import EssayAnswerItem from "../Items/EssayAnswerItem"; -import { NoDefaultHoverLink } from '../Items/EssayAnswerItem.styles'; - -const EssayAnswerList = ({ essayAnswers }) => { - return ( -
    li:not(:last-child) { - margin-bottom: 1.6rem; - } - `} - > - {essayAnswers.map((essayAnswer) => ( -
  • - - - -
  • - ))} -
- ); -}; - -export default EssayAnswerList; diff --git a/frontend/src/components/Lists/QuizAnswerList.tsx b/frontend/src/components/Lists/QuizAnswerList.tsx new file mode 100644 index 000000000..e3f8e6002 --- /dev/null +++ b/frontend/src/components/Lists/QuizAnswerList.tsx @@ -0,0 +1,39 @@ +/** @jsxImportSource @emotion/react */ +import {css} from '@emotion/react'; +import EssayAnswerItem from "../Items/EssayAnswerItem"; +import { NoDefaultHoverLink } from '../Items/EssayAnswerItem.styles'; +import { EssayAnswer } from '../../models/EssayAnswers'; + +interface QuizAnswerListProps { + essayAnswers: EssayAnswer[]; + showQuizTitle?: boolean; +} + +const QuizAnswerList = (props: QuizAnswerListProps) => { + const { essayAnswers, showQuizTitle = false } = props; + + return ( +
    li:not(:last-child) { + margin-bottom: 1.6rem; + } + `} + > + {essayAnswers.map(({ id, answer, author, quiz: { question } }) => ( +
  • + + + +
  • + ))} +
+ ); +}; + +export default QuizAnswerList; diff --git a/frontend/src/components/SearchBar/SearchBar.styles.ts b/frontend/src/components/SearchBar/SearchBar.styles.ts new file mode 100644 index 000000000..22d8e7724 --- /dev/null +++ b/frontend/src/components/SearchBar/SearchBar.styles.ts @@ -0,0 +1,42 @@ +import { InterpolationWithTheme } from '@emotion/core'; +import { Theme } from '@emotion/react'; +import styled from '@emotion/styled'; +import COLOR from '../../constants/color'; + +const Container = styled.div<{ css?: InterpolationWithTheme }>` + background-color: ${COLOR.LIGHT_GRAY_100}; + height: 4.8rem; + width: 36rem; + + padding: 0.5rem 1rem; + border-radius: 1.6rem; + + display: flex; + justify-content: center; + align-items: center; + + input { + height: 90%; + flex-grow: 1; + + border: 0; + outline: none; + border-radius: 1rem; + + padding: 0 1rem; + margin-right: 1rem; + + :focus { + border: 2px solid ${COLOR.LIGHT_GRAY_400}; + } + } + + button { + width: 3.6rem; + background-color: transparent; + } + + ${({ css }) => css} +`; + +export { Container }; diff --git a/frontend/src/components/SearchBar/SearchBar.styles.tsx b/frontend/src/components/SearchBar/SearchBar.styles.tsx index 5b28d146d..22d8e7724 100644 --- a/frontend/src/components/SearchBar/SearchBar.styles.tsx +++ b/frontend/src/components/SearchBar/SearchBar.styles.tsx @@ -1,10 +1,9 @@ +import { InterpolationWithTheme } from '@emotion/core'; +import { Theme } from '@emotion/react'; import styled from '@emotion/styled'; import COLOR from '../../constants/color'; -import { css } from '@emotion/react'; -const Container = styled.div<{ - css: ReturnType; -}>` +const Container = styled.div<{ css?: InterpolationWithTheme }>` background-color: ${COLOR.LIGHT_GRAY_100}; height: 4.8rem; width: 36rem; diff --git a/frontend/src/components/SearchBar/SearchBar.tsx b/frontend/src/components/SearchBar/SearchBar.tsx index 9a241ee79..43af72e7a 100644 --- a/frontend/src/components/SearchBar/SearchBar.tsx +++ b/frontend/src/components/SearchBar/SearchBar.tsx @@ -7,7 +7,7 @@ import { ChangeEventHandler, FormEventHandler } from 'react'; interface SearchBarProps { onSubmit?: FormEventHandler; onChange: ChangeEventHandler; - css: ReturnType; + css?: ReturnType; value: string; } diff --git a/frontend/src/constants/reactQueryKey.ts b/frontend/src/constants/reactQueryKey.ts index d68ae254d..5d32e9f12 100644 --- a/frontend/src/constants/reactQueryKey.ts +++ b/frontend/src/constants/reactQueryKey.ts @@ -1,8 +1,10 @@ const REACT_QUERY_KEY = { STUDYLOG: 'STUDYLOG', - QUIZ:'QUIZ', + QUIZ: 'QUIZ', + QUIZZES: 'QUIZZES', ESSAY_ANSWER: 'ESSAY_ANSWER', - ESSAY_ANSWER_LIST: 'ESSAY_ANSWER_LIST', + QUIZ_ANSWERS: 'QUIZ_ANSWERS', + ESSAY_ANSWER_FILTER_LIST: 'ESSAY_ANSWER_FILTER_LIST', }; export default REACT_QUERY_KEY; diff --git a/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts b/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts deleted file mode 100644 index d94a5285c..000000000 --- a/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useGetEssayAnswerList, useGetQuiz } from '../queries/essayanswer'; -import { useParams } from 'react-router-dom'; - -export const useEssayAnswerList = () => { - const { quizId } = useParams<{ quizId: string }>(); - - const { data: essayAnswers } = useGetEssayAnswerList({ quizId }); - const { data: quiz } = useGetQuiz({ quizId }) - - return { quiz, essayAnswers }; -}; diff --git a/frontend/src/hooks/queries/essayanswer.ts b/frontend/src/hooks/queries/essayanswer.ts index 37e929aa7..c9988ca74 100644 --- a/frontend/src/hooks/queries/essayanswer.ts +++ b/frontend/src/hooks/queries/essayanswer.ts @@ -1,23 +1,29 @@ +import { AxiosError } from 'axios'; import { useMutation, useQuery } from 'react-query'; +import { useHistory } from 'react-router-dom'; import { createNewEssayAnswerRequest, requestDeleteEssayAnswer, requestEditEssayAnswer, requestGetEssayAnswer, - requestGetEssayAnswerList, - requestGetQuizAsync + requestGetEssayAnswers, + requestGetQuizAnswers, + requestGetQuizAsync, + requestGetQuizzes, } from '../../apis/essayanswers'; -import { EssayAnswerRequest, EssayAnswerResponse } from '../../models/EssayAnswers'; - -import { AxiosError } from 'axios'; -import { useHistory } from 'react-router-dom'; +import { ResponseError } from '../../apis/studylogs'; import { ALERT_MESSAGE, PATH } from '../../constants'; import ERROR_CODE from '../../constants/errorCode'; -import useSnackBar from '../useSnackBar'; +import { ERROR_MESSAGE, SUCCESS_MESSAGE } from '../../constants/message'; import REACT_QUERY_KEY from '../../constants/reactQueryKey'; +import { + EssayAnswer, + EssayAnswerFilterRequest, + EssayAnswerRequest, + EssayAnswerResponse, +} from '../../models/EssayAnswers'; import { Quiz } from '../../models/Keywords'; -import { ERROR_MESSAGE, SUCCESS_MESSAGE } from '../../constants/message'; -import { ResponseError } from '../../apis/studylogs'; +import useSnackBar from '../useSnackBar'; export const useCreateNewEssayAnswerMutation = ({ onSuccess = () => {}, @@ -32,51 +38,46 @@ export const useCreateNewEssayAnswerMutation = ({ }, }); -export const useEditEssayAnswer = ( - { essayAnswerId }: { essayAnswerId: number }, -) => { +export const useEditEssayAnswer = ({ essayAnswerId }: { essayAnswerId: number }) => { const history = useHistory(); - return useMutation( - (data: { answer: string }) => - requestEditEssayAnswer(essayAnswerId, data), - { - onSuccess: () => { - alert(SUCCESS_MESSAGE.EDIT_POST); - history.push(`/essay-answers/${essayAnswerId}`); - }, + return useMutation((data: { answer: string }) => requestEditEssayAnswer(essayAnswerId, data), { + onSuccess: () => { + alert(SUCCESS_MESSAGE.EDIT_POST); + history.push(`/essay-answers/${essayAnswerId}`); + }, - onError: (error: ResponseError) => { - alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.FAIL_TO_EDIT_STUDYLOG); - }, - } - ); -} + onError: (error: ResponseError) => { + alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.FAIL_TO_EDIT_STUDYLOG); + }, + }); +}; export const useGetEssayAnswer = ( { essayAnswerId }, - { - onSuccess = (essayAnswer: EssayAnswerResponse) => {}, - onError = () => {} - } = {} + { onSuccess = (essayAnswer: EssayAnswer) => {}, onError = () => {} } = {} ) => { const history = useHistory(); const { openSnackBar } = useSnackBar(); - return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER, essayAnswerId], () => requestGetEssayAnswer(essayAnswerId), { - onSuccess: (essayAnswer: EssayAnswerResponse) => { - onSuccess?.(essayAnswer); - }, - onError: (error) => { - const { response } = (error as unknown) as AxiosError; + return useQuery( + [REACT_QUERY_KEY.ESSAY_ANSWER, essayAnswerId], + () => requestGetEssayAnswer(essayAnswerId), + { + onSuccess: (essayAnswer: EssayAnswer) => { + onSuccess?.(essayAnswer); + }, + onError: (error) => { + const { response } = (error as unknown) as AxiosError; - if (response?.data.code === ERROR_CODE.NO_CONTENT) { - openSnackBar(ALERT_MESSAGE.NO_EXIST_POST); - history.push(PATH.ROADMAP); - } - }, - refetchOnWindowFocus: false, - retry: false, - }); + if (response?.data.code === ERROR_CODE.NO_CONTENT) { + openSnackBar(ALERT_MESSAGE.NO_EXIST_POST); + history.push(PATH.ROADMAP); + } + }, + refetchOnWindowFocus: false, + retry: false, + } + ); }; export const useDeleteEssayAnswerMutation = ({ @@ -92,36 +93,45 @@ export const useDeleteEssayAnswerMutation = ({ }, }); -export const useGetEssayAnswerList = ( +export const useGetQuizAnswerList = ( { quizId }, - { - onSuccess = (essayAnswer: EssayAnswerResponse[]) => {}, - onError = () => {} - } = {} + { onSuccess = (essayAnswer: EssayAnswer[]) => {}, onError = () => {} } = {} ) => { - return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER_LIST, quizId], () => requestGetEssayAnswerList(quizId), { - onSuccess: (essayAnswer: EssayAnswerResponse[]) => { - onSuccess?.(essayAnswer); - }, - onError: (error) => {}, - refetchOnWindowFocus: false, - retry: false, - }); + return useQuery( + [REACT_QUERY_KEY.QUIZ_ANSWERS, quizId], + () => requestGetQuizAnswers(quizId), + { + onSuccess: (essayAnswer: EssayAnswer[]) => { + onSuccess?.(essayAnswer); + }, + onError: (error) => {}, + refetchOnWindowFocus: false, + retry: false, + } + ); +}; + +export const useGetEssayAnswers = (filter: EssayAnswerFilterRequest) => { + return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER_FILTER_LIST, filter], () => + requestGetEssayAnswers(filter) + ); }; export const useGetQuiz = ( { quizId }, - { - onSuccess = (quiz: Quiz) => {}, - onError = () => {} - } = {} + { onSuccess = (quiz: Quiz) => {}, onError = () => {} } = {} ) => { return useQuery([REACT_QUERY_KEY.QUIZ, quizId], () => requestGetQuizAsync(quizId), { onSuccess: (quiz: Quiz) => { onSuccess?.(quiz); }, - onError: (error) => {}, refetchOnWindowFocus: false, retry: false, }); }; + +export const useGetQuizzes = ({ curriculumId }: { curriculumId: number }) => { + return useQuery>([REACT_QUERY_KEY.QUIZZES], () => + requestGetQuizzes(curriculumId) + ); +}; diff --git a/frontend/src/hooks/queries/filters.ts b/frontend/src/hooks/queries/filters.ts index 86c6d9e38..312a2cf95 100644 --- a/frontend/src/hooks/queries/filters.ts +++ b/frontend/src/hooks/queries/filters.ts @@ -1,14 +1,14 @@ +import { useContext } from 'react'; import { useQuery } from 'react-query'; - +import { getMembersForFilter } from '../../apis/filter'; import { requestGetMissions, requestGetSessions, requestGetTags, ResponseError, } from '../../apis/studylogs'; -import { Mission, Session, Tag } from '../../models/Studylogs'; -import { useContext } from 'react'; import { UserContext } from '../../contexts/UserProvider'; +import { Author, Mission, Session, Tag } from '../../models/Studylogs'; export const useTags = () => useQuery( @@ -47,3 +47,14 @@ export const useGetMySessions = () => { { initialData: [] } ); }; + +export const useGetMembers = () => { + return useQuery( + ['memberList'], + async () => { + const members = await getMembersForFilter(); + return members; + }, + { initialData: [] } + ); +}; diff --git a/frontend/src/hooks/queries/keywords.ts b/frontend/src/hooks/queries/keywords.ts index 0f66e98a7..855db360f 100644 --- a/frontend/src/hooks/queries/keywords.ts +++ b/frontend/src/hooks/queries/keywords.ts @@ -1,6 +1,6 @@ import { useQuery } from 'react-query'; import { getKeyword, getQuizListByKeyword, getRoadmap } from '../../apis/keywords'; -import type { RoadmapRequest } from '../../models/Keywords'; +import type { KeywordResponse, RoadmapRequest } from '../../models/Keywords'; const QUERY_KEY = { keyword: 'keyword', @@ -16,12 +16,8 @@ export const useRoadmap = ({ curriculumId }: RoadmapRequest) => { }); }; -export const useGetKeyword = ({ keywordId }: { keywordId: number }) => { - const { data } = useQuery([QUERY_KEY.keyword, keywordId], () => getKeyword({ keywordId })); - - return { - keyword: data, - }; +export const useGetKeywords = ({ keywordId }: { keywordId: number }) => { + return useQuery([QUERY_KEY.keyword, keywordId], () => getKeyword({ keywordId })); }; export const useGetQuizListByKeyword = ({ keywordId }: { keywordId: number }) => { diff --git a/frontend/src/mocks/fixtures/essayAnswers.ts b/frontend/src/mocks/fixtures/essayAnswers.ts new file mode 100644 index 000000000..8b1735259 --- /dev/null +++ b/frontend/src/mocks/fixtures/essayAnswers.ts @@ -0,0 +1,134 @@ +import { EssayAnswer } from '../../models/EssayAnswers'; + +type EssayAnswersFilterParams = { + curriculumId: number; + keywordId?: number; + quizIds?: number[]; + memberIds?: number[]; + page?: number; + size?: number; +}; + +const data: Array<{ + curriculumId: number; + essayAnswers: (EssayAnswer & { + keywordId: number; + })[]; +}> = [ + { + curriculumId: 1, + essayAnswers: [ + { + id: 71, + keywordId: 1, + quiz: { + quizId: 29, + isLearning: true, + question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '# Varargs\n***\nVarargs(가변인자)는 JDK 5에서 새로 도입된 기능이다.\nparameter의 수를 가변적으로 조절할 수 있게 해주는 기능이다.\n
\n\n## 사용법\n***\nparameter를 여러개 정의하고 싶은 method를 선언할 때, parameter 부분에 ...을 붙이면 된다.\n\n```java\npublic int calculateSum(int... numbers) {\n return Arrays.stream(numbers).sum();\n}\n```\n\n위 코드에서 유추할 수 있듯이, varargs는 내부적으로 array를 반환한다.\ncompile 시에 Object array가 만들어지고, varargs에 할당된 인자들이 array의 원소로 들어간다.\n
\n\n## 장점\n***\n만약 위의 method에서, varargs를 사용하지 않는다고 가정해보자.\n그렇다면, parameter가 2개, 3개, 4개, ..., 인 calculateSum() method를 전부 overloading해야 할 것이다.\n\n```java\npublic int getSum(int number1, int number2) {\n\treturn number1 + number2;\n}\npublic int getSum(int number1, int number2, int number3) {\n\treturn number1 + number2 + number3;\n}\n\n...\n\n\n```\n\nvarargs를 사용하면 이러한 overloading을 하지 않도록 할 수 있다.', + author: { + id: 304, + username: 'Jaeyoung22', + nickname: 'ReO', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/89302528?v=4', + }, + createdAt: '2023-04-09T17:56:25.642891', + updatedAt: '2023-04-09T17:56:25.642891', + }, + { + id: 69, + keywordId: 2, + quiz: { + quizId: 29, + isLearning: false, + question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '# Varargs 탄생 배경\n\nJDK 1.4 전까지는 다양한 수의 인자를 가진 메서드를 선언할 수 없었습니다.\n이를 해결하기 위해선 두 가지 방식을 사용했습니다.\n1. 메서드 오버로딩\n이 방법은 코드의 길이를 무한대로 증가시킵니다.\n2. 전달할 값들을 배열로 받기\n\n결국, Varargs는 **다양한 범위의 인자를 받기 위한 해결책**으로 탄생하게 되었습니다.\n\n# Varargs 사용법 및 특징\n\n```java\npublic class VarargsPractice {\n public static void main(String[] args) {\n printValues(); // 특징 1. 0개 이상의 인자를 전달할 수 있습니다.\n printValues("1");\n printValues("1", "2");\n printValues("1", "2", "3");\n printValues(new String[]{"1", "2", "3"}); // 특징 2. 실제로는 배열로 동작합니다.\n }\n\n // 사용법: 파라미터에 타입과 ... 을 함께 작성합니다. (String...)\n public static void printValues(final String... values) {\n for (int i = 0; i < values.length; i++) {\n System.out.println(values[i]); // 배열로 동작하기에 인덱스로 접근 가능합니다.\n }\n }\n}\n```\n\n# 잘못된 Varargs 사용법\n\n1. 한 메서드에 2가지 타입의 가변인자(varargs)는 사용하지 못합니다.\n```java\nvoid method(String... inputs, int... numbers);\n```\n\n2. 가변인자를 첫 번째 파라미터로 선언하지 못합니다.\n```java\nvoid method(int... numbers, String input);\n```\n\n# Varargs의 장점\n\n1. 메서드 인자의 수를 동적으로 결정 및 처리할 수 있습니다.\n2. 코드를 간결하게 유지할 수 있습니다.\n\n# Varargs의 단점\n\n1. 공간 복잡도가 증가합니다.\n가변 인자는 곧, 배열입니다.\n가변 인자를 전달받은 메서드는 매번 새로운 배열을 생성해야 합니다.\n\n# Varargs를 사용하는 기준\n(개인적인 기준입니다.)\n\n1. 인자의 개수를 다양하게 받아야 하는 상황이다.\n2. 성능상 그다지 중요하지 않은 상황이다.', + author: { + id: 287, + username: 'kdkdhoho', + nickname: '도기\uD83D\uDC36', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/66300965?v=4', + }, + createdAt: '2023-04-08T23:54:48.667266', + updatedAt: '2023-04-08T23:54:48.667266', + }, + ], + }, + { + curriculumId: 2, + essayAnswers: [ + { + id: 71, + keywordId: 1, + quiz: { + quizId: 29, + isLearning: true, + question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '수를 가변적으로 조절할 수 있게 해주는 기능이다.\n
\n\n## 사용법\n***\nparameter를 여러개 정의하고 싶은 method를 선언할 때, parameter 부분에 ...을 붙이면 된다.\n\n```java\npublic int calculateSum(int... numbers) {\n return Arrays.stream(numbers).sum();\n}\n```\n\n위 코드에서 유추할 수 있듯이, varargs는 내부적으로 array를 반환한다.\ncompile 시에 Object array가 만들어지고, varargs에 할당된 인자들이 array의 원소로 들어간다.\n
\n\n## 장점\n***\n만약 위의 method에서, varargs를 사용하지 않는다고 가정해보자.\n그렇다면, parameter가 2개, 3개, 4개, ..., 인 calculateSum() method를 전부 overloading해야 할 것이다.\n\n```java\npublic int getSum(int number1, int number2) {\n\treturn number1 + number2;\n}\npublic int getSum(int number1, int number2, int number3) {\n\treturn number1 + number2 + number3;\n}\n\n...\n\n\n```\n\nvarargs를 사용하면 이러한 overloading을 하지 않도록 할 수 있다.', + author: { + id: 304, + username: 'Jaeyoung22', + nickname: 'ReO', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/89302528?v=4', + }, + createdAt: '2023-04-09T17:56:25.642891', + updatedAt: '2023-04-09T17:56:25.642891', + }, + { + id: 69, + keywordId: 1, + quiz: { + quizId: 29, + isLearning: false, + question: 'Vara하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '# Va습니다.\n이를 해결하기 위해선 두 가지 방식을 사용했습니다.\n1. 메서드 오버로딩\n이 방법은 코드의 길이를 무한대로 증가시킵니다.\n2. 전달할 값들을 배열로 받기\n\n결국, Varargs는 **다양한 범위의 인자를 받기 위한 해결책**으로 탄생하게 되었습니다.\n\n# Varargs 사용법 및 특징\n\n```java\npublic class VarargsPractice {\n public static void main(String[] args) {\n printValues(); // 특징 1. 0개 이상의 인자를 전달할 수 있습니다.\n printValues("1");\n printValues("1", "2");\n printValues("1", "2", "3");\n printValues(new String[]{"1", "2", "3"}); // 특징 2. 실제로는 배열로 동작합니다.\n }\n\n // 사용법: 파라미터에 타입과 ... 을 함께 작성합니다. (String...)\n public static void printValues(final String... values) {\n for (int i = 0; i < values.length; i++) {\n System.out.println(values[i]); // 배열로 동작하기에 인덱스로 접근 가능합니다.\n }\n }\n}\n```\n\n# 잘못된 Varargs 사용법\n\n1. 한 메서드에 2가지 타입의 가변인자(varargs)는 사용하지 못합니다.\n```java\nvoid method(String... inputs, int... numbers);\n```\n\n2. 가변인자를 첫 번째 파라미터로 선언하지 못합니다.\n```java\nvoid method(int... numbers, String input);\n```\n\n# Varargs의 장점\n\n1. 메서드 인자의 수를 동적으로 결정 및 처리할 수 있습니다.\n2. 코드를 간결하게 유지할 수 있습니다.\n\n# Varargs의 단점\n\n1. 공간 복잡도가 증가합니다.\n가변 인자는 곧, 배열입니다.\n가변 인자를 전달받은 메서드는 매번 새로운 배열을 생성해야 합니다.\n\n# Varargs를 사용하는 기준\n(개인적인 기준입니다.)\n\n1. 인자의 개수를 다양하게 받아야 하는 상황이다.\n2. 성능상 그다지 중요하지 않은 상황이다.', + author: { + id: 287, + username: 'kdkdhoho', + nickname: '도기\uD83D\uDC36', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/66300965?v=4', + }, + createdAt: '2023-04-08T23:54:48.667266', + updatedAt: '2023-04-08T23:54:48.667266', + }, + ], + }, +]; + +const essayAnswersMock = { + data, + filter(params: EssayAnswersFilterParams) { + const { curriculumId, keywordId, memberIds, page = 1, quizIds, size = 10 } = params; + + const essayAnswers = + this.data.find( + (essayAnswersByCurriculum) => essayAnswersByCurriculum.curriculumId === curriculumId + )?.essayAnswers ?? []; + + return essayAnswers + .filter((essayAnswer) => { + if (keywordId && essayAnswer.keywordId !== keywordId) return false; + + if (memberIds && !memberIds.includes(essayAnswer.author.id)) return false; + + if (quizIds && quizIds.includes(essayAnswer.quiz.quizId)) return false; + + return true; + }) + .slice(0, size); + }, +}; + +export default essayAnswersMock; diff --git a/frontend/src/mocks/handlers/essayAnswers.ts b/frontend/src/mocks/handlers/essayAnswers.ts new file mode 100644 index 000000000..01fbb2bb2 --- /dev/null +++ b/frontend/src/mocks/handlers/essayAnswers.ts @@ -0,0 +1,41 @@ +import { rest } from 'msw'; +import { BASE_URL } from '../../configs/environment'; +import essayAnswersMock from '../fixtures/essayAnswers'; + +export const essayAnswerHandler = [ + rest.get(`${BASE_URL}/essay-answers`, (req, res, ctx) => { + const { searchParams } = req.url; + + const params = { + curriculumId: searchParams.get('curriculumId'), + keywordId: searchParams.get('keywordId'), + quizIds: searchParams.get('quizIds'), + memberIds: searchParams.get('memberIds'), + page: searchParams.get('page'), + size: searchParams.get('size'), + }; + + const curriculumId = Number(params.curriculumId); + if (Number.isNaN(curriculumId)) { + return res(ctx.status(400)); + } + + return res( + ctx.status(200), + ctx.json( + essayAnswersMock.filter({ + curriculumId: curriculumId, + keywordId: params.keywordId ? Number(params.keywordId) : undefined, + quizIds: params.quizIds + ? params.quizIds.slice(1, -1).split(',').map(Number) ?? [] + : undefined, + memberIds: params.memberIds + ? params.memberIds.slice(1, -1).split(',').map(Number) ?? [] + : undefined, + page: params.page ? Number(params.page) : undefined, + size: params.size ? Number(params.size) : undefined, + }) + ) + ); + }), +]; diff --git a/frontend/src/mocks/handlers/index.ts b/frontend/src/mocks/handlers/index.ts index e01b78117..f7e9029c1 100644 --- a/frontend/src/mocks/handlers/index.ts +++ b/frontend/src/mocks/handlers/index.ts @@ -1,13 +1,15 @@ import { articlesHandler } from './articles'; import { commentsHandler } from './comment'; +import { essayAnswerHandler } from './essayAnswers'; +import { roadmapHandler } from './roadmap'; import { levellogHandler } from './levellog'; import { popularStudyLogHandler } from './popularStudyLog'; -import { roadmapHandler } from './roadmap'; export const handlers = [ - ...popularStudyLogHandler, ...commentsHandler, - ...levellogHandler, + ...essayAnswerHandler, ...roadmapHandler, ...articlesHandler, + ...levellogHandler, + ...popularStudyLogHandler, ]; diff --git a/frontend/src/models/EssayAnswers.ts b/frontend/src/models/EssayAnswers.ts index 5323d79f6..3b893bfef 100644 --- a/frontend/src/models/EssayAnswers.ts +++ b/frontend/src/models/EssayAnswers.ts @@ -1,16 +1,14 @@ +import { Quiz } from './Keywords'; import { Author } from './Studylogs'; -import {Quiz} from "./Keywords"; export interface EssayAnswerRequest { quizId: number; answer: string; } -export interface EssayEditRequest { - answer: string; -} +export type EssayEditRequest = Pick; -export interface EssayAnswerResponse { +export interface EssayAnswer { id: number; quiz: Quiz; answer: string; @@ -18,3 +16,31 @@ export interface EssayAnswerResponse { createdAt: string; updatedAt: string; } + +export interface EssayAnswerResponse { + data: EssayAnswer[]; + totalSize: number; + totalPage: number; + currPage: number; +} + +export type EssayAnswerFilter = { + curriculumId: number; + keywordId?: number; + quizIds?: number[]; + memberIds?: number[]; +}; + +export type EssayAnswerListRequest = EssayAnswerFilter & { + page?: number; + size?: number; +}; + +export type EssayAnswerFilterRequest = { + curriculumId: number; + keywordId?: number; + quizIds?: number[]; + memberIds?: number[]; + page?: number; + size?: number; +}; diff --git a/frontend/src/models/filter.ts b/frontend/src/models/filter.ts new file mode 100644 index 000000000..e7c66cfbb --- /dev/null +++ b/frontend/src/models/filter.ts @@ -0,0 +1,13 @@ +import type { Author } from './Studylogs'; + +interface FilterData { + id: number; + name: string; +} + +export interface FilterResponse { + sessions: FilterData[]; + missions: unknown[]; + tags: FilterData[]; + members: Author[]; +} diff --git a/frontend/src/pages/EditEssayAnswerPage/index.tsx b/frontend/src/pages/EditEssayAnswerPage/index.tsx index cb142934c..b3b7b18b7 100644 --- a/frontend/src/pages/EditEssayAnswerPage/index.tsx +++ b/frontend/src/pages/EditEssayAnswerPage/index.tsx @@ -12,7 +12,7 @@ import {useContext, useEffect, useRef, useState} from "react"; import { UserContext } from '../../contexts/UserProvider'; import { useHistory, useParams } from 'react-router'; import { useEditEssayAnswer, useGetEssayAnswer } from '../../hooks/queries/essayanswer'; -import { EssayAnswerResponse } from '../../models/EssayAnswers'; +import { EssayAnswer } from '../../models/EssayAnswers'; const EditEssayAnswerPage = () => { const history = useHistory(); @@ -28,7 +28,7 @@ const EditEssayAnswerPage = () => { const previousEssayAnswer = useGetEssayAnswer( { essayAnswerId: id }, { - onSuccess: ({ quiz: { question }, answer }: EssayAnswerResponse) => { + onSuccess: ({ quiz: { question }, answer }: EssayAnswer) => { setAnswer(answer); setQuizTitle(question); }, diff --git a/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.styles.ts b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.styles.ts new file mode 100644 index 000000000..5773922a4 --- /dev/null +++ b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.styles.ts @@ -0,0 +1,59 @@ +import styled from '@emotion/styled'; +import COLOR from '../../../../constants/color'; +import MEDIA_QUERY from '../../../../constants/mediaQuery'; + +export const Container = styled.div` + background-color: ${COLOR.LIGHT_GRAY_50}; + + margin-bottom: 30px; + + border: 1px solid ${COLOR.DARK_GRAY_400}; + border-radius: 1.4rem; + padding: 0 3.2rem; + display: flex; + font-size: 1.4rem; + height: 46px; + justify-content: space-between; + align-items: center; +`; + +export const FilterContainer = styled.div` + display: flex; + + & > div:not(:last-child) { + margin-right: 3.2rem; + + ${MEDIA_QUERY.xs} { + margin-right: 2rem; + } + } + + ${MEDIA_QUERY.sm} { + display: grid; + grid-template-columns: repeat(2, 1fr); + } + + & button { + display: flex; + align-items: center; + height: 100%; + text-align: center; + color: ${COLOR.DARK_GRAY_500}; + + ::after { + content: ''; + width: 0; + height: 0; + transform: translateY(50%); + margin-left: 0.2rem; + border-top: 0.5rem solid ${COLOR.DARK_GRAY_500}; + border-bottom: 0.5rem solid transparent; + border-left: 0.5rem solid transparent; + border-right: 0.5rem solid transparent; + } + } +`; + +export const ResetFilterButton = styled.button` + cursor: pointer; +`; diff --git a/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.tsx b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.tsx new file mode 100644 index 000000000..99106c9ce --- /dev/null +++ b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.tsx @@ -0,0 +1,136 @@ +import { css } from '@emotion/react'; +import { useEffect, useState } from 'react'; +import DropdownMenu from '../../../../components/DropdownMenu/DropdownMenu'; +import { useGetQuizzes } from '../../../../hooks/queries/essayanswer'; +import { useGetMembers } from '../../../../hooks/queries/filters'; +import { useRoadmap } from '../../../../hooks/queries/keywords'; +import { KeywordResponse } from '../../../../models/Keywords'; +import { Container, FilterContainer } from './RoadmapFilter.styles'; + +const filterKoreanNames: Record = { + keywordId: '주제', + memberIds: '회원', + quizIds: '질문', +}; + +const useGetKeywords = ({ curriculumId }: { curriculumId: number }) => { + const { data: roadmap } = useRoadmap({ curriculumId }); + + const extractKeywords = ( + keywordResponse: KeywordResponse + ): Pick[] => { + return [ + { keywordId: keywordResponse.keywordId, name: keywordResponse.name }, + ...keywordResponse.childrenKeywords.map(extractKeywords).flat(), + ]; + }; + + return roadmap?.data.map(extractKeywords).flat() ?? []; +}; + +interface RoadmapFilterProps { + curriculumId: number; + filter: Record; + onFilterChange: (filter: Record) => void; +} + +const RoadmapFilter = ({ curriculumId, filter, onFilterChange }: RoadmapFilterProps) => { + const [activeFilterKeyword, setActiveFilterKeyword] = useState(null); + + const keywords = useGetKeywords({ curriculumId }); + const { data: quizzes } = useGetQuizzes({ curriculumId }); + const { data: members } = useGetMembers(); + + const filterData: Record> = { + keywordId: keywords.map((keyword) => ({ key: String(keyword.keywordId), label: keyword.name })), + memberIds: members?.map((member) => ({ key: String(member.id), label: member.nickname })) ?? [], + quizIds: quizzes?.map((quiz) => ({ key: String(quiz.id), label: quiz.question })) ?? [], + }; + + const handleFilter = (filterName: string, filterItemKey: string) => { + if (filterName === 'curriculumId' || filterName === 'keywordId') { + if (!filter[filterName] || filter[filterName] !== filterItemKey) { + onFilterChange({ ...filter, [filterName]: filterItemKey }); + return; + } + + if (filter[filterName] === filterItemKey) { + const updatedFilter = { ...filter }; + delete updatedFilter[filterName]; + + onFilterChange(updatedFilter); + return; + } + } + + if (filterName === 'quizIds' || filterName === 'memberIds') { + if (!filter[filterName]) { + onFilterChange({ ...filter, [filterName]: String(filterItemKey) }); + return; + } + const idsList = filter[filterName].split(','); + if (!idsList.includes(String(filterItemKey))) { + const ids = filter[filterName] + ',' + String(filterItemKey); + onFilterChange({ ...filter, [filterName]: ids }); + return; + } + const idsFilteredList = idsList.filter((id) => id !== filterItemKey); + if (idsFilteredList.length === 0) { + const updatedFilter = { ...filter }; + delete updatedFilter[filterName]; + + onFilterChange(updatedFilter); + return; + } + + onFilterChange({ ...filter, [filterName]: idsFilteredList.join(',') }); + return; + } + }; + + const resetFilter = () => { + onFilterChange({}); + }; + + const closeDropdown = () => { + setActiveFilterKeyword(null); + }; + + useEffect(() => { + window.addEventListener('click', closeDropdown); + + return () => window.removeEventListener('click', closeDropdown); + }, []); + + return ( + + + {Object.keys(filterData).map((filterKeyword, index) => ( +
event.stopPropagation()} + css={css` + position: relative; + `} + > + + {activeFilterKeyword === filterKeyword && ( + +
    + {filterData[filterKeyword].map((item) => ( +
  • handleFilter(filterKeyword, item.key)}>{item.label}
  • + ))} +
+
+ )} +
+ ))} +
+ {Object.keys(filter).length !== 0 && } +
+ ); +}; + +export default RoadmapFilter; diff --git a/frontend/src/pages/EssayAnswerListPage/index.tsx b/frontend/src/pages/EssayAnswerListPage/index.tsx index 88b4e1ea7..0f8d338ef 100644 --- a/frontend/src/pages/EssayAnswerListPage/index.tsx +++ b/frontend/src/pages/EssayAnswerListPage/index.tsx @@ -1,19 +1,57 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import EssayAnswerList from '../../components/Lists/EssayAnswerList'; -import MEDIA_QUERY from '../../constants/mediaQuery'; -import { useEssayAnswerList } from '../../hooks/EssayAnswer/useEssayAnswerList'; +import { useEffect, useMemo, useState } from 'react'; +import { useHistory, useLocation } from 'react-router'; import { MainContentStyle } from '../../PageRouter'; +import EssayAnswerList from '../../components/Lists/QuizAnswerList'; +import RoadmapFilter from './components/RoadmapFilter/RoadmapFilter'; +import MEDIA_QUERY from '../../constants/mediaQuery'; +import { useGetCurriculums } from '../../hooks/queries/curriculum'; +import { useGetEssayAnswers } from '../../hooks/queries/essayanswer'; import { AlignItemsCenterStyle, FlexStyle, - JustifyContentSpaceBtwStyle + JustifyContentSpaceBtwStyle, } from '../../styles/flex.styles'; import { HeaderContainer, PostListContainer } from './styles'; +export interface FilterlingType { + curriculumId: string; + keywordId: string; + quizIds: string; + memberIds: string; +} + const EssayAnswerListPage = () => { - const { quiz, essayAnswers } = useEssayAnswerList(); + const history = useHistory(); + const { curriculums } = useGetCurriculums(); + const { search } = useLocation(); + + const [filter, setFilter] = useState>(() => + Object.fromEntries(new URLSearchParams(search).entries()) + ); + + const selectedCurriculum = + (curriculums ?? []).find((curriculum) => curriculum.id === Number(filter.curriculumId))?.name ?? + '😎'; + + const { curriculumId, keywordId, quizIds, memberIds } = filter; + const { data: { data: essayAnswers } = { data: [] } } = useGetEssayAnswers({ + curriculumId: Number(curriculumId), + keywordId: keywordId ? Number(keywordId) : undefined, + quizIds: quizIds ? quizIds.split(',').map(Number) : undefined, + memberIds: memberIds ? memberIds.split(',').map(Number) : undefined, + }); + + const handleFilterChange = (filter: Record) => { + filter['curriculumId'] = curriculumId; + setFilter(filter); + } + + useEffect(() => { + history.replace(`/essay-answers?${new URLSearchParams(filter).toString()}`); + }, [filter]); return (
@@ -37,13 +75,16 @@ const EssayAnswerListPage = () => { font-size: 3.4rem; `} > - {!!quiz && quiz.question} 🤔 + {selectedCurriculum} 분야의 모든 로드맵 답변
+ + + {(!essayAnswers || essayAnswers.length === 0) && '작성된 글이 없습니다.'} - {!!essayAnswers && } + {!!essayAnswers && }
); diff --git a/frontend/src/pages/QuizAnswerListPage/index.tsx b/frontend/src/pages/QuizAnswerListPage/index.tsx new file mode 100644 index 000000000..232f8e8ae --- /dev/null +++ b/frontend/src/pages/QuizAnswerListPage/index.tsx @@ -0,0 +1,56 @@ +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; +import { useParams } from 'react-router'; +import QuizAnswerList from '../../components/Lists/QuizAnswerList'; +import MEDIA_QUERY from '../../constants/mediaQuery'; +import { useGetQuiz, useGetQuizAnswerList } from '../../hooks/queries/essayanswer'; +import { MainContentStyle } from '../../PageRouter'; +import { + AlignItemsCenterStyle, + FlexStyle, + JustifyContentSpaceBtwStyle +} from '../../styles/flex.styles'; +import { HeaderContainer, PostListContainer } from './styles'; + +const QuizAnswerListPage = () => { + const { quizId } = useParams<{ quizId: string }>(); + + const { data: essayAnswers } = useGetQuizAnswerList({ quizId }); + const { data: quiz } = useGetQuiz({ quizId }); + + return ( +
+ +
+

+ {!!quiz && quiz.question} 🤔 +

+
+
+ + {(!essayAnswers || essayAnswers.length === 0) && '작성된 글이 없습니다.'} + {!!essayAnswers && } + +
+ ); +}; + +export default QuizAnswerListPage; diff --git a/frontend/src/pages/QuizAnswerListPage/styles.ts b/frontend/src/pages/QuizAnswerListPage/styles.ts new file mode 100644 index 000000000..542b66cfa --- /dev/null +++ b/frontend/src/pages/QuizAnswerListPage/styles.ts @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; +import MEDIA_QUERY from '../../constants/mediaQuery'; + +const HeaderContainer = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + + ${MEDIA_QUERY.xs} { + margin-bottom: 0.8rem; + } +`; + +const PostListContainer = styled.div` + display: grid; + grid-row-gap: 2rem; + word-break: break-all; +`; + +export { HeaderContainer, PostListContainer }; diff --git a/frontend/src/pages/RoadmapPage/index.tsx b/frontend/src/pages/RoadmapPage/index.tsx index e7d81f40b..a7e5eaf6a 100644 --- a/frontend/src/pages/RoadmapPage/index.tsx +++ b/frontend/src/pages/RoadmapPage/index.tsx @@ -9,6 +9,7 @@ import { useGetCurriculums } from '../../hooks/queries/curriculum'; import ImportanceLegend from './components/ImportanceLegend/ImportanceLegend'; import ResponsiveButton from '../../components/Button/ResponsiveButton'; import { COLOR } from '../../constants'; +import { Link } from 'react-router-dom'; const lastSeenCurriculumId = Number(localStorage.getItem('curriculumId') ?? 1); @@ -52,22 +53,25 @@ const RoadmapPage = () => {
로드맵 - - {curriculums?.map((curriculum) => ( - handleClickCurriculum(curriculum.id)} - text={curriculum.name} - color={selectedCurriculumId === curriculum.id ? COLOR.WHITE : COLOR.BLACK_600} - backgroundColor={ - selectedCurriculumId === curriculum.id - ? `hsl(${getHueHeuristically(curriculum.name)}, 50%, 40%)` - : COLOR.LIGHT_GRAY_400 - } - height="32px" - /> - ))} - + + + {curriculums?.map((curriculum) => ( + handleClickCurriculum(curriculum.id)} + text={curriculum.name} + color={selectedCurriculumId === curriculum.id ? COLOR.WHITE : COLOR.BLACK_600} + backgroundColor={ + selectedCurriculumId === curriculum.id + ? `hsl(${getHueHeuristically(curriculum.name)}, 50%, 40%)` + : COLOR.LIGHT_GRAY_400 + } + height="32px" + /> + ))} + + {selectedCurriculum && 전체 답변 보러가기} +
diff --git a/frontend/src/pages/RoadmapPage/styles.tsx b/frontend/src/pages/RoadmapPage/styles.tsx index 7b04f8cce..4019e002c 100644 --- a/frontend/src/pages/RoadmapPage/styles.tsx +++ b/frontend/src/pages/RoadmapPage/styles.tsx @@ -25,6 +25,11 @@ export const RoadmapContainer = styled.div` margin: 0 -4rem; `; +export const RoadmapHeader = styled.div` + display: flex; + justify-content: space-between; +` + export const CurriculumButtonList = styled.div` display: flex; flex-wrap: wrap; diff --git a/frontend/src/pages/StudylogListPage/index.tsx b/frontend/src/pages/StudylogListPage/index.tsx index 3d5345cfb..9f78c2c6c 100644 --- a/frontend/src/pages/StudylogListPage/index.tsx +++ b/frontend/src/pages/StudylogListPage/index.tsx @@ -73,8 +73,8 @@ const StudylogListPage = (): JSX.Element => { history.push(`${PATH.STUDYLOG}?${params.toString()}`); }; - const onSearchKeywordsChange = (event) => { - setSearchKeywords(event.target.value); + const onSearchKeywordsChange = (value: string) => { + setSearchKeywords(value); }; const onSearch = async (event) => { @@ -157,9 +157,8 @@ const StudylogListPage = (): JSX.Element => { {/* 타입스크립트 일부 적용 이슈로 인한 css 빈 string 전달 */} onSearchKeywordsChange(value)} value={searchKeywords} /> diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 3894dd208..913e65252 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -1,17 +1,18 @@ +export { default as EditEssayAnswerPage } from './EditEssayAnswerPage'; +export { default as EditStudylogPage } from './EditStudylogPage'; +export { default as EssayAnswerListPage } from './EssayAnswerListPage'; +export { default as EssayAnswerPage } from './EssayAnswerPage'; +export { default as LoginCallbackPage } from './LoginCallbackPage'; export { default as MainPage } from './MainPage'; -export { default as StudylogPage } from './StudylogPage'; +export { default as NewEssayAnswerPage } from './NewEssayAnswerPage'; export { default as NewStudylogPage } from './NewStudylogPage'; export { default as ProfilePage } from './ProfilePage'; -export { default as ProfilePageStudylogs } from './ProfilePageStudylogs'; -export { default as ProfilePageScraps } from './ProfilePageScraps'; export { default as ProfilePageAccount } from './ProfilePageAccount'; -export { default as LoginCallbackPage } from './LoginCallbackPage'; -export { default as EditStudylogPage } from './EditStudylogPage'; -export { default as StudylogListPage } from './StudylogListPage'; +export { default as ProfilePageScraps } from './ProfilePageScraps'; +export { default as ProfilePageStudylogs } from './ProfilePageStudylogs'; +export { default as QuizAnswerListPage } from './QuizAnswerListPage'; export { default as RoadmapPage } from './RoadmapPage'; -export { default as NewEssayAnswerPage } from './NewEssayAnswerPage'; -export { default as EssayAnswerPage } from './EssayAnswerPage'; -export { default as EssayAnswerListPage } from './EssayAnswerListPage'; export { default as ArticleListPage } from './ArticleListPage'; -export { default as EditEssayAnswerPage } from './EditEssayAnswerPage'; export { default as NewArticlePage } from './NewArticlePage'; +export { default as StudylogListPage } from './StudylogListPage'; +export { default as StudylogPage } from './StudylogPage'; diff --git a/frontend/src/routes.js b/frontend/src/routes.js index ba1ff6ae3..b8b0c7bfb 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -1,24 +1,29 @@ import { PATH, PROFILE_PAGE_MENU } from './constants'; import { + EditEssayAnswerPage, EditStudylogPage, + EssayAnswerListPage, + EssayAnswerPage, LoginCallbackPage, MainPage, + NewEssayAnswerPage, NewStudylogPage, - StudylogPage, ProfilePage, - ProfilePageStudylogs, ProfilePageScraps, - StudylogListPage, + ProfilePageStudylogs, + QuizAnswerListPage, RoadmapPage, - NewEssayAnswerPage, - EssayAnswerPage, - EssayAnswerListPage, - EditEssayAnswerPage, NewArticlePage, ArticleListPage, + StudylogListPage, + StudylogPage, } from './pages'; const pageRoutes = [ + { + path: '/essay-answers', + render: () => , + }, { path: [PATH.ROADMAP], render: () => , @@ -35,8 +40,14 @@ const pageRoutes = [ path: [PATH.LOGIN_CALLBACK], render: () => , }, - { path: [PATH.STUDYLOG], render: () => }, - { path: [PATH.NEW_STUDYLOG], render: () => }, + { + path: [PATH.STUDYLOG], + render: () => , + }, + { + path: [PATH.NEW_STUDYLOG], + render: () => , + }, { path: [`${PATH.STUDYLOG}/:id`], render: () => , @@ -76,12 +87,12 @@ const pageRoutes = [ }, { path: [PATH.ESSAY_ANSWER_LIST], - render: () => , + render: () => , }, { path: '/essay-answers/:id/edit', render: () => , - } + }, ]; export default pageRoutes;