diff --git a/.github/workflows/pull_request_gradle_build.yml b/.github/workflows/pull_request_gradle_build.yml index c01e2b432..dabd4105b 100644 --- a/.github/workflows/pull_request_gradle_build.yml +++ b/.github/workflows/pull_request_gradle_build.yml @@ -4,13 +4,16 @@ on: pull_request: branches: ["develop"] +permissions: + pull-requests: write + jobs: build-test: runs-on: ubuntu-latest steps: - name: Git Checkout - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v4 - name: JDK 설치 uses: actions/setup-java@v4 @@ -25,8 +28,16 @@ jobs: - name: Start containers run: docker-compose -f ./docker-compose-test.yaml up -d - - name: Gradle Build - uses: gradle/gradle-build-action@v2 + - name: Build with Gradle + id: gradle + uses: gradle/actions/setup-gradle@v3 with: - arguments: check - cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + arguments: | + check + --configuration-cache + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} # feature 브랜치는 캐시를 읽기 전용으로 설정 + cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + add-job-summary-as-pr-comment: always + build-scan-publish: true + build-scan-terms-of-service-url: "https://gradle.com/terms-of-service" + build-scan-terms-of-service-agree: "yes" diff --git a/build.gradle b/build.gradle index 21b2e716a..55883f404 100644 --- a/build.gradle +++ b/build.gradle @@ -31,12 +31,16 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + // Spring Security implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index 139e6f896..28b747dd5 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -4,7 +4,7 @@ import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; import com.gdschongik.gdsc.domain.member.dto.request.MemberGrantRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberPaymentRequest; -import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; +import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryOption; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.AdminMemberResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberGrantResponse; @@ -37,8 +37,8 @@ public class AdminMemberController { @Operation(summary = "전체 회원 목록 조회", description = "전체 회원 목록을 조회합니다.") @GetMapping - public ResponseEntity> getMembers(MemberQueryRequest queryRequest, Pageable pageable) { - Page response = adminMemberService.findAll(queryRequest, pageable); + public ResponseEntity> getMembers(MemberQueryOption queryOption, Pageable pageable) { + Page response = adminMemberService.findAll(queryOption, pageable); return ResponseEntity.ok().body(response); } @@ -52,8 +52,8 @@ public ResponseEntity withdrawMember(@PathVariable Long memberId) { @Operation(summary = "대기중인 회원 목록 조회", description = "대기중인 회원 목록을 조회합니다.") @GetMapping("/pending") public ResponseEntity> getPendingMembers( - MemberQueryRequest queryRequest, Pageable pageable) { - Page response = adminMemberService.findAllPendingMembers(queryRequest, pageable); + MemberQueryOption queryOption, Pageable pageable) { + Page response = adminMemberService.findAllPendingMembers(queryOption, pageable); return ResponseEntity.ok().body(response); } @@ -75,19 +75,19 @@ public ResponseEntity grantMember(@Valid @RequestBody Membe @Operation(summary = "승인 가능 회원 전체 조회", description = "승인 가능한 회원 전체를 조회합니다.") @GetMapping("/grantable") public ResponseEntity> getGrantableMembers( - MemberQueryRequest queryRequest, Pageable pageable) { - Page response = adminMemberService.getGrantableMembers(queryRequest, pageable); + MemberQueryOption queryOption, Pageable pageable) { + Page response = adminMemberService.getGrantableMembers(queryOption, pageable); return ResponseEntity.ok().body(response); } @Operation(summary = "회비 납부 상태에 따른 회원 전체 조회", description = "회비 납부 상태에 따라 회원 목록을 조회합니다.") @GetMapping("/payment") public ResponseEntity> getMembersByPaymentStatus( - MemberQueryRequest queryRequest, + MemberQueryOption queryOption, @RequestParam(name = "status", required = false) RequirementStatus paymentStatus, Pageable pageable) { Page response = - adminMemberService.getMembersByPaymentStatus(queryRequest, paymentStatus, pageable); + adminMemberService.getMembersByPaymentStatus(queryOption, paymentStatus, pageable); return ResponseEntity.ok().body(response); } @@ -102,8 +102,8 @@ public ResponseEntity updatePayment( @Operation(summary = "승인된 회원 전체 조회", description = "승인된 회원 전체를 조회합니다.") @GetMapping("/granted") public ResponseEntity> getGrantedMembers( - MemberQueryRequest queryRequest, Pageable pageable) { - Page response = adminMemberService.findAllGrantedMembers(queryRequest, pageable); + MemberQueryOption queryOption, Pageable pageable) { + Page response = adminMemberService.findAllGrantedMembers(queryOption, pageable); return ResponseEntity.ok().body(response); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java index 76f55ac08..243e6917f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java @@ -8,7 +8,7 @@ import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; import com.gdschongik.gdsc.domain.member.dto.request.MemberGrantRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberPaymentRequest; -import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; +import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryOption; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.AdminMemberResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberGrantResponse; @@ -32,8 +32,8 @@ public class AdminMemberService { private final MemberRepository memberRepository; private final ExcelUtil excelUtil; - public Page findAll(MemberQueryRequest queryRequest, Pageable pageable) { - Page members = memberRepository.findAllByRole(queryRequest, pageable, null); + public Page findAll(MemberQueryOption queryOption, Pageable pageable) { + Page members = memberRepository.findAllByRole(queryOption, pageable, null); return members.map(AdminMemberResponse::from); } @@ -57,8 +57,8 @@ public void updateMember(Long memberId, MemberUpdateRequest request) { request.nickname()); } - public Page findAllPendingMembers(MemberQueryRequest queryRequest, Pageable pageable) { - Page members = memberRepository.findAllByRole(queryRequest, pageable, GUEST); + public Page findAllPendingMembers(MemberQueryOption queryOption, Pageable pageable) { + Page members = memberRepository.findAllByRole(queryOption, pageable, GUEST); return members.map(AdminMemberResponse::from); } @@ -70,14 +70,14 @@ public MemberGrantResponse grantMember(MemberGrantRequest request) { return MemberGrantResponse.from(classifiedMember); } - public Page getGrantableMembers(MemberQueryRequest queryRequest, Pageable pageable) { - Page members = memberRepository.findAllGrantable(queryRequest, pageable); + public Page getGrantableMembers(MemberQueryOption queryOption, Pageable pageable) { + Page members = memberRepository.findAllGrantable(queryOption, pageable); return members.map(AdminMemberResponse::from); } public Page getMembersByPaymentStatus( - MemberQueryRequest queryRequest, RequirementStatus paymentStatus, Pageable pageable) { - Page members = memberRepository.findAllByPaymentStatus(queryRequest, paymentStatus, pageable); + MemberQueryOption queryOption, RequirementStatus paymentStatus, Pageable pageable) { + Page members = memberRepository.findAllByPaymentStatus(queryOption, paymentStatus, pageable); return members.map(AdminMemberResponse::from); } @@ -87,8 +87,8 @@ public void updatePaymentStatus(Long memberId, MemberPaymentRequest request) { member.updatePaymentStatus(request.status()); } - public Page findAllGrantedMembers(MemberQueryRequest queryRequest, Pageable pageable) { - Page members = memberRepository.findAllByRole(queryRequest, pageable, USER); + public Page findAllGrantedMembers(MemberQueryOption queryOption, Pageable pageable) { + Page members = memberRepository.findAllByRole(queryOption, pageable, USER); return members.map(AdminMemberResponse::from); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java index 85f8da311..e48be9528 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java @@ -1,5 +1,7 @@ package com.gdschongik.gdsc.domain.member.application; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.member.dto.request.MemberSignupRequest; @@ -7,7 +9,6 @@ import com.gdschongik.gdsc.domain.member.dto.response.MemberInfoResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberUnivStatusResponse; import com.gdschongik.gdsc.global.exception.CustomException; -import com.gdschongik.gdsc.global.exception.ErrorCode; import com.gdschongik.gdsc.global.util.MemberUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -38,12 +39,15 @@ public void updateMember(OnboardingMemberUpdateRequest request) { private void validateDiscordUsernameDuplicate(Member member) { if (memberRepository.existsByDiscordUsername(member.getDiscordUsername())) { - throw new CustomException(ErrorCode.MEMBER_DISCORD_USERNAME_DUPLICATE); + throw new CustomException(MEMBER_DISCORD_USERNAME_DUPLICATE); } } public MemberInfoResponse getMemberInfo() { Member currentMember = memberUtil.getCurrentMember(); + if (!currentMember.isApplied()) { + throw new CustomException(MEMBER_NOT_APPLIED); + } return MemberInfoResponse.of(currentMember); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java index fc7e5b3ea..3d38a932e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java @@ -3,7 +3,7 @@ import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.member.domain.MemberRole; import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; -import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; +import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryOption; import jakarta.annotation.Nullable; import java.util.List; import java.util.Map; @@ -14,12 +14,12 @@ public interface MemberCustomRepository { Optional findNormalByOauthId(String oauthId); - Page findAllGrantable(MemberQueryRequest queryRequest, Pageable pageable); + Page findAllGrantable(MemberQueryOption queryOption, Pageable pageable); - Page findAllByRole(MemberQueryRequest queryRequest, Pageable pageable, @Nullable MemberRole role); + Page findAllByRole(MemberQueryOption queryOption, Pageable pageable, @Nullable MemberRole role); Page findAllByPaymentStatus( - MemberQueryRequest queryRequest, RequirementStatus paymentStatus, Pageable pageable); + MemberQueryOption queryOption, RequirementStatus paymentStatus, Pageable pageable); Map> groupByVerified(List memberIdList); diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java index feebf0633..4b52752af 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java @@ -1,18 +1,12 @@ package com.gdschongik.gdsc.domain.member.dao; import static com.gdschongik.gdsc.domain.member.domain.QMember.*; -import static com.gdschongik.gdsc.domain.member.domain.RequirementStatus.*; import static com.querydsl.core.group.GroupBy.*; -import com.gdschongik.gdsc.domain.member.domain.Department; import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.member.domain.MemberRole; -import com.gdschongik.gdsc.domain.member.domain.MemberStatus; import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; -import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; -import com.querydsl.core.BooleanBuilder; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.EnumPath; +import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryOption; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.annotation.Nullable; @@ -27,27 +21,21 @@ import org.springframework.data.support.PageableExecutionUtils; @RequiredArgsConstructor -public class MemberCustomRepositoryImpl implements MemberCustomRepository { +public class MemberCustomRepositoryImpl extends MemberQueryMethod implements MemberCustomRepository { private final JPAQueryFactory queryFactory; @Override public Optional findNormalByOauthId(String oauthId) { - return Optional.ofNullable(queryFactory - .selectFrom(member) - .where(eqOauthId(oauthId), eqStatus(MemberStatus.NORMAL)) - .fetchOne()); + return Optional.ofNullable( + queryFactory.selectFrom(member).where(eqOauthId(oauthId)).fetchOne()); } @Override - public Page findAllGrantable(MemberQueryRequest queryRequest, Pageable pageable) { + public Page findAllGrantable(MemberQueryOption queryOption, Pageable pageable) { List fetch = queryFactory .selectFrom(member) - .where( - queryOption(queryRequest), - eqStatus(MemberStatus.NORMAL), - eqRole(MemberRole.GUEST), - requirementVerified()) + .where(matchesQueryOption(queryOption), eqRole(MemberRole.GUEST), isGrantAvailable()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .orderBy(member.createdAt.desc()) @@ -56,20 +44,16 @@ public Page findAllGrantable(MemberQueryRequest queryRequest, Pageable p JPAQuery countQuery = queryFactory .select(member.count()) .from(member) - .where( - queryOption(queryRequest), - eqStatus(MemberStatus.NORMAL), - eqRole(MemberRole.GUEST), - requirementVerified()); + .where(matchesQueryOption(queryOption), eqRole(MemberRole.GUEST), isGrantAvailable()); return PageableExecutionUtils.getPage(fetch, pageable, countQuery::fetchOne); } @Override - public Page findAllByRole(MemberQueryRequest queryRequest, Pageable pageable, @Nullable MemberRole role) { + public Page findAllByRole(MemberQueryOption queryOption, Pageable pageable, @Nullable MemberRole role) { List fetch = queryFactory .selectFrom(member) - .where(queryOption(queryRequest), eqRole(role), eqStatus(MemberStatus.NORMAL), isStudentIdNotNull()) + .where(matchesQueryOption(queryOption), eqRole(role), isStudentIdNotNull()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .orderBy(member.createdAt.desc()) @@ -78,19 +62,18 @@ public Page findAllByRole(MemberQueryRequest queryRequest, Pageable page JPAQuery countQuery = queryFactory .select(member.count()) .from(member) - .where(queryOption(queryRequest), eqRole(role), eqStatus(MemberStatus.NORMAL), isStudentIdNotNull()); + .where(matchesQueryOption(queryOption), eqRole(role), isStudentIdNotNull()); return PageableExecutionUtils.getPage(fetch, pageable, countQuery::fetchOne); } @Override public Page findAllByPaymentStatus( - MemberQueryRequest queryRequest, RequirementStatus paymentStatus, Pageable pageable) { + MemberQueryOption queryOption, RequirementStatus paymentStatus, Pageable pageable) { List fetch = queryFactory .selectFrom(member) .where( - queryOption(queryRequest), - eqStatus(MemberStatus.NORMAL), + matchesQueryOption(queryOption), eqRequirementStatus(member.requirement.paymentStatus, paymentStatus), isStudentIdNotNull()) .offset(pageable.getOffset()) @@ -102,8 +85,7 @@ public Page findAllByPaymentStatus( .select(member.count()) .from(member) .where( - queryOption(queryRequest), - eqStatus(MemberStatus.NORMAL), + matchesQueryOption(queryOption), eqRequirementStatus(member.requirement.paymentStatus, paymentStatus), isStudentIdNotNull()); @@ -115,20 +97,11 @@ public Map> groupByVerified(List memberIdList) { Map> groupByVerified = queryFactory .selectFrom(member) .where(member.id.in(memberIdList)) - .transform(groupBy(requirementVerified()).as(list(member))); + .transform(groupBy(isGrantAvailable()).as(list(member))); return replaceNullByEmptyList(groupByVerified); } - @Override - public List findAllByRole(MemberRole role) { - return queryFactory - .selectFrom(member) - .where(eqRole(role), eqStatus(MemberStatus.NORMAL), isStudentIdNotNull()) - .orderBy(member.studentId.asc(), member.name.asc()) - .fetch(); - } - private Map> replaceNullByEmptyList(Map> groupByVerified) { Map> classifiedMember = new HashMap<>(); List emptyList = new ArrayList<>(); @@ -137,77 +110,12 @@ private Map> replaceNullByEmptyList(Map requirement, RequirementStatus requirementStatus) { - return requirementStatus != null ? requirement.eq(requirementStatus) : null; - } - - private BooleanExpression eqId(Long id) { - return member.id.eq(id); - } - - private BooleanExpression eqOauthId(String oauthId) { - return member.oauthId.eq(oauthId); - } - - private BooleanExpression eqStatus(MemberStatus status) { - return member.status.eq(status); - } - - private BooleanBuilder queryOption(MemberQueryRequest queryRequest) { - BooleanBuilder booleanBuilder = new BooleanBuilder(); - - return booleanBuilder - .and(eqStudentId(queryRequest.studentId())) - .and(eqName(queryRequest.name())) - .and(eqPhone(queryRequest.phone())) - .and(inDepartmentList(Department.getDepartmentCodes(queryRequest.department()))) - .and(eqEmail(queryRequest.email())) - .and(eqDiscordUsername(queryRequest.discordUsername())) - .and(eqNickname(queryRequest.nickname())); - } - - private BooleanExpression inDepartmentList(List departmentCodes) { - return departmentCodes != null ? member.department.in(departmentCodes) : null; - } - - private BooleanExpression eqStudentId(String studentId) { - return studentId != null ? member.studentId.containsIgnoreCase(studentId) : null; - } - - private BooleanExpression eqName(String name) { - return name != null ? member.name.containsIgnoreCase(name) : null; - } - - private BooleanExpression eqPhone(String phone) { - return phone != null ? member.phone.contains(phone.replaceAll("-", "")) : null; - } - - private BooleanExpression eqEmail(String email) { - return email != null ? member.email.containsIgnoreCase(email) : null; - } - - private BooleanExpression eqDiscordUsername(String discordUsername) { - return discordUsername != null ? member.discordUsername.containsIgnoreCase(discordUsername) : null; - } - - private BooleanExpression eqNickname(String nickname) { - return nickname != null ? member.nickname.containsIgnoreCase(nickname) : null; - } - - private BooleanExpression isStudentIdNotNull() { - return member.studentId.isNotNull(); + @Override + public List findAllByRole(MemberRole role) { + return queryFactory + .selectFrom(member) + .where(eqRole(role), isStudentIdNotNull()) + .orderBy(member.studentId.asc(), member.name.asc()) + .fetch(); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberQueryMethod.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberQueryMethod.java new file mode 100644 index 000000000..172eb661d --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberQueryMethod.java @@ -0,0 +1,80 @@ +package com.gdschongik.gdsc.domain.member.dao; + +import static com.gdschongik.gdsc.domain.member.domain.QMember.*; +import static com.gdschongik.gdsc.domain.member.domain.RequirementStatus.*; + +import com.gdschongik.gdsc.domain.member.domain.Department; +import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; +import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryOption; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.EnumPath; +import java.util.List; + +public class MemberQueryMethod { + + protected BooleanExpression eqRole(MemberRole role) { + return role != null ? member.role.eq(role) : null; + } + + protected BooleanExpression eqStudentId(String studentId) { + return studentId != null ? member.studentId.containsIgnoreCase(studentId) : null; + } + + protected BooleanExpression eqName(String name) { + return name != null ? member.name.containsIgnoreCase(name) : null; + } + + protected BooleanExpression eqPhone(String phone) { + return phone != null ? member.phone.contains(phone.replaceAll("-", "")) : null; + } + + protected BooleanExpression eqEmail(String email) { + return email != null ? member.email.containsIgnoreCase(email) : null; + } + + protected BooleanExpression eqDiscordUsername(String discordUsername) { + return discordUsername != null ? member.discordUsername.containsIgnoreCase(discordUsername) : null; + } + + protected BooleanExpression eqNickname(String nickname) { + return nickname != null ? member.nickname.containsIgnoreCase(nickname) : null; + } + + protected BooleanExpression eqOauthId(String oauthId) { + return member.oauthId.eq(oauthId); + } + + protected BooleanExpression eqRequirementStatus( + EnumPath requirement, RequirementStatus requirementStatus) { + return requirementStatus != null ? requirement.eq(requirementStatus) : null; + } + + protected BooleanExpression inDepartmentList(List departmentCodes) { + return departmentCodes != null ? member.department.in(departmentCodes) : null; + } + + protected BooleanExpression isStudentIdNotNull() { + return member.studentId.isNotNull(); + } + + protected BooleanBuilder isGrantAvailable() { + return new BooleanBuilder() + .and(eqRequirementStatus(member.requirement.discordStatus, VERIFIED)) + .and(eqRequirementStatus(member.requirement.univStatus, VERIFIED)) + .and(eqRequirementStatus(member.requirement.paymentStatus, VERIFIED)) + .and(eqRequirementStatus(member.requirement.bevyStatus, VERIFIED)); + } + + protected BooleanBuilder matchesQueryOption(MemberQueryOption queryOption) { + return new BooleanBuilder() + .and(eqStudentId(queryOption.studentId())) + .and(eqName(queryOption.name())) + .and(eqPhone(queryOption.phone())) + .and(inDepartmentList(Department.searchDepartments(queryOption.department()))) + .and(eqEmail(queryOption.email())) + .and(eqDiscordUsername(queryOption.discordUsername())) + .and(eqNickname(queryOption.nickname())); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java index 184a77168..650f353ea 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java @@ -1,8 +1,8 @@ package com.gdschongik.gdsc.domain.member.domain; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.Optional; import lombok.AllArgsConstructor; import lombok.Getter; @@ -87,13 +87,15 @@ public enum Department { D076("역사교육과"), D077("영어교육과"); - private String departmentName; + private final String departmentName; - public static List getDepartmentCodes(String keyword) { - return Optional.ofNullable(keyword) - .map(s -> Arrays.stream(Department.values()) - .filter(department -> department.getDepartmentName().contains(s)) - .toList()) - .orElse(null); + public static List searchDepartments(String keyword) { + if (keyword == null) { + return Collections.emptyList(); + } + + return Arrays.stream(Department.values()) + .filter(department -> department.getDepartmentName().contains(keyword)) + .toList(); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index 6273b3793..e6607c02f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -18,9 +18,11 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter +@SQLRestriction("status='NORMAL'") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member extends BaseTimeEntity { @@ -238,6 +240,11 @@ public boolean isGranted() { return role.equals(USER) || role.equals(MemberRole.ADMIN); } + /** + * 회원 승인 가능 여부를 반환합니다. + * + * @see com.gdschongik.gdsc.domain.member.dao.MemberQueryMethod#isGrantAvailable() + */ public boolean isGrantAvailable() { try { validateGrantAvailable(); @@ -247,6 +254,13 @@ public boolean isGrantAvailable() { } } + /** + * 가입 신청서 제출 여부를 반환합니다. + */ + public boolean isApplied() { + return studentId != null; + } + // 기타 로직 public void updateLastLoginAt() { diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryOption.java similarity index 95% rename from src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java rename to src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryOption.java index 56ac95fa8..26daf58e2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryOption.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record MemberQueryRequest( +public record MemberQueryOption( @Schema(description = "학번", pattern = STUDENT_ID) String studentId, @Schema(description = "이름") String name, @Schema(description = "전화번호", pattern = PHONE_WITHOUT_HYPHEN) String phone, diff --git a/src/main/java/com/gdschongik/gdsc/global/config/JavaMailSenderConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/JavaMailSenderConfig.java index ea5d21267..7f1b3ff6b 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/JavaMailSenderConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/JavaMailSenderConfig.java @@ -1,7 +1,7 @@ package com.gdschongik.gdsc.global.config; -import com.gdschongik.gdsc.global.property.email.EmailProperty; -import com.gdschongik.gdsc.global.property.email.EmailProperty.Gmail; +import com.gdschongik.gdsc.global.property.EmailProperty; +import com.gdschongik.gdsc.global.property.EmailProperty.Gmail; import java.util.Properties; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java index 107edc5fe..b03a1b5d2 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java @@ -2,9 +2,9 @@ import com.gdschongik.gdsc.global.property.BasicAuthProperty; import com.gdschongik.gdsc.global.property.DiscordProperty; +import com.gdschongik.gdsc.global.property.EmailProperty; import com.gdschongik.gdsc.global.property.JwtProperty; import com.gdschongik.gdsc.global.property.RedisProperty; -import com.gdschongik.gdsc.global.property.email.EmailProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; 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 ee7e8ce32..6d2d73c3c 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -33,6 +33,7 @@ public enum ErrorCode { MEMBER_ALREADY_VERIFIED(HttpStatus.CONFLICT, "이미 인증된 상태입니다."), MEMBER_DISCORD_USERNAME_DUPLICATE(HttpStatus.CONFLICT, "이미 등록된 디스코드 유저네임입니다."), MEMBER_NICKNAME_DUPLICATE(HttpStatus.CONFLICT, "이미 사용중인 닉네임입니다."), + MEMBER_NOT_APPLIED(HttpStatus.CONFLICT, "가입신청서를 제출하지 않은 회원입니다."), // Requirement UNIV_NOT_VERIFIED(HttpStatus.CONFLICT, "재학생 인증이 완료되지 않았습니다."), diff --git a/src/main/java/com/gdschongik/gdsc/global/property/email/EmailProperty.java b/src/main/java/com/gdschongik/gdsc/global/property/EmailProperty.java similarity index 91% rename from src/main/java/com/gdschongik/gdsc/global/property/email/EmailProperty.java rename to src/main/java/com/gdschongik/gdsc/global/property/EmailProperty.java index f66ca581a..03a6f9f05 100644 --- a/src/main/java/com/gdschongik/gdsc/global/property/email/EmailProperty.java +++ b/src/main/java/com/gdschongik/gdsc/global/property/EmailProperty.java @@ -1,4 +1,4 @@ -package com.gdschongik.gdsc.global.property.email; +package com.gdschongik.gdsc.global.property; import java.util.Map; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java b/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java index 3abc80e72..98901a2c1 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java @@ -1,11 +1,13 @@ package com.gdschongik.gdsc.global.security; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.MemberRole; import java.time.LocalDate; import java.time.LocalDateTime; public enum LandingStatus { ONBOARDING_NOT_OPENED, // 대기 페이지로 랜딩 + ONBOARDING_CLOSED, // 모집 기간 마감 TO_STUDENT_AUTHENTICATION, // 재학생 인증 페이지로 랜딩 TO_REGISTRATION, // 가입신청 페이지로 랜딩 TO_DASHBOARD, // 대시보드로 랜딩 @@ -19,6 +21,17 @@ public static LandingStatus of(Member member) { return ONBOARDING_NOT_OPENED; } + // 2차 모집기간 종료일 12시 30분 이후, 신청서 미제출 상태면 마감 페이지로 랜딩 + if (LocalDateTime.now().isAfter(Constants.SECOND_RECRUITMENT_END_DATE.atTime(0, 30)) && !member.isApplied()) { + return ONBOARDING_CLOSED; + } + + // 2차 모집기간 종료일 1시 이후, Guest를 마감 페이지로 랜딩. + if (LocalDateTime.now().isAfter(Constants.SECOND_RECRUITMENT_END_DATE.atTime(1, 0)) + && member.getRole().equals(MemberRole.GUEST)) { + return ONBOARDING_CLOSED; + } + // 아직 재학생 인증을 하지 않았다면 재학생 인증 페이지로 랜딩 if (!member.getRequirement().isUnivVerified()) { return TO_STUDENT_AUTHENTICATION; @@ -26,7 +39,7 @@ public static LandingStatus of(Member member) { // 재학생 인증은 했지만 가입신청을 하지 않았다면 가입신청 페이지로 랜딩 // 가입신청 여부는 학번 존재여부로 판단 - if (member.getStudentId() == null) { + if (!member.isApplied()) { return TO_REGISTRATION; } @@ -37,5 +50,6 @@ public static LandingStatus of(Member member) { private static class Constants { private static final LocalDate FIRST_RECRUITMENT_END_DATE = LocalDate.of(2024, 3, 2); private static final LocalDate SECOND_RECRUITMENT_START_DATE = LocalDate.of(2024, 3, 4); + private static final LocalDate SECOND_RECRUITMENT_END_DATE = LocalDate.of(2024, 3, 9); } } diff --git a/src/test/java/com/gdschongik/gdsc/config/TestQuerydslConfig.java b/src/test/java/com/gdschongik/gdsc/config/TestQuerydslConfig.java index 621c310c4..c8e932d30 100644 --- a/src/test/java/com/gdschongik/gdsc/config/TestQuerydslConfig.java +++ b/src/test/java/com/gdschongik/gdsc/config/TestQuerydslConfig.java @@ -1,5 +1,6 @@ package com.gdschongik.gdsc.config; +import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -13,6 +14,6 @@ public class TestQuerydslConfig { @Bean public JPAQueryFactory jpaQueryFactory() { - return new JPAQueryFactory(entityManager); + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); } } diff --git a/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java index 65b349fad..48eef3197 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java @@ -8,14 +8,12 @@ import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; +import com.gdschongik.gdsc.integration.IntegrationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -@SpringBootTest -@ActiveProfiles("test") -class AdminMemberServiceTest { +class AdminMemberServiceTest extends IntegrationTest { + @Autowired private MemberRepository memberRepository; @@ -34,6 +32,6 @@ class AdminMemberServiceTest { "A111111", "name", "010-1234-5678", Department.D001, "email@email.com", "discordUsername", "한글"); assertThatThrownBy(() -> adminMemberService.updateMember(member.getId(), requestBody)) .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.MEMBER_DELETED.getMessage()); + .hasMessage(ErrorCode.MEMBER_NOT_FOUND.getMessage()); } } diff --git a/src/test/java/com/gdschongik/gdsc/integration/DatabaseCleaner.java b/src/test/java/com/gdschongik/gdsc/integration/DatabaseCleaner.java new file mode 100644 index 000000000..e6070531a --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/integration/DatabaseCleaner.java @@ -0,0 +1,48 @@ +package com.gdschongik.gdsc.integration; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.metamodel.EntityType; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import org.hibernate.Session; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +@Component +public class DatabaseCleaner implements InitializingBean { + + @PersistenceContext + private EntityManager em; + + private List tableNames; + + @Override + public void afterPropertiesSet() { + em.unwrap(Session.class).doWork(this::extractTableNames); + } + + private void extractTableNames(Connection conn) throws SQLException { + tableNames = em.getMetamodel().getEntities().stream() + .map(EntityType::getName) + .toList(); + } + + public void execute() { + em.unwrap(Session.class).doWork(this::cleanTables); + } + + private void cleanTables(Connection conn) throws SQLException { + Statement statement = conn.createStatement(); + statement.executeUpdate("SET REFERENTIAL_INTEGRITY FALSE"); + + for (String name : tableNames) { + statement.executeUpdate(String.format("TRUNCATE TABLE %s", name)); + statement.executeUpdate(String.format("ALTER TABLE %s ALTER COLUMN %s_id RESTART WITH 1", name, name)); + } + + statement.executeUpdate("SET REFERENTIAL_INTEGRITY TRUE"); + } +} diff --git a/src/test/java/com/gdschongik/gdsc/integration/IntegrationTest.java b/src/test/java/com/gdschongik/gdsc/integration/IntegrationTest.java new file mode 100644 index 000000000..c8634f8b6 --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/integration/IntegrationTest.java @@ -0,0 +1,19 @@ +package com.gdschongik.gdsc.integration; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +public abstract class IntegrationTest { + + @Autowired + protected DatabaseCleaner databaseCleaner; + + @BeforeEach + void setUp() { + databaseCleaner.execute(); + } +}