diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java index dc06b605..1f4390e1 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java @@ -1,7 +1,7 @@ package clap.server.adapter.inbound.web.admin; import clap.server.adapter.inbound.security.service.SecurityUserDetails; -import clap.server.application.port.inbound.admin.RegisterMemberUsecase; +import clap.server.application.port.inbound.admin.RegisterMemberCSVUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -17,10 +17,10 @@ @WebAdapter @RequestMapping("/api/managements") public class RegisterMemberCsvController { - private final RegisterMemberUsecase registerMemberUsecase; + private final RegisterMemberCSVUsecase registerMemberCSVUsecase; - public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase) { - this.registerMemberUsecase = registerMemberUsecase; + public RegisterMemberCsvController(RegisterMemberCSVUsecase registerMemberCSVUsecase) { + this.registerMemberCSVUsecase = registerMemberCSVUsecase; } @Operation(summary = "CSV 파일로 회원 등록 API") @@ -29,7 +29,7 @@ public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase) public ResponseEntity registerMembersFromCsv( @AuthenticationPrincipal SecurityUserDetails userInfo, @RequestParam("file") MultipartFile file) { - int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file); + int addedCount = registerMemberCSVUsecase.registerMembersFromCsv(userInfo.getUserId(), file); return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다."); } -} +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberCSVUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberCSVUsecase.java new file mode 100644 index 00000000..c3e33ccb --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberCSVUsecase.java @@ -0,0 +1,7 @@ +package clap.server.application.port.inbound.admin; + +import org.springframework.web.multipart.MultipartFile; + +public interface RegisterMemberCSVUsecase { + int registerMembersFromCsv(Long adminId, MultipartFile file); +} diff --git a/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java index 08972e2e..a143bd5a 100644 --- a/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java @@ -5,6 +5,4 @@ public interface RegisterMemberUsecase { void registerMember(Long adminId, RegisterMemberRequest request); - - int registerMembersFromCsv(Long adminId, MultipartFile file); } diff --git a/src/main/java/clap/server/application/service/admin/CsvParseService.java b/src/main/java/clap/server/application/service/admin/CsvParseService.java index 6fd8deb7..b6fd6646 100644 --- a/src/main/java/clap/server/application/service/admin/CsvParseService.java +++ b/src/main/java/clap/server/application/service/admin/CsvParseService.java @@ -1,6 +1,7 @@ package clap.server.application.service.admin; import clap.server.application.port.outbound.member.LoadDepartmentPort; +import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Department; import clap.server.domain.model.member.Member; import clap.server.domain.model.member.MemberInfo; @@ -10,7 +11,6 @@ import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; @@ -24,7 +24,7 @@ @Slf4j -@Service +@ApplicationService @RequiredArgsConstructor public class CsvParseService { @@ -33,6 +33,11 @@ public class CsvParseService { public List parse(MultipartFile file) { List members = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { + // 첫 번째 줄은 헤더로 간주하고 다음 줄부터 파싱 + String headerLine = reader.readLine(); + if (headerLine == null) { + throw ApplicationException.from(MemberErrorCode.INVALID_CSV_FORMAT); + } String line; while ((line = reader.readLine()) != null) { String[] fields = line.split(","); diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java new file mode 100644 index 00000000..1b9068e6 --- /dev/null +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java @@ -0,0 +1,32 @@ +package clap.server.application.service.admin; + +import clap.server.application.port.inbound.admin.RegisterMemberCSVUsecase; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.outbound.member.CommandMemberPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@ApplicationService +@RequiredArgsConstructor +public class RegisterMemberCSVService implements RegisterMemberCSVUsecase { + private final MemberService memberService; + private final CommandMemberPort commandMemberPort; + private final CsvParseService csvParser; + + @Override + @Transactional + public int registerMembersFromCsv(Long adminId, MultipartFile file) { + List members = csvParser.parse(file); + Member admin = memberService.findActiveMember(adminId); + members.forEach(member -> { + member.register(admin); + commandMemberPort.save(member); + }); + return members.size(); + } +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java index 115e9410..4019b897 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -14,7 +14,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; + import java.util.List; @@ -41,15 +41,4 @@ public void registerMember(Long adminId, RegisterMemberRequest request) { commandMemberPort.save(member); } - @Override - @Transactional - public int registerMembersFromCsv(Long adminId, MultipartFile file) { - List members = csvParser.parse(file); - Member admin = memberService.findActiveMember(adminId); - members.forEach(member -> { - member.register(admin); - commandMemberPort.save(member); - }); - return members.size(); - } } diff --git a/src/test/java/clap/server/application/service/admin/RegisterMemberCSVServiceTest.java b/src/test/java/clap/server/application/service/admin/RegisterMemberCSVServiceTest.java new file mode 100644 index 00000000..0d8bdf43 --- /dev/null +++ b/src/test/java/clap/server/application/service/admin/RegisterMemberCSVServiceTest.java @@ -0,0 +1,125 @@ +package clap.server.application.service.admin; + +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.outbound.member.CommandMemberPort; +import clap.server.domain.model.member.Member; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.MemberErrorCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class RegisterMemberCSVServiceTest { + + private RegisterMemberCSVService registerMemberCSVService; + private MemberService memberService; + private CommandMemberPort commandMemberPort; + private CsvParseService csvParseService; + + + @BeforeEach + void setup() { + memberService = Mockito.mock(MemberService.class); + commandMemberPort = Mockito.mock(CommandMemberPort.class); + csvParseService = Mockito.mock(CsvParseService.class); + registerMemberCSVService = new RegisterMemberCSVService(memberService, commandMemberPort, csvParseService); + } + + /** + * 정상적인 회원 등록 테스트 + * - 주어진 CSV 파일을 정상적으로 파싱하여 회원이 등록되는지 검증 + */ + @Test + void testRegisterMembersFromCsvSuccess() { + Long adminId = 1L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + Member admin = Mockito.mock(Member.class); + List parsedMembers = List.of(Mockito.mock(Member.class), Mockito.mock(Member.class)); + + when(memberService.findActiveMember(adminId)).thenReturn(admin); + when(csvParseService.parse(file)).thenReturn(parsedMembers); + + int addedCount = registerMemberCSVService.registerMembersFromCsv(adminId, file); + + assertEquals(2, addedCount); + verify(commandMemberPort, times(2)).save(any(Member.class)); + verify(parsedMembers.get(0), times(1)).register(admin); + verify(parsedMembers.get(1), times(1)).register(admin); + } + + /** + * ❌ 관리자 찾기 실패 (MEMBER_NOT_FOUND) + */ + @Test + void testRegisterMembersFromCsvThrowsWhenAdminNotFound() { + Long adminId = 99L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + when(memberService.findActiveMember(adminId)).thenThrow(new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + registerMemberCSVService.registerMembersFromCsv(adminId, file); + }); + + // 검증: 발생한 예외가 `MEMBER_NOT_FOUND`인지 확인 + assertEquals(MemberErrorCode.MEMBER_NOT_FOUND.getCustomCode(), exception.getCode().getCustomCode()); + verifyNoInteractions(commandMemberPort); // 회원 저장 로직이 실행안됨 + } + + /** + * ❌ CSV 파싱 실패 (CSV_PARSING_ERROR) + */ + @Test + void testRegisterMembersFromCsvThrowsWhenCsvParsingFails() { + Long adminId = 1L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + // ✅ Mock 객체 설정: CSV 파싱 과정에서 예외 발생하도록 설정 + when(csvParseService.parse(file)).thenThrow(new ApplicationException(MemberErrorCode.CSV_PARSING_ERROR)); + + // 🔹 유스케이스 실행 및 예외 검증 + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + registerMemberCSVService.registerMembersFromCsv(adminId, file); + }); + + // ✅ 검증: 발생한 예외가 `CSV_PARSING_ERROR`인지 확인 + assertEquals(MemberErrorCode.CSV_PARSING_ERROR.getCustomCode(), exception.getCode().getCustomCode()); + verifyNoInteractions(commandMemberPort); // ❗ 회원 저장 로직이 실행되지 않아야 함 + } + + /** + * ❌ 회원 등록 실패 (MEMBER_REGISTRATION_FAILED) + * + */ + @Test + void testRegisterMembersFromCsvThrowsWhenSavingMemberFails() { + Long adminId = 1L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + Member admin = Mockito.mock(Member.class); + Member failingMember = Mockito.mock(Member.class); + List parsedMembers = List.of(failingMember, Mockito.mock(Member.class)); + + // 특정 회원 등록 중 예외 발생 + when(memberService.findActiveMember(adminId)).thenReturn(admin); + when(csvParseService.parse(file)).thenReturn(parsedMembers); + doThrow(new ApplicationException(MemberErrorCode.MEMBER_REGISTRATION_FAILED)) + .when(commandMemberPort).save(failingMember); + + // Usecase 실행 + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + registerMemberCSVService.registerMembersFromCsv(adminId, file); + }); + + assertEquals(MemberErrorCode.MEMBER_REGISTRATION_FAILED.getCustomCode(), exception.getCode().getCustomCode()); + verify(commandMemberPort, times(1)).save(failingMember); // ❗ 실패한 회원만 저장 시도해야 함 + } +}