Skip to content

Commit

Permalink
refactor: 회원 컴포넌트 리팩토링
Browse files Browse the repository at this point in the history
- 회원 전체 코드 리팩토링
- 회원 요청 DTO 내부 클래스화
- 특정 컨트롤러 메소드 권한 설정

Related to: #95
  • Loading branch information
juwon-code committed Oct 7, 2024
1 parent 5016341 commit 4b30314
Show file tree
Hide file tree
Showing 17 changed files with 376 additions and 340 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
package com.prgrms2.java.bitta.member.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.prgrms2.java.bitta.member.dto.MemberDTO;
import com.prgrms2.java.bitta.member.dto.SignInDTO;
import com.prgrms2.java.bitta.member.dto.SignUpDTO;
import com.prgrms2.java.bitta.member.exception.NoChangeException;
import com.prgrms2.java.bitta.global.exception.AuthenticationException;
import com.prgrms2.java.bitta.member.dto.MemberRequestDto;
import com.prgrms2.java.bitta.member.entity.Role;
import com.prgrms2.java.bitta.member.service.MemberProvider;
import com.prgrms2.java.bitta.member.service.MemberService;
import com.prgrms2.java.bitta.security.JwtToken;
import com.prgrms2.java.bitta.security.SecurityUtil;
import com.prgrms2.java.bitta.security.dto.RefreshTokenRequestDTO;
import com.prgrms2.java.bitta.token.dto.TokenResponseDto;
import com.prgrms2.java.bitta.global.util.AuthenticationProvider;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.util.Map;

import static com.prgrms2.java.bitta.global.constants.ApiResponses.*;
Expand All @@ -32,19 +29,19 @@
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1/members")
@RequestMapping("api/v1/member")
public class MemberController {

private final MemberService memberService;
private final ObjectMapper objectMapper;

private final MemberProvider memberProvider;

@Operation(
summary = "테스트",
description = "SecurityUtil 로부터 회원아이디를 얻는 테스트용 API입니다."
)
@PostMapping("/test")
public String test() {
return SecurityUtil.getCurrentUsername();
return AuthenticationProvider.getUsername();
}

@Operation(
Expand All @@ -61,11 +58,12 @@ public String test() {
content = @Content)
}
)
@PostMapping("/sign-in")
public ResponseEntity<JwtToken> signIn(@RequestBody SignInDTO signInDTO) {
JwtToken jwtToken = memberService.signIn(signInDTO.getUsername(), signInDTO.getPassword());
log.info("로그인 성공: username={}, accessToken={}", signInDTO.getUsername(), jwtToken.getAccessToken());
return ResponseEntity.ok(jwtToken);
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody MemberRequestDto.Login loginDto) {
TokenResponseDto tokenResponseDto = memberService.validate(loginDto);

return ResponseEntity.ok(Map.of("message", "로그인에 성공했습니다."
, "result", tokenResponseDto));
}

