Skip to content
Open
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
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ dependencies {
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'

// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,54 @@
package com.example.umc10th.domain.member.controller;


import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.domain.member.service.MemberService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
import com.example.umc10th.global.security.entity.AuthMember;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

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

private final MemberService memberService;

@PostMapping("/users/me")
public ApiResponse<MemberResDTO.GetInfo> getInfo(
@RequestBody @Valid MemberReqDTO.GetInfo dto
) {
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.success(code, memberService.getInfo(dto));
}

// 마이페이지
// TODO: Security 적용 후 매개변수 변경
@GetMapping("/myPage")
public ApiResponse<MemberResDTO.GetInfo> getMyPage(
@RequestParam Long memberId
) {
BaseSuccessCode code = MemberSuccessCode.MY_PAGE_OK;
return ApiResponse.success(code, memberService.getMyPage(memberId));
}

// 회원가입
@Operation(summary = "회원가입", description = "이메일/비밀번호 기반 회원가입")
@PostMapping("/auth/signup")
public ApiResponse<MemberResDTO.SignUp> signUp(
@RequestBody @Valid MemberReqDTO.SignUp dto
) {
BaseSuccessCode code = MemberSuccessCode.SIGNUP_OK;
return ApiResponse.success(code, memberService.signUp(dto));
}
}

// 로그인 → JWT 발급
@Operation(summary = "로그인", description = "이메일/비밀번호로 로그인 후 JWT 액세스 토큰 발급")
@PostMapping("/auth/login")
public ApiResponse<MemberResDTO.Login> login(
@RequestBody @Valid MemberReqDTO.Login dto
) {
BaseSuccessCode code = MemberSuccessCode.LOGIN_OK;
return ApiResponse.success(code, memberService.login(dto));
}

