Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/one/global/enums/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ 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", "이미 존재하는 전화번호입니다.");

private final HttpStatus status;
private final String code;
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/one/global/pagination/RequestPagingDto.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
30 changes: 30 additions & 0 deletions src/main/java/org/one/global/pagination/ResponsePagingDto.java
Original file line number Diff line number Diff line change
@@ -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<T> {

private List<T> content;
private Integer page;
private Integer size;
private Long totalElements;
private Integer totalPages;
private Boolean last;

public static <T> ResponsePagingDto<T> from(Page<T> page) {
return ResponsePagingDto.<T>builder()
.content(page.getContent())
.page(page.getNumber())
.size(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.last(page.isLast())
.build();
}
}
73 changes: 73 additions & 0 deletions src/main/java/org/one/member/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,77 @@
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.MemberListRequestDto;
import org.one.member.dto.MemberListResponseDto;
import org.one.member.dto.MemberRegisterRequestDto;
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<ApiResponse<List<MemberListResponseDto>>> getMemberList(
@ModelAttribute @Valid MemberListRequestDto requestDto){

List<MemberListResponseDto> 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<ApiResponse<Void>> registerMember(@RequestBody @Valid MemberRegisterRequestDto requestDto){
//등록 정보를 service로 넘겨 부원 등록 진행
memberService.registerMember(requestDto);

//오류없이 넘어왔을 경우 성공 처리
return ResponseEntity.ok(ApiResponse.success(null));
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/one/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@
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;

/**
* 정규 부원의 기본 정보, 상태, 학년 승급 기준 시각을 저장하는 엔티티입니다.
*/
@Entity
@Table(name = "member")
@Builder
@AllArgsConstructor
public class Member extends BaseEntity {

@Id
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/org/one/member/dto/MemberListRequestDto.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
42 changes: 42 additions & 0 deletions src/main/java/org/one/member/dto/MemberListResponseDto.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
41 changes: 41 additions & 0 deletions src/main/java/org/one/member/dto/MemberRegisterRequestDto.java
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions src/main/java/org/one/member/repository/MemberRepository.java
Original file line number Diff line number Diff line change
@@ -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입니다.
*/
Expand All @@ -24,4 +27,17 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
@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<Member> findAllByAdmin(Pageable pageable);

/**
* 특정 전화번호를 가진 부원이 이미 존재하는지 확인합니다.
*
* @param phoneNumber 전화번호
* @return 존재하면 true
*/
boolean existsByPhoneNumber(String phoneNumber);

}
Loading