diff --git a/.gitignore b/.gitignore index c2065bc..056f822 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +.yml +.properties \ No newline at end of file diff --git a/build.gradle b/build.gradle index 15b77ef..3a60e1c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,26 @@ +buildscript { + ext { + queryDslVersion = "5.0.0" + } +} + plugins { id 'java' - id 'org.springframework.boot' version '3.0.1' + id 'org.springframework.boot' version '2.7.1' id 'io.spring.dependency-management' version '1.1.0' + id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } group = 'com.dku' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } + querydsl.extendsFrom compileClasspath + } repositories { @@ -22,13 +31,41 @@ 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-web' + implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE' + compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' + + runtimeOnly 'org.postgresql:postgresql' + + implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" + annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}" + + // Swagger + implementation 'io.springfox:springfox-boot-starter:3.0.0' + implementation 'io.springfox:springfox-swagger-ui:3.0.0' } tasks.named('test') { useJUnitPlatform() } + + +def querydslDir = "$buildDir/generated/querydsl" +querydsl { + jpa = true + querydslSourcesDir = querydslDir +} +sourceSets { + main.java.srcDir querydslDir +} +configurations { + querydsl.extendsFrom compileClasspath +} +compileQuerydsl { + options.annotationProcessorPath = configurations.querydsl +} diff --git a/src/main/java/com/dku/springstudy/SpringStudyApplication.java b/src/main/java/com/dku/springstudy/SpringStudyApplication.java index ef164c9..71906bf 100644 --- a/src/main/java/com/dku/springstudy/SpringStudyApplication.java +++ b/src/main/java/com/dku/springstudy/SpringStudyApplication.java @@ -2,8 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +/** + * @author 최재민 + */ @SpringBootApplication +@EnableJpaAuditing public class SpringStudyApplication { public static void main(String[] args) { diff --git a/src/main/java/com/dku/springstudy/auth/JwtAuthenticationFilter.java b/src/main/java/com/dku/springstudy/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..508d6df --- /dev/null +++ b/src/main/java/com/dku/springstudy/auth/JwtAuthenticationFilter.java @@ -0,0 +1,52 @@ +package com.dku.springstudy.auth; + + +import com.dku.springstudy.auth.exception.TokenException; +import com.dku.springstudy.error.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) throws IOException, ServletException { + + String token = jwtProvider.resolveToken(request); + + try { + if (token != null && jwtProvider.validateJwtToken(token)) { + Authentication authentication = jwtProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (TokenException e) { + HttpServletResponse errorResponse = (HttpServletResponse) response; + errorResponse.setStatus(e.getErrorCode().getHttpStatus()); + errorResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); + ResponseEntity exceptionDto = ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(ErrorResponse.from(e.getErrorCode())); + + ObjectMapper objectMapper = new ObjectMapper(); + String exceptionMessage = objectMapper.writeValueAsString(exceptionDto); + + errorResponse.getWriter().write(exceptionMessage); + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/dku/springstudy/auth/JwtProvider.java b/src/main/java/com/dku/springstudy/auth/JwtProvider.java new file mode 100644 index 0000000..678ac8a --- /dev/null +++ b/src/main/java/com/dku/springstudy/auth/JwtProvider.java @@ -0,0 +1,103 @@ +package com.dku.springstudy.auth; + +import com.dku.springstudy.member.service.MemberDetailService; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.*; + + +@Service +@RequiredArgsConstructor +@Slf4j +public class JwtProvider { + private final MemberDetailService memberDetailService; + private final SecretKey secretKey; + + public String createAccessToken(String payload) { + Claims claims = Jwts.claims().setSubject(payload); + Date now = new Date(); + Date validityTime = new Date(now.getTime() + secretKey.getJwtValidityTime()); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validityTime) + .signWith(SignatureAlgorithm.HS256, secretKey.getJwtSecretKey()) + .compact(); + } + + public Authentication getAuthentication(String token) { + UserDetails userDetails = memberDetailService.loadUserByUsername(this.extractEmail(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String extractEmail(String token) { + return (String) Jwts.parser().setSigningKey(secretKey.getJwtSecretKey()).parseClaimsJws(token).getBody().get("email"); + } + + public String resolveToken(HttpServletRequest request) { + String header = request.getHeader("Authorization"); + if (header == null) { + return null; + } + return header.replace("Bearer ", ""); + } + + public boolean validateJwtToken(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey.getJwtSecretKey()) + .parseClaimsJws(token); + return !claims.getBody().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + throw new IllegalArgumentException("유효하지 않은 토큰 정보입니다."); + } + } + + public Map createRefreshToken(String payload) { + + Claims claims = Jwts.claims().setSubject(payload); + Date now = new Date(); + Date validityTime = new Date(now.getTime() + secretKey.getJwtValidityTime()); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); + String refreshTokenExpirationAt = simpleDateFormat.format(validityTime); + + String jwt = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validityTime) + .signWith(SignatureAlgorithm.HS256, secretKey.getJwtSecretKey()) + .compact(); + + Map result = new HashMap<>(); + result.put("refreshToken", jwt); + result.put("refreshTokenExpirationAt", refreshTokenExpirationAt); + return result; + } + + + public Long getTokenExpireTime(String accessToken) { + + Base64.Decoder decoder = Base64.getUrlDecoder(); + String[] parts = accessToken.split("\\."); + ObjectMapper mapper = new ObjectMapper(); + String payload = new String(decoder.decode(parts[1])); + Map exp = null; + + try { + exp = mapper.readValue(payload, Map.class); + return ((Number) exp.get("exp")).longValue(); + } catch (IOException err) { + throw new RuntimeException(err); + } + } +} diff --git a/src/main/java/com/dku/springstudy/auth/SecretConfig.java b/src/main/java/com/dku/springstudy/auth/SecretConfig.java new file mode 100644 index 0000000..ef09e38 --- /dev/null +++ b/src/main/java/com/dku/springstudy/auth/SecretConfig.java @@ -0,0 +1,26 @@ +package com.dku.springstudy.auth; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SecretConfig { + + private final String jwtSecretKey; + private final Long jwtValidityTime; + private final Long refreshValidityTime; + + @Bean + public SecretKey newSecretKey() { + return new SecretKey(jwtSecretKey, jwtValidityTime, refreshValidityTime); + } + + public SecretConfig(@Value("${security.jwt.token.secret-key}") String jwtSecretKey, + @Value("${security.jwt.token.expire-length}")Long jwtValidityTime, + @Value("${security.jwt.token.expire-length-refresh}") Long refreshValidityTime) { + this.jwtSecretKey = jwtSecretKey; + this.jwtValidityTime = jwtValidityTime; + this.refreshValidityTime = refreshValidityTime; + } +} diff --git a/src/main/java/com/dku/springstudy/auth/SecretKey.java b/src/main/java/com/dku/springstudy/auth/SecretKey.java new file mode 100644 index 0000000..103a569 --- /dev/null +++ b/src/main/java/com/dku/springstudy/auth/SecretKey.java @@ -0,0 +1,17 @@ +package com.dku.springstudy.auth; + +import lombok.Getter; + +@Getter +public class SecretKey { + + private final String jwtSecretKey; + private final Long jwtValidityTime; + private final Long refreshValidityTime; + + public SecretKey(String jwtSecretKey, Long jwtValidityTime, Long refreshValidityTime) { + this.jwtSecretKey = jwtSecretKey; + this.jwtValidityTime = jwtValidityTime; + this.refreshValidityTime = refreshValidityTime; + } +} diff --git a/src/main/java/com/dku/springstudy/auth/UserPrincipal.java b/src/main/java/com/dku/springstudy/auth/UserPrincipal.java new file mode 100644 index 0000000..fa876a4 --- /dev/null +++ b/src/main/java/com/dku/springstudy/auth/UserPrincipal.java @@ -0,0 +1,56 @@ +package com.dku.springstudy.auth; + +import com.dku.springstudy.member.entity.Member; +import lombok.Getter; +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.Collections; + + +@Getter +public class UserPrincipal implements UserDetails { + private Member member; + + public UserPrincipal(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getMemberRole().name()); + return Collections.singletonList(authority); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } +} diff --git a/src/main/java/com/dku/springstudy/auth/exception/TokenException.java b/src/main/java/com/dku/springstudy/auth/exception/TokenException.java new file mode 100644 index 0000000..46281a3 --- /dev/null +++ b/src/main/java/com/dku/springstudy/auth/exception/TokenException.java @@ -0,0 +1,10 @@ +package com.dku.springstudy.auth.exception; + +import com.dku.springstudy.error.ApplicationException; +import com.dku.springstudy.error.ErrorCode; + +public class TokenException extends ApplicationException { + public TokenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/dku/springstudy/error/ApplicationException.java b/src/main/java/com/dku/springstudy/error/ApplicationException.java new file mode 100644 index 0000000..bb67040 --- /dev/null +++ b/src/main/java/com/dku/springstudy/error/ApplicationException.java @@ -0,0 +1,19 @@ +package com.dku.springstudy.error; + +import lombok.Getter; + +@Getter +public class ApplicationException extends RuntimeException { + + private final ErrorCode errorCode; + + public ApplicationException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + protected ApplicationException(String message, ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/dku/springstudy/error/ApplicationExceptionHandler.java b/src/main/java/com/dku/springstudy/error/ApplicationExceptionHandler.java new file mode 100644 index 0000000..f1eb2c7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/error/ApplicationExceptionHandler.java @@ -0,0 +1,18 @@ +package com.dku.springstudy.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Slf4j +public class ApplicationExceptionHandler { + + @ExceptionHandler + protected ResponseEntity handleApplicationException(final ApplicationException exception) { + log.error("handleApplicationException", exception); + final ErrorCode errorCode = exception.getErrorCode(); + final ErrorResponse response = ErrorResponse.from(errorCode); + return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getHttpStatus())); + } +} diff --git a/src/main/java/com/dku/springstudy/error/ErrorCode.java b/src/main/java/com/dku/springstudy/error/ErrorCode.java new file mode 100644 index 0000000..f7dd428 --- /dev/null +++ b/src/main/java/com/dku/springstudy/error/ErrorCode.java @@ -0,0 +1,21 @@ +package com.dku.springstudy.error; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; + +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +@Getter +public enum ErrorCode { + ALREADY_EXIST(400, "G001", "이미 존재합니다"), + NOT_EXIST(400, "G002", "존재하지 않습니다"), + TOKEN_VALIDATE_FAILED(400, "A001", "인증 토큰이 잘못되었습니다"); + + private final int httpStatus; + private final String code; + private final String message; + ErrorCode(final int httpStatus, final String code, final String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/com/dku/springstudy/error/ErrorResponse.java b/src/main/java/com/dku/springstudy/error/ErrorResponse.java new file mode 100644 index 0000000..0e6f825 --- /dev/null +++ b/src/main/java/com/dku/springstudy/error/ErrorResponse.java @@ -0,0 +1,76 @@ +package com.dku.springstudy.error; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.validation.BindingResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ErrorResponse { + + private int httpStatus; + private String code; + private String message; + private List errors; + + private ErrorResponse(final ErrorCode errorcode) { + this.httpStatus = errorcode.getHttpStatus(); + this.code = errorcode.getCode(); + this.message = errorcode.getMessage(); + this.errors = new ArrayList<>(); + } + + private ErrorResponse(final ErrorCode errorCode, final List errors) { + this.httpStatus = errorCode.getHttpStatus(); + this.code = errorCode.getCode(); + this.message = errorCode.getMessage(); + this.errors = errors; + } + + public static ErrorResponse from(final ErrorCode errorCode) { + return new ErrorResponse(errorCode); + } + + public static ErrorResponse of(final ErrorCode errorCode, final List errors) { + return new ErrorResponse(errorCode, errors); + } + + public void changeMessage(String message) { + this.message = message; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class FieldError { + private String field; + private String value; + private String reason; + + public FieldError(String field, String value,String reason) { + this.field = field; + this.value = value; + this.reason = reason; + } + + public static List of(final String field, final String value, final String reason) { + List fieldErrors = new ArrayList<>(); + fieldErrors.add(new FieldError(field, value, reason)); + return fieldErrors; + } + + private static List of(final BindingResult bindingResult) { + final List fieldErrors = bindingResult.getFieldErrors(); + return fieldErrors.stream() + .map(error -> new FieldError( + error.getField(), + error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(), + error.getDefaultMessage())) + .collect(Collectors.toList()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/global/BaseEntity.java b/src/main/java/com/dku/springstudy/global/BaseEntity.java new file mode 100644 index 0000000..944b86d --- /dev/null +++ b/src/main/java/com/dku/springstudy/global/BaseEntity.java @@ -0,0 +1,29 @@ +package com.dku.springstudy.global; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +/** + * @author 최재민 + */ +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseEntity { + + @CreatedDate + @Column(updatable = false, name = "created_at") + private LocalDateTime createDate; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updateDate; + +} diff --git a/src/main/java/com/dku/springstudy/global/PageResponse.java b/src/main/java/com/dku/springstudy/global/PageResponse.java new file mode 100644 index 0000000..5bb9143 --- /dev/null +++ b/src/main/java/com/dku/springstudy/global/PageResponse.java @@ -0,0 +1,36 @@ +package com.dku.springstudy.global; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.io.Serializable; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PageResponse implements Serializable { + + private List content; + private boolean hasNext; + private int totalPages; + private long totalElements; + private int page; + private int size; + private boolean first; + private boolean last; + + public PageResponse(List content, Pageable pageable, long totalCount){ + final PageImpl page = new PageImpl<>(content, pageable, totalCount); + this.content = content; + this.hasNext = page.hasNext(); + this.totalPages = page.getTotalPages(); + this.totalElements = page.getTotalElements(); + this.page = page.getNumber()+1; + this.size = page.getSize(); + this.first = page.isFirst(); + this.last = page.isLast(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/global/SecurityConfig.java b/src/main/java/com/dku/springstudy/global/SecurityConfig.java new file mode 100644 index 0000000..75f0723 --- /dev/null +++ b/src/main/java/com/dku/springstudy/global/SecurityConfig.java @@ -0,0 +1,74 @@ +package com.dku.springstudy.global; + +import com.dku.springstudy.auth.JwtAuthenticationFilter; +import com.dku.springstudy.auth.JwtProvider; +import com.dku.springstudy.member.repository.MemberRepository; +import com.dku.springstudy.member.service.MemberDetailService; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@RequiredArgsConstructor +@EnableWebSecurity +@Configuration +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final MemberDetailService memberDetailService; + + private final JwtProvider jwtProvider; + + @Override + public void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .headers().frameOptions().disable() + .and() + .authorizeRequests() + .antMatchers("/login", "/signup", "/").permitAll() + .antMatchers(HttpMethod.POST, "/item").hasRole("USER") + .anyRequest().authenticated() + .and() + .exceptionHandling() + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); + + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(memberDetailService) + .passwordEncoder(new BCryptPasswordEncoder()); + } + + @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + public void configure(WebSecurity web) { + web.ignoring().antMatchers( + // -- Static resources + "/css/**", "/images/**", "/js/**" + // -- Swagger UI v2 + , "/v2/api-docs", "/swagger-resources/**" + , "/swagger-ui.html", "/webjars/**", "/swagger/**" + // -- Swagger UI v3 (Open API) + , "/v3/api-docs/**", "/swagger-ui/**" + ); } +} diff --git a/src/main/java/com/dku/springstudy/global/SwaggerConfig.java b/src/main/java/com/dku/springstudy/global/SwaggerConfig.java new file mode 100644 index 0000000..03b06f4 --- /dev/null +++ b/src/main/java/com/dku/springstudy/global/SwaggerConfig.java @@ -0,0 +1,50 @@ +package com.dku.springstudy.global; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +@Configuration +public class SwaggerConfig implements WebMvcConfigurer { + + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/swagger-ui.html") + .addResourceLocations("classpath:/META-INF/resources/"); + + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/"); + // -- Static resources + registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); + registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css"); + registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/"); + registry.addResourceHandler("/images/**").addResourceLocations("classpath:/static/images/"); + } + + @Bean + public Docket api() { + return new Docket(DocumentationType.OAS_30) + .useDefaultResponseMessages(false) + .select() + .apis(RequestHandlerSelectors.basePackage("com.dku.springstudy")) + .paths(PathSelectors.any()) + .build() + .apiInfo(apiInfo()); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("단국대 스프링 스터디") + .description("당근마켓 클론 REST API") + .contact(new Contact("dku19jam","https://www.github.com/dku19jam","panzzang518@gmail.com")) + .version("1.0") + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/item/controller/ItemController.java b/src/main/java/com/dku/springstudy/item/controller/ItemController.java new file mode 100644 index 0000000..60ded41 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/controller/ItemController.java @@ -0,0 +1,50 @@ +package com.dku.springstudy.item.controller; + +import com.dku.springstudy.auth.JwtProvider; +import com.dku.springstudy.global.PageResponse; +import com.dku.springstudy.item.dto.CreateItemDto; +import com.dku.springstudy.item.dto.ItemResponseDto; +import com.dku.springstudy.item.service.ItemService; +import com.dku.springstudy.member.entity.Member; +import com.dku.springstudy.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequiredArgsConstructor +public class ItemController { + + private final ItemService itemService; + private final JwtProvider jwtProvider; + private final MemberRepository memberRepository; + + @PostMapping + public ResponseEntity createItem(HttpServletRequest request, + @RequestBody CreateItemDto dto) { + String token = jwtProvider.resolveToken(request); + Member member = memberRepository.findByEmail(jwtProvider.extractEmail(token)).orElseThrow(); + Long item = itemService.createItem(member, dto); + + return ResponseEntity.ok().body(item); + } + + @GetMapping("/{id}") + public ResponseEntity getOneItem(@RequestBody Long itemId) { + ItemResponseDto item = itemService.getItemById(itemId); + + return ResponseEntity.ok().body(item); + } + + @GetMapping + public PageResponse getAllItem(@RequestParam(value = "query") String query, + @RequestParam(value = "category") String category, + Pageable pageable) { + Page items = itemService.getItemByParam(query, category, pageable); + return new PageResponse<>(items.getContent(), items.getPageable(), items.getTotalElements()); + } +} diff --git a/src/main/java/com/dku/springstudy/item/dto/CreateItemDto.java b/src/main/java/com/dku/springstudy/item/dto/CreateItemDto.java new file mode 100644 index 0000000..454b0f1 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/dto/CreateItemDto.java @@ -0,0 +1,18 @@ +package com.dku.springstudy.item.dto; + +import com.dku.springstudy.item.entity.Category; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class CreateItemDto { + private String title; + private Long price; + private Category category; + private String content; + + private List files = new ArrayList<>(); +} diff --git a/src/main/java/com/dku/springstudy/item/dto/ItemResponseDto.java b/src/main/java/com/dku/springstudy/item/dto/ItemResponseDto.java new file mode 100644 index 0000000..6ed0e83 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/dto/ItemResponseDto.java @@ -0,0 +1,39 @@ +package com.dku.springstudy.item.dto; + +import com.dku.springstudy.item.entity.Category; +import com.dku.springstudy.item.entity.Image; +import com.dku.springstudy.item.entity.Item; +import com.dku.springstudy.item.entity.Status; +import com.dku.springstudy.like.entity.Like; +import com.dku.springstudy.member.entity.Member; + +import javax.persistence.*; +import java.util.List; + +public class ItemResponseDto { + private String title; + + private Long price; + + private List image; + + private String seller; + + private Category category; + + private String content; + + private Status status; + + private int likeCount; + + public ItemResponseDto(Item item) { + this.title = item.getTitle(); + this.content = item.getContent(); + this.image = item.getImage(); + this.seller = item.getSeller().getName(); + this.category = item.getCategory(); + this.status = item.getStatus(); + this.likeCount = item.getLikes().size(); + } +} diff --git a/src/main/java/com/dku/springstudy/item/entity/Category.java b/src/main/java/com/dku/springstudy/item/entity/Category.java new file mode 100644 index 0000000..edd6752 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/entity/Category.java @@ -0,0 +1,29 @@ +package com.dku.springstudy.item.entity; + +/** + * @author 최재민 + */ +public enum Category { + DIGITAL_DEVICE("디지털기기"), + HOUSEHOLD_APPLIANCE("생활가전"), + FURNITURE("가구"), + CHILDREN("유아동"), + PROCESSED_FOOD("생활/가공식품"), + CHILDREN_BOOK("유아도서"), + WOMEN_CLOTHING("여성의류"), + MEN_CLOTHING("남성의류"), + LEISURE("게임/취미"), + BEAUTY("뷰티"), + PET("반려동물용품"), + BOOK("도서/티켓/음반"), + PLANT("식물"), + CAR("중고차"), + OTHERS("기타 중고물품") + ; + + + private String name; + Category(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/dku/springstudy/item/entity/Image.java b/src/main/java/com/dku/springstudy/item/entity/Image.java new file mode 100644 index 0000000..3119567 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/entity/Image.java @@ -0,0 +1,22 @@ +package com.dku.springstudy.item.entity; + + +import com.dku.springstudy.global.BaseEntity; + +import javax.persistence.*; + +@Entity +public class Image extends BaseEntity { + + @Id + @Column(name = "image_id") + @GeneratedValue + private String id; + + private String fileName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + +} diff --git a/src/main/java/com/dku/springstudy/item/entity/Item.java b/src/main/java/com/dku/springstudy/item/entity/Item.java new file mode 100644 index 0000000..dd3cbda --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/entity/Item.java @@ -0,0 +1,52 @@ +package com.dku.springstudy.item.entity; + +import com.dku.springstudy.global.BaseEntity; +import com.dku.springstudy.like.entity.Like; +import com.dku.springstudy.member.entity.Member; +import lombok.*; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author 최재민 + */ +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +public class Item extends BaseEntity { + + @Id + @GeneratedValue + @Column(name = "item_id") + private Long id; + + private String title; + + private Long price; + + @OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true) + private List image = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "member_id") + private Member seller; + + @Enumerated(EnumType.STRING) + private Category category; + + @Lob + private String content; + + private Status status; + + @OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes = new ArrayList<>(); + + public void addLike(Like like) { + this.likes.add(like); + } +} diff --git a/src/main/java/com/dku/springstudy/item/entity/Status.java b/src/main/java/com/dku/springstudy/item/entity/Status.java new file mode 100644 index 0000000..87b7915 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/entity/Status.java @@ -0,0 +1,17 @@ +package com.dku.springstudy.item.entity; + +/** + * @author 최재민 + */ + +public enum Status { + ON_SALE("판매중"), + RESERVED("예약완료"), + SOLD("판매완료"); + + private String name; + + Status(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/dku/springstudy/item/repository/ItemRepository.java b/src/main/java/com/dku/springstudy/item/repository/ItemRepository.java new file mode 100644 index 0000000..aaafce3 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/repository/ItemRepository.java @@ -0,0 +1,7 @@ +package com.dku.springstudy.item.repository; + +import com.dku.springstudy.item.entity.Item; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ItemRepository extends JpaRepository, ItemRepositoryCustom{ +} diff --git a/src/main/java/com/dku/springstudy/item/repository/ItemRepositoryCustom.java b/src/main/java/com/dku/springstudy/item/repository/ItemRepositoryCustom.java new file mode 100644 index 0000000..9db812b --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/repository/ItemRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.dku.springstudy.item.repository; + +import com.dku.springstudy.item.entity.Category; +import com.dku.springstudy.item.entity.Item; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ItemRepositoryCustom { + Page findItemByParam(String query, String category, Pageable pageable); +} diff --git a/src/main/java/com/dku/springstudy/item/repository/ItemRepositoryImpl.java b/src/main/java/com/dku/springstudy/item/repository/ItemRepositoryImpl.java new file mode 100644 index 0000000..e36ce22 --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/repository/ItemRepositoryImpl.java @@ -0,0 +1,49 @@ +package com.dku.springstudy.item.repository; + +import com.dku.springstudy.item.entity.Category; +import com.dku.springstudy.item.entity.Item; +import com.dku.springstudy.item.entity.Status; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; + +import javax.persistence.EntityManager; + +import java.util.List; + +import static com.dku.springstudy.item.entity.QItem.item; + +public class ItemRepositoryImpl implements ItemRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public ItemRepositoryImpl(EntityManager entityManager) { + this.queryFactory = new JPAQueryFactory(entityManager); + } + @Override + public Page findItemByParam(String query, String category, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(item.category.eq(Category.valueOf(category))) + .and(item.status.ne(Status.SOLD)); + + if (query != null) { + builder.and(item.title.contains(query)); + } + + List items = queryFactory + .selectFrom(item) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .selectFrom(item) + .where(builder); + + return PageableExecutionUtils.getPage(items, pageable, () -> countQuery.fetch().size()); + + } +} diff --git a/src/main/java/com/dku/springstudy/item/service/ItemService.java b/src/main/java/com/dku/springstudy/item/service/ItemService.java new file mode 100644 index 0000000..95c111f --- /dev/null +++ b/src/main/java/com/dku/springstudy/item/service/ItemService.java @@ -0,0 +1,48 @@ +package com.dku.springstudy.item.service; + +import com.dku.springstudy.item.dto.ItemResponseDto; +import com.dku.springstudy.item.entity.Item; +import com.dku.springstudy.item.entity.Status; +import com.dku.springstudy.item.repository.ItemRepository; +import com.dku.springstudy.item.dto.CreateItemDto; +import com.dku.springstudy.member.entity.Member; +import com.dku.springstudy.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class ItemService { + private final ItemRepository itemRepository; + private final MemberRepository memberRepository; + + public Long createItem(Member member, CreateItemDto dto) { + Item item = Item.builder() + .title(dto.getTitle()) + .price(dto.getPrice()) + .category(dto.getCategory()) + .content(dto.getContent()) + .seller(member) + .status(Status.ON_SALE) +// .image() 이미지 저장 시 추가 + .build(); + + Item save = itemRepository.save(item); + return save.getId(); + } + + public ItemResponseDto getItemById(Long itemId) { + Item item = itemRepository.findById(itemId).orElseThrow(); + return new ItemResponseDto(item); + } + + public Page getItemByParam(String query, String category, Pageable pageable) { + Page itemByParam = itemRepository.findItemByParam(query, category, pageable); + return itemByParam + .map(ItemResponseDto::new); + } +} diff --git a/src/main/java/com/dku/springstudy/like/controller/LikeController.java b/src/main/java/com/dku/springstudy/like/controller/LikeController.java new file mode 100644 index 0000000..3fbb9c2 --- /dev/null +++ b/src/main/java/com/dku/springstudy/like/controller/LikeController.java @@ -0,0 +1,56 @@ +package com.dku.springstudy.like.controller; + +import com.dku.springstudy.auth.JwtProvider; +import com.dku.springstudy.item.entity.Item; +import com.dku.springstudy.item.repository.ItemRepository; +import com.dku.springstudy.like.dto.CreateLikeDto; +import com.dku.springstudy.like.entity.Like; +import com.dku.springstudy.like.repository.LikeRepository; +import com.dku.springstudy.like.service.LikeService; +import com.dku.springstudy.member.entity.Member; +import com.dku.springstudy.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/like") +public class LikeController { + + private final LikeService likeService; + private final ItemRepository itemRepository; + private final JwtProvider jwtProvider; + private final MemberRepository memberRepository; + private final LikeRepository likeRepository; + + @PostMapping + public ResponseEntity likeItem(@RequestBody CreateLikeDto dto, + HttpServletRequest request) { + String token = jwtProvider.resolveToken(request); + Member member = memberRepository.findByEmail(jwtProvider.extractEmail(token)).orElseThrow(); + Item item = itemRepository.findById(dto.getItemId()).orElseThrow(); + Like like = likeService.createLike(member, item); + Like save = likeRepository.save(like); + + return ResponseEntity.ok().body(save.getLikeId()); + } + + @PostMapping("/unCheck") + public ResponseEntity unCheckLike(@RequestBody CreateLikeDto dto, + HttpServletRequest request) { + String token = jwtProvider.resolveToken(request); + Member member = memberRepository.findByEmail(jwtProvider.extractEmail(token)).orElseThrow(); + Item item = itemRepository.findById(dto.getItemId()).orElseThrow(); + Like.LikeId likeId = new Like.LikeId(member.getId(), item.getId()); + Like like = likeRepository.findById(likeId).orElseThrow(); + like.unCheckLike(); + + return ResponseEntity.ok().body("성공!"); + } +} diff --git a/src/main/java/com/dku/springstudy/like/dto/CreateLikeDto.java b/src/main/java/com/dku/springstudy/like/dto/CreateLikeDto.java new file mode 100644 index 0000000..8d804bb --- /dev/null +++ b/src/main/java/com/dku/springstudy/like/dto/CreateLikeDto.java @@ -0,0 +1,13 @@ +package com.dku.springstudy.like.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class CreateLikeDto { + private Long itemId; +} diff --git a/src/main/java/com/dku/springstudy/like/entity/Like.java b/src/main/java/com/dku/springstudy/like/entity/Like.java new file mode 100644 index 0000000..04cb293 --- /dev/null +++ b/src/main/java/com/dku/springstudy/like/entity/Like.java @@ -0,0 +1,60 @@ +package com.dku.springstudy.like.entity; + +import com.dku.springstudy.global.BaseEntity; +import com.dku.springstudy.item.entity.Item; +import com.dku.springstudy.member.entity.Member; + +import lombok.*; + +import javax.persistence.*; +import java.io.Serializable; + +@Entity +@Getter +@Table(name = "likes") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Like extends BaseEntity { + + @EmbeddedId + private LikeId likeId; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("memberId") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("itemId") + private Item item; + + private boolean checked; + + + /** + * composite key 사용 + */ + @Embeddable + @Getter @Setter + @EqualsAndHashCode + @AllArgsConstructor + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class LikeId implements Serializable { + @Column(name = "item_id") + private Long itemId; + @Column(name = "member_id") + private Long memberId; + } + + public Like(Member member, Item item) { + this.member = member; + this.item = item; + this.checked = true; + } + + public void unCheckLike() { + this.checked = false; + } + + public void checkLike() { + this.checked = true; + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/like/repository/LikeRepository.java b/src/main/java/com/dku/springstudy/like/repository/LikeRepository.java new file mode 100644 index 0000000..61b4e0a --- /dev/null +++ b/src/main/java/com/dku/springstudy/like/repository/LikeRepository.java @@ -0,0 +1,8 @@ +package com.dku.springstudy.like.repository; + +import com.dku.springstudy.like.entity.Like; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeRepository extends JpaRepository { + +} diff --git a/src/main/java/com/dku/springstudy/like/service/LikeService.java b/src/main/java/com/dku/springstudy/like/service/LikeService.java new file mode 100644 index 0000000..05c2c41 --- /dev/null +++ b/src/main/java/com/dku/springstudy/like/service/LikeService.java @@ -0,0 +1,38 @@ +package com.dku.springstudy.like.service; + +import com.dku.springstudy.item.entity.Item; +import com.dku.springstudy.like.entity.Like; +import com.dku.springstudy.like.repository.LikeRepository; +import com.dku.springstudy.member.entity.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Transactional +@RequiredArgsConstructor +public class LikeService { + + private final LikeRepository likeRepository; + + public Like createLike(Member member, Item item) { + Like.LikeId likeId = new Like.LikeId(member.getId(), item.getId()); + Like like; + Optional byId = likeRepository.findById(likeId); + if (byId.isPresent()) { + like = byId.get(); + like.checkLike(); + }else { + like = new Like(member, item); + member.addLike(like); + item.addLike(like); + likeRepository.save(like); + } + return like; + } + + public void unCheckLike(Like.LikeId likeId) { + Like like = likeRepository.findById(likeId).orElseThrow(); + like.unCheckLike(); + } +} diff --git a/src/main/java/com/dku/springstudy/member/controller/MemberController.java b/src/main/java/com/dku/springstudy/member/controller/MemberController.java new file mode 100644 index 0000000..19326c6 --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/controller/MemberController.java @@ -0,0 +1,30 @@ +package com.dku.springstudy.member.controller; + +import com.dku.springstudy.member.dto.LoginRequestDto; +import com.dku.springstudy.member.dto.LoginResponseDto; +import com.dku.springstudy.member.dto.SignupRequestDto; +import com.dku.springstudy.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody SignupRequestDto request) { + memberService.signup(request); + return ResponseEntity.ok().body("회원가입에 성공하셨습니다."); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequestDto request) { + LoginResponseDto member = memberService.login(request); + return ResponseEntity.ok().body(member); + } +} diff --git a/src/main/java/com/dku/springstudy/member/dto/LoginRequestDto.java b/src/main/java/com/dku/springstudy/member/dto/LoginRequestDto.java new file mode 100644 index 0000000..838c45d --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/dto/LoginRequestDto.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.member.dto; + +import lombok.Data; + +@Data +public class LoginRequestDto { + + private String email; + + private String password; +} diff --git a/src/main/java/com/dku/springstudy/member/dto/LoginResponseDto.java b/src/main/java/com/dku/springstudy/member/dto/LoginResponseDto.java new file mode 100644 index 0000000..a8ab8f6 --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/dto/LoginResponseDto.java @@ -0,0 +1,14 @@ +package com.dku.springstudy.member.dto; + +import lombok.Data; + +@Data +public class LoginResponseDto { + private final String accessToken; + private final Long tokenExpireTime; + + public LoginResponseDto(String accessToken, Long tokenExpireTime) { + this.accessToken = accessToken; + this.tokenExpireTime = tokenExpireTime; + } +} diff --git a/src/main/java/com/dku/springstudy/member/dto/SignupRequestDto.java b/src/main/java/com/dku/springstudy/member/dto/SignupRequestDto.java new file mode 100644 index 0000000..0c60026 --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/dto/SignupRequestDto.java @@ -0,0 +1,17 @@ +package com.dku.springstudy.member.dto; + +import com.dku.springstudy.member.entity.MemberROLE; +import lombok.Data; + +@Data +public class SignupRequestDto { + private String email; + + private String password; + + private String name; + + private String nickname; + + private String phoneNumber; +} diff --git a/src/main/java/com/dku/springstudy/member/dto/TokenResponse.java b/src/main/java/com/dku/springstudy/member/dto/TokenResponse.java new file mode 100644 index 0000000..ad4843e --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/dto/TokenResponse.java @@ -0,0 +1,9 @@ +package com.dku.springstudy.member.dto; + +import lombok.Data; + +@Data +public class TokenResponse { + + private final String accessToken; +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/member/entity/Member.java b/src/main/java/com/dku/springstudy/member/entity/Member.java new file mode 100644 index 0000000..0b2a428 --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/entity/Member.java @@ -0,0 +1,53 @@ +package com.dku.springstudy.member.entity; + +import com.dku.springstudy.global.BaseEntity; +import com.dku.springstudy.item.entity.Item; +import com.dku.springstudy.like.entity.Like; +import lombok.*; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author 최재민 + */ +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@Builder +public class Member extends BaseEntity { + + @Id + @GeneratedValue + @Column(name = "member_id") + private Long id; + + @Column(unique = true) + private String email; + + private String password; + + private String name; + + private String nickname; + + @Enumerated(EnumType.STRING) + private MemberROLE memberRole; + + @Column(name = "phone_number") + private String phoneNumber; + + private String profileImageUrl; + + @OneToMany(mappedBy = "seller", cascade = CascadeType.ALL, orphanRemoval = true) + private List products = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes = new ArrayList<>(); + + public void addLike(Like like) { + this.likes.add(like); + } +} diff --git a/src/main/java/com/dku/springstudy/member/entity/MemberROLE.java b/src/main/java/com/dku/springstudy/member/entity/MemberROLE.java new file mode 100644 index 0000000..5ec7271 --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/entity/MemberROLE.java @@ -0,0 +1,16 @@ +package com.dku.springstudy.member.entity; + +public enum MemberROLE { + //Config에서 검사 시 ROLE + config에 등록한 이름으로 검사함... + ROLE_USER("USER"), + ROLE_ADMIN("ADMIN"), + ; + + final String role; + + MemberROLE(String role) { + this.role = role; + } + + public String role() { return role; } +} diff --git a/src/main/java/com/dku/springstudy/member/repository/MemberRepository.java b/src/main/java/com/dku/springstudy/member/repository/MemberRepository.java new file mode 100644 index 0000000..672fa9c --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/repository/MemberRepository.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.member.repository; + +import com.dku.springstudy.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); +} diff --git a/src/main/java/com/dku/springstudy/member/service/MemberDetailService.java b/src/main/java/com/dku/springstudy/member/service/MemberDetailService.java new file mode 100644 index 0000000..902225c --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/service/MemberDetailService.java @@ -0,0 +1,34 @@ +package com.dku.springstudy.member.service; + +import com.dku.springstudy.member.entity.Member; +import com.dku.springstudy.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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 java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class MemberDetailService implements UserDetailsService{ + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + if (email.isBlank()) { +// throw new EmailEmptyException(); + } + + Optional optionalUser = memberRepository.findByEmail(email); + Member member = optionalUser.orElseThrow(); + + Collection authorities = new ArrayList<>(); + return new org.springframework.security.core.userdetails.User(member.getEmail(), member.getPassword(), authorities); + } +} diff --git a/src/main/java/com/dku/springstudy/member/service/MemberService.java b/src/main/java/com/dku/springstudy/member/service/MemberService.java new file mode 100644 index 0000000..3502d52 --- /dev/null +++ b/src/main/java/com/dku/springstudy/member/service/MemberService.java @@ -0,0 +1,63 @@ +package com.dku.springstudy.member.service; + + +import com.dku.springstudy.member.dto.LoginRequestDto; +import com.dku.springstudy.member.dto.LoginResponseDto; +import com.dku.springstudy.member.dto.SignupRequestDto; +import com.dku.springstudy.member.dto.TokenResponse; +import com.dku.springstudy.member.entity.Member; +import com.dku.springstudy.member.entity.MemberROLE; +import com.dku.springstudy.member.repository.MemberRepository; +import com.dku.springstudy.auth.JwtProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author 최재민 + */ +@Service +@RequiredArgsConstructor +@Transactional +public class MemberService { + + private final AuthenticationManager authenticationManager; + private final MemberRepository memberRepository; + private final JwtProvider jwtProvider; + + public void signup(SignupRequestDto request) { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String encodedPassword = encoder.encode(request.getPassword()); + Member member = Member.builder() + .name(request.getName()) + .email(request.getEmail()) + .password(encodedPassword) + .phoneNumber(request.getPhoneNumber()) + .nickname(request.getNickname()) + .memberRole(MemberROLE.ROLE_USER) + .build(); + + memberRepository.save(member); + } + + + public LoginResponseDto login(LoginRequestDto loginRequest) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); + TokenResponse createToken = createTokenReturn(loginRequest); + Long tokenExpireTime = jwtProvider.getTokenExpireTime(createToken.getAccessToken()); + return new LoginResponseDto( + createToken.getAccessToken(), + tokenExpireTime); + } + + private TokenResponse createTokenReturn(LoginRequestDto loginRequest) { + + String accessToken = jwtProvider.createAccessToken(loginRequest.getEmail()); + + return new TokenResponse(accessToken); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d9251ad --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,28 @@ +spring: + sql: + init: + mode: always + servlet: + multipart: + max-file-size: 15MB + max-request-size: 15MB + datasource: + url: jdbc:postgresql://localhost:5432/carrot + username: postgres + password: 32194691 + driver-class-name: org.postgresql.Driver + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + database: postgresql + database-platform: org.hibernate.dialect.PostgreSQLDialect + generate-ddl: true + show-sql: true + mvc: + pathmatch: + matching-strategy: ant_path_matcher + + diff --git "a/\354\265\234\354\236\254\353\257\274/1\354\243\274\354\260\250 \352\263\274\354\240\234.txt" "b/\354\265\234\354\236\254\353\257\274/1\354\243\274\354\260\250 \352\263\274\354\240\234.txt" new file mode 100644 index 0000000..e69de29