From b355bf22191f5e67794a206794292c2957f2805d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:17:54 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=EC=B6=9C=EC=84=9D=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=20=EC=97=AC=EB=9F=AC=EB=B2=88=20=EA=B0=80=EB=8A=A5=ED=95=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(#745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 출석체크 여러번 가능한 문제 해결 * refactor: 중복 출석 에러메세지 변경 * refactor: 출석 검증 테스트 코드 메소드명 변경 --- .../application/StudentStudyService.java | 5 +++- .../study/dao/AttendanceRepository.java | 4 ++- .../study/domain/AttendanceValidator.java | 8 +++++- .../gdsc/global/exception/ErrorCode.java | 1 + .../study/domain/AttendanceValidatorTest.java | 27 ++++++++++++++++--- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java index 1daeb72b0..0bc4af0c7 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java @@ -90,11 +90,14 @@ public void attend(Long studyDetailId, StudyAttendCreateRequest request) { .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); final Member currentMember = memberUtil.getCurrentMember(); final Study study = studyDetail.getStudy(); + final boolean isAlreadyAttended = + attendanceRepository.existsByStudentIdAndStudyDetailId(currentMember.getId(), studyDetailId); final StudyHistory studyHistory = studyHistoryRepository .findByStudentAndStudy(currentMember, study) .orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND)); - attendanceValidator.validateAttendance(studyDetail, request.attendanceNumber(), LocalDate.now()); + attendanceValidator.validateAttendance( + studyDetail, request.attendanceNumber(), LocalDate.now(), isAlreadyAttended); Attendance attendance = Attendance.create(currentMember, studyDetail); attendanceRepository.save(attendance); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java index 4c8d83d20..d259ab9cd 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java @@ -3,4 +3,6 @@ import com.gdschongik.gdsc.domain.study.domain.Attendance; import org.springframework.data.jpa.repository.JpaRepository; -public interface AttendanceRepository extends JpaRepository, AttendanceCustomRepository {} +public interface AttendanceRepository extends JpaRepository, AttendanceCustomRepository { + boolean existsByStudentIdAndStudyDetailId(Long studentId, Long studyDetailId); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java index 2637fd41a..59532818c 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java @@ -8,7 +8,8 @@ @DomainService public class AttendanceValidator { - public void validateAttendance(StudyDetail studyDetail, String attendanceNumber, LocalDate date) { + public void validateAttendance( + StudyDetail studyDetail, String attendanceNumber, LocalDate date, boolean isAlreadyAttended) { // 출석체크 날짜 검증 LocalDate attendanceDay = studyDetail.getAttendanceDay(); if (!attendanceDay.equals(date)) { @@ -19,5 +20,10 @@ public void validateAttendance(StudyDetail studyDetail, String attendanceNumber, if (!studyDetail.getAttendanceNumber().equals(attendanceNumber)) { throw new CustomException(ATTENDANCE_NUMBER_MISMATCH); } + + // 출석체크 번호 검증 + if (isAlreadyAttended) { + throw new CustomException(STUDY_DETAIL_ALREADY_ATTENDED); + } } } diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index bec818fe9..069a55f0c 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -130,6 +130,7 @@ public enum ErrorCode { // Attendance ATTENDANCE_DATE_INVALID(HttpStatus.CONFLICT, "강의일이 아니면 출석체크할 수 없습니다."), ATTENDANCE_NUMBER_MISMATCH(HttpStatus.CONFLICT, "출석번호가 일치하지 않습니다."), + STUDY_DETAIL_ALREADY_ATTENDED(HttpStatus.CONFLICT, "이미 출석 처리된 스터디입니다."), // Order ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문이 존재하지 않습니다."), diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java index b39091045..a394dff05 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java @@ -3,6 +3,7 @@ import static com.gdschongik.gdsc.global.common.constant.StudyConstant.ATTENDANCE_NUMBER; import static com.gdschongik.gdsc.global.exception.ErrorCode.ATTENDANCE_DATE_INVALID; import static com.gdschongik.gdsc.global.exception.ErrorCode.ATTENDANCE_NUMBER_MISMATCH; +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_DETAIL_ALREADY_ATTENDED; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.gdschongik.gdsc.domain.member.domain.Member; @@ -36,7 +37,7 @@ class 스터디_출석체크시 { // when & then assertThatThrownBy(() -> attendanceValidator.validateAttendance( - studyDetail, ATTENDANCE_NUMBER, attendanceDay.plusDays(1))) + studyDetail, ATTENDANCE_NUMBER, attendanceDay.plusDays(1), false)) .isInstanceOf(CustomException.class) .hasMessage(ATTENDANCE_DATE_INVALID.getMessage()); } @@ -55,10 +56,30 @@ class 스터디_출석체크시 { LocalDate attendanceDay = studyDetail.getAttendanceDay(); // when & then - assertThatThrownBy(() -> - attendanceValidator.validateAttendance(studyDetail, ATTENDANCE_NUMBER + 1, attendanceDay)) + assertThatThrownBy(() -> attendanceValidator.validateAttendance( + studyDetail, ATTENDANCE_NUMBER + 1, attendanceDay, false)) .isInstanceOf(CustomException.class) .hasMessage(ATTENDANCE_NUMBER_MISMATCH.getMessage()); } + + @Test + void 이미_출석했다면_실패한다() { + // given + Member mentor = fixtureHelper.createAssociateMember(1L); + + LocalDateTime now = LocalDateTime.now(); + Period period = Period.createPeriod(now.plusDays(10), now.plusDays(65)); + Period applicationPeriod = Period.createPeriod(now.minusDays(10), now.plusDays(5)); + Study study = fixtureHelper.createStudy(mentor, period, applicationPeriod); + StudyDetail studyDetail = fixtureHelper.createStudyDetail(study, now, now.plusDays(7)); + + LocalDate attendanceDay = studyDetail.getAttendanceDay(); + + // when & then + assertThatThrownBy(() -> + attendanceValidator.validateAttendance(studyDetail, ATTENDANCE_NUMBER, attendanceDay, true)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_DETAIL_ALREADY_ATTENDED.getMessage()); + } } } From 83182137297393c45f8d5d7133a2a8e5371f5952 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:13:04 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=ED=91=9C=ED=98=84=EC=8B=9D=20=EC=B6=94=EA=B0=80=20(#7?= =?UTF-8?q?43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 깃허브 레포지토리 정규표현식 추가 * fix: 레포지토리 이름 정규표현식 수정 --- .../domain/study/dto/request/RepositoryUpdateRequest.java | 5 ++++- .../gdsc/global/common/constant/RegexConstant.java | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java index 7f311f37b..ab256e540 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java @@ -1,5 +1,8 @@ package com.gdschongik.gdsc.domain.study.dto.request; +import com.gdschongik.gdsc.global.common.constant.RegexConstant; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; -public record RepositoryUpdateRequest(@NotBlank String repositoryLink) {} +public record RepositoryUpdateRequest( + @NotBlank @Pattern(regexp = RegexConstant.GITHUB_REPOSITORY) String repositoryLink) {} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java index d2bf20ab0..6d1d05063 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java @@ -12,6 +12,8 @@ public class RegexConstant { public static final String ZONED_DATETIME = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; public static final String DATE = "yyyy-MM-dd"; public static final String ACADEMIC_YEAR = "^[0-9]{4}$"; + public static final String GITHUB_REPOSITORY = + "^https:\\/\\/github\\.com\\/[A-Za-z0-9]+(-[A-Za-z0-9]+)*\\/[A-Za-z0-9._-]+$"; public static final String ATTENDANCE_NUMBER = "^[0-9]{4}$"; From 634ebdd0ade6b3043983465bc0d4a0d86d6380e6 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:13:34 +0900 Subject: [PATCH 3/6] =?UTF-8?q?test:=201=EC=B0=A8=20=EB=AA=A8=EC=A7=91?= =?UTF-8?q?=EC=8B=9C=20=EB=A9=A4=EB=B2=84=EC=8B=AD=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=ED=9B=84=20=EC=8B=A4=EC=A0=9C=20=EA=B0=80=EC=9E=85=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EA=B2=BD=EC=9A=B0=202?= =?UTF-8?q?=EC=B0=A8=20=EB=AA=A8=EC=A7=91=20=EB=A9=A4=EB=B2=84=EC=8B=AD=20?= =?UTF-8?q?=EC=A0=91=EC=88=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#744)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/MembershipServiceTest.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/gdschongik/gdsc/domain/membership/application/MembershipServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/membership/application/MembershipServiceTest.java index de3092c77..1d940b2c9 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/membership/application/MembershipServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/membership/application/MembershipServiceTest.java @@ -2,15 +2,19 @@ import static com.gdschongik.gdsc.domain.common.model.RequirementStatus.SATISFIED; import static com.gdschongik.gdsc.domain.member.domain.MemberRole.ASSOCIATE; +import static com.gdschongik.gdsc.global.common.constant.RecruitmentConstant.*; import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import static org.assertj.core.api.Assertions.*; +import com.gdschongik.gdsc.domain.common.vo.Money; import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.membership.dao.MembershipRepository; import com.gdschongik.gdsc.domain.membership.domain.Membership; import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound; +import com.gdschongik.gdsc.domain.recruitment.domain.RoundType; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.helper.IntegrationTest; +import java.time.LocalDateTime; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,7 +42,36 @@ class 멤버십_가입신청시 { .hasMessage(RECRUITMENT_ROUND_NOT_FOUND.getMessage()); } - // todo: 1차 모집시 멤버십 생성 후 실제 가입은 하지 않고 2차 모집 시 가입하려고 하는 케이스 추가 + @Test + void 멤버십을_1차모집시_생성했지만_정회원_가입조건을_만족하지_않았다면_2차모집에서_멤버십_가입신청에_성공한다() { + // given + createMember(); + logoutAndReloginAs(1L, ASSOCIATE); + + RecruitmentRound firstRound = createRecruitmentRound( + RECRUITMENT_ROUND_NAME, + LocalDateTime.now().minusDays(5), + LocalDateTime.now().plusDays(1), + ACADEMIC_YEAR, + SEMESTER_TYPE, + ROUND_TYPE, + Money.from(20000L)); + + RecruitmentRound secondRound = createRecruitmentRound( + RECRUITMENT_ROUND_NAME, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1), + ACADEMIC_YEAR, + SEMESTER_TYPE, + RoundType.SECOND, + Money.from(20000L)); + + // when & then + membershipService.submitMembership(firstRound.getId()); + + assertThatCode(() -> membershipService.submitMembership(secondRound.getId())) + .doesNotThrowAnyException(); + } } @Test From e44cabc147976e5ab3cea1f79c6512ab1c4a8b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:38:02 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=EC=A3=BC=EC=B0=A8=EB=B3=84=20?= =?UTF-8?q?=EC=B6=9C=EA=B2=B0=20=EB=B2=88=ED=98=B8=20=EC=9D=B4=EB=B2=88?= =?UTF-8?q?=EC=A3=BC=EC=B0=A8=EC=99=80=20=EB=8B=A4=EC=9D=8C=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=EB=A7=8C=20=EB=9C=A8=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#746)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 주차별 출결 번호 이번주차와 다음주차만 뜨도록 변경 * refactor: 리스트 최소조건 추가 * refactor: 리미트 최소조건 제거 --- .../gdsc/domain/study/application/MentorStudyDetailService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java index 8d60df0ca..21c975f61 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java @@ -105,6 +105,7 @@ public List getAttendanceNumbers(Long studyId) { return studyDetails.stream() .filter(studyDetail -> studyDetail.isAttendanceDayNotPassed(LocalDate.now())) .map(StudyMentorAttendanceResponse::from) + .limit(2) .toList(); } } From 920825d718415e0cef910568f1cc174f96fc2688 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:30:49 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EA=B0=80=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=EB=A5=BC=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#750)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 레포지토리 조회 메서드가 적절한 값을 받도로 수정 * remove: 메서드 중복 호출 제거 --- .../study/application/StudentStudyHistoryService.java | 9 +-------- .../gdsc/infra/github/client/GithubClient.java | 8 +++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java index 17da03cf1..d631f3779 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -1,6 +1,5 @@ package com.gdschongik.gdsc.domain.study.application; -import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*; import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.member.domain.Member; @@ -56,8 +55,7 @@ public void updateRepository(Long studyId, RepositoryUpdateRequest request) thro boolean isAnyAssignmentSubmitted = assignmentHistoryRepository.existsSubmittedAssignmentByMemberAndStudy(currentMember, study); - String ownerRepo = getOwnerRepo(request.repositoryLink()); - GHRepository repository = githubClient.getRepository(ownerRepo); + GHRepository repository = githubClient.getRepository(request.repositoryLink()); // TODO: GHRepository 등을 wrapper로 감싸서 테스트 가능하도록 변경 studyHistoryValidator.validateUpdateRepository( isAnyAssignmentSubmitted, String.valueOf(repository.getOwner().getId()), currentMember.getOauthId()); @@ -71,11 +69,6 @@ public void updateRepository(Long studyId, RepositoryUpdateRequest request) thro request.repositoryLink()); } - private String getOwnerRepo(String repositoryLink) { - int startIndex = repositoryLink.indexOf(GITHUB_DOMAIN) + GITHUB_DOMAIN.length(); - return repositoryLink.substring(startIndex); - } - @Transactional(readOnly = true) public List getAllAssignmentHistories(Long studyId) { Member currentMember = memberUtil.getCurrentMember(); diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java index b7eb73fb5..37f3d33c1 100644 --- a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java +++ b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java @@ -30,8 +30,9 @@ public class GithubClient { private final GitHub github; private final GitHubConnector gitHubConnector = GitHubConnector.DEFAULT; - public GHRepository getRepository(String ownerRepo) { + public GHRepository getRepository(String repo) { try { + String ownerRepo = getOwnerRepo(repo); return github.getRepository(ownerRepo); } catch (IOException e) { throw new CustomException(GITHUB_REPOSITORY_NOT_FOUND); @@ -107,4 +108,9 @@ private LocalDateTime getCommitDate(GHCommit ghLatestCommit) { throw new CustomException(GITHUB_COMMIT_DATE_FETCH_FAILED); } } + + private String getOwnerRepo(String repositoryLink) { + int startIndex = repositoryLink.indexOf(GITHUB_DOMAIN) + GITHUB_DOMAIN.length(); + return repositoryLink.substring(startIndex); + } } From 6471966af583e1d101af3842c52d34abcfc90040 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:53:30 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=EB=A9=A4=EB=B2=84=EC=8B=AD=20u?= =?UTF-8?q?nique=20constraint=20=EC=B6=94=EA=B0=80=20(#748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdschongik/gdsc/domain/membership/domain/Membership.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java index 128836c8a..7a1e70789 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java +++ b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java @@ -16,6 +16,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -23,6 +25,7 @@ @Entity @Getter +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "recruitment_round_id"})}) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Membership extends BaseEntity {