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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
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 @@ -16,16 +16,17 @@

@Tag(name = "05. Admin")
@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,35 @@
package clap.server.adapter.inbound.web.admin;

import clap.server.adapter.inbound.security.SecurityUserDetails;
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;
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 + "명의 회원이 등록되었습니다.");
}
}
15 changes: 15 additions & 0 deletions src/main/java/clap/server/application/mapper/MemberMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import clap.server.adapter.inbound.web.dto.member.MemberDetailInfoResponse;
import clap.server.adapter.inbound.web.dto.member.MemberProfileResponse;
import clap.server.domain.model.member.Member;
import clap.server.domain.model.member.MemberInfo;

public class MemberMapper {
private MemberMapper() {
Expand Down Expand Up @@ -40,5 +41,19 @@ public static MemberDetailInfoResponse.NotificationSettingInfoResponse toNotific
member.getEmailNotificationEnabled(),
member.getKakaoworkNotificationEnabled()
);

}
public static Member toMember(MemberInfo memberInfo) {
return Member.builder()
.memberInfo(memberInfo)
.agitNotificationEnabled(null)
.emailNotificationEnabled(null)
.kakaoworkNotificationEnabled(null)
.admin(null)
.imageUrl(null)
.status(null)
.password(null)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package clap.server.application.port.inbound.admin;

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,68 @@
package clap.server.application.service.admin;

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.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;

import static clap.server.application.mapper.MemberMapper.toMember;
import static clap.server.domain.model.member.MemberInfo.toMemberInfo;


@Slf4j
@Service
@RequiredArgsConstructor
public class CsvParseService {

private final LoadDepartmentPort loadDepartmentPort;

public List<Member> parse(MultipartFile file) {
List<Member> members = 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);
}
members.add(mapToMember(fields));
}
} catch (IOException e) {
throw ApplicationException.from(MemberErrorCode.CSV_PARSING_ERROR);
}
return members;
}

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
department, // department
MemberRole.valueOf(fields[5].trim()), // role
fields[3].trim() // departmentRole
);

return toMember(memberInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
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;

@ApplicationService
@RequiredArgsConstructor
Expand All @@ -22,18 +25,31 @@ class RegisterMemberService implements RegisterMemberUsecase {
private final CommandMemberPort commandMemberPort;
private final LoadDepartmentPort loadDepartmentPort;
private final PasswordEncoder passwordEncoder;
private final CsvParseService csvParser;

@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));

//TODO: 인프라팀만 담당자가 될 수 있도록 수정해야함
// TODO: 인프라팀만 담당자가 될 수 있도록 수정해야함
MemberInfo memberInfo = MemberInfo.toMemberInfo(request.name(), request.email(), request.nickname(), request.isReviewer(),
department, request.role(), request.departmentRole());
Member member = Member.createMember(admin, memberInfo);
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();
}
}
4 changes: 4 additions & 0 deletions src/main/java/clap/server/domain/model/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,8 @@ public void verifyPassword(String encodedPassword) {
throw new DomainException(MemberErrorCode.PASSWORD_VERIFY_FAILED);
}
}

public void register(Member admin) {
this.admin = admin; // 관리자 설정
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
42 changes: 32 additions & 10 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.GlobalErrorCode;
import clap.server.exception.code.MemberErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
Expand Down Expand Up @@ -48,7 +49,7 @@ public ResponseEntity<Object> handleMethodArgumentNotValid(
return handleExceptionInternalArgs(
e,
HttpHeaders.EMPTY,
GlobalErrorCode.BAD_REQUEST,
GlobalErrorCode.BAD_REQUEST, // GlobalErrorCode 사용
request,
errors
);
Expand All @@ -57,11 +58,16 @@ 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, GlobalErrorCode.valueOf(errorMessage), HttpHeaders.EMPTY, request);
return handleExceptionInternalConstraint(
e,
GlobalErrorCode.valueOf(errorMessage), // GlobalErrorCode 사용
HttpHeaders.EMPTY,
request
);
}

@ExceptionHandler
Expand All @@ -70,20 +76,28 @@ public ResponseEntity<Object> exception(Exception e, WebRequest request) {

return handleExceptionInternalFalse(
e,
GlobalErrorCode.INTERNAL_SERVER_ERROR,
GlobalErrorCode.INTERNAL_SERVER_ERROR, // GlobalErrorCode 사용
HttpHeaders.EMPTY,
GlobalErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus(),
request,
e.getMessage()
);
}

@ExceptionHandler(value = { BaseException.class })
public ResponseEntity<Object> onThrowException(
BaseException exception,
HttpServletRequest request) {
@ExceptionHandler(ApplicationException.class)
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 = { BaseException.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 +184,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()
));
}
}
6 changes: 4 additions & 2 deletions src/main/java/clap/server/exception/code/MemberErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ public enum MemberErrorCode implements BaseErrorCode {
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_001", "회원을 찾을 수 없습니다."),
ACTIVE_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_002", "활성화 회원을 찾을 수 없습니다."),
NOT_A_REVIEWER(HttpStatus.FORBIDDEN, "MEMBER_003", "리뷰어 권한이 없습니다."),
MEMBER_REGISTER_FAILED(HttpStatus.BAD_REQUEST, "MEMBER_004", "회원 등록에 실패하였습니다"),
NOT_A_COMMENTER(HttpStatus.FORBIDDEN, "MEMBER_005", "댓글 권한이 없습니다."),
PASSWORD_VERIFY_FAILED(HttpStatus.BAD_REQUEST, "MEMBER_006", "비밀번호 검증에 실패하였습니다")
PASSWORD_VERIFY_FAILED(HttpStatus.BAD_REQUEST, "MEMBER_006", "비밀번호 검증에 실패하였습니다"),
INVALID_CSV_FORMAT(HttpStatus.BAD_REQUEST, "MEMBER_007", "CSV 파일 형식이 잘못되었습니다."),
CSV_PARSING_ERROR(HttpStatus.BAD_REQUEST, "MEMBER_008", "CSV 데이터 파싱 중 오류가 발생했습니다."),
MEMBER_REGISTRATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER_009", "회원 등록 중 오류가 발생했습니다.")
;

private final HttpStatus httpStatus;
Expand Down