Skip to content

Commit 36324b4

Browse files
authored
[Feat] bulk로 member 등록하는 기능 추가 (#102)
* feat : member 리스트 bulk로 추가하는 기능 추가 * fix : test 오류 수정 * test : bulk 관련 Controller과 Service 레이어 테스트 코드 추가 * feat : bulk 관련 로그 및 예외 처리 추가 - Http 409 Conflict 오류 반환 - LogMessage 활용하여 오류 발생하게 한 학번 List 반환 * refactor : MemberFinder로 이전 - findExistingStudentIds를 MemberFinder로 이전 * fix : refactor 관련 테스트 오류 수정 * refactor : error 반환 메세지 형태 개선 * refactor : error 반환 메세지 형태 개선
1 parent eb911c1 commit 36324b4

File tree

9 files changed

+251
-1
lines changed

9 files changed

+251
-1
lines changed

src/main/java/gdsc/konkuk/platformcore/application/member/MemberFinder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public Member fetchMemberById(Long memberId) {
2424
.findById(memberId)
2525
.orElseThrow(() -> UserNotFoundException.of(MemberErrorCode.USER_NOT_FOUND));
2626
}
27+
public List<String> filterExistingStudentIds(List<String> studentIds) {
28+
return memberRepository.findExistingStudentIds(studentIds);
29+
}
2730

2831
public Map<Long, Member> fetchMembersByIdsAndBatch(List<Long> memberIds, String batch) {
2932
List<Member> members = memberRepository.findAllByIdsAndBatch(memberIds, batch);

src/main/java/gdsc/konkuk/platformcore/application/member/MemberService.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@ public Member register(MemberCreateCommand memberCreateCommand) {
4949
return memberRepository.save(MemberCreateCommand
5050
.toEntity(memberCreateCommand));
5151
}
52+
@Transactional
53+
public List<Member> bulkRegister(List<MemberCreateCommand> memberCreateCommands) {
54+
List<String> studentIds = memberCreateCommands.stream()
55+
.map(MemberCreateCommand::getStudentId)
56+
.toList();
57+
58+
List<String> existingStudentIds = memberFinder.filterExistingStudentIds(studentIds);
59+
if (!existingStudentIds.isEmpty()) {
60+
throw UserAlreadyExistException.of(
61+
MemberErrorCode.USER_ALREADY_EXISTS,
62+
"이미 존재하는 유저 학번 List : " + existingStudentIds
63+
);
64+
}
65+
66+
List<Member> members = memberCreateCommands.stream()
67+
.map(MemberCreateCommand::toEntity)
68+
.toList();
69+
return memberRepository.saveAll(members);
70+
}
5271

5372
@Transactional
5473
public void withdraw(Long currentId) {

src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyExistException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,10 @@ protected UserAlreadyExistException(CustomErrorCode errorCode, String logMessage
1111

1212
public static UserAlreadyExistException of(CustomErrorCode errorCode) {
1313
return new UserAlreadyExistException(errorCode, errorCode.getLogMessage());
14+
15+
}
16+
// 유저 정보 중복시 어떤 정보가 중복되었는지 로그에 남기기 위한 메서드
17+
public static UserAlreadyExistException of(CustomErrorCode errorCode, String causeMember) {
18+
return new UserAlreadyExistException(errorCode, errorCode.getLogMessage()+". "+causeMember);
1419
}
1520
}

src/main/java/gdsc/konkuk/platformcore/controller/member/MemberController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ public ResponseEntity<SuccessResponse> signup(
4747
return ResponseEntity.created(getCreatedURI(registeredMember.getId()))
4848
.body(SuccessResponse.messageOnly());
4949
}
50+
@PostMapping("/bulk")
51+
public ResponseEntity<SuccessResponse> bulkSignup(
52+
@RequestBody @Valid List<MemberRegisterRequest> registerRequests) {
53+
List<Member> registeredMembers = memberService.bulkRegister(
54+
registerRequests.stream()
55+
.map(MemberRegisterRequest::toCommand)
56+
.toList()
57+
);
58+
return ResponseEntity.created(getCreatedURI((long)registeredMembers.size()))
59+
.body(SuccessResponse.messageOnly());
60+
}
5061

5162
@DeleteMapping("/{batch}/{memberId}")
5263
public ResponseEntity<SuccessResponse> withdraw(

src/main/java/gdsc/konkuk/platformcore/domain/member/repository/MemberRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
1919
Optional<Member> findByEmail(String memberEmail);
2020

2121
List<Member> findAllByBatch(String batch);
22+
23+
@Query("SELECT m.studentId FROM Member m WHERE m.studentId IN :studentIds")
24+
List<String> findExistingStudentIds(List<String> studentIds);
2225
}

src/main/java/gdsc/konkuk/platformcore/global/controller/GlobalExceptionHandler.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package gdsc.konkuk.platformcore.global.controller;
22

3+
import gdsc.konkuk.platformcore.application.member.exceptions.MemberErrorCode;
4+
import gdsc.konkuk.platformcore.application.member.exceptions.UserAlreadyExistException;
35
import gdsc.konkuk.platformcore.global.exceptions.BusinessException;
46
import gdsc.konkuk.platformcore.global.exceptions.GlobalErrorCode;
57
import gdsc.konkuk.platformcore.global.responses.ErrorResponse;
@@ -44,6 +46,14 @@ protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchExceptio
4446
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
4547
}
4648

49+
@ExceptionHandler(UserAlreadyExistException.class)
50+
protected ResponseEntity<ErrorResponse> handleUserAlreadyExistException(
51+
UserAlreadyExistException e) {
52+
log.error("UserAlreadyExistException Caught!", e);
53+
final ErrorResponse response = ErrorResponse.of(e.getLogMessage(), MemberErrorCode.USER_ALREADY_EXISTS);
54+
return new ResponseEntity<>(response, HttpStatus.CONFLICT);
55+
}
56+
4757
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
4858
protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(
4959
HttpRequestMethodNotSupportedException e) {

src/main/java/gdsc/konkuk/platformcore/global/responses/ErrorResponse.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ public static ErrorResponse of(CustomErrorCode errorCode) {
2525
public static ErrorResponse of(final String message, final String errorCode) {
2626
return new ErrorResponse(message, errorCode);
2727
}
28+
public static ErrorResponse of(final String message, final CustomErrorCode errorCode) {
29+
return new ErrorResponse(message, errorCode.getName());
30+
}
2831
}

src/test/java/gdsc/konkuk/platformcore/application/member/MemberServiceTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package gdsc.konkuk.platformcore.application.member;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
34
import static org.junit.jupiter.api.Assertions.assertNotNull;
45
import static org.junit.jupiter.api.Assertions.assertThrows;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -25,6 +26,9 @@
2526
import org.mockito.MockitoAnnotations;
2627
import org.springframework.security.crypto.password.PasswordEncoder;
2728

29+
import java.util.Collections;
30+
import java.util.List;
31+
2832
class MemberServiceTest {
2933

3034
private MemberService subject;
@@ -89,6 +93,67 @@ void should_fail_when_already_exist_member_register() {
8993
assertThrows(UserAlreadyExistException.class, action);
9094
}
9195

96+
@Test
97+
@DisplayName("bulkRegister : bulk로 가입하려는 멤버들 회원가입 성공")
98+
void should_success_when_newMembers_bulkRegister() {
99+
// given
100+
MemberCreateCommand memberCreateCommand1 = MemberRegisterRequestFixture.builder().build()
101+
.getFixture()
102+
.toCommand();
103+
MemberCreateCommand memberCreateCommand2 = MemberRegisterRequestFixture.builder().build()
104+
.getFixture()
105+
.toCommand();
106+
107+
Member savedMember1 = MemberFixture.builder().build().getFixture();
108+
Member savedMember2 = MemberFixture.builder().build().getFixture();
109+
110+
List<String> studentIds = List.of(
111+
memberCreateCommand1.getStudentId(),
112+
memberCreateCommand2.getStudentId()
113+
);
114+
115+
given(memberRepository.findExistingStudentIds(studentIds))
116+
.willReturn(Collections.emptyList()); // 기존 회원 없음
117+
given(memberRepository.saveAll(any(List.class)))
118+
.willReturn(List.of(savedMember1, savedMember2));
119+
120+
// when
121+
var actual = subject.bulkRegister(
122+
List.of(memberCreateCommand1, memberCreateCommand2));
123+
124+
// then
125+
assertNotNull(actual);
126+
assertThat(actual).hasSize(2);
127+
}
128+
129+
@Test
130+
@DisplayName("bulkRegister : bulk로 가입하려는 멤버 중 기등록된 회원이 존재하는 경우 회원가입 실패")
131+
void should_fail_when_already_exist_members_bulkRegister() {
132+
// given
133+
MemberCreateCommand memberCreateCommand1 = MemberRegisterRequestFixture.builder()
134+
.studentId("12345678")
135+
.build().getFixture().toCommand();
136+
MemberCreateCommand memberCreateCommand2 = MemberRegisterRequestFixture.builder()
137+
.studentId("87654321")
138+
.build().getFixture().toCommand();
139+
MemberCreateCommand memberCreateCommand3 = MemberRegisterRequestFixture.builder()
140+
.studentId("12344321")
141+
.build().getFixture().toCommand();
142+
List<String> studentIds = List.of(
143+
memberCreateCommand1.getStudentId(),
144+
memberCreateCommand2.getStudentId(),
145+
memberCreateCommand3.getStudentId()
146+
);
147+
given(memberFinder.filterExistingStudentIds(studentIds))
148+
.willReturn(List.of("12345678"));
149+
150+
// when & then
151+
assertThrows(UserAlreadyExistException.class,
152+
() -> subject.bulkRegister(List.of(
153+
memberCreateCommand1, memberCreateCommand2,memberCreateCommand3
154+
)));
155+
}
156+
92157
@Test
93158
@DisplayName("withdraw : 존재하는 멤버 탈퇴 성공")
94159
void should_success_when_user_exists() {

src/test/java/gdsc/konkuk/platformcore/controller/member/MemberControllerTest.java

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static com.epages.restdocs.apispec.ResourceDocumentation.headerWithName;
44
import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName;
55
import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
6+
import static org.mockito.ArgumentMatchers.argThat;
67
import static org.mockito.BDDMockito.any;
78
import static org.mockito.BDDMockito.anyString;
89
import static org.mockito.BDDMockito.given;
@@ -24,6 +25,7 @@
2425
import gdsc.konkuk.platformcore.application.member.MemberService;
2526
import gdsc.konkuk.platformcore.application.member.dtos.AttendanceUpdateCommand;
2627
import gdsc.konkuk.platformcore.application.member.dtos.MemberCreateCommand;
28+
import gdsc.konkuk.platformcore.application.member.exceptions.MemberErrorCode;
2729
import gdsc.konkuk.platformcore.application.member.exceptions.UserAlreadyExistException;
2830
import gdsc.konkuk.platformcore.controller.member.dtos.AttendanceUpdateRequest;
2931
import gdsc.konkuk.platformcore.controller.member.dtos.MemberRegisterRequest;
@@ -32,6 +34,7 @@
3234
import gdsc.konkuk.platformcore.domain.attendance.entity.AttendanceType;
3335
import gdsc.konkuk.platformcore.domain.member.entity.Member;
3436
import gdsc.konkuk.platformcore.domain.member.entity.MemberRole;
37+
import gdsc.konkuk.platformcore.domain.member.repository.MemberRepository;
3538
import gdsc.konkuk.platformcore.filter.auth.JwtTokenProvider;
3639
import gdsc.konkuk.platformcore.util.annotation.RestDocsTest;
3740
import gdsc.konkuk.platformcore.util.fixture.member.MemberAttendancesFixture;
@@ -61,6 +64,9 @@ class MemberControllerTest {
6164
@MockBean
6265
private MemberService memberService;
6366

67+
@MockBean
68+
private MemberRepository memberRepository;
69+
6470
@Autowired
6571
private ObjectMapper objectMapper;
6672

@@ -216,9 +222,134 @@ void should_fail_when_existingMember() throws Exception {
216222
.with(csrf()));
217223

218224
// then
219-
result.andDo(print()).andExpect(status().isBadRequest());
225+
result.andDo(print()).andExpect(status().isConflict());
226+
}
227+
228+
229+
@Test
230+
@DisplayName("여러 멤버 회원 가입 성공")
231+
void should_success_when_bulk_new_member() throws Exception {
232+
// given
233+
Member member = MemberFixture.builder().role(MemberRole.CORE).build().getFixture();
234+
String jwt = jwtTokenProvider.createToken(member);
235+
List<MemberRegisterRequest> memberRegisterRequests = List.of(
236+
MemberRegisterRequestFixture.builder().studentId("202400000").build()
237+
.getFixture(),
238+
MemberRegisterRequestFixture.builder().studentId("202400001").build()
239+
.getFixture(),
240+
MemberRegisterRequestFixture.builder().studentId("202400002").build()
241+
.getFixture());
242+
List<Member> memberToRegister = List.of(
243+
MemberFixture.builder().studentId("202400000").build().getFixture(),
244+
MemberFixture.builder().studentId("202400001").build().getFixture(),
245+
MemberFixture.builder().studentId("202400002").build().getFixture(),
246+
MemberFixture.builder().studentId("202400003").build().getFixture(),
247+
MemberFixture.builder().studentId("202400004").build().getFixture());
248+
given(memberService.bulkRegister(any()))
249+
.willReturn(memberToRegister);
250+
251+
// when
252+
ResultActions result =
253+
mockMvc.perform(
254+
RestDocumentationRequestBuilders.post("/api/v1/members/bulk")
255+
.contentType(APPLICATION_JSON)
256+
.header("Authorization", "Bearer " + jwt)
257+
.content(objectMapper.writeValueAsString(memberRegisterRequests))
258+
.with(csrf()));
259+
260+
// then
261+
result
262+
.andDo(print())
263+
.andExpect(status().isCreated())
264+
.andDo(
265+
document(
266+
"member/bulk_register",
267+
preprocessRequest(prettyPrint()),
268+
preprocessResponse(prettyPrint()),
269+
resource(
270+
ResourceSnippetParameters.builder()
271+
.description("여러 멤버 회원 가입 성공")
272+
.tag("member")
273+
.responseHeaders(
274+
headerWithName("Location").description(
275+
"등록한 Member URI"))
276+
.requestFields(
277+
fieldWithPath("[].studentId").description(
278+
"회원 아이디"),
279+
fieldWithPath("[].email").description(
280+
"이메일"),
281+
fieldWithPath("[].name").description(
282+
"이름"),
283+
fieldWithPath("[].department").description(
284+
"학과"),
285+
fieldWithPath("[].batch").description(
286+
"배치"),
287+
fieldWithPath("[].role").description(
288+
"역할"))
289+
.responseFields(
290+
fieldWithPath("success").description(true),
291+
fieldWithPath("message").description(
292+
"회원 가입 성공"),
293+
fieldWithPath("data").description("null"))
294+
.build())));
220295
}
221296

297+
@Test
298+
@DisplayName("여러 멤버 회원 가입 실패 - 이미 존재하는 학번이 포함된 경우")
299+
void should_throw_UserAlreadyExistException_when_studentId_already_exists() throws Exception {
300+
// given
301+
Member member = MemberFixture.builder().role(MemberRole.CORE).build().getFixture();
302+
String jwt = jwtTokenProvider.createToken(member);
303+
304+
List<MemberRegisterRequest> memberRegisterRequests = List.of(
305+
MemberRegisterRequestFixture.builder().studentId("202400000").build().getFixture(),
306+
MemberRegisterRequestFixture.builder().studentId("202400001").build().getFixture(), // 중복 학번
307+
MemberRegisterRequestFixture.builder().studentId("202400002").build().getFixture()
308+
);
309+
310+
given(memberService.bulkRegister(argThat(requests ->
311+
requests.stream().anyMatch(request -> "202400001".equals(request.getStudentId()))
312+
)))
313+
.willThrow(UserAlreadyExistException.of(
314+
MemberErrorCode.USER_ALREADY_EXISTS,
315+
"이미 존재하는 유저 학번 List : [202400001]" // 테스트용 메시지
316+
));
317+
318+
// when
319+
ResultActions result =
320+
mockMvc.perform(
321+
RestDocumentationRequestBuilders.post("/api/v1/members/bulk")
322+
.contentType(APPLICATION_JSON)
323+
.header("Authorization", "Bearer " + jwt)
324+
.content(objectMapper.writeValueAsString(memberRegisterRequests))
325+
.with(csrf()));
326+
327+
// then
328+
result
329+
.andDo(print())
330+
.andExpect(status().isConflict())
331+
.andDo(
332+
document(
333+
"member/bulk_register_fail",
334+
preprocessRequest(prettyPrint()),
335+
preprocessResponse(prettyPrint()),
336+
resource(
337+
ResourceSnippetParameters.builder()
338+
.description("여러 멤버 회원 가입 실패 - 중복 학번 포함")
339+
.tag("member")
340+
.requestFields(
341+
fieldWithPath("[].studentId").description("회원 아이디"),
342+
fieldWithPath("[].email").description("이메일"),
343+
fieldWithPath("[].name").description("이름"),
344+
fieldWithPath("[].department").description("학과"),
345+
fieldWithPath("[].batch").description("배치"),
346+
fieldWithPath("[].role").description("역할"))
347+
.responseFields(
348+
fieldWithPath("success").description(false),
349+
fieldWithPath("message").description("에러 메시지"),
350+
fieldWithPath("errorCode").description("에러 코드")) // data 제거, errorCode 추가
351+
.build())));
352+
}
222353
@Test
223354
@DisplayName("회원 탈퇴 성공")
224355
void should_success_when_delete_member() throws Exception {

0 commit comments

Comments
 (0)