diff --git a/build.gradle b/build.gradle index c8423068..a69fc1a9 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,18 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // spring security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // thymeleaf + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE' + // validation implementation 'org.springframework.boot:spring-boot-starter-validation' diff --git a/src/main/java/umc/apiPayload/code/status/ErrorStatus.java b/src/main/java/umc/apiPayload/code/status/ErrorStatus.java index 60689069..16082235 100644 --- a/src/main/java/umc/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/umc/apiPayload/code/status/ErrorStatus.java @@ -16,10 +16,13 @@ public enum ErrorStatus implements BaseErrorCode { _UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."), _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + // 인증 관련 에러 + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "COMMON401", "토큰이 틀립니다."), // 멤버 관려 에러 USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자가 없습니다."), NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4003", "비밀번호가 틀립니다"), // 식당 관련 에러 RESTAURANT_NOT_FOUND(HttpStatus.BAD_REQUEST, "RESTAURANT_4001", "식당이 없습니다."), @@ -33,6 +36,7 @@ public enum ErrorStatus implements BaseErrorCode { // 미션 관련 에러 MISSION_NOT_FOUND(HttpStatus.BAD_REQUEST, "MISSION4001", "미션이 존재하지 않습니다"), MISSION_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MISSION4002", "해당 미션을 수행 중 또는 수행 완료했습니다."), + USERMISSION_NOT_FOUND(HttpStatus.BAD_REQUEST, "MISSION4003", "유저 미션이 존재하지 않습니다."), // For test TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"), diff --git a/src/main/java/umc/apiPayload/exception/handler/UserHandler.java b/src/main/java/umc/apiPayload/exception/handler/UserHandler.java new file mode 100644 index 00000000..3e678ffa --- /dev/null +++ b/src/main/java/umc/apiPayload/exception/handler/UserHandler.java @@ -0,0 +1,9 @@ +package umc.apiPayload.exception.handler; + +import umc.apiPayload.code.BaseErrorCode; +import umc.apiPayload.exception.GeneralException; + +public class UserHandler extends GeneralException { + + public UserHandler(BaseErrorCode errorCode) {super(errorCode);} +} diff --git a/src/main/java/umc/config/properties/Constants.java b/src/main/java/umc/config/properties/Constants.java new file mode 100644 index 00000000..eac31927 --- /dev/null +++ b/src/main/java/umc/config/properties/Constants.java @@ -0,0 +1,6 @@ +package umc.config.properties; + +public final class Constants { + public static final String AUTH_HEADER = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; +} \ No newline at end of file diff --git a/src/main/java/umc/config/properties/JwtProperties.java b/src/main/java/umc/config/properties/JwtProperties.java new file mode 100644 index 00000000..1f4c01f7 --- /dev/null +++ b/src/main/java/umc/config/properties/JwtProperties.java @@ -0,0 +1,22 @@ +package umc.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@ConfigurationProperties("jwt.token") +public class JwtProperties { + private String secretKey=""; + private Expiration expiration; + + @Getter + @Setter + public static class Expiration{ + private Long access; + // TODO: refreshToken + } +} \ No newline at end of file diff --git a/src/main/java/umc/config/security/CustomUserDetailsService.java b/src/main/java/umc/config/security/CustomUserDetailsService.java new file mode 100644 index 00000000..feb252fb --- /dev/null +++ b/src/main/java/umc/config/security/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +package umc.config.security; + +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; +import umc.domain.User; +import umc.repository.user.UserRepository; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("해당 이메일을 가진 유저가 존재하지 않습니다: " + username)); + + return org.springframework.security.core.userdetails.User + .withUsername(user.getEmail()) + .password(user.getPassword()) + .roles(user.getRole().name()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/umc/config/security/SecurityConfig.java b/src/main/java/umc/config/security/SecurityConfig.java new file mode 100644 index 00000000..342afb28 --- /dev/null +++ b/src/main/java/umc/config/security/SecurityConfig.java @@ -0,0 +1,67 @@ +package umc.config.security; + +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.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import umc.config.security.jwt.JwtAuthenticationFilter; +import umc.config.security.jwt.JwtTokenProvider; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + /* @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + + http + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests( + (requests) -> requests + .requestMatchers("/", "/users/join", "/users/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.disable()) + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + }*/ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/", "/home", "/signup","/users/signup", "/css/**").permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .formLogin((form) -> form + .loginPage("/login") + .defaultSuccessUrl("/home", true) + .permitAll() + ) + .logout((logout) -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/umc/config/security/jwt/JwtAuthenticationFilter.java b/src/main/java/umc/config/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 00000000..e3c454d9 --- /dev/null +++ b/src/main/java/umc/config/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,44 @@ +package umc.config.security.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import umc.config.properties.Constants; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String token = resolveToken(request); + + if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(Constants.AUTH_HEADER); + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } +} + diff --git a/src/main/java/umc/config/security/jwt/JwtTokenProvider.java b/src/main/java/umc/config/security/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..72eb07cd --- /dev/null +++ b/src/main/java/umc/config/security/jwt/JwtTokenProvider.java @@ -0,0 +1,85 @@ +package umc.config.security.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import umc.apiPayload.code.status.ErrorStatus; +import umc.apiPayload.exception.handler.UserHandler; +import umc.config.properties.Constants; +import umc.config.properties.JwtProperties; + +import java.security.Key; +import java.util.Date; +import java.util.Collections; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final JwtProperties jwtProperties; + + private Key getSigningKey() { + return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes()); + } + + public String generateToken(Authentication authentication) { + String email = authentication.getName(); + + return Jwts.builder() + .setSubject(email) + .claim("role", authentication.getAuthorities().iterator().next().getAuthority()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration().getAccess())) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + String email = claims.getSubject(); + String role = claims.get("role", String.class); + + User principal = new User(email, "", Collections.singleton(() -> role)); + return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); + } + + public static String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(Constants.AUTH_HEADER); + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } + + public Authentication extractAuthentication(HttpServletRequest request){ + String accessToken = resolveToken(request); + if(accessToken == null || !validateToken(accessToken)) { + throw new UserHandler(ErrorStatus.INVALID_TOKEN); + } + return getAuthentication(accessToken); + } +} + diff --git a/src/main/java/umc/converter/MissionConverter.java b/src/main/java/umc/converter/MissionConverter.java index bbbd2a75..583105d4 100644 --- a/src/main/java/umc/converter/MissionConverter.java +++ b/src/main/java/umc/converter/MissionConverter.java @@ -1,10 +1,19 @@ package umc.converter; +import org.springframework.data.domain.Page; +import umc.converter.user.UserMissionConverter; import umc.domain.Location; import umc.domain.Mission; import umc.domain.Restaurant; +import umc.domain.Review; +import umc.domain.mapping.UserMission; import umc.web.dto.mission.MissionRequestDTO; +import umc.web.dto.mission.MissionResponseDTO; import umc.web.dto.restaurant.RestaurantRequestDTO; +import umc.web.dto.review.ReviewResponseDTO; + +import java.util.List; +import java.util.stream.Collectors; public class MissionConverter { @@ -18,4 +27,59 @@ public static Mission toMission(MissionRequestDTO.createMissionDTO request, Rest .location(restaurant.getLocation()) .build(); } + + public static MissionResponseDTO.MissionDTO toMissionDTO(Mission mission){ + + return MissionResponseDTO.MissionDTO.builder() + .missionName(mission.getName()) + .contents(mission.getContents()) + .points(mission.getPoints()) + .restaurantId(mission.getRestaurant().getId()) + .createdAt(mission.getCreatedAt().toLocalDate()) + .build(); + } + + public static MissionResponseDTO.MissionDTO toMissionDTO(UserMission userMission){ + + return MissionResponseDTO.MissionDTO.builder() + .missionName(userMission.getMission().getName()) + .contents(userMission.getMission().getContents()) + .points(userMission.getMission().getPoints()) + .restaurantId(userMission.getMission().getRestaurant().getId()) + .createdAt(userMission.getMission().getCreatedAt().toLocalDate()) + .build(); + } + + + + public static MissionResponseDTO.MissionListDTO toMissionListDTO(Page missionList){ + + List missionDTOList = missionList.stream() + .map(MissionConverter::toMissionDTO).collect(Collectors.toList()); + + return MissionResponseDTO.MissionListDTO.builder() + .isLast(missionList.isLast()) + .isFirst(missionList.isFirst()) + .totalPage(missionList.getTotalPages()) + .totalElements(missionList.getTotalElements()) + .listSize(missionDTOList.size()) + .missionDTOList(missionDTOList) + .build(); + } + + public static MissionResponseDTO.MissionListDTO toUserMissionListDTO(Page userMissionList) { + + List userMissionDTOList = userMissionList.stream() + .map(MissionConverter::toMissionDTO).collect(Collectors.toList()); + + return MissionResponseDTO.MissionListDTO.builder() + .isLast(userMissionList.isLast()) + .isFirst(userMissionList.isFirst()) + .totalPage(userMissionList.getTotalPages()) + .totalElements(userMissionList.getTotalElements()) + .listSize(userMissionDTOList.size()) + .missionDTOList(userMissionDTOList) + .build(); + + } } diff --git a/src/main/java/umc/converter/ReviewConverter.java b/src/main/java/umc/converter/ReviewConverter.java index 13238743..3380a81a 100644 --- a/src/main/java/umc/converter/ReviewConverter.java +++ b/src/main/java/umc/converter/ReviewConverter.java @@ -1,11 +1,16 @@ package umc.converter; +import org.springframework.data.domain.Page; import umc.domain.Location; import umc.domain.Restaurant; import umc.domain.Review; import umc.domain.User; import umc.web.dto.restaurant.RestaurantRequestDTO; import umc.web.dto.review.ReviewRequestDTO; +import umc.web.dto.review.ReviewResponseDTO; + +import java.util.List; +import java.util.stream.Collectors; public class ReviewConverter { @@ -19,4 +24,29 @@ public static Review toReview(ReviewRequestDTO.createReviewDTO reviewDTO, Restau .build(); } + public static ReviewResponseDTO.ReviewPreViewDTO toReviewPreViewDTO(Review review){ + + return ReviewResponseDTO.ReviewPreViewDTO.builder() + .ownerNickname(review.getUser().getNickname()) + .stars(review.getStars()) + .createdAt(review.getCreatedAt().toLocalDate()) + .content(review.getContent()) + .build(); + } + + public static ReviewResponseDTO.ReviewPreViewListDTO toReviewPreViewListDTO(Page reviewList){ + + List reviewPreViewDTOList = reviewList.stream() + .map(ReviewConverter::toReviewPreViewDTO).collect(Collectors.toList()); + + return ReviewResponseDTO.ReviewPreViewListDTO.builder() + .isLast(reviewList.isLast()) + .isFirst(reviewList.isFirst()) + .totalPage(reviewList.getTotalPages()) + .totalElements(reviewList.getTotalElements()) + .listSize(reviewPreViewDTOList.size()) + .reviewList(reviewPreViewDTOList) + .build(); + } + } diff --git a/src/main/java/umc/converter/user/UserConverter.java b/src/main/java/umc/converter/user/UserConverter.java index 6e8ab5f3..51a30079 100644 --- a/src/main/java/umc/converter/user/UserConverter.java +++ b/src/main/java/umc/converter/user/UserConverter.java @@ -1,6 +1,7 @@ package umc.converter.user; import umc.domain.User; +import umc.domain.enums.Role; import umc.domain.enums.Status; import umc.web.dto.user.UserRequestDTO; import umc.web.dto.user.UserResponseDTO; @@ -24,7 +25,7 @@ public static User toUser(UserRequestDTO.JoinDto request){ .password(request.getPassword()) .nickname(request.getNickname()) .phoneNumber(request.getPhoneNumber()) - .role("ROLE_USER") + .role(Role.USER) .birthDate(request.getBirthDate()) .gender(request.getGender()) .status(Status.ACTIVE) @@ -35,4 +36,19 @@ public static User toUser(UserRequestDTO.JoinDto request){ .build(); } + public static UserResponseDTO.LoginResultDTO toLoginResultDTO(Long userId, String accessToken) { + return UserResponseDTO.LoginResultDTO.builder() + .userId(userId) + .accessToken(accessToken) + .build(); + } + + public static UserResponseDTO.UserInfoDTO toUserInfoDTO(User user) { + return UserResponseDTO.UserInfoDTO.builder() + .email(user.getEmail()) + .name(user.getNickname()) + .gender(user.getGender()) + .build(); + } + } diff --git a/src/main/java/umc/domain/User.java b/src/main/java/umc/domain/User.java index 58e534c7..c76d6937 100644 --- a/src/main/java/umc/domain/User.java +++ b/src/main/java/umc/domain/User.java @@ -6,6 +6,7 @@ import org.hibernate.annotations.DynamicUpdate; import umc.domain.common.BaseEntity; import umc.domain.enums.Gender; +import umc.domain.enums.Role; import umc.domain.enums.Status; import umc.domain.mapping.UserMission; import umc.domain.mapping.UserPreference; @@ -40,8 +41,9 @@ public class User extends BaseEntity { @Column private String phoneNumber; + @Enumerated(EnumType.STRING) @Column - private String role; + private Role role; @Column private LocalDateTime birthDate; @@ -75,4 +77,11 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List userPreferenceList = new ArrayList<>(); + /** + * 비즈니스 로직 + * **/ + public void encodePassword(String password) { + this.password = password; + } + } diff --git a/src/main/java/umc/domain/enums/Role.java b/src/main/java/umc/domain/enums/Role.java new file mode 100644 index 00000000..bb2227a1 --- /dev/null +++ b/src/main/java/umc/domain/enums/Role.java @@ -0,0 +1,5 @@ +package umc.domain.enums; + +public enum Role { + ADMIN, USER +} diff --git a/src/main/java/umc/domain/mapping/UserMission.java b/src/main/java/umc/domain/mapping/UserMission.java index de7a5ecc..eb9366b5 100644 --- a/src/main/java/umc/domain/mapping/UserMission.java +++ b/src/main/java/umc/domain/mapping/UserMission.java @@ -28,4 +28,9 @@ public class UserMission extends BaseEntity { @JoinColumn(name = "user_id") private User user; + public void changeIsCompleted(Boolean isCompleted) { + this.isCompleted = isCompleted; + } + + } diff --git a/src/main/java/umc/repository/mission/MissionRepository.java b/src/main/java/umc/repository/mission/MissionRepository.java index e614acde..5522f2c4 100644 --- a/src/main/java/umc/repository/mission/MissionRepository.java +++ b/src/main/java/umc/repository/mission/MissionRepository.java @@ -1,7 +1,16 @@ package umc.repository.mission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import umc.domain.Mission; +import umc.domain.Restaurant; +import umc.domain.User; public interface MissionRepository extends JpaRepository { + + Page findAllByRestaurant(Restaurant restaurant, Pageable pageable); + + } diff --git a/src/main/java/umc/repository/review/ReviewRepository.java b/src/main/java/umc/repository/review/ReviewRepository.java index 791d8d63..e345e78c 100644 --- a/src/main/java/umc/repository/review/ReviewRepository.java +++ b/src/main/java/umc/repository/review/ReviewRepository.java @@ -1,7 +1,19 @@ package umc.repository.review; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import umc.domain.Restaurant; import umc.domain.Review; +import umc.domain.User; public interface ReviewRepository extends JpaRepository { + + Page findAllByRestaurant(Restaurant restaurant, Pageable pageable); + + Page findAllByUser(User user, Pageable pageable); + Slice findReviewsByUser(User user, Pageable pageable); + //Page findAllByRestaurant(Restaurant restaurant, Pageable pageable); + } diff --git a/src/main/java/umc/repository/user/UserMissionRepository.java b/src/main/java/umc/repository/user/UserMissionRepository.java index df285c5b..9a378543 100644 --- a/src/main/java/umc/repository/user/UserMissionRepository.java +++ b/src/main/java/umc/repository/user/UserMissionRepository.java @@ -1,12 +1,36 @@ package umc.repository.user; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import umc.domain.Mission; import umc.domain.User; import umc.domain.mapping.UserMission; +import java.util.Optional; + public interface UserMissionRepository extends JpaRepository { Boolean existsUserMissionByUserAndMission(User user, Mission mission); + @Query( + value = "SELECT um " + + "FROM UserMission um " + + "JOIN FETCH um.mission m " + + "WHERE um.user.id = :userId " + + " AND um.isCompleted = false", + countQuery = "SELECT COUNT(um) " + + "FROM UserMission um " + + "WHERE um.user.id = :userId " + + " AND um.isCompleted = false" + ) + Page findOngoingByUserId( + @Param("userId") Long userId, + Pageable pageable + ); + + Optional findByUserAndMission(User user, Mission mission); + } diff --git a/src/main/java/umc/repository/user/UserRepository.java b/src/main/java/umc/repository/user/UserRepository.java index 15a32725..408fb3b8 100644 --- a/src/main/java/umc/repository/user/UserRepository.java +++ b/src/main/java/umc/repository/user/UserRepository.java @@ -3,7 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import umc.domain.User; -public interface UserRepository extends JpaRepository { +import java.util.Optional; +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/umc/service/mission/MissionCommandService.java b/src/main/java/umc/service/mission/MissionCommandService.java index 8fc99743..34850804 100644 --- a/src/main/java/umc/service/mission/MissionCommandService.java +++ b/src/main/java/umc/service/mission/MissionCommandService.java @@ -5,4 +5,6 @@ public interface MissionCommandService { void createMission(MissionRequestDTO.createMissionDTO request, Long restaurantId); + + void changeMissionStatus(Long userId, Long missionId); } diff --git a/src/main/java/umc/service/mission/MissionCommandServiceImpl.java b/src/main/java/umc/service/mission/MissionCommandServiceImpl.java index a80feee1..a0090fae 100644 --- a/src/main/java/umc/service/mission/MissionCommandServiceImpl.java +++ b/src/main/java/umc/service/mission/MissionCommandServiceImpl.java @@ -8,8 +8,12 @@ import umc.converter.MissionConverter; import umc.domain.Mission; import umc.domain.Restaurant; +import umc.domain.User; +import umc.domain.mapping.UserMission; import umc.repository.mission.MissionRepository; import umc.repository.restaurant.RestaurantRepository; +import umc.repository.user.UserMissionRepository; +import umc.repository.user.UserRepository; import umc.web.dto.mission.MissionRequestDTO; @Service @@ -19,6 +23,8 @@ public class MissionCommandServiceImpl implements MissionCommandService { private final MissionRepository missionRepository; private final RestaurantRepository restaurantRepository; + private final UserMissionRepository userMissionRepository; + private final UserRepository userRepository; @Override public void createMission(MissionRequestDTO.createMissionDTO request, Long restaurantId) { @@ -33,4 +39,28 @@ public void createMission(MissionRequestDTO.createMissionDTO request, Long resta missionRepository.save(mission); } + + @Override + public void changeMissionStatus(Long userId, Long missionId) { + + Mission mission = missionRepository.findById(missionId).orElseThrow( + () -> new GeneralException(ErrorStatus.MISSION_NOT_FOUND) + ); + + User user = userRepository.findById(userId).orElseThrow( + () -> new GeneralException(ErrorStatus.USER_NOT_FOUND) + ); + + UserMission userMission = userMissionRepository.findByUserAndMission(user, mission).orElseThrow( + () -> new GeneralException(ErrorStatus.USERMISSION_NOT_FOUND) + ); + + if(userMission.getIsCompleted()){ + userMission.changeIsCompleted(false); + }else{ + userMission.changeIsCompleted(true); + } + + userMissionRepository.save(userMission); + } } diff --git a/src/main/java/umc/service/mission/MissionQueryService.java b/src/main/java/umc/service/mission/MissionQueryService.java new file mode 100644 index 00000000..3ad8d6de --- /dev/null +++ b/src/main/java/umc/service/mission/MissionQueryService.java @@ -0,0 +1,12 @@ +package umc.service.mission; + +import org.springframework.data.domain.Page; +import umc.domain.Mission; +import umc.domain.mapping.UserMission; + +public interface MissionQueryService { + + Page getMissionListByRestaurantId(Long restaurantId, Integer page); + + Page getMissionListByUserId(Long userId, Integer page); +} diff --git a/src/main/java/umc/service/mission/MissionQueryServiceImpl.java b/src/main/java/umc/service/mission/MissionQueryServiceImpl.java new file mode 100644 index 00000000..ddea3b74 --- /dev/null +++ b/src/main/java/umc/service/mission/MissionQueryServiceImpl.java @@ -0,0 +1,53 @@ +package umc.service.mission; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.apiPayload.code.status.ErrorStatus; +import umc.apiPayload.exception.GeneralException; +import umc.domain.Mission; +import umc.domain.Restaurant; +import umc.domain.User; +import umc.domain.mapping.UserMission; +import umc.repository.mission.MissionRepository; +import umc.repository.restaurant.RestaurantRepository; +import umc.repository.user.UserMissionRepository; +import umc.repository.user.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MissionQueryServiceImpl implements MissionQueryService { + + private final MissionRepository missionRepository; + private final RestaurantRepository restaurantRepository; + private final UserRepository userRepository; + private final UserMissionRepository userMissionRepository; + + @Override + public Page getMissionListByRestaurantId(Long restaurantId, Integer page) { + + Restaurant restaurant = restaurantRepository.findById(restaurantId).orElseThrow( + () -> new GeneralException(ErrorStatus.RESTAURANT_NOT_FOUND) + ); + + Page missionPage = missionRepository.findAllByRestaurant(restaurant, PageRequest.of(page, 10)); + return missionPage; + } + + @Override + public Page getMissionListByUserId(Long userId, Integer page) { + + User user = userRepository.findById(userId).orElseThrow( + () -> new GeneralException(ErrorStatus.USER_NOT_FOUND) + ); + + Page userMissionPage = userMissionRepository.findOngoingByUserId(userId, PageRequest.of(page, 10)); + + return userMissionPage; + } + + +} diff --git a/src/main/java/umc/service/restaurant/RestaurantQueryService.java b/src/main/java/umc/service/restaurant/RestaurantQueryService.java index 6794ac81..ba79b5b1 100644 --- a/src/main/java/umc/service/restaurant/RestaurantQueryService.java +++ b/src/main/java/umc/service/restaurant/RestaurantQueryService.java @@ -9,4 +9,5 @@ public interface RestaurantQueryService { Optional findRestaurant(Long id); List findRestaurantsByNameAndScore(String name, Float score); + } diff --git a/src/main/java/umc/service/review/ReviewQueryService.java b/src/main/java/umc/service/review/ReviewQueryService.java new file mode 100644 index 00000000..0352a2f4 --- /dev/null +++ b/src/main/java/umc/service/review/ReviewQueryService.java @@ -0,0 +1,14 @@ +package umc.service.review; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.domain.Review; + +public interface ReviewQueryService { + Page getReviewList(Long storeId, Integer page); + + Page getReviewListByUserId(Long userId, Integer page); +} diff --git a/src/main/java/umc/service/review/ReviewQueryServiceImpl.java b/src/main/java/umc/service/review/ReviewQueryServiceImpl.java new file mode 100644 index 00000000..e33c7899 --- /dev/null +++ b/src/main/java/umc/service/review/ReviewQueryServiceImpl.java @@ -0,0 +1,57 @@ +package umc.service.review; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.apiPayload.code.status.ErrorStatus; +import umc.apiPayload.exception.GeneralException; +import umc.domain.Restaurant; +import umc.domain.Review; +import umc.domain.User; +import umc.repository.restaurant.RestaurantRepository; +import umc.repository.review.ReviewRepository; +import umc.repository.user.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewQueryServiceImpl implements ReviewQueryService { + + private final ReviewRepository reviewRepository; + private final RestaurantRepository restaurantRepository; + + private final UserRepository userRepository; + + + @Override + public Page getReviewList(Long storeId, Integer page) { + + Restaurant restaurant = restaurantRepository.findById(storeId).orElseThrow( + () -> new GeneralException(ErrorStatus.RESTAURANT_NOT_FOUND) + ); + + Page reviewPage = reviewRepository.findAllByRestaurant(restaurant, PageRequest.of(page, 10)); + return reviewPage; + } + + @Override + public Page getReviewListByUserId(Long userId, Integer page) { + + User user = userRepository.findById(userId).orElseThrow( + () -> new GeneralException(ErrorStatus.USER_NOT_FOUND) + ); + + Page reviewPage = reviewRepository.findAllByUser(user, PageRequest.of(page, 10)); + Slice reviewSlice = reviewRepository.findReviewsByUser(user, PageRequest.of(page, 10)); + + System.out.println(reviewSlice); + System.out.println(reviewPage); + + return reviewPage; + + } + +} diff --git a/src/main/java/umc/service/user/UserCommandService.java b/src/main/java/umc/service/user/UserCommandService.java index caef4116..94689909 100644 --- a/src/main/java/umc/service/user/UserCommandService.java +++ b/src/main/java/umc/service/user/UserCommandService.java @@ -2,10 +2,13 @@ import umc.domain.User; import umc.web.dto.user.UserRequestDTO; +import umc.web.dto.user.UserResponseDTO; public interface UserCommandService { User joinUser(UserRequestDTO.JoinDto request); void challengeMission(Long missionId); + + UserResponseDTO.LoginResultDTO loginUser(UserRequestDTO.LoginRequestDTO request); } diff --git a/src/main/java/umc/service/user/UserCommandServiceImpl.java b/src/main/java/umc/service/user/UserCommandServiceImpl.java index 96e5912d..3157db6a 100644 --- a/src/main/java/umc/service/user/UserCommandServiceImpl.java +++ b/src/main/java/umc/service/user/UserCommandServiceImpl.java @@ -1,11 +1,17 @@ package umc.service.user; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import umc.apiPayload.code.status.ErrorStatus; import umc.apiPayload.exception.GeneralException; import umc.apiPayload.exception.handler.FoodCategoryHandler; +import umc.apiPayload.exception.handler.UserHandler; +import umc.config.security.jwt.JwtTokenProvider; import umc.converter.user.UserConverter; import umc.converter.user.UserPreferenceConverter; import umc.domain.FoodCategory; @@ -20,7 +26,9 @@ import umc.repository.user.UserPreferenceRepository; import umc.repository.user.UserRepository; import umc.web.dto.user.UserRequestDTO; +import umc.web.dto.user.UserResponseDTO; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -37,6 +45,8 @@ public class UserCommandServiceImpl implements UserCommandService{ private final MissionRepository missionRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; @Override @Transactional @@ -76,4 +86,30 @@ public void challengeMission(Long missionId) { userMissionRepository.save(userMission); } + + @Override + public UserResponseDTO.LoginResultDTO loginUser(UserRequestDTO.LoginRequestDTO request) { + + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(()-> new UserHandler(ErrorStatus.USER_NOT_FOUND)); + + + if(!request.getPassword().equals(user.getPassword())) { + throw new UserHandler(ErrorStatus.INVALID_PASSWORD); + } + + + Authentication authentication = new UsernamePasswordAuthenticationToken( + user.getEmail(), null, + Collections.singleton(() -> user.getRole().name()) + ); + + String accessToken = jwtTokenProvider.generateToken(authentication); + + return UserConverter.toLoginResultDTO( + user.getId(), + accessToken + ); + + } } \ No newline at end of file diff --git a/src/main/java/umc/service/user/UserQueryService.java b/src/main/java/umc/service/user/UserQueryService.java index 4963ec89..dd698cf5 100644 --- a/src/main/java/umc/service/user/UserQueryService.java +++ b/src/main/java/umc/service/user/UserQueryService.java @@ -1,4 +1,9 @@ package umc.service.user; +import jakarta.servlet.http.HttpServletRequest; +import umc.web.dto.user.UserResponseDTO; + public interface UserQueryService { + + UserResponseDTO.UserInfoDTO getUserInfo(HttpServletRequest request); } diff --git a/src/main/java/umc/service/user/UserQueryServiceImpl.java b/src/main/java/umc/service/user/UserQueryServiceImpl.java index fe96bbbc..0f9b4476 100644 --- a/src/main/java/umc/service/user/UserQueryServiceImpl.java +++ b/src/main/java/umc/service/user/UserQueryServiceImpl.java @@ -1,4 +1,35 @@ package umc.service.user; -public class UserQueryServiceImpl { +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.apiPayload.code.status.ErrorStatus; +import umc.apiPayload.exception.handler.UserHandler; +import umc.config.security.jwt.JwtTokenProvider; +import umc.converter.user.UserConverter; +import umc.domain.User; +import umc.repository.user.UserRepository; +import umc.web.dto.user.UserResponseDTO; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserQueryServiceImpl implements UserQueryService { + + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + + @Override + @Transactional(readOnly = true) + public UserResponseDTO.UserInfoDTO getUserInfo(HttpServletRequest request){ + Authentication authentication = jwtTokenProvider.extractAuthentication(request); + String email = authentication.getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(()-> new UserHandler(ErrorStatus.USER_NOT_FOUND)); + return UserConverter.toUserInfoDTO(user); + } + } diff --git a/src/main/java/umc/validation/annotation/CheckPage.java b/src/main/java/umc/validation/annotation/CheckPage.java new file mode 100644 index 00000000..6f0af54a --- /dev/null +++ b/src/main/java/umc/validation/annotation/CheckPage.java @@ -0,0 +1,19 @@ +package umc.validation.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import umc.validation.validator.CategoriesExistValidator; +import umc.validation.validator.PageCheckValidator; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PageCheckValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPage { + + String message() default "페이지 파라미터 오류입니다~~"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/umc/validation/validator/PageCheckValidator.java b/src/main/java/umc/validation/validator/PageCheckValidator.java new file mode 100644 index 00000000..5c5be7ec --- /dev/null +++ b/src/main/java/umc/validation/validator/PageCheckValidator.java @@ -0,0 +1,32 @@ +package umc.validation.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import umc.apiPayload.code.status.ErrorStatus; +import umc.repository.location.LocationRepository; +import umc.validation.annotation.CheckPage; +import umc.validation.annotation.ExistLocation; + +@Component +@RequiredArgsConstructor +public class PageCheckValidator implements ConstraintValidator { + + @Override + public void initialize(CheckPage constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Integer aInteger, ConstraintValidatorContext context) { + + boolean isValid = true; + + if(aInteger < 0){ + isValid = false; + } + + return isValid; + } +} diff --git a/src/main/java/umc/web/controller/MissionRestController.java b/src/main/java/umc/web/controller/MissionRestController.java index 71effa67..a9262f0c 100644 --- a/src/main/java/umc/web/controller/MissionRestController.java +++ b/src/main/java/umc/web/controller/MissionRestController.java @@ -1,15 +1,27 @@ package umc.web.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import umc.apiPayload.ApiResponse; +import umc.converter.MissionConverter; +import umc.domain.Mission; +import umc.domain.Review; +import umc.domain.mapping.UserMission; import umc.service.mission.MissionCommandService; +import umc.service.mission.MissionQueryService; import umc.service.restaurant.RestaurantCommandService; -import umc.validation.annotation.ExistLocation; -import umc.validation.annotation.ExistRestaurant; +import umc.validation.annotation.*; import umc.web.dto.mission.MissionRequestDTO; +import umc.web.dto.mission.MissionResponseDTO; import umc.web.dto.restaurant.RestaurantRequestDTO; @RestController @@ -18,6 +30,7 @@ public class MissionRestController { private final MissionCommandService missionCommandService; + private final MissionQueryService missionQueryService; @PostMapping("/restaurants/{restaurantId}/missions") public ApiResponse createMission( @@ -28,7 +41,58 @@ public ApiResponse createMission( } + @GetMapping("/restaurants/{restaurantId}/missions") + @Operation(summary = "특정 가게의 미션 목록 조회 API",description = "특정 가게의 미션들의 목록을 조회하는 API이며, 페이징을 포함합니다. query String 으로 page 번호를 주세요") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200",description = "OK, 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "acess 토큰 모양이 이상함",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + }) + @Parameters({ + @Parameter(name = "restaurantId", description = "가게의 아이디, path variable 입니다!") + }) + public ApiResponse getMissionListByRestaurantId( + @ExistRestaurant @PathVariable(name = "restaurantId") Long restaurantId, + @CheckPage @RequestParam(name = "page") Integer page) { + Page missionListByRestaurantId = missionQueryService.getMissionListByRestaurantId(restaurantId, page); + return ApiResponse.onSuccess(MissionConverter.toMissionListDTO(missionListByRestaurantId)); + } + + + @GetMapping("/missions") + @Operation(summary = "내가 진행 중인 미션 목록 조회 API",description = "내가 진행 중인 미션들의 목록을 조회하는 API이며, 페이징을 포함합니다. query String 으로 page 번호를 주세요") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200",description = "OK, 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "acess 토큰 모양이 이상함",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + }) + public ApiResponse getMyMissionList( + @CheckPage @RequestParam(name = "page") Integer page) { + Long userId = 1L; // TODO. SecurityUtils 구현 후 토큰에서 userID 가져오기 + + Page userMissionList = missionQueryService.getMissionListByUserId(userId, page); + return ApiResponse.onSuccess(MissionConverter.toUserMissionListDTO(userMissionList)); + } + + + @PatchMapping("/users/{userId}/missions/{missionId}") + @Operation(summary = "진행 중인 미션 진행 완료로 바꾸기 API",description = "해당 mission의 상태를 진행 완료로 변경하는 API이다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200",description = "OK, 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "acess 토큰 모양이 이상함",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + }) + public ApiResponse changeMissionStatus( + @ExistUser @PathVariable(name = "userId") Long userId, + @ExistMission @PathVariable(name = "missionId") Long missionId) { + + missionCommandService.changeMissionStatus(userId, missionId); + return ApiResponse.onSuccess("미션의 상태가 변경되었습니다."); + } } diff --git a/src/main/java/umc/web/controller/ReviewRestController.java b/src/main/java/umc/web/controller/ReviewRestController.java index def8fc59..0c0236bc 100644 --- a/src/main/java/umc/web/controller/ReviewRestController.java +++ b/src/main/java/umc/web/controller/ReviewRestController.java @@ -1,13 +1,27 @@ package umc.web.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import umc.apiPayload.ApiResponse; +import umc.converter.ReviewConverter; +import umc.domain.Review; +import umc.service.restaurant.RestaurantQueryService; import umc.service.review.ReviewCommandService; +import umc.service.review.ReviewQueryService; +import umc.validation.annotation.CheckPage; import umc.validation.annotation.ExistLocation; +import umc.validation.annotation.ExistRestaurant; import umc.web.dto.review.ReviewRequestDTO; +import umc.web.dto.review.ReviewResponseDTO; @RestController @RequiredArgsConstructor @@ -16,8 +30,10 @@ public class ReviewRestController { private final ReviewCommandService reviewCommandService; + private final ReviewQueryService reviewQueryService; - @PostMapping("/{restaurantId}/reviews") + + @PostMapping("/{restaurantId}") public ApiResponse createReview( @RequestBody @Valid ReviewRequestDTO.createReviewDTO request, @ExistLocation @PathVariable Long restaurantId) { @@ -27,5 +43,40 @@ public ApiResponse createReview( } + @GetMapping("/{restaurantId}") + @Operation(summary = "특정 가게의 리뷰 목록 조회 API",description = "특정 가게의 리뷰들의 목록을 조회하는 API이며, 페이징을 포함합니다. query String 으로 page 번호를 주세요") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200",description = "OK, 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "acess 토큰 모양이 이상함",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + }) + @Parameters({ + @Parameter(name = "restaurantId", description = "가게의 아이디, path variable 입니다!") + }) + public ApiResponse getReviewList(@ExistRestaurant @PathVariable(name = "restaurantId") Long restaurantId, @CheckPage @RequestParam(name = "page") Integer page){ + Page reviewList = reviewQueryService.getReviewList(restaurantId,page); + return ApiResponse.onSuccess(ReviewConverter.toReviewPreViewListDTO(reviewList)); + } + + + + @GetMapping("") + @Operation(summary = "특정 유저가 작성한 가게의 리뷰 목록 조회 API",description = "내가 작성한 리뷰들의 목록을 조회하는 API이며, 페이징을 포함합니다. query String 으로 page 번호를 주세요") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200",description = "OK, 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "acess 토큰 만료",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "acess 토큰 모양이 이상함",content = @Content(schema = @Schema(implementation = ApiResponse.class))), + }) + + public ApiResponse getMyReviewList(@CheckPage @RequestParam(name = "page") Integer page){ + Long userId = 1L; // TODO. SecurityUtils 구현 후 토큰에서 userID 가져오기 + + Page myReviewList = reviewQueryService.getReviewListByUserId(userId, page); + return ApiResponse.onSuccess(ReviewConverter.toReviewPreViewListDTO(myReviewList)); + } + + } diff --git a/src/main/java/umc/web/controller/UserRestController.java b/src/main/java/umc/web/controller/UserRestController.java index 228b6f78..0fe5a9dd 100644 --- a/src/main/java/umc/web/controller/UserRestController.java +++ b/src/main/java/umc/web/controller/UserRestController.java @@ -1,5 +1,8 @@ package umc.web.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; @@ -8,6 +11,7 @@ import umc.converter.user.UserConverter; import umc.domain.User; import umc.service.user.UserCommandService; +import umc.service.user.UserQueryService; import umc.validation.annotation.ExistMission; import umc.validation.annotation.ExistUser; import umc.web.dto.user.UserRequestDTO; @@ -20,8 +24,9 @@ public class UserRestController { private final UserCommandService userCommandService; + private final UserQueryService userQueryService; - @PostMapping("/") + @PostMapping("/join") public ApiResponse join(@RequestBody @Valid UserRequestDTO.JoinDto request){ User user = userCommandService.joinUser(request); @@ -36,4 +41,20 @@ public ApiResponse challengeMission( @ExistMission @PathVariable Long mi userCommandService.challengeMission(missionId); return ApiResponse.onSuccess("해당 미션의 도전을 시작하셨습니다"); } + + @PostMapping("/login") + @Operation(summary = "유저 로그인 API",description = "유저가 로그인하는 API입니다.") + public ApiResponse login(@RequestBody @Valid UserRequestDTO.LoginRequestDTO request) { + return ApiResponse.onSuccess(userCommandService.loginUser(request)); + } + + + @GetMapping("/info") + @Operation(summary = "유저 내 정보 조회 API - 인증 필요", + description = "유저가 내 정보를 조회하는 API입니다.", + security = { @SecurityRequirement(name = "JWT TOKEN") } + ) + public ApiResponse getMyInfo(HttpServletRequest request) { + return ApiResponse.onSuccess(userQueryService.getUserInfo(request)); + } } diff --git a/src/main/java/umc/web/controller/UserViewController.java b/src/main/java/umc/web/controller/UserViewController.java new file mode 100644 index 00000000..4e1b1cb5 --- /dev/null +++ b/src/main/java/umc/web/controller/UserViewController.java @@ -0,0 +1,35 @@ +package umc.web.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping;// thymeleaf 사용을 위해 일부가 변경되었습니다. +import umc.service.user.UserCommandService; +import umc.web.dto.user.UserRequestDTO; + +// 실제로는 8주차에서 작성한 컨트롤러와 동일하게 작성하시면 됩니다!! +@RequiredArgsConstructor +public class UserViewController{ + + private UserCommandService userCommandService; + + @PostMapping("/users/signup-form") + public String joinMember(@ModelAttribute("memberJoinDto") UserRequestDTO.JoinDto request, // 협업시에는 기존 RequestBody 어노테이션을 붙여주시면 됩니다! + BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + // 뷰에 데이터 바인딩이 실패할 경우 signup 페이지를 유지합니다. + return "signup"; + } + + try { + userCommandService.joinUser(request); + return "redirect:/login"; + } catch (Exception e) { + // 회원가입 과정에서 에러가 발생할 경우 에러 메시지를 보내고, signup 페이디를 유지합니다. + model.addAttribute("error", e.getMessage()); + return "signup"; + } + } +} \ No newline at end of file diff --git a/src/main/java/umc/web/dto/mission/MissionResponseDTO.java b/src/main/java/umc/web/dto/mission/MissionResponseDTO.java new file mode 100644 index 00000000..d8e002dc --- /dev/null +++ b/src/main/java/umc/web/dto/mission/MissionResponseDTO.java @@ -0,0 +1,45 @@ +package umc.web.dto.mission; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.domain.Location; +import umc.domain.Restaurant; +import umc.web.dto.review.ReviewResponseDTO; + +import java.time.LocalDate; +import java.util.List; + +public class MissionResponseDTO { + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MissionListDTO{ + List missionDTOList; + Integer listSize; + Integer totalPage; + Long totalElements; + Boolean isFirst; + Boolean isLast; + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MissionDTO{ + String missionName; + String contents; + Integer points; + Long restaurantId; + LocalDate createdAt; + } + + +} diff --git a/src/main/java/umc/web/dto/review/ReviewResponseDTO.java b/src/main/java/umc/web/dto/review/ReviewResponseDTO.java new file mode 100644 index 00000000..1a19e121 --- /dev/null +++ b/src/main/java/umc/web/dto/review/ReviewResponseDTO.java @@ -0,0 +1,40 @@ +package umc.web.dto.review; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +public class ReviewResponseDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ReviewPreViewListDTO{ + List reviewList; + Integer listSize; + Integer totalPage; + Long totalElements; + Boolean isFirst; + Boolean isLast; + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ReviewPreViewDTO{ + String ownerNickname; + Integer stars; + String content; + LocalDate createdAt; + } + + + +} diff --git a/src/main/java/umc/web/dto/user/UserRequestDTO.java b/src/main/java/umc/web/dto/user/UserRequestDTO.java index 201a9b08..7a47bc75 100644 --- a/src/main/java/umc/web/dto/user/UserRequestDTO.java +++ b/src/main/java/umc/web/dto/user/UserRequestDTO.java @@ -1,7 +1,12 @@ package umc.web.dto.user; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Getter; +import lombok.Setter; import umc.domain.enums.Gender; +import umc.domain.enums.Role; import umc.validation.annotation.ExistCategories; import java.time.LocalDateTime; @@ -11,14 +16,35 @@ public class UserRequestDTO { @Getter public static class JoinDto{ + @NotBlank String username; + @NotBlank String password; + @NotBlank String nickname; + @NotBlank String phoneNumber; + @NotNull LocalDateTime birthDate; + @NotNull Gender gender; + @NotBlank + @Email String email; + @NotNull + Role role; // 역할 필드 추가 @ExistCategories List preferCategory; } + + @Getter + @Setter + public static class LoginRequestDTO{ + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "올바른 이메일 형식이어야 합니다.") + private String email; + + @NotBlank(message = "패스워드는 필수입니다.") + private String password; + } } \ No newline at end of file diff --git a/src/main/java/umc/web/dto/user/UserResponseDTO.java b/src/main/java/umc/web/dto/user/UserResponseDTO.java index 2c0959c1..f15291ed 100644 --- a/src/main/java/umc/web/dto/user/UserResponseDTO.java +++ b/src/main/java/umc/web/dto/user/UserResponseDTO.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import umc.domain.enums.Gender; import java.time.LocalDateTime; @@ -18,4 +19,25 @@ public static class JoinResultDTO{ Long userId; LocalDateTime createdAt; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LoginResultDTO { + Long userId; + String accessToken; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class UserInfoDTO{ + String name; + String email; + Gender gender; + } + + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 421b22a4..40bb983f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,4 +23,10 @@ spring: discord: webhook: - url: https://discord.com/api/webhooks/1372426888278183988/DsQlUXaoj6yysZToho3eBis6h1FE-1-tey771P_eMVyKpVIqhW5iL_T2j4oX2_9cMzCR \ No newline at end of file + url: https://discord.com/api/webhooks/1372426888278183988/DsQlUXaoj6yysZToho3eBis6h1FE-1-tey771P_eMVyKpVIqhW5iL_T2j4oX2_9cMzCR + +jwt: + token: + secretKey: umceightfightingjwttokenauthentication + expiration: + access: 14400000 \ No newline at end of file diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html new file mode 100644 index 00000000..55dbff1a --- /dev/null +++ b/src/main/resources/templates/admin.html @@ -0,0 +1,10 @@ + + + + Admin Page + + +

Admin Page

+

관리자만 접근할 수 있는 페이지입니다.

+ + \ No newline at end of file diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 00000000..8c10cb16 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,20 @@ + + + + Home + + +

Welcome to Home Page!

+ +

+ + + + + +
+ +
+ \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 00000000..5cb6795f --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,26 @@ + + + + Login + + +

Login

+
+
+ + +
+
+ + +
+ +
+ +

사용자 이름 또는 비밀번호가 잘못되었습니다.

+

로그아웃되었습니다.

+ + +

계정이 없나요? Sign up

+ + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 00000000..33fe95bb --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,130 @@ + + + + 회원가입 + + + +

회원가입

+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+ + +
+ +
+ + \ No newline at end of file