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 @@ -47,6 +47,15 @@ dependencies {
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// 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'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.foodmaster.domain.member.dto.join.MemberResDTO;
import com.example.foodmaster.domain.member.exception.code.MemberSuccessCode;
import com.example.foodmaster.domain.member.service.command.MemberCommandService;
import com.example.foodmaster.domain.member.service.query.MemberQueryService;
import com.example.foodmaster.global.apiPayLoad.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -16,10 +17,20 @@
public class MemberQueryController {

private final MemberCommandService memberCommandService;
private final MemberQueryService memberQueryService;

@PostMapping("/sign-up")
public ApiResponse<MemberResDTO.JoinDTO> signUp(
@RequestBody @Valid MemberReqDTO.JoinDTO dto) {
return ApiResponse.onSuccess(MemberSuccessCode.FOUND,memberCommandService.signUp(dto));
}

// 로그인
@PostMapping("/login")
public ApiResponse<MemberResDTO.LoginDTO> login(
@RequestBody @Valid MemberReqDTO.LoginDTO dto
) {
return ApiResponse.onSuccess(MemberSuccessCode.FOUND, memberQueryService.login(dto));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.foodmaster.domain.member.dto.join.MemberReqDTO;
import com.example.foodmaster.domain.member.dto.join.MemberResDTO;
import com.example.foodmaster.domain.member.entity.Member;
import com.example.foodmaster.global.auth.Role;
import lombok.Builder;

@Builder
Expand All @@ -20,13 +21,32 @@ public static MemberResDTO.JoinDTO toJoinDTO(

// DTO -> Entity
public static Member toMember(
MemberReqDTO.JoinDTO dto
MemberReqDTO.JoinDTO dto,
String password,
Role role
) {
return Member.builder()
// DTO에서 가져온 필드
.name(dto.name())
.email(dto.email())
.password(password)
.birth(dto.birth())
.detailAddress(dto.detailAddress())
.gender(dto.gender())
.nickName(dto.nickName())
.loginType(dto.loginType())
.phoneNumber(dto.phoneNumber())

// Converter에서 기본값을 설정하는 필드
.role(role) // Role.ROLE_USER
.point(0) // int의 기본값
.build();
}

// 로그인 응답 변환: Member 엔티티와 생성된 토큰을 받아 LoginDTO로 변환
public static MemberResDTO.LoginDTO toLoginDTO(Member member, String accessToken) {
return MemberResDTO.LoginDTO.builder()
.memberId(member.getId())
.accessToken(accessToken)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.example.foodmaster.domain.address.entity.DetailAddress;
import com.example.foodmaster.domain.member.entity.mapping.MemberFood;
import com.example.foodmaster.domain.member.enums.Gender;
import com.example.foodmaster.domain.member.enums.LoginType;
import com.example.foodmaster.global.annotation.ExistsFoods;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

Expand All @@ -15,18 +17,33 @@ public class MemberReqDTO {
public record JoinDTO(
@NotBlank
String name,
@Email
String email,
@NotBlank
String password,
@NotNull
Gender gender,
@NotNull
LocalDate birth,
@NotNull
DetailAddress detailAddress,
@ExistsFoods
List<MemberFood> preferFoods
@NotBlank String nickName,
@NotNull LoginType loginType,
@NotBlank String phoneNumber
// @NotNull
// DetailAddress detailAddress,
// @ExistsFoods
// List<MemberFood> preferFoods
) {}

public record MemberMissionDTO(
Long missionId
// memberId는 인증된 사용자 정보를 사용해 DTO에 포함하지 않는다.
) {}

// 로그인, 글로벌 auth에도 가능
public record LoginDTO(
@NotBlank
String email,
@NotBlank
String password
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ public record JoinDTO(
Long memberId,
LocalDateTime createAt
) {}

// 로그인 글로벌 auth에도 가능
@Builder
public record LoginDTO(
Long memberId,
String accessToken
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.example.foodmaster.domain.member.enums.Gender;
import com.example.foodmaster.domain.member.enums.LoginType;
import com.example.foodmaster.domain.review.entity.Review;
import com.example.foodmaster.global.auth.Role;
import com.example.foodmaster.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
Expand Down Expand Up @@ -46,9 +47,15 @@ public class Member extends BaseEntity {
@Column(name = "point")
private int point;

@Column(name = "email", nullable = false)
@Column(name = "email", nullable = false, unique = true)
private String email;

@Column(name = "password", nullable = false)
private String password;

@Enumerated(EnumType.STRING)
private Role role;

@Column(name = "phone_number", nullable = false)
private String phoneNumber;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

// 마이 페이지 화면 쿼리
Expand All @@ -28,4 +31,6 @@ Page<HomeDTO> findMemberHomePage(
@Param("memberId") Long memberId,
Pageable pageable
);

Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import com.example.foodmaster.domain.member.entity.mapping.MemberFood;
import com.example.foodmaster.domain.member.repository.MemberFoodRepository;
import com.example.foodmaster.domain.member.repository.MemberRepository;
import com.example.foodmaster.global.auth.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
Expand All @@ -24,35 +26,39 @@ public class MemberCommandServiceImpl implements MemberCommandService{
private final MemberRepository memberRepository;
private final MemberFoodRepository memberFoodRepository;
private final FoodRepository foodRepository;
private final PasswordEncoder passwordEncoder;

@Override
public MemberResDTO.JoinDTO signUp(
MemberReqDTO.JoinDTO dto
) {
// 솔트된 비밀번호 생성
String salt = passwordEncoder.encode(dto.password());

// 사용자 생성
Member member = MemberConverter.toMember(dto);
Member member = MemberConverter.toMember(dto, salt, Role.ROLE_USER);
// DB 적용
memberRepository.save(member);

if (dto.preferFoods().size() > 1) {
List<MemberFood> memberFoodList = new ArrayList<>();

// 선호 음식 ID별 조회
for (MemberFood preferFood : dto.preferFoods()) {
Food food = foodRepository.findById(preferFood.getId()).orElseThrow(() -> new FoodException(FoodErrorCode.NOT_FOUND));

// memberFood 엔티티 생성(컨버터 이용)
MemberFood memberFood = MemberFood.builder()
.member(member)
.food(food)
.build();

memberFoodList.add(memberFood);
}

// 모든 선호 음식 추가: DB 적용
memberFoodRepository.saveAll(memberFoodList);
}
// if (dto.preferFoods().size() > 1) {
// List<MemberFood> memberFoodList = new ArrayList<>();
//
// // 선호 음식 ID별 조회
// for (MemberFood preferFood : dto.preferFoods()) {
// Food food = foodRepository.findById(preferFood.getId()).orElseThrow(() -> new FoodException(FoodErrorCode.NOT_FOUND));
//
// // memberFood 엔티티 생성(컨버터 이용)
// MemberFood memberFood = MemberFood.builder()
// .member(member)
// .food(food)
// .build();
//
// memberFoodList.add(memberFood);
// }
//
// // 모든 선호 음식 추가: DB 적용
// memberFoodRepository.saveAll(memberFoodList);
// }

return MemberConverter.toJoinDTO(member);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
package com.example.foodmaster.domain.member.service.query;

import com.example.foodmaster.domain.member.dto.join.MemberReqDTO;
import com.example.foodmaster.domain.member.dto.join.MemberResDTO;
import jakarta.validation.Valid;

public interface MemberQueryService {
MemberResDTO.LoginDTO login(MemberReqDTO.LoginDTO dto);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,46 @@
package com.example.foodmaster.domain.member.service.query;

public class MemberQueryServiceImpl {
import com.example.foodmaster.domain.member.converter.MemberConverter;
import com.example.foodmaster.domain.member.dto.join.MemberReqDTO;
import com.example.foodmaster.domain.member.dto.join.MemberResDTO;
import com.example.foodmaster.domain.member.entity.Member;
import com.example.foodmaster.domain.member.exception.MemberException;
import com.example.foodmaster.domain.member.exception.code.MemberErrorCode;
import com.example.foodmaster.domain.member.repository.MemberRepository;
import com.example.foodmaster.global.auth.CustomUserDetails;
import com.example.foodmaster.global.auth.JwtUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberQueryServiceImpl implements MemberQueryService {

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

@Override
public MemberResDTO.LoginDTO login(
MemberReqDTO.@Valid LoginDTO dto) {
// member 조회
Member member = memberRepository.findByEmail(dto.email())
.orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND));

// 비밀번호 검증
if (!encoder.matches(dto.password(), member.getPassword())){
throw new MemberException(MemberErrorCode.NOT_FOUND);
}

// JWT 토큰 발급용 UserDetails
CustomUserDetails userDetails = new CustomUserDetails(member);

// 엑세스 토큰 발급
String accessToken = jwtUtil.createAccessToken(userDetails);

// DTO 조립
return MemberConverter.toLoginDTO(member, accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.foodmaster.global.auth;

import com.example.foodmaster.global.apiPayLoad.ApiResponse;
import com.example.foodmaster.global.apiPayLoad.code.GeneralErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import java.io.IOException;

public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

ApiResponse<Void> errorResponse = ApiResponse.onFailure(
GeneralErrorCode.UNAUTHORIZED,
null
);

objectMapper.writeValue(response.getOutputStream(), errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.foodmaster.global.auth;

import com.example.foodmaster.domain.member.entity.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

private final Member member;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> member.getRole().toString());
}

@Override
public String getPassword() {
return member.getPassword();
}

@Override
public String getUsername() {
return member.getEmail();
}
}
Loading