Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fc276d2
feat : 명부 전체 조회 service, dto, repository 추가 완료 및 임시저장
marianddi May 23, 2026
e30feea
Merge remote-tracking branch 'origin/develop' into web-36
marianddi May 23, 2026
1f926c2
feat : 관리자 전용 명부 전체 조회 API 구현
marianddi May 23, 2026
6c5ad0d
feat : 관리자 부원 등록 API 구현
marianddi May 23, 2026
1dbf2d8
feat : 관리자 부원 상세 조회 api 구현
marianddi May 23, 2026
e2c74f5
fix : develop과 머지 후 컨플릭트 해결
marianddi May 25, 2026
fe0ad2a
fix : Member entity형태 반영하여 수정
marianddi May 25, 2026
24da152
fix : develop 최신 코드 반영 및 컨플릭트 해결
marianddi May 25, 2026
93f63c3
Merge branch 'feature/web-37-1' into feature/web-37
marianddi May 25, 2026
dcbfe30
docs : api설명 주석 등을 정확한 설명으로 수정
marianddi May 25, 2026
78b8847
Merge branch 'develop' into feature/web-37-1
marianddi May 25, 2026
037c276
Merge branch 'develop' into feature/web-36
marianddi May 25, 2026
2d6f485
Merge branch 'develop' into feature/web-37
marianddi May 25, 2026
f482458
refactor : MemberListResponseDto 스웨거 추가 및 memberId필드 추가, 이 외 주석 수정
marianddi May 28, 2026
8988833
fix : develop pull받은 후 컨플릭트 해결
marianddi May 30, 2026
c8d46de
refactor : 명부 전체 조회 api 리뷰 반영 및 구조 정리
marianddi May 30, 2026
6627c57
fix : 최신 코드 반영(web-36 pull) 후 컨플릭트 해결
marianddi May 30, 2026
da91a0a
refactor : 리뷰 반영 및 구조 수정
marianddi May 30, 2026
b83bafb
fix : 최신코드(web-37 pull) 반영 및 컨플릭트 해결
marianddi May 31, 2026
98877a8
refactor : 구조 변경 및 리뷰 반영
marianddi May 31, 2026
d4dbb39
docs : ApiErrorEceptions어노테이션에 에러 추가
marianddi May 31, 2026
4718f63
Merge branch 'feature/web-37-1' of https://github.com/DongyangOne/web…
marianddi May 31, 2026
aa918de
refactor : pagination > dto 필드 수정 및 리뷰 반영
marianddi May 31, 2026
19c28d9
refactor : web-36과 병합 후 컨플릭트 해결
marianddi May 31, 2026
da36d70
refactor : 리뷰 반영하여 수정
marianddi May 31, 2026
8dad793
fix : web-37-1 pull 받아 병합 후 컨플릭트 해결
marianddi May 31, 2026
6e552bb
refactor : 리뷰 반영 하여 수정
marianddi May 31, 2026
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
6 changes: 5 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,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;
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();
}
}
92 changes: 92 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,96 @@
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 jakarta.validation.constraints.Positive;
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.MemberDetailResponseDto;
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));
}

/**
* 특정 부원 정보 조회 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<ApiResponse<MemberDetailResponseDto>> getMemberDetail(@PathVariable Long memberId){
MemberDetailResponseDto responseDto= memberService.getMemberDetail(memberId);
return ResponseEntity.ok(ApiResponse.success(responseDto));
}
}
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
40 changes: 40 additions & 0 deletions src/main/java/org/one/member/dto/MemberDetailResponseDto.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
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;
}
Loading