Skip to content

Commit e37c176

Browse files
authored
Merge pull request #273 from TaskFlow-CLAP/CLAP-253
CLAP-253 CSV 회원 일괄 등록 리팩토링 및 테스트 코드
2 parents 1e5525e + 486d6eb commit e37c176

File tree

7 files changed

+178
-22
lines changed

7 files changed

+178
-22
lines changed

src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package clap.server.adapter.inbound.web.admin;
22

33
import clap.server.adapter.inbound.security.service.SecurityUserDetails;
4-
import clap.server.application.port.inbound.admin.RegisterMemberUsecase;
4+
import clap.server.application.port.inbound.admin.RegisterMemberCSVUsecase;
55
import clap.server.common.annotation.architecture.WebAdapter;
66
import io.swagger.v3.oas.annotations.Operation;
77
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -17,10 +17,10 @@
1717
@WebAdapter
1818
@RequestMapping("/api/managements")
1919
public class RegisterMemberCsvController {
20-
private final RegisterMemberUsecase registerMemberUsecase;
20+
private final RegisterMemberCSVUsecase registerMemberCSVUsecase;
2121

22-
public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase) {
23-
this.registerMemberUsecase = registerMemberUsecase;
22+
public RegisterMemberCsvController(RegisterMemberCSVUsecase registerMemberCSVUsecase) {
23+
this.registerMemberCSVUsecase = registerMemberCSVUsecase;
2424
}
2525

2626
@Operation(summary = "CSV 파일로 회원 등록 API")
@@ -29,7 +29,7 @@ public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase)
2929
public ResponseEntity<String> registerMembersFromCsv(
3030
@AuthenticationPrincipal SecurityUserDetails userInfo,
3131
@RequestParam("file") MultipartFile file) {
32-
int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file);
32+
int addedCount = registerMemberCSVUsecase.registerMembersFromCsv(userInfo.getUserId(), file);
3333
return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다.");
3434
}
35-
}
35+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package clap.server.application.port.inbound.admin;
2+
3+
import org.springframework.web.multipart.MultipartFile;
4+
5+
public interface RegisterMemberCSVUsecase {
6+
int registerMembersFromCsv(Long adminId, MultipartFile file);
7+
}

src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,4 @@
55

