Skip to content

Commit

Permalink
[feat #39] 사용자 프로필 조회/수정 API (#40)
Browse files Browse the repository at this point in the history
* [rename] 인가 관련 API 컨트롤러 위치 이동(MemberController -> AuthController)

* [test] : 인가 관련 API 컨트롤러 위치 이동으로 인한 테스트 코드 위치 변경

* [feat] : 임시 로그인 API 이메일 Valid 추가

* [test] : 사용자 프로필 조회 TDD

* [rename] : 패키지 위치 변경

* [fix] : ResponseDTO 필드명 오탈자 수정으로 인한 테스트 코드 수정

* [feat] : 회원 프로필 조회 응답 DTO 추가

* [feat] : MemberMapper(DTO <-> Entity) 추가

* [feat] : 회원 프로필 조회 API 추가

* [rename] : 정적 메서드 네이밍 변경(of -> from)

* [feat] : MemberErrorCode 프로필 수정 에러코드 추가

* [feat] : 공무원 이메일이 일치하는 회원 반환 쿼리 메서드 추가

* [feat] : 사용자 프로필 수정 API 로직 추가

* [test] : 사용자 프로필 수정 단위 테스트 추가

* [test] : 사용자 프로필 조회/수정 컨트롤러 통합 테스트 추가

* [feat] : 프로필 조회 시 트랜잭션 읽기 전용 설정 추가

* [rename] : 프로필 수정 API 파라티터 변수 네이밍 변경

* [test] : 프로필 조회/수정 API 테스트 성공 확인 데이터 정확도를 위한 수정

* [rename] : 리뷰에 따른 네이밍 수정
  • Loading branch information
dudxo authored Aug 13, 2024
1 parent e424872 commit 37fbfde
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 139 deletions.
54 changes: 51 additions & 3 deletions src/main/java/com/dnd/gongmuin/auth/cotroller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,30 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.auth.dto.LoginRequest;
import com.dnd.gongmuin.auth.dto.TempLoginRequest;
import com.dnd.gongmuin.auth.service.AuthService;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.dto.request.AdditionalInfoRequest;
import com.dnd.gongmuin.member.dto.request.LogoutRequest;
import com.dnd.gongmuin.member.dto.request.ReissueRequest;
import com.dnd.gongmuin.member.dto.request.ValidateNickNameRequest;
import com.dnd.gongmuin.member.dto.response.LogoutResponse;
import com.dnd.gongmuin.member.dto.response.ReissueResponse;
import com.dnd.gongmuin.member.dto.response.SignUpResponse;
import com.dnd.gongmuin.member.dto.response.ValidateNickNameResponse;
import com.dnd.gongmuin.member.service.MemberService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "Social Login API", description = "소셜 로그인 요청 API")
Expand All @@ -26,6 +38,7 @@
public class AuthController {

private final AuthService authService;
private final MemberService memberService;

@Operation(summary = "카카오 로그인 API", description = "카카오 로그인 페이지로 이동 요청한다.(사용불가!!)")
@ApiResponse(useReturnTypeSchema = true)
Expand All @@ -40,10 +53,45 @@ public ResponseEntity<?> kakaoLoginRedirect() {
@Operation(summary = "임시 로그인/회원가입(토큰 발급) API", description = "로그인 또는 회원가입 후 토큰을 발급한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/token")
public ResponseEntity<String> swaggerToken(@RequestBody LoginRequest loginRequest) {
String accessToken = authService.swaggerToken(loginRequest);
public ResponseEntity<String> getTempToken(@RequestBody @Valid TempLoginRequest request) {
String accessToken = authService.swaggerToken(request);
return ResponseEntity.ok(accessToken);

}

@Operation(summary = "닉네임 중복 검증 API", description = "닉네임 중복을 검증한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/check-nickname")
public ResponseEntity<ValidateNickNameResponse> checkNickName(
@RequestBody @Valid ValidateNickNameRequest request) {
return ResponseEntity.ok(memberService.isDuplicatedNickname(request));
}

@Operation(summary = "추가정보 API", description = "추가 정보를 저장한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/member")
public ResponseEntity<SignUpResponse> signUp(
@RequestBody @Valid AdditionalInfoRequest request,
@AuthenticationPrincipal Member loginMember) {
SignUpResponse response = memberService.signUp(request, loginMember.getSocialEmail());

return ResponseEntity.ok(response);
}

@Operation(summary = "로그아웃 API", description = "로그아웃한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/logout")
public ResponseEntity<LogoutResponse> logout(@RequestBody @Valid LogoutRequest request) {
LogoutResponse response = memberService.logout(request);
return ResponseEntity.ok(response);
}

@Operation(summary = "토큰 재발급 API", description = "토큰을 재발급한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/reissue/token")
public ResponseEntity<ReissueResponse> reissue(@RequestBody @Valid ReissueRequest request) {
ReissueResponse response = memberService.reissue(request);
return ResponseEntity.ok(response);
}
}

9 changes: 0 additions & 9 deletions src/main/java/com/dnd/gongmuin/auth/dto/LoginRequest.java

This file was deleted.

13 changes: 13 additions & 0 deletions src/main/java/com/dnd/gongmuin/auth/dto/TempLoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dnd.gongmuin.auth.dto;

import jakarta.validation.constraints.Email;

public record TempLoginRequest(

String socialName,

@Email
String socialEmail

) {
}
6 changes: 3 additions & 3 deletions src/main/java/com/dnd/gongmuin/auth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import com.dnd.gongmuin.auth.domain.Auth;
import com.dnd.gongmuin.auth.domain.AuthStatus;
import com.dnd.gongmuin.auth.domain.Provider;
import com.dnd.gongmuin.auth.dto.LoginRequest;
import com.dnd.gongmuin.auth.dto.TempLoginRequest;
import com.dnd.gongmuin.auth.exception.AuthErrorCode;
import com.dnd.gongmuin.auth.repository.AuthRepository;
import com.dnd.gongmuin.common.exception.runtime.NotFoundException;
Expand Down Expand Up @@ -59,9 +59,9 @@ private Auth createAuth(Member savedMember) {
}

@Transactional
public String swaggerToken(LoginRequest loginRequest) {
public String swaggerToken(TempLoginRequest tempLoginRequest) {
Date now = new Date();
Member member = Member.of(loginRequest.socialName(), "kakao/" + loginRequest.socialEmail(), 10000);
Member member = Member.of(tempLoginRequest.socialName(), "kakao/" + tempLoginRequest.socialEmail(), 10000);

if (memberRepository.existsBySocialEmail(member.getSocialEmail())) {
throw new NotFoundException(MemberErrorCode.NOT_FOUND_NEW_MEMBER);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,46 @@

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.dto.request.AdditionalInfoRequest;
import com.dnd.gongmuin.member.dto.request.LogoutRequest;
import com.dnd.gongmuin.member.dto.request.ReissueRequest;
import com.dnd.gongmuin.member.dto.request.ValidateNickNameRequest;
import com.dnd.gongmuin.member.dto.response.LogoutResponse;
import com.dnd.gongmuin.member.dto.response.ReissueResponse;
import com.dnd.gongmuin.member.dto.response.SignUpResponse;
import com.dnd.gongmuin.member.dto.response.ValidateNickNameResponse;
import com.dnd.gongmuin.member.dto.request.UpdateMemberProfileRequest;
import com.dnd.gongmuin.member.dto.response.MemberProfileResponse;
import com.dnd.gongmuin.member.service.MemberService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "Member API", description = "회원 관련 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
@RequestMapping("/api/members")
public class MemberController {

private final MemberService memberService;

@Operation(summary = "닉네임 중복 검증 API", description = "닉네임 중복을 검증한다.")
@Operation(summary = "프로필 조회 API", description = "로그인 된 사용자 프로필 정보를 조회한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/check-nickname")
public ResponseEntity<ValidateNickNameResponse> checkNickName(
@RequestBody @Valid ValidateNickNameRequest validateNickNameRequest) {
return ResponseEntity.ok(memberService.isDuplicatedNickname(validateNickNameRequest));
}

@Operation(summary = "추가정보 API", description = "추가 정보를 저장한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/member")
public ResponseEntity<SignUpResponse> signUp(
@RequestBody @Valid AdditionalInfoRequest request,
@AuthenticationPrincipal Member loginMember) {
SignUpResponse response = memberService.signUp(request, loginMember.getSocialEmail());

@GetMapping("/profile")
public ResponseEntity<MemberProfileResponse> getMemberProfile(@AuthenticationPrincipal Member member) {
MemberProfileResponse response = memberService.getMemberProfile(member);
return ResponseEntity.ok(response);
}

@Operation(summary = "로그아웃 API", description = "로그아웃한다.")
@Operation(summary = "프로필 수정 API", description = "로그인 된 사용자 프로필 정보를 수정한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/logout")
public ResponseEntity<LogoutResponse> logout(@RequestBody @Valid LogoutRequest request) {
LogoutResponse response = memberService.logout(request);
@PatchMapping("/profile/edit")
public ResponseEntity<MemberProfileResponse> updateMemberProfile(
@RequestBody UpdateMemberProfileRequest request,
@AuthenticationPrincipal Member member) {
MemberProfileResponse response = memberService.updateMemberProfile(request, member);
return ResponseEntity.ok(response);
}

@Operation(summary = "토큰 재발급 API", description = "토큰을 재발급한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/reissue/token")
public ResponseEntity<ReissueResponse> reissue(@RequestBody @Valid ReissueRequest request) {
ReissueResponse response = memberService.reissue(request);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public enum JobCategory {

private final String label;

public static JobCategory of(String input) {
public static JobCategory from(String input) {
return Arrays.stream(values())
.filter(category -> category.isEqual(input))
.findAny()
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/dnd/gongmuin/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,10 @@ public void increaseCredit(int credit) {
this.credit += credit;
}

public void updateProfile(String nickname, JobGroup jobGroup, JobCategory jobCategory) {
this.nickname = nickname;
this.jobGroup = jobGroup;
this.jobCategory = jobCategory;
}

}
20 changes: 20 additions & 0 deletions src/main/java/com/dnd/gongmuin/member/dto/MemberMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dnd.gongmuin.member.dto;

import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.dto.response.MemberProfileResponse;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MemberMapper {

public static MemberProfileResponse toMemberProfileResponse(Member member) {
return new MemberProfileResponse(
member.getNickname(),
member.getJobGroup().getLabel(),
member.getJobCategory().getLabel(),
member.getCredit()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.dnd.gongmuin.member.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record UpdateMemberProfileRequest(

@NotBlank(message = "닉네임은 필수 입력 항목입니다.")
@Size(min = 2, max = 12, message = "닉네임은 최소 2자리 이상 최대 12자 이하입니다.")
String nickname,

@NotBlank(message = "직군은 필수 입력 항목입니다.")
String jobGroup,

@NotBlank(message = "직렬은 필수 입력 항목입니다.")
String jobCategory
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.gongmuin.member.dto.response;

public record MemberProfileResponse(
String nickname,
String jobGroup,
String jobCategory,
int credit
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public enum MemberErrorCode implements ErrorCode {
NOT_FOUND_MEMBER("특정 회원을 찾을 수 없습니다.", "MEMBER_001"),
NOT_FOUND_NEW_MEMBER("신규 회원이 아닙니다.", "MEMBER_002"),
LOGOUT_FAILED("로그아웃을 실패했습니다.", "MEMBER_003"),
NOT_ENOUGH_CREDIT("보유한 크레딧이 부족합니다.", "MEMBER_004");
NOT_ENOUGH_CREDIT("보유한 크레딧이 부족합니다.", "MEMBER_004"),
UPDATE_PROFILE_FAILED("프로필 수정에 실패했습니다.", "MEMBER_005");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByOfficialEmail(String officialEmail);

boolean existsBySocialEmail(String socialEmail);

Member findByOfficialEmail(String officialEmail);
}
34 changes: 33 additions & 1 deletion src/main/java/com/dnd/gongmuin/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
import com.dnd.gongmuin.member.domain.JobCategory;
import com.dnd.gongmuin.member.domain.JobGroup;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.dto.MemberMapper;
import com.dnd.gongmuin.member.dto.request.AdditionalInfoRequest;
import com.dnd.gongmuin.member.dto.request.LogoutRequest;
import com.dnd.gongmuin.member.dto.request.ReissueRequest;
import com.dnd.gongmuin.member.dto.request.UpdateMemberProfileRequest;
import com.dnd.gongmuin.member.dto.request.ValidateNickNameRequest;
import com.dnd.gongmuin.member.dto.response.LogoutResponse;
import com.dnd.gongmuin.member.dto.response.MemberProfileResponse;
import com.dnd.gongmuin.member.dto.response.ReissueResponse;
import com.dnd.gongmuin.member.dto.response.SignUpResponse;
import com.dnd.gongmuin.member.dto.response.ValidateNickNameResponse;
Expand Down Expand Up @@ -98,7 +101,7 @@ private void updateAdditionalInfo(AdditionalInfoRequest request, Member findMemb
request.nickname(),
request.officialEmail(),
JobGroup.from(request.jobGroup()),
JobCategory.of(request.jobCategory())
JobCategory.from(request.jobCategory())
);
}

Expand Down Expand Up @@ -157,4 +160,33 @@ public ReissueResponse reissue(ReissueRequest request) {

return new ReissueResponse(reissuedAccessToken);
}

@Transactional(readOnly = true)
public MemberProfileResponse getMemberProfile(Member member) {
try {
Member findMember = memberRepository.findByOfficialEmail(member.getOfficialEmail());
return MemberMapper.toMemberProfileResponse(findMember);
} catch (Exception e) {
throw new NotFoundException(MemberErrorCode.NOT_FOUND_MEMBER);
}
}

@Transactional
public MemberProfileResponse updateMemberProfile(UpdateMemberProfileRequest request, Member member) {
try {
Member findMember = memberRepository.findByOfficialEmail(member.getOfficialEmail());
JobGroup jobGroup = JobGroup.from(request.jobGroup());
JobCategory jobCategory = JobCategory.from(request.jobCategory());

findMember.updateProfile(
request.nickname(),
jobGroup,
jobCategory
);

return MemberMapper.toMemberProfileResponse(findMember);
} catch (Exception e) {
throw new ValidationException(MemberErrorCode.UPDATE_PROFILE_FAILED);
}
}
}
Loading

0 comments on commit 37fbfde

Please sign in to comment.