Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,16 +17,17 @@

@Tag(name = "회원 관리 - 등록")
@WebAdapter
@RequiredArgsConstructor
@RequestMapping("/api/managements")
@RequiredArgsConstructor
public class RegisterMemberController {

private final 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 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;
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;

@Tag(name = "05. Admin")
@WebAdapter
@RequestMapping("/api/managements")
public class RegisterMemberCsvController {
private final RegisterMemberUsecase registerMemberUsecase;

public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase) {
this.registerMemberUsecase = registerMemberUsecase;
}

@Operation(summary = "CSV 파일로 회원 등록 API")
@PostMapping("/members/upload")
@Secured("ROLE_ADMIN")
public ResponseEntity<String> registerMembersFromCsv(
@AuthenticationPrincipal SecurityUserDetails userInfo,
@RequestParam("file") MultipartFile file) {
int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file);
return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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;
import clap.server.exception.ApplicationException;
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;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class CsvParseService {

Comment thread
joowojr marked this conversation as resolved.
public List<RegisterMemberRequest> parse(MultipartFile file) {
List<RegisterMemberRequest> memberRequests = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
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;
}

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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,50 @@
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;

import static clap.server.application.mapper.MemberInfoMapper.toMemberInfo;
import static clap.server.application.mapper.MemberMapper.toMember;

@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 CsvParseService csvParser; // CsvParseService 주입

@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));
MemberInfo memberInfo = toMemberInfo(request.name(), request.email(), request.nickname(), request.isReviewer(),
department, request.role(), request.departmentRole());
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) {
List<RegisterMemberRequest> memberRequests = csvParser.parse(file);
memberRequests.forEach(request -> registerMember(adminId, request));
return memberRequests.size();
}
}
31 changes: 24 additions & 7 deletions src/main/java/clap/server/exception/ExceptionAdvice.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,9 +58,9 @@ public ResponseEntity<Object> handleMethodArgumentNotValid(
@ExceptionHandler
public ResponseEntity<Object> 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);
}
Expand All @@ -78,12 +79,20 @@ public ResponseEntity<Object> exception(Exception e, WebRequest request) {
);
}

@ExceptionHandler(value = { ApplicationException.class, DomainException.class })
public ResponseEntity<Object> onThrowException(
BaseException exception,
HttpServletRequest request) {
@ExceptionHandler(ApplicationException.class)
Comment thread
Sihun23 marked this conversation as resolved.
public ResponseEntity<Object> 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<Object> onThrowException(BaseException exception, HttpServletRequest request) {
BaseErrorCode baseErrorCode = exception.getCode();
log.error("BaseException occurred: {}", baseErrorCode.getMessage());
return handleExceptionInternal(exception, baseErrorCode, null, request);
}

Expand Down Expand Up @@ -170,4 +179,12 @@ public ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException
AuthErrorCode.FORBIDDEN.getMessage()
);
}

private ResponseEntity<Object> buildErrorResponse(BaseErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(Map.of(
"code", errorCode.getCustomCode(),
"message", errorCode.getMessage()
));
}
}
3 changes: 3 additions & 0 deletions src/main/java/clap/server/exception/code/MemberErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/env.properties
Comment thread
Sihun23 marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DATABASE_USERNAME=root
DATABASE_PASSWORD=root

SWAGGER_SERVER_URL=http://localhost:9999

APPLICATION_PORT=9999
Comment thread
Sihun23 marked this conversation as resolved.
Outdated