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
2 changes: 2 additions & 0 deletions naru/umc10th/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class Umc10thApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.umc10th.domain.auth.controller;

import com.example.umc10th.domain.auth.dto.AuthRequestDto;
import com.example.umc10th.domain.auth.dto.AuthResponseDto;
import com.example.umc10th.domain.auth.service.AuthService;
import com.example.umc10th.domain.user.exception.code.UserSuccessCode;
import com.example.umc10th.global.apiPayload.ApiResponse;
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.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;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
@Tag(name = "인증 API", description = "회원가입 및 로그인 관련 API")
public class AuthController {

private final AuthService authService;

@Operation(summary = "회원가입 API")
@PostMapping("/signup")
public ApiResponse<AuthResponseDto.SignUpResultDto> signUp(
@RequestBody @Valid AuthRequestDto.SignUpDto request
) {
AuthResponseDto.SignUpResultDto result = authService.signUp(request);
return ApiResponse.onSuccess(UserSuccessCode.USER_JOIN_SUCCESS, result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.umc10th.domain.auth.converter;

import com.example.umc10th.domain.auth.dto.AuthRequestDto;
import com.example.umc10th.domain.auth.dto.AuthResponseDto;
import com.example.umc10th.domain.user.entity.User;
import com.example.umc10th.domain.user.entity.enums.Role;
import com.example.umc10th.domain.user.entity.enums.Status;

public class AuthConverter {

public static User toUser(AuthRequestDto.SignUpDto request, String encodedPassword) {
return User.builder()
.email(request.email())
.password(encodedPassword)
.name(request.name())
.role(Role.USER)
.status(Status.ACTIVE)
.step(4)
.totalPoint(0)
.gender(request.gender())
.birth(request.birth())
.baseAddress(request.address())
.isVerified(false)
.build();
}

public static AuthResponseDto.SignUpResultDto toSignUpResultDto(User user) {
return AuthResponseDto.SignUpResultDto.builder()
.userId(user.getId())
.email(user.getEmail())
.name(user.getName())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.umc10th.domain.auth.dto;

import com.example.umc10th.domain.user.entity.enums.Gender;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import java.time.LocalDate;
import java.util.List;

public class AuthRequestDto {

public record SignUpDto(
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email,

@NotBlank(message = "비밀번호는 필수입니다.")
@Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다.")
String password,

@NotBlank(message = "이름은 필수입니다.")
String name,

@NotNull(message = "성별은 필수입니다.")
Gender gender,

@NotNull(message = "생년월일은 필수입니다.")
LocalDate birth,

@NotBlank(message = "주소는 필수입니다.")
String address,

@NotNull(message = "선호 음식 카테고리는 필수입니다.")
@NotEmpty(message = "선호 음식 카테고리는 1개 이상 선택해야 합니다.")
List<@NotNull(message = "선호 음식 카테고리 ID는 필수입니다.") Long> preferredFoodCategoryIds,

@NotNull(message = "약관 동의 목록은 필수입니다.")
@NotEmpty(message = "약관 동의 목록은 1개 이상이어야 합니다.")
List<@NotNull(message = "약관 동의 항목은 null일 수 없습니다.") @Valid AgreementDto> agreements
) {}

public record AgreementDto(
@NotNull(message = "약관 ID는 필수입니다.")
Long termId,

@NotNull(message = "약관 동의 여부는 필수입니다.")
Boolean isAgreed
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.umc10th.domain.auth.dto;

import lombok.Builder;

public class AuthResponseDto {

@Builder
public record SignUpResultDto(
Long userId,
String email,
String name
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.umc10th.domain.auth.exception;

import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
import com.example.umc10th.global.exception.ProjectException;

public class AuthException extends ProjectException {
public AuthException(BaseErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.umc10th.domain.auth.exception.code;

import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum AuthErrorCode implements BaseErrorCode {

FOOD_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH404_0", "존재하지 않는 음식 카테고리가 포함되어 있습니다."),
TERM_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH404_1", "약관을 찾을 수 없습니다."),
REQUIRED_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "AUTH400_1", "필수 약관에 동의해야 합니다.")
;

private final HttpStatus status;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.umc10th.domain.auth.service;

import com.example.umc10th.domain.auth.dto.AuthRequestDto;
import com.example.umc10th.domain.auth.dto.AuthResponseDto;

public interface AuthService {

AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.example.umc10th.domain.auth.service;

import com.example.umc10th.domain.auth.converter.AuthConverter;
import com.example.umc10th.domain.auth.dto.AuthRequestDto;
import com.example.umc10th.domain.auth.dto.AuthResponseDto;
import com.example.umc10th.domain.auth.exception.AuthException;
import com.example.umc10th.domain.auth.exception.code.AuthErrorCode;
import com.example.umc10th.domain.category.entity.FoodCategory;
import com.example.umc10th.domain.category.entity.mapping.UserFoodCategory;
import com.example.umc10th.domain.category.repository.FoodCategoryRepository;
import com.example.umc10th.domain.category.repository.UserFoodCategoryRepository;
import com.example.umc10th.domain.term.entity.Term;
import com.example.umc10th.domain.term.entity.mapping.UserAgreement;
import com.example.umc10th.domain.term.repository.TermRepository;
import com.example.umc10th.domain.term.repository.UserAgreementRepository;
import com.example.umc10th.domain.user.entity.User;
import com.example.umc10th.domain.user.exception.UserException;
import com.example.umc10th.domain.user.exception.code.UserErrorCode;
import com.example.umc10th.domain.user.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

private final UserRepository userRepository;
private final FoodCategoryRepository foodCategoryRepository;
private final UserFoodCategoryRepository userFoodCategoryRepository;
private final TermRepository termRepository;
private final UserAgreementRepository userAgreementRepository;
private final PasswordEncoder passwordEncoder;

@Override
@Transactional
public AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request) {
if (userRepository.existsByEmail(request.email())) {
throw new UserException(UserErrorCode.DUPLICATE_EMAIL);
}

Map<Long, Boolean> agreementMap = request.agreements().stream()
.collect(Collectors.toMap(
AuthRequestDto.AgreementDto::termId,
AuthRequestDto.AgreementDto::isAgreed,
(first, second) -> second
));

validateRequiredTerms(agreementMap);
List<Term> requestedTerms = getRequestedTerms(agreementMap.keySet());
List<FoodCategory> preferredFoodCategories = getPreferredFoodCategories(request.preferredFoodCategoryIds());

User user = AuthConverter.toUser(request, passwordEncoder.encode(request.password()));
User savedUser = userRepository.save(user);

List<UserFoodCategory> userFoodCategories = preferredFoodCategories.stream()
.map(foodCategory -> UserFoodCategory.builder()
.user(savedUser)
.foodCategory(foodCategory)
.build())
.toList();

userFoodCategoryRepository.saveAll(userFoodCategories);
saveUserAgreements(savedUser, requestedTerms, agreementMap);
return AuthConverter.toSignUpResultDto(savedUser);
}

private void validateRequiredTerms(Map<Long, Boolean> agreementMap) {
List<Term> requiredTerms = termRepository.findAllByIsRequiredTrue();

boolean hasNotAgreedRequiredTerm = requiredTerms.stream()
.anyMatch(term -> !Boolean.TRUE.equals(agreementMap.get(term.getId())));

if (hasNotAgreedRequiredTerm) {
throw new AuthException(AuthErrorCode.REQUIRED_TERM_NOT_AGREED);
}
}

private List<Term> getRequestedTerms(Set<Long> termIds) {
List<Term> terms = termRepository.findAllById(termIds);

if (terms.size() != termIds.size()) {
throw new AuthException(AuthErrorCode.TERM_NOT_FOUND);
}

return terms;
}

private List<FoodCategory> getPreferredFoodCategories(List<Long> foodCategoryIds) {
Set<Long> uniqueFoodCategoryIds = Set.copyOf(foodCategoryIds);
List<FoodCategory> foodCategories = foodCategoryRepository.findAllById(uniqueFoodCategoryIds);

if (foodCategories.size() != uniqueFoodCategoryIds.size()) {
throw new AuthException(AuthErrorCode.FOOD_CATEGORY_NOT_FOUND);
}

return foodCategories;
}

private void saveUserAgreements(User user, List<Term> terms, Map<Long, Boolean> agreementMap) {
List<UserAgreement> userAgreements = terms.stream()
.map(term -> UserAgreement.builder()
.user(user)
.term(term)
.isAgreed(agreementMap.get(term.getId()))
.build())
.toList();

userAgreementRepository.saveAll(userAgreements);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.umc10th.domain.category.repository;

import com.example.umc10th.domain.category.entity.FoodCategory;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface FoodCategoryRepository extends JpaRepository<FoodCategory, Long> {
Optional<FoodCategory> findByName(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.umc10th.domain.category.repository;

import com.example.umc10th.domain.category.entity.mapping.UserFoodCategory;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserFoodCategoryRepository extends JpaRepository<UserFoodCategory, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.umc10th.domain.term.repository;

import com.example.umc10th.domain.term.entity.Term;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface TermRepository extends JpaRepository<Term, Long> {
List<Term> findAllByIsRequiredTrue();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.umc10th.domain.term.repository;

import com.example.umc10th.domain.term.entity.mapping.UserAgreement;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserAgreementRepository extends JpaRepository<UserAgreement, Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public class User extends BaseEntity {
@Column(nullable = false, length = 255)
private String email;

@Column(length = 255)
private String password;

@Enumerated(EnumType.STRING)
@Column(columnDefinition = "VARCHAR(20)")
private SocialType socialType;
Expand Down Expand Up @@ -73,4 +76,4 @@ public class User extends BaseEntity {

@Column(length = 500)
private String profileImageKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
public enum UserErrorCode implements BaseErrorCode {

USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404_1", "사용자를 찾을 수 없습니다."),
DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "USER400_0", "이미 사용 중인 이메일입니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "USER400_1", "비밀번호가 일치하지 않습니다.")
;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
import com.example.umc10th.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);

boolean existsByEmail(String email);
}
Loading