diff --git a/.gitignore b/.gitignore index c2065bc..649136d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +*.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..ead592a --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +> 당근마켓 backend server project + +

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

+ +
+ +### 1주차 + +> 회원가입 및 로그인 기능 +- 요구사항 분석 +- 도메인 모델 및 테이블 설계 고민 +- JWT 구현 및 테스트 코드 작성 +- 이메일 인증하기 + +
+ +### 2주차 + +> 초기 도메인 모델 및 테이블 설계 구현 +- 상품 등록, 수정, 삭제 기능 +- 마이페이지(회원 수정 기능) + +
+ +
+ +### 3주차 + +> 여러개의 테이블이 얽힌 화면에 맞는 DTO설정 및 API구현 + +
+ +## 개발 기록 + +- 회원가입 & 로그인 API 구현 ```commit``` : [2d556a0](https://github.com/ji-hyeon97/Spring-JPA-study/commit/2d556a0b00a58f70c7d10b7e15a1170d2d8b27a8) +- SMPT 이메일 인증 API 구현 ```commit``` : [3fd9d89](https://github.com/ji-hyeon97/Spring-JPA-study/commit/3fd9d898213aa011d04684e3c5fa40c10f0d1ca9) +- access 토큰 재발급 API 구현 ```commit``` : [6851e06](https://github.com/ji-hyeon97/Spring-JPA-study/commit/6851e06e80ce4e7688d169878cca5806d32592fd) +- AWS S3 연동 및 다중 이미지 업로드 API 구현 ```commit``` : [930beb3](https://github.com/ji-hyeon97/Spring-JPA-study/commit/930beb3b92a0449e94f22ce436d5174a7d4c6d46) +- 좋아요 API 구현 ```commit``` : [a2b1cd6](https://github.com/ji-hyeon97/Spring-JPA-study/commit/a2b1cd6f38c901c66067dff0e09e7fc55ac2c572) +- 상품 수정 및 삭제 API 구현 ```commit``` : [2925870](https://github.com/ji-hyeon97/Spring-JPA-study/commit/29258702188606488dc7aabc1c665a78b34096a4) , [a2fb7d0](https://github.com/ji-hyeon97/Spring-JPA-study/commit/a2fb7d08234ea39b7c3d08f1a4b474ed20188984) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 15b77ef..b1fd040 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,11 @@ plugins { + id 'org.springframework.boot' version '2.7.4' + id 'io.spring.dependency-management' version '1.0.14.RELEASE' id 'java' - id 'org.springframework.boot' version '3.0.1' - id 'io.spring.dependency-management' version '1.1.0' } - group = 'com.dku' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +sourceCompatibility = '11' configurations { compileOnly { @@ -19,16 +18,25 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' + implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + implementation 'org.springframework.boot:spring-boot-starter-mail:2.7.1' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.12.349' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5' + developmentOnly 'org.springframework.boot:spring-boot-devtools' 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' + runtimeOnly 'com.h2database:h2' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } } -tasks.named('test') { +test { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/SpringStudyApplication.java b/src/main/java/com/dku/springstudy/SpringStudyApplication.java index ef164c9..1bef475 100644 --- a/src/main/java/com/dku/springstudy/SpringStudyApplication.java +++ b/src/main/java/com/dku/springstudy/SpringStudyApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class SpringStudyApplication { diff --git a/src/main/java/com/dku/springstudy/config/AwsS3Config.java b/src/main/java/com/dku/springstudy/config/AwsS3Config.java new file mode 100644 index 0000000..332fb91 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/AwsS3Config.java @@ -0,0 +1,30 @@ +package com.dku.springstudy.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsS3Config { + @Value("${spring.cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${spring.cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${spring.cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) + .build(); + } +} diff --git a/src/main/java/com/dku/springstudy/config/MailConfig.java b/src/main/java/com/dku/springstudy/config/MailConfig.java new file mode 100644 index 0000000..71a8e28 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/MailConfig.java @@ -0,0 +1,43 @@ +package com.dku.springstudy.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class MailConfig{ + @Value("${mail.host}") + private String host; + + @Value("${mail.id}") + private String id; + + @Value("${mail.password}") + private String password; + + @Bean + public JavaMailSender javaMailService() { + JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); + javaMailSender.setHost(host); + javaMailSender.setUsername(id); + javaMailSender.setPassword(password); + javaMailSender.setPort(465); + javaMailSender.setJavaMailProperties(getMailProperties()); + return javaMailSender; + } + + private Properties getMailProperties() { + Properties properties = new Properties(); + properties.setProperty("mail.transport.protocol", "smtp"); + properties.setProperty("mail.smtp.auth", "true"); + properties.setProperty("mail.smtp.starttls.enable", "true"); + properties.setProperty("mail.debug", "true"); + properties.setProperty("mail.smtp.ssl.trust","smtp.naver.com"); + properties.setProperty("mail.smtp.ssl.enable","true"); + return properties; + } +} diff --git a/src/main/java/com/dku/springstudy/config/security/AppConfig.java b/src/main/java/com/dku/springstudy/config/security/AppConfig.java new file mode 100644 index 0000000..fb67415 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/AppConfig.java @@ -0,0 +1,47 @@ +package com.dku.springstudy.config.security; + +import com.dku.springstudy.config.security.jwt.CustomAuthenticationEntryPoint; +import com.dku.springstudy.config.security.jwt.JwtAuthenticationFilter; +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.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.filter.CorsFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class AppConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + + @Bean + public PasswordEncoder passwordEncoder(){ + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ + return http + .csrf().disable() + .httpBasic().disable() + .exceptionHandling() + .authenticationEntryPoint(customAuthenticationEntryPoint) + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/i/**").access("hasRole('ADMIN')") + .antMatchers("/account/**").permitAll() + .anyRequest().authenticated() + .and() + .addFilterAfter(jwtAuthenticationFilter, CorsFilter.class) + .build(); + } +} diff --git a/src/main/java/com/dku/springstudy/config/security/LazyLoadingProxyConfig.java b/src/main/java/com/dku/springstudy/config/security/LazyLoadingProxyConfig.java new file mode 100644 index 0000000..006e6b0 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/LazyLoadingProxyConfig.java @@ -0,0 +1,13 @@ +package com.dku.springstudy.config.security; + +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class LazyLoadingProxyConfig { + @Bean + Hibernate5Module hibernate5Module() { + return new Hibernate5Module(); + } +} diff --git a/src/main/java/com/dku/springstudy/config/security/RedisConfig.java b/src/main/java/com/dku/springstudy/config/security/RedisConfig.java new file mode 100644 index 0000000..56685d1 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/RedisConfig.java @@ -0,0 +1,36 @@ +package com.dku.springstudy.config.security; + + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.redis.host}") + private String host; + + @Value("${spring.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/config/security/RedisDao.java b/src/main/java/com/dku/springstudy/config/security/RedisDao.java new file mode 100644 index 0000000..ec88786 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/RedisDao.java @@ -0,0 +1,31 @@ +package com.dku.springstudy.config.security; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class RedisDao { + + private final RedisTemplate redisTemplate; + + public RedisDao(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void setValues(String key, String data, Duration duration) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data, duration); + } + + public String getValues(String key) { + ValueOperations values = redisTemplate.opsForValue(); + return values.get(key); + } + + public void deleteValues(String key) { + redisTemplate.delete(key); + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/config/security/SwaggerConfig.java b/src/main/java/com/dku/springstudy/config/security/SwaggerConfig.java new file mode 100644 index 0000000..afc8de1 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/SwaggerConfig.java @@ -0,0 +1,33 @@ +package com.dku.springstudy.config.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + private ApiInfo commonInfo(){ + return new ApiInfoBuilder() + .title("CarrotMarket API") + .version("1.0") + .build(); + } + @Bean + public Docket allApi(){ + return new Docket(DocumentationType.SWAGGER_2) + .groupName("USER") + .useDefaultResponseMessages(false) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build() + .apiInfo(commonInfo()); + } +} diff --git a/src/main/java/com/dku/springstudy/config/security/jwt/CustomAuthenticationEntryPoint.java b/src/main/java/com/dku/springstudy/config/security/jwt/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..63b7cbd --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/jwt/CustomAuthenticationEntryPoint.java @@ -0,0 +1,41 @@ +package com.dku.springstudy.config.security.jwt; + +import com.dku.springstudy.exception.JwtTokenError; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Getter +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + String exceptionMessage = (String) request.getAttribute("exception"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + JwtTokenError jwtTokenError = new JwtTokenError(exceptionMessage, HttpStatus.UNAUTHORIZED); + String res = this.convertObjectToJson(jwtTokenError); + response.getWriter().print(res); + } + + private String convertObjectToJson(Object object) throws JsonProcessingException { + return object == null ? null : objectMapper.writeValueAsString(object); + } +} diff --git a/src/main/java/com/dku/springstudy/config/security/jwt/JwtAuthenticationFilter.java b/src/main/java/com/dku/springstudy/config/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..87b0a38 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,100 @@ +package com.dku.springstudy.config.security.jwt; + +import com.dku.springstudy.dto.ResponseDTO; +import com.dku.springstudy.exception.Message; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +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; +import java.util.Date; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProvider jwtProvider; + private final ObjectMapper objectMapper; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + // 요청에서 토큰 가져오기 + String token = parseBearerToken(request); + log.info("Filter is running..."); + + if (token != null && !token.equalsIgnoreCase("null")) { + Claims claims = jwtProvider.validateAndGetUserId(token); + String requestURI = request.getRequestURI(); + System.out.println(claims); + if (claims.get("type").equals("RTK") && !requestURI.equals("/account/reissue")) { + throw new JwtException("토큰을 확인하세요."); + } + String userId = (String) claims.get("userId"); + String userRole = (String) claims.get("role"); + Date expiration = claims.getExpiration(); + log.info("Authenticated user ID : " + userId); + log.info("Authenticated expiration : " + expiration); + + AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userId, // 인증된 사용자의 정보. 문자열이 아니어도 아무것이나 넣을 수 있다. 보통 UserDetails 오브젝트를 넣음 + null, + AuthorityUtils.createAuthorityList(userRole)); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } + filterChain.doFilter(request, response); + } catch (ExpiredJwtException e) { + logger.error("Could not set user authentication in security context {}", e); + jwtExceptionHandler(response, Message.JWT_TOKEN_EXPIRED); + } catch (UnsupportedJwtException e) { + logger.error("Could not set user authentication in security context {}", e); + jwtExceptionHandler(response, Message.JWT_UNSUPPORTED); + } catch (MalformedJwtException e) { + logger.error("Could not set user authentication in security context {}", e); + jwtExceptionHandler(response, Message.JWT_MALFORMED); + } catch (SignatureException e) { + logger.error("Could not set user authentication in security context {}", e); + jwtExceptionHandler(response, Message.JWT_SIGNATURE); + } catch (IllegalArgumentException e) { + logger.error("Could not set user authentication in security context {}", e); + jwtExceptionHandler(response, Message.JWT_ILLEGAL_ARGUMENT); + } + } + private String parseBearerToken(HttpServletRequest request){ + String bearerToken = request.getHeader("Authorization"); + + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){ + return bearerToken.substring(7); + } + return null; + } + + private void jwtExceptionHandler(HttpServletResponse response, String message) throws IOException{ + ResponseDTO dto = new ResponseDTO<>(HttpStatus.UNAUTHORIZED.value(), message); + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setCharacterEncoding("UTF-8"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + objectMapper.writeValue(response.getWriter(), dto); + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/config/security/jwt/JwtProvider.java b/src/main/java/com/dku/springstudy/config/security/jwt/JwtProvider.java new file mode 100644 index 0000000..7ff17cd --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/jwt/JwtProvider.java @@ -0,0 +1,110 @@ +package com.dku.springstudy.config.security.jwt; + +import com.dku.springstudy.config.security.RedisDao; +import com.dku.springstudy.dto.user.TokensResponseDTO; +import com.dku.springstudy.dto.user.UserResponseDTO; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.time.Duration; +import java.util.Base64; +import java.util.Date; +import java.util.Objects; + +@Component +@RequiredArgsConstructor +public class JwtProvider { + private final RedisDao redisDao; + private final ObjectMapper objectMapper; + + @Value("${jwt.key}") + private String key; + + @Value("${jwt.live.atk}") + private Long atkLive; + + @Value("${jwt.live.rtk}") + private Long rtkLive; + + @PostConstruct + protected void init() { + key = Base64.getEncoder().encodeToString(key.getBytes()); + } + + public TokensResponseDTO reissueAtk(UserResponseDTO userResponse) throws JsonProcessingException { + String rtkInRedis = redisDao.getValues(userResponse.getEmail()); + if (Objects.isNull(rtkInRedis)) throw new IllegalStateException("인증 정보가 만료되었습니다."); + Subject atkSubject = Subject.atk( + userResponse.getUserId(), + userResponse.getEmail(), + userResponse.getNickname(), + userResponse.getRole()); + String atk = createToken(atkSubject, atkLive); + return new TokensResponseDTO(atk, null); + } + public TokensResponseDTO createTokensByLogin(UserResponseDTO userResponse) throws JsonProcessingException { + Subject atkSubject = Subject.atk( + userResponse.getUserId(), + userResponse.getEmail(), + userResponse.getNickname(), + userResponse.getRole()); + Subject rtkSubject = Subject.rtk( + userResponse.getUserId(), + userResponse.getEmail(), + userResponse.getNickname(), + userResponse.getRole()); + String atk = createToken(atkSubject, atkLive); + String rtk = createToken(rtkSubject, rtkLive); + redisDao.setValues(userResponse.getEmail(), rtk, Duration.ofMillis(rtkLive)); + return new TokensResponseDTO(atk, rtk); + } + + private String createToken(Subject subject, Long tokenLive) throws JsonProcessingException { + String subjectStr = objectMapper.writeValueAsString(subject); + Claims claims = Jwts.claims().setSubject(subjectStr); + Date date = new Date(); + System.out.println(subject.getType()); + return Jwts.builder() + .setSubject(claims.getId()) + .claim("userId", subject.getUserId()) + .claim("email",subject.getEmail()) + .claim("role",subject.getRole().name()) + .claim("type",subject.getType()) + .setIssuedAt(date) + .setExpiration(new Date(date.getTime() + tokenLive)) + .signWith(SignatureAlgorithm.HS256, key) + .compact(); + } + + public Subject getSubject(String atk) throws JsonProcessingException { + String subjectStr = Jwts.parser().setSigningKey(key).parseClaimsJws(atk).getBody().getSubject(); + return objectMapper.readValue(subjectStr, Subject.class); + } + + public Claims validateAndGetUserId(String token){ + Claims claims = Jwts.parser() + .setSigningKey(key) + .parseClaimsJws(token) + .getBody(); + return claims; + } + + public boolean isValidToken(String token) { + try { + Jwts.parser() + .setSigningKey(key) + .parseClaimsJws(token) + .getBody(); + return true; + }catch (Exception e){ + return false; + } + } +} diff --git a/src/main/java/com/dku/springstudy/config/security/jwt/Subject.java b/src/main/java/com/dku/springstudy/config/security/jwt/Subject.java new file mode 100644 index 0000000..c42c3c7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/security/jwt/Subject.java @@ -0,0 +1,30 @@ +package com.dku.springstudy.config.security.jwt; + +import com.dku.springstudy.model.Role; +import lombok.Getter; + +@Getter +public class Subject { + private final String userId; + private final String email; + private final String nickname; + private final String type; + + private final Role role; + + public Subject(String userId, String email, String nickname, String type, Role role) { + this.userId = userId; + this.email = email; + this.nickname = nickname; + this.type = type; + this.role = role; + } + + public static Subject atk(String userId, String email, String nickname, Role role) { + return new Subject(userId, email, nickname, "ATK", Role.USER); + } + + public static Subject rtk(String userId, String email, String nickname, Role role) { + return new Subject(userId, email, nickname, "RTK", Role.USER); + } +} diff --git a/src/main/java/com/dku/springstudy/controller/ItemController.java b/src/main/java/com/dku/springstudy/controller/ItemController.java new file mode 100644 index 0000000..eff8b35 --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/ItemController.java @@ -0,0 +1,73 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.dto.ItemsDTO; +import com.dku.springstudy.dto.ResponseDTO; +import com.dku.springstudy.model.Category; +import com.dku.springstudy.model.Images; +import com.dku.springstudy.model.Items; +import com.dku.springstudy.repository.ItemsRepository; +import com.dku.springstudy.service.ImageService; +import com.dku.springstudy.service.ItemsService; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class ItemController { + private final ItemsService itemsService; + private final ItemsRepository itemsRepository; + private final ImageService imageService; + + @ApiOperation(value = "게시판 글쓰기", notes = "여러장의 이미지와 제목,가격,카테고리,게시글 내용 값을 받아서 게시판을 작성한뒤 게시판 id 반환") + @PostMapping("/board") + public ResponseDTO uploadFile(@AuthenticationPrincipal String userId, ItemsDTO itemsDTO, + @RequestPart("file") List file) { + Long itemId = imageService.multipleUpload(file, userId, itemsDTO); + return new ResponseDTO<>(HttpStatus.OK.value(), itemId); + } + + /** + * 화면에 맞는 dto, api스펙, 무한 스크롤 페이징 고민해야 한다 + * @return + */ + @ApiOperation(value = "게시판 보기", notes = "사진,제목,가격,장소, 등을 게시판을 통해 보여준다") + @GetMapping("/board") + public ResponseDTO index(){ + Items itemLists = itemsService.index(); + //List result = itemLists.stream() + // .map(b -> new ItemsResponseDTO(b)) + // .collect(Collectors.toList()); + return new ResponseDTO<>(HttpStatus.OK.value(), itemLists); + } + + @ApiOperation(value = "상품 카데고리 보기", notes = "다양한 상품의 카테고리 정보를 제공한다") + @GetMapping("/enum") + public ResponseDTO category(){ + return new ResponseDTO<>(HttpStatus.OK.value(), Category.values()); + } + + @ApiOperation(value = "상품 삭제하기", notes = "게시글에 올린 상품을 삭제할 수 있다") + @DeleteMapping("/board/{itemId}") + public ResponseDTO delete(@AuthenticationPrincipal String userId, @PathVariable Long itemId){ + Items items = itemsRepository.findById(itemId).orElseThrow(()->new IllegalStateException("게시글에 올린 상품이 없음")); + List images = items.getImages(); + imageService.deleteFile(images); + itemsRepository.deleteById(itemId); + return new ResponseDTO<>(HttpStatus.OK.value(), "삭제 완료"); + } + + @ApiOperation(value = "상품정보 수정하기", notes = "상품정보를 수정하며 이미지가 있는 경우 기존의 이미지 파일을 삭제합니다.") + @PatchMapping("/board/{itemId}") + public ResponseDTO update(@AuthenticationPrincipal String userId, ItemsDTO itemsDTO, + @RequestPart("file") List file, @PathVariable Long itemId) { + + imageService.multipleModify(file, userId, itemsDTO, itemId); + return new ResponseDTO<>(HttpStatus.OK.value(), "수정완료"); + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/controller/LikesController.java b/src/main/java/com/dku/springstudy/controller/LikesController.java new file mode 100644 index 0000000..4a4e5d7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/LikesController.java @@ -0,0 +1,24 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.dto.ResponseDTO; +import com.dku.springstudy.service.LikesService; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class LikesController { + + private final LikesService likesService; + + @ApiOperation(value = "상품 좋아요 기능", notes = "좋아요 기능 이미 했으면 취소 안했으면 추가를 실행한다") + @GetMapping("/likes/{itemId}") + public ResponseDTO clickLikes(@AuthenticationPrincipal String userId, @PathVariable Long itemId) { + return new ResponseDTO<>(HttpStatus.OK.value(), likesService.clickLikes(userId, itemId)); + } +} diff --git a/src/main/java/com/dku/springstudy/controller/MailController.java b/src/main/java/com/dku/springstudy/controller/MailController.java new file mode 100644 index 0000000..d84aa1f --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/MailController.java @@ -0,0 +1,25 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.service.MailServiceImpl; +import com.dku.springstudy.dto.user.MailDTO; +import com.dku.springstudy.dto.ResponseDTO; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class MailController { + private final MailServiceImpl mailService; + @PostMapping("/account/mailCheck") + @ApiOperation(value = "이메일 인증", notes = "이메일 인증을 진행합니다.") + public ResponseDTO mailConfirm(@RequestBody MailDTO mailDTO) throws Exception { + String email = mailDTO.getEmail(); + String code = mailService.sendSimpleMessage(email); + log.info("인증코드 : "+code); + return new ResponseDTO<>(HttpStatus.OK.value(), code); + } +} diff --git a/src/main/java/com/dku/springstudy/controller/UserController.java b/src/main/java/com/dku/springstudy/controller/UserController.java new file mode 100644 index 0000000..37b5e24 --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/UserController.java @@ -0,0 +1,62 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.config.security.jwt.JwtProvider; +import com.dku.springstudy.dto.*; +import com.dku.springstudy.dto.user.*; +import com.dku.springstudy.model.User; +import com.dku.springstudy.repository.UserRepository; +import com.dku.springstudy.service.S3Service; +import com.dku.springstudy.service.UserService; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@RestController +@RequiredArgsConstructor +public class UserController { + + private final UserRepository userRepository; + private final UserService userService; + private final JwtProvider jwtProvider; + private final S3Service s3Service; + + @PostMapping("/account/sign-up") + @ApiOperation(value = "회원가입", notes = "SignUpRequest 객체로 받아 유저를 등록합니다.") + public ResponseDTO signUp(@RequestBody SignUpRequestDTO signUpRequest){ + userService.signUp(signUpRequest); + return new ResponseDTO<>(HttpStatus.OK.value(), signUpRequest); + } + + @PostMapping("/account/login") + @ApiOperation(value = "로그인", notes = "LoginRequest 객체로 email과 password를 받아 로그인을 진행하고, access token과 refresh token을 리턴합니다.") + public ResponseDTO login(@RequestBody LoginRequestDTO loginRequest) throws JsonProcessingException { + UserResponseDTO userResponse = userService.login(loginRequest); + TokensResponseDTO tokens = jwtProvider.createTokensByLogin(userResponse); + return new ResponseDTO<>(HttpStatus.OK.value(), tokens); + } + + @GetMapping("/account/reissue") + @ApiOperation(value = "access토큰 재발급", notes = "access 토큰이 만료된 경우 refresh 토큰을 이용하여 access 토큰을 갱신합니다.") + public ResponseDTO reissue(@AuthenticationPrincipal String userId) throws JsonProcessingException { + User user = userRepository.findById(userId).orElseThrow(()->new IllegalStateException("오류 : 유저없음")); + UserResponseDTO userResponse = UserResponseDTO.of(user); + TokensResponseDTO tokens = jwtProvider.reissueAtk(userResponse); + return new ResponseDTO<>(HttpStatus.OK.value(),tokens); + } + + @PutMapping("/change/image") + @ApiOperation(value = "회원정보 수정", notes = "프로필 이미지와 닉네임을 변경 할 수 있습니다.") + public ResponseDTO changePersonalInformation(@AuthenticationPrincipal String userId, UserInformationChangeRequestDTO userImageChangeDTO, + @RequestPart("file") MultipartFile file) throws IOException { + String imgPath = s3Service.upload(file); + userImageChangeDTO.setImageUrl(imgPath); + UserInformationChangeResponseDTO userResponse = userService.changeImageAndNickname(userId, userImageChangeDTO); + return new ResponseDTO<>(HttpStatus.OK.value(), userResponse); + } +} diff --git a/src/main/java/com/dku/springstudy/dto/ItemsDTO.java b/src/main/java/com/dku/springstudy/dto/ItemsDTO.java new file mode 100644 index 0000000..2ad9860 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/ItemsDTO.java @@ -0,0 +1,17 @@ +package com.dku.springstudy.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ItemsDTO { + + private Long id; + + private List imageUrls; + private String title; + private int price; + private String category; + private String intro; +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/dto/ItemsResponseDTO.java b/src/main/java/com/dku/springstudy/dto/ItemsResponseDTO.java new file mode 100644 index 0000000..ba1beef --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/ItemsResponseDTO.java @@ -0,0 +1,33 @@ +package com.dku.springstudy.dto; + +import com.dku.springstudy.model.Images; +import com.dku.springstudy.model.Items; +import com.dku.springstudy.model.Category; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class ItemsResponseDTO { + + private Long id; + + private List images; + private String title; + private String intro; + private Category category; + private int price; + + private LocalDateTime modifiedDate; + + public ItemsResponseDTO(Items items){ + id = items.getId(); + images = items.getImages(); + title = items.getTitle(); + intro = items.getIntro(); + category = items.getCategory(); + price = items.getPrice(); + modifiedDate = items.getModifiedDate(); + } +} diff --git a/src/main/java/com/dku/springstudy/dto/ResponseDTO.java b/src/main/java/com/dku/springstudy/dto/ResponseDTO.java new file mode 100644 index 0000000..90856eb --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/ResponseDTO.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResponseDTO { + private int status; + private T data; +} diff --git a/src/main/java/com/dku/springstudy/dto/user/LoginRequestDTO.java b/src/main/java/com/dku/springstudy/dto/user/LoginRequestDTO.java new file mode 100644 index 0000000..861c8d7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/user/LoginRequestDTO.java @@ -0,0 +1,9 @@ +package com.dku.springstudy.dto.user; + +import lombok.Data; + +@Data +public class LoginRequestDTO { + private String email; + private String password; +} diff --git a/src/main/java/com/dku/springstudy/dto/user/MailDTO.java b/src/main/java/com/dku/springstudy/dto/user/MailDTO.java new file mode 100644 index 0000000..a177132 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/user/MailDTO.java @@ -0,0 +1,8 @@ +package com.dku.springstudy.dto.user; + +import lombok.Data; + +@Data +public class MailDTO { + private String email; +} diff --git a/src/main/java/com/dku/springstudy/dto/user/SignUpRequestDTO.java b/src/main/java/com/dku/springstudy/dto/user/SignUpRequestDTO.java new file mode 100644 index 0000000..48d2824 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/user/SignUpRequestDTO.java @@ -0,0 +1,12 @@ +package com.dku.springstudy.dto.user; + +import lombok.Data; + +@Data +public class SignUpRequestDTO { + private final String email; + private final String password; + private final String username; + private final String phoneNumber; + private final String nickname; +} diff --git a/src/main/java/com/dku/springstudy/dto/user/TokensResponseDTO.java b/src/main/java/com/dku/springstudy/dto/user/TokensResponseDTO.java new file mode 100644 index 0000000..2f58642 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/user/TokensResponseDTO.java @@ -0,0 +1,9 @@ +package com.dku.springstudy.dto.user; + +import lombok.Data; + +@Data +public class TokensResponseDTO { + private final String atk; + private final String rtk; +} diff --git a/src/main/java/com/dku/springstudy/dto/user/UserInformationChangeRequestDTO.java b/src/main/java/com/dku/springstudy/dto/user/UserInformationChangeRequestDTO.java new file mode 100644 index 0000000..70b128f --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/user/UserInformationChangeRequestDTO.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.dto.user; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserInformationChangeRequestDTO { + private String imageUrl; + private String nickname; +} diff --git a/src/main/java/com/dku/springstudy/dto/user/UserInformationChangeResponseDTO.java b/src/main/java/com/dku/springstudy/dto/user/UserInformationChangeResponseDTO.java new file mode 100644 index 0000000..4cbf85e --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/user/UserInformationChangeResponseDTO.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.dto.user; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserInformationChangeResponseDTO { + private String imageUrl; + private String nickname; +} diff --git a/src/main/java/com/dku/springstudy/dto/user/UserResponseDTO.java b/src/main/java/com/dku/springstudy/dto/user/UserResponseDTO.java new file mode 100644 index 0000000..b71566c --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/user/UserResponseDTO.java @@ -0,0 +1,30 @@ +package com.dku.springstudy.dto.user; + +import com.dku.springstudy.model.Role; +import com.dku.springstudy.model.User; +import lombok.Data; + +@Data +public class UserResponseDTO { + private final String userId; + + private final String email; + + private final String nickname; + private Role role = Role.USER; + + public UserResponseDTO(String userId, String email, String nickname, Role role) { + this.userId = userId; + this.email = email; + this.nickname = nickname; + this.role = role; + } + + public static UserResponseDTO of(User user) { + return new UserResponseDTO( + user.getId(), + user.getEmail(), + user.getNickname(), + user.getRole()); + } +} diff --git a/src/main/java/com/dku/springstudy/exception/JwtTokenError.java b/src/main/java/com/dku/springstudy/exception/JwtTokenError.java new file mode 100644 index 0000000..aab80d9 --- /dev/null +++ b/src/main/java/com/dku/springstudy/exception/JwtTokenError.java @@ -0,0 +1,12 @@ +package com.dku.springstudy.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public class JwtTokenError { + private String message; + private HttpStatus status; +} diff --git a/src/main/java/com/dku/springstudy/exception/Message.java b/src/main/java/com/dku/springstudy/exception/Message.java new file mode 100644 index 0000000..2fbed7d --- /dev/null +++ b/src/main/java/com/dku/springstudy/exception/Message.java @@ -0,0 +1,9 @@ +package com.dku.springstudy.exception; + +public class Message { + public static final String JWT_TOKEN_EXPIRED = "JWT 토큰이 만료되었습니다."; + public static final String JWT_UNSUPPORTED = "지원하지 않는 JWT 토큰입니다."; + public static final String JWT_MALFORMED = "올바른 JWT 토큰의 형태가 아닙니다."; + public static final String JWT_SIGNATURE = "올바른 SIGNATURE가 아닙니다."; + public static final String JWT_ILLEGAL_ARGUMENT = "잘못된 정보를 넣었습니다."; +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/model/BaseTimeEntity.java b/src/main/java/com/dku/springstudy/model/BaseTimeEntity.java new file mode 100644 index 0000000..ade7483 --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/BaseTimeEntity.java @@ -0,0 +1,22 @@ +package com.dku.springstudy.model; + +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.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; +} diff --git a/src/main/java/com/dku/springstudy/model/Category.java b/src/main/java/com/dku/springstudy/model/Category.java new file mode 100644 index 0000000..d040b94 --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/Category.java @@ -0,0 +1,25 @@ +package com.dku.springstudy.model; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum Category { + DIGITAL_DEVICE("디지털기기"), HOME_APPLIANCE("생활가전"), FURNITURE_INTERIOR("가구/인테리어"), + BABY_PRODUCTS("유아용품"), LIFE_PROCESSED_FOOD("생활/가공식품"), CHILDREN_BOOKS("유아도서"), WOMEN_CLOTHING("여성의류"), + MEN_CLOTHING_STUFF("남성패션/잡화"), GAMES_HOBBY("게임/취미"), BEAUTY("뷰티/미용"), COMPANION_ANIMAL_STUFF("반려동물용품"), + BOOK_TICKET_MUSIC("도서/티켓/음반"), OTHER_USED_ITEMS("기타중고물품"), USED_CAR("중고차"); + + @JsonCreator + public static Category from(String s) { + return Category.valueOf(s.toUpperCase()); + } + + private final String label; + + Category(String label) { + this.label = label; + } + + public String label() { + return label; + } +} diff --git a/src/main/java/com/dku/springstudy/model/Images.java b/src/main/java/com/dku/springstudy/model/Images.java new file mode 100644 index 0000000..5926480 --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/Images.java @@ -0,0 +1,28 @@ +package com.dku.springstudy.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; + +import javax.persistence.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter @Setter +@Builder +@Entity +public class Images extends BaseTimeEntity{ + + @Id @GeneratedValue + @Column(name = "image_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Items items; + + private String url; +} diff --git a/src/main/java/com/dku/springstudy/model/ItemStatus.java b/src/main/java/com/dku/springstudy/model/ItemStatus.java new file mode 100644 index 0000000..7192f6d --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/ItemStatus.java @@ -0,0 +1,5 @@ +package com.dku.springstudy.model; + +public enum ItemStatus { + SELLING,RESERVING,COMPLETE +} diff --git a/src/main/java/com/dku/springstudy/model/Items.java b/src/main/java/com/dku/springstudy/model/Items.java new file mode 100644 index 0000000..ae31881 --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/Items.java @@ -0,0 +1,64 @@ +package com.dku.springstudy.model; + +import com.dku.springstudy.dto.ItemsDTO; +import lombok.*; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter @Builder +@Entity +public class Items extends BaseTimeEntity { + @Id @GeneratedValue + @Column(name = "item_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @OneToMany(mappedBy = "items",cascade = CascadeType.ALL) + private List likes = new ArrayList<>(); + + @OneToMany(mappedBy = "items", cascade = CascadeType.ALL) + private List images = new ArrayList<>(); + + private String title; + @Lob + private String intro; + + @Enumerated(EnumType.STRING) + private Category category; + + private int price; + + @Enumerated(EnumType.STRING) + private ItemStatus itemStatus; + + public void modifyItems(ItemsDTO itemsDTO) { + this.title = itemsDTO.getTitle(); + this.price = itemsDTO.getPrice(); + this.intro = itemsDTO.getIntro(); + this.category = Category.valueOf(itemsDTO.getCategory()); + } + + public void addItemWithImage(Images image){ + images.add(image); + image.setItems(this); + } + + public void addLike(Likes likes){ + this.likes.add(likes); + } + + public void deleteLike(Likes likes){ + this.likes.remove(likes); + } + + public int getTotalLikes(){ + return likes.size(); + } +} diff --git a/src/main/java/com/dku/springstudy/model/Likes.java b/src/main/java/com/dku/springstudy/model/Likes.java new file mode 100644 index 0000000..4324236 --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/Likes.java @@ -0,0 +1,32 @@ +package com.dku.springstudy.model; + +import lombok.*; + +import javax.persistence.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter @Builder +@Entity +public class Likes extends BaseTimeEntity{ + + @Id @GeneratedValue + @Column(name = "likes_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Items items; + + public void addLike(){ + this.items.addLike(this); + } + + public void deleteLike(){ + this.items.deleteLike(this); + } +} diff --git a/src/main/java/com/dku/springstudy/model/Role.java b/src/main/java/com/dku/springstudy/model/Role.java new file mode 100644 index 0000000..d5da4fb --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/Role.java @@ -0,0 +1,5 @@ +package com.dku.springstudy.model; + +public enum Role { + USER,ADMIN +} diff --git a/src/main/java/com/dku/springstudy/model/User.java b/src/main/java/com/dku/springstudy/model/User.java new file mode 100644 index 0000000..7fffe51 --- /dev/null +++ b/src/main/java/com/dku/springstudy/model/User.java @@ -0,0 +1,48 @@ +package com.dku.springstudy.model; + +import lombok.*; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@NoArgsConstructor @AllArgsConstructor +@Getter @Builder +@Table(name = "Users", uniqueConstraints = {@UniqueConstraint(columnNames = "email")}) +@Entity +public class User extends BaseTimeEntity{ + + @Id + @GeneratedValue(generator = "system-uuid") + @GenericGenerator(name = "system-uuid",strategy = "uuid") + @Column(name = "user_id") + private String id; + + @Column(nullable = false) + private String username; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + private String profileImageUrl; + + @Setter + @Lob + private String token; + + @Column(nullable = false) + private String phoneNumber; + + private String nickname; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Role role; + + public void changeProfileImageAndNickname(String url, String nickname){ + this.profileImageUrl = url; + this.nickname = nickname; + } +} diff --git a/src/main/java/com/dku/springstudy/repository/ImageRepository.java b/src/main/java/com/dku/springstudy/repository/ImageRepository.java new file mode 100644 index 0000000..e6aa105 --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/ImageRepository.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.repository; + +import com.dku.springstudy.model.Images; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; + +public interface ImageRepository extends JpaRepository { + + @Transactional + @Modifying + @Query("delete from Images im where im.items.id = :id") + void deleteByItemsId(Long id); +} diff --git a/src/main/java/com/dku/springstudy/repository/ItemsRepository.java b/src/main/java/com/dku/springstudy/repository/ItemsRepository.java new file mode 100644 index 0000000..aeaf824 --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/ItemsRepository.java @@ -0,0 +1,8 @@ +package com.dku.springstudy.repository; + +import com.dku.springstudy.model.Items; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ItemsRepository extends JpaRepository { + +} diff --git a/src/main/java/com/dku/springstudy/repository/LikesRepository.java b/src/main/java/com/dku/springstudy/repository/LikesRepository.java new file mode 100644 index 0000000..eeb8468 --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/LikesRepository.java @@ -0,0 +1,16 @@ +package com.dku.springstudy.repository; + +import com.dku.springstudy.model.Items; +import com.dku.springstudy.model.Likes; +import com.dku.springstudy.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface LikesRepository extends JpaRepository { + + Likes findByItemsAndUser(Items items, User user); + + List findByUser(User user); + +} diff --git a/src/main/java/com/dku/springstudy/repository/UserRepository.java b/src/main/java/com/dku/springstudy/repository/UserRepository.java new file mode 100644 index 0000000..7568e38 --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.repository; + +import com.dku.springstudy.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + Optional findByEmail(String email); +} diff --git a/src/main/java/com/dku/springstudy/service/ImageService.java b/src/main/java/com/dku/springstudy/service/ImageService.java new file mode 100644 index 0000000..a7925e3 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/ImageService.java @@ -0,0 +1,128 @@ +package com.dku.springstudy.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.dku.springstudy.dto.ItemsDTO; +import com.dku.springstudy.model.*; +import com.dku.springstudy.repository.ImageRepository; +import com.dku.springstudy.repository.ItemsRepository; +import com.dku.springstudy.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class ImageService { + @Value("${spring.cloud.aws.s3.bucket}") + private String bucket; + + @Value("${spring.cloud.aws.s3.targetURL}") + private String target; + + private final ItemsRepository itemsRepository; + private final ImageRepository imageRepository; + private final AmazonS3 amazonS3; + private final UserRepository userRepository; + @Transactional + public Long multipleUpload(List multipartFile, String userId, ItemsDTO itemsDTO) { + List fileNameList = new ArrayList<>(); + + User user = userRepository.findById(userId).orElseThrow(() -> new IllegalStateException("멤버가 없습니다")); + + Items items = Items.builder() + .user(user) + .images(fileNameList) + .title(itemsDTO.getTitle()) + .price(itemsDTO.getPrice()) + .intro(itemsDTO.getIntro()) + .category(Category.valueOf(itemsDTO.getCategory())) + .itemStatus(ItemStatus.SELLING) + .build(); + + multipartFile.forEach(file -> { + String fileName = createFileName(file.getOriginalFilename()); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(file.getSize()); + objectMetadata.setContentType(file.getContentType()); + + Images images = Images.builder() + .user(user) + .url(amazonS3.getUrl(bucket, fileName).toString()) + .build(); + fileNameList.add(images); + items.addItemWithImage(images); + + try (InputStream inputStream = file.getInputStream()) { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (IOException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다."); + } + }); + itemsRepository.save(items); + return items.getId(); + } + + public void deleteFile(List fileName) { + fileName.forEach(file-> + { + String url = file.getUrl(); + url = url.replace(target,""); + amazonS3.deleteObject(new DeleteObjectRequest(bucket, url)); + }); + + } + + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + private String getFileExtension(String fileName) { + try { + return fileName.substring(fileName.lastIndexOf(".")); + } catch (StringIndexOutOfBoundsException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일(" + fileName + ") 입니다."); + } + } + + @Transactional + public void multipleModify(List multipartFile, String userId, ItemsDTO itemsDTO, Long itemId) { + User user = userRepository.findById(userId).orElseThrow(() -> new IllegalStateException("멤버가 없습니다")); + Items items = itemsRepository.findById(itemId).orElseThrow(() -> new IllegalStateException("상품이 없습니다")); + + List beforeImages = items.getImages(); + if (multipartFile.size()!=0) { + deleteFile(beforeImages); + imageRepository.deleteByItemsId(items.getId()); + } + + multipartFile.forEach(file -> { + String fileNameModify = createFileName(file.getOriginalFilename()); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(file.getSize()); + objectMetadata.setContentType(file.getContentType()); + + Images images = Images.builder() + .user(user) + .url(amazonS3.getUrl(bucket, fileNameModify).toString()) + .build(); + items.addItemWithImage(images); + }); + items.modifyItems(itemsDTO); + } +} diff --git a/src/main/java/com/dku/springstudy/service/ItemsService.java b/src/main/java/com/dku/springstudy/service/ItemsService.java new file mode 100644 index 0000000..b0c1b90 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/ItemsService.java @@ -0,0 +1,39 @@ +package com.dku.springstudy.service; + +import com.dku.springstudy.dto.ItemsDTO; +import com.dku.springstudy.model.*; +import com.dku.springstudy.repository.ItemsRepository; +import com.dku.springstudy.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class ItemsService { + private final ItemsRepository itemsRepository; + private final UserRepository userRepository; + + + public List index(){ + List items = itemsRepository.findAll(); + return items ; + } + + public void itemUpload(ItemsDTO itemsDTO, String userId) { + User user = userRepository.findById(userId).orElseThrow(()-> new IllegalStateException("오류 : 없는 사용자 입니다.")); +/** + Items items = Items.builder() + .user(user) + .title(itemsDTO.getTitle()) + .price(itemsDTO.getPrice()) + .intro(itemsDTO.getIntro()) + .category(Category.valueOf(itemsDTO.getCategory())) + .itemStatus(ItemStatus.SELLING) + .build(); + + itemsRepository.save(items); + */ + } +} diff --git a/src/main/java/com/dku/springstudy/service/LikesService.java b/src/main/java/com/dku/springstudy/service/LikesService.java new file mode 100644 index 0000000..9d28e45 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/LikesService.java @@ -0,0 +1,45 @@ +package com.dku.springstudy.service; + +import com.dku.springstudy.model.Items; +import com.dku.springstudy.model.Likes; +import com.dku.springstudy.model.User; +import com.dku.springstudy.repository.ItemsRepository; +import com.dku.springstudy.repository.LikesRepository; +import com.dku.springstudy.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class LikesService { + + private final UserRepository userRepository; + private final LikesRepository likesRepository; + private final ItemsRepository itemsRepository; + + @Transactional + public String clickLikes(String userId, Long itemId){ + User user = userRepository.findById(userId).orElseThrow(()->new IllegalStateException("유저 없음 오류")); + Items items = itemsRepository.findById(itemId).orElseThrow(()->new IllegalStateException("아이템 없음 오류")); + + Likes likes = likesRepository.findByItemsAndUser(items, user); + if(likes != null){ + likes.deleteLike(); + likesRepository.delete(likes); + return "좋아요 취소"; + } + else{ + Likes like = Likes.builder() + .user(user) + .items(items) + .build(); + likesRepository.save(like); + like.addLike(); + return "좋아요 완료"; + } + } +} diff --git a/src/main/java/com/dku/springstudy/service/MailService.java b/src/main/java/com/dku/springstudy/service/MailService.java new file mode 100644 index 0000000..17e8fac --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/MailService.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.service; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.UnsupportedEncodingException; + +public interface MailService { + MimeMessage createMessage(String to) throws MessagingException, UnsupportedEncodingException; + + // 랜덤 인증 코드 전송 + String createKey(); + + // 메일 발송 + String sendSimpleMessage(String to) throws Exception; +} diff --git a/src/main/java/com/dku/springstudy/service/MailServiceImpl.java b/src/main/java/com/dku/springstudy/service/MailServiceImpl.java new file mode 100644 index 0000000..1dc9d90 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/MailServiceImpl.java @@ -0,0 +1,89 @@ +package com.dku.springstudy.service; + +import java.io.UnsupportedEncodingException; +import java.util.Random; + +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMessage.RecipientType; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MailServiceImpl implements MailService { + private final JavaMailSender mailSender; + + @Value("${mail.email}") + private String email; + + @Value("${mail.name}") + private String name; + + private String confirmNumber; // 인증번호 + + // 메일 내용 작성 + @Override + public MimeMessage createMessage(String receiver) throws MessagingException, UnsupportedEncodingException { + + MimeMessage message = mailSender.createMimeMessage(); + message.addRecipients(RecipientType.TO, receiver); //받는사람 + message.setSubject("당근마켓 회원가입 이메일 인증"); + + String msgg = ""; + msgg += "
"; + msgg += "

