Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.ject.support.admin.member.controller;

import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
import org.ject.support.admin.member.dto.request.MemberSemesterSearchCondition;
import org.ject.support.admin.member.dto.response.SearchMemberSemesterResponse;
import org.ject.support.common.response.CursorPageResponse;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;

import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -14,4 +18,12 @@ public interface AdminMemberSemesterApiSpec {
description = "일반 구성원을 추가합니다."
)
void createAdminMemberSemester(@RequestBody @Valid CreateMemberSemesterRequest request);

@Operation(
summary = "일반 구성원 리스트 조회",
description = "일반 구성원 목록을 커서기반 페이징과 필터를 적용해 조회합니다."
)
CursorPageResponse<SearchMemberSemesterResponse> searchAdminMemberSemesterList(
@ModelAttribute @Valid MemberSemesterSearchCondition request
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package org.ject.support.admin.member.controller;

import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
import org.ject.support.admin.member.dto.request.MemberSemesterSearchCondition;
import org.ject.support.admin.member.dto.response.SearchMemberSemesterResponse;
import org.ject.support.admin.member.service.AdminMemberUseCase;
import org.ject.support.common.response.CursorPageResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -15,10 +22,19 @@ public class AdminMemberSemesterController implements AdminMemberSemesterApiSpec

private final AdminMemberUseCase adminMemberUsecase;

//일반 구성원 추가
// 일반 구성원 추가
@Override
@PostMapping
public void createAdminMemberSemester(CreateMemberSemesterRequest request) {
public void createAdminMemberSemester(
@RequestBody @Valid CreateMemberSemesterRequest request) {
adminMemberUsecase.createMemberSemester(request);
}

// 일반 구성원 목록 조회
@Override
@GetMapping
public CursorPageResponse<SearchMemberSemesterResponse> searchAdminMemberSemesterList(
@ModelAttribute @Valid MemberSemesterSearchCondition request) {
return adminMemberUsecase.searchMemberSemester(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.ject.support.admin.member.dto.projection;

import org.ject.support.domain.member.ActivityStatus;
import org.ject.support.domain.member.CareerDetails;
import org.ject.support.domain.member.ExperiencePeriod;
import org.ject.support.domain.member.JobFamily;

public record SearchMemberSemesterProjection(
Long memberActivityId,
String name,
JobFamily jobFamily,
String phoneNumber,
CareerDetails careerDetails,
ExperiencePeriod experiencePeriod,
ActivityStatus status
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.ject.support.admin.member.dto.request;

import java.util.List;

import org.ject.support.domain.member.ActivityStatus;
import org.ject.support.domain.member.CareerDetails;
import org.ject.support.domain.member.JobFamily;
import org.ject.support.domain.recruit.domain.RecruitTypeDetail;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;

public record MemberSemesterSearchCondition(

//-- 페이징 --

//MemberActivityId 기준 내림차순
@Positive
Long cursor,

// 정책 변경에 대비해 여유롭게 설정
@Min(1)
@Max(100)
Integer size,

//-- 필터 --

// 기수
@Positive
Long semesterId,
// 직군 (다중 선택)
List<JobFamily> jobFamilies,
// 모집 단위 (다중 선택)
List<RecruitTypeDetail> recruitTypeDetails,
// 신분 (다중 선택)
List<CareerDetails> careerDetails,
// 팀 (다중 선택)
List<@Positive Long> teamIds,
// 활동 상태 (다중 선택)
List<ActivityStatus> statuses
) {
//size가 없으면 기본 30개
public int getSizeOrDefault() {
return size == null ? 30 : size;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.ject.support.admin.member.dto.response;

import org.ject.support.admin.member.dto.projection.SearchMemberSemesterProjection;
import org.ject.support.domain.member.ActivityStatus;
import org.ject.support.domain.member.CareerDetails;
import org.ject.support.domain.member.ExperiencePeriod;
import org.ject.support.domain.member.JobFamily;

public record SearchMemberSemesterResponse(
Long memberActivityId,
String name,
JobFamily jobFamily,
String phoneNumber,
CareerDetails careerDetails,
ExperiencePeriod experiencePeriod,
ActivityStatus status
) {
public static SearchMemberSemesterResponse from(SearchMemberSemesterProjection projection) {
return new SearchMemberSemesterResponse(
projection.memberActivityId(),
projection.name(),
projection.jobFamily(),
projection.phoneNumber(),
projection.careerDetails(),
projection.experiencePeriod(),
projection.status()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.ject.support.admin.member.dto.result;

import java.util.List;

import org.ject.support.admin.member.dto.projection.SearchMemberSemesterProjection;

public record SearchMemberSemesterPageResult(
List<SearchMemberSemesterProjection> content,
long totalCount
) {
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package org.ject.support.admin.member.service;

import org.ject.support.admin.member.dto.projection.SearchMemberSemesterProjection;
import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
import org.ject.support.admin.member.dto.request.MemberSemesterSearchCondition;
import org.ject.support.admin.member.dto.result.SearchMemberSemesterPageResult;
import org.ject.support.domain.member.MemberType;
import org.ject.support.domain.member.entity.MemberActivity;
import org.ject.support.domain.member.exception.MemberException;
import org.ject.support.domain.member.repository.MemberActivityRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

import static org.ject.support.domain.member.exception.MemberErrorCode.ALREADY_EXIST_MEMBER_SEMESTER_ACTIVITY;

import java.util.List;

@Service
@RequiredArgsConstructor
public class AdminMemberActivityService {
Expand All @@ -37,6 +43,20 @@ public void createMemberSemesterActivity(CreateMemberSemesterRequest request, Lo
memberActivityRepository.save(memberActivity);
}

// 동적 필터로 일반 구성원 목록 조회
public SearchMemberSemesterPageResult searchMemberSemesterList(MemberSemesterSearchCondition condition) {
// size+1로 조회 (다음 페이지 유무 확인)
List<SearchMemberSemesterProjection> projections =
memberActivityRepository.searchMemberSemesters(condition, condition.getSizeOrDefault()+1);
long totalCount = memberActivityRepository.countMemberSemesters(condition);
return new SearchMemberSemesterPageResult(projections, totalCount);
}


/*
유틸, 검증 함수
*/

// 동일 기수 일반 구성원 활동 중복 검증
private void validateDuplicateSemesterActivity(Long memberId, Long semesterId) {
if (memberActivityRepository.existsSemesterActivity(memberId, MemberType.SEMESTER, semesterId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class AdminMemberService {
(2026.06.23)
1. email로 삭제 포함 구성원 신상 전부 조회
2. 삭제된 구성원 존재시: isDeleted를 복구하고 새로 입력받은 값으로 덮어쓰기
3. 활성 구성원 존재시: 기존 값 재사용
3. 활성 구성원 존재시: 기존 값 재사용 Todo: 대체 가능한 값은 새로운 입력으로 대체
4. 미 존재시: 새로 생성
*/

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
package org.ject.support.admin.member.service;

import static org.ject.support.domain.member.exception.MemberErrorCode.NOT_FOUND_TEAM_OF_SEMESTER;
import static org.ject.support.domain.member.exception.MemberErrorCode.*;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.ject.support.admin.member.dto.projection.SearchMemberSemesterProjection;
import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
import org.ject.support.admin.member.dto.request.MemberSemesterSearchCondition;
import org.ject.support.admin.member.dto.response.SearchMemberSemesterResponse;
import org.ject.support.admin.member.dto.result.SearchMemberSemesterPageResult;
import org.ject.support.common.response.CursorPageResponse;
import org.ject.support.domain.member.ActivityStatus;
import org.ject.support.domain.member.MemberType;
import org.ject.support.domain.member.exception.MemberException;
import org.ject.support.domain.recruit.service.SemesterInquiryUsecase;
import org.springframework.stereotype.Service;
Expand All @@ -22,7 +33,7 @@ public class AdminMemberUseCase {
// 일반 구성원 생성 흐름 처리
@Transactional
public void createMemberSemester(CreateMemberSemesterRequest request) {
// 기수 유효성 검증
// 유효성 검증
semesterInquiryUsecase.getSemester(request.semesterId());
validateTeam(request.semesterId(), request.teamId());

Expand All @@ -33,6 +44,56 @@ public void createMemberSemester(CreateMemberSemesterRequest request) {
adminMemberActivityService.createMemberSemesterActivity(request, memberId);
}


// 일반 구성원 목록 조회 (커서 기반 페이징)
@Transactional(readOnly=true)
public CursorPageResponse<SearchMemberSemesterResponse> searchMemberSemester(
MemberSemesterSearchCondition condition
) {
// 유효성 검증
validateSemester(condition.semesterId());
validateTeams(condition.semesterId(), condition.teamIds());
validateStatuses(condition.statuses(), MemberType.SEMESTER);
// size만큼 조회 + 전체 행 수 조회
SearchMemberSemesterPageResult pageResult = adminMemberActivityService.searchMemberSemesterList(condition);
// 페이징 값 처리
boolean hasNext = pageResult.content().size() > condition.getSizeOrDefault();
long totalCount = pageResult.totalCount();
List<SearchMemberSemesterProjection> content = hasNext
? pageResult.content().subList(0,condition.getSizeOrDefault())
: pageResult.content();

List<SearchMemberSemesterResponse> responses = content.stream()
.map(SearchMemberSemesterResponse::from)
.toList();

Long nextCursor = hasNext && !content.isEmpty()
? content.get(content.size()-1).memberActivityId()
: null;

// 응답
return CursorPageResponse.of(
responses,
condition.getSizeOrDefault(),
hasNext,
nextCursor,
totalCount
);
}

/*
유틸 함수
*/

// 존재하는 기수인지 검증
private void validateSemester(Long semesterId) {
if(semesterId == null){
return;
}
// 존재하지 않으면 semester 도메인에서 예외 처리
semesterInquiryUsecase.getSemester(semesterId);
}

// 선택한 팀의 기수 소속 검증
private void validateTeam(Long semesterId, Long teamId) {
if (teamId == null) {
Expand All @@ -43,4 +104,32 @@ private void validateTeam(Long semesterId, Long teamId) {
throw new MemberException(NOT_FOUND_TEAM_OF_SEMESTER);
}
}

// 넘겨받은 팀 목록 전체의 기수 소속 및 유효성 검증
private void validateTeams(Long semesterId, List<Long> teamIds) {
if(teamIds == null || teamIds.isEmpty()){
return;
}

// 팀이 있는데 기수가 없으면 잘못된 요청
if (semesterId == null) {
throw new MemberException(REQUIRED_SEMESTER_FOR_TEAM_FILTER);
}

Set<Long> validTeamIds = new HashSet<>(
adminMemberTeamService.getTeamIdsBySemesterId(semesterId)
);

if(!validTeamIds.containsAll(teamIds)) {
throw new MemberException(NOT_FOUND_TEAM_OF_SEMESTER);
}
}

// 구성원 유형에서 허용되는 활동 상태인지 검증
private void validateStatuses(List<ActivityStatus> statuses, MemberType type) {
if (!ActivityStatus.isAllAvailableFor(statuses, type)) {
throw new MemberException(INVALID_ACTIVITY_STATUS);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.ject.support.common.response;

import java.util.List;

public record CursorPageResponse<T>(
List<T> content,
int size,
boolean hasNext,
Long nextCursor,
long totalCount
) {
public static <T> CursorPageResponse<T> of(
List<T> content,
int size,
boolean hasNext,
Long nextCursor,
long totalCount
) {
return new CursorPageResponse<>(content, size, hasNext, nextCursor, totalCount);
}
}
Loading
Loading