Skip to content

Commit ff59fa9

Browse files
authored
[feat] 일반 구성원 추가 기능 구현 (#578)
* [refactor] MemberActivity 일반 구성원 aggregate 구성 * [feat] 기수 단건 조회 기능 추가 * [feat] 일반 구성원 활동 추가 기능 구현 * [test] Member 생성 팩토리 사용으로 테스트 수정 * [test] Member 도메인계층 테스트 생성, 일부 코드 가독성 개선 * [test] 구성원 추가 기능 테스트 보강 * [fix] 일반 구성원 추가 시 teamId 검증 및 저장 누락 수정 * [feat] 일반 구성원 추가 dto 검증 조건 추가 * [feat] 삭제된 구성원 존재시 복구하는 로직 추가
1 parent cc6a1bc commit ff59fa9

32 files changed

Lines changed: 1556 additions & 13 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.ject.support.admin.member.controller;
2+
3+
import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
4+
import org.springframework.web.bind.annotation.RequestBody;
5+
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import jakarta.validation.Valid;
9+
10+
@Tag(name = "AdminMemberSemester", description = "일반 구성원 API")
11+
public interface AdminMemberSemesterApiSpec {
12+
@Operation(
13+
summary = "일반 구성원 추가",
14+
description = "일반 구성원을 추가합니다."
15+
)
16+
void createAdminMemberSemester(@RequestBody @Valid CreateMemberSemesterRequest request);
17+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.ject.support.admin.member.controller;
2+
3+
import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
4+
import org.ject.support.admin.member.service.AdminMemberUseCase;
5+
import org.springframework.web.bind.annotation.PostMapping;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import lombok.RequiredArgsConstructor;
10+
11+
@RestController
12+
@RequestMapping("/admin/members/semester")
13+
@RequiredArgsConstructor
14+
public class AdminMemberSemesterController implements AdminMemberSemesterApiSpec {
15+
16+
private final AdminMemberUseCase adminMemberUsecase;
17+
18+
//일반 구성원 추가
19+
@Override
20+
@PostMapping
21+
public void createAdminMemberSemester(CreateMemberSemesterRequest request) {
22+
adminMemberUsecase.createMemberSemester(request);
23+
}
24+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.ject.support.admin.member.dto.request;
2+
3+
import java.util.List;
4+
5+
import org.ject.support.domain.member.CareerDetails;
6+
import org.ject.support.domain.member.ExperiencePeriod;
7+
import org.ject.support.domain.member.JobFamily;
8+
import org.ject.support.domain.member.Region;
9+
import org.ject.support.domain.recruit.domain.RecruitTypeDetail;
10+
11+
import jakarta.validation.constraints.Email;
12+
import jakarta.validation.constraints.NotBlank;
13+
import jakarta.validation.constraints.NotNull;
14+
import jakarta.validation.constraints.Pattern;
15+
import jakarta.validation.constraints.Size;
16+
17+
public record CreateMemberSemesterRequest(
18+
19+
/*
20+
필수 항목: 이름, 전화번호, 이메일, 직군(포지션), 지원자 신분, 모집단위, 기수id
21+
선택 항목: 팀id, 비고, 관심도메인, 거주지역, 직무 관련 경험
22+
*/
23+
@NotBlank(message = "이름을 입력해주세요")
24+
@Size(max = 20, message = "이름은 20자 이하로 입력해주세요.")
25+
@Pattern(regexp = "^[^\\s]+$", message = "이름은 공백없이 입력해주세요")
26+
String name,
27+
28+
@NotBlank(message = "이메일을 입력해주세요.")
29+
@Email(message = "올바른 이메일 형식이 아닙니다.")
30+
@Size(max = 30, message = "이메일은 30자 이하로 입력해주세요.")
31+
String email,
32+
33+
@NotBlank(message = "전화번호를 입력해주세요")
34+
@Pattern(regexp = "^010\\d{8}$", message = "010으로 시작하는 11자리 숫자를 입력해주세요.")
35+
String phoneNumber,
36+
37+
@NotNull(message = "포지션을 선택해주세요")
38+
JobFamily jobFamily,
39+
40+
@NotNull(message = "모집 단위를 선택해주세요")
41+
RecruitTypeDetail recruitTypeDetail,
42+
43+
@NotNull(message = "지원자 신분을 선택해주세요")
44+
CareerDetails careerDetails,
45+
46+
@NotNull(message = "기수를 선택해주세요")
47+
Long semesterId,
48+
49+
Long teamId,
50+
51+
ExperiencePeriod experiencePeriod,
52+
53+
@Size(max = 100, message = "비고는 100자 이하로 입력해주세요.")
54+
String memo,
55+
56+
@Size(min = 1, max = 3, message = "관심 도메인은 최소 1개부터 최대 3개까지 선택 가능합니다.")
57+
List<String> interestedDomains,
58+
59+
Region region
60+
61+
) {
62+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.ject.support.admin.member.service;
2+
3+
import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
4+
import org.ject.support.domain.member.MemberType;
5+
import org.ject.support.domain.member.entity.MemberActivity;
6+
import org.ject.support.domain.member.exception.MemberException;
7+
import org.ject.support.domain.member.repository.MemberActivityRepository;
8+
import org.springframework.stereotype.Service;
9+
10+
import lombok.RequiredArgsConstructor;
11+
12+
import static org.ject.support.domain.member.exception.MemberErrorCode.ALREADY_EXIST_MEMBER_SEMESTER_ACTIVITY;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class AdminMemberActivityService {
17+
18+
private final MemberActivityRepository memberActivityRepository;
19+
20+
21+
public void createMemberSemesterActivity(CreateMemberSemesterRequest request, Long memberId) {
22+
// 추가하려는 대상이 같은 기수에 이미 등록되어있는지 중복 검증
23+
validateDuplicateSemesterActivity(memberId, request.semesterId());
24+
25+
// MemberActivity생성, 내부에서 MemberSemester 생성
26+
MemberActivity memberActivity = MemberActivity.createSemesterActivity(
27+
memberId,
28+
request.jobFamily(),
29+
request.recruitTypeDetail(),
30+
request.careerDetails(),
31+
request.experiencePeriod(),
32+
request.memo(),
33+
request.semesterId(),
34+
request.teamId()
35+
);
36+
37+
memberActivityRepository.save(memberActivity);
38+
}
39+
40+
// 동일 기수 일반 구성원 활동 중복 검증
41+
private void validateDuplicateSemesterActivity(Long memberId, Long semesterId) {
42+
if (memberActivityRepository.existsSemesterActivity(memberId, MemberType.SEMESTER, semesterId)) {
43+
throw new MemberException(ALREADY_EXIST_MEMBER_SEMESTER_ACTIVITY);
44+
}
45+
}
46+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.ject.support.admin.member.service;
2+
3+
import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
4+
import org.ject.support.domain.member.entity.Member;
5+
import org.ject.support.domain.member.repository.MemberRepository;
6+
import org.springframework.stereotype.Service;
7+
8+
import lombok.RequiredArgsConstructor;
9+
10+
@Service
11+
@RequiredArgsConstructor
12+
public class AdminMemberService {
13+
14+
private final MemberRepository memberRepository;
15+
16+
/*
17+
(2026.06.23)
18+
1. email로 삭제 포함 구성원 신상 전부 조회
19+
2. 삭제된 구성원 존재시: isDeleted를 복구하고 새로 입력받은 값으로 덮어쓰기
20+
3. 활성 구성원 존재시: 기존 값 재사용
21+
4. 미 존재시: 새로 생성
22+
*/
23+
24+
// email 기준 구성원 조회 또는 신상정보 생성
25+
public Long findOrCreateMember(CreateMemberSemesterRequest request) {
26+
return memberRepository.findByEmailIncludingDeleted(request.email())
27+
.map(member -> useExistingMember(member, request))
28+
.orElseGet(() -> createMember(request));
29+
}
30+
31+
// 기존 구성원 상태에 따라 재사용 또는 복구
32+
private Long useExistingMember(Member member, CreateMemberSemesterRequest request) {
33+
if (member.getIsDeleted()) {
34+
return restoreMember(member, request);
35+
}
36+
37+
return member.getId();
38+
}
39+
40+
// 삭제된 구성원 신상정보 갱신 후 복구
41+
private Long restoreMember(Member member, CreateMemberSemesterRequest request) {
42+
member.restore(
43+
request.name(),
44+
request.phoneNumber(),
45+
request.interestedDomains(),
46+
request.region()
47+
);
48+
return member.getId();
49+
}
50+
51+
// 구성원 신상정보 생성
52+
private Long createMember(CreateMemberSemesterRequest request) {
53+
Member member = Member.create(
54+
request.name(),
55+
request.email(),
56+
request.phoneNumber(),
57+
request.interestedDomains(),
58+
request.region()
59+
);
60+
61+
Member savedMember = memberRepository.save(member);
62+
return savedMember.getId();
63+
}
64+
65+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.ject.support.admin.member.service;
2+
3+
import java.util.List;
4+
5+
import org.ject.support.domain.member.repository.TeamRepository;
6+
import org.springframework.stereotype.Service;
7+
8+
import lombok.RequiredArgsConstructor;
9+
10+
@Service
11+
@RequiredArgsConstructor
12+
public class AdminMemberTeamService {
13+
14+
private final TeamRepository teamRepository;
15+
16+
// 기수에 속한 팀 ID 목록 조회
17+
public List<Long> getTeamIdsBySemesterId(Long semesterId) {
18+
return teamRepository.findIdsBySemesterId(semesterId);
19+
}
20+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.ject.support.admin.member.service;
2+
3+
import static org.ject.support.domain.member.exception.MemberErrorCode.NOT_FOUND_TEAM_OF_SEMESTER;
4+
5+
import org.ject.support.admin.member.dto.request.CreateMemberSemesterRequest;
6+
import org.ject.support.domain.member.exception.MemberException;
7+
import org.ject.support.domain.recruit.service.SemesterInquiryUsecase;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.transaction.annotation.Transactional;
10+
11+
import lombok.RequiredArgsConstructor;
12+
13+
@Service
14+
@RequiredArgsConstructor
15+
public class AdminMemberUseCase {
16+
17+
private final AdminMemberService adminMemberService;
18+
private final AdminMemberActivityService adminMemberActivityService;
19+
private final AdminMemberTeamService adminMemberTeamService;
20+
private final SemesterInquiryUsecase semesterInquiryUsecase;
21+
22+
// 일반 구성원 생성 흐름 처리
23+
@Transactional
24+
public void createMemberSemester(CreateMemberSemesterRequest request) {
25+
// 기수 유효성 검증
26+
semesterInquiryUsecase.getSemester(request.semesterId());
27+
validateTeam(request.semesterId(), request.teamId());
28+
29+
// email 기준 기존 Member 조회 또는 신규 생성
30+
Long memberId = adminMemberService.findOrCreateMember(request);
31+
32+
// memberId 기준 MemberActivity와 MemberSemester 생성 및 저장
33+
adminMemberActivityService.createMemberSemesterActivity(request, memberId);
34+
}
35+
36+
// 선택한 팀의 기수 소속 검증
37+
private void validateTeam(Long semesterId, Long teamId) {
38+
if (teamId == null) {
39+
return;
40+
}
41+
42+
if (!adminMemberTeamService.getTeamIdsBySemesterId(semesterId).contains(teamId)) {
43+
throw new MemberException(NOT_FOUND_TEAM_OF_SEMESTER);
44+
}
45+
}
46+
}

src/main/java/org/ject/support/domain/member/entity/Member.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
@SQLRestriction("is_deleted = false")
2929
@Entity
3030
@Getter
31-
@Builder
31+
@Builder(access = AccessLevel.PRIVATE)
3232
@AllArgsConstructor(access = AccessLevel.PRIVATE)
3333
@NoArgsConstructor(access = AccessLevel.PROTECTED)
3434
public class Member extends BaseTimeEntity {
@@ -61,4 +61,34 @@ public class Member extends BaseTimeEntity {
6161
@Builder.Default
6262
private Boolean isDeleted = false;
6363

64+
65+
public static Member create(
66+
String name,
67+
String email,
68+
String phoneNumber,
69+
List<String> interestedDomains,
70+
Region region
71+
){
72+
return Member.builder()
73+
.name(name)
74+
.email(email)
75+
.phoneNumber(phoneNumber)
76+
.region(region)
77+
.interestedDomains(interestedDomains)
78+
.build();
79+
}
80+
81+
// 삭제된 구성원 정보가 있는 경우 복구한 뒤 새로 입력한 값으로 덮어쓰기
82+
public void restore(
83+
String name,
84+
String phoneNumber,
85+
List<String> interestedDomains,
86+
Region region
87+
) {
88+
this.isDeleted = false;
89+
this.name = name;
90+
this.phoneNumber = phoneNumber;
91+
this.interestedDomains = interestedDomains;
92+
this.region = region;
93+
}
6494
}

src/main/java/org/ject/support/domain/member/entity/MemberActivity.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package org.ject.support.domain.member.entity;
22

3+
import jakarta.persistence.CascadeType;
34
import jakarta.persistence.Column;
45
import jakarta.persistence.Entity;
56
import jakarta.persistence.EnumType;
67
import jakarta.persistence.Enumerated;
8+
import jakarta.persistence.FetchType;
79
import jakarta.persistence.GeneratedValue;
810
import jakarta.persistence.GenerationType;
911
import jakarta.persistence.Id;
12+
import jakarta.persistence.OneToOne;
1013
import jakarta.persistence.Table;
1114
import java.time.LocalDate;
1215

@@ -19,11 +22,13 @@
1922
import org.ject.support.domain.member.ExperiencePeriod;
2023
import org.ject.support.domain.member.JobFamily;
2124
import org.ject.support.domain.member.MemberType;
25+
import org.ject.support.domain.member.exception.MemberErrorCode;
26+
import org.ject.support.domain.member.exception.MemberException;
2227
import org.ject.support.domain.recruit.domain.RecruitTypeDetail;
2328

2429
@Entity
2530
@Getter
26-
@Builder
31+
@Builder(access = AccessLevel.PRIVATE)
2732
@Table(name = "member_activity")
2833
@SQLDelete(sql = "UPDATE member_activity SET is_deleted = true WHERE id = ?")
2934
@SQLRestriction("is_deleted = false")
@@ -73,4 +78,37 @@ public class MemberActivity extends BaseTimeEntity {
7378
@Column(name = "recruit_type_detail", length = 45, nullable = false)
7479
@Enumerated(EnumType.STRING)
7580
private RecruitTypeDetail recruitTypeDetail;
81+
82+
@OneToOne(mappedBy = "memberActivity", fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
83+
private MemberSemester memberSemester;
84+
85+
// 일반 구성원 활동과 관리 항목 생성
86+
public static MemberActivity createSemesterActivity(
87+
Long memberId,
88+
JobFamily jobFamily,
89+
RecruitTypeDetail recruitTypeDetail,
90+
CareerDetails careerDetails,
91+
ExperiencePeriod experiencePeriod,
92+
String memo,
93+
Long semesterId,
94+
Long teamId
95+
){
96+
MemberActivity memberActivity = MemberActivity.builder()
97+
.memberId(memberId)
98+
.memberType(MemberType.SEMESTER)
99+
.jobFamily(jobFamily)
100+
.recruitTypeDetail(recruitTypeDetail)
101+
.careerDetails(careerDetails)
102+
.experiencePeriod(experiencePeriod)
103+
.memo(memo)
104+
.build();
105+
//애그리거트 루트인 MemberActivity쪽에서 식별 관계인 엔티티 생성 책임
106+
//MemberSemester는 MemberActivity없이 단독으로 사용되지 않음
107+
memberActivity.memberSemester = MemberSemester.create(memberActivity, semesterId, teamId);
108+
return memberActivity;
109+
}
110+
111+
//Todo: 메이커스 구성원 활동과 관리 항목 생성
112+
113+
//Todo: 운영 서포터즈 구성원 활동과 관리 항목 생성
76114
}

0 commit comments

Comments
 (0)