From 6c9fb8a9ddcd7ce15f990a53cecb78eecf1a9697 Mon Sep 17 00:00:00 2001 From: Jae_Philip_Yang Date: Mon, 31 Jul 2023 12:46:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[BE]=20Member,=20MemberTeamPlace,=20TeamPla?= =?UTF-8?q?ce=20=EA=B5=AC=EC=A1=B0=20=EB=B0=8F=20=EC=97=B0=EA=B4=80?= =?UTF-8?q?=EA=B4=80=EA=B3=84=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#2?= =?UTF-8?q?39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: MemberTeamPlace 필드 추가 - DisplayMemberName 필드 추가 - DisplayTeamPlaceName 필드 추가 * refactor: TeamPlace -> MemberTeamPlace 의존관계 제거 * feat: 팀플레이스 소속멤버 정보 조회 repository 기능 구현 * refactor: 멤버 팀플레이스 소속 확인 인터셉터 네이밍 변경 * style: 공백줄 제거 --- ...=> TeamPlaceParticipationInterceptor.java} | 12 ++-- .../configuration/WebMvcConfiguration.java | 6 +- .../teambyteam/member/domain/Member.java | 15 ++++- .../domain/MemberIdAndDisplayNameOnly.java | 9 +++ .../member/domain/MemberTeamPlace.java | 24 ++++++-- .../domain/MemberTeamPlaceRepository.java | 7 +++ .../member/domain/vo/DisplayMemberName.java | 40 ++++++++++++ .../domain/vo/DisplayTeamPlaceName.java | 40 ++++++++++++ .../exception/MemberTeamPlaceException.java | 32 ++++++++++ .../teamplace/domain/TeamPlace.java | 22 ++----- .../domain/MemberTeamPlaceRepositoryTest.java | 61 +++++++++++++++++++ .../member/domain/MemberTeamPlaceTest.java | 29 +++++++++ .../domain/vo/DisplayMemberNameTest.java | 42 +++++++++++++ .../domain/vo/DisplayTeamPlaceNameTest.java | 42 +++++++++++++ .../TeamCalendarScheduleAcceptanceTest.java | 37 ++++++----- .../docs/TeamCalendarScheduleApiDocsTest.java | 6 +- 16 files changed, 376 insertions(+), 48 deletions(-) rename backend/src/main/java/team/teamby/teambyteam/auth/presentation/{TeamPlaceInterceptor.java => TeamPlaceParticipationInterceptor.java} (80%) create mode 100644 backend/src/main/java/team/teamby/teambyteam/member/domain/MemberIdAndDisplayNameOnly.java create mode 100644 backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberName.java create mode 100644 backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceName.java create mode 100644 backend/src/main/java/team/teamby/teambyteam/member/exception/MemberTeamPlaceException.java create mode 100644 backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepositoryTest.java create mode 100644 backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceTest.java create mode 100644 backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberNameTest.java create mode 100644 backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceNameTest.java diff --git a/backend/src/main/java/team/teamby/teambyteam/auth/presentation/TeamPlaceInterceptor.java b/backend/src/main/java/team/teamby/teambyteam/auth/presentation/TeamPlaceParticipationInterceptor.java similarity index 80% rename from backend/src/main/java/team/teamby/teambyteam/auth/presentation/TeamPlaceInterceptor.java rename to backend/src/main/java/team/teamby/teambyteam/auth/presentation/TeamPlaceParticipationInterceptor.java index 92c9037fa..f78963852 100644 --- a/backend/src/main/java/team/teamby/teambyteam/auth/presentation/TeamPlaceInterceptor.java +++ b/backend/src/main/java/team/teamby/teambyteam/auth/presentation/TeamPlaceParticipationInterceptor.java @@ -8,20 +8,20 @@ import org.springframework.web.servlet.HandlerMapping; import team.teamby.teambyteam.auth.jwt.JwtTokenExtractor; import team.teamby.teambyteam.auth.jwt.JwtTokenProvider; +import team.teamby.teambyteam.member.domain.Member; +import team.teamby.teambyteam.member.domain.MemberRepository; import team.teamby.teambyteam.member.domain.vo.Email; -import team.teamby.teambyteam.teamplace.domain.TeamPlace; -import team.teamby.teambyteam.teamplace.domain.TeamPlaceRepository; import team.teamby.teambyteam.teamplace.exception.TeamPlaceException; import java.util.Map; @Component @RequiredArgsConstructor -public final class TeamPlaceInterceptor implements HandlerInterceptor { +public final class TeamPlaceParticipationInterceptor implements HandlerInterceptor { private final JwtTokenExtractor jwtTokenExtractor; private final JwtTokenProvider jwtTokenProvider; - private final TeamPlaceRepository teamPlaceRepository; + private final MemberRepository memberRepository; @Override public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { @@ -37,8 +37,8 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp } private boolean hasNotMemberInTeamPlace(final Long teamPlaceId, final String email) { - final TeamPlace teamPlace = teamPlaceRepository.findById(teamPlaceId) + final Member member = memberRepository.findByEmail(new Email(email)) .orElseThrow(TeamPlaceException.NotFoundException::new); - return !teamPlace.hasMemberByMemberEmail(new Email(email)); + return !member.isMemberOf(teamPlaceId); } } diff --git a/backend/src/main/java/team/teamby/teambyteam/global/configuration/WebMvcConfiguration.java b/backend/src/main/java/team/teamby/teambyteam/global/configuration/WebMvcConfiguration.java index bee31f68f..a25d951f5 100644 --- a/backend/src/main/java/team/teamby/teambyteam/global/configuration/WebMvcConfiguration.java +++ b/backend/src/main/java/team/teamby/teambyteam/global/configuration/WebMvcConfiguration.java @@ -7,7 +7,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import team.teamby.teambyteam.member.configuration.MemberArgumentResolver; import team.teamby.teambyteam.auth.presentation.MemberInterceptor; -import team.teamby.teambyteam.auth.presentation.TeamPlaceInterceptor; +import team.teamby.teambyteam.auth.presentation.TeamPlaceParticipationInterceptor; import java.util.List; @@ -16,7 +16,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer { private final MemberInterceptor memberInterceptor; - private final TeamPlaceInterceptor teamPlaceInterceptor; + private final TeamPlaceParticipationInterceptor teamPlaceParticipationInterceptor; @Override public void addArgumentResolvers(final List resolvers) { @@ -28,7 +28,7 @@ public void addInterceptors(final InterceptorRegistry registry) { registry.addInterceptor(memberInterceptor) .order(1) .addPathPatterns("/api/**"); - registry.addInterceptor(teamPlaceInterceptor) + registry.addInterceptor(teamPlaceParticipationInterceptor) .order(2) .addPathPatterns("/**/team-place/**"); } diff --git a/backend/src/main/java/team/teamby/teambyteam/member/domain/Member.java b/backend/src/main/java/team/teamby/teambyteam/member/domain/Member.java index 9191bba6a..14648b271 100644 --- a/backend/src/main/java/team/teamby/teambyteam/member/domain/Member.java +++ b/backend/src/main/java/team/teamby/teambyteam/member/domain/Member.java @@ -1,6 +1,13 @@ package team.teamby.teambyteam.member.domain; -import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -51,4 +58,10 @@ public List getTeamPlaces() { .map(MemberTeamPlace::getTeamPlace) .toList(); } + + public boolean isMemberOf(final Long targetTeamPlaceId) { + return getMemberTeamPlaces().stream() + .mapToLong(memberTeamPlace -> memberTeamPlace.getMember().getId()) + .anyMatch(teamPlaceId -> teamPlaceId == targetTeamPlaceId); + } } diff --git a/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberIdAndDisplayNameOnly.java b/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberIdAndDisplayNameOnly.java new file mode 100644 index 000000000..c61dc671c --- /dev/null +++ b/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberIdAndDisplayNameOnly.java @@ -0,0 +1,9 @@ +package team.teamby.teambyteam.member.domain; + +import team.teamby.teambyteam.member.domain.vo.DisplayMemberName; + +public record MemberIdAndDisplayNameOnly( + Long id, + DisplayMemberName displayMemberName +) { +} diff --git a/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlace.java b/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlace.java index e5202fdea..d8e5f1791 100644 --- a/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlace.java +++ b/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlace.java @@ -1,9 +1,18 @@ package team.teamby.teambyteam.member.domain; -import jakarta.persistence.*; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.Getter; import lombok.NoArgsConstructor; import team.teamby.teambyteam.global.domain.BaseEntity; +import team.teamby.teambyteam.member.domain.vo.DisplayMemberName; +import team.teamby.teambyteam.member.domain.vo.DisplayTeamPlaceName; import team.teamby.teambyteam.teamplace.domain.TeamPlace; @Getter @@ -16,17 +25,24 @@ public class MemberTeamPlace extends BaseEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) + @JoinColumn(nullable = false, updatable = false) private Member member; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) + @JoinColumn(nullable = false, updatable = false) private TeamPlace teamPlace; + @Embedded + private DisplayMemberName displayMemberName; + + @Embedded + private DisplayTeamPlaceName displayTeamPlaceName; + public void setMemberAndTeamPlace(final Member member, final TeamPlace teamPlace) { this.member = member; this.teamPlace = teamPlace; + this.displayMemberName = new DisplayMemberName(member.getName().getValue()); + this.displayTeamPlaceName = new DisplayTeamPlaceName(teamPlace.getName().getValue()); member.getMemberTeamPlaces().add(this); - teamPlace.getMemberTeamPlaces().add(this); } } diff --git a/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepository.java b/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepository.java index ca84bcb73..3b1783793 100644 --- a/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepository.java +++ b/backend/src/main/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepository.java @@ -2,5 +2,12 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + public interface MemberTeamPlaceRepository extends JpaRepository { + + Optional findByTeamPlaceIdAndMemberId(Long teamPlaceId, Long memberId); + + List findAllByTeamPlaceId(Long teamPlaceId); } diff --git a/backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberName.java b/backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberName.java new file mode 100644 index 000000000..a872976bb --- /dev/null +++ b/backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberName.java @@ -0,0 +1,40 @@ +package team.teamby.teambyteam.member.domain.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.teamby.teambyteam.member.exception.MemberTeamPlaceException; + +import java.util.Objects; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +@Getter +public class DisplayMemberName { + + private static final int MAX_LENGTH = 20; + + @Column(name = "display_member_name", nullable = false, length = MAX_LENGTH) + private String value; + + public DisplayMemberName(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (Objects.isNull(value)) { + throw new NullPointerException("멤버 이름은 null일 수 없습니다."); + } + if (value.length() > MAX_LENGTH) { + throw new MemberTeamPlaceException.MemberDisplayNameLengthException(); + } + if (value.isBlank()) { + throw new MemberTeamPlaceException.MemberNameBlankException(); + } + } +} diff --git a/backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceName.java b/backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceName.java new file mode 100644 index 000000000..823e3675e --- /dev/null +++ b/backend/src/main/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceName.java @@ -0,0 +1,40 @@ +package team.teamby.teambyteam.member.domain.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.teamby.teambyteam.member.exception.MemberTeamPlaceException; + +import java.util.Objects; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +@Getter +public class DisplayTeamPlaceName { + + private static final int MAX_LENGTH = 30; + + @Column(name = "display_team_place_name", nullable = false, length = MAX_LENGTH) + private String value; + + public DisplayTeamPlaceName(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (Objects.isNull(value)) { + throw new NullPointerException("팀플레이스의 이름은 null일 수 없습니다."); + } + if (value.length() > MAX_LENGTH) { + throw new MemberTeamPlaceException.TeamPlaceDisplayNameLengthException(); + } + if (value.isBlank()) { + throw new MemberTeamPlaceException.TeamPlaceNameBlankException(); + } + } +} diff --git a/backend/src/main/java/team/teamby/teambyteam/member/exception/MemberTeamPlaceException.java b/backend/src/main/java/team/teamby/teambyteam/member/exception/MemberTeamPlaceException.java new file mode 100644 index 000000000..5f31fba56 --- /dev/null +++ b/backend/src/main/java/team/teamby/teambyteam/member/exception/MemberTeamPlaceException.java @@ -0,0 +1,32 @@ +package team.teamby.teambyteam.member.exception; + +public class MemberTeamPlaceException extends RuntimeException { + + public MemberTeamPlaceException(final String message) { + super(message); + } + + public static class MemberDisplayNameLengthException extends MemberTeamPlaceException { + public MemberDisplayNameLengthException() { + super("멤버 이름의 길이가 최대 이름 길이를 초과했습니다."); + } + } + + public static class MemberNameBlankException extends MemberTeamPlaceException { + public MemberNameBlankException() { + super("멤버 이름은 공백을 제외한 1자 이상이어야합니다."); + } + } + + public static class TeamPlaceDisplayNameLengthException extends MemberTeamPlaceException { + public TeamPlaceDisplayNameLengthException() { + super("팀플레이스의 이름의 길이가 최대 이름 길이를 초과했습니다."); + } + } + + public static class TeamPlaceNameBlankException extends MemberTeamPlaceException { + public TeamPlaceNameBlankException() { + super("팀플레이스의 이름은 공백을 제외한 1자 이상이어야합니다."); + } + } +} diff --git a/backend/src/main/java/team/teamby/teambyteam/teamplace/domain/TeamPlace.java b/backend/src/main/java/team/teamby/teambyteam/teamplace/domain/TeamPlace.java index e3acec061..9e1e843c4 100644 --- a/backend/src/main/java/team/teamby/teambyteam/teamplace/domain/TeamPlace.java +++ b/backend/src/main/java/team/teamby/teambyteam/teamplace/domain/TeamPlace.java @@ -1,17 +1,16 @@ package team.teamby.teambyteam.teamplace.domain; -import jakarta.persistence.*; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import team.teamby.teambyteam.global.domain.BaseEntity; -import team.teamby.teambyteam.member.domain.MemberTeamPlace; -import team.teamby.teambyteam.member.domain.vo.Email; import team.teamby.teambyteam.teamplace.domain.vo.Name; -import java.util.ArrayList; -import java.util.List; - @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -24,18 +23,7 @@ public class TeamPlace extends BaseEntity { @Embedded private Name name; - @OneToMany(mappedBy = "teamPlace", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) - private List memberTeamPlaces; - public TeamPlace(final Name name) { this.name = name; - this.memberTeamPlaces = new ArrayList<>(); - } - - public boolean hasMemberByMemberEmail(final Email email) { - return memberTeamPlaces.stream() - .anyMatch(memberTeamPlace -> memberTeamPlace.getMember() - .getEmail() - .equals(email)); } } diff --git a/backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepositoryTest.java b/backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepositoryTest.java new file mode 100644 index 000000000..add6d39d0 --- /dev/null +++ b/backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceRepositoryTest.java @@ -0,0 +1,61 @@ +package team.teamby.teambyteam.member.domain; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import team.teamby.teambyteam.common.RepositoryTest; +import team.teamby.teambyteam.common.fixtures.MemberFixtures; +import team.teamby.teambyteam.common.fixtures.TeamPlaceFixtures; +import team.teamby.teambyteam.member.domain.vo.DisplayMemberName; +import team.teamby.teambyteam.teamplace.domain.TeamPlace; + +import java.util.List; + +class MemberTeamPlaceRepositoryTest extends RepositoryTest { + + @Autowired + private MemberTeamPlaceRepository memberTeamPlaceRepository; + + @Test + @DisplayName("멤버아이디와 소속된 팀의 아이디로 해당 팀에서의 사용자 이름을 조회한다.") + void findMemberIdAndDisplayNameByTeamPlaceIdAndMemberId() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(MemberFixtures.PHILIP()); + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(TeamPlaceFixtures.ENGLISH_TEAM_PLACE()); + testFixtureBuilder.buildMemberTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + + // when + final MemberIdAndDisplayNameOnly actual = memberTeamPlaceRepository.findByTeamPlaceIdAndMemberId(ENGLISH_TEAM_PLACE.getId(), PHILIP.getId()).get(); + + //then + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(actual.id()).isEqualTo(PHILIP.getId()); + softly.assertThat(actual.displayMemberName().getValue()).isEqualTo(PHILIP.getName().getValue()); + }); + } + + @Test + @DisplayName("팀플레이스에 소속된 모든 멤버들의 아이디와 해당 팀에서의 사용자 이름을 조회한다.") + void findAllMemberIdAndDisplayNameByTeamPlace() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(MemberFixtures.PHILIP()); + final Member ENDLE = testFixtureBuilder.buildMember(MemberFixtures.ENDEL()); + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(TeamPlaceFixtures.ENGLISH_TEAM_PLACE()); + testFixtureBuilder.buildMemberTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + testFixtureBuilder.buildMemberTeamPlace(ENDLE, ENGLISH_TEAM_PLACE); + + // when + final List actual = memberTeamPlaceRepository.findAllByTeamPlaceId(ENGLISH_TEAM_PLACE.getId()); + final List displayNames = actual.stream() + .map(MemberIdAndDisplayNameOnly::displayMemberName) + .map(DisplayMemberName::getValue) + .toList(); + + //then + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(actual).hasSize(2); + softly.assertThat(displayNames).containsExactlyInAnyOrder(PHILIP.getName().getValue(), ENDLE.getName().getValue()); + }); + } +} diff --git a/backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceTest.java b/backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceTest.java new file mode 100644 index 000000000..fb7277cf1 --- /dev/null +++ b/backend/src/test/java/team/teamby/teambyteam/member/domain/MemberTeamPlaceTest.java @@ -0,0 +1,29 @@ +package team.teamby.teambyteam.member.domain; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import team.teamby.teambyteam.common.fixtures.MemberFixtures; +import team.teamby.teambyteam.common.fixtures.TeamPlaceFixtures; +import team.teamby.teambyteam.teamplace.domain.TeamPlace; + +class MemberTeamPlaceTest { + + @Test + @DisplayName("멤버와 팀플레이스를 통해 멤버팀플레이스를 생성시 기본 이름으로 display name들이 설정된다.") + void setDefaultDisplayName() { + // given + final Member philip = MemberFixtures.PHILIP(); + final TeamPlace japaneseTeamPlace = TeamPlaceFixtures.JAPANESE_TEAM_PLACE(); + final MemberTeamPlace memberTeamPlace = new MemberTeamPlace(); + + // when + memberTeamPlace.setMemberAndTeamPlace(philip, japaneseTeamPlace); + + //then + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(memberTeamPlace.getDisplayMemberName().getValue()).isEqualTo(philip.getName().getValue()); + softly.assertThat(memberTeamPlace.getDisplayTeamPlaceName().getValue()).isEqualTo(japaneseTeamPlace.getName().getValue()); + }); + } +} diff --git a/backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberNameTest.java b/backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberNameTest.java new file mode 100644 index 000000000..b2e830d68 --- /dev/null +++ b/backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayMemberNameTest.java @@ -0,0 +1,42 @@ +package team.teamby.teambyteam.member.domain.vo; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import team.teamby.teambyteam.member.exception.MemberTeamPlaceException; + +class DisplayMemberNameTest { + + @Test + @DisplayName("팀플레이스에 사용할 멤버의 이름으로 null value가 입력되면 예외를 발생시킨다.") + void failWithNullValue() { + // given + final String nullString = null; + + // when & then + Assertions.assertThatThrownBy(() -> new DisplayMemberName(nullString)) + .isInstanceOf(NullPointerException.class) + .hasMessage("멤버 이름은 null일 수 없습니다."); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " "}) + @DisplayName("팀플레이스에 사용할 멤버의 이름으로는 공백이 들어올 수 없다.") + void failWithBlankDisplayName(final String value) { + // when & then + Assertions.assertThatThrownBy(() -> new DisplayMemberName(value)) + .isInstanceOf(MemberTeamPlaceException.MemberNameBlankException.class) + .hasMessage("멤버 이름은 공백을 제외한 1자 이상이어야합니다."); + } + + @Test + @DisplayName("팀플레이스에 사용할 멤버의 이름으로는 20자가 초과할 수 없다.") + void failWithOverNameLength() { + // when & then + Assertions.assertThatThrownBy(() -> new DisplayMemberName(".".repeat(21))) + .isInstanceOf(MemberTeamPlaceException.MemberDisplayNameLengthException.class) + .hasMessage("멤버 이름의 길이가 최대 이름 길이를 초과했습니다."); + } +} diff --git a/backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceNameTest.java b/backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceNameTest.java new file mode 100644 index 000000000..9a899e393 --- /dev/null +++ b/backend/src/test/java/team/teamby/teambyteam/member/domain/vo/DisplayTeamPlaceNameTest.java @@ -0,0 +1,42 @@ +package team.teamby.teambyteam.member.domain.vo; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import team.teamby.teambyteam.member.exception.MemberTeamPlaceException; + +class DisplayTeamPlaceNameTest { + + @Test + @DisplayName("팀플레이스의 별명으로 null value가 입력되면 예외를 발생시킨다.") + void failWithNullValue() { + // given + final String nullString = null; + + // when & then + Assertions.assertThatThrownBy(() -> new DisplayTeamPlaceName(nullString)) + .isInstanceOf(NullPointerException.class) + .hasMessage("팀플레이스의 이름은 null일 수 없습니다."); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " "}) + @DisplayName("팀플레이스의 별명으로는 공백이 들어올 수 없다.") + void failWithBlankDisplayName(final String value) { + // when & then + Assertions.assertThatThrownBy(() -> new DisplayTeamPlaceName(value)) + .isInstanceOf(MemberTeamPlaceException.TeamPlaceNameBlankException.class) + .hasMessage("팀플레이스의 이름은 공백을 제외한 1자 이상이어야합니다."); + } + + @Test + @DisplayName("팀플레이스의 별명으로는 20자가 초과할 수 없다.") + void failWithOverNameLength() { + // when & then + Assertions.assertThatThrownBy(() -> new DisplayTeamPlaceName(".".repeat(31))) + .isInstanceOf(MemberTeamPlaceException.TeamPlaceDisplayNameLengthException.class) + .hasMessage("팀플레이스의 이름의 길이가 최대 이름 길이를 초과했습니다."); + } +} diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java index d60564832..87f09a9af 100644 --- a/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java @@ -34,10 +34,19 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static team.teamby.teambyteam.common.fixtures.MemberFixtures.PHILIP; import static team.teamby.teambyteam.common.fixtures.MemberTeamPlaceFixtures.PHILIP_ENGLISH_TEAM_PLACE; -import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.*; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE_REGISTER_REQUEST; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE_TITLE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE_UPDATE_REQUEST; import static team.teamby.teambyteam.common.fixtures.TeamPlaceFixtures.ENGLISH_TEAM_PLACE; import static team.teamby.teambyteam.common.fixtures.TeamPlaceFixtures.JAPANESE_TEAM_PLACE; -import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.*; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.DELETE_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.FIND_DAILY_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.FIND_PERIOD_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.FIND_SPECIFIC_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.REGISTER_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.UPDATE_SCHEDULE_REQUEST; public class TeamCalendarScheduleAcceptanceTest extends AcceptanceTest { @@ -270,7 +279,7 @@ void successNotExistSchedule() { } @Test - @DisplayName("조회할 팀 플레이스가 존재하지 않으면 조회에 실패한다.") + @DisplayName("조회할 팀 플레이스에 속해있지 않으면 조회에 실패한다.") void failTeamPlaceNotExist() { // given final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); @@ -284,8 +293,8 @@ void failTeamPlaceNotExist() { // then assertSoftly(softly -> { - softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); - softly.assertThat(response.body().asString()).isEqualTo("조회한 팀 플레이스가 존재하지 않습니다."); + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + softly.assertThat(response.body().asString()).isEqualTo("접근할 수 없는 팀플레이스입니다."); }); } } @@ -472,7 +481,7 @@ void failSpanWrongOrder() { } @Test - @DisplayName("없는 팀 플레이스 ID로 요청하면 실패한다.") + @DisplayName("소속되지 않는 팀 플레이스 ID로 요청하면 실패한다.") void failNotExistTeamPlaceIdRequest() { // given Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); @@ -483,8 +492,8 @@ void failNotExistTeamPlaceIdRequest() { // then assertSoftly(softly -> { - softly.assertThat(notExistTeamPlaceIdRequest.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); - softly.assertThat(notExistTeamPlaceIdRequest.body().asString()).isEqualTo("조회한 팀 플레이스가 존재하지 않습니다."); + softly.assertThat(notExistTeamPlaceIdRequest.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + softly.assertThat(notExistTeamPlaceIdRequest.body().asString()).isEqualTo("접근할 수 없는 팀플레이스입니다."); }); } @@ -579,7 +588,7 @@ void failWrongDateTimeTypeRequest(final String wrongStartDateTimeType) throws Js } @Test - @DisplayName("없는 팀 플레이스 ID로 요청하면 실패한다.") + @DisplayName("소속되지 않는 팀 플레이스 ID로 요청하면 실패한다.") void failNotExistTeamPlaceIdRequest() { // given final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); @@ -597,8 +606,8 @@ void failNotExistTeamPlaceIdRequest() { // then assertSoftly(softly -> { - softly.assertThat(notExistTeamPlaceIdRequest.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); - softly.assertThat(notExistTeamPlaceIdRequest.body().asString()).isEqualTo("조회한 팀 플레이스가 존재하지 않습니다."); + softly.assertThat(notExistTeamPlaceIdRequest.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + softly.assertThat(notExistTeamPlaceIdRequest.body().asString()).isEqualTo("접근할 수 없는 팀플레이스입니다."); }); } @@ -636,7 +645,7 @@ void success() { } @Test - @DisplayName("없는 팀 플레이스의 ID로 요청하면 실패한다.") + @DisplayName("소속되지 않는 팀 플레이스의 ID로 요청하면 실패한다.") void failNotExistTeamPlaceIdRequest() { // given final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); @@ -653,8 +662,8 @@ void failNotExistTeamPlaceIdRequest() { // then assertSoftly(softly -> { - softly.assertThat(notExistTeamPlaceIdDeleteScheduleResponse.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); - softly.assertThat(notExistTeamPlaceIdDeleteScheduleResponse.body().asString()).isEqualTo("조회한 팀 플레이스가 존재하지 않습니다."); + softly.assertThat(notExistTeamPlaceIdDeleteScheduleResponse.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + softly.assertThat(notExistTeamPlaceIdDeleteScheduleResponse.body().asString()).isEqualTo("접근할 수 없는 팀플레이스입니다."); }); } diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java index b6fba3572..ee223fef5 100644 --- a/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java @@ -16,7 +16,7 @@ import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; import team.teamby.teambyteam.auth.presentation.MemberInterceptor; -import team.teamby.teambyteam.auth.presentation.TeamPlaceInterceptor; +import team.teamby.teambyteam.auth.presentation.TeamPlaceParticipationInterceptor; import team.teamby.teambyteam.schedule.application.TeamCalendarScheduleService; import team.teamby.teambyteam.schedule.application.dto.ScheduleRegisterRequest; import team.teamby.teambyteam.schedule.application.dto.ScheduleUpdateRequest; @@ -73,13 +73,13 @@ public class TeamCalendarScheduleApiDocsTest { private MemberInterceptor memberInterceptor; @MockBean - private TeamPlaceInterceptor teamPlaceInterceptor; + private TeamPlaceParticipationInterceptor teamPlaceParticipationInterceptor; @BeforeEach void setup() throws Exception { given(memberInterceptor.preHandle(any(), any(), any())) .willReturn(true); - given(teamPlaceInterceptor.preHandle(any(), any(), any())) + given(teamPlaceParticipationInterceptor.preHandle(any(), any(), any())) .willReturn(true); } From d0909e75413276e4cd3b6357edd494d1ca83d3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=94=EC=88=A0=ED=86=A0=EB=81=BC?= Date: Mon, 31 Jul 2023 16:12:31 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[FE]=20=EA=B3=B5=ED=86=B5=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EB=B0=95=EC=8A=A4=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 체크박스 구현을 위한 체크 아이콘 svg 업로드 * feat: 체크박스 크기에 대한 타입 정의 * feat: 체크박스 컴포넌트 구현 * test: 체크박스 컴포넌트에 대한 스토리 작성 * refactor: 공용 체크박스의 size prop에 대해 기본값을 md로 지정 --- frontend/src/assets/svg/check.svg | 1 + frontend/src/assets/svg/index.ts | 1 + .../common/Checkbox/Checkbox.stories.tsx | 96 +++++++++++++++ .../common/Checkbox/Checkbox.styled.ts | 113 ++++++++++++++++++ .../components/common/Checkbox/Checkbox.tsx | 29 +++++ frontend/src/types/size.ts | 1 + 6 files changed, 241 insertions(+) create mode 100644 frontend/src/assets/svg/check.svg create mode 100644 frontend/src/components/common/Checkbox/Checkbox.stories.tsx create mode 100644 frontend/src/components/common/Checkbox/Checkbox.styled.ts create mode 100644 frontend/src/components/common/Checkbox/Checkbox.tsx diff --git a/frontend/src/assets/svg/check.svg b/frontend/src/assets/svg/check.svg new file mode 100644 index 000000000..4fc9efa02 --- /dev/null +++ b/frontend/src/assets/svg/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/index.ts b/frontend/src/assets/svg/index.ts index a478035a9..757c219b7 100644 --- a/frontend/src/assets/svg/index.ts +++ b/frontend/src/assets/svg/index.ts @@ -5,3 +5,4 @@ export { ReactComponent as DeleteIcon } from './delete.svg'; export { ReactComponent as EditIcon } from './edit.svg'; export { ReactComponent as LogoIcon } from './logo.svg'; export { ReactComponent as PlusIcon } from './plus.svg'; +export { ReactComponent as CheckIcon } from './check.svg'; diff --git a/frontend/src/components/common/Checkbox/Checkbox.stories.tsx b/frontend/src/components/common/Checkbox/Checkbox.stories.tsx new file mode 100644 index 000000000..3be2f6766 --- /dev/null +++ b/frontend/src/components/common/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,96 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Checkbox from './Checkbox'; +import { css } from 'styled-components'; + +/** + * `Checkbox`는 대부분의 상황에서 사용할 수 있는 공용 체크박스 컴포넌트입니다. + */ +const meta = { + title: 'common/Checkbox', + component: Checkbox, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Medium: Story = { + args: { + isChecked: true, + onChange: () => + alert( + '체크박스의 onChange 이벤트가 실행되었습니다! 추후 이 이벤트를 핸들링해 state를 변경한다면, 체크박스도 바뀔 것입니다!', + ), + }, +}; + +export const MediumNotChecked: Story = { + args: { + isChecked: false, + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + isChecked: true, + }, +}; + +export const SmallNotChecked: Story = { + args: { + size: 'sm', + isChecked: false, + }, +}; + +export const Large: Story = { + args: { + size: 'lg', + isChecked: true, + }, +}; + +export const LargeNotChecked: Story = { + args: { + size: 'lg', + isChecked: false, + }, +}; + +export const ExtraLarge: Story = { + args: { + size: 'xl', + isChecked: true, + }, +}; + +export const ExtraLargeNotChecked: Story = { + args: { + size: 'xl', + isChecked: false, + }, +}; + +export const CustomColor: Story = { + args: { + size: 'md', + isChecked: true, + color: '#ff8888', + }, +}; + +export const CustomStyle: Story = { + args: { + size: 'xl', + isChecked: true, + css: css` + width: 70px; + height: 50px; + + border: none; + background: linear-gradient(45deg, #00ffe5, #2600ff, #ff0ff7); + `, + }, +}; diff --git a/frontend/src/components/common/Checkbox/Checkbox.styled.ts b/frontend/src/components/common/Checkbox/Checkbox.styled.ts new file mode 100644 index 000000000..70b307d05 --- /dev/null +++ b/frontend/src/components/common/Checkbox/Checkbox.styled.ts @@ -0,0 +1,113 @@ +import { styled, css } from 'styled-components'; +import type { CheckboxSize } from '~/types/size'; +import type { CheckboxProps } from './Checkbox'; +import type { CSSProp } from 'styled-components'; + +type CustomCheckboxProps = Pick; + +type CheckIconWrapperProps = Pick; + +const checkboxSizes: Record = { + sm: css` + width: 20px; + height: 20px; + border-radius: 2px; + `, + + md: css` + width: 26px; + height: 26px; + border-radius: 3px; + `, + + lg: css` + width: 32px; + height: 32px; + border-radius: 4px; + `, + + xl: css` + width: 38px; + height: 38px; + border-radius: 5px; + `, +}; + +const checkIconSizes: Record = { + sm: css` + width: 14px; + height: 14px; + `, + + md: css` + width: 20px; + height: 20px; + `, + + lg: css` + width: 26px; + height: 26px; + `, + + xl: css` + width: 32px; + height: 32px; + `, +}; + +export const RealCheckbox = styled.input` + appearance: none; +`; + +export const CustomCheckbox = styled.span` + display: inline-block; + + ${({ size = 'md' }) => checkboxSizes[size]} + + border: 3px solid + ${({ color, theme }) => { + if (color) { + return color; + } + + return theme.color.PRIMARY; + }}; + background: transparent; + + transition: 0.2s; + cursor: pointer; + + ${RealCheckbox}:checked ~ & { + background-color: ${({ color, theme }) => { + if (color) { + return color; + } + + return theme.color.PRIMARY; + }}; + } + + ${RealCheckbox} ~ & svg { + opacity: 0; + transition: 0.2s; + } + + ${RealCheckbox}:checked ~ & svg { + opacity: 1; + } + + ${({ css }) => css}; +`; + +export const CheckIconWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + + & svg { + ${({ size = 'md' }) => checkIconSizes[size]} + } +`; diff --git a/frontend/src/components/common/Checkbox/Checkbox.tsx b/frontend/src/components/common/Checkbox/Checkbox.tsx new file mode 100644 index 000000000..2045754fb --- /dev/null +++ b/frontend/src/components/common/Checkbox/Checkbox.tsx @@ -0,0 +1,29 @@ +import * as S from './Checkbox.styled'; +import type { CSSProp } from 'styled-components'; +import type { CheckboxSize } from '~/types/size'; +import { CheckIcon } from '~/assets/svg'; + +export interface CheckboxProps { + isChecked: boolean; + color?: string; + size?: CheckboxSize; + css?: CSSProp; + onChange?: () => void; +} + +const Checkbox = (props: CheckboxProps) => { + const { isChecked, onChange, color, size = 'md', css } = props; + + return ( + + ); +}; + +export default Checkbox; diff --git a/frontend/src/types/size.ts b/frontend/src/types/size.ts index 919c4e3d9..407683fcc 100644 --- a/frontend/src/types/size.ts +++ b/frontend/src/types/size.ts @@ -10,3 +10,4 @@ export type ThreadSize = Extract; export type NotificationSize = Extract; +export type CheckboxSize = Extract; From 424523da4f13e0829cc096d143ba6aa941d3fa35 Mon Sep 17 00:00:00 2001 From: Suyoung Date: Mon, 31 Jul 2023 16:15:36 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[FE]=20MenuItem=EC=9D=98=20css=20prop=20opt?= =?UTF-8?q?ional=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Menu/MenuItem/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/Menu/MenuItem/MenuItem.tsx b/frontend/src/components/common/Menu/MenuItem/MenuItem.tsx index 108b97777..0502511eb 100644 --- a/frontend/src/components/common/Menu/MenuItem/MenuItem.tsx +++ b/frontend/src/components/common/Menu/MenuItem/MenuItem.tsx @@ -3,7 +3,7 @@ import type { PropsWithChildren } from 'react'; import type { CSSProp } from 'styled-components'; export interface MenuItemProps { - css: CSSProp; + css?: CSSProp; } const MenuItem = (props: PropsWithChildren) => {