안녕하세요

"; + msgg += "

당근 마켓 서지현입니다.

"; + msgg += "
"; + msgg += "

아래 코드를 회원가입 창으로 돌아가 입력해주세요

"; + msgg += "
"; + msgg += "
"; + msgg += "

"; + msgg += "

회원가입 인증 코드입니다.

"; + msgg += "
"; + msgg += "CODE : "; + msgg += confirmNumber + "

"; + msgg += "
"; + message.setText(msgg, "utf-8", "html"); + message.setFrom(new InternetAddress(email,name));// 보내는 사람 + return message; + } + @Override + public String createKey() { + StringBuffer key = new StringBuffer(); + Random random = new Random(); + + for (int i = 0; i < 8; i++) { + int index = random.nextInt(3); + switch (index) { + case 0: + key.append((char) ((int) (random.nextInt(26)) + 97)); //소문자 + break; + case 1: + key.append((char) ((int) (random.nextInt(26)) + 65)); //대문자 + break; + case 2: + key.append((random.nextInt(10))); //숫자 + break; + } + } + return key.toString(); + } + @Override + public String sendSimpleMessage(String receiver) throws Exception { + confirmNumber = createKey(); + + MimeMessage message = createMessage(receiver); // 메일 발송 + try { + mailSender.send(message); + } catch (MailException es) { + throw new IllegalArgumentException(); + } + return confirmNumber; + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/service/S3Service.java b/src/main/java/com/dku/springstudy/service/S3Service.java new file mode 100644 index 0000000..d115ae6 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/S3Service.java @@ -0,0 +1,52 @@ +package com.dku.springstudy.service; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import java.io.IOException; + +@Service +@NoArgsConstructor +public class S3Service { + private AmazonS3 s3Client; + + @Value("${spring.cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${spring.cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${spring.cloud.aws.s3.bucket}") + private String bucket; + + @Value("${spring.cloud.aws.region.static}") + private String region; + + @PostConstruct + public void setS3Client() { + AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey); + + s3Client = AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(this.region) + .build(); + } + + public String upload(MultipartFile file) throws IOException { + String fileName = file.getOriginalFilename(); + + s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return s3Client.getUrl(bucket, fileName).toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dku/springstudy/service/UserService.java b/src/main/java/com/dku/springstudy/service/UserService.java new file mode 100644 index 0000000..8c7c950 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/UserService.java @@ -0,0 +1,78 @@ +package com.dku.springstudy.service; + +import com.dku.springstudy.config.security.jwt.JwtProvider; +import com.dku.springstudy.dto.user.*; +import com.dku.springstudy.model.Role; +import com.dku.springstudy.model.User; +import com.dku.springstudy.repository.UserRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final JwtProvider jwtProvider; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public void signUp(SignUpRequestDTO signUpRequest){ + + boolean isExist = userRepository.existsByEmail(signUpRequest.getEmail()); + if(isExist){ + throw new IllegalStateException("이미 존재하는 이메일입니다."); + } + + String encodedPassword = passwordEncoder.encode(signUpRequest.getPassword()); + User user = User.builder() + .username(signUpRequest.getUsername()) + .email(signUpRequest.getEmail()) + .password(encodedPassword) + .phoneNumber(signUpRequest.getPhoneNumber()) + .profileImageUrl("https://user-images.githubusercontent.com/79920930/215020145-7d548f6e-a45e-4041-913c-24a16081a07a.jpg") + .nickname(signUpRequest.getNickname()) + .role(Role.USER) + .build(); + + userRepository.save(user); + } + + @Transactional + public UserResponseDTO login(LoginRequestDTO loginRequest) throws JsonProcessingException { + User user = userRepository.findByEmail(loginRequest.getEmail()) + .orElseThrow(() -> new IllegalStateException("아이디 혹은 비밀번호를 확인하세요.")); + + boolean matches = passwordEncoder.matches(loginRequest.getPassword(), user.getPassword()); + if(!matches){ + throw new IllegalStateException("아이디 혹은 비밀번호를 확인하세요."); + } + + UserResponseDTO userResponse = UserResponseDTO.of(user); + final String token = jwtProvider.createTokensByLogin(userResponse).getAtk(); + user.setToken(token); + + return UserResponseDTO.of(user); + } + + @Transactional + public UserInformationChangeResponseDTO changeImageAndNickname(@AuthenticationPrincipal String userId, UserInformationChangeRequestDTO userImageChangeDTO){ + User user = userRepository.findById(userId).orElseThrow(()-> new IllegalStateException("오류 : 없는 사용자 입니다.")); + String newImage = userImageChangeDTO.getImageUrl(); + String newNickname = userImageChangeDTO.getNickname(); + + user.changeProfileImageAndNickname(newImage,newNickname); + + UserInformationChangeResponseDTO userResponse = UserInformationChangeResponseDTO.builder(). + imageUrl(userImageChangeDTO.getImageUrl()) + .nickname(userImageChangeDTO.getNickname()) + .build(); + + return userResponse; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..d01e763 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,6 @@ +spring.datasource.url=jdbc:h2:tcp://localhost/~/test +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=none