Skip to content

Commit 18d4a8d

Browse files
Sihun23Sihun23
authored andcommitted
CLAP-320 fix:nickname 및 email 중복 검증
1 parent 227040e commit 18d4a8d

File tree

6 files changed

+183
-3
lines changed

6 files changed

+183
-3
lines changed

src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,11 @@ public Optional<Member> findByNicknameAndEmail(String nickname, String email) {
113113
public Optional<Member> findByNameAndEmail(String name, String email) {
114114
return memberRepository.findByNameAndEmail(name, email).map(memberPersistenceMapper::toDomain);
115115
}
116-
}
117116

117+
@Override
118+
public Optional<Member> findByEmail(String email) {
119+
return memberRepository.findByEmail(email)
120+
.map(memberPersistenceMapper::toDomain);
121+
}
122+
123+
}

src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@ public interface MemberRepository extends JpaRepository<MemberEntity, Long>, Me
2626
Optional<MemberEntity> findByNicknameAndEmail(String nickname, String email);
2727

2828
Optional<MemberEntity> findByNameAndEmail(String name, String email);
29+
30+
Optional<MemberEntity> findByEmail(String email);
2931
}
3032

src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ public interface LoadMemberPort {
3434
Optional<Member> findByNicknameAndEmail(String nickname, String email);
3535

3636
Optional<Member> findByNameAndEmail(String name, String email);
37+
38+
Optional<Member> findByEmail(String email);
39+
3740
}

