From e8d1f135b803059be0f7502994f381cac54f8398 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Sat, 25 Jan 2025 02:52:54 +0900 Subject: [PATCH 01/13] =?UTF-8?q?CLAP-116=20Feat:=ED=9A=8C=EC=9B=90=20CSV?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../web/admin/RegisterMemberController.java | 2 + .../admin/RegisterMemberCsvController.java | 42 +++++++++++++++ .../outbound/persistense/CsvParseAdapter.java | 52 +++++++++++++++++++ .../management/RegisterMemberCsvUsecase.java | 10 ++++ .../management/RegisterMemberUsecase.java | 3 ++ .../admin/RegisterMemberCsvService.java | 42 +++++++++++++++ .../service/admin/RegisterMemberService.java | 25 ++++++++- src/main/resources/env.properties | 6 +++ 9 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java create mode 100644 src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java create mode 100644 src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java create mode 100644 src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java create mode 100644 src/main/resources/env.properties diff --git a/build.gradle b/build.gradle index b87517d6..c5ded38c 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter' //ElasticSearch implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java index e26db4f4..160ee222 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; @@ -19,6 +20,7 @@ @RequiredArgsConstructor @RequestMapping("/api/managements") public class RegisterMemberController { + @Qualifier("registerMemberService") private final RegisterMemberUsecase registerMemberUsecase; @Operation(summary = "단일 회원 등록 API") 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 new file mode 100644 index 00000000..2e68737a --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java @@ -0,0 +1,42 @@ +package clap.server.adapter.inbound.web.admin; + +import clap.server.adapter.inbound.security.SecurityUserDetails; +import clap.server.application.port.inbound.management.RegisterMemberUsecase; +import clap.server.common.annotation.architecture.WebAdapter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.beans.factory.annotation.Qualifier; + +@WebAdapter +@RequestMapping("/api/admin") +public class RegisterMemberCsvController { + private final RegisterMemberUsecase registerMemberUsecase; + + public RegisterMemberCsvController(@Qualifier("registerMemberCsvService") RegisterMemberUsecase registerMemberUsecase) { + this.registerMemberUsecase = registerMemberUsecase; + } + + @Operation(summary = "CSV 파일로 회원 등록 API") + @PostMapping("/members/upload") + @Secured("ROLE_ADMIN") + public ResponseEntity registerMembersFromCsv( + @AuthenticationPrincipal SecurityUserDetails userInfo, + @RequestParam("file") MultipartFile file) { + try { + int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file); + return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다."); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body("CSV 형식 오류: " + e.getMessage()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("회원 등록 중 오류가 발생했습니다: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java new file mode 100644 index 00000000..ed8665f4 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java @@ -0,0 +1,52 @@ + package clap.server.adapter.outbound.persistense; + + import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; + import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; + import clap.server.common.annotation.architecture.WebAdapter; + import clap.server.domain.model.member.Member; + import org.springframework.stereotype.Component; + import org.springframework.web.multipart.MultipartFile; + + import java.io.BufferedReader; + import java.io.IOException; + import java.io.InputStreamReader; + import java.util.ArrayList; + import java.util.List; + + + @Component + public class CsvParseAdapter { + + public List parse(MultipartFile file) throws IOException { + List memberRequests = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { + String line; + int lineNumber = 0; + while ((line = reader.readLine()) != null) { + lineNumber++; + String[] fields = line.split(","); + + // 필드 검증 + if (fields.length != 7) { + throw new IllegalArgumentException("CSV 데이터가 잘못되었습니다. " + lineNumber + "번째 줄"); + } + try { + // DTO 생성 + memberRequests.add(new RegisterMemberRequest( + fields[0].trim(), // name + fields[4].trim(), // email + fields[1].trim(), // nickname + Boolean.valueOf(fields[6].trim()), // isReviewer (Boolean 객체) + Long.valueOf(fields[2].trim()), // departmentId (Long 객체) + MemberRole.valueOf(fields[5].trim()), // role (enum) + fields[3].trim() // departmentRole + )); + } catch (Exception e) { + throw new IllegalArgumentException("CSV 데이터 파싱 오류: " + lineNumber + "번째 줄, 내용: " + line, e); + } + } + } + return memberRequests; + } + } diff --git a/src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java b/src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java new file mode 100644 index 00000000..36240f01 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java @@ -0,0 +1,10 @@ +package clap.server.application.port.inbound.management; + +import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; +import org.springframework.web.multipart.MultipartFile; + +public interface RegisterMemberCsvUsecase { + void registerMember(Long adminId, RegisterMemberRequest request); + + int registerMembersFromCsv(Long adminId, MultipartFile file); +} diff --git a/src/main/java/clap/server/application/port/inbound/management/RegisterMemberUsecase.java b/src/main/java/clap/server/application/port/inbound/management/RegisterMemberUsecase.java index 909842c8..79527a28 100644 --- a/src/main/java/clap/server/application/port/inbound/management/RegisterMemberUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/management/RegisterMemberUsecase.java @@ -1,7 +1,10 @@ package clap.server.application.port.inbound.management; import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; +import org.springframework.web.multipart.MultipartFile; 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/RegisterMemberCsvService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java new file mode 100644 index 00000000..e9613873 --- /dev/null +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java @@ -0,0 +1,42 @@ +package clap.server.application.service.admin; + +import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; +import clap.server.adapter.outbound.persistense.CsvParseAdapter; +import clap.server.application.port.inbound.management.RegisterMemberUsecase; +import clap.server.application.port.outbound.member.CommandMemberPort; +import clap.server.domain.model.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RegisterMemberCsvService implements RegisterMemberUsecase { + private final CsvParseAdapter csvParser; + + @Override + @Transactional + public int registerMembersFromCsv(Long adminId, MultipartFile file) { + try { + // CSV 파싱 + List memberRequests = csvParser.parse(file); + + // 각 회원 등록 호출 + memberRequests.forEach(request -> registerMember(adminId, request)); + + return memberRequests.size(); + } catch (IOException e) { + throw new RuntimeException("CSV 파일 처리 중 오류 발생", e); + } + } + + @Override + @Transactional + public void registerMember(Long adminId, RegisterMemberRequest request) { + // 기존 단일 회원 추가 로직 그대로 유지 + } +} 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 906cf06b..c0cf28ee 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -1,6 +1,7 @@ package clap.server.application.service.admin; import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; +import clap.server.adapter.outbound.persistense.CsvParseAdapter; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.management.RegisterMemberUsecase; import clap.server.application.port.outbound.member.CommandMemberPort; @@ -14,6 +15,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; import static clap.server.application.mapper.MemberInfoMapper.toMemberInfo; import static clap.server.application.mapper.MemberMapper.toMember; @@ -25,16 +30,34 @@ class RegisterMemberService implements RegisterMemberUsecase { private final CommandMemberPort commandMemberPort; private final LoadDepartmentPort loadDepartmentPort; private final PasswordEncoder passwordEncoder; + private final CsvParseAdapter csvParser; // CsvParseAdapter 필드 추가 @Override @Transactional public void registerMember(Long adminId, RegisterMemberRequest request) { Member admin = memberService.findActiveMember(adminId); - Department department = loadDepartmentPort.findById(request.departmentId()).orElseThrow(()-> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); + Department department = loadDepartmentPort.findById(request.departmentId()) + .orElseThrow(() -> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); MemberInfo memberInfo = toMemberInfo(request.name(), request.email(), request.nickname(), request.isReviewer(), department, request.role(), request.departmentRole()); Member member = toMember(memberInfo); member.register(admin); commandMemberPort.save(member); } + + @Override + @Transactional + public int registerMembersFromCsv(Long adminId, MultipartFile file) { + try { + // CSV 파일 파싱 + List memberRequests = csvParser.parse(file); + + // 기존 단일 회원 등록 로직 재사용 + memberRequests.forEach(request -> registerMember(adminId, request)); + + return memberRequests.size(); + } catch (IOException e) { + throw new RuntimeException("CSV 파일 처리 중 오류 발생", e); + } + } } diff --git a/src/main/resources/env.properties b/src/main/resources/env.properties new file mode 100644 index 00000000..c795a2b1 --- /dev/null +++ b/src/main/resources/env.properties @@ -0,0 +1,6 @@ +DATABASE_USERNAME=root +DATABASE_PASSWORD=root + +SWAGGER_SERVER_URL=http://localhost:9999 + +APPLICATION_PORT=9999 \ No newline at end of file From db9c8614c36e320a2fe104932eeeafce09474097 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Sat, 25 Jan 2025 12:04:05 +0900 Subject: [PATCH 02/13] =?UTF-8?q?CLAP-116=20fix:RegisterMemberCsvUsecase?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inbound/web/admin/RegisterMemberController.java | 10 +++++++--- .../inbound/web/admin/RegisterMemberCsvController.java | 1 + .../adapter/outbound/persistense/CsvParseAdapter.java | 9 +++------ .../service/admin/RegisterMemberCsvService.java | 1 + 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java index 160ee222..8a3600f8 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java @@ -17,17 +17,21 @@ @Tag(name = "회원 관리 - 등록") @WebAdapter -@RequiredArgsConstructor @RequestMapping("/api/managements") public class RegisterMemberController { - @Qualifier("registerMemberService") + private final RegisterMemberUsecase registerMemberUsecase; + // @Qualifier 추가 + public RegisterMemberController(@Qualifier("registerMemberService") RegisterMemberUsecase registerMemberUsecase) { + this.registerMemberUsecase = registerMemberUsecase; + } + @Operation(summary = "단일 회원 등록 API") @PostMapping("/members") @Secured("ROLE_ADMIN") public void registerMember(@AuthenticationPrincipal SecurityUserDetails userInfo, - @RequestBody @Valid RegisterMemberRequest request){ + @RequestBody @Valid RegisterMemberRequest request) { registerMemberUsecase.registerMember(userInfo.getUserId(), request); } } \ No newline at end of file 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 2e68737a..c14bc8b4 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 @@ -23,6 +23,7 @@ public RegisterMemberCsvController(@Qualifier("registerMemberCsvService") Regist this.registerMemberUsecase = registerMemberUsecase; } + @Operation(summary = "CSV 파일로 회원 등록 API") @PostMapping("/members/upload") @Secured("ROLE_ADMIN") diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java index ed8665f4..3476c81b 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java @@ -2,8 +2,6 @@ import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; - import clap.server.common.annotation.architecture.WebAdapter; - import clap.server.domain.model.member.Member; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -32,14 +30,13 @@ public List parse(MultipartFile file) throws IOException throw new IllegalArgumentException("CSV 데이터가 잘못되었습니다. " + lineNumber + "번째 줄"); } try { - // DTO 생성 memberRequests.add(new RegisterMemberRequest( fields[0].trim(), // name fields[4].trim(), // email fields[1].trim(), // nickname - Boolean.valueOf(fields[6].trim()), // isReviewer (Boolean 객체) - Long.valueOf(fields[2].trim()), // departmentId (Long 객체) - MemberRole.valueOf(fields[5].trim()), // role (enum) + Boolean.valueOf(fields[6].trim()), // isReviewer + Long.valueOf(fields[2].trim()), // departmentId + MemberRole.valueOf(fields[5].trim()), // role fields[3].trim() // departmentRole )); } catch (Exception e) { diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java index e9613873..a46654b7 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java @@ -18,6 +18,7 @@ public class RegisterMemberCsvService implements RegisterMemberUsecase { private final CsvParseAdapter csvParser; + @Override @Transactional public int registerMembersFromCsv(Long adminId, MultipartFile file) { From 548ea439a9c396c10a39cb0b90f53e9f2f336061 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Sun, 26 Jan 2025 04:51:38 +0900 Subject: [PATCH 03/13] =?UTF-8?q?CLAP-116=20fix:=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/RegisterMemberCsvController.java | 11 +-- .../outbound/persistense/CsvParseAdapter.java | 78 ++++++++++--------- .../management/RegisterMemberCsvUsecase.java | 10 --- .../admin/RegisterMemberCsvService.java | 27 ++++--- .../exception/code/MemberErrorCode.java | 3 + src/test/resources/application-test.yml | 41 ++++++++++ 6 files changed, 108 insertions(+), 62 deletions(-) delete mode 100644 src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java create mode 100644 src/test/resources/application-test.yml 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 c14bc8b4..6ff58406 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 @@ -3,6 +3,7 @@ import clap.server.adapter.inbound.security.SecurityUserDetails; import clap.server.application.port.inbound.management.RegisterMemberUsecase; import clap.server.common.annotation.architecture.WebAdapter; +import clap.server.exception.ApplicationException; import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -23,7 +24,6 @@ public RegisterMemberCsvController(@Qualifier("registerMemberCsvService") Regist this.registerMemberUsecase = registerMemberUsecase; } - @Operation(summary = "CSV 파일로 회원 등록 API") @PostMapping("/members/upload") @Secured("ROLE_ADMIN") @@ -33,11 +33,12 @@ public ResponseEntity registerMembersFromCsv( try { int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file); return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다."); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body("CSV 형식 오류: " + e.getMessage()); + } catch (ApplicationException e) { + return ResponseEntity.status(e.getCode().getHttpStatus()) + .body(e.getCode().getMessage()); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("회원 등록 중 오류가 발생했습니다: " + e.getMessage()); + .body("회원 등록 중 알 수 없는 오류가 발생했습니다: " + e.getMessage()); } } -} \ No newline at end of file +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java index 3476c81b..95f98874 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java @@ -1,49 +1,53 @@ - package clap.server.adapter.outbound.persistense; +package clap.server.adapter.outbound.persistense; - import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; - import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; - import org.springframework.stereotype.Component; - import org.springframework.web.multipart.MultipartFile; +import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.MemberErrorCode; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import lombok.extern.slf4j.Slf4j; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.MemberErrorCode; - import java.io.BufferedReader; - import java.io.IOException; - import java.io.InputStreamReader; - import java.util.ArrayList; - import java.util.List; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +@Slf4j +@Component +public class CsvParseAdapter { - @Component - public class CsvParseAdapter { + public List parse(MultipartFile file) throws IOException { + List memberRequests = new ArrayList<>(); - public List parse(MultipartFile file) throws IOException { - List memberRequests = new ArrayList<>(); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { - String line; - int lineNumber = 0; - while ((line = reader.readLine()) != null) { - lineNumber++; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { + String line; + int lineNumber = 0; + while ((line = reader.readLine()) != null) { + lineNumber++; + try { String[] fields = line.split(","); - - // 필드 검증 if (fields.length != 7) { - throw new IllegalArgumentException("CSV 데이터가 잘못되었습니다. " + lineNumber + "번째 줄"); - } - try { - memberRequests.add(new RegisterMemberRequest( - fields[0].trim(), // name - fields[4].trim(), // email - fields[1].trim(), // nickname - Boolean.valueOf(fields[6].trim()), // isReviewer - Long.valueOf(fields[2].trim()), // departmentId - MemberRole.valueOf(fields[5].trim()), // role - fields[3].trim() // departmentRole - )); - } catch (Exception e) { - throw new IllegalArgumentException("CSV 데이터 파싱 오류: " + lineNumber + "번째 줄, 내용: " + line, e); + throw ApplicationException.from(MemberErrorCode.INVALID_CSV_FORMAT); } + + memberRequests.add(new RegisterMemberRequest( + fields[0].trim(), // name + fields[4].trim(), // email + fields[1].trim(), // nickname + Boolean.parseBoolean(fields[6].trim()), // isReviewer + Long.parseLong(fields[2].trim()), // departmentId + MemberRole.valueOf(fields[5].trim()), // role + fields[3].trim() // departmentRole + )); + } catch (Exception e) { + throw ApplicationException.from(MemberErrorCode.CSV_PARSING_ERROR); } } - return memberRequests; } + return memberRequests; } +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java b/src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java deleted file mode 100644 index 36240f01..00000000 --- a/src/main/java/clap/server/application/port/inbound/management/RegisterMemberCsvUsecase.java +++ /dev/null @@ -1,10 +0,0 @@ -package clap.server.application.port.inbound.management; - -import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; -import org.springframework.web.multipart.MultipartFile; - -public interface RegisterMemberCsvUsecase { - void registerMember(Long adminId, RegisterMemberRequest request); - - int registerMembersFromCsv(Long adminId, MultipartFile file); -} diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java index a46654b7..76c2649a 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java @@ -3,41 +3,48 @@ import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; import clap.server.adapter.outbound.persistense.CsvParseAdapter; import clap.server.application.port.inbound.management.RegisterMemberUsecase; -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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.MemberErrorCode; import java.io.IOException; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class RegisterMemberCsvService implements RegisterMemberUsecase { private final CsvParseAdapter csvParser; - @Override @Transactional public int registerMembersFromCsv(Long adminId, MultipartFile file) { try { - // CSV 파싱 List memberRequests = csvParser.parse(file); - // 각 회원 등록 호출 - memberRequests.forEach(request -> registerMember(adminId, request)); - - return memberRequests.size(); + int successCount = 0; + for (RegisterMemberRequest request : memberRequests) { + try { + registerMember(adminId, request); + successCount++; + } catch (Exception e) { + throw ApplicationException.from(MemberErrorCode.MEMBER_REGISTRATION_FAILED); + } + } + return successCount; } catch (IOException e) { - throw new RuntimeException("CSV 파일 처리 중 오류 발생", e); + throw ApplicationException.from(MemberErrorCode.CSV_PARSING_ERROR); } } @Override @Transactional public void registerMember(Long adminId, RegisterMemberRequest request) { - // 기존 단일 회원 추가 로직 그대로 유지 } } diff --git a/src/main/java/clap/server/exception/code/MemberErrorCode.java b/src/main/java/clap/server/exception/code/MemberErrorCode.java index d09423b8..6c96e8fd 100644 --- a/src/main/java/clap/server/exception/code/MemberErrorCode.java +++ b/src/main/java/clap/server/exception/code/MemberErrorCode.java @@ -9,6 +9,9 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_001", "회원을 찾을 수 없습니다."), ACTIVE_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_002", "활성화 회원을 찾을 수 없습니다."), + INVALID_CSV_FORMAT(HttpStatus.BAD_REQUEST, "MEMBER_003", "CSV 파일 형식이 잘못되었습니다."), + CSV_PARSING_ERROR(HttpStatus.BAD_REQUEST, "MEMBER_004", "CSV 데이터 파싱 중 오류가 발생했습니다."), + MEMBER_REGISTRATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER_005", "회원 등록 중 오류가 발생했습니다."), ; private final HttpStatus httpStatus; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 00000000..db697aa2 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,41 @@ +spring: + flyway: + enabled: false # Flyway 비활성화 + elasticsearch: + enabled: false # Elasticsearch 비활성화 + + application: + name: taskflow # 애플리케이션 이름 설정 + + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE # H2 임베디드 메모리 데이터베이스 + driver-class-name: org.h2.Driver # H2 데이터베이스 드라이버 + username: sa # 데이터베이스 사용자명 + password: # 비밀번호 없음 + sql: + init: + mode: always # SQL 초기화 활성화 + + jpa: + hibernate: + ddl-auto: create-drop # 테스트 실행 시 스키마 생성 및 삭제 + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect # H2용 방언 설정 + show_sql: true # SQL 쿼리 출력 + format_sql: true # SQL 쿼리를 보기 좋게 출력 + use_sql_comments: true # SQL 쿼리에 주석 포함 + default_batch_fetch_size: 1000 # 배치 페치 크기 설정 + + logging: + level: + root: debug # 디버그 레벨로 로그 출력 + +swagger: + server: + url: http://localhost:8080 # Swagger 기본 서버 URL + +jwt: + secret-key: + access-token: exampleSecretKeyForTFSystemAccessSecretKeyTestForPadding + temporary-token: exampleSecretKeyForTFSystem From 0a73fb0bbbf56581fc0fb3c62dc587bad5f9259c Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Wed, 29 Jan 2025 01:52:24 +0900 Subject: [PATCH 04/13] =?UTF-8?q?CLAP-116=20fix:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20ExceptionHan?= =?UTF-8?q?dler=EB=A1=9C=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/RegisterMemberCsvController.java | 13 ++---- .../outbound/persistense/CsvParseAdapter.java | 46 +++++++++---------- .../admin/RegisterMemberCsvService.java | 20 +++----- .../server/exception/ExceptionAdvice.java | 25 ++++++++-- 4 files changed, 52 insertions(+), 52 deletions(-) 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 6ff58406..ffb97692 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 @@ -5,6 +5,7 @@ import clap.server.common.annotation.architecture.WebAdapter; import clap.server.exception.ApplicationException; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; @@ -30,15 +31,7 @@ public RegisterMemberCsvController(@Qualifier("registerMemberCsvService") Regist public ResponseEntity registerMembersFromCsv( @AuthenticationPrincipal SecurityUserDetails userInfo, @RequestParam("file") MultipartFile file) { - try { - int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file); - return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다."); - } catch (ApplicationException e) { - return ResponseEntity.status(e.getCode().getHttpStatus()) - .body(e.getCode().getMessage()); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("회원 등록 중 알 수 없는 오류가 발생했습니다: " + e.getMessage()); - } + int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file); + return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다."); } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java index 95f98874..c0d822fd 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java @@ -4,11 +4,9 @@ import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.exception.ApplicationException; import clap.server.exception.code.MemberErrorCode; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import lombok.extern.slf4j.Slf4j; -import clap.server.exception.ApplicationException; -import clap.server.exception.code.MemberErrorCode; import java.io.BufferedReader; import java.io.IOException; @@ -20,34 +18,32 @@ @Component public class CsvParseAdapter { - public List parse(MultipartFile file) throws IOException { + public List parse(MultipartFile file) { List memberRequests = new ArrayList<>(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { String line; - int lineNumber = 0; while ((line = reader.readLine()) != null) { - lineNumber++; - try { - String[] fields = line.split(","); - if (fields.length != 7) { - throw ApplicationException.from(MemberErrorCode.INVALID_CSV_FORMAT); - } - - memberRequests.add(new RegisterMemberRequest( - fields[0].trim(), // name - fields[4].trim(), // email - fields[1].trim(), // nickname - Boolean.parseBoolean(fields[6].trim()), // isReviewer - Long.parseLong(fields[2].trim()), // departmentId - MemberRole.valueOf(fields[5].trim()), // role - fields[3].trim() // departmentRole - )); - } catch (Exception e) { - throw ApplicationException.from(MemberErrorCode.CSV_PARSING_ERROR); + String[] fields = line.split(","); + if (fields.length != 7) { + throw ApplicationException.from(MemberErrorCode.INVALID_CSV_FORMAT); } + memberRequests.add(mapToRegisterMemberRequest(fields)); } + } catch (IOException e) { + throw ApplicationException.from(MemberErrorCode.CSV_PARSING_ERROR); } return memberRequests; } -} \ No newline at end of file + + private RegisterMemberRequest mapToRegisterMemberRequest(String[] fields) { + return new RegisterMemberRequest( + fields[0].trim(), // name + fields[4].trim(), // email + fields[1].trim(), // nickname + Boolean.parseBoolean(fields[6].trim()), // isReviewer + Long.parseLong(fields[2].trim()), // departmentId + MemberRole.valueOf(fields[5].trim()), // role + fields[3].trim() // departmentRole + ); + } +} diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java index 76c2649a..a96891d9 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java @@ -25,22 +25,14 @@ public class RegisterMemberCsvService implements RegisterMemberUsecase { @Override @Transactional public int registerMembersFromCsv(Long adminId, MultipartFile file) { - try { - List memberRequests = csvParser.parse(file); + List memberRequests = csvParser.parse(file); + int successCount = 0; - int successCount = 0; - for (RegisterMemberRequest request : memberRequests) { - try { - registerMember(adminId, request); - successCount++; - } catch (Exception e) { - throw ApplicationException.from(MemberErrorCode.MEMBER_REGISTRATION_FAILED); - } - } - return successCount; - } catch (IOException e) { - throw ApplicationException.from(MemberErrorCode.CSV_PARSING_ERROR); + for (RegisterMemberRequest request : memberRequests) { + registerMember(adminId, request); + successCount++; } + return successCount; } @Override diff --git a/src/main/java/clap/server/exception/ExceptionAdvice.java b/src/main/java/clap/server/exception/ExceptionAdvice.java index d36b3e2a..354251e2 100644 --- a/src/main/java/clap/server/exception/ExceptionAdvice.java +++ b/src/main/java/clap/server/exception/ExceptionAdvice.java @@ -3,6 +3,7 @@ import clap.server.exception.code.AuthErrorCode; import clap.server.exception.code.BaseErrorCode; import clap.server.exception.code.CommonErrorCode; +import clap.server.exception.code.MemberErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; @@ -57,9 +58,9 @@ public ResponseEntity handleMethodArgumentNotValid( @ExceptionHandler public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { String errorMessage = e.getConstraintViolations().stream() - .map(ConstraintViolation::getMessage) - .findFirst() - .orElseThrow(() -> new RuntimeException("ConstraintViolationException Error")); + .map(ConstraintViolation::getMessage) + .findFirst() + .orElseThrow(() -> new RuntimeException("ConstraintViolationException Error")); return handleExceptionInternalConstraint(e, CommonErrorCode.valueOf(errorMessage), HttpHeaders.EMPTY, request); } @@ -87,6 +88,16 @@ public ResponseEntity onThrowException( return handleExceptionInternal(exception, baseErrorCode, null, request); } + @ExceptionHandler(value = { ApplicationException.class }) + public ResponseEntity handleCsvApplicationException(ApplicationException e, WebRequest request) { + if (e.getCode() == MemberErrorCode.CSV_PARSING_ERROR || e.getCode() == MemberErrorCode.INVALID_CSV_FORMAT) { + log.error("CSV Parsing Error: {}", e.getCode().getMessage()); + return buildErrorResponse(e.getCode()); + } + + return buildErrorResponse(e.getCode()); + } + private ResponseEntity handleExceptionInternal( BaseException e, BaseErrorCode baseErrorCode, @@ -170,4 +181,12 @@ public ResponseEntity handleAccessDeniedException(AccessDeniedException AuthErrorCode.FORBIDDEN.getMessage() ); } + + private ResponseEntity buildErrorResponse(BaseErrorCode errorCode) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(Map.of( + "code", errorCode.getCustomCode(), + "message", errorCode.getMessage() + )); + } } From 9716cd3c855618ddf97d23524c958a15a64c0d5d Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Wed, 29 Jan 2025 01:54:19 +0900 Subject: [PATCH 05/13] Delete src/test/resources/application-test.yml --- src/test/resources/application-test.yml | 41 ------------------------- 1 file changed, 41 deletions(-) delete mode 100644 src/test/resources/application-test.yml diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml deleted file mode 100644 index db697aa2..00000000 --- a/src/test/resources/application-test.yml +++ /dev/null @@ -1,41 +0,0 @@ -spring: - flyway: - enabled: false # Flyway 비활성화 - elasticsearch: - enabled: false # Elasticsearch 비활성화 - - application: - name: taskflow # 애플리케이션 이름 설정 - - datasource: - url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE # H2 임베디드 메모리 데이터베이스 - driver-class-name: org.h2.Driver # H2 데이터베이스 드라이버 - username: sa # 데이터베이스 사용자명 - password: # 비밀번호 없음 - sql: - init: - mode: always # SQL 초기화 활성화 - - jpa: - hibernate: - ddl-auto: create-drop # 테스트 실행 시 스키마 생성 및 삭제 - properties: - hibernate: - dialect: org.hibernate.dialect.H2Dialect # H2용 방언 설정 - show_sql: true # SQL 쿼리 출력 - format_sql: true # SQL 쿼리를 보기 좋게 출력 - use_sql_comments: true # SQL 쿼리에 주석 포함 - default_batch_fetch_size: 1000 # 배치 페치 크기 설정 - - logging: - level: - root: debug # 디버그 레벨로 로그 출력 - -swagger: - server: - url: http://localhost:8080 # Swagger 기본 서버 URL - -jwt: - secret-key: - access-token: exampleSecretKeyForTFSystemAccessSecretKeyTestForPadding - temporary-token: exampleSecretKeyForTFSystem From 3e3e21246016e24a6a0e51baf443b0b9cb83d808 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Wed, 29 Jan 2025 02:57:03 +0900 Subject: [PATCH 06/13] =?UTF-8?q?CLAP-116=20fix:RegistermemberCsvService?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/admin/RegisterMemberController.java | 6 +-- .../admin/RegisterMemberCsvController.java | 4 +- .../admin/RegisterMemberCsvService.java | 42 ------------------- .../service/admin/RegisterMemberService.java | 34 ++++++++------- .../server/exception/ExceptionAdvice.java | 22 +++++----- 5 files changed, 31 insertions(+), 77 deletions(-) delete mode 100644 src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java index 8a3600f8..2d14a8b9 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java @@ -18,15 +18,11 @@ @Tag(name = "회원 관리 - 등록") @WebAdapter @RequestMapping("/api/managements") +@RequiredArgsConstructor public class RegisterMemberController { private final RegisterMemberUsecase registerMemberUsecase; - // @Qualifier 추가 - public RegisterMemberController(@Qualifier("registerMemberService") RegisterMemberUsecase registerMemberUsecase) { - this.registerMemberUsecase = registerMemberUsecase; - } - @Operation(summary = "단일 회원 등록 API") @PostMapping("/members") @Secured("ROLE_ADMIN") 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 ffb97692..9de44e9b 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 @@ -14,14 +14,14 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; -import org.springframework.beans.factory.annotation.Qualifier; +@Tag(name = "Admin") @WebAdapter @RequestMapping("/api/admin") public class RegisterMemberCsvController { private final RegisterMemberUsecase registerMemberUsecase; - public RegisterMemberCsvController(@Qualifier("registerMemberCsvService") RegisterMemberUsecase registerMemberUsecase) { + public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase) { this.registerMemberUsecase = registerMemberUsecase; } diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java deleted file mode 100644 index a96891d9..00000000 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberCsvService.java +++ /dev/null @@ -1,42 +0,0 @@ -package clap.server.application.service.admin; - -import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; -import clap.server.adapter.outbound.persistense.CsvParseAdapter; -import clap.server.application.port.inbound.management.RegisterMemberUsecase; -import clap.server.exception.ApplicationException; -import clap.server.exception.code.MemberErrorCode; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; -import clap.server.exception.ApplicationException; -import clap.server.exception.code.MemberErrorCode; - -import java.io.IOException; -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class RegisterMemberCsvService implements RegisterMemberUsecase { - private final CsvParseAdapter csvParser; - - @Override - @Transactional - public int registerMembersFromCsv(Long adminId, MultipartFile file) { - List memberRequests = csvParser.parse(file); - int successCount = 0; - - for (RegisterMemberRequest request : memberRequests) { - registerMember(adminId, request); - successCount++; - } - return successCount; - } - - @Override - @Transactional - public void registerMember(Long adminId, RegisterMemberRequest request) { - } -} 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 c0cf28ee..843ab306 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -25,12 +25,12 @@ @ApplicationService @RequiredArgsConstructor -class RegisterMemberService implements RegisterMemberUsecase { +public class RegisterMemberService implements RegisterMemberUsecase { private final MemberService memberService; private final CommandMemberPort commandMemberPort; private final LoadDepartmentPort loadDepartmentPort; private final PasswordEncoder passwordEncoder; - private final CsvParseAdapter csvParser; // CsvParseAdapter 필드 추가 + private final CsvParseAdapter csvParser; // CsvParseAdapter 주입 @Override @Transactional @@ -38,26 +38,28 @@ public void registerMember(Long adminId, RegisterMemberRequest request) { Member admin = memberService.findActiveMember(adminId); Department department = loadDepartmentPort.findById(request.departmentId()) .orElseThrow(() -> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); - MemberInfo memberInfo = toMemberInfo(request.name(), request.email(), request.nickname(), request.isReviewer(), - department, request.role(), request.departmentRole()); + + MemberInfo memberInfo = toMemberInfo( + request.name(), + request.email(), + request.nickname(), + request.isReviewer(), + department, + request.role(), + request.departmentRole() + ); + Member member = toMember(memberInfo); member.register(admin); + commandMemberPort.save(member); } @Override @Transactional public int registerMembersFromCsv(Long adminId, MultipartFile file) { - try { - // CSV 파일 파싱 - List memberRequests = csvParser.parse(file); - - // 기존 단일 회원 등록 로직 재사용 - memberRequests.forEach(request -> registerMember(adminId, request)); - - return memberRequests.size(); - } catch (IOException e) { - throw new RuntimeException("CSV 파일 처리 중 오류 발생", e); - } + List memberRequests = csvParser.parse(file); + memberRequests.forEach(request -> registerMember(adminId, request)); + return memberRequests.size(); } -} +} \ No newline at end of file diff --git a/src/main/java/clap/server/exception/ExceptionAdvice.java b/src/main/java/clap/server/exception/ExceptionAdvice.java index 354251e2..99b07a05 100644 --- a/src/main/java/clap/server/exception/ExceptionAdvice.java +++ b/src/main/java/clap/server/exception/ExceptionAdvice.java @@ -79,25 +79,23 @@ public ResponseEntity exception(Exception e, WebRequest request) { ); } - @ExceptionHandler(value = { ApplicationException.class, DomainException.class }) - public ResponseEntity onThrowException( - BaseException exception, - HttpServletRequest request) { - - BaseErrorCode baseErrorCode = exception.getCode(); - return handleExceptionInternal(exception, baseErrorCode, null, request); - } - - @ExceptionHandler(value = { ApplicationException.class }) - public ResponseEntity handleCsvApplicationException(ApplicationException e, WebRequest request) { + @ExceptionHandler(ApplicationException.class) + public ResponseEntity handleApplicationException(ApplicationException e, WebRequest request) { + // CSV 관련 에러만 처리 if (e.getCode() == MemberErrorCode.CSV_PARSING_ERROR || e.getCode() == MemberErrorCode.INVALID_CSV_FORMAT) { log.error("CSV Parsing Error: {}", e.getCode().getMessage()); return buildErrorResponse(e.getCode()); } - return buildErrorResponse(e.getCode()); } + @ExceptionHandler(value = { DomainException.class }) + public ResponseEntity onThrowException(BaseException exception, HttpServletRequest request) { + BaseErrorCode baseErrorCode = exception.getCode(); + log.error("BaseException occurred: {}", baseErrorCode.getMessage()); + return handleExceptionInternal(exception, baseErrorCode, null, request); + } + private ResponseEntity handleExceptionInternal( BaseException e, BaseErrorCode baseErrorCode, From 22de55ecbf9d0533900b080f54fec5ab09eb489d Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Wed, 29 Jan 2025 03:05:28 +0900 Subject: [PATCH 07/13] =?UTF-8?q?CLAP-116=20fix:CsvParser=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/admin/CsvParseService.java} | 7 ++++--- .../application/service/admin/RegisterMemberService.java | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) rename src/main/java/clap/server/{adapter/outbound/persistense/CsvParseAdapter.java => application/service/admin/CsvParseService.java} (93%) diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java b/src/main/java/clap/server/application/service/admin/CsvParseService.java similarity index 93% rename from src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java rename to src/main/java/clap/server/application/service/admin/CsvParseService.java index c0d822fd..3a038f5e 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/CsvParseAdapter.java +++ b/src/main/java/clap/server/application/service/admin/CsvParseService.java @@ -1,4 +1,4 @@ -package clap.server.adapter.outbound.persistense; +package clap.server.application.service.admin; import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; @@ -6,6 +6,7 @@ import clap.server.exception.code.MemberErrorCode; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; @@ -15,8 +16,8 @@ import java.util.List; @Slf4j -@Component -public class CsvParseAdapter { +@Service +public class CsvParseService { public List parse(MultipartFile file) { List memberRequests = new ArrayList<>(); 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 843ab306..0afc2201 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -1,7 +1,6 @@ package clap.server.application.service.admin; import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; -import clap.server.adapter.outbound.persistense.CsvParseAdapter; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.management.RegisterMemberUsecase; import clap.server.application.port.outbound.member.CommandMemberPort; @@ -17,7 +16,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; import java.util.List; import static clap.server.application.mapper.MemberInfoMapper.toMemberInfo; @@ -30,7 +28,7 @@ public class RegisterMemberService implements RegisterMemberUsecase { private final CommandMemberPort commandMemberPort; private final LoadDepartmentPort loadDepartmentPort; private final PasswordEncoder passwordEncoder; - private final CsvParseAdapter csvParser; // CsvParseAdapter 주입 + private final CsvParseService csvParser; // CsvParseService 주입 @Override @Transactional @@ -62,4 +60,4 @@ public int registerMembersFromCsv(Long adminId, MultipartFile file) { memberRequests.forEach(request -> registerMember(adminId, request)); return memberRequests.size(); } -} \ No newline at end of file +} From 5af313ca99d4cf610fc5f2235fa05c895f4a9345 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Wed, 29 Jan 2025 19:22:49 +0900 Subject: [PATCH 08/13] =?UTF-8?q?CLAP-116=20fix:=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=ED=83=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/inbound/web/admin/RegisterMemberCsvController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9de44e9b..301cc756 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 @@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; -@Tag(name = "Admin") +@Tag(name = "05. Admin") @WebAdapter @RequestMapping("/api/admin") public class RegisterMemberCsvController { From b0fbda55ecdef7e4df4bbaa2ab983a0dd20cbc2f Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Wed, 29 Jan 2025 19:32:13 +0900 Subject: [PATCH 09/13] =?UTF-8?q?CLAP-116=20fix:API=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/inbound/web/admin/RegisterMemberCsvController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 301cc756..fddada0c 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 @@ -17,7 +17,7 @@ @Tag(name = "05. Admin") @WebAdapter -@RequestMapping("/api/admin") +@RequestMapping("/api/managements") public class RegisterMemberCsvController { private final RegisterMemberUsecase registerMemberUsecase; From 687dcb0af43d14a9805a26c3e6af767826944d12 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Fri, 31 Jan 2025 15:46:06 +0900 Subject: [PATCH 10/13] Delete src/main/resources/env.properties --- src/main/resources/env.properties | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/main/resources/env.properties diff --git a/src/main/resources/env.properties b/src/main/resources/env.properties deleted file mode 100644 index c795a2b1..00000000 --- a/src/main/resources/env.properties +++ /dev/null @@ -1,6 +0,0 @@ -DATABASE_USERNAME=root -DATABASE_PASSWORD=root - -SWAGGER_SERVER_URL=http://localhost:9999 - -APPLICATION_PORT=9999 \ No newline at end of file From 79eb301938fdce8da8b902d532e9b2a35e2486a0 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Fri, 31 Jan 2025 16:47:09 +0900 Subject: [PATCH 11/13] =?UTF-8?q?CLAP-116=20fix:=20Csv=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20Member=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/admin/RegisterMemberController.java | 1 - .../admin/RegisterMemberCsvController.java | 2 -- .../service/admin/CsvParseService.java | 35 ++++++++++++++----- .../service/admin/RegisterMemberService.java | 14 +++++--- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java index 2d14a8b9..898e159a 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberController.java @@ -8,7 +8,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; 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 fddada0c..05bf96dc 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 @@ -3,10 +3,8 @@ import clap.server.adapter.inbound.security.SecurityUserDetails; import clap.server.application.port.inbound.management.RegisterMemberUsecase; import clap.server.common.annotation.architecture.WebAdapter; -import clap.server.exception.ApplicationException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; 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 3a038f5e..dedf2310 100644 --- a/src/main/java/clap/server/application/service/admin/CsvParseService.java +++ b/src/main/java/clap/server/application/service/admin/CsvParseService.java @@ -1,11 +1,15 @@ package clap.server.application.service.admin; -import clap.server.adapter.inbound.web.dto.admin.RegisterMemberRequest; +import clap.server.application.port.outbound.member.LoadDepartmentPort; +import clap.server.domain.model.member.Department; +import clap.server.domain.model.member.Member; +import clap.server.domain.model.member.MemberInfo; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.exception.ApplicationException; +import clap.server.exception.code.DepartmentErrorCode; import clap.server.exception.code.MemberErrorCode; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -15,12 +19,18 @@ import java.util.ArrayList; import java.util.List; +import static clap.server.application.mapper.MemberInfoMapper.toMemberInfo; +import static clap.server.application.mapper.MemberMapper.toMember; + @Slf4j @Service +@RequiredArgsConstructor public class CsvParseService { - public List parse(MultipartFile file) { - List memberRequests = new ArrayList<>(); + private final LoadDepartmentPort loadDepartmentPort; + + public List parse(MultipartFile file) { + List members = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { @@ -28,23 +38,30 @@ public List parse(MultipartFile file) { if (fields.length != 7) { throw ApplicationException.from(MemberErrorCode.INVALID_CSV_FORMAT); } - memberRequests.add(mapToRegisterMemberRequest(fields)); + members.add(mapToMember(fields)); } } catch (IOException e) { throw ApplicationException.from(MemberErrorCode.CSV_PARSING_ERROR); } - return memberRequests; + return members; } - private RegisterMemberRequest mapToRegisterMemberRequest(String[] fields) { - return new RegisterMemberRequest( + private Member mapToMember(String[] fields) { + // 부서 ID로 Department 객체 조회 + Long departmentId = Long.parseLong(fields[2].trim()); + Department department = loadDepartmentPort.findById(departmentId) + .orElseThrow(() -> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); + + MemberInfo memberInfo = toMemberInfo( fields[0].trim(), // name fields[4].trim(), // email fields[1].trim(), // nickname Boolean.parseBoolean(fields[6].trim()), // isReviewer - Long.parseLong(fields[2].trim()), // departmentId + department, // department MemberRole.valueOf(fields[5].trim()), // role fields[3].trim() // departmentRole ); + + return toMember(memberInfo); } } 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 0afc2201..b386a523 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -28,7 +28,7 @@ public class RegisterMemberService implements RegisterMemberUsecase { private final CommandMemberPort commandMemberPort; private final LoadDepartmentPort loadDepartmentPort; private final PasswordEncoder passwordEncoder; - private final CsvParseService csvParser; // CsvParseService 주입 + private final CsvParseService csvParser; @Override @Transactional @@ -56,8 +56,12 @@ public void registerMember(Long adminId, RegisterMemberRequest request) { @Override @Transactional public int registerMembersFromCsv(Long adminId, MultipartFile file) { - List memberRequests = csvParser.parse(file); - memberRequests.forEach(request -> registerMember(adminId, request)); - return memberRequests.size(); + 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 From ae5883e2965e8b7e7c9db44827886b19dcd7b38d Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Sun, 2 Feb 2025 13:56:24 +0900 Subject: [PATCH 12/13] =?UTF-8?q?CLAP-116=20fix:=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20import=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/inbound/web/admin/RegisterMemberCsvController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 05bf96dc..0a9b229b 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.SecurityUserDetails; -import clap.server.application.port.inbound.management.RegisterMemberUsecase; +import clap.server.application.port.inbound.admin.RegisterMemberUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; From 0d941ccd4ba59f01c28dd8e226568265cd4e64d4 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Sun, 2 Feb 2025 14:03:56 +0900 Subject: [PATCH 13/13] =?UTF-8?q?CLAP-116=20fix:=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B6=88=EC=9D=BC=EC=B9=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/clap/server/domain/model/member/MemberInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/clap/server/domain/model/member/MemberInfo.java b/src/main/java/clap/server/domain/model/member/MemberInfo.java index 9f4d3bd4..e6a9aece 100644 --- a/src/main/java/clap/server/domain/model/member/MemberInfo.java +++ b/src/main/java/clap/server/domain/model/member/MemberInfo.java @@ -65,7 +65,7 @@ public void updateName(String name) { public static void assertReviewerIsManager(boolean isReviewer, MemberRole role) { if (isReviewer) { if (role != MemberRole.ROLE_MANAGER) { - throw new DomainException(MemberErrorCode.MEMBER_REGISTER_FAILED); + throw new DomainException(MemberErrorCode.MEMBER_REGISTRATION_FAILED); } } }