66
public interface RegisterMemberUsecase {
77
void registerMember(Long adminId, RegisterMemberRequest request);
8-
9-
int registerMembersFromCsv(Long adminId, MultipartFile file);
108
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package clap.server.application.service.admin;
22

33
import clap.server.application.port.outbound.member.LoadDepartmentPort;
4+
import clap.server.common.annotation.architecture.ApplicationService;
45
import clap.server.domain.model.member.Department;
56
import clap.server.domain.model.member.Member;
67
import clap.server.domain.model.member.MemberInfo;
@@ -10,7 +11,6 @@
1011
import clap.server.exception.code.MemberErrorCode;
1112
import lombok.RequiredArgsConstructor;
1213
import lombok.extern.slf4j.Slf4j;
13-
import org.springframework.stereotype.Service;
1414
import org.springframework.web.multipart.MultipartFile;
1515

1616
import java.io.BufferedReader;
@@ -24,7 +24,7 @@
2424

2525

2626
@Slf4j
27-
@Service
27+
@ApplicationService
2828
@RequiredArgsConstructor
2929
public class CsvParseService {
3030

@@ -33,6 +33,11 @@ public class CsvParseService {
3333
public List<Member> parse(MultipartFile file) {
3434
List<Member> members = new ArrayList<>();
3535
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
36+
// 첫 번째 줄은 헤더로 간주하고 다음 줄부터 파싱
37+
String headerLine = reader.readLine();
38+
if (headerLine == null) {
39+
throw ApplicationException.from(MemberErrorCode.INVALID_CSV_FORMAT);
40+
}
3641
String line;
3742
while ((line = reader.readLine()) != null) {
3843
String[] fields = line.split(",");
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package clap.server.application.service.admin;
2+
3+
import clap.server.application.port.inbound.admin.RegisterMemberCSVUsecase;
4+
import clap.server.application.port.inbound.domain.MemberService;
5+
import clap.server.application.port.outbound.member.CommandMemberPort;
6+
import clap.server.common.annotation.architecture.ApplicationService;
7+
import clap.server.domain.model.member.Member;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.transaction.annotation.Transactional;
10+
import org.springframework.web.multipart.MultipartFile;
11+
12+
import java.util.List;
13+
14+
@ApplicationService
15+
@RequiredArgsConstructor
16+
public class RegisterMemberCSVService implements RegisterMemberCSVUsecase {
17+
private final MemberService memberService;
18+
private final CommandMemberPort commandMemberPort;
19+
private final CsvParseService csvParser;
20+
21+
@Override
22+
@Transactional
23+
public int registerMembersFromCsv(Long adminId, MultipartFile file) {
24+
List<Member> members = csvParser.parse(file);
25+
Member admin = memberService.findActiveMember(adminId);
26+
members.forEach(member -> {
27+
member.register(admin);
28+
commandMemberPort.save(member);
29+
});
30+
return members.size();
31+
}
32+
}

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

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import lombok.RequiredArgsConstructor;
1515
import org.springframework.security.crypto.password.PasswordEncoder;
1616
import org.springframework.transaction.annotation.Transactional;
17-
import org.springframework.web.multipart.MultipartFile;
17+
1818

1919
import java.util.List;
2020

@@ -41,15 +41,4 @@ public void registerMember(Long adminId, RegisterMemberRequest request) {
4141
commandMemberPort.save(member);
4242
}
4343

44-
@Override
45-
@Transactional
46-
public int registerMembersFromCsv(Long adminId, MultipartFile file) {
47-
List<Member> members = csvParser.parse(file);
48-
Member admin = memberService.findActiveMember(adminId);
49-
members.forEach(member -> {
50-
member.register(admin);
51-
commandMemberPort.save(member);
52-
});
53-
return members.size();
54-
}
5544
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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.domain.model.member.Member;
6+
import clap.server.exception.ApplicationException;
7+
import clap.server.exception.code.MemberErrorCode;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.mockito.Mockito;
11+
import org.springframework.mock.web.MockMultipartFile;
12+
import org.springframework.web.multipart.MultipartFile;
13+
14+
import java.util.List;
15+
16+
import static org.junit.jupiter.api.Assertions.*;
17+
import static org.mockito.Mockito.*;
18+
19+
class RegisterMemberCSVServiceTest {
20+
21+
private RegisterMemberCSVService registerMemberCSVService;
22+
private MemberService memberService;
23+
private CommandMemberPort commandMemberPort;
24+
private CsvParseService csvParseService;
25+
26+
27+
@BeforeEach
28+
void setup() {
29+
memberService = Mockito.mock(MemberService.class);
30+
commandMemberPort = Mockito.mock(CommandMemberPort.class);
31+
csvParseService = Mockito.mock(CsvParseService.class);
32+
registerMemberCSVService = new RegisterMemberCSVService(memberService, commandMemberPort, csvParseService);
33+
}
34+
35+
/**
36+
* 정상적인 회원 등록 테스트
37+
* - 주어진 CSV 파일을 정상적으로 파싱하여 회원이 등록되는지 검증
38+
*/
39+
@Test
40+
void testRegisterMembersFromCsvSuccess() {
41+
Long adminId = 1L;
42+
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes());
43+
44+
Member admin = Mockito.mock(Member.class);
45+
List<Member> parsedMembers = List.of(Mockito.mock(Member.class), Mockito.mock(Member.class));
46+
47+
when(memberService.findActiveMember(adminId)).thenReturn(admin);
48+
when(csvParseService.parse(file)).thenReturn(parsedMembers);
49+
50+
int addedCount = registerMemberCSVService.registerMembersFromCsv(adminId, file);
51+
52+
assertEquals(2, addedCount);
53+
verify(commandMemberPort, times(2)).save(any(Member.class));
54+
verify(parsedMembers.get(0), times(1)).register(admin);
55+
verify(parsedMembers.get(1), times(1)).register(admin);
56+
}
57+
58+
/**
59+
* ❌ 관리자 찾기 실패 (MEMBER_NOT_FOUND)
60+
*/
61+
@Test
62+
void testRegisterMembersFromCsvThrowsWhenAdminNotFound() {
63+
Long adminId = 99L;
64+
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes());
65+
66+
when(memberService.findActiveMember(adminId)).thenThrow(new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND));
67+
68+
ApplicationException exception = assertThrows(ApplicationException.class, () -> {
69+
registerMemberCSVService.registerMembersFromCsv(adminId, file);
70+
});
71+
72+
// 검증: 발생한 예외가 `MEMBER_NOT_FOUND`인지 확인
73+
assertEquals(MemberErrorCode.MEMBER_NOT_FOUND.getCustomCode(), exception.getCode().getCustomCode());
74+
verifyNoInteractions(commandMemberPort); // 회원 저장 로직이 실행안됨
75+
}
76+
77+
/**
78+
* ❌ CSV 파싱 실패 (CSV_PARSING_ERROR)
79+
*/
80+
@Test
81+
void testRegisterMembersFromCsvThrowsWhenCsvParsingFails() {
82+
Long adminId = 1L;
83+
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes());
84+
85+
// ✅ Mock 객체 설정: CSV 파싱 과정에서 예외 발생하도록 설정
86+
when(csvParseService.parse(file)).thenThrow(new ApplicationException(MemberErrorCode.CSV_PARSING_ERROR));
87+
88+
// 🔹 유스케이스 실행 및 예외 검증
89+
ApplicationException exception = assertThrows(ApplicationException.class, () -> {
90+
registerMemberCSVService.registerMembersFromCsv(adminId, file);
91+
});
92+
93+
// ✅ 검증: 발생한 예외가 `CSV_PARSING_ERROR`인지 확인
94+
assertEquals(MemberErrorCode.CSV_PARSING_ERROR.getCustomCode(), exception.getCode().getCustomCode());
95+
verifyNoInteractions(commandMemberPort); // ❗ 회원 저장 로직이 실행되지 않아야 함
96+
}
97+
98+
/**
99+
* ❌ 회원 등록 실패 (MEMBER_REGISTRATION_FAILED)
100+
*
101+
*/
102+
@Test
103+
void testRegisterMembersFromCsvThrowsWhenSavingMemberFails() {
104+
Long adminId = 1L;
105+
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes());
106+
107+
Member admin = Mockito.mock(Member.class);
108+
Member failingMember = Mockito.mock(Member.class);
109+
List<Member> parsedMembers = List.of(failingMember, Mockito.mock(Member.class));
110+
111+
// 특정 회원 등록 중 예외 발생
112+
when(memberService.findActiveMember(adminId)).thenReturn(admin);
113+
when(csvParseService.parse(file)).thenReturn(parsedMembers);
114+
doThrow(new ApplicationException(MemberErrorCode.MEMBER_REGISTRATION_FAILED))
115+
.when(commandMemberPort).save(failingMember);
116+
117+
// Usecase 실행
118+
ApplicationException exception = assertThrows(ApplicationException.class, () -> {
119+
registerMemberCSVService.registerMembersFromCsv(adminId, file);
120+
});
121+
122+
assertEquals(MemberErrorCode.MEMBER_REGISTRATION_FAILED.getCustomCode(), exception.getCode().getCustomCode());
123+
verify(commandMemberPort, times(1)).save(failingMember); // ❗ 실패한 회원만 저장 시도해야 함
124+
}
125+
}

0 commit comments

Comments
 (0)