From 15e4090e9187d6175d064d5a80bad88e2d5fa3ff Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 16:30:37 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20Spring=20Security=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- naru/umc10th/build.gradle | 2 + .../umc10th/global/config/SecurityConfig.java | 63 +++++++++++++++++++ .../global/security/CustomUserDetails.java | 56 +++++++++++++++++ .../security/CustomUserDetailsService.java | 22 +++++++ .../handler/CustomAccessDeniedHandler.java | 24 +++++++ .../CustomAuthenticationEntryPoint.java | 24 +++++++ .../CustomAuthenticationFailureHandler.java | 24 +++++++ .../CustomAuthenticationSuccessHandler.java | 24 +++++++ .../handler/SecurityResponseWriter.java | 34 ++++++++++ 9 files changed, 273 insertions(+) create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetails.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationFailureHandler.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationSuccessHandler.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/SecurityResponseWriter.java diff --git a/naru/umc10th/build.gradle b/naru/umc10th/build.gradle index f09b873..7ba1dfd 100644 --- a/naru/umc10th/build.gradle +++ b/naru/umc10th/build.gradle @@ -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' diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/naru/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 0000000..cf48e4d --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,63 @@ +package com.example.umc10th.global.config; + +import com.example.umc10th.global.security.handler.CustomAccessDeniedHandler; +import com.example.umc10th.global.security.handler.CustomAuthenticationEntryPoint; +import com.example.umc10th.global.security.handler.CustomAuthenticationFailureHandler; +import com.example.umc10th.global.security.handler.CustomAuthenticationSuccessHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final CustomAuthenticationEntryPoint authenticationEntryPoint; + private final CustomAccessDeniedHandler accessDeniedHandler; + private final CustomAuthenticationSuccessHandler authenticationSuccessHandler; + private final CustomAuthenticationFailureHandler authenticationFailureHandler; + + private static final String[] PUBLIC_URLS = { + "/auth/**", + "/login", + "/swagger-ui/**", + "/swagger-ui.html", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**" + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers(PUBLIC_URLS).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginProcessingUrl("/login") + .usernameParameter("email") + .passwordParameter("password") + .successHandler(authenticationSuccessHandler) + .failureHandler(authenticationFailureHandler) + .permitAll() + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler) + ) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetails.java b/naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetails.java new file mode 100644 index 0000000..b51bf78 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.domain.user.entity.User; +import com.example.umc10th.domain.user.entity.enums.Status; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + + private final User user; + + public Long getUserId() { + return user.getId(); + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return user.getStatus() == Status.ACTIVE; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return user.getStatus() == Status.ACTIVE; + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java b/naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java new file mode 100644 index 0000000..c29e001 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java @@ -0,0 +1,22 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findByEmail(email) + .map(CustomUserDetails::new) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..edf593f --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java @@ -0,0 +1,24 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final SecurityResponseWriter responseWriter; + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + responseWriter.writeFailure(response, GeneralErrorCode.FORBIDDEN); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..062207a --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java @@ -0,0 +1,24 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final SecurityResponseWriter responseWriter; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + responseWriter.writeFailure(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationFailureHandler.java b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationFailureHandler.java new file mode 100644 index 0000000..cf5c6d7 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationFailureHandler.java @@ -0,0 +1,24 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final SecurityResponseWriter responseWriter; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException { + responseWriter.writeFailure(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationSuccessHandler.java b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationSuccessHandler.java new file mode 100644 index 0000000..9314d69 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationSuccessHandler.java @@ -0,0 +1,24 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.domain.user.exception.code.UserSuccessCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final SecurityResponseWriter responseWriter; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + responseWriter.writeSuccess(response, UserSuccessCode.USER_LOGIN_SUCCESS); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/SecurityResponseWriter.java b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/SecurityResponseWriter.java new file mode 100644 index 0000000..30f65ce --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/security/handler/SecurityResponseWriter.java @@ -0,0 +1,34 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Component +@RequiredArgsConstructor +public class SecurityResponseWriter { + + private final ObjectMapper objectMapper; + + public void writeFailure(HttpServletResponse response, BaseErrorCode code) throws IOException { + response.setStatus(code.getStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + objectMapper.writeValue(response.getWriter(), ApiResponse.onFailure(code, null)); + } + + public void writeSuccess(HttpServletResponse response, BaseSuccessCode code) throws IOException { + response.setStatus(code.getStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + objectMapper.writeValue(response.getWriter(), ApiResponse.onSuccess(code, null)); + } +} From 455c502cf1919f35a8d0e4741b563c0af86f83fa Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 16:30:49 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EC=9C=84=ED=95=9C=20User=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/user/entity/User.java | 5 ++++- .../umc10th/domain/user/exception/code/UserErrorCode.java | 1 + .../umc10th/domain/user/repository/UserRepository.java | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/user/entity/User.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/user/entity/User.java index 685bc5f..9471ffc 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/user/entity/User.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/user/entity/User.java @@ -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; @@ -73,4 +76,4 @@ public class User extends BaseEntity { @Column(length = 500) private String profileImageKey; -} \ No newline at end of file +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java index d2fc3b6..a269e05 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java @@ -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", "비밀번호가 일치하지 않습니다.") ; diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/user/repository/UserRepository.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/user/repository/UserRepository.java index 2d14540..edfedf1 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/user/repository/UserRepository.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/user/repository/UserRepository.java @@ -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 { + Optional findByEmail(String email); + + boolean existsByEmail(String email); } From d9cd6fe819a8563f264a23e9542a9bdcd82a3bcb Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 16:31:08 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20Auth=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 33 +++++++++++ .../domain/auth/converter/AuthConverter.java | 34 +++++++++++ .../domain/auth/dto/AuthRequestDto.java | 39 ++++++++++++ .../domain/auth/dto/AuthResponseDto.java | 13 ++++ .../domain/auth/service/AuthService.java | 9 +++ .../domain/auth/service/AuthServiceImpl.java | 59 +++++++++++++++++++ .../repository/FoodCategoryRepository.java | 10 ++++ .../UserFoodCategoryRepository.java | 7 +++ 8 files changed, 204 insertions(+) create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResponseDto.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/FoodCategoryRepository.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/UserFoodCategoryRepository.java diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..6fcdc73 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java @@ -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 signUp( + @RequestBody @Valid AuthRequestDto.SignUpDto request + ) { + AuthResponseDto.SignUpResultDto result = authService.signUp(request); + return ApiResponse.onSuccess(UserSuccessCode.USER_JOIN_SUCCESS, result); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java new file mode 100644 index 0000000..ab26fe1 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/converter/AuthConverter.java @@ -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(); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java new file mode 100644 index 0000000..8f0e758 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java @@ -0,0 +1,39 @@ +package com.example.umc10th.domain.auth.dto; + +import com.example.umc10th.domain.user.entity.enums.Gender; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +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, + + @NotEmpty(message = "선호 음식 종류는 1개 이상 선택해야 합니다.") + List<@NotBlank(message = "선호 음식 이름은 비어 있을 수 없습니다.") String> preferredFoods + ) {} +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResponseDto.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResponseDto.java new file mode 100644 index 0000000..d341f89 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthResponseDto.java @@ -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 + ) {} +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java new file mode 100644 index 0000000..caf63ba --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java @@ -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); +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java new file mode 100644 index 0000000..e66bfce --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java @@ -0,0 +1,59 @@ +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.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.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; + +@Service +@RequiredArgsConstructor +public class AuthServiceImpl implements AuthService { + + private final UserRepository userRepository; + private final FoodCategoryRepository foodCategoryRepository; + private final UserFoodCategoryRepository userFoodCategoryRepository; + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional + public AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request) { + if (userRepository.existsByEmail(request.email())) { + throw new UserException(UserErrorCode.DUPLICATE_EMAIL); + } + + User user = AuthConverter.toUser(request, passwordEncoder.encode(request.password())); + User savedUser = userRepository.save(user); + + List userFoodCategories = request.preferredFoods().stream() + .distinct() + .map(foodName -> { + FoodCategory foodCategory = foodCategoryRepository.findByName(foodName) + .orElseGet(() -> foodCategoryRepository.save( + FoodCategory.builder() + .name(foodName) + .build() + )); + return UserFoodCategory.builder() + .user(savedUser) + .foodCategory(foodCategory) + .build(); + }) + .toList(); + + userFoodCategoryRepository.saveAll(userFoodCategories); + return AuthConverter.toSignUpResultDto(savedUser); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/FoodCategoryRepository.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/FoodCategoryRepository.java new file mode 100644 index 0000000..aae9ae2 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/FoodCategoryRepository.java @@ -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 { + Optional findByName(String name); +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/UserFoodCategoryRepository.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/UserFoodCategoryRepository.java new file mode 100644 index 0000000..2d7cbf6 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/category/repository/UserFoodCategoryRepository.java @@ -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 { +} From 9388b0f4a86a1a478ea4feb444dbff7c4823e8e3 Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 16:31:26 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20JSON=20=EC=A7=81=EB=A0=AC=ED=99=94?= =?UTF-8?q?=EC=99=80=20JPA=20Auditing=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/Umc10thApplication.java | 2 ++ .../umc10th/global/config/JacksonConfig.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java diff --git a/naru/umc10th/src/main/java/com/example/umc10th/Umc10thApplication.java b/naru/umc10th/src/main/java/com/example/umc10th/Umc10thApplication.java index 35d830f..187a7e2 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/Umc10thApplication.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/Umc10thApplication.java @@ -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 { diff --git a/naru/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java b/naru/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java new file mode 100644 index 0000000..ce59648 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/global/config/JacksonConfig.java @@ -0,0 +1,18 @@ +package com.example.umc10th.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } +} From 199522b3028b0a65974f49b6ee8167fd876c474c Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 16:39:28 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EC=95=BD=EA=B4=80=20=EB=8F=99=EC=9D=98?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/dto/AuthRequestDto.java | 14 ++++- .../domain/auth/exception/AuthException.java | 10 ++++ .../auth/exception/code/AuthErrorCode.java | 19 +++++++ .../domain/auth/service/AuthServiceImpl.java | 55 +++++++++++++++++++ .../term/repository/TermRepository.java | 10 ++++ .../repository/UserAgreementRepository.java | 7 +++ 6 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/TermRepository.java create mode 100644 naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/UserAgreementRepository.java diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java index 8f0e758..e14a497 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java @@ -1,6 +1,7 @@ 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.NotBlank; import jakarta.validation.constraints.NotEmpty; @@ -34,6 +35,17 @@ public record SignUpDto( String address, @NotEmpty(message = "선호 음식 종류는 1개 이상 선택해야 합니다.") - List<@NotBlank(message = "선호 음식 이름은 비어 있을 수 없습니다.") String> preferredFoods + List<@NotBlank(message = "선호 음식 이름은 비어 있을 수 없습니다.") String> preferredFoods, + + @NotEmpty(message = "약관 동의 목록은 필수입니다.") + List<@Valid AgreementDto> agreements + ) {} + + public record AgreementDto( + @NotNull(message = "약관 ID는 필수입니다.") + Long termId, + + @NotNull(message = "약관 동의 여부는 필수입니다.") + Boolean isAgreed ) {} } diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java new file mode 100644 index 0000000..e8265d1 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java @@ -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); + } +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java new file mode 100644 index 0000000..d53cf28 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java @@ -0,0 +1,19 @@ +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 { + + 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; +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java index e66bfce..e2871d4 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java @@ -3,10 +3,16 @@ 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; @@ -17,6 +23,9 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -25,6 +34,8 @@ 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 @@ -34,6 +45,16 @@ public AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request) throw new UserException(UserErrorCode.DUPLICATE_EMAIL); } + Map agreementMap = request.agreements().stream() + .collect(Collectors.toMap( + AuthRequestDto.AgreementDto::termId, + AuthRequestDto.AgreementDto::isAgreed, + (first, second) -> second + )); + + validateRequiredTerms(agreementMap); + List requestedTerms = getRequestedTerms(agreementMap.keySet()); + User user = AuthConverter.toUser(request, passwordEncoder.encode(request.password())); User savedUser = userRepository.save(user); @@ -54,6 +75,40 @@ public AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request) .toList(); userFoodCategoryRepository.saveAll(userFoodCategories); + saveUserAgreements(savedUser, requestedTerms, agreementMap); return AuthConverter.toSignUpResultDto(savedUser); } + + private void validateRequiredTerms(Map agreementMap) { + List 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 getRequestedTerms(Set termIds) { + List terms = termRepository.findAllById(termIds); + + if (terms.size() != termIds.size()) { + throw new AuthException(AuthErrorCode.TERM_NOT_FOUND); + } + + return terms; + } + + private void saveUserAgreements(User user, List terms, Map agreementMap) { + List userAgreements = terms.stream() + .map(term -> UserAgreement.builder() + .user(user) + .term(term) + .isAgreed(agreementMap.get(term.getId())) + .build()) + .toList(); + + userAgreementRepository.saveAll(userAgreements); + } } diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/TermRepository.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/TermRepository.java new file mode 100644 index 0000000..cbf1c85 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/TermRepository.java @@ -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 { + List findAllByIsRequiredTrue(); +} diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/UserAgreementRepository.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/UserAgreementRepository.java new file mode 100644 index 0000000..814cd30 --- /dev/null +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/term/repository/UserAgreementRepository.java @@ -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 { +} From 7b44ed8b3292c4a556dcaef44dab82445523d269 Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 16:43:07 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=9A=94=EC=B2=AD=20DTO=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/umc10th/domain/auth/dto/AuthRequestDto.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java index e14a497..b80004f 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java @@ -34,11 +34,13 @@ public record SignUpDto( @NotBlank(message = "주소는 필수입니다.") String address, + @NotNull(message = "선호 음식 종류는 필수입니다.") @NotEmpty(message = "선호 음식 종류는 1개 이상 선택해야 합니다.") List<@NotBlank(message = "선호 음식 이름은 비어 있을 수 없습니다.") String> preferredFoods, - @NotEmpty(message = "약관 동의 목록은 필수입니다.") - List<@Valid AgreementDto> agreements + @NotNull(message = "약관 동의 목록은 필수입니다.") + @NotEmpty(message = "약관 동의 목록은 1개 이상이어야 합니다.") + List<@NotNull(message = "약관 동의 항목은 null일 수 없습니다.") @Valid AgreementDto> agreements ) {} public record AgreementDto( From b8cd2c0dc46f4ce0836d9dc13873d69c9d8f63b0 Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 16:54:29 +0900 Subject: [PATCH 7/8] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EB=8D=94=EB=AF=B8=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/dummy-my-mission-review.sql | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 naru/umc10th/src/main/resources/dummy-my-mission-review.sql diff --git a/naru/umc10th/src/main/resources/dummy-my-mission-review.sql b/naru/umc10th/src/main/resources/dummy-my-mission-review.sql new file mode 100644 index 0000000..d09f415 --- /dev/null +++ b/naru/umc10th/src/main/resources/dummy-my-mission-review.sql @@ -0,0 +1,174 @@ +-- Dummy data for: +-- 1. GET /api/v1/missions/offset?userId=1&page=0&size=10 +-- 2. GET /api/v1/reviews/my?userId=1&sort=ID&size=10 +-- 3. GET /api/v1/reviews/my?userId=1&sort=RATING&size=10 +-- 4. POST /auth/signup with term agreement test data +-- +-- Dummy IDs start from 1 for local signup and API testing. + +-- Term agreement dummy data for POST /auth/signup. +-- Use termId 1 and 2 as required terms in signup requests. +INSERT IGNORE INTO term (id, title, content, is_required, version, created_at, updated_at) VALUES +(1, '서비스 이용약관', '서비스 이용약관 내용입니다.', TRUE, '1.0', NOW(), NOW()), +(2, '개인정보 처리방침', '개인정보 처리방침 내용입니다.', TRUE, '1.0', NOW(), NOW()), +(3, '위치정보 이용약관', '위치정보 이용약관 내용입니다.', FALSE, '1.0', NOW(), NOW()), +(4, '마케팅 정보 수신 동의', '마케팅 정보 수신 동의 내용입니다.', FALSE, '1.0', NOW(), NOW()); + +INSERT IGNORE INTO region (id, name, created_at, updated_at) VALUES +(1, '테스트 지역', NOW(), NOW()); + +INSERT IGNORE INTO food_category (id, name, created_at, updated_at) VALUES +(1, '한식', NOW(), NOW()); + +INSERT IGNORE INTO `user` +(id, email, social_type, social_id, name, role, status, step, total_point, gender, birth, base_address, detail_address, phone_number, is_verified, profile_image_key, created_at, updated_at) +VALUES +(1, 'mission-review-user@example.com', 'GOOGLE', 'dummy-user-1', '미션리뷰테스터', 'USER', 'ACTIVE', 4, 25000, 'NONE', '1998-01-01', '서울시 테스트구', '101동 1001호', '010-9000-0001', TRUE, 'profiles/dummy-user-1.png', NOW(), NOW()), +(2, 'dummy-owner@example.com', 'GOOGLE', 'dummy-owner-2', '테스트사장님', 'OWNER', 'ACTIVE', 4, 0, 'NONE', '1988-01-01', '서울시 테스트구', '상가 1층', '010-9000-0002', TRUE, 'profiles/dummy-owner-2.png', NOW(), NOW()); + +INSERT IGNORE INTO owner (id, user_id, business_number, is_verified, created_at, updated_at) VALUES +(1, 2, '900-00-00001', TRUE, NOW(), NOW()); + +INSERT IGNORE INTO store +(id, name, address, latitude, longitude, phone_number, owner_id, region_id, food_category_id) +VALUES +(1, '테스트 식당 1호점', '서울시 테스트구 더미로 1', 37.50000000, 127.00000000, '02-9000-0001', 1, 1, 1), +(2, '테스트 식당 2호점', '서울시 테스트구 더미로 2', 37.50100000, 127.00100000, '02-9000-0002', 1, 1, 1), +(3, '테스트 식당 3호점', '서울시 테스트구 더미로 3', 37.50200000, 127.00200000, '02-9000-0003', 1, 1, 1), +(4, '테스트 식당 4호점', '서울시 테스트구 더미로 4', 37.50300000, 127.00300000, '02-9000-0004', 1, 1, 1), +(5, '테스트 식당 5호점', '서울시 테스트구 더미로 5', 37.50400000, 127.00400000, '02-9000-0005', 1, 1, 1); + +INSERT IGNORE INTO mission +(id, store_id, content, reward_point, status, deadline, end_at, created_at, updated_at) +VALUES +(1, 1, '테스트 식당 1호점에서 10000원 이상 식사하기', 500, TRUE, 14, '2026-06-01 23:59:59', '2026-05-01 10:01:00', '2026-05-01 10:01:00'), +(2, 2, '테스트 식당 2호점에서 대표 메뉴 주문하기', 600, TRUE, 14, '2026-06-02 23:59:59', '2026-05-01 10:02:00', '2026-05-01 10:02:00'), +(3, 3, '테스트 식당 3호점에서 포장 주문하기', 700, TRUE, 14, '2026-06-03 23:59:59', '2026-05-01 10:03:00', '2026-05-01 10:03:00'), +(4, 4, '테스트 식당 4호점에서 점심 방문하기', 800, TRUE, 14, '2026-06-04 23:59:59', '2026-05-01 10:04:00', '2026-05-01 10:04:00'), +(5, 5, '테스트 식당 5호점에서 저녁 방문하기', 900, TRUE, 14, '2026-06-05 23:59:59', '2026-05-01 10:05:00', '2026-05-01 10:05:00'), +(6, 1, '테스트 식당 1호점에서 음료 함께 주문하기', 500, TRUE, 14, '2026-06-06 23:59:59', '2026-05-01 10:06:00', '2026-05-01 10:06:00'), +(7, 2, '테스트 식당 2호점에서 2인 이상 방문하기', 600, TRUE, 14, '2026-06-07 23:59:59', '2026-05-01 10:07:00', '2026-05-01 10:07:00'), +(8, 3, '테스트 식당 3호점에서 신메뉴 주문하기', 700, TRUE, 14, '2026-06-08 23:59:59', '2026-05-01 10:08:00', '2026-05-01 10:08:00'), +(9, 4, '테스트 식당 4호점에서 세트 메뉴 주문하기', 800, TRUE, 14, '2026-06-09 23:59:59', '2026-05-01 10:09:00', '2026-05-01 10:09:00'), +(10, 5, '테스트 식당 5호점에서 매장 식사하기', 900, TRUE, 14, '2026-06-10 23:59:59', '2026-05-01 10:10:00', '2026-05-01 10:10:00'), +(11, 1, '테스트 식당 1호점에서 사이드 메뉴 주문하기', 500, TRUE, 14, '2026-06-11 23:59:59', '2026-05-01 10:11:00', '2026-05-01 10:11:00'), +(12, 2, '테스트 식당 2호점에서 영수증 첨부하기', 600, TRUE, 14, '2026-06-12 23:59:59', '2026-05-01 10:12:00', '2026-05-01 10:12:00'), +(13, 3, '테스트 식당 3호점에서 영수증 인증하기', 700, TRUE, 14, '2026-06-13 23:59:59', '2026-05-01 10:13:00', '2026-05-01 10:13:00'), +(14, 4, '테스트 식당 4호점에서 추천 메뉴 주문하기', 800, TRUE, 14, '2026-06-14 23:59:59', '2026-05-01 10:14:00', '2026-05-01 10:14:00'), +(15, 5, '테스트 식당 5호점에서 15000원 이상 식사하기', 900, TRUE, 14, '2026-06-15 23:59:59', '2026-05-01 10:15:00', '2026-05-01 10:15:00'), +(16, 1, '테스트 식당 1호점에서 주말 방문하기', 500, TRUE, 14, '2026-06-16 23:59:59', '2026-05-01 10:16:00', '2026-05-01 10:16:00'), +(17, 2, '테스트 식당 2호점에서 사진 미션 완료하기', 600, TRUE, 14, '2026-06-17 23:59:59', '2026-05-01 10:17:00', '2026-05-01 10:17:00'), +(18, 3, '테스트 식당 3호점에서 함께 미션 완료하기', 700, TRUE, 14, '2026-06-18 23:59:59', '2026-05-01 10:18:00', '2026-05-01 10:18:00'), +(19, 4, '테스트 식당 4호점에서 쿠폰 사용하기', 800, TRUE, 14, '2026-06-19 23:59:59', '2026-05-01 10:19:00', '2026-05-01 10:19:00'), +(20, 5, '테스트 식당 5호점에서 친구와 방문하기', 900, TRUE, 14, '2026-06-20 23:59:59', '2026-05-01 10:20:00', '2026-05-01 10:20:00'), +(21, 1, '테스트 식당 1호점에서 오늘의 메뉴 주문하기', 500, TRUE, 14, '2026-06-21 23:59:59', '2026-05-01 10:21:00', '2026-05-01 10:21:00'), +(22, 2, '테스트 식당 2호점에서 인기 메뉴 주문하기', 600, TRUE, 14, '2026-06-22 23:59:59', '2026-05-01 10:22:00', '2026-05-01 10:22:00'), +(23, 3, '테스트 식당 3호점에서 배달 대신 방문하기', 700, TRUE, 14, '2026-06-23 23:59:59', '2026-05-01 10:23:00', '2026-05-01 10:23:00'), +(24, 4, '테스트 식당 4호점에서 디저트 주문하기', 800, TRUE, 14, '2026-06-24 23:59:59', '2026-05-01 10:24:00', '2026-05-01 10:24:00'), +(25, 5, '테스트 식당 5호점에서 마지막 미션 완료하기', 900, TRUE, 14, '2026-06-25 23:59:59', '2026-05-01 10:25:00', '2026-05-01 10:25:00'); + +INSERT IGNORE INTO user_mission +(id, status, code, expires_at, verified_at, mission_id, user_id, created_at, updated_at) +VALUES +(1, 'SUCCESS', 'DUMMY0001', '2026-06-01 23:59:59', '2026-05-02 11:01:00', 1, 1, '2026-05-02 11:01:00', '2026-05-02 11:01:00'), +(2, 'SUCCESS', 'DUMMY0002', '2026-06-02 23:59:59', '2026-05-02 11:02:00', 2, 1, '2026-05-02 11:02:00', '2026-05-02 11:02:00'), +(3, 'SUCCESS', 'DUMMY0003', '2026-06-03 23:59:59', '2026-05-02 11:03:00', 3, 1, '2026-05-02 11:03:00', '2026-05-02 11:03:00'), +(4, 'SUCCESS', 'DUMMY0004', '2026-06-04 23:59:59', '2026-05-02 11:04:00', 4, 1, '2026-05-02 11:04:00', '2026-05-02 11:04:00'), +(5, 'SUCCESS', 'DUMMY0005', '2026-06-05 23:59:59', '2026-05-02 11:05:00', 5, 1, '2026-05-02 11:05:00', '2026-05-02 11:05:00'), +(6, 'SUCCESS', 'DUMMY0006', '2026-06-06 23:59:59', '2026-05-02 11:06:00', 6, 1, '2026-05-02 11:06:00', '2026-05-02 11:06:00'), +(7, 'SUCCESS', 'DUMMY0007', '2026-06-07 23:59:59', '2026-05-02 11:07:00', 7, 1, '2026-05-02 11:07:00', '2026-05-02 11:07:00'), +(8, 'SUCCESS', 'DUMMY0008', '2026-06-08 23:59:59', '2026-05-02 11:08:00', 8, 1, '2026-05-02 11:08:00', '2026-05-02 11:08:00'), +(9, 'SUCCESS', 'DUMMY0009', '2026-06-09 23:59:59', '2026-05-02 11:09:00', 9, 1, '2026-05-02 11:09:00', '2026-05-02 11:09:00'), +(10, 'SUCCESS', 'DUMMY0010', '2026-06-10 23:59:59', '2026-05-02 11:10:00', 10, 1, '2026-05-02 11:10:00', '2026-05-02 11:10:00'), +(11, 'SUCCESS', 'DUMMY0011', '2026-06-11 23:59:59', '2026-05-02 11:11:00', 11, 1, '2026-05-02 11:11:00', '2026-05-02 11:11:00'), +(12, 'SUCCESS', 'DUMMY0012', '2026-06-12 23:59:59', '2026-05-02 11:12:00', 12, 1, '2026-05-02 11:12:00', '2026-05-02 11:12:00'), +(13, 'SUCCESS', 'DUMMY0013', '2026-06-13 23:59:59', '2026-05-02 11:13:00', 13, 1, '2026-05-02 11:13:00', '2026-05-02 11:13:00'), +(14, 'SUCCESS', 'DUMMY0014', '2026-06-14 23:59:59', '2026-05-02 11:14:00', 14, 1, '2026-05-02 11:14:00', '2026-05-02 11:14:00'), +(15, 'SUCCESS', 'DUMMY0015', '2026-06-15 23:59:59', '2026-05-02 11:15:00', 15, 1, '2026-05-02 11:15:00', '2026-05-02 11:15:00'), +(16, 'SUCCESS', 'DUMMY0016', '2026-06-16 23:59:59', '2026-05-02 11:16:00', 16, 1, '2026-05-02 11:16:00', '2026-05-02 11:16:00'), +(17, 'SUCCESS', 'DUMMY0017', '2026-06-17 23:59:59', '2026-05-02 11:17:00', 17, 1, '2026-05-02 11:17:00', '2026-05-02 11:17:00'), +(18, 'SUCCESS', 'DUMMY0018', '2026-06-18 23:59:59', '2026-05-02 11:18:00', 18, 1, '2026-05-02 11:18:00', '2026-05-02 11:18:00'), +(19, 'SUCCESS', 'DUMMY0019', '2026-06-19 23:59:59', '2026-05-02 11:19:00', 19, 1, '2026-05-02 11:19:00', '2026-05-02 11:19:00'), +(20, 'SUCCESS', 'DUMMY0020', '2026-06-20 23:59:59', '2026-05-02 11:20:00', 20, 1, '2026-05-02 11:20:00', '2026-05-02 11:20:00'), +(21, 'SUCCESS', 'DUMMY0021', '2026-06-21 23:59:59', '2026-05-02 11:21:00', 21, 1, '2026-05-02 11:21:00', '2026-05-02 11:21:00'), +(22, 'SUCCESS', 'DUMMY0022', '2026-06-22 23:59:59', '2026-05-02 11:22:00', 22, 1, '2026-05-02 11:22:00', '2026-05-02 11:22:00'), +(23, 'SUCCESS', 'DUMMY0023', '2026-06-23 23:59:59', '2026-05-02 11:23:00', 23, 1, '2026-05-02 11:23:00', '2026-05-02 11:23:00'), +(24, 'SUCCESS', 'DUMMY0024', '2026-06-24 23:59:59', '2026-05-02 11:24:00', 24, 1, '2026-05-02 11:24:00', '2026-05-02 11:24:00'), +(25, 'SUCCESS', 'DUMMY0025', '2026-06-25 23:59:59', '2026-05-02 11:25:00', 25, 1, '2026-05-02 11:25:00', '2026-05-02 11:25:00'); + +INSERT IGNORE INTO review +(id, rating, content, user_mission_id, user_id, created_at, updated_at) +VALUES +(1, 4.1, '더미 리뷰 1번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 1, 1, '2026-05-03 12:01:00', '2026-05-03 12:01:00'), +(2, 4.2, '더미 리뷰 2번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 2, 1, '2026-05-03 12:02:00', '2026-05-03 12:02:00'), +(3, 4.3, '더미 리뷰 3번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 3, 1, '2026-05-03 12:03:00', '2026-05-03 12:03:00'), +(4, 4.4, '더미 리뷰 4번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 4, 1, '2026-05-03 12:04:00', '2026-05-03 12:04:00'), +(5, 4.5, '더미 리뷰 5번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 5, 1, '2026-05-03 12:05:00', '2026-05-03 12:05:00'), +(6, 3.6, '더미 리뷰 6번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 6, 1, '2026-05-03 12:06:00', '2026-05-03 12:06:00'), +(7, 3.7, '더미 리뷰 7번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 7, 1, '2026-05-03 12:07:00', '2026-05-03 12:07:00'), +(8, 3.8, '더미 리뷰 8번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 8, 1, '2026-05-03 12:08:00', '2026-05-03 12:08:00'), +(9, 3.9, '더미 리뷰 9번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 9, 1, '2026-05-03 12:09:00', '2026-05-03 12:09:00'), +(10, 4.0, '더미 리뷰 10번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 10, 1, '2026-05-03 12:10:00', '2026-05-03 12:10:00'), +(11, 4.6, '더미 리뷰 11번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 11, 1, '2026-05-03 12:11:00', '2026-05-03 12:11:00'), +(12, 4.7, '더미 리뷰 12번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 12, 1, '2026-05-03 12:12:00', '2026-05-03 12:12:00'), +(13, 4.8, '더미 리뷰 13번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 13, 1, '2026-05-03 12:13:00', '2026-05-03 12:13:00'), +(14, 4.9, '더미 리뷰 14번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 14, 1, '2026-05-03 12:14:00', '2026-05-03 12:14:00'), +(15, 5.0, '더미 리뷰 15번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 15, 1, '2026-05-03 12:15:00', '2026-05-03 12:15:00'), +(16, 3.1, '더미 리뷰 16번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 16, 1, '2026-05-03 12:16:00', '2026-05-03 12:16:00'), +(17, 3.2, '더미 리뷰 17번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 17, 1, '2026-05-03 12:17:00', '2026-05-03 12:17:00'), +(18, 3.3, '더미 리뷰 18번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 18, 1, '2026-05-03 12:18:00', '2026-05-03 12:18:00'), +(19, 3.4, '더미 리뷰 19번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 19, 1, '2026-05-03 12:19:00', '2026-05-03 12:19:00'), +(20, 3.5, '더미 리뷰 20번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 20, 1, '2026-05-03 12:20:00', '2026-05-03 12:20:00'), +(21, 2.6, '더미 리뷰 21번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 21, 1, '2026-05-03 12:21:00', '2026-05-03 12:21:00'), +(22, 2.7, '더미 리뷰 22번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 22, 1, '2026-05-03 12:22:00', '2026-05-03 12:22:00'), +(23, 2.8, '더미 리뷰 23번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 23, 1, '2026-05-03 12:23:00', '2026-05-03 12:23:00'), +(24, 2.9, '더미 리뷰 24번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 24, 1, '2026-05-03 12:24:00', '2026-05-03 12:24:00'), +(25, 3.0, '더미 리뷰 25번입니다. 리뷰 조회 페이지 테스트용 데이터입니다.', 25, 1, '2026-05-03 12:25:00', '2026-05-03 12:25:00'); + +INSERT IGNORE INTO review_image +(id, review_id, image_key, sequence, created_at, updated_at) +VALUES +(1, 1, 'reviews/dummy/review-001-1.jpg', 1, '2026-05-03 12:01:10', '2026-05-03 12:01:10'), +(2, 1, 'reviews/dummy/review-001-2.jpg', 2, '2026-05-03 12:01:20', '2026-05-03 12:01:20'), +(3, 2, 'reviews/dummy/review-002-1.jpg', 1, '2026-05-03 12:02:10', '2026-05-03 12:02:10'), +(4, 2, 'reviews/dummy/review-002-2.jpg', 2, '2026-05-03 12:02:20', '2026-05-03 12:02:20'), +(5, 3, 'reviews/dummy/review-003-1.jpg', 1, '2026-05-03 12:03:10', '2026-05-03 12:03:10'), +(6, 4, 'reviews/dummy/review-004-1.jpg', 1, '2026-05-03 12:04:10', '2026-05-03 12:04:10'), +(7, 5, 'reviews/dummy/review-005-1.jpg', 1, '2026-05-03 12:05:10', '2026-05-03 12:05:10'), +(8, 6, 'reviews/dummy/review-006-1.jpg', 1, '2026-05-03 12:06:10', '2026-05-03 12:06:10'), +(9, 7, 'reviews/dummy/review-007-1.jpg', 1, '2026-05-03 12:07:10', '2026-05-03 12:07:10'), +(10, 8, 'reviews/dummy/review-008-1.jpg', 1, '2026-05-03 12:08:10', '2026-05-03 12:08:10'), +(11, 9, 'reviews/dummy/review-009-1.jpg', 1, '2026-05-03 12:09:10', '2026-05-03 12:09:10'), +(12, 10, 'reviews/dummy/review-010-1.jpg', 1, '2026-05-03 12:10:10', '2026-05-03 12:10:10'), +(13, 11, 'reviews/dummy/review-011-1.jpg', 1, '2026-05-03 12:11:10', '2026-05-03 12:11:10'), +(14, 12, 'reviews/dummy/review-012-1.jpg', 1, '2026-05-03 12:12:10', '2026-05-03 12:12:10'), +(15, 13, 'reviews/dummy/review-013-1.jpg', 1, '2026-05-03 12:13:10', '2026-05-03 12:13:10'), +(16, 14, 'reviews/dummy/review-014-1.jpg', 1, '2026-05-03 12:14:10', '2026-05-03 12:14:10'), +(17, 15, 'reviews/dummy/review-015-1.jpg', 1, '2026-05-03 12:15:10', '2026-05-03 12:15:10'), +(18, 16, 'reviews/dummy/review-016-1.jpg', 1, '2026-05-03 12:16:10', '2026-05-03 12:16:10'), +(19, 17, 'reviews/dummy/review-017-1.jpg', 1, '2026-05-03 12:17:10', '2026-05-03 12:17:10'), +(20, 18, 'reviews/dummy/review-018-1.jpg', 1, '2026-05-03 12:18:10', '2026-05-03 12:18:10'), +(21, 19, 'reviews/dummy/review-019-1.jpg', 1, '2026-05-03 12:19:10', '2026-05-03 12:19:10'), +(22, 20, 'reviews/dummy/review-020-1.jpg', 1, '2026-05-03 12:20:10', '2026-05-03 12:20:10'), +(23, 21, 'reviews/dummy/review-021-1.jpg', 1, '2026-05-03 12:21:10', '2026-05-03 12:21:10'), +(24, 22, 'reviews/dummy/review-022-1.jpg', 1, '2026-05-03 12:22:10', '2026-05-03 12:22:10'), +(25, 23, 'reviews/dummy/review-023-1.jpg', 1, '2026-05-03 12:23:10', '2026-05-03 12:23:10'), +(26, 24, 'reviews/dummy/review-024-1.jpg', 1, '2026-05-03 12:24:10', '2026-05-03 12:24:10'), +(27, 25, 'reviews/dummy/review-025-1.jpg', 1, '2026-05-03 12:25:10', '2026-05-03 12:25:10'); + +INSERT IGNORE INTO review_answer +(id, review_id, owner_id, content, created_at, updated_at) +VALUES +(1, 1, 1, '방문해 주셔서 감사합니다. 더 좋은 서비스로 보답하겠습니다.', '2026-05-04 09:01:00', '2026-05-04 09:01:00'), +(2, 3, 1, '소중한 리뷰 감사합니다. 다음 방문도 기다리겠습니다.', '2026-05-04 09:03:00', '2026-05-04 09:03:00'), +(3, 5, 1, '리뷰 남겨주셔서 감사합니다.', '2026-05-04 09:05:00', '2026-05-04 09:05:00'), +(4, 7, 1, '좋은 평가 감사합니다. 맛과 위생을 계속 챙기겠습니다.', '2026-05-04 09:07:00', '2026-05-04 09:07:00'), +(5, 9, 1, '방문 후기 감사합니다.', '2026-05-04 09:09:00', '2026-05-04 09:09:00'), +(6, 11, 1, '정성스러운 리뷰 감사합니다.', '2026-05-04 09:11:00', '2026-05-04 09:11:00'), +(7, 13, 1, '다음에도 만족하실 수 있도록 준비하겠습니다.', '2026-05-04 09:13:00', '2026-05-04 09:13:00'), +(8, 15, 1, '최고 평점 감사합니다.', '2026-05-04 09:15:00', '2026-05-04 09:15:00'), +(9, 17, 1, '의견 감사합니다. 더 개선하겠습니다.', '2026-05-04 09:17:00', '2026-05-04 09:17:00'), +(10, 19, 1, '방문해 주셔서 감사합니다.', '2026-05-04 09:19:00', '2026-05-04 09:19:00'), +(11, 21, 1, '리뷰 확인했습니다. 감사합니다.', '2026-05-04 09:21:00', '2026-05-04 09:21:00'), +(12, 23, 1, '소중한 피드백 감사합니다.', '2026-05-04 09:23:00', '2026-05-04 09:23:00'), +(13, 25, 1, '마지막 테스트 리뷰 답글입니다.', '2026-05-04 09:25:00', '2026-05-04 09:25:00'); From 171ce26da250c77cf88255321be5560c69523f6f Mon Sep 17 00:00:00 2001 From: yangjiae12 Date: Wed, 20 May 2026 17:02:00 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=EC=84=A0=ED=98=B8=20=EC=9D=8C?= =?UTF-8?q?=EC=8B=9D=20=EC=84=A0=ED=83=9D=EC=9D=84=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20ID=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/dto/AuthRequestDto.java | 8 ++--- .../auth/exception/code/AuthErrorCode.java | 1 + .../domain/auth/service/AuthServiceImpl.java | 31 ++++++++++--------- .../resources/dummy-my-mission-review.sql | 13 +++++++- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java index b80004f..1878961 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/dto/AuthRequestDto.java @@ -3,9 +3,9 @@ import com.example.umc10th.domain.user.entity.enums.Gender; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import java.time.LocalDate; @@ -34,9 +34,9 @@ public record SignUpDto( @NotBlank(message = "주소는 필수입니다.") String address, - @NotNull(message = "선호 음식 종류는 필수입니다.") - @NotEmpty(message = "선호 음식 종류는 1개 이상 선택해야 합니다.") - List<@NotBlank(message = "선호 음식 이름은 비어 있을 수 없습니다.") String> preferredFoods, + @NotNull(message = "선호 음식 카테고리는 필수입니다.") + @NotEmpty(message = "선호 음식 카테고리는 1개 이상 선택해야 합니다.") + List<@NotNull(message = "선호 음식 카테고리 ID는 필수입니다.") Long> preferredFoodCategoryIds, @NotNull(message = "약관 동의 목록은 필수입니다.") @NotEmpty(message = "약관 동의 목록은 1개 이상이어야 합니다.") diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java index d53cf28..8a66ed9 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java @@ -9,6 +9,7 @@ @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", "필수 약관에 동의해야 합니다.") ; diff --git a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java index e2871d4..bae9aa3 100644 --- a/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java +++ b/naru/umc10th/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java @@ -54,24 +54,16 @@ public AuthResponseDto.SignUpResultDto signUp(AuthRequestDto.SignUpDto request) validateRequiredTerms(agreementMap); List requestedTerms = getRequestedTerms(agreementMap.keySet()); + List preferredFoodCategories = getPreferredFoodCategories(request.preferredFoodCategoryIds()); User user = AuthConverter.toUser(request, passwordEncoder.encode(request.password())); User savedUser = userRepository.save(user); - List userFoodCategories = request.preferredFoods().stream() - .distinct() - .map(foodName -> { - FoodCategory foodCategory = foodCategoryRepository.findByName(foodName) - .orElseGet(() -> foodCategoryRepository.save( - FoodCategory.builder() - .name(foodName) - .build() - )); - return UserFoodCategory.builder() - .user(savedUser) - .foodCategory(foodCategory) - .build(); - }) + List userFoodCategories = preferredFoodCategories.stream() + .map(foodCategory -> UserFoodCategory.builder() + .user(savedUser) + .foodCategory(foodCategory) + .build()) .toList(); userFoodCategoryRepository.saveAll(userFoodCategories); @@ -100,6 +92,17 @@ private List getRequestedTerms(Set termIds) { return terms; } + private List getPreferredFoodCategories(List foodCategoryIds) { + Set uniqueFoodCategoryIds = Set.copyOf(foodCategoryIds); + List 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 terms, Map agreementMap) { List userAgreements = terms.stream() .map(term -> UserAgreement.builder() diff --git a/naru/umc10th/src/main/resources/dummy-my-mission-review.sql b/naru/umc10th/src/main/resources/dummy-my-mission-review.sql index d09f415..8361276 100644 --- a/naru/umc10th/src/main/resources/dummy-my-mission-review.sql +++ b/naru/umc10th/src/main/resources/dummy-my-mission-review.sql @@ -18,7 +18,18 @@ INSERT IGNORE INTO region (id, name, created_at, updated_at) VALUES (1, '테스트 지역', NOW(), NOW()); INSERT IGNORE INTO food_category (id, name, created_at, updated_at) VALUES -(1, '한식', NOW(), NOW()); +(1, '한식', NOW(), NOW()), +(2, '일식', NOW(), NOW()), +(3, '중식', NOW(), NOW()), +(4, '양식', NOW(), NOW()), +(5, '치킨', NOW(), NOW()), +(6, '분식', NOW(), NOW()), +(7, '고기/구이', NOW(), NOW()), +(8, '도시락', NOW(), NOW()), +(9, '야식', NOW(), NOW()), +(10, '패스트푸드', NOW(), NOW()), +(11, '디저트', NOW(), NOW()), +(12, '아시안푸드', NOW(), NOW()); INSERT IGNORE INTO `user` (id, email, social_type, social_id, name, role, status, step, total_point, gender, birth, base_address, detail_address, phone_number, is_verified, profile_image_key, created_at, updated_at)