Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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")
Expand All @@ -29,7 +29,7 @@ public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase)
public ResponseEntity<String> 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 + "명의 회원이 등록되었습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@

public interface RegisterMemberUsecase {
void registerMember(Long adminId, RegisterMemberRequest request);

int registerMembersFromCsv(Long adminId, MultipartFile file);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -24,7 +24,7 @@


@Slf4j
@Service
@ApplicationService
@RequiredArgsConstructor
public class CsvParseService {

Expand All @@ -33,6 +33,11 @@ public class CsvParseService {
public List<Member> parse(MultipartFile file) {
List<Member> 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(",");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Member> members = csvParser.parse(file);
Member admin = memberService.findActiveMember(adminId);
members.forEach(member -> {
member.register(admin);
commandMemberPort.save(member);
});
return members.size();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

@Override
@Transactional
public int registerMembersFromCsv(Long adminId, MultipartFile file) {
List<Member> members = csvParser.parse(file);
Member admin = memberService.findActiveMember(adminId);
members.forEach(member -> {
member.register(admin);
commandMemberPort.save(member);
});
return members.size();
}
}
Original file line number Diff line number Diff line change
@@ -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<Member> 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<Member> 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); // ❗ 실패한 회원만 저장 시도해야 함
}
}
Loading