// 마이페이지 v2 - JWT 토큰에서 사용자 정보 자동 추출
@Operation(summary = "마이페이지", description = "JWT 토큰으로 인증 후 내 프로필 정보 조회 (Authorization: Bearer {token})")
@GetMapping("/users/me")
public ApiResponse<MemberResDTO.GetInfo> getMyPage(
@AuthenticationPrincipal AuthMember member
) {
BaseSuccessCode code = MemberSuccessCode.MY_PAGE_OK;
return ApiResponse.success(code, memberService.getInfo(member));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.enums.Address;
import com.example.umc10th.global.security.dto.OAuthDTO;

import java.time.LocalDate;

public class MemberConverter {

Expand All @@ -20,7 +24,7 @@ public static MemberResDTO.GetInfo toGetInfo(Member member) {
.build();
}

// 회원가입
// 일반 회원가입 (이메일/비밀번호)
public static Member toMember(MemberReqDTO.SignUp dto, String encodedPassword) {
return Member.builder()
.name(dto.name())
Expand All @@ -34,6 +38,21 @@ public static Member toMember(MemberReqDTO.SignUp dto, String encodedPassword) {
.build();
}

// OAuth 회원가입 (소셜 로그인 신규 유저)
// - 비밀번호/생년월일/주소 등 OAuth에서 제공하지 않는 정보는 기본값 처리
public static Member toMember(OAuthDTO dto) {
return Member.builder()
.name(dto.getName())
.email(dto.getSocialEmail())
.socialType(dto.getSocialType())
.socialUid(dto.getSocialUid())
.password("") // OAuth 유저는 비밀번호 없음
.address(Address.NONE) // 추후 프로필 설정에서 입력
.detailAddress("") // 추후 프로필 설정에서 입력
.birth(LocalDate.of(1900, 1, 1)) // 추후 프로필 설정에서 입력
.build();
}

public static MemberResDTO.SignUp toSignUp(Member member) {
return MemberResDTO.SignUp.builder()
.memberId(member.getId())
Expand All @@ -42,4 +61,12 @@ public static MemberResDTO.SignUp toSignUp(Member member) {
.createdAt(member.getCratedAt())
.build();
}

// OAuth 로그인 성공 응답 DTO
public static MemberResDTO.Login toLogin(String accessToken) {
return MemberResDTO.Login.builder()
.accessToken(accessToken)
.build();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.example.umc10th.domain.member.enums.Address;
import com.example.umc10th.domain.member.enums.Gender;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDate;
Expand All @@ -15,7 +17,6 @@ public record GetInfo(

// 회원가입
public record SignUp(

@NotNull(message = "이름을 입력해주세요.")
String name,
@NotNull(message = "성별을 입력해주세요.")
Expand All @@ -25,11 +26,21 @@ public record SignUp(
@NotNull(message = "주소를 입력해주세요.")
Address address,
String detailAddress,
@Email(message = "올바른 이메일 형식을 입력해주세요.")
@NotNull(message = "이메일을 입력해주세요.")
String email,
@NotNull(message = "비밀번호를 입력해주세요.")
String password,
@NotNull(message = "전화번호를 입력해주세요.")
String phoneNumber
) {}

// 로그인
public record Login(
@Email(message = "올바른 이메일 형식을 입력해주세요.")
@NotBlank(message = "이메일을 입력해주세요.")
String email,
@NotBlank(message = "비밀번호를 입력해주세요.")
String password
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ public record SignUp(
String email,
LocalDateTime createdAt
) {}
}

@Builder
public record Login(
String accessToken
) {}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.umc10th.domain.member.enums;

public enum Address {
NONE, // 미설정 (OAuth 회원가입 초기값)
SEOUL,
GYEONGGI,
INCHEON,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ public enum MemberErrorCode implements BaseErrorCode {
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST,
"MEMBER404_1",
"사용자가 존재하지 않습니다."),

INVALID_PASSWORD(HttpStatus.UNAUTHORIZED,
"MEMBER401_1",
"비밀번호가 일치하지 않습니다."),

NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST,
"MEMBER400_1",
"지원하지 않는 소셜 로그인 제공자입니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public enum MemberSuccessCode implements BaseSuccessCode {
"MEMBER200_2",
"마이페이지 정보를 성공적으로 조회했습니다."),

LOGIN_OK(HttpStatus.OK,
"MEMBER200_3",
"로그인이 성공적으로 완료되었습니다."),

SIGNUP_OK(HttpStatus.CREATED,
"MEMBER201_1",
"회원가입이 성공적으로 완료되었습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.example.umc10th.domain.member.repository;

import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.enums.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Integer> {
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByNameAndDeletedAtIsNull(String name);

Optional<Member> findByEmail(String email);

Optional<Member> findBySocialTypeAndSocialUid(SocialType socialType, String socialUid);
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.exception.MemberException;
import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
import com.example.umc10th.domain.member.repository.MemberRepository;
import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
import com.example.umc10th.global.security.entity.AuthMember;
import com.example.umc10th.global.security.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
Expand All @@ -18,26 +22,44 @@ public class MemberService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;

// 구버전: Request Body로 ID를 직접 받아 조회
public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) {
// TODO: 구현 예정
return null;
}

public MemberResDTO.GetInfo getMyPage(Long memberId) {
// v2: SecurityContextHolder에서 AuthMember를 받아 컨버터에 직접 전달 (추가 DB 조회 불필요)
public MemberResDTO.GetInfo getInfo(AuthMember authMember) {
return MemberConverter.toGetInfo(authMember.getMember());
}

Member member = memberRepository.findById(Math.toIntExact(memberId))
public MemberResDTO.GetInfo getMyPage(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new RuntimeException(GeneralErrorCode.NOT_FOUND.getMessage()));

return MemberConverter.toGetInfo(member);
}

// 회원가입
@Transactional
public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp dto) {
String encodedPassword = passwordEncoder.encode(dto.password());
Member member = MemberConverter.toMember(dto, encodedPassword);
Member savedMember = memberRepository.save(member);

return MemberConverter.toSignUp(savedMember);
}

public MemberResDTO.Login login(MemberReqDTO.Login dto) {

Member member = memberRepository.findByEmail(dto.email())
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

if (!passwordEncoder.matches(dto.password(), member.getPassword())) {
throw new MemberException(MemberErrorCode.INVALID_PASSWORD);
}

String accessToken = jwtUtil.createAccessToken(new AuthMember(member));
return MemberConverter.toLogin(accessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
public interface MissionRespository extends JpaRepository<Mission, Long> {

@EntityGraph(attributePaths = "store")
Page<Mission> findByStore_Location_Id(Integer locationId, Pageable pageable);
Page<Mission> findByStore_Location_Id(Long locationId, Pageable pageable);

List<Mission> findAllByStore_Id(Long storeId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ public class MissionService {
// 홈 화면
public MissionResDTO.GetHome getHome(MissionReqDTO.GetHome dto, Pageable pageable) {

Member member = memberRepository.findById(Math.toIntExact(dto.memberId()))
Member member = memberRepository.findById(dto.memberId())
.orElseThrow(() -> new RuntimeException(GeneralErrorCode.NOT_FOUND.getMessage()));

Page<Mission> missionPage = missionRepository.findByStore_Location_Id(
Math.toIntExact(dto.locationId()), pageable
dto.locationId(), pageable
);

return MissionConverter.toGetHome(member, missionPage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class ReviewService {
// 리뷰 작성
public ReviewResDTO.CreateReview createReview(Long storeId, ReviewReqDTO.CreateReview dto) {

Member member = memberRepository.findById(Math.toIntExact(dto.memberId()))
Member member = memberRepository.findById(dto.memberId())
.orElseThrow(() -> new RuntimeException(GeneralErrorCode.NOT_FOUND.getMessage()));

Store store = storeRepository.findById(storeId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.http.ResponseEntity;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Getter
@JsonPropertyOrder({"isSuccess","timestamp","code","message","result"})
Expand All @@ -25,14 +26,15 @@ public class ApiResponse<T> {
private final String message;

@JsonProperty("timestamp")
private final LocalDateTime timestamp;
private final String timestamp;

@JsonProperty("result")
private T result;

private ApiResponse(Boolean isSuccess, String code, String message, T result) {
this.isSuccess = isSuccess;
this.timestamp = LocalDateTime.now();
this.timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
this.code = code;
this.message = message;
this.result = result;
Expand All @@ -43,6 +45,11 @@ public static <T> ApiResponse<T> success(BaseSuccessCode code, T result) {
return new ApiResponse<>(true, code.getCode(), code.getMessage(), result);
}

// [성공] 필터/핸들러에서 ObjectMapper로 직접 쓸 때 사용 (success의 alias)
public static <T> ApiResponse<T> onSuccess(BaseSuccessCode code, T result) {
return success(code, result);
}

// [실패] 에러 핸들러에서 사용 (데이터 없는 일반 에러)
public static <T> ResponseEntity<ApiResponse<T>> onFailureEntity(BaseErrorCode code) {
return ResponseEntity
Expand All @@ -57,6 +64,11 @@ public static <T> ResponseEntity<ApiResponse<T>> onFailureEntity(BaseErrorCode c
.body(onFailureBody(code, result));
}

// [실패] 필터에서 ObjectMapper로 직접 쓸 때 사용 (ResponseEntity 없이 body만 필요)
public static <T> ApiResponse<T> onFailure(BaseErrorCode code, T result) {
return onFailureBody(code, result);
}

private static <T> ApiResponse<T> onFailureBody(BaseErrorCode code, T result) {
return new ApiResponse<>(false, code.getCode(), code.getMessage(), result);
}
Expand Down
Loading