diff --git a/spring/build.gradle b/spring/build.gradle index 141211e..b9994e7 100644 --- a/spring/build.gradle +++ b/spring/build.gradle @@ -31,6 +31,8 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' // queryDSL implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' @@ -40,6 +42,16 @@ dependencies { // validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE' + + // 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' } tasks.named('test') { diff --git a/spring/src/main/generated/umc/spring/domain/QStore.java b/spring/src/main/generated/umc/spring/domain/QStore.java index 03a4f09..ec233fb 100644 --- a/spring/src/main/generated/umc/spring/domain/QStore.java +++ b/spring/src/main/generated/umc/spring/domain/QStore.java @@ -35,6 +35,8 @@ public class QStore extends EntityPathBase { public final QRegion region; + public final NumberPath score = createNumber("score", Float.class); + //inherited public final DateTimePath updatedAt = _super.updatedAt; diff --git a/spring/src/main/generated/umc/spring/domain/QUser.java b/spring/src/main/generated/umc/spring/domain/QUser.java index b676091..95386a3 100644 --- a/spring/src/main/generated/umc/spring/domain/QUser.java +++ b/spring/src/main/generated/umc/spring/domain/QUser.java @@ -31,16 +31,24 @@ public class QUser extends EntityPathBase { //inherited public final DateTimePath createdAt = _super.createdAt; + public final StringPath email = createString("email"); + public final EnumPath gender = createEnum("gender", umc.spring.domain.enums.Gender.class); public final NumberPath id = createNumber("id", Long.class); public final StringPath name = createString("name"); + public final StringPath password = createString("password"); + public final NumberPath points = createNumber("points", Integer.class); + public final ListPath preferCategory = this.createList("preferCategory", FoodCategory.class, QFoodCategory.class, PathInits.DIRECT2); + public final ListPath reviews = this.createList("reviews", Review.class, QReview.class, PathInits.DIRECT2); + public final EnumPath role = createEnum("role", umc.spring.domain.enums.Role.class); + //inherited public final DateTimePath updatedAt = _super.updatedAt; diff --git a/spring/src/main/java/umc/spring/apiPayload/exception/handler/UserHandler.java b/spring/src/main/java/umc/spring/apiPayload/exception/handler/UserHandler.java new file mode 100644 index 0000000..d12e8f5 --- /dev/null +++ b/spring/src/main/java/umc/spring/apiPayload/exception/handler/UserHandler.java @@ -0,0 +1,16 @@ +package umc.spring.apiPayload.exception.handler; + + +import lombok.Getter; +import umc.spring.apiPayload.status.ErrorResponse; + +@Getter +public class UserHandler extends RuntimeException { + + private final ErrorResponse errorResponse; + + public UserHandler(ErrorResponse errorResponse) { + super(errorResponse.getMessage()); + this.errorResponse = errorResponse; + } +} diff --git a/spring/src/main/java/umc/spring/apiPayload/status/ErrorResponse.java b/spring/src/main/java/umc/spring/apiPayload/status/ErrorResponse.java index 273d609..4b1b325 100644 --- a/spring/src/main/java/umc/spring/apiPayload/status/ErrorResponse.java +++ b/spring/src/main/java/umc/spring/apiPayload/status/ErrorResponse.java @@ -22,8 +22,11 @@ public enum ErrorResponse implements BaseErrorResponse { // For test TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"), - ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."); + ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."), + // jwt 에러 + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "JWT401","유효하지 않은 토큰입니다."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "JWT402", "비밀번호가 일치하지 않습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/spring/src/main/java/umc/spring/config/jwt/JwtAuthenticationFilter.java b/spring/src/main/java/umc/spring/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..a97509f --- /dev/null +++ b/spring/src/main/java/umc/spring/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,42 @@ +package umc.spring.config.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +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.spring.config.properties.Constants; + +@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/spring/src/main/java/umc/spring/config/jwt/JwtTokenProvider.java b/spring/src/main/java/umc/spring/config/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..c2972e0 --- /dev/null +++ b/spring/src/main/java/umc/spring/config/jwt/JwtTokenProvider.java @@ -0,0 +1,84 @@ +package umc.spring.config.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 java.security.Key; +import java.util.Date; +import java.util.Collections; +import umc.spring.apiPayload.exception.handler.UserHandler; +import umc.spring.apiPayload.status.ErrorResponse; +import umc.spring.config.properties.Constants; +import umc.spring.config.properties.JwtProperties; + +@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(ErrorResponse.INVALID_TOKEN); + } + return getAuthentication(accessToken); + } +} diff --git a/spring/src/main/java/umc/spring/config/properties/Constants.java b/spring/src/main/java/umc/spring/config/properties/Constants.java new file mode 100644 index 0000000..e6b2850 --- /dev/null +++ b/spring/src/main/java/umc/spring/config/properties/Constants.java @@ -0,0 +1,6 @@ +package umc.spring.config.properties; + +public final class Constants { + public static final String AUTH_HEADER = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; +} diff --git a/spring/src/main/java/umc/spring/config/properties/JwtProperties.java b/spring/src/main/java/umc/spring/config/properties/JwtProperties.java new file mode 100644 index 0000000..4288385 --- /dev/null +++ b/spring/src/main/java/umc/spring/config/properties/JwtProperties.java @@ -0,0 +1,22 @@ +package umc.spring.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/spring/src/main/java/umc/spring/config/security/CustomUserDetailsService.java b/spring/src/main/java/umc/spring/config/security/CustomUserDetailsService.java new file mode 100644 index 0000000..f34b722 --- /dev/null +++ b/spring/src/main/java/umc/spring/config/security/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +package umc.spring.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.spring.domain.User; +import umc.spring.repository.userrepository.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(); + } +} diff --git a/spring/src/main/java/umc/spring/config/security/SecurityConfig.java b/spring/src/main/java/umc/spring/config/security/SecurityConfig.java new file mode 100644 index 0000000..3a541a3 --- /dev/null +++ b/spring/src/main/java/umc/spring/config/security/SecurityConfig.java @@ -0,0 +1,44 @@ +package umc.spring.config.security; + +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.spring.config.jwt.JwtAuthenticationFilter; +import umc.spring.config.jwt.JwtTokenProvider; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http,JwtTokenProvider jwtTokenProvider) throws Exception { + http + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests( + (requests) -> requests + .requestMatchers("/", "/members/join", "/members/login", "/swagger-ui/**", "/v3/api-docs/**", + "/css/**", "/js/**", "/images/**", "/favicon.ico","/login").permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .csrf() + .disable() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + System.out.println("hi"); + return new BCryptPasswordEncoder(); + } +} diff --git a/spring/src/main/java/umc/spring/config/swagger/SwaggerConfig.java b/spring/src/main/java/umc/spring/config/swagger/SwaggerConfig.java new file mode 100644 index 0000000..8042dcc --- /dev/null +++ b/spring/src/main/java/umc/spring/config/swagger/SwaggerConfig.java @@ -0,0 +1,33 @@ +package umc.spring.config.swagger; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + final String securitySchemeName = "JWT TOKEN"; // @SecurityRequirement 이름과 일치해야 함 + + return new OpenAPI() + .info(new Info() + .title("U.M.C API 문서") + .description("JWT 인증 테스트용 Swagger 문서입니다.") + .version("v1.0")) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) // 인증 적용 + .components(new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + )); + } +} diff --git a/spring/src/main/java/umc/spring/converter/UserConverter.java b/spring/src/main/java/umc/spring/converter/UserConverter.java new file mode 100644 index 0000000..e021a74 --- /dev/null +++ b/spring/src/main/java/umc/spring/converter/UserConverter.java @@ -0,0 +1,48 @@ +package umc.spring.converter; + +import java.util.ArrayList; +import umc.spring.domain.User; +import umc.spring.domain.enums.Gender; +import umc.spring.web.dto.UserRequestDTO; +import umc.spring.web.dto.UserResponseDTO; +import umc.spring.web.dto.UserResponseDTO.UserInfoDTO; + +public class UserConverter { + public static User toUser(UserRequestDTO.JoinDto request) { + Gender gender = null; + switch (request.getGender()) { + case 1: gender = Gender.MALE; break; + case 2: gender = Gender.FEMALE; break; + case 3: gender = Gender.NONE; break; + } + + return User.builder() + .name(request.getName()) + .email(request.getEmail()) // 추가된 코드 + .password(request.getPassword()) // 추가된 코드 + .gender(gender) + .address(request.getAddress()) + .role(request.getRole()) // 추가된 코드 + .preferCategory(new ArrayList<>()) + .birth(String.format("%04d-%02d-%02d", + request.getBirthYear(), + request.getBirthMonth(), + request.getBirthDay())) + .build(); + } + + public static UserResponseDTO.LoginResultDTO toLoginResultDTO(Long userId, String accessToken) { + return UserResponseDTO.LoginResultDTO.builder() + .memberId(userId) + .accessToken(accessToken) + .build(); + } + + public static UserResponseDTO.UserInfoDTO toUserInfoDTO(User user) { + return UserResponseDTO.UserInfoDTO.builder() + .name(user.getName()) + .email(user.getEmail()) + .gender(user.getGender().name()) // Enum → String + .build(); + } +} \ No newline at end of file diff --git a/spring/src/main/java/umc/spring/domain/User.java b/spring/src/main/java/umc/spring/domain/User.java index 90c49c7..da3efb1 100644 --- a/spring/src/main/java/umc/spring/domain/User.java +++ b/spring/src/main/java/umc/spring/domain/User.java @@ -4,6 +4,7 @@ import lombok.*; import umc.spring.domain.common.Base; import umc.spring.domain.enums.Gender; +import umc.spring.domain.enums.Role; import umc.spring.domain.mapping.UserMission; import umc.spring.domain.mapping.UserTerms; @@ -26,6 +27,10 @@ public class User extends Base { private String address; private int points; + @OneToMany + @JoinColumn(name = "user_id") // 또는 적절한 매핑 전략 + private List preferCategory = new ArrayList<>(); + @OneToMany( mappedBy = "user", cascade = CascadeType.ALL, @@ -40,14 +45,28 @@ public class User extends Base { cascade = CascadeType.ALL, orphanRemoval = true ) - @JoinColumn(name = "user_id") // ← Authentication 테이블에 FK 컬럼 생성 + @JoinColumn(name = "user_id") private List authentications = new ArrayList<>(); @OneToMany( cascade = CascadeType.ALL, orphanRemoval = true ) - @JoinColumn(name = "user_id") // ← Authentication 테이블에 FK 컬럼 생성 + @JoinColumn(name = "user_id") private List reviews = new ArrayList<>(); + + // 보안 관련 필드 및 비밀번호 설정 함수 + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + + public void encodePassword(String password) { + this.password = password; + } } diff --git a/spring/src/main/java/umc/spring/domain/enums/Gender.java b/spring/src/main/java/umc/spring/domain/enums/Gender.java index b74c14e..1315bd3 100644 --- a/spring/src/main/java/umc/spring/domain/enums/Gender.java +++ b/spring/src/main/java/umc/spring/domain/enums/Gender.java @@ -1,5 +1,5 @@ package umc.spring.domain.enums; public enum Gender { - MALE, FEMALE + MALE, FEMALE,NONE } diff --git a/spring/src/main/java/umc/spring/domain/enums/Role.java b/spring/src/main/java/umc/spring/domain/enums/Role.java new file mode 100644 index 0000000..61d3b50 --- /dev/null +++ b/spring/src/main/java/umc/spring/domain/enums/Role.java @@ -0,0 +1,5 @@ +package umc.spring.domain.enums; + +public enum Role { + ADMIN, USER +} diff --git a/spring/src/main/java/umc/spring/repository/userrepository/UserRepository.java b/spring/src/main/java/umc/spring/repository/userrepository/UserRepository.java new file mode 100644 index 0000000..8f5aaf1 --- /dev/null +++ b/spring/src/main/java/umc/spring/repository/userrepository/UserRepository.java @@ -0,0 +1,10 @@ +package umc.spring.repository.userrepository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import umc.spring.domain.User; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + Optional findByEmail(String email); +} diff --git a/spring/src/main/java/umc/spring/service/tempservice/TempQueryServiceImpl.java b/spring/src/main/java/umc/spring/service/tempservice/TempQueryServiceImpl.java index 0d5c72a..8962d9d 100644 --- a/spring/src/main/java/umc/spring/service/tempservice/TempQueryServiceImpl.java +++ b/spring/src/main/java/umc/spring/service/tempservice/TempQueryServiceImpl.java @@ -2,15 +2,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import umc.spring.apiPayload.status.ErrorResponse; -import umc.spring.apiPayload.exception.handler.TempHandler; +//import umc.spring.apiPayload.status.ErrorResponse; +//import umc.spring.apiPayload.exception.handler.TempHandler; @Service @RequiredArgsConstructor public class TempQueryServiceImpl implements TempQueryService{ @Override public void CheckFlag(Integer flag) { - if (flag == 1) - throw new TempHandler(ErrorResponse.TEMP_EXCEPTION); + // if (flag == 1) + // throw new TempHandler(ErrorResponse.TEMP_EXCEPTION); } } diff --git a/spring/src/main/java/umc/spring/service/userservice/UserCommandService.java b/spring/src/main/java/umc/spring/service/userservice/UserCommandService.java new file mode 100644 index 0000000..c940510 --- /dev/null +++ b/spring/src/main/java/umc/spring/service/userservice/UserCommandService.java @@ -0,0 +1,12 @@ +package umc.spring.service.userservice; + +import umc.spring.domain.User; +import umc.spring.web.dto.UserRequestDTO; +import umc.spring.web.dto.UserResponseDTO; + +public interface UserCommandService { + User join(UserRequestDTO.JoinDto request); + UserResponseDTO.LoginResultDTO loginMember(UserRequestDTO.LoginRequestDTO request); +} + + diff --git a/spring/src/main/java/umc/spring/service/userservice/UserCommandServiceImpl.java b/spring/src/main/java/umc/spring/service/userservice/UserCommandServiceImpl.java new file mode 100644 index 0000000..1bd0cdb --- /dev/null +++ b/spring/src/main/java/umc/spring/service/userservice/UserCommandServiceImpl.java @@ -0,0 +1,73 @@ +package umc.spring.service.userservice; + +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.spring.apiPayload.exception.handler.UserHandler; +import umc.spring.apiPayload.status.ErrorResponse; +import umc.spring.config.jwt.JwtTokenProvider; +import umc.spring.converter.UserConverter; +import umc.spring.domain.FoodCategory; +import umc.spring.domain.User; +import umc.spring.repository.userrepository.UserRepository; +import umc.spring.web.dto.UserRequestDTO; + +import lombok.extern.slf4j.Slf4j; +import umc.spring.web.dto.UserResponseDTO; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserCommandServiceImpl implements UserCommandService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + @Override + @Transactional + public User join(UserRequestDTO.JoinDto request) { + if (userRepository.existsByEmail(request.getEmail())) { + throw new IllegalArgumentException("이미 사용 중인 이메일입니다."); + } + + log.info("📥 회원가입 요청 - email: {}", request.getEmail()); + log.info("📦 성별: {}, 생년월일: {}-{}-{}", request.getGender(), request.getBirthYear(), request.getBirthMonth(), request.getBirthDay()); + log.info("📦 카테고리: {}", request.getPreferCategory()); + + User user = UserConverter.toUser(request); + user.encodePassword(passwordEncoder.encode(request.getPassword())); + User savedUser = userRepository.save(user); + + log.info("✅ 저장 완료 - userId: {}", savedUser.getId()); + + return savedUser; + } + + @Override + public UserResponseDTO.LoginResultDTO loginMember(UserRequestDTO.LoginRequestDTO request) { + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(()-> new UserHandler(ErrorResponse.MEMBER_NOT_FOUND)); + + if(!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + throw new UserHandler(ErrorResponse.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 + ); + } +} diff --git a/spring/src/main/java/umc/spring/service/userservice/UserQueryService.java b/spring/src/main/java/umc/spring/service/userservice/UserQueryService.java new file mode 100644 index 0000000..9b3c7f5 --- /dev/null +++ b/spring/src/main/java/umc/spring/service/userservice/UserQueryService.java @@ -0,0 +1,8 @@ +package umc.spring.service.userservice; + +import jakarta.servlet.http.HttpServletRequest; +import umc.spring.web.dto.UserResponseDTO; + +public interface UserQueryService { + UserResponseDTO.UserInfoDTO getMemberInfo(HttpServletRequest request); +} diff --git a/spring/src/main/java/umc/spring/service/userservice/UserQueryServiceImpl.java b/spring/src/main/java/umc/spring/service/userservice/UserQueryServiceImpl.java new file mode 100644 index 0000000..4590cf5 --- /dev/null +++ b/spring/src/main/java/umc/spring/service/userservice/UserQueryServiceImpl.java @@ -0,0 +1,33 @@ +package umc.spring.service.userservice; + +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.spring.apiPayload.exception.handler.UserHandler; +import umc.spring.apiPayload.status.ErrorResponse; +import umc.spring.config.jwt.JwtTokenProvider; +import umc.spring.converter.UserConverter; +import umc.spring.domain.User; +import umc.spring.repository.userrepository.UserRepository; +import umc.spring.web.dto.UserResponseDTO; + +@Service +@RequiredArgsConstructor +public class UserQueryServiceImpl implements UserQueryService{ + + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + + @Override + @Transactional(readOnly = true) + public UserResponseDTO.UserInfoDTO getMemberInfo(HttpServletRequest request){ + Authentication authentication = jwtTokenProvider.extractAuthentication(request); + String email = authentication.getName(); + User user = userRepository.findByEmail(email) + .orElseThrow(()-> new UserHandler(ErrorResponse.MEMBER_NOT_FOUND)); + + return UserConverter.toUserInfoDTO(user); + } +} diff --git a/spring/src/main/java/umc/spring/web/controller/MemberRestController.java b/spring/src/main/java/umc/spring/web/controller/MemberRestController.java new file mode 100644 index 0000000..722d7da --- /dev/null +++ b/spring/src/main/java/umc/spring/web/controller/MemberRestController.java @@ -0,0 +1,41 @@ +package umc.spring.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.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import umc.spring.apiPayload.ApiResponse; +import umc.spring.service.userservice.UserCommandServiceImpl; +import umc.spring.service.userservice.UserQueryService; +import umc.spring.web.dto.UserRequestDTO; +import umc.spring.web.dto.UserResponseDTO; +import umc.spring.web.dto.UserResponseDTO.LoginResultDTO; + +@RestController +@RequiredArgsConstructor +public class MemberRestController { + + private final UserCommandServiceImpl userCommandService; + private final UserQueryService userQueryService; + + @PostMapping("/login") + @Operation(summary = "유저 로그인 API", description = "유저가 로그인하는 API입니다.") + public ApiResponse login(@RequestBody @Valid UserRequestDTO.LoginRequestDTO request) { + System.out.println(request); + return ApiResponse.onSuccess(userCommandService.loginMember(request)); + } + + @GetMapping("/info") + @Operation(summary = "유저 내 정보 조회 API - 인증 필요", + description = "유저가 내 정보를 조회하는 API입니다.", + security = { @SecurityRequirement(name = "JWT TOKEN") } + ) + public ApiResponse getMyInfo(HttpServletRequest request) { + return ApiResponse.onSuccess(userQueryService.getMemberInfo(request)); + } +} diff --git a/spring/src/main/java/umc/spring/web/dto/UserRequestDTO.java b/spring/src/main/java/umc/spring/web/dto/UserRequestDTO.java new file mode 100644 index 0000000..ea35271 --- /dev/null +++ b/spring/src/main/java/umc/spring/web/dto/UserRequestDTO.java @@ -0,0 +1,55 @@ +// 파일 위치: umc.spring.web.dto.UserRequestDTO.java +package umc.spring.web.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import umc.spring.domain.enums.Role; + +@Getter +@Setter +public class UserRequestDTO { + + @Getter + @Setter + public static class JoinDto { + @NotBlank + String name; + @NotBlank + @Email + String email; // 이메일 필드 추가 + @NotBlank + String password; // 비밀번호 필드 추가 + @NotNull + Integer gender; + @NotNull + Integer birthYear; + @NotNull + Integer birthMonth; + @NotNull + Integer birthDay; + String address; + List preferCategory; + @NotNull + Role role; // 역할 필드 추가 + String specAddress; + } + + @Getter + @Setter + public static class LoginRequestDTO{ + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "올바른 이메일 형식이어야 합니다.") + private String email; + + @NotBlank(message = "패스워드는 필수입니다.") + private String password; + } +} + + + diff --git a/spring/src/main/java/umc/spring/web/dto/UserResponseDTO.java b/spring/src/main/java/umc/spring/web/dto/UserResponseDTO.java new file mode 100644 index 0000000..8ec43c6 --- /dev/null +++ b/spring/src/main/java/umc/spring/web/dto/UserResponseDTO.java @@ -0,0 +1,27 @@ +package umc.spring.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class UserResponseDTO { + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LoginResultDTO { + Long memberId; + String accessToken; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class UserInfoDTO{ + String name; + String email; + String gender; + } +} diff --git a/spring/src/main/resources/application.yml b/spring/src/main/resources/application.yml index fe48531..9d86aeb 100644 --- a/spring/src/main/resources/application.yml +++ b/spring/src/main/resources/application.yml @@ -1,21 +1,36 @@ spring: main: allow-bean-definition-overriding: true + datasource: url: jdbc:mysql://localhost:3306/study username: root password: 1266 driver-class-name: com.mysql.cj.jdbc.Driver + sql: init: mode: never + jpa: + hibernate: + ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect show_sql: true format_sql: true use_sql_comments: true - hbm2ddl: - auto: update - default_batch_fetch_size: 1000 \ No newline at end of file + default_batch_fetch_size: 1000 + +logging: + level: + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql: TRACE + umc.spring.service.userservice: DEBUG + +jwt: + token: + secretKey: umceightfightingjwttokenauthentication + expiration: + access: 14400000 diff --git a/spring/src/main/resources/templates/admin.html b/spring/src/main/resources/templates/admin.html new file mode 100644 index 0000000..e1f3436 --- /dev/null +++ b/spring/src/main/resources/templates/admin.html @@ -0,0 +1,10 @@ +Add commentMore actions + + + Admin Page + + +

Admin Page

+

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

+ + \ No newline at end of file diff --git a/spring/src/main/resources/templates/home.html b/spring/src/main/resources/templates/home.html new file mode 100644 index 0000000..d3ec7f9 --- /dev/null +++ b/spring/src/main/resources/templates/home.html @@ -0,0 +1,20 @@ +Add commentMore actions + + + Home + + +

Welcome to Home Page!

+ +

+ + + + + +
+ +
+ \ No newline at end of file diff --git a/spring/src/main/resources/templates/login.html b/spring/src/main/resources/templates/login.html new file mode 100644 index 0000000..9d29dc1 --- /dev/null +++ b/spring/src/main/resources/templates/login.html @@ -0,0 +1,26 @@ +Add commentMore actions + + + Login + + +

Login

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

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

+

로그아웃되었습니다.

+ + +

계정이 없나요? Sign up

+ + \ No newline at end of file diff --git a/spring/src/main/resources/templates/signup.html b/spring/src/main/resources/templates/signup.html new file mode 100644 index 0000000..5d13e54 --- /dev/null +++ b/spring/src/main/resources/templates/signup.html @@ -0,0 +1,68 @@ +Add commentMore actions + + + 회원가입 + + + +

회원가입

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