diff --git a/build.gradle b/build.gradle index 5629fad..64dc761 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,8 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'com.h2database:h2' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/src/main/java/org/one/global/enums/ErrorCode.java b/src/main/java/org/one/global/enums/ErrorCode.java index 3990368..b0120ff 100644 --- a/src/main/java/org/one/global/enums/ErrorCode.java +++ b/src/main/java/org/one/global/enums/ErrorCode.java @@ -17,7 +17,11 @@ public enum ErrorCode { INVALID_DATE_RANGE(HttpStatus.BAD_REQUEST, "INVALID_DATE_RANGE", "날짜 범위가 올바르지 않습니다."), PHOTO_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "PHOTO_LIMIT_EXCEEDED", "사진은 최대 3장까지 업로드할 수 있습니다."), INVALID_OBJECT_KEY(HttpStatus.BAD_REQUEST, "INVALID_OBJECT_KEY", "유효하지 않은 파일 경로입니다."), - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "서버 오류가 발생했습니다."); + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "서버 오류가 발생했습니다."), + DUPLICATE_STUDENT_ID(HttpStatus.CONFLICT, "DUPLICATE_STUDENT_ID", "이미 존재하는 학번입니다."), + DUPLICATE_PHONE_NUMBER(HttpStatus.CONFLICT, "DUPLICATE_PHONE_NUMBER", "이미 존재하는 전화번호입니다."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_NOT_FOUND", "존재하지 않는 부원입니다."); + private final HttpStatus status; private final String code; diff --git a/src/main/java/org/one/global/pagination/RequestPagingDto.java b/src/main/java/org/one/global/pagination/RequestPagingDto.java new file mode 100644 index 0000000..f61ee28 --- /dev/null +++ b/src/main/java/org/one/global/pagination/RequestPagingDto.java @@ -0,0 +1,28 @@ +package org.one.global.pagination; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RequestPagingDto { + private Integer page = 0; + private Integer size = 10; + private String sort = "id"; + private String direction = "DESC"; + + public Pageable toPageable() { + Sort.Direction dir = Sort.Direction.fromString(direction); + return PageRequest.of(page, size, Sort.by(dir, sort)); + } +} \ No newline at end of file diff --git a/src/main/java/org/one/global/pagination/ResponsePagingDto.java b/src/main/java/org/one/global/pagination/ResponsePagingDto.java new file mode 100644 index 0000000..4550e0e --- /dev/null +++ b/src/main/java/org/one/global/pagination/ResponsePagingDto.java @@ -0,0 +1,30 @@ +package org.one.global.pagination; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +@Builder +public class ResponsePagingDto { + + private List content; + private Integer page; + private Integer size; + private Long totalElements; + private Integer totalPages; + private Boolean last; + + public static ResponsePagingDto from(Page page) { + return ResponsePagingDto.builder() + .content(page.getContent()) + .page(page.getNumber()) + .size(page.getSize()) + .totalElements(page.getTotalElements()) + .totalPages(page.getTotalPages()) + .last(page.isLast()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/one/member/controller/ApplicantMemberController.java b/src/main/java/org/one/member/controller/ApplicantMemberController.java new file mode 100644 index 0000000..c3dee27 --- /dev/null +++ b/src/main/java/org/one/member/controller/ApplicantMemberController.java @@ -0,0 +1,83 @@ +package org.one.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.one.global.annotation.ApiErrorExceptions; +import org.one.global.dto.ApiResponse; +import org.one.global.enums.ErrorCode; +import org.one.member.dto.ApplicantInfoResponseDto; +import org.one.member.dto.ApplicantMemberDetailResponseDto; +import org.one.member.dto.ApplicantMemberListRequestDto; +import org.one.member.dto.ApplicantMemberListResponseDto; +import org.one.member.service.ApplicantMemberService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "ApplicantMember", description = "신청 부원 관리 (관리자 전용)") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/applicantMembers") +public class ApplicantMemberController { + private final ApplicantMemberService applicantMemberService; + + + /** + * 신청 부원 조회 api + * + * api 요청 예시 : GET /api/v1/applicantMembers + * + * 응답 데이터 : 신청 부원 리스트 + */ + @ApiErrorExceptions({ErrorCode.INVALID_INPUT}) + @Operation(summary = "신청 부원 조회", description = "관리자 권한(ADMIN)이 있는 계정만 신청 부원을 조회할 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @GetMapping + public ResponseEntity>> getApplicantMemberList(@ModelAttribute @Valid ApplicantMemberListRequestDto requestDto){ + List response = applicantMemberService.getApplicantList(requestDto); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + /** + * 신청 부원 상세 정보 조회 API : 신청 부원의 상세 정보를 조회하기 위한 api + * 요청 시, applicantMemberId를 @PathVariable로 url을 통해 전달 + * + * api 요청 예시 : GET /api/v1/applicantMembers/{applicantMemberId} + * + * 응답 데이터 : 특정 신청 부원에 대한 상세 정보 + */ + @ApiErrorExceptions({ErrorCode.INVALID_INPUT, ErrorCode.MEMBER_NOT_FOUND}) + @Operation(summary = "신청 부원 상세 정보 조회", description = "관리자 권한(ADMIN)이 있는 계정만 신청 부원의 상세 정보를 조회할 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/{applicantMemberId}") + public ResponseEntity> getApplicantMemberDetail(@PathVariable Long applicantMemberId){ + ApplicantMemberDetailResponseDto responseDto= applicantMemberService.getApplicantMemberDetail(applicantMemberId); + return ResponseEntity.ok(ApiResponse.success(responseDto)); + } + + /** + * 부원(Member) 등록 시 신청 정보 불러오기 API : 신청 부원의 정보를 불러오기 위한 api + * 요청 시, applicantMemberId를 @PathVariable로 url을 통해 전달 + * + * api 요청 예시 : GET /api/v1/applicantMembers/{applicantMemberId}/registration-form + * + * 응답 데이터 : 특정 신청 부원의 등록 시 사용할 정보 + */ + @ApiErrorExceptions({ErrorCode.INVALID_INPUT, ErrorCode.MEMBER_NOT_FOUND}) + @Operation(summary = "신청 정보 불러오기", description = "관리자 권한(ADMIN)이 있는 계정만 신청 정보를 불러올 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/{applicantMemberId}/registration-form") + public ResponseEntity> getApplicantInfo(@PathVariable Long applicantMemberId){ + ApplicantInfoResponseDto responseDto = applicantMemberService.getApplicantInfo(applicantMemberId); + return ResponseEntity.ok(ApiResponse.success(responseDto)); + } + +} diff --git a/src/main/java/org/one/member/controller/MemberController.java b/src/main/java/org/one/member/controller/MemberController.java index caaa123..54e9825 100644 --- a/src/main/java/org/one/member/controller/MemberController.java +++ b/src/main/java/org/one/member/controller/MemberController.java @@ -1,4 +1,136 @@ package org.one.member.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.one.global.annotation.ApiErrorExceptions; +import org.one.global.dto.ApiResponse; +import org.one.global.enums.ErrorCode; +import org.one.member.dto.*; +import org.one.member.service.MemberService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "Member", description = "부원 명부 관리 (관리자 전용)") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members") public class MemberController { + private final MemberService memberService; + + /** + * 명부 전체 조회 API + * 요청 시, 선택적으로 page관련 설정(정렬 등) + * + * api 요청 예시 : GET /api/v1/members?page=1&size=10&sort=... + * + * 응답 데이터 : 전체 member의 명부리스트 + */ + @ApiErrorExceptions({ErrorCode.INVALID_INPUT}) + @Operation(summary = "명부 전체 조회", description = "관리자 권한(ADMIN)이 있는 계정만 전체 부원 명부를 조회할 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @GetMapping + public ResponseEntity>> getMemberList( + @ModelAttribute @Valid MemberListRequestDto requestDto){ + + List response = memberService.getMemberListByAdmin(requestDto); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + /** + * 부원 등록 API + * 요청 시, requestBody를 이용 MemberRegisterRequestDto 필드 입력 + * + * api 요청 예시 : POST /api/v1/members + * + * requestBody 예시 + * "name" : "aa", + * "grade" : 1, + * "studentId" : "20991111", + * "age" : 22, + * "phoneNum" : "010-1111-2222" + * + * 응답 데이터 : x + */ + @ApiErrorExceptions({ErrorCode.DUPLICATE_PHONE_NUMBER, ErrorCode.DUPLICATE_STUDENT_ID, ErrorCode.INVALID_INPUT}) + @Operation(summary = "부원 등록", description = "관리자 권한(ADMIN)이 있는 계정만 전체 부원 명부를 조회할 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @PostMapping + public ResponseEntity> registerMember(@RequestBody @Valid MemberRegisterRequestDto requestDto){ + //등록 정보를 service로 넘겨 부원 등록 진행 + memberService.registerMember(requestDto); + + //오류없이 넘어왔을 경우 성공 처리 + return ResponseEntity.ok(ApiResponse.success(null)); + } + + /** + * 특정 부원 정보 조회 API : 부원 수정 시 정보를 불러오기 위한 api + * 요청 시, memberId를 @PathVariable로 url을 통해 전달 + * + * api 요청 예시 : GET /api/members/{memberId} + * + * 응답 데이터 : 특정 부원에 대한 정보 + */ + @ApiErrorExceptions({ErrorCode.MEMBER_NOT_FOUND, ErrorCode.INVALID_INPUT}) + @Operation(summary = "부원 정보 가져오기", description = "관리자 권한(ADMIN)이 있는 계정만 부원 상세 정보를 조회할 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/{memberId}") + public ResponseEntity> getMemberDetail(@PathVariable Long memberId){ + MemberDetailResponseDto responseDto= memberService.getMemberDetail(memberId); + return ResponseEntity.ok(ApiResponse.success(responseDto)); + } + + /** + * 부원 정보 수정 api + * 요청 시, + * @PathVariable와 @requestBody를 통해 memberId와 MemberUpdateReqeustDto전달 + * + * api 요청 예시 : PATCH /api/members/{memberId} + * + * 응답 데이터 : 수정 완료 메시지 + */ + @ApiErrorExceptions({ErrorCode.MEMBER_NOT_FOUND, ErrorCode.INVALID_INPUT, ErrorCode.DUPLICATE_PHONE_NUMBER, ErrorCode.DUPLICATE_STUDENT_ID}) + @Operation(summary = "부원 수정", description = "관리자 권한(ADMIN)이 있는 계정만 특정 부원의 정보를 일부 수정할 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @PatchMapping("/{memberId}") + public ResponseEntity> updateMember( + @PathVariable Long memberId, + @Valid @RequestBody MemberUpdateRequestDto requestDto) + { + memberService.updateMember(memberId, requestDto); + + return ResponseEntity.ok(null); + } + + /** + * 부원 삭제 api + * 요청 시, + * @requestBody를 통해 memberId리스트를 전달 + * + * api 요청 예시 : Delete /api/members + * + * requestBody 예시 + * "memberIds" : [2, 7] + * + * 응답 데이터 : x + */ + @ApiErrorExceptions({ErrorCode.MEMBER_NOT_FOUND, ErrorCode.INVALID_INPUT}) + @Operation(summary = "부원 삭제", description = "관리자 권한(ADMIN)이 있는 계정만 부원을 삭제할 수 있습니다.") + @PreAuthorize("hasRole('ADMIN')") + @DeleteMapping + public ResponseEntity> deleteMembers(@RequestBody @Valid MemberDeleteListRequestDto requestDto){ + memberService.deleteMembers(requestDto.getMemberIds()); + return ResponseEntity.ok(ApiResponse.success(null)); + } + } diff --git a/src/main/java/org/one/member/domain/Member.java b/src/main/java/org/one/member/domain/Member.java index ddff715..bcd1ceb 100644 --- a/src/main/java/org/one/member/domain/Member.java +++ b/src/main/java/org/one/member/domain/Member.java @@ -10,6 +10,9 @@ import jakarta.persistence.Table; import java.time.LocalDateTime; import org.one.member.enums.MemberStatus; + +import lombok.AllArgsConstructor; +import lombok.Builder; import org.one.global.entity.BaseEntity; /** @@ -17,6 +20,8 @@ */ @Entity @Table(name = "member") +@Builder +@AllArgsConstructor public class Member extends BaseEntity { @Id @@ -144,12 +149,14 @@ public Member(MemberStatus status, String name, String studentId, * 부원 기본 정보를 수정합니다. * * @param name 이름 + * @param studentId 학번 * @param phoneNumber 연락처 * @param grade 학년 * @param age 나이 */ - public void updateInfo(String name, String phoneNumber, Integer grade, Integer age) { + public void updateInfo(String name,String studentId, String phoneNumber, Integer grade, Integer age) { this.name = name; + this.studentId = studentId; this.phoneNumber = phoneNumber; this.grade = grade; this.age = age; diff --git a/src/main/java/org/one/member/dto/ApplicantInfoResponseDto.java b/src/main/java/org/one/member/dto/ApplicantInfoResponseDto.java new file mode 100644 index 0000000..77c9d8a --- /dev/null +++ b/src/main/java/org/one/member/dto/ApplicantInfoResponseDto.java @@ -0,0 +1,44 @@ +package org.one.member.dto; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import org.one.member.domain.ApplicantMember; + +import java.time.LocalDate; + +@JsonPropertyOrder({"applicantId", "name","age", "studentId", "grade", "phoneNumber"}) +@Schema(description = "신청 정보 불러오기 응답") +@Getter +public class ApplicantInfoResponseDto { + @Schema(description = "신청 부원 id", example = "3") + private Long applicantId; + + @Schema(description = "신청 부원 이름", example = "홍길동") + private String name; + + @Schema(description = "신청 부원 학번", example = "20991234") + private String studentId; + + @Schema(description = "신청 부원 나이", example = "21") + private Integer age; + + @Schema(description = "신청 부원 학년", example = "3") + private Integer grade; + + @Schema(description = "신청 부원 전화번호", example = "010-1111-2222") + private String phoneNumber; + + private ApplicantInfoResponseDto(ApplicantMember applicantMember){ + this.applicantId = applicantMember.getApplicantId(); + this.name = applicantMember.getName(); + this.age = LocalDate.now().getYear() - applicantMember.getBirthday().getYear()+1; //한국식 나이 적용 + this.studentId = applicantMember.getStudentId(); + this.grade = applicantMember.getGrade(); + this.phoneNumber = applicantMember.getPhoneNumber(); + } + + public static ApplicantInfoResponseDto from(ApplicantMember applicantMember){ + return new ApplicantInfoResponseDto(applicantMember); + } +} diff --git a/src/main/java/org/one/member/dto/ApplicantMemberDetailResponseDto.java b/src/main/java/org/one/member/dto/ApplicantMemberDetailResponseDto.java new file mode 100644 index 0000000..5c65010 --- /dev/null +++ b/src/main/java/org/one/member/dto/ApplicantMemberDetailResponseDto.java @@ -0,0 +1,70 @@ +package org.one.member.dto; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import org.one.member.domain.ApplicantMember; +import org.one.member.enums.Gender; + +import java.time.LocalDate; + + +@JsonPropertyOrder({"applicantId", "name", "studentId", "department", "grade", "gender", "phoneNumber", "birthday", "techStack", "desiredActivity", "motivation", "finalWords"}) +@Schema(description = "신청 부원 상세 정보 응답") +@Getter +public class ApplicantMemberDetailResponseDto { + @Schema(description = "신청 부원 id", example = "3") + private Long applicantId; + + @Schema(description = "신청 부원 이름", example = "홍길동") + private String name; + + @Schema(description = "신청 부원 학과", example = "웹응용소프트웨어공학과") + private String department; + + @Schema(description = "신청 부원 학번", example = "20991234") + private String studentId; + + @Schema(description = "신청 부원 생년월일", example = "2001-01-01") + private LocalDate birthday; + + @Schema(description = "신청 부원 학년", example = "3") + private Integer grade; + + @Schema(description = "신청 부원 전화번호", example = "010-1111-2222") + private String phoneNumber; + + @Schema(description = "신청 부원 성별", example = "FEMALE") + private Gender gender; + + @Schema(description = "신청 부원 지원 동기", example = "웹 개발에 관심이 많아 실무적인 프로젝트 경험을 쌓고 싶어 지원했습니다.") + private String motivation; + + @Schema(description = "신청 부원 기술 스택", example = "Java, Spring") + private String techStack; + + @Schema(description = "신청 부원 희망활동", example = "프론트엔드 UI/UX 개발") + private String desiredActivity; + + @Schema(description = "신청 부원 마지막으로 하고 싶은 말", example = "잘 부탁드립니다.") + private String finalWords; + + private ApplicantMemberDetailResponseDto(ApplicantMember applicantMember){ + this.applicantId = applicantMember.getApplicantId(); + this.name = applicantMember.getName(); + this.birthday = applicantMember.getBirthday(); + this.department = applicantMember.getDepartment(); + this.studentId = applicantMember.getStudentId(); + this.motivation = applicantMember.getMotivation(); + this.grade = applicantMember.getGrade(); + this.phoneNumber = applicantMember.getPhoneNumber(); + this.gender = applicantMember.getGender(); + this.techStack = applicantMember.getTechStack(); + this.desiredActivity = applicantMember.getDesiredActivity(); + this.finalWords = applicantMember.getFinalWords(); + } + + public static ApplicantMemberDetailResponseDto from(ApplicantMember applicantMember){ + return new ApplicantMemberDetailResponseDto(applicantMember); + } +} diff --git a/src/main/java/org/one/member/dto/ApplicantMemberListRequestDto.java b/src/main/java/org/one/member/dto/ApplicantMemberListRequestDto.java new file mode 100644 index 0000000..e6a5884 --- /dev/null +++ b/src/main/java/org/one/member/dto/ApplicantMemberListRequestDto.java @@ -0,0 +1,51 @@ +package org.one.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; +import org.one.global.pagination.RequestPagingDto; + + +@Schema(description = "신청 부원 리스트 조회 요청 dto") +@Getter +public class ApplicantMemberListRequestDto extends RequestPagingDto { + //생성자를 통해 기본값설정(페이지 번호, 한번에 가져올 개수, 오래된 순) + public ApplicantMemberListRequestDto(){ + this.setPage(0); + this.setSize(15); + this.setSort("createdAt"); + this.setDirection("ASC"); + } + + @Override + @Schema(description = "페이지 번호 (0부터 시작)", example = "0") + @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다.") + public Integer getPage() { + return super.getPage(); + } + + @Override + @Schema(description = "한 페이지에 보여줄 부원 수 [15 고정]", example = "15") + @Min(value = 15, message = "명부 조회의 페이지 크기는 15로 고정되어 있습니다.") + @Max(value = 15, message = "명부 조회의 페이지 크기는 15로 고정되어 있습니다.") + public Integer getSize() { + return super.getSize(); + } + + @Override + @Schema(description = "정렬 기준 필드 [createdAt 고정]", example = "createdAt") + @Pattern(regexp = "^(createdAt)$", message = "정렬 기준은 createdAt만 가능합니다.") + public String getSort() { + return super.getSort(); + } + + @Override + @Schema(description = "정렬 방향 [ASC 고정]", example = "ASC") + @Pattern(regexp = "^(ASC)$", message = "정렬 방향은 ASC만 가능합니다.") + public String getDirection() { + return super.getDirection(); + } +} diff --git a/src/main/java/org/one/member/dto/ApplicantMemberListResponseDto.java b/src/main/java/org/one/member/dto/ApplicantMemberListResponseDto.java new file mode 100644 index 0000000..1cb90c2 --- /dev/null +++ b/src/main/java/org/one/member/dto/ApplicantMemberListResponseDto.java @@ -0,0 +1,40 @@ +package org.one.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import org.one.member.domain.ApplicantMember; + +import java.time.LocalDateTime; + +/** + * 신청 부원 리스트 조회 시 리스트의 요소가 될 데이터 구조 + */ +@Schema(description = "신청 부원 리스트 요소 데이터 구조") +@Getter +public class ApplicantMemberListResponseDto { + @Schema(description = "신청 부원 id", example = "3") + private Long applicantId; + @Schema(description = "신청 부원 이름", example = "홍길동") + private String name; + @Schema(description = "신청 부원 학번", example = "20991234") + private String studentId; + @Schema(description = "신청 부원 전화번호", example = "3") + private String phoneNum; + @Schema(description = "신청 부원 신청 날짜", example = "2026-08-08") + private LocalDateTime createdAt; + @Schema(description = "신청 부원 정보 조회 여부", example = "true") + private Boolean isFirstView; + + private ApplicantMemberListResponseDto(ApplicantMember applicantMember){ + this.applicantId = applicantMember.getApplicantId(); + this.name = applicantMember.getName(); + this.studentId = applicantMember.getStudentId(); + this.phoneNum = applicantMember.getPhoneNumber(); + this.createdAt = applicantMember.getCreatedAt(); + this.isFirstView = applicantMember.getIsFirstView(); + } + + public static ApplicantMemberListResponseDto from(ApplicantMember applicantMember){ + return new ApplicantMemberListResponseDto(applicantMember); + } +} diff --git a/src/main/java/org/one/member/dto/MemberDeleteListRequestDto.java b/src/main/java/org/one/member/dto/MemberDeleteListRequestDto.java new file mode 100644 index 0000000..2a03d20 --- /dev/null +++ b/src/main/java/org/one/member/dto/MemberDeleteListRequestDto.java @@ -0,0 +1,18 @@ +package org.one.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Schema(description = "부원 삭제 요청") +@Getter +public class MemberDeleteListRequestDto { + @Schema(description = "삭제할 부원(들)의 id리스트", example = "[1, 2, 7]") + @NotEmpty(message = "삭제할 부원(들)의 id리스트를 입력해주세요.") + private List<@NotNull(message = "id는 비어있을 수 없습니다.") @Positive(message="올바르지 않은 부원의 id가 포함되어있습니다.") Long> memberIds; +} diff --git a/src/main/java/org/one/member/dto/MemberDetailResponseDto.java b/src/main/java/org/one/member/dto/MemberDetailResponseDto.java new file mode 100644 index 0000000..ede5767 --- /dev/null +++ b/src/main/java/org/one/member/dto/MemberDetailResponseDto.java @@ -0,0 +1,40 @@ +package org.one.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import org.one.member.domain.Member; + + +/** + * 특정 부원 정보 조회 성공 시 응답하는 dto + */ +@Schema(description = "부원 상세 정보 응답") +@Getter +public class MemberDetailResponseDto { + @Schema(description = "부원 이름", example = "홍길동") + private String name; + + @Schema(description = "부원 학년", example = "2") + private Integer grade; + + @Schema(description = "부원 학번", example = "20991234") + private String studentId; + + @Schema(description = "부원 나이", example = "22") + private Integer age; + + @Schema(description = "부원 전화번호", example = "010-1111-2222") + private String phoneNum; + + private MemberDetailResponseDto(Member member){ + this.name = member.getName(); + this.grade = member.getGrade(); + this.studentId = member.getStudentId(); + this.age = member.getAge(); + this.phoneNum = member.getPhoneNumber(); + } + + public static MemberDetailResponseDto from(Member member){ + return new MemberDetailResponseDto(member); + } +} diff --git a/src/main/java/org/one/member/dto/MemberListRequestDto.java b/src/main/java/org/one/member/dto/MemberListRequestDto.java new file mode 100644 index 0000000..73f590d --- /dev/null +++ b/src/main/java/org/one/member/dto/MemberListRequestDto.java @@ -0,0 +1,51 @@ +package org.one.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import org.one.global.pagination.RequestPagingDto; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +@Schema(description = "명부 리스트 조회 요청(페이지 설정)") +public class MemberListRequestDto extends RequestPagingDto { + public MemberListRequestDto(){ + //기본값 세팅 + this.setPage(0); + this.setSize(15); + this.setSort("createdAt"); + this.setDirection("ASC"); + } + + @Override + @Schema(description = "페이지 번호 (0부터 시작)", example = "0") + @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다.") + public Integer getPage() { + return super.getPage(); + } + + @Override + @Schema(description = "한 페이지에 보여줄 부원 수 [15 고정]", example = "15") + @Min(value = 15, message = "명부 조회의 페이지 크기는 15로 고정되어 있습니다.") + @Max(value = 15, message = "명부 조회의 페이지 크기는 15로 고정되어 있습니다.") + public Integer getSize() { + return super.getSize(); + } + + @Override + @Schema(description = "정렬 기준 필드", example = "createdAt") + @Pattern(regexp = "^(createdAt|grade)$", message = "정렬 기준은 createdAt 또는 grade만 가능합니다.") + public String getSort() { + return super.getSort(); + } + + @Override + @Schema(description = "정렬 방향", example = "ASC") + @Pattern(regexp = "^(ASC|DESC)$", message = "정렬 방향은 ASC 또는 DESC만 가능합니다.") + public String getDirection() { + return super.getDirection(); + } +} diff --git a/src/main/java/org/one/member/dto/MemberListResponseDto.java b/src/main/java/org/one/member/dto/MemberListResponseDto.java new file mode 100644 index 0000000..39f6984 --- /dev/null +++ b/src/main/java/org/one/member/dto/MemberListResponseDto.java @@ -0,0 +1,42 @@ +package org.one.member.dto; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import org.one.member.domain.Member; +import org.one.member.enums.MemberStatus; + + +@JsonPropertyOrder({"memberId", "name", "age", "studentId", "grade", "phoneNumber", "status"}) +@Schema(description = "명부 리스트 조회 응답") +@Getter +public class MemberListResponseDto { + @Schema(description = "부원 id", example = "2") + private Long memberId; + @Schema(description = "부원 이름", example = "홍길동") + private String name; + @Schema(description = "부원 학번", example = "20991234") + private String studentId; + @Schema(description = "부원 학년", example = "4") + private Integer grade; + @Schema(description = "부원 나이", example = "22") + private Integer age; + @Schema(description = "부원 전화번호", example = "010-1234-5678") + private String phoneNumber; + @Schema(description = "부원 활동 상태", example = "ACTIVE") + private MemberStatus status; + + private MemberListResponseDto(Member member){ + this.memberId = member.getMemberId(); + this.name = member.getName(); + this.studentId = member.getStudentId(); + this.grade = member.getGrade(); + this.age = member.getAge(); + this.phoneNumber = member.getPhoneNumber(); + this.status = member.getStatus(); + } + + public static MemberListResponseDto from(Member member){ + return new MemberListResponseDto(member); + } +} diff --git a/src/main/java/org/one/member/dto/MemberRegisterRequestDto.java b/src/main/java/org/one/member/dto/MemberRegisterRequestDto.java new file mode 100644 index 0000000..9ec2d0c --- /dev/null +++ b/src/main/java/org/one/member/dto/MemberRegisterRequestDto.java @@ -0,0 +1,41 @@ +package org.one.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "새로운 부원 등록 요청") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MemberRegisterRequestDto { + + @Schema(description = "부원 이름", example = "홍길동") + @NotBlank(message = "이름을 입력해주세요.") + @Size(min = 2, max = 20, message = "이름은 2~20자 사이로 입력해주세요.") + private String name; + + @Schema(description = "부원 학년", example = "1") + @NotNull(message = "학년을 선택해주세요.") + @Min(value = 1, message = "학년은 1학년 이상이어야 합니다.") + @Max(value = 4, message = "학년은 4학년 이하이어야 합니다.") + private Integer grade; + + @Schema(description = "부원 학번", example = "20991111") + @NotBlank(message = "학번을 입력해주세요.") + @Pattern(regexp = "^\\d{8}$", message = "학번은 8자리의 숫자만 입력 가능합니다.") + private String studentId; + + @Schema(description = "부원 나이", example = "22") + @NotNull(message = "나이를 입력해주세요.") + @Min(value = 18, message = "나이는 18세 이상이어야 합니다.") + @Max(value = 100, message = "나이가 올바르지 않습니다.") + private Integer age; + + @Schema(description = "부원 전화번호", example = "010-1111-2222") + @NotBlank(message = "전화번호를 입력해주세요.") + @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다. (예: 010-1234-5678)") + private String phoneNum; +} \ No newline at end of file diff --git a/src/main/java/org/one/member/dto/MemberUpdateRequestDto.java b/src/main/java/org/one/member/dto/MemberUpdateRequestDto.java new file mode 100644 index 0000000..7465470 --- /dev/null +++ b/src/main/java/org/one/member/dto/MemberUpdateRequestDto.java @@ -0,0 +1,40 @@ +package org.one.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "부원 정보 수정 요청") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MemberUpdateRequestDto { + @Schema(description = "부원 이름", example = "홍길동") + @NotBlank(message = "이름을 입력해주세요.") + @Size(min = 2, max = 20, message = "이름은 2~20자 사이로 입력해주세요.") + private String name; + + @Schema(description = "부원 학년", example = "1") + @NotNull(message = "학년을 선택해주세요.") + @Min(value = 1, message = "학년은 1학년 이상이어야 합니다.") + @Max(value = 4, message = "학년은 4학년 이하이어야 합니다.") + private Integer grade; + + @Schema(description = "부원 학번", example = "20991111") + @NotBlank(message = "학번을 입력해주세요.") + @Pattern(regexp = "^\\d{8}$", message = "학번은 8자리의 숫자만 입력 가능합니다.") + private String studentId; + + @Schema(description = "부원 나이", example = "22") + @NotNull(message = "나이를 입력해주세요.") + @Min(value = 18, message = "나이는 18세 이상이어야 합니다.") + @Max(value = 100, message = "나이가 올바르지 않습니다.") + private Integer age; + + @Schema(description = "부원 전화번호", example = "010-1111-2222") + @NotBlank(message = "전화번호를 입력해주세요.") + @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다. (예: 010-1234-5678)") + private String phoneNum; +} diff --git a/src/main/java/org/one/member/repository/ApplicantMemberRepository.java b/src/main/java/org/one/member/repository/ApplicantMemberRepository.java index 6db4ec0..89bb1bd 100644 --- a/src/main/java/org/one/member/repository/ApplicantMemberRepository.java +++ b/src/main/java/org/one/member/repository/ApplicantMemberRepository.java @@ -2,6 +2,9 @@ import java.time.LocalDateTime; import org.one.member.domain.ApplicantMember; +import java.util.List; + +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -20,4 +23,11 @@ public interface ApplicantMemberRepository extends JpaRepository findAllBy(Pageable pageable); } diff --git a/src/main/java/org/one/member/repository/MemberRepository.java b/src/main/java/org/one/member/repository/MemberRepository.java index 2b8da64..51263e4 100644 --- a/src/main/java/org/one/member/repository/MemberRepository.java +++ b/src/main/java/org/one/member/repository/MemberRepository.java @@ -1,10 +1,13 @@ package org.one.member.repository; import org.one.member.domain.Member; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import java.util.List; + /** * 정규 부원 데이터 조회와 일괄 갱신을 담당하는 JPA Repository입니다. */ @@ -24,4 +27,17 @@ public interface MemberRepository extends JpaRepository { @Modifying @Query("UPDATE Member m SET m.grade = m.grade + 1, m.age = m.age + 1") void incrementGradeAndAge(); + + //동아리에 소속중인 부원들의 모든 정보를 가져와 리스트로 만듦. + @Query("SELECT m FROM Member m") + List findAllByAdmin(Pageable pageable); + + /** + * 특정 전화번호를 가진 부원이 이미 존재하는지 확인합니다. + * + * @param phoneNumber 전화번호 + * @return 존재하면 true + */ + boolean existsByPhoneNumber(String phoneNumber); + } diff --git a/src/main/java/org/one/member/service/ApplicantMemberService.java b/src/main/java/org/one/member/service/ApplicantMemberService.java new file mode 100644 index 0000000..eed1092 --- /dev/null +++ b/src/main/java/org/one/member/service/ApplicantMemberService.java @@ -0,0 +1,69 @@ +package org.one.member.service; + +import lombok.RequiredArgsConstructor; +import org.one.global.enums.ErrorCode; +import org.one.global.exception.BusinessException; +import org.one.member.domain.ApplicantMember; +import org.one.member.dto.ApplicantInfoResponseDto; +import org.one.member.dto.ApplicantMemberDetailResponseDto; +import org.one.member.dto.ApplicantMemberListRequestDto; +import org.one.member.dto.ApplicantMemberListResponseDto; +import org.one.member.repository.ApplicantMemberRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ApplicantMemberService { + private final ApplicantMemberRepository applicantMemberRepository; + + /** + * 신청부원 리스트를 모두 가져옴. + * @Param ApplicantMemberListRequestDto : 페이지 설정 정보(정렬, 개수 등) 전달 + */ + public List getApplicantList(ApplicantMemberListRequestDto requestDto){ + Pageable pageable = requestDto.toPageable(); + + List applicantMemberList = applicantMemberRepository.findAllBy(pageable); + + //엔티티에서 dto형태로 구조를 변환하여 리스트를 만들어 반환 + return applicantMemberList.stream() + .map(ApplicantMemberListResponseDto::from) + .toList(); + } + + + /** + * 요청값(memberId)를 통해 해당 신청 부원의 상세 정보를 불러옴. + * Param : applicantMemberId + * return : ApplicantMemberDetailResponseDto + */ + public ApplicantMemberDetailResponseDto getApplicantMemberDetail(Long applicantMemberId){ + if(applicantMemberId == null || applicantMemberId <= 0){ + throw new BusinessException(ErrorCode.INVALID_INPUT); + } + ApplicantMember applicantMember = applicantMemberRepository.findById(applicantMemberId) + .orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); + return ApplicantMemberDetailResponseDto.from(applicantMember); + } + + /** + * 요청값(memberId)를 통해 등록에 사용할 신청 정보를 불러옴. + * Param : applicantMemberId + * return : ApplicantInfoResponseDto + */ + public ApplicantInfoResponseDto getApplicantInfo(Long applicantMemberId){ + if(applicantMemberId == null || applicantMemberId <= 0){ + throw new BusinessException(ErrorCode.INVALID_INPUT); + } + ApplicantMember applicantMember = applicantMemberRepository.findById(applicantMemberId) + .orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); + return ApplicantInfoResponseDto.from(applicantMember); + } + +} diff --git a/src/main/java/org/one/member/service/MemberService.java b/src/main/java/org/one/member/service/MemberService.java new file mode 100644 index 0000000..14d7f21 --- /dev/null +++ b/src/main/java/org/one/member/service/MemberService.java @@ -0,0 +1,134 @@ +package org.one.member.service; + + +import lombok.RequiredArgsConstructor; +import org.one.auth.repository.AdminRepository; +import org.one.global.enums.ErrorCode; +import org.one.global.exception.BusinessException; +import org.one.member.domain.Member; +import org.one.member.dto.*; +import org.one.member.enums.MemberStatus; +import org.one.member.repository.MemberRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberService { + private final MemberRepository memberRepository; + private final AdminRepository adminRepository; + + public List getMemberListByAdmin(MemberListRequestDto requestDto) { + //requestDto로 설정한 sort, size 등을 바탕으로 Pageable객체를 만듦. + Pageable pageable = requestDto.toPageable(); + + //memberRepository를 이용해 모든 부원 리스트를 가져옴. + List members = memberRepository.findAllByAdmin(pageable); + + //모든 부원 리스트를 Member(entity) -> MemberListResponseDto로 필요한 데이터만 빼서 리스트를 만듦. + return members.stream() + .map(MemberListResponseDto::from) + .toList(); + } + + /** + * 새로운 부원을 추가함 + * Param : MemberRegisterRequestDto 부원 추가 dto + */ + @Transactional + public void registerMember(MemberRegisterRequestDto requestDto){ + //학번 중복 시 예외처리 + if(memberRepository.existsByStudentId(requestDto.getStudentId())) { + throw new BusinessException(ErrorCode.DUPLICATE_STUDENT_ID); + } + + //전화번호 중복 시 예외처리 + if(memberRepository.existsByPhoneNumber(requestDto.getPhoneNum())) { + throw new BusinessException(ErrorCode.DUPLICATE_PHONE_NUMBER); + } + + //요청데이터인 dto를 사용하여 새로운 Member엔티티 설정 + Member member = Member.builder() + .name(requestDto.getName()) + .grade(requestDto.getGrade()) + .studentId(requestDto.getStudentId()) + .age(requestDto.getAge()) + .phoneNumber(requestDto.getPhoneNum()) + .status(MemberStatus.ACTIVE) //status는 기본 값(활동중) + .build(); + + //새로 생성한 부원 객체를 save(insert)해줌. + memberRepository.save(member); + } + + /** + * 요청값(memberId)를 통해 해당 부원의 정보를 불러옴. + * Param : memberId + * return : MemberDetailResponseDto + */ + public MemberDetailResponseDto getMemberDetail(Long memberId){ + if(memberId == null || memberId <= 0){ + throw new BusinessException(ErrorCode.INVALID_INPUT); + } + + Member member = memberRepository.findById(memberId) + .orElseThrow(()->new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); + return MemberDetailResponseDto.from(member); + } + + @Transactional + public void updateMember(Long memberId, MemberUpdateRequestDto requestDto){ + if(memberId == null || memberId <= 0){ + throw new BusinessException(ErrorCode.INVALID_INPUT); + } + + Member member = memberRepository.findById(memberId) + .orElseThrow(()->new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); + + //중복 값 예외처리(학번, 전화번호) + if(!member.getStudentId().equals(requestDto.getStudentId())) { + if(memberRepository.existsByStudentId(requestDto.getStudentId())) { + throw new BusinessException(ErrorCode.DUPLICATE_STUDENT_ID); + } + } + if(!member.getPhoneNumber().equals(requestDto.getPhoneNum())) { + if(memberRepository.existsByPhoneNumber(requestDto.getPhoneNum())) { + throw new BusinessException(ErrorCode.DUPLICATE_PHONE_NUMBER); + } + } + + member.updateInfo(requestDto.getName(), requestDto.getStudentId(), requestDto.getPhoneNum(), requestDto.getGrade(), requestDto.getAge()); + } + + /** + * 요청 값(memberIds)에 들어있는 id를 가진 데이터들을 삭제 + * Param : memberIds + */ + @Transactional + public void deleteMembers(List memberIds) + { + //리스트 내 중복값 있을 경우 예외처리 + Set uniqueIds = new HashSet<>(memberIds); + if (memberIds.size() != uniqueIds.size()) { + throw new BusinessException(ErrorCode.INVALID_INPUT); + } + //리스트 내 id 중 db에 없는 값이 하나라도 있을 경우 예외처리 + List existingMembers = memberRepository.findAllById(uniqueIds); + if (existingMembers.size() != uniqueIds.size()) { + throw new BusinessException(ErrorCode.MEMBER_NOT_FOUND); + } + + memberRepository.deleteAllByIdInBatch(memberIds); + } + + + + +}