@Operation(
Expand All @@ -84,9 +82,11 @@ public ResponseEntity<JwtToken> signIn(@RequestBody SignInDTO signInDTO) {
content = @Content)
}
)
@PostMapping("/sign-up")
public ResponseEntity<MemberDTO> signUp(@RequestBody SignUpDTO signUpDTO) {
return ResponseEntity.ok(memberService.signUp(signUpDTO));
@PostMapping
public ResponseEntity<?> register(@RequestBody MemberRequestDto.Register registerDto) {
memberService.insert(registerDto);

return ResponseEntity.ok(Map.of("message", "회원가입에 성공했습니다."));
}

@Operation(
Expand All @@ -104,8 +104,27 @@ public ResponseEntity<MemberDTO> signUp(@RequestBody SignUpDTO signUpDTO) {
}
)
@GetMapping("/{id}")
public ResponseEntity<MemberDTO> getMemberById(@PathVariable Long id) {
return ResponseEntity.ok(memberService.getMemberById(id));
public ResponseEntity<?> read(@PathVariable("id") @Min(1) Long id) {
if (!checkPermission(id)) {
throw AuthenticationException.CANNOT_ACCESS.get();
}

return ResponseEntity.ok(Map.of("message", "회원을 성공적으로 조회했습니다."
, "result", memberService.getDtoById(id)));
}

@PutMapping("/{id}")
public ResponseEntity<?> changePassword(@PathVariable("id") @Min(1) Long id
, @RequestBody @Valid MemberRequestDto.ChangePassword changePasswordDto) {
if (!checkPermission(id)) {
throw AuthenticationException.CANNOT_ACCESS.get();
}

changePasswordDto.setId(id);

memberService.changePassword(changePasswordDto);

return ResponseEntity.ok().body(Map.of("message", "비밀번호가 수정되었습니다."));
}

@Operation(
Expand All @@ -126,22 +145,22 @@ public ResponseEntity<MemberDTO> getMemberById(@PathVariable Long id) {
content = @Content)
}
)
@PutMapping(value = "/{id}", consumes = "multipart/form-data")
public ResponseEntity<MemberDTO> updateMemberById(@PathVariable Long id,
@RequestParam("dto") String dtoJson,
@RequestParam(value = "profileImage", required = false) MultipartFile profileImage,
@RequestParam(value = "removeProfileImage", required = false, defaultValue = "false") Boolean removeProfileImage) {
try {
MemberDTO memberDTO = objectMapper.readValue(dtoJson, MemberDTO.class);
MemberDTO updatedMember = memberService.updateMember(id, memberDTO, profileImage, removeProfileImage);
return ResponseEntity.ok(updatedMember);
} catch (NoChangeException e) {
log.warn("변경된 내용이 없습니다 - 사용자 ID: {}", id);
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
} catch (IOException e) {
log.error("프로필 이미지 처리 오류 - 사용자 ID: {}, 에러: {}", id, e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
@PutMapping(value = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<?> modify(@PathVariable("id") @Min(1) Long id, @RequestPart(value = "member") @Valid MemberRequestDto.Modify modifyDto
, @RequestPart(value = "file", required = false) MultipartFile file) {
if (!checkPermission(id)) {
throw AuthenticationException.CANNOT_ACCESS.get();
}

modifyDto.setId(id);

if (file.isEmpty()) {
memberService.update(modifyDto, file);
} else {
memberService.update(modifyDto);
}

return ResponseEntity.ok().body(Map.of("message", "회원이 수정되었습니다."));
}

@Operation(
Expand All @@ -159,52 +178,21 @@ public ResponseEntity<MemberDTO> updateMemberById(@PathVariable Long id,
}
)
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteMemberById(@PathVariable Long id) {
memberService.deleteMember(id);
public ResponseEntity<String> delete(@PathVariable Long id) {
if (!checkPermission(id)) {
throw AuthenticationException.CANNOT_ACCESS.get();
}

memberService.delete(id);
log.info("회원 삭제 완료 - 사용자 ID: {}", id);
return ResponseEntity.ok("회원 탈퇴가 완료되었습니다.");
}


private String extractToken(String bearerToken) {
return bearerToken.substring(7);
}

@Operation(
summary = "토큰 재발급",
description = "Refresh 토큰으로 Access 토큰을 재발급 합니다.",
responses = {
@ApiResponse(
responseCode = "200",
description = "토큰을 성공적으로 재발급했습니다.",
content = @Content(mediaType = "application/json")),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청",
content = @Content),
@ApiResponse(
responseCode = "401",
description = "유효하지 않은 리프레시 토큰",
content = @Content)
}
)
@PostMapping("/refresh")
public ResponseEntity<?> reissueToken(@RequestHeader("Authorization") String bearerAccessToken,
@RequestBody RefreshTokenRequestDTO request) {
try {
String accessToken = extractToken(bearerAccessToken);
JwtToken newToken = memberService.reissueToken(accessToken, request.getRefreshToken());

if (newToken == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message", "액세스 토큰이 유효합니다."));
}

log.info("토큰 재발급 성공 - 새로운 액세스 토큰: {}", newToken.getAccessToken());
return ResponseEntity.ok(newToken);
} catch (AuthenticationException e) {
log.error("토큰 재발급 실패 - 이유: {}", e.getMessage());
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, e.getMessage());
private boolean checkPermission(Long id) {
if (AuthenticationProvider.getRoles() == Role.ADMIN) {
return true;
}

return memberService.checkAuthority(id, AuthenticationProvider.getUsername());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.prgrms2.java.bitta.member.controller.advice;

import com.prgrms2.java.bitta.member.exception.MemberTaskException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;


@Slf4j
@RestControllerAdvice
public class MemberControllerAdvice {
@ExceptionHandler(AuthorizationDeniedException.class)
public ResponseEntity<?> handleArgsException(AuthorizationDeniedException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "해당 리소스에 대한 권한이 없습니다."));
}

@ExceptionHandler(MemberTaskException.class)
public ResponseEntity<?> handleArgsException(MemberTaskException e) {
return ResponseEntity.status(e.getCode())
.body(Map.of("error", e.getMessage()));
}
}

19 changes: 3 additions & 16 deletions src/main/java/com/prgrms2/java/bitta/member/dto/MemberDTO.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package com.prgrms2.java.bitta.member.dto;

import com.prgrms2.java.bitta.member.entity.Member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.*;

@Getter
@Setter
@ToString
@AllArgsConstructor
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(title = "회원 DTO", description = "회원 요청 및 응답에 사용하는 DTO입니다.")
public class MemberDTO {

@Schema(title = "회원 ID (PK)", description = "회원의 고유 ID 입니다.", example = "1", minimum = "1")
@Min(value = 1, message = "회원 ID는 0 또는 음수가 될 수 없습니다.")
private Long id;
Expand All @@ -37,14 +33,5 @@ public class MemberDTO {
private String address;

@Schema(title = "프로필 이미지 URL", description = "프로필 이미지의 URL 입니다.", example = "IMAGE_URL")
private String profile;

static public MemberDTO toDTO(Member member) {
return MemberDTO.builder()
.id(member.getId())
.username(member.getUsername())
.nickname(member.getNickname())
.address(member.getAddress())
.profile(member.getProfile()).build();
}
private String profileUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.prgrms2.java.bitta.member.dto;

import com.prgrms2.java.bitta.member.entity.Role;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

@Schema(title = "회원 요청 DTO", description = "회원 관련 요청에 사용하는 DTO입니다.")
public class MemberRequestDto {
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(title = "로그인 DTO", description = "로그인 요청에 사용하는 DTO입니다.")
public static class Login {
@Schema(title = "아이디", description = "로그인에 사용할 아이디입니다.", example = "username")
private String username;

@Schema(title = "비밀번호", description = "로그인에 사용할 비밀번호입니다.", example = "password")
private String password;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(title = "회원가입 DTO", description = "회원가입 요청에 사용하는 DTO입니다.")
public static class Register {
@Schema(title = "아이디", description = "회원가입에 사용할 아이디입니다.", example = "username")
private String username;

@Schema(title = "비밀번호", description = "회원가입에 사용할 비밀번호입니다.", example = "password")
private String password;

@Schema(title = "별명", description = "회원가입에 사용할 별명입니다.", example = "nickname")
private String nickname;

@Schema(title = "주소", description = "회원의 주소입니다.", example = "경기도 고양시 일산동구 중앙로 1256")
private String address;

@Builder.Default
@Schema(title = "회원 권한", description = "회원이 갖는 액세스 권한입니다.", example = "USER")
private Role role = Role.USER;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(title = "비밀번호 변경 DTO", description = "비밀번호 변경 요청에 사용하는 DTO입니다.")
public static class ChangePassword {
@Schema(title = "회원 ID (PK)", description = "변경할 회원의 기본키입니다.", example = "1")
private Long id;

@Schema(title = "이전 비밀번호", description = "이전에 사용하던 비밀번호입니다.", example = "password1")
private String beforePassword;

@Schema(title = "새로운 비밀번호", description = "새롭게 변경할 비밀번호입니다.", example = "password1")
private String afterPassword;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(title = "회원정보 변경 DTO", description = "회원정보 변경 요청에 사용하는 DTO입니다.")
public static class Modify {
@Schema(title = "회원 ID (PK)", description = "변경할 회원의 기본키입니다.", example = "1")
private Long id;

@Schema(title = "아이디", description = "비밀번호를 변경할 아이디입니다.", example = "username")
private String username;

@Schema(title = "새로운 별명", description = "새롭게 변경할 별명입니다.", example = "nickname")
private String nickname;

@Schema(title = "새로운 주소", description = "새롭게 변경할 회원의 주소입니다.", example = "경기도 고양시 일산동구 중앙로 1256")
private String address;
}
}
21 changes: 0 additions & 21 deletions src/main/java/com/prgrms2/java/bitta/member/dto/SignInDTO.java

This file was deleted.

Loading

0 comments on commit 4b30314

Please sign in to comment.