src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import clap.server.application.port.inbound.admin.RegisterMemberCSVUsecase;
44
import clap.server.application.port.inbound.domain.MemberService;
55
import clap.server.application.port.outbound.member.CommandMemberPort;
6+
import clap.server.application.port.outbound.member.LoadMemberPort;
67
import clap.server.common.annotation.architecture.ApplicationService;
78
import clap.server.domain.model.member.Member;
9+
import clap.server.exception.ApplicationException;
10+
import clap.server.exception.code.MemberErrorCode;
811
import lombok.RequiredArgsConstructor;
912
import org.springframework.transaction.annotation.Transactional;
1013
import org.springframework.web.multipart.MultipartFile;
@@ -17,13 +20,24 @@ public class RegisterMemberCSVService implements RegisterMemberCSVUsecase {
1720
private final MemberService memberService;
1821
private final CommandMemberPort commandMemberPort;
1922
private final CsvParseService csvParser;
23+
private final LoadMemberPort loadMemberPort;
24+
2025

2126
@Override
2227
@Transactional
2328
public int registerMembersFromCsv(Long adminId, MultipartFile file) {
2429
List<Member> members = csvParser.parseDataAndMapToMember(file);
2530
Member admin = memberService.findActiveMember(adminId);
2631

32+
members.forEach(member -> {
33+
String nickname = member.getMemberInfo().getNickname();
34+
String email = member.getMemberInfo().getEmail();
35+
if (loadMemberPort.findByNickname(nickname).isPresent() ||
36+
loadMemberPort.findByEmail(email).isPresent()) {
37+
throw new ApplicationException(MemberErrorCode.DUPLICATE_NICKNAME_OR_EMAIL);
38+
}
39+
});
40+
2741
List<Member> newMembers = members.stream()
2842
.map(memberData -> Member.createMember(admin, memberData.getMemberInfo()))
2943
.toList();

src/main/java/clap/server/exception/code/MemberErrorCode.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ public enum MemberErrorCode implements BaseErrorCode {
1616
CSV_PARSING_ERROR(HttpStatus.BAD_REQUEST, "MEMBER_008", "CSV 데이터 파싱 중 오류가 발생했습니다."),
1717
MEMBER_REGISTRATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER_009", "담당자만 리뷰 권한이 있습니다."),
1818
NAME_CANNOT_BE_EMPTY(HttpStatus.BAD_REQUEST, "MEMBER_010", "이름은 공백일 수 없습니다."),
19-
DUPLICATE_NICKNAME(HttpStatus.BAD_REQUEST,"MEMBER_011", "중복된 닉네임입니다")
20-
;
19+
DUPLICATE_NICKNAME(HttpStatus.BAD_REQUEST,"MEMBER_011", "중복된 닉네임입니다"),
20+
DUPLICATE_NICKNAME_OR_EMAIL(HttpStatus.BAD_REQUEST, "MEMBER_012", "중복된 닉네임이나 email이 존재합니다")
21+
;
2122

2223
private final HttpStatus httpStatus;
2324
private final String customCode;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package clap.server.application.service.admin;
2+
3+
import clap.server.application.port.inbound.domain.MemberService;
4+
import clap.server.application.port.outbound.member.CommandMemberPort;
5+
import clap.server.application.port.outbound.member.LoadMemberPort;
6+
import clap.server.domain.model.member.Department;
7+
import clap.server.domain.model.member.Member;
8+
import clap.server.domain.model.member.MemberInfo;
9+
import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole;
10+
import clap.server.exception.ApplicationException;
11+
import clap.server.exception.code.MemberErrorCode;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
import org.mockito.*;
15+
import org.springframework.mock.web.MockMultipartFile;
16+
import org.springframework.web.multipart.MultipartFile;
17+
18+
import java.util.Arrays;
19+
import java.util.List;
20+
import java.util.Optional;
21+
22+
import static org.junit.jupiter.api.Assertions.*;
23+
import static org.mockito.ArgumentMatchers.*;
24+
import static org.mockito.Mockito.*;
25+
26+
class RegisterMemberCSVServiceTest {
27+
28+
@Mock
29+
private MemberService memberService;
30+
31+
@Mock
32+
private CommandMemberPort commandMemberPort;
33+
34+
@Mock
35+
private CsvParseService csvParser;
36+
37+
@Mock
38+
private LoadMemberPort loadMemberPort;
39+
40+
@InjectMocks
41+
private RegisterMemberCSVService registerMemberCSVService;
42+
43+
// 더미 Department: department는 not null이어야 하므로, 예시로 departmentId가 채워진 객체를 생성합니다.
44+
private Department dummyDepartment;
45+
46+
@BeforeEach
47+
void setUp() {
48+
MockitoAnnotations.openMocks(this);
49+
// 예시로 Department가 빌더를 제공한다고 가정
50+
dummyDepartment = Department.builder().departmentId(100L).build();
51+
}
52+
53+
@Test
54+
void testRegisterMembersFromCsv_success() throws Exception {
55+
// given
56+
Long adminId = 1L;
57+
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv",
58+
"header\nrow1\nrow2".getBytes());
59+
60+
// CSV 파싱 결과로 반환할 회원 객체들 (각 MemberInfo에 dummyDepartment 적용)
61+
Member csvMember1 = mock(Member.class);
62+
Member csvMember2 = mock(Member.class);
63+
64+
MemberInfo dummyMemberInfo1 = MemberInfo.builder()
65+
.name("John Doe")
66+
.email("john@example.com")
67+
.nickname("johnny")
68+
.isReviewer(false)
69+
.department(dummyDepartment)
70+
.role(MemberRole.ROLE_USER)
71+
.departmentRole("Dept Role")
72+
.build();
73+
74+
MemberInfo dummyMemberInfo2 = MemberInfo.builder()
75+
.name("Jane Doe")
76+
.email("jane@example.com")
77+
.nickname("janie")
78+
.isReviewer(false)
79+
.department(dummyDepartment)
80+
.role(MemberRole.ROLE_USER)
81+
.departmentRole("Dept Role")
82+
.build();
83+
84+
when(csvMember1.getMemberInfo()).thenReturn(dummyMemberInfo1);
85+
when(csvMember2.getMemberInfo()).thenReturn(dummyMemberInfo2);
86+
List<Member> csvMembers = Arrays.asList(csvMember1, csvMember2);
87+
when(csvParser.parseDataAndMapToMember(file)).thenReturn(csvMembers);
88+
89+
when(loadMemberPort.findByNickname(anyString())).thenReturn(Optional.empty());
90+
when(loadMemberPort.findByEmail(anyString())).thenReturn(Optional.empty());
91+
92+
Member adminMember = mock(Member.class);
93+
when(memberService.findActiveMember(adminId)).thenReturn(adminMember);
94+
95+
Member newMember1 = mock(Member.class);
96+
Member newMember2 = mock(Member.class);
97+
98+
try (MockedStatic<Member> mockedStatic = Mockito.mockStatic(Member.class)) {
99+
mockedStatic.when(() -> Member.createMember(eq(adminMember), eq(dummyMemberInfo1)))
100+
.thenReturn(newMember1);
101+
mockedStatic.when(() -> Member.createMember(eq(adminMember), eq(dummyMemberInfo2)))
102+
.thenReturn(newMember2);
103+
104+
// when
105+
int result = registerMemberCSVService.registerMembersFromCsv(adminId, file);
106+
107+
// then
108+
ArgumentCaptor<List<Member>> captor = ArgumentCaptor.forClass(List.class);
109+
verify(commandMemberPort).saveAll(captor.capture());
110+
List<Member> savedMembers = captor.getValue();
111+
112+
assertEquals(2, savedMembers.size(), "CSV 파싱된 회원 수 만큼 새로운 회원이 생성");
113+
assertEquals(2, result, "등록된 회원 수는 CSV 파일의 회원 수와 동일");
114+
115+
mockedStatic.verify(() -> Member.createMember(adminMember, dummyMemberInfo1), times(1));
116+
mockedStatic.verify(() -> Member.createMember(adminMember, dummyMemberInfo2), times(1));
117+
}
118+
}
119+
120+
@Test
121+
void testRegisterMembersFromCsv_duplicateThrowsException() throws Exception {
122+
// given
123+
Long adminId = 1L;
124+
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv",
125+
"header\nrow1".getBytes());
126+
127+
Member csvMember1 = mock(Member.class);
128+
MemberInfo dummyMemberInfo1 = MemberInfo.builder()
129+
.name("John Doe")
130+
.email("john@example.com")
131+
.nickname("johnny")
132+
.isReviewer(false)
133+
.department(dummyDepartment)
134+
.role(MemberRole.ROLE_USER)
135+
.departmentRole("Dept Role")
136+
.build();
137+
when(csvMember1.getMemberInfo()).thenReturn(dummyMemberInfo1);
138+
List<Member> csvMembers = Arrays.asList(csvMember1);
139+
when(csvParser.parseDataAndMapToMember(file)).thenReturn(csvMembers);
140+
141+
Member adminMember = mock(Member.class);
142+
when(memberService.findActiveMember(adminId)).thenReturn(adminMember);
143+
144+
// 중복 체크: 닉네임 또는 email 중 하나라도 중복이 있으면 에러 발생
145+
when(loadMemberPort.findByNickname(dummyMemberInfo1.getNickname()))
146+
.thenReturn(Optional.of(mock(Member.class)));
147+
148+
ApplicationException exception = assertThrows(ApplicationException.class, () ->
149+
registerMemberCSVService.registerMembersFromCsv(adminId, file)
150+
);
151+
assertEquals(MemberErrorCode.DUPLICATE_NICKNAME_OR_EMAIL.getMessage(), exception.getMessage(),
152+
"중복된 닉네임이나 email이 존재하면 MEMBER_012 예외가 발생해야 합니다.");
153+
}
154+
}

0 commit comments

Comments
 (0)