diff --git a/build.gradle b/build.gradle index 6dd9425..297fced 100644 --- a/build.gradle +++ b/build.gradle @@ -23,14 +23,19 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' -// implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-mail:3.0.5' + implementation 'com.sun.mail:jakarta.mail:2.0.1' // 버전을 2.0.1로 변경 implementation 'org.springframework.boot:spring-boot-starter-validation' + // LOMBOK compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' // SPRING BOOT DEVTOOLS developmentOnly 'org.springframework.boot:spring-boot-devtools' @@ -46,6 +51,12 @@ dependencies { annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // JWT Support + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // JSON 처리를 위해 jackson 연동 모듈 추가 + // AWS S3 implementation 'com.amazonaws:aws-java-sdk-s3' implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' @@ -53,12 +64,15 @@ dependencies { // JSON implementation 'org.json:json:20231013' + // thumbnailator + implementation 'net.coobird:thumbnailator:0.4.14' + + // TEST IMPLEMENTATION ======================== // TEST DATABASE H2 testImplementation 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' - // TEST JUNIT testImplementation 'org.springframework.boot:spring-boot-starter-test' // testImplementation 'org.springframework.security:spring-security-test' diff --git a/src/main/java/com/ssg/starroad/board/DTO/BoardContentDTO.java b/src/main/java/com/ssg/starroad/board/DTO/BoardContentDTO.java new file mode 100644 index 0000000..7cb22d4 --- /dev/null +++ b/src/main/java/com/ssg/starroad/board/DTO/BoardContentDTO.java @@ -0,0 +1,18 @@ +package com.ssg.starroad.board.DTO; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class BoardContentDTO { + private Long id; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private String imagePath; + private Long boardId; +} diff --git a/src/main/java/com/ssg/starroad/board/DTO/BoardDTO.java b/src/main/java/com/ssg/starroad/board/DTO/BoardDTO.java new file mode 100644 index 0000000..7701010 --- /dev/null +++ b/src/main/java/com/ssg/starroad/board/DTO/BoardDTO.java @@ -0,0 +1,21 @@ +package com.ssg.starroad.board.DTO; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class BoardDTO { + private Long id; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private Long managerId; + private String category; + private String content; + private String title; + private Long userId; +} diff --git a/src/main/java/com/ssg/starroad/board/controller/BoardController.java b/src/main/java/com/ssg/starroad/board/controller/BoardController.java index 38905e4..33ca56a 100644 --- a/src/main/java/com/ssg/starroad/board/controller/BoardController.java +++ b/src/main/java/com/ssg/starroad/board/controller/BoardController.java @@ -1,13 +1,29 @@ package com.ssg.starroad.board.controller; +import com.ssg.starroad.board.DTO.BoardDTO; import com.ssg.starroad.board.service.BoardService; +import com.ssg.starroad.coupon.DTO.CouponDTO; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RequiredArgsConstructor @RestController @RequestMapping("/board") public class BoardController { private final BoardService boardService; + + + @GetMapping("/list") + public ResponseEntity> getBoardList() { + List boardDTOS = boardService.getEventBoards(); + if (boardDTOS.isEmpty()) { + return ResponseEntity.noContent().build(); // 내용이 없을 경우 No Content (204) 반환 + } + return ResponseEntity.ok(boardDTOS); // 내용이 있을 경우 OK (200)와 함께 데이터 반환 + } } diff --git a/src/main/java/com/ssg/starroad/board/controller/BoardImageController.java b/src/main/java/com/ssg/starroad/board/controller/BoardImageController.java index 7ef3be2..76957fc 100644 --- a/src/main/java/com/ssg/starroad/board/controller/BoardImageController.java +++ b/src/main/java/com/ssg/starroad/board/controller/BoardImageController.java @@ -1,13 +1,29 @@ package com.ssg.starroad.board.controller; +import com.ssg.starroad.board.DTO.BoardContentDTO; +import com.ssg.starroad.board.DTO.BoardDTO; import com.ssg.starroad.board.service.BoardImageService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RequiredArgsConstructor @RestController @RequestMapping("/board-image") public class BoardImageController { private final BoardImageService boardImageService; + + @GetMapping("/content/{boardId}") + public ResponseEntity getBoardContent(@PathVariable Long boardId) { + BoardContentDTO boardContent = boardImageService.getEventContent(boardId); + if (boardContent==null) { + return ResponseEntity.noContent().build(); // 내용이 없을 경우 No Content (204) 반환 + } + return ResponseEntity.ok(boardContent); // 내용이 있을 경우 OK (200)와 함께 데이터 반환 + } } diff --git a/src/main/java/com/ssg/starroad/board/repository/BoardImageRepository.java b/src/main/java/com/ssg/starroad/board/repository/BoardImageRepository.java index b991da5..23965f6 100644 --- a/src/main/java/com/ssg/starroad/board/repository/BoardImageRepository.java +++ b/src/main/java/com/ssg/starroad/board/repository/BoardImageRepository.java @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface BoardImageRepository extends JpaRepository { + + public BoardImage findByBoardId(Long boardId); } diff --git a/src/main/java/com/ssg/starroad/board/repository/BoardRepository.java b/src/main/java/com/ssg/starroad/board/repository/BoardRepository.java index d7120fd..e4cefdb 100644 --- a/src/main/java/com/ssg/starroad/board/repository/BoardRepository.java +++ b/src/main/java/com/ssg/starroad/board/repository/BoardRepository.java @@ -1,7 +1,12 @@ package com.ssg.starroad.board.repository; import com.ssg.starroad.board.entity.Board; +import com.ssg.starroad.board.enums.BoardCategory; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface BoardRepository extends JpaRepository { + + public List findByCategory(BoardCategory category); } diff --git a/src/main/java/com/ssg/starroad/board/service/BoardImageService.java b/src/main/java/com/ssg/starroad/board/service/BoardImageService.java index 1afab99..0a1d371 100644 --- a/src/main/java/com/ssg/starroad/board/service/BoardImageService.java +++ b/src/main/java/com/ssg/starroad/board/service/BoardImageService.java @@ -1,4 +1,8 @@ package com.ssg.starroad.board.service; +import com.ssg.starroad.board.DTO.BoardContentDTO; + public interface BoardImageService { + + public BoardContentDTO getEventContent (Long boardid); } diff --git a/src/main/java/com/ssg/starroad/board/service/BoardService.java b/src/main/java/com/ssg/starroad/board/service/BoardService.java index 54e5bc2..ba3da29 100644 --- a/src/main/java/com/ssg/starroad/board/service/BoardService.java +++ b/src/main/java/com/ssg/starroad/board/service/BoardService.java @@ -1,4 +1,10 @@ package com.ssg.starroad.board.service; +import com.ssg.starroad.board.DTO.BoardDTO; + +import java.util.List; + public interface BoardService { + + public List getEventBoards(); } diff --git a/src/main/java/com/ssg/starroad/board/service/impl/BoardImageServiceImpl.java b/src/main/java/com/ssg/starroad/board/service/impl/BoardImageServiceImpl.java index eb74c59..2fb06ea 100644 --- a/src/main/java/com/ssg/starroad/board/service/impl/BoardImageServiceImpl.java +++ b/src/main/java/com/ssg/starroad/board/service/impl/BoardImageServiceImpl.java @@ -1,10 +1,19 @@ package com.ssg.starroad.board.service.impl; +import com.ssg.starroad.board.DTO.BoardContentDTO; +import com.ssg.starroad.board.repository.BoardImageRepository; import com.ssg.starroad.board.service.BoardImageService; import lombok.RequiredArgsConstructor; +import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class BoardImageServiceImpl implements BoardImageService { + private final BoardImageRepository boardImageRepository; + private final ModelMapper modelMapper; + @Override + public BoardContentDTO getEventContent(Long boardid) { + return modelMapper.map(boardImageRepository.findByBoardId(boardid), BoardContentDTO.class); + } } diff --git a/src/main/java/com/ssg/starroad/board/service/impl/BoardServiceImpl.java b/src/main/java/com/ssg/starroad/board/service/impl/BoardServiceImpl.java index 0ff5d4c..3d4726b 100644 --- a/src/main/java/com/ssg/starroad/board/service/impl/BoardServiceImpl.java +++ b/src/main/java/com/ssg/starroad/board/service/impl/BoardServiceImpl.java @@ -1,10 +1,25 @@ package com.ssg.starroad.board.service.impl; +import com.ssg.starroad.board.DTO.BoardDTO; +import com.ssg.starroad.board.enums.BoardCategory; +import com.ssg.starroad.board.repository.BoardRepository; import com.ssg.starroad.board.service.BoardService; import lombok.RequiredArgsConstructor; +import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class BoardServiceImpl implements BoardService { + + private final BoardRepository boardRepository; + private final ModelMapper modelMapper; + + @Override + public List getEventBoards() { + return boardRepository.findByCategory(BoardCategory.EVENT).stream().map(board -> + modelMapper.map(board,BoardDTO.class)).toList(); + } } diff --git a/src/main/java/com/ssg/starroad/common/service/S3Uploader.java b/src/main/java/com/ssg/starroad/common/service/S3Uploader.java index f36e8ac..53093ef 100644 --- a/src/main/java/com/ssg/starroad/common/service/S3Uploader.java +++ b/src/main/java/com/ssg/starroad/common/service/S3Uploader.java @@ -5,10 +5,16 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -43,17 +49,30 @@ public String upload(MultipartFile multipartFile, String dirName) { String encodedFileName = null; encodedFileName = Base64.getUrlEncoder().encodeToString(fileName.getBytes(StandardCharsets.UTF_8)); + // S3에 저장될 파일명 구성 String s3FileName = dirName + "/" + UUID.randomUUID() + "_" + encodedFileName + "." + extension; ObjectMetadata objectMetadata = new ObjectMetadata(); + // 파일을 byte로 변환 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { - objectMetadata.setContentLength(multipartFile.getInputStream().available()); + // Thumbnailator를 사용하여 이미지 리사이징 + Thumbnails.of(multipartFile.getInputStream()) + .size(800, 800) + .outputFormat(extension) + .toOutputStream(outputStream); + + byte[] resizedImageBytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(resizedImageBytes); + + + objectMetadata.setContentLength(resizedImageBytes.length); objectMetadata.setContentType(multipartFile.getContentType()); // 권한 공개 objectMetadata.setHeader("x-amz-acl", "public-read"); - amazonS3.putObject(bucket, s3FileName, multipartFile.getInputStream(), objectMetadata); + amazonS3.putObject(bucket, s3FileName, inputStream, objectMetadata); } catch (IOException e) { log.error("S3 upload fail : " + fileName, e); return String.format("S3 upload fail: %s", multipartFile.getOriginalFilename()); @@ -72,27 +91,57 @@ public String upload(MultipartFile multipartFile, String dirName) { public List upload(MultipartFile[] multipartFile, String dirName) { List urlList = new ArrayList<>(); for (MultipartFile file : multipartFile) { - // 파일명과 확장자 분리 + // Separate file name and extension String originalFileName = file.getOriginalFilename(); int lastIndex = originalFileName.lastIndexOf('.'); String fileName = originalFileName.substring(0, lastIndex); String extension = originalFileName.substring(lastIndex + 1); - // 파일명 URL 인코딩 - String encodedFileName = null; - encodedFileName = Base64.getUrlEncoder().encodeToString(fileName.getBytes(StandardCharsets.UTF_8)); + // URL encode the file name + String encodedFileName = Base64.getUrlEncoder().encodeToString(fileName.getBytes(StandardCharsets.UTF_8)); - // S3에 저장될 파일명 구성 + // Construct the file name to be stored in S3 String s3FileName = dirName + "/" + UUID.randomUUID() + "_" + encodedFileName + "." + extension; ObjectMetadata objectMetadata = new ObjectMetadata(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { - objectMetadata.setContentLength(file.getInputStream().available()); - objectMetadata.setContentType(file.getContentType()); - // 권한 공개 - objectMetadata.setHeader("x-amz-acl", "public-read"); - - amazonS3.putObject(bucket, s3FileName, file.getInputStream(), objectMetadata); + if (file.getContentType().contains("image")) { + // Resize the image using Thumbnailator + Thumbnails.of(file.getInputStream()) + .size(800, 800) + .outputFormat(extension) + .toOutputStream(outputStream); + + byte[] resizedImageBytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(resizedImageBytes); + + objectMetadata.setContentLength(resizedImageBytes.length); + objectMetadata.setContentType("image/jpeg"); + // Set public-read permission + objectMetadata.setHeader("x-amz-acl", "public-read"); + + amazonS3.putObject(bucket, s3FileName, inputStream, objectMetadata); + } else { + // Convert non-image file to a basic image (JPEG) and resize + BufferedImage bufferedImage = new BufferedImage(800, 800, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = bufferedImage.createGraphics(); + g2d.setColor(Color.WHITE); // Set background to white + g2d.fillRect(0, 0, 800, 800); + g2d.setColor(Color.BLACK); // Set text color to black + g2d.drawString("Converted to Image", 50, 400); + g2d.dispose(); + + ImageIO.write(bufferedImage, "jpg", outputStream); + byte[] imageBytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes); + + objectMetadata.setContentLength(imageBytes.length); + objectMetadata.setContentType("image/jpeg"); + objectMetadata.setHeader("x-amz-acl", "public-read"); + + amazonS3.putObject(bucket, s3FileName, inputStream, objectMetadata); + } } catch (IOException e) { log.error("S3 upload fail : " + file.getOriginalFilename(), e); urlList.add(String.format("S3 upload fail: %s", file.getOriginalFilename())); diff --git a/src/main/java/com/ssg/starroad/common/service/clova/Sentiment.java b/src/main/java/com/ssg/starroad/common/service/clova/Sentiment.java index 26443b9..2532905 100644 --- a/src/main/java/com/ssg/starroad/common/service/clova/Sentiment.java +++ b/src/main/java/com/ssg/starroad/common/service/clova/Sentiment.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.ssg.starroad.review.DTO.SentimentDTO; +import com.ssg.starroad.review.DTO.SentimentDetailDTO; import com.ssg.starroad.review.entity.ReviewSentiment; import com.ssg.starroad.review.enums.ConfidenceType; import lombok.RequiredArgsConstructor; @@ -24,14 +26,14 @@ public class Sentiment { private final RestTemplate restTemplate; - @Value("${clova.client.key.id}") + @Value("${naver.openapi.client-id}") private String CLOVA_API_KEY_ID; - @Value("${clova.client.key.secret}") + @Value("${naver.openapi.client}") private String CLOVA_API_KEY; private final String clovaUrl = "https://naveropenapi.apigw.ntruss.com/sentiment-analysis/v1/analyze"; - public List getSentiment(String content) { + public SentimentDTO getSentiment(String content) { // 요청 설정 // 요청 헤더 설정 @@ -52,6 +54,8 @@ public List getSentiment(String content) { // 응답 받기 위해 준비 List sentimentList = new ArrayList<>(); + List detailDTOList = new ArrayList<>(); + SentimentDTO build = null; try { // 응답 파싱하기 ObjectMapper objectMapper = new ObjectMapper(); @@ -65,7 +69,19 @@ public List getSentiment(String content) { // ReviewSentiment 객체로 변환 for (JsonNode sentence : sentences) { - ReviewSentiment reviewSentiment = ReviewSentiment.builder() +// ReviewSentiment reviewSentiment = ReviewSentiment.builder() +// .content(sentence.get("content").asText()) +// .confidence(ConfidenceType.valueOf(sentence.get("sentiment").asText().toUpperCase())) +// .offset(sentence.get("offset").asInt()) +// .length(sentence.get("length").asInt()) +// .highlightLength(sentence.get("highlights").get(0).get("length").asInt()) +// .highlightOffset(sentence.get("highlights").get(0).get("offset").asInt()) +// .confidenceStat(confidenceType) +// .build(); + + // ReviewSentiment 객체 리스트에 추가 +// sentimentList.add(reviewSentiment); + SentimentDetailDTO detailDTO = SentimentDetailDTO.builder() .content(sentence.get("content").asText()) .confidence(ConfidenceType.valueOf(sentence.get("sentiment").asText().toUpperCase())) .offset(sentence.get("offset").asInt()) @@ -74,15 +90,20 @@ public List getSentiment(String content) { .highlightOffset(sentence.get("highlights").get(0).get("offset").asInt()) .build(); - // ReviewSentiment 객체 리스트에 추가 - sentimentList.add(reviewSentiment); + detailDTOList.add(detailDTO); } + + build = SentimentDTO.builder() + .documentConfidence(confidenceType) + .sentimentDetailDTOList(detailDTOList) + .build(); } catch (JsonMappingException e) { e.printStackTrace(); } catch (JsonProcessingException e) { e.printStackTrace(); } finally { - return sentimentList; + return build; +// return sentimentList; } } } diff --git a/src/main/java/com/ssg/starroad/common/util/CookieUtil.java b/src/main/java/com/ssg/starroad/common/util/CookieUtil.java new file mode 100644 index 0000000..2519a82 --- /dev/null +++ b/src/main/java/com/ssg/starroad/common/util/CookieUtil.java @@ -0,0 +1,66 @@ +package com.ssg.starroad.common.util; + +// OAuth2 인증 플로우에서 쿠키를 사용하는 주된 이유는 인증 과정에서의 상태 관리와 보안을 강화하기 위해 +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.SerializationUtils; + +import java.util.Base64; + +public class CookieUtil { + + // 새로운 쿠키를 생성하고 클라이언트에게 전송 + public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); // 모든 경로에서 사용 + cookie.setHttpOnly(true); // JavaScript를 통한 접근 방지 + cookie.setSecure(true); // HTTPS 통신에서만 쿠키 전송 + cookie.setMaxAge(maxAge); // 쿠키의 생명 주기 제어 + response.addCookie(cookie); // 응답에 쿠키를 추가하여 클라이언트에게 전송 + } + // HTTPOnly와 Secure 플래그를 가지게 설정하여 XSS와 데이터 중간자 공격을 방지 + + // 지정된 이름의 쿠키를 삭제 + public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { + Cookie[] cookies = request.getCookies(); + // 요청에서 쿠키 배열을 가져옴 쿠키가 없으면 메소드 종료 + if (cookies == null) { + return; + } + + for (Cookie cookie : cookies) { + if (name.equals(cookie.getName())) { + cookie.setValue(""); // 해당 쿠키의 값을 빈 문자열로 설정 + cookie.setPath("/"); + cookie.setMaxAge(0); // 0으로 설정하여 즉시 만료되게 + response.addCookie(cookie); + } + // 변경된 쿠키를 응답에 추가하여 클라이언트에게 전송, 쿠키를 삭제 + } + } + + // 객체를 직렬화한 후 Base64 인코딩된 문자열로 변환하여 반환 + // dbj : 직렬화할 객체 + public static String serialize(Object obj) { + return Base64.getUrlEncoder() + .encodeToString(SerializationUtils.serialize(obj)); + } + // SerializationUtils를 사용하여 객체를 바이트 배열로 직렬화 + // 직렬화된 데이터를 Base64 인코딩하여 안전하게 문자열 형태로 변환 + // HTTP 헤더나 쿠키 값으로 사용될 때 유용 + + // 쿠키에 저장된 Base64 인코딩된 문자열을 객체로 역직렬화 + // cookie: 역직렬화할 데이터를 포함하고 있는 쿠키 + // cls: 반환될 객체의 클래스 타입 + public static T deserialize(Cookie cookie, Class cls) { + return cls.cast( + SerializationUtils.deserialize( + Base64.getUrlDecoder().decode(cookie.getValue()) + ) + ); + } + // 쿠키에서 값을 가져와 Base64 디코딩을 수행하여 바이트 배열로 변환 + // SerializationUtils를 사용하여 바이트 배열을 객체로 역직렬화 + // 역직렬화된 객체를 지정된 클래스 타입으로 캐스팅하여 반환 +} diff --git a/src/main/java/com/ssg/starroad/common/util/KeyGenerator.java b/src/main/java/com/ssg/starroad/common/util/KeyGenerator.java new file mode 100644 index 0000000..c1ca794 --- /dev/null +++ b/src/main/java/com/ssg/starroad/common/util/KeyGenerator.java @@ -0,0 +1,17 @@ +package com.ssg.starroad.common.util; + +import java.security.SecureRandom; +import java.util.Base64; + +// 주로 암호화 또는 토큰 서명에 사용 +// JWT 토큰을 서명할 때 사용되는 비밀키는 해당 토큰의 보안을 유지하는 데 필수적 +// 토큰의 내용을 암호화하거나 서명하는 데 사용되어, 토큰이 조작되지 않았음을 검증 가능 +public class KeyGenerator { + public static void main(String[] args) { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[32]; // 32바이트로 256비트 키 생성 + random.nextBytes(bytes); + String encodedKey = Base64.getEncoder().encodeToString(bytes); + System.out.println("생성된 Base64 인코딩된 비밀 키: " + encodedKey); + } +} diff --git a/src/main/java/com/ssg/starroad/common/util/TokenGenerator.java b/src/main/java/com/ssg/starroad/common/util/TokenGenerator.java new file mode 100644 index 0000000..0518ee2 --- /dev/null +++ b/src/main/java/com/ssg/starroad/common/util/TokenGenerator.java @@ -0,0 +1,21 @@ +package com.ssg.starroad.common.util; + +import java.security.SecureRandom; +import java.util.Base64; + +public class TokenGenerator { + private static final SecureRandom secureRandom = new SecureRandom(); + private static final Base64.Encoder base64Encoder = Base64.getUrlEncoder(); + + public static String generateUniqueToken() { + byte[] randomBytes = new byte[24]; // 192 bits + secureRandom.nextBytes(randomBytes); + return base64Encoder.encodeToString(randomBytes); + } +} + +// SecureRandom을 사용하여 토큰을 생성하는 것은 토큰 값이 완전히 무작위이고, 외부에서 예측할 수 없게 만들기 +// SecureRandom은 암호학적으로 안전한 무작위 수 생성기로, 높은 수준의 무작위성을 보장 + +// TokenGenerator에서 고유한 리프레시 토큰을 생성하는 로직을 구성하는 것은 +// 보안, 데이터 무결성, 그리고 사용자 간의 간섭 방지를 위해 필수적 \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/config/Bcrypt.java b/src/main/java/com/ssg/starroad/config/Bcrypt.java new file mode 100644 index 0000000..e254099 --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/Bcrypt.java @@ -0,0 +1,14 @@ +package com.ssg.starroad.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class Bcrypt +{ + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/ssg/starroad/config/CorsConfig.java b/src/main/java/com/ssg/starroad/config/CorsConfig.java new file mode 100644 index 0000000..766079f --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/CorsConfig.java @@ -0,0 +1,38 @@ +package com.ssg.starroad.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + @Bean + public UrlBasedCorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("http://223.130.155.196:8080"); // 프로덕션용 + config.addAllowedOriginPattern("http://localhost:8080"); // Vue 애플리케이션 주소 추가 + + config.addAllowedHeader("*"); // 서버가 받을 수 있는 모든 헤더를 정의 + + // 각 HTTP 메소드를 개별적으로 추가 + config.addAllowedMethod("GET"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("PATCH"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("OPTIONS"); + + config.addExposedHeader("Authorization"); // 클라이언트가 읽을 수 있도록 서버의 응답에서 노출할 헤더를 정의 + config.addExposedHeader("refreshToken"); + source.registerCorsConfiguration("/**", config); // 모든 경로에 CORS 설정 적용 + return source; + } + @Bean + public CorsFilter corsFilter() { + return new CorsFilter(corsConfigurationSource()); + } +} diff --git a/src/main/java/com/ssg/starroad/config/EmailConfig.java b/src/main/java/com/ssg/starroad/config/EmailConfig.java new file mode 100644 index 0000000..a910502 --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/EmailConfig.java @@ -0,0 +1,64 @@ +package com.ssg.starroad.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; +// JavaMailSender 인터페이스를 구현 +// JavaMail API를 사용하여 이메일을 전송하는 데 사용 + +@Configuration +public class EmailConfig { + + @Value("${spring.mail.host}") + private String host; + + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Value("${spring.mail.properties.mail.smtp.auth}") + private boolean auth; + + @Value("${spring.mail.properties.mail.smtp.starttls.enable}") + private boolean starttlsEnable; + + @Value("${spring.mail.properties.mail.smtp.starttls.required}") + private boolean starttlsRequired; + + @Value("${spring.mail.properties.mail.smtp.connectiontimeout}") + private int connectionTimeout; + + @Value("${spring.mail.properties.mail.smtp.timeout}") + private int timeout; + + @Value("${spring.mail.properties.mail.smtp.writetimeout}") + private int writeTimeout; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.smtp.auth", auth); + props.put("mail.smtp.starttls.enable", starttlsEnable); + props.put("mail.smtp.starttls.required", starttlsRequired); + props.put("mail.smtp.connectiontimeout", connectionTimeout); + props.put("mail.smtp.timeout", timeout); + props.put("mail.smtp.writetimeout", writeTimeout); + + return mailSender; + } +} diff --git a/src/main/java/com/ssg/starroad/config/TokenAuthenticationFilter.java b/src/main/java/com/ssg/starroad/config/TokenAuthenticationFilter.java new file mode 100644 index 0000000..75c472f --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/TokenAuthenticationFilter.java @@ -0,0 +1,111 @@ +package com.ssg.starroad.config; + +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.user.entity.User; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +// Spring Security의 일부로서 인증 과정에서 중요한 역할 +// HTTP 요청에서 인증 토큰 (예: JWT)을 추출하고, 이를 검증하여 사용자의 인증 상태를 결정하는 것 +// Spring Security에서 제공하는 OncePerRequestFilter 클래스를 상속받아 구현하는 것이 일반적 +// OncePerRequestFilter를 사용하면, 필터가 한 요청에 대해 한 번만 실행되는 것을 보장 +// doFilterInternal 메소드를 오버라이드하여 필터의 로직을 구현 +@RequiredArgsConstructor +public class TokenAuthenticationFilter extends OncePerRequestFilter { + private final TokenProvider tokenProvider; + // 토큰 생성하고 검증하는 컴포넌트 + + private final static String HEADER_AUTHORIZATION = "Authorization"; + // HTTP 요청에서 사용되는 헤더 필드의 이름 정의 + // 인증 토큰은 보통 이 헤더에 "Bearer" 형식으로 포함됨 + // 소유자라는 뜻인데 이 토큰의 소유자에게 권한을 부여해 + + private final static String TOKEN_PREFIX = "Bearer "; + // 인증 헤더에 사용되는 토큰의 접두사 실제 토큰 값만 추출하기 위해 사용 + + private final static Logger log = LoggerFactory.getLogger(TokenAuthenticationFilter.class); + // 로깅 + + // 실제 필터의 로직을 구현하는 메소드 + // 요청의 헤더에서 토큰을 추출하고, 이 토큰이 유효한 경우에만 + // Spring Security의 SecurityContext에 인증 객체(Authentication)를 설정 + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + // 1. HTTP 요청에서 Authorization 헤더는 사용자 인증 정보를 전송하는 데 사용되는 표준 헤더 + // 특히 보안이 필요한 애플리케이션에서 이 헤더를 사용하여 클라이언트가 서버에 자신의 인증 정보를 제공 + // Bearer Token (주로 JWT 사용): 클라이언트는 접근 토큰을 'Bearer' 라는 접두사와 함께 전송 + String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION); + String token = getAccessToken(authorizationHeader); + + // 2. TokenProvider를 사용하여 토큰 검증 + if (token != null) { + try { + // 3. 유효한 토큰인 경우, 사용자 인증 객체를 생성하고 SecurityContextHolder에 등록 + if (tokenProvider.validToken(token)) { + Authentication authentication = tokenProvider.getAuthentication(token); + + // 사용자 상태 확인 + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + if (userDetails instanceof User) { + User user = (User) userDetails; + if (!user.isEnabled()) { + log.info("User is not enabled: {}", user.getUsername()); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + } + + SecurityContextHolder.getContext().setAuthentication(authentication); + } else { + log.info("JWT가 유효하지 않음: {}", token); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + // 4. 유효하지 않은 토큰 또는 예외 발생 시 적절한 HTTP 상태 코드 설정 + } catch (Exception e) { + log.error("Authentication error: ", e); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + } + + filterChain.doFilter(request, response); + } + + // Authorization 헤더에서 실제 토큰 값을 추출 + // TOKEN_PREFIX는 "Bearer "로 설정되어 있으며, 이는 토큰 타입을 명시하는 표준 방식 + // substring 메소드를 사용하여 "Bearer " 다음의 문자열을 잘라내고, 실제 토큰 값을 반환 + // 이렇게 추출된 토큰은 유효성 검사를 위해 TokenProvider에 전달 + // 유효한 경우 사용자 인증 정보를 생성하여 SecurityContext에 설정 + private String getAccessToken(String authorizationHeader) { + if (authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)) { + return authorizationHeader.substring(TOKEN_PREFIX.length()); + } + return null; + } + + // 특정 경로에 대해 필터를 적용하지 않도록 설정 + // 일반적으로 인증이 필요 없는 공개 API 경로에서 필터를 적용하지 않도록 하여 성능을 최적화 + // 로그인, 회원 가입, 공개적으로 접근 가능한 정보 제공 API 등 인증이 필요 없는 경로들에 대해 필터를 적용하지 않도록 설정 + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + return path.startsWith("/user/login") || + path.startsWith("/user/join"); + } +} \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/config/WebConfigDev.java b/src/main/java/com/ssg/starroad/config/WebConfigDev.java index 88ccd93..a9ae090 100644 --- a/src/main/java/com/ssg/starroad/config/WebConfigDev.java +++ b/src/main/java/com/ssg/starroad/config/WebConfigDev.java @@ -1,19 +1,20 @@ -package com.ssg.starroad.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Profile("development") -@Configuration -public class WebConfigDev implements WebMvcConfigurer { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins("http://localhost:8080") - .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); - } -} +//package com.ssg.starroad.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.context.annotation.Profile; +//import org.springframework.web.servlet.config.annotation.CorsRegistry; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +//@Profile("development") +//@Configuration +//public class WebConfigDev implements WebMvcConfigurer { +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// registry.addMapping("/**") +// .allowedOrigins("http://localhost:8080") +// .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") +// .allowedHeaders("*") +// .allowCredentials(true) +// .exposedHeaders("Authorization", "refreshToken"); // 서버의 응답에서 노출할 헤더 추가;; +// } +//} diff --git a/src/main/java/com/ssg/starroad/config/WebConfigProd.java b/src/main/java/com/ssg/starroad/config/WebConfigProd.java index 1a74cc4..58c26a9 100644 --- a/src/main/java/com/ssg/starroad/config/WebConfigProd.java +++ b/src/main/java/com/ssg/starroad/config/WebConfigProd.java @@ -1,19 +1,20 @@ -package com.ssg.starroad.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Profile("production") -@Configuration -public class WebConfigProd implements WebMvcConfigurer { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins("http://10.0.15.6", "http://223.130.155.196") - .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); - } -} +//package com.ssg.starroad.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.context.annotation.Profile; +//import org.springframework.web.servlet.config.annotation.CorsRegistry; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +//@Profile("production") +//@Configuration +//public class WebConfigProd implements WebMvcConfigurer { +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// registry.addMapping("/**") +// .allowedOrigins("http://10.0.15.6:8080", "http://223.130.155.196:8080") +// .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") +// .allowedHeaders("*") +// .allowCredentials(true) +// .exposedHeaders("Authorization", "refreshToken"); // 서버의 응답에서 노출할 헤더 추가; +// } +//} diff --git a/src/main/java/com/ssg/starroad/config/WebSecurityConfig.java b/src/main/java/com/ssg/starroad/config/WebSecurityConfig.java new file mode 100644 index 0000000..48bab59 --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/WebSecurityConfig.java @@ -0,0 +1,111 @@ +package com.ssg.starroad.config; + +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.config.oauth.OAuth2AuthorizationRequestBasedOnCookieRepository; +import com.ssg.starroad.config.oauth.OAuth2SuccessHandler; +import com.ssg.starroad.user.repository.RefreshTokenRepository; +import com.ssg.starroad.user.service.CustomOAuth2UserService; +import com.ssg.starroad.user.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + +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.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + + +import java.io.IOException; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class WebSecurityConfig{ + + private final TokenProvider tokenProvider; + private final UserService userService; + private final Bcrypt passwordEncoder; + private final RefreshTokenRepository refreshTokenRepository; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final CorsConfig corsConfig; // CORS 설정을 위해 CorsConfig 클래스 주입 + + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { // security를 적용하지 않을 리소스 + return web -> web.ignoring() + // error endpoint를 열어줌 + .requestMatchers("/error","/favicon.ico"); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagementConfig -> sessionManagementConfig + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/user/login","/login/**","/user/logout","/user/join/**","/oauth2/**","/api/token/**","/auth/**","/user/update-password").permitAll() + .requestMatchers("/api/oauth2/**").permitAll() + .requestMatchers("/review-likes/**").permitAll() + .requestMatchers("/store/**").permitAll() + .requestMatchers("/user/update-profile","/user/details","/user/inactive").authenticated() + .anyRequest().authenticated()) + .oauth2Login(oauth -> oauth + .loginPage("/user/login") + .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) + .successHandler(oAuth2SuccessHandler)) + .addFilterBefore(new TokenAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(this::authenticationEntryPoint) + .accessDeniedHandler(this::accessDeniedHandler)); + + return http.build(); + } + private void authenticationEntryPoint(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + } + + private void accessDeniedHandler(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); + } + + @Bean + public OAuth2SuccessHandler oAuth2SuccessHandler() { + return new OAuth2SuccessHandler(tokenProvider, customOAuth2UserService); + } + + @Bean + public TokenAuthenticationFilter tokenAuthenticationFilter() { + return new TokenAuthenticationFilter(tokenProvider); + } + + @Bean + public OAuth2AuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository() { + return new OAuth2AuthorizationRequestBasedOnCookieRepository(); + } + + @Bean + public AuthenticationManager authManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder + .userDetailsService(userService) + .passwordEncoder(passwordEncoder.passwordEncoder()); + return authenticationManagerBuilder.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/config/jwt/JwtProperties.java b/src/main/java/com/ssg/starroad/config/jwt/JwtProperties.java new file mode 100644 index 0000000..9156084 --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/jwt/JwtProperties.java @@ -0,0 +1,23 @@ +package com.ssg.starroad.config.jwt; + + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Getter +@Setter +@Component +@Validated +@ConfigurationProperties(prefix = "jwt") +// 자바 클래스에 프로퍼티 값을 가져와서 사용. 프로퍼티들을 자동으로 바인딩. +// prefix = "jwt"는 프로퍼티 이름이 jwt.xxx 형식임을 의미 + +public class JwtProperties { + @NotBlank + private String secretKey; + private String issuer; +} diff --git a/src/main/java/com/ssg/starroad/config/jwt/TokenProvider.java b/src/main/java/com/ssg/starroad/config/jwt/TokenProvider.java new file mode 100644 index 0000000..868ea6c --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/jwt/TokenProvider.java @@ -0,0 +1,114 @@ +package com.ssg.starroad.config.jwt; +// JWT를 생성하고 파싱하여 유효성을 검사하는 역할 +// JwtProperties 클래스에서 설정된 값을 사용하여 토큰을 관리 + +import com.ssg.starroad.user.entity.User; +import io.jsonwebtoken.Jws; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.io.Decoders; +import javax.crypto.SecretKey; +import java.time.Duration; +import java.util.Collections; +import java.util.Date; +import java.util.Set; + +@RequiredArgsConstructor +@Service +public class TokenProvider { + + private final JwtProperties jwtProperties; + + public String generateToken(User user, Duration expiredAt) { + Date now = new Date(); + return createToken(new Date(now.getTime() + expiredAt.toMillis()), user); + } + // 사용자 객체와 만료 시간(Duration)을 받아 jwt 토큰 생성 + // 파라미터 (사용자 정보, 토큰 유효 기간) + // 현재 시간을 구하고 expiredAt으로 전달된 기간을 현재 시간에 더해 토큰 만료 시간 expiry 계산 + // creatToken 메소드 호출 실제 토큰 생성 + + + // 실제 jwt 토큰 생성 사용자 정보와 만료 시간을 받아 구성, 서명해 반환 + public String createToken(Date expiry, User user) { + + Date now = new Date(); + SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.getSecretKey())); + + + return Jwts.builder() + .setHeaderParam("typ", "JWT") // 헤더 타입 jwt + // 내용 jwt의 클레임 설정 + .setIssuer(jwtProperties.getIssuer()) // 토큰 발급자 + .setIssuedAt(now) // 토큰 발급 시간 + .setExpiration(expiry) // 토큰 만료 시간 + .setSubject(user.getEmail()) // 사용자 이메일 + .claim("userId", user.getId()) // 추가 정보 클레임 id : 유저 아이디 + // 토큰에 사용자 고유 id 정보 담아 토큰 해석 시 사용자 정보 접근에 용이 + // 서명 비밀값과 합께 해시값을 HS256 방식으로 암호화 + .signWith(key) + .compact(); + } + + // 제공된 토큰이 유효한지 검사. 토큰의 서명 검증. 파싱 중 발생할 수 있는 예외 처리 +// 예외가 발생하면 false 그렇지 않으면 true + public boolean validToken(String token) { + try { + SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.getSecretKey())); + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + + Claims claims = claimsJws.getBody(); + Date expiration = claims.getExpiration(); + System.out.println("Token expiration time: " + expiration); + + return !expiration.before(new Date()); + } catch (Exception e) { + System.out.println("Invalid token: " + e.getMessage()); + return false; + } + } + + + // 토큰 기반으로 Authentication 인증 객체 생성. 시큐리티에서 사용자의 인증 정보 나타내는 데 사용 + public Authentication getAuthentication(String token) { + Claims claims = getClaims(token); + // 토큰에서 클레임 추출 + // 클레임에서 사용자 역할 포함하는 authorities 설정 + Set authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")); + + return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User + (claims.getSubject(), "", authorities), token, authorities); + } + + // 토큰에서 사용자 id 추출 + public Long getUserId(String token) { + Claims claims = getClaims(token); + return claims.get("userId", Long.class); + } + + // 토큰에서 클레임을 추출하는 보조 메소드 + // 토큰의 파싱 담당 클레임 세트 반환 + private Claims getClaims(String token) { + SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.getSecretKey())); + return Jwts.parserBuilder() // 클레임 조회 + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String generateAccessToken(User user) { + // 액세스 토큰의 유효 기간을 설정합니다 (예: 1시간) + Duration accessTokenValidity = Duration.ofHours(1); + return generateToken(user, accessTokenValidity); + } +} diff --git a/src/main/java/com/ssg/starroad/config/oauth/OAuth2AuthorizationRequestBasedOnCookieRepository.java b/src/main/java/com/ssg/starroad/config/oauth/OAuth2AuthorizationRequestBasedOnCookieRepository.java new file mode 100644 index 0000000..6524adc --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/oauth/OAuth2AuthorizationRequestBasedOnCookieRepository.java @@ -0,0 +1,47 @@ +package com.ssg.starroad.config.oauth; + +import com.ssg.starroad.common.util.CookieUtil; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.web.util.WebUtils; + +public class OAuth2AuthorizationRequestBasedOnCookieRepository implements AuthorizationRequestRepository { + + public final static String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + private final static int COOKIE_EXPIRE_SECONDS = 180; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + return CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class); + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { + if (authorizationRequest == null) { + removeAuthorizationRequestCookies(request, response); + return; + } + + CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS); + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { + return this.loadAuthorizationRequest(request); + } + + public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) { + CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + } +} + +// 쿠키 삭제 메소드: removeAuthorizationRequest 메소드와 removeAuthorizationRequestCookies 메소드 +// 모두 CookieUtil.deleteCookie를 사용하여 쿠키를 삭제 -> 코드의 일관성과 재사용성을 높임 +// 예외 처리 및 유효성 검증: loadAuthorizationRequest 메소드에서 쿠키가 null일 경우 적절하게 null을 반환하도록 처리 +// NullPointerException을 방지 +// 직렬화 및 역직렬화 로직의 캡슐화: CookieUtil 클래스의 serialize 및 deserialize 메소드를 사용하여 +// OAuth2 인증 요청 객체의 직렬화와 역직렬화를 수행 -> 보안을 강화하고 코드의 유지보수를 용이 diff --git a/src/main/java/com/ssg/starroad/config/oauth/OAuth2SuccessHandler.java b/src/main/java/com/ssg/starroad/config/oauth/OAuth2SuccessHandler.java new file mode 100644 index 0000000..877c83e --- /dev/null +++ b/src/main/java/com/ssg/starroad/config/oauth/OAuth2SuccessHandler.java @@ -0,0 +1,98 @@ +package com.ssg.starroad.config.oauth; + +import com.ssg.starroad.common.util.CookieUtil; +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.service.CustomOAuth2UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.time.Duration; + +@Component +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final TokenProvider tokenProvider; + private final CustomOAuth2UserService customOAuth2UserService; + // 액세스 토큰 및 리프레시 토큰 생성과 사용자 정보 조회 기능 활용 + // CustomOAuth2UserService를 사용하여 OAuth2 로그인 성공 후 사용자 정보를 처리 + + private static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token"; + private static final String ACCESS_TOKEN_COOKIE_NAME = "access_token"; + private static final int REFRESH_TOKEN_EXPIRY = 60 * 60 * 24 * 14; + // "60초 * 60분 * 24시간 * 14일" 결과적으로 리프레시 토큰의 유효 기간은 14일(두 주) + // (액세스 토큰보다 긴 유효 기간)사용자가 액세스 토큰이 만료된 후에도 새로운 액세스 토큰을 얻을 수 있음 + + private static final int ACCESS_TOKEN_EXPIRY = 60 * 60 * 24; + // "60초 * 60분 * 24시간" 결과적으로 액세스 토큰의 유효 기간은 24시간, 즉 1일 + // (리소스에 대한 접근을 제어하는 짧은 유효 기간의 토큰) 사용자가 인증된 상태로 서비스를 이용할 수 있게 함 + + public OAuth2SuccessHandler(TokenProvider tokenProvider, CustomOAuth2UserService customOAuth2UserService) { + this.tokenProvider = tokenProvider; + this.customOAuth2UserService = customOAuth2UserService; + } + + public static int getRefreshTokenExpiry() { + return REFRESH_TOKEN_EXPIRY; + } + + public static int getAccessTokenExpiry() { + return ACCESS_TOKEN_EXPIRY; + } + + + // OAuth2 인증 과정이 성공적으로 완료된 후 실행되는 메소드 + // 1) 사용자 인증 정보를 활용하여 토큰을 생성하고, + // 2) 이를 클라이언트 측에 전달하기 위해 쿠키에 저장 + // 3) 사용자를 최종 목적지 URL로 리디렉션 + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + try { + OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; + OAuth2User oAuth2User = oauthToken.getPrincipal(); + + // 이메일을 통해 사용자 정보 조회 + User user = customOAuth2UserService.findUserByEmail(oAuth2User.getAttribute("email")); + + // 토큰 생성 + String refreshToken = tokenProvider.generateToken(user, Duration.ofSeconds(REFRESH_TOKEN_EXPIRY)); + String accessToken = tokenProvider.generateToken(user, Duration.ofSeconds(ACCESS_TOKEN_EXPIRY)); + + // 쿠키에 토큰 추가 + addCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, REFRESH_TOKEN_EXPIRY); + addCookie(response, ACCESS_TOKEN_COOKIE_NAME, accessToken, ACCESS_TOKEN_EXPIRY); + + // 세션 무효화 + request.getSession().invalidate(); + + // 최종 목적지 URL로 리다이렉트 + String targetUrl = determineTargetUrl(request, response, authentication); + clearAuthenticationAttributes(request); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } catch (Exception e) { + throw new RuntimeException("OAuth2 인증 성공 처리 중 오류 발생", e); + } + } + + // 지정된 이름, 값, 최대 유지 시간을 가진 쿠키를 HTTP 응답에 추가하는 메소드 + // 인증 토큰을 클라이언트에게 안전하게 전달하는 데 사용 + // addCookie 메소드 하나만 있어도 충분히 토큰 종류(액세스/리프레시)를 구별하여 쿠키에 저장 + private void addCookie(HttpServletResponse response, String name, String value, int maxAge) { + CookieUtil.addCookie(response,name, value, maxAge); + } + + // 인증 성공 후 리디렉션할 최종 URL을 결정하는 메소드 + // URL은 사용자가 인증 후 이동할 최종 목적지를 지정 + // UriComponentsBuilder를 사용하여 리디렉션할 URL을 구성하고, 이를 문자열로 반환 + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + return UriComponentsBuilder.fromUriString("/main") + .build().toUriString(); + } +} diff --git a/src/main/java/com/ssg/starroad/coupon/DTO/IssueCouponRequest.java b/src/main/java/com/ssg/starroad/coupon/DTO/IssueCouponRequest.java index f2ac4a1..f26f3df 100644 --- a/src/main/java/com/ssg/starroad/coupon/DTO/IssueCouponRequest.java +++ b/src/main/java/com/ssg/starroad/coupon/DTO/IssueCouponRequest.java @@ -3,18 +3,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; public class IssueCouponRequest { - @JsonProperty("userId") - private Long userId; + @JsonProperty("email") + private String email; @JsonProperty("couponId") private Long couponId; // Getters and Setters - public Long getUserId() { - return userId; + public String getUserEmail() { + return email; } - public void setUserId(Long userId) { - this.userId = userId; + public void setUserEmail(String userId) { + this.email = userId; } public Long getCouponId() { diff --git a/src/main/java/com/ssg/starroad/coupon/controller/CouponController.java b/src/main/java/com/ssg/starroad/coupon/controller/CouponController.java index a8b39fd..5b85fe1 100644 --- a/src/main/java/com/ssg/starroad/coupon/controller/CouponController.java +++ b/src/main/java/com/ssg/starroad/coupon/controller/CouponController.java @@ -17,19 +17,19 @@ public class CouponController { private final CouponHistoryService couponHistoryService; @PostMapping("/issue") - public ResponseEntity issueCoupon(@RequestBody IssueCouponRequest request) { + public ResponseEntity issueCoupon(@RequestBody IssueCouponRequest request) { - if (request.getUserId() == null || request.getCouponId() == null) { - return ResponseEntity.badRequest().body("UserId or CouponId cannot be null"); + if (request.getUserEmail() == null || request.getCouponId() == null) { + return ResponseEntity.badRequest().body(0); } - couponHistoryService.CouponUserAdd(request.getUserId(), request.getCouponId()); + int rate = couponHistoryService.CouponUserAdd(request.getUserEmail(), request.getCouponId()); - return ResponseEntity.ok("Coupon issued successfully to user ID " + request.getUserId()); + return ResponseEntity.ok(rate); } - @GetMapping("/{userId}/coupon/list") - public ResponseEntity> getUserCoupons(@PathVariable Long userId) { - List coupons = couponHistoryService.CouponsUserList(userId); + @GetMapping("/{email}/coupon/list") + public ResponseEntity> getUserCoupons(@PathVariable String email) { + List coupons = couponHistoryService.CouponsUserList(email); if (coupons.isEmpty()) { return ResponseEntity.noContent().build(); // 내용이 없을 경우 No Content (204) 반환 } diff --git a/src/main/java/com/ssg/starroad/coupon/entity/CouponHistory.java b/src/main/java/com/ssg/starroad/coupon/entity/CouponHistory.java index 2b5f9eb..b0d95dd 100644 --- a/src/main/java/com/ssg/starroad/coupon/entity/CouponHistory.java +++ b/src/main/java/com/ssg/starroad/coupon/entity/CouponHistory.java @@ -29,6 +29,7 @@ public class CouponHistory extends BaseTimeEntity { private Long couponId; private boolean usageStatus; private LocalDate expiredAt; + private int discountRate; public void useCoupon(){ this.usageStatus=true; diff --git a/src/main/java/com/ssg/starroad/coupon/repository/impl/CouponHistoryRepositoryCustom.java b/src/main/java/com/ssg/starroad/coupon/repository/impl/CouponHistoryRepositoryCustom.java index 6106fce..eaf931f 100644 --- a/src/main/java/com/ssg/starroad/coupon/repository/impl/CouponHistoryRepositoryCustom.java +++ b/src/main/java/com/ssg/starroad/coupon/repository/impl/CouponHistoryRepositoryCustom.java @@ -37,7 +37,7 @@ public List findCouponsByUserId(Long userId) { qCouponHistory.id.as("couponHistoryId"), qCoupon.id.as("couponId"), qCoupon.name.as("couponName"), - qCoupon.discountRate.as("couponDiscountRate"), + qCouponHistory.discountRate.as("couponDiscountRate"), qCoupon.discountAmount.as("couponDiscountAmount"), qCoupon.status.as("couponStatus"), qCouponHistory.usageStatus.as("couponUsageStatus"), diff --git a/src/main/java/com/ssg/starroad/coupon/service/CouponHistoryService.java b/src/main/java/com/ssg/starroad/coupon/service/CouponHistoryService.java index b5ac3b8..df905fa 100644 --- a/src/main/java/com/ssg/starroad/coupon/service/CouponHistoryService.java +++ b/src/main/java/com/ssg/starroad/coupon/service/CouponHistoryService.java @@ -7,7 +7,7 @@ public interface CouponHistoryService { - void CouponUserAdd(Long userID, Long couponID); + int CouponUserAdd(String userEmail, Long couponID); @@ -19,5 +19,5 @@ public interface CouponHistoryService { - List CouponsUserList(Long userID); + List CouponsUserList(String email); } diff --git a/src/main/java/com/ssg/starroad/coupon/service/impl/CouponHistoryServiceImpl.java b/src/main/java/com/ssg/starroad/coupon/service/impl/CouponHistoryServiceImpl.java index 19ffee3..af2a846 100644 --- a/src/main/java/com/ssg/starroad/coupon/service/impl/CouponHistoryServiceImpl.java +++ b/src/main/java/com/ssg/starroad/coupon/service/impl/CouponHistoryServiceImpl.java @@ -1,6 +1,7 @@ package com.ssg.starroad.coupon.service.impl; import com.ssg.starroad.coupon.DTO.CouponDTO; +import com.ssg.starroad.coupon.entity.Coupon; import com.ssg.starroad.coupon.entity.CouponHistory; import com.ssg.starroad.coupon.repository.CouponHistoryRepository; import com.ssg.starroad.coupon.repository.CouponRepository; @@ -13,10 +14,13 @@ import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; +import java.time.DayOfWeek; import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; @Service @@ -30,16 +34,22 @@ public class CouponHistoryServiceImpl implements CouponHistoryService { private final RewardProcessRepository rewardProcessRepository; @Override - public void CouponUserAdd(Long userID, Long couponID) { + public int CouponUserAdd(String email, Long couponID) { + Coupon coupon = couponRepository.findById(couponID).orElseThrow(); + int max = coupon.getMaxAmount(); + int min = coupon.getMinAmount(); + int rate = ThreadLocalRandom.current().nextInt(min, max + 1); + CouponHistory couponHistory = - new CouponHistory(null, userRepository.findById(userID).orElseThrow(), - couponID,false, LocalDate.now().plusDays(7)); + new CouponHistory(null, userRepository.findByEmail(email).orElseThrow(), + couponID,false, LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY)),rate); couponHistoryRepository.save(couponHistory); + Long userID = userRepository.findByEmail(email).orElseThrow().getId(); RewardProcess rewardProcess =rewardProcessRepository.findById(userID).orElseThrow(); rewardProcess.setIssueStatus(true); rewardProcessRepository.save(rewardProcess); - + return rate; } @Override @@ -67,7 +77,8 @@ public boolean CouponUsageModify(Long couponHistoryId) { } @Override - public List CouponsUserList(Long userID) { + public List CouponsUserList(String email) { + Long userID = userRepository.findByEmail(email).orElseThrow().getId(); List coupons = couponHistoryRepositoryCustom.findCouponsByUserId(userID); LocalDate today = LocalDate.now(); diff --git a/src/main/java/com/ssg/starroad/review/DTO/ReviewDTO.java b/src/main/java/com/ssg/starroad/review/DTO/ReviewDTO.java index 5c69b3a..ecc2d21 100644 --- a/src/main/java/com/ssg/starroad/review/DTO/ReviewDTO.java +++ b/src/main/java/com/ssg/starroad/review/DTO/ReviewDTO.java @@ -1,14 +1,12 @@ package com.ssg.starroad.review.DTO; import com.ssg.starroad.review.enums.ConfidenceType; -import com.ssg.starroad.user.entity.User; -import jakarta.persistence.Embedded; +import com.ssg.starroad.shop.entity.Store; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import lombok.*; import org.springframework.web.multipart.MultipartFile; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -22,7 +20,9 @@ public class ReviewDTO { private String userNickname; private Long id; private Long userId; + private String userEmail; private String imagePath; + private boolean following; //팔로우 상태 private Long storeId; private boolean visible; private Long likeCount; @@ -32,10 +32,11 @@ public class ReviewDTO { private List reviewImages; // 리뷰 이미지 리스트 추가 private List reviewFeedbacks; // 리뷰 피드백 추가 private Long reviewcount; // 리뷰 갯수 + private boolean isLiked; // 내가 추가한 필드 choims 24/05/23 private LocalDateTime createDate; - @Enumerated(EnumType.STRING) private ConfidenceType confidence; + private Store store; private String shopName; private String purchaseDate; private String selectedTime; diff --git a/src/main/java/com/ssg/starroad/review/DTO/ReviewLikeDTO.java b/src/main/java/com/ssg/starroad/review/DTO/ReviewLikeDTO.java new file mode 100644 index 0000000..ad893cb --- /dev/null +++ b/src/main/java/com/ssg/starroad/review/DTO/ReviewLikeDTO.java @@ -0,0 +1,14 @@ +package com.ssg.starroad.review.DTO; + +import lombok.*; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ReviewLikeDTO { + private boolean isLiked; + private long likeCount; + +} diff --git a/src/main/java/com/ssg/starroad/review/DTO/ReviewSentimentDTO.java b/src/main/java/com/ssg/starroad/review/DTO/ReviewSentimentDTO.java index 938ca04..fa9c808 100644 --- a/src/main/java/com/ssg/starroad/review/DTO/ReviewSentimentDTO.java +++ b/src/main/java/com/ssg/starroad/review/DTO/ReviewSentimentDTO.java @@ -10,8 +10,8 @@ public class ReviewSentimentDTO { private Long id; private Long reviewId; // Review 엔티티의 ID private String content; - private int offset; - private int length; + private int totalOffset; + private int totalLength; private ConfidenceType confidence; private int highlightOffset; private int highlightLength; diff --git a/src/main/java/com/ssg/starroad/review/DTO/SentimentDTO.java b/src/main/java/com/ssg/starroad/review/DTO/SentimentDTO.java new file mode 100644 index 0000000..9152ebd --- /dev/null +++ b/src/main/java/com/ssg/starroad/review/DTO/SentimentDTO.java @@ -0,0 +1,19 @@ +package com.ssg.starroad.review.DTO; + +import com.ssg.starroad.review.enums.ConfidenceType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class SentimentDTO { + ConfidenceType documentConfidence; + + List sentimentDetailDTOList; +} diff --git a/src/main/java/com/ssg/starroad/review/DTO/SentimentDetailDTO.java b/src/main/java/com/ssg/starroad/review/DTO/SentimentDetailDTO.java new file mode 100644 index 0000000..6ba4c01 --- /dev/null +++ b/src/main/java/com/ssg/starroad/review/DTO/SentimentDetailDTO.java @@ -0,0 +1,23 @@ +package com.ssg.starroad.review.DTO; + +import com.ssg.starroad.review.enums.ConfidenceType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class SentimentDetailDTO { + + private String content; + private int offset; + private int length; + + private ConfidenceType confidence; + + private int highlightOffset; + private int highlightLength; +} diff --git a/src/main/java/com/ssg/starroad/review/controller/ReviewController.java b/src/main/java/com/ssg/starroad/review/controller/ReviewController.java index f2a3395..4966ab5 100644 --- a/src/main/java/com/ssg/starroad/review/controller/ReviewController.java +++ b/src/main/java/com/ssg/starroad/review/controller/ReviewController.java @@ -1,27 +1,25 @@ package com.ssg.starroad.review.controller; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.ssg.starroad.common.service.S3Uploader; -import com.ssg.starroad.review.DTO.*; +import com.ssg.starroad.review.DTO.ResponseReviewDTO; +import com.ssg.starroad.review.DTO.ReviewDTO; +import com.ssg.starroad.review.DTO.ReviewReceiptDTO; +import com.ssg.starroad.review.DTO.ReviewRequestDTO; import com.ssg.starroad.review.entity.Review; -import com.ssg.starroad.review.entity.ReviewImage; import com.ssg.starroad.review.repository.ReviewImageRepository; import com.ssg.starroad.review.service.ReviewFeedbackService; import com.ssg.starroad.review.service.ReviewReceiptService; import com.ssg.starroad.review.service.ReviewService; -import com.ssg.starroad.shop.entity.Store; -import com.ssg.starroad.shop.repository.StoreRepository; -import com.ssg.starroad.user.entity.User; -import com.ssg.starroad.user.repository.UserRepository; +import com.ssg.starroad.user.dto.RankUserDTO; +import com.ssg.starroad.user.service.UserService; import lombok.RequiredArgsConstructor; -import org.springframework.http.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; +import java.util.List; @RequestMapping("/reviews") @RestController @@ -31,11 +29,8 @@ public class ReviewController { private final ReviewService reviewService; private final ReviewReceiptService reviewReceiptService; private final ReviewFeedbackService reviewFeedbackService; - private final StoreRepository storeRepository; - private final UserRepository userRepository; - private final S3Uploader s3Uploader; private final ReviewImageRepository imageRepository; - private final ReviewImageRepository reviewImageRepository; + private final UserService userService; @PostMapping("/write") @@ -67,26 +62,39 @@ public ResponseEntity getReviewById(@PathVariable Long id) { } - @GetMapping - public ResponseEntity getAllReviews(@RequestParam(defaultValue = "0") int page, + public ResponseEntity getAllReviews(@RequestParam String userEmail, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { try { - ResponseReviewDTO responseReviewDTO = reviewService.findAllReview(page, size); + ResponseReviewDTO responseReviewDTO = reviewService.findAllReview(userEmail, page, size); return ResponseEntity.ok(responseReviewDTO); } catch (RuntimeException e) { - return ResponseEntity.notFound().build(); + return ResponseEntity.badRequest().build(); } } @GetMapping("/following") - public ResponseEntity getFollowingReviews(@RequestParam Long id, + public ResponseEntity getFollowingReviews(@RequestParam String userEmail, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { - System.out.printf("following 리뷰 메소드 진입"); + System.out.printf("CReviewController::getFollowingReviews userEmail : %s\n", userEmail); try { System.out.printf("try문 실행"); - ResponseReviewDTO responseReviewDTO = reviewService.findFollowingReview(id, page, size); + ResponseReviewDTO responseReviewDTO = reviewService.findFollowingReview(userEmail, page, size); + return ResponseEntity.ok(responseReviewDTO); + } catch (RuntimeException e) { + System.out.printf(e.getMessage()); + return ResponseEntity.badRequest().build(); + } + } + @GetMapping("/myreveiw") + public ResponseEntity getMyReviews(@RequestParam String email, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + System.out.printf("my 리뷰 메소드 진입"); + try { + ResponseReviewDTO responseReviewDTO = reviewService.getUserReview(email, page, size); return ResponseEntity.ok(responseReviewDTO); } catch (RuntimeException e) { System.out.printf(e.getMessage()); @@ -98,51 +106,39 @@ public ResponseEntity getFollowingReviews(@RequestParam Long @PostMapping("/submit") public ResponseEntity submitSurvey( @RequestPart("review") String reviewStr, // JSON 문자열로 받습니다. - @RequestPart("images") List uploadedImages) throws IOException { + @RequestPart(value = "images", required = false) List uploadedImages) throws IOException { + System.out.printf("UploadedImaged : " + uploadedImages); // JSON 문자열을 ReviewDTO 객체로 변환합니다. ReviewDTO reviewDTO = new ObjectMapper().readValue(reviewStr, ReviewDTO.class); - System.out.println("reviewSelectionDTO toString : " + reviewDTO.toString()); - - User user = userRepository.findBynickname(reviewDTO.getUserNickname()).orElseThrow(() -> new IllegalArgumentException("User not found for the nickname: " + reviewDTO.getUserNickname())); - // Store 엔티티가 아직 저장되지 않은 경우 저장 - Store store = storeRepository.findByName(reviewDTO.getShopName()).orElseThrow(() -> new IllegalArgumentException("Store not found for the shop name: " + reviewDTO.getShopName())); - System.out.printf("Store Id : " + store.getId()); - // Review 엔티티 생성 및 저장 - Review savedReview = reviewService.createReview(Review.builder() - .user(user) - .store(store) - .paymentNum(reviewDTO.getPaymentNum()) - .contents(reviewDTO.getContents()) - .visible(true) - .likeCount(0L) - .build()); - Long reviewId = savedReview.getId(); - - MultipartFile[] imagesArray = uploadedImages.toArray(new MultipartFile[0]); - - List imageUrls = s3Uploader.upload(imagesArray, "reviews"); - List urls = imageUrls.stream().map(url -> url.split("\\[|\\]")[1]).toList(); -// String url = imageUrls.split("\\[|\\]")[1]; - - // surveyData를 합쳐서 ReviewFeedbackDTO 생성 - String combinedSurveyData = reviewDTO.getCombinedSurveyData(); - ReviewFeedbackDTO reviewFeedbackDTO = ReviewFeedbackDTO.builder() - .reviewId(reviewId) - .reviewFeedbackSelection(combinedSurveyData) - .build(); - - - urls.stream().map(url -> reviewImageRepository.save(ReviewImage.builder() - .review(savedReview) - .imagePath(url) - .build())); - - reviewFeedbackService.addReviewFeedback(reviewFeedbackDTO); - - return ResponseEntity.ok("설문이 성공적으로 제출되었습니다!"); + ResponseEntity result = reviewService.saveSurvey(reviewDTO, uploadedImages); + + return result; + } + + @GetMapping("/rank") + public ResponseEntity> getRankUser(@RequestParam String userEmail) { + List rankUserDTOS = userService.getRankUser(userEmail); + + return ResponseEntity.ok(rankUserDTOS); } + @GetMapping("/allUser") + public ResponseEntity> getAllUser(@RequestParam String userEmail) { + List rankUserDTOS = userService.getAllUser(userEmail); + + return ResponseEntity.ok(rankUserDTOS); + } + + @PostMapping("/addFollowUser") + public ResponseEntity getAllReviews(@RequestParam String userName, + @RequestParam String userEmail + ) { + System.out.printf("CReviewController getAllReviews userName : %s, userEmail : %s", userName, userEmail); + String followState = userService.addFollowUser(userName, userEmail); + + return ResponseEntity.ok("팔로우 성공"); + } } diff --git a/src/main/java/com/ssg/starroad/review/controller/ReviewLikeController.java b/src/main/java/com/ssg/starroad/review/controller/ReviewLikeController.java index 8bf2ead..cbce234 100644 --- a/src/main/java/com/ssg/starroad/review/controller/ReviewLikeController.java +++ b/src/main/java/com/ssg/starroad/review/controller/ReviewLikeController.java @@ -1,20 +1,34 @@ package com.ssg.starroad.review.controller; +import com.ssg.starroad.review.DTO.ReviewLikeDTO; import com.ssg.starroad.review.service.ReviewLikeService; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/review-likes") @RequiredArgsConstructor public class ReviewLikeController { - private final ReviewLikeService reviewLikeService; + private final UserRepository userRepository; + + @GetMapping("/{userEmail}") + public ResponseEntity> getUserLikes(@PathVariable String userEmail) { + User user = userRepository.findByEmail(userEmail) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + List likedReviewIds = reviewLikeService.getLikedReviewIdsByUserId(user.getId()); + return ResponseEntity.ok(likedReviewIds); + } + + @PostMapping("/{reviewId}/{userEmail}") + public ResponseEntity addLike(@PathVariable Long reviewId, @PathVariable String userEmail) { - @PostMapping - public ResponseEntity addLike(@RequestParam Long userId, @RequestParam Long reviewId) { - reviewLikeService.addLike(userId, reviewId); - return ResponseEntity.ok().build(); + ReviewLikeDTO response = reviewLikeService.addLike(userEmail, reviewId); + return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/ssg/starroad/review/entity/ReviewKeyword.java b/src/main/java/com/ssg/starroad/review/entity/ReviewKeyword.java index c363598..0ec78fc 100644 --- a/src/main/java/com/ssg/starroad/review/entity/ReviewKeyword.java +++ b/src/main/java/com/ssg/starroad/review/entity/ReviewKeyword.java @@ -1,5 +1,6 @@ package com.ssg.starroad.review.entity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -22,4 +23,8 @@ public class ReviewKeyword { private Long id; private String name; + + + @Column(name = "store_type") // 이 부분이 필요할 수 있습니다. + private String storeType; } diff --git a/src/main/java/com/ssg/starroad/review/entity/ReviewSentiment.java b/src/main/java/com/ssg/starroad/review/entity/ReviewSentiment.java index df742c5..55bb83e 100644 --- a/src/main/java/com/ssg/starroad/review/entity/ReviewSentiment.java +++ b/src/main/java/com/ssg/starroad/review/entity/ReviewSentiment.java @@ -25,8 +25,8 @@ public class ReviewSentiment { private Review review; private String content; - private int offset; - private int length; + private int totalOffset; + private int totalLength; @Enumerated(EnumType.STRING) private ConfidenceType confidence; diff --git a/src/main/java/com/ssg/starroad/review/repository/ReviewFollowRepository.java b/src/main/java/com/ssg/starroad/review/repository/ReviewFollowRepository.java index 31e2cd5..cd585da 100644 --- a/src/main/java/com/ssg/starroad/review/repository/ReviewFollowRepository.java +++ b/src/main/java/com/ssg/starroad/review/repository/ReviewFollowRepository.java @@ -1,6 +1,5 @@ package com.ssg.starroad.review.repository; -import com.ssg.starroad.review.entity.ReviewImage; import com.ssg.starroad.user.entity.Follow; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -12,4 +11,6 @@ public interface ReviewFollowRepository extends JpaRepository { @Query("SELECT f.toUser.id FROM Follow f WHERE f.fromUser.id = :fromUserId") List findToUserIdsByFromUserId(@Param("fromUserId") Long fromUserId); + + } diff --git a/src/main/java/com/ssg/starroad/review/repository/ReviewKeywordRepository.java b/src/main/java/com/ssg/starroad/review/repository/ReviewKeywordRepository.java index 5a46d0b..0aaf80b 100644 --- a/src/main/java/com/ssg/starroad/review/repository/ReviewKeywordRepository.java +++ b/src/main/java/com/ssg/starroad/review/repository/ReviewKeywordRepository.java @@ -2,6 +2,13 @@ import com.ssg.starroad.review.entity.ReviewKeyword; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface ReviewKeywordRepository extends JpaRepository { + @Query("SELECT rk FROM ReviewKeyword rk WHERE rk.storeType = :storeType") + List findAllByStoreType(@Param("storeType") String storeType); + } diff --git a/src/main/java/com/ssg/starroad/review/repository/ReviewLikeRepository.java b/src/main/java/com/ssg/starroad/review/repository/ReviewLikeRepository.java index 440afb6..ab12b44 100644 --- a/src/main/java/com/ssg/starroad/review/repository/ReviewLikeRepository.java +++ b/src/main/java/com/ssg/starroad/review/repository/ReviewLikeRepository.java @@ -4,11 +4,32 @@ import com.ssg.starroad.review.entity.ReviewLike; import com.ssg.starroad.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; import java.util.Optional; public interface ReviewLikeRepository extends JpaRepository { + // 주어진 userId와 reviewId로 ReviewLike 엔티티를 조회합니다. +// userId와 reviewId를 기준으로 ReviewLike를 찾습니다. 존재하지 않을 경우 Optional.empty()를 반환합니다. Optional findByUserIdAndReviewId(Long userId, Long reviewId); + + // 주어진 userId와 reviewId로 ReviewLike 엔티티를 삭제합니다. +// userId와 reviewId를 기준으로 ReviewLike를 삭제합니다. void deleteByUserIdAndReviewId(Long userId, Long reviewId); + // 주어진 userId로 사용자가 좋아요를 누른 리뷰 ID 목록을 조회합니다. +// JPQL 쿼리를 사용하여 userId를 기준으로 사용자가 좋아요를 누른 리뷰 ID 목록을 조회하여 반환합니다. + @Query("SELECT rl.review.id FROM ReviewLike rl WHERE rl.user.id = :userId") + List findLikedReviewIdsByUserId(@Param("userId") Long userId); + + // 주어진 User와 Review로 ReviewLike 존재 여부를 확인합니다. +// User와 Review를 기준으로 ReviewLike의 존재 여부를 확인하여 반환합니다. boolean existsByUserAndReview(User user, Review review); + + // 주어진 userId와 reviewId로 ReviewLike 존재 여부를 확인합니다. +// userId와 reviewId를 기준으로 ReviewLike의 존재 여부를 확인하여 반환합니다. + boolean existsByUser_IdAndReview_Id(Long userId, Long reviewId); + } diff --git a/src/main/java/com/ssg/starroad/review/repository/ReviewRepository.java b/src/main/java/com/ssg/starroad/review/repository/ReviewRepository.java index 416bb1f..242b661 100644 --- a/src/main/java/com/ssg/starroad/review/repository/ReviewRepository.java +++ b/src/main/java/com/ssg/starroad/review/repository/ReviewRepository.java @@ -39,4 +39,14 @@ public interface ReviewRepository extends JpaRepository { @Query("SELECT r FROM Review r WHERE r.user.id IN :userIds") Page findAllByUserIds(@Param("userIds") List userIds, Pageable pageable); + + @Query("SELECT r FROM Review r JOIN ReviewFeedback rf ON r.id = rf.review.id WHERE r.store.id = :storeId AND rf.reviewFeedbackSelection = :selection") + Page findAllWithPageByStoreIdAndReviewFeedbackSelection(@Param("storeId") Long storeId, @Param("selection") String selection, Pageable pageable); + // 리뷰 ID 목록을 기반으로 페이징하여 리뷰를 조회하는 메서드 추가 + Page findByIdIn(List ids, Pageable pageable); + + // 새 메소드 추가 + Page findByStoreIdAndContentsContaining(Long storeId, String keyword, Pageable pageable); + + } diff --git a/src/main/java/com/ssg/starroad/review/repository/ReviewSentimentRepository.java b/src/main/java/com/ssg/starroad/review/repository/ReviewSentimentRepository.java index 84be73a..3ebc883 100644 --- a/src/main/java/com/ssg/starroad/review/repository/ReviewSentimentRepository.java +++ b/src/main/java/com/ssg/starroad/review/repository/ReviewSentimentRepository.java @@ -2,6 +2,16 @@ import com.ssg.starroad.review.entity.ReviewSentiment; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface ReviewSentimentRepository extends JpaRepository { + + @Query("SELECT rs FROM ReviewSentiment rs WHERE rs.review.store.id = :storeId AND rs.content LIKE %:keyword%") + List findAllByStoreIdAndKeyword(@Param("storeId") Long storeId, @Param("keyword") String keyword); } + + + diff --git a/src/main/java/com/ssg/starroad/review/service/ReviewLikeService.java b/src/main/java/com/ssg/starroad/review/service/ReviewLikeService.java index c9900d9..6bd28bb 100644 --- a/src/main/java/com/ssg/starroad/review/service/ReviewLikeService.java +++ b/src/main/java/com/ssg/starroad/review/service/ReviewLikeService.java @@ -1,7 +1,12 @@ package com.ssg.starroad.review.service; +import com.ssg.starroad.review.DTO.ReviewLikeDTO; + +import java.util.List; + public interface ReviewLikeService { - void addLike(Long userId, Long reviewId); + ReviewLikeDTO addLike(String userEmail, Long reviewId); void removeLike(Long userId, Long reviewId); boolean existsByUserIdAndReviewId(Long userId, Long reviewId); + List getLikedReviewIdsByUserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/review/service/ReviewService.java b/src/main/java/com/ssg/starroad/review/service/ReviewService.java index 5d1119a..046fc9e 100644 --- a/src/main/java/com/ssg/starroad/review/service/ReviewService.java +++ b/src/main/java/com/ssg/starroad/review/service/ReviewService.java @@ -3,7 +3,6 @@ import com.ssg.starroad.review.DTO.ResponseReviewDTO; import com.ssg.starroad.review.DTO.ReviewDTO; import com.ssg.starroad.review.entity.Review; -import com.ssg.starroad.shop.DTO.StoreWithReviewDTO; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; @@ -13,7 +12,7 @@ public interface ReviewService { Long countReviewsByUserId(Long userId); - + Review createReview(Review review); Optional getReviewById(Long id); @@ -26,7 +25,13 @@ public interface ReviewService { ResponseEntity callOcrApi(MultipartFile imageFile) throws IOException; - ResponseReviewDTO findAllReview(int pageNo, int pageSize); + ResponseReviewDTO findAllReview(String userEmail, int pageNo, int pageSize); + + ResponseReviewDTO findFollowingReview(String userEmail, int pageNo, int pageSize); + + String makeSummary(String content); + + ResponseEntity saveSurvey(ReviewDTO reviewDTO, List uploadedImages); - ResponseReviewDTO findFollowingReview(Long id, int pageNo, int pageSize); + ResponseReviewDTO getUserReview(String email,int pageNo, int pageSize); } diff --git a/src/main/java/com/ssg/starroad/review/service/impl/ReviewFeedbackServiceImpl.java b/src/main/java/com/ssg/starroad/review/service/impl/ReviewFeedbackServiceImpl.java index b7fc865..7e42063 100644 --- a/src/main/java/com/ssg/starroad/review/service/impl/ReviewFeedbackServiceImpl.java +++ b/src/main/java/com/ssg/starroad/review/service/impl/ReviewFeedbackServiceImpl.java @@ -8,6 +8,8 @@ import com.ssg.starroad.review.repository.ReviewRepository; import com.ssg.starroad.review.service.ReviewFeedbackService; import com.ssg.starroad.review.service.ReviewSelectionService; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +24,7 @@ public class ReviewFeedbackServiceImpl implements ReviewFeedbackService { private final ReviewRepository reviewRepository; private final S3Uploader s3Uploader; private final ReviewSelectionService reviewSelectionService; + private final UserRepository userRepository; @Override public List getReviewFeedback(Long reviewId) { @@ -37,6 +40,20 @@ public List getReviewFeedback(Long reviewId) { @Transactional @Override public ReviewFeedbackDTO addReviewFeedback(ReviewFeedbackDTO reviewFeedbackDTO) { + System.out.println("addReviewFeedBack 진입"); + Long userId = reviewFeedbackDTO.getId(); + User user = userRepository.findById(userId).get(); + + Integer reviewExp = user.getReviewExp(); + Integer additionalPoint = reviewExp < 100 ? 100 : reviewExp < 200 ? 200 : 300; + System.out.println("addReviewFeedBack User : " + user.toString()); + // 기존 User 엔티티를 업데이트합니다. + user.setReviewExp(user.getReviewExp() + 10); + user.setPoint(user.getPoint() + additionalPoint); + + // 변경된 User 엔티티를 저장합니다. + userRepository.save(user); + // reviewFeedbackSelection을 ','를 기준으로 나눠 String 배열에 담는다. String[] selections = reviewFeedbackDTO.getReviewFeedbackSelection().split(","); System.out.printf("selections : " + selections); diff --git a/src/main/java/com/ssg/starroad/review/service/impl/ReviewLikeServiceImpl.java b/src/main/java/com/ssg/starroad/review/service/impl/ReviewLikeServiceImpl.java index d99de0e..f02a430 100644 --- a/src/main/java/com/ssg/starroad/review/service/impl/ReviewLikeServiceImpl.java +++ b/src/main/java/com/ssg/starroad/review/service/impl/ReviewLikeServiceImpl.java @@ -1,5 +1,6 @@ package com.ssg.starroad.review.service.impl; +import com.ssg.starroad.review.DTO.ReviewLikeDTO; import com.ssg.starroad.review.entity.Review; import com.ssg.starroad.review.entity.ReviewLike; import com.ssg.starroad.review.repository.ReviewLikeRepository; @@ -11,9 +12,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; - @Service @RequiredArgsConstructor public class ReviewLikeServiceImpl implements ReviewLikeService { @@ -22,18 +23,28 @@ public class ReviewLikeServiceImpl implements ReviewLikeService { private final UserRepository userRepository; private final ReviewRepository reviewRepository; + @Override + public List getLikedReviewIdsByUserId(Long userId) { + return reviewLikeRepository.findLikedReviewIdsByUserId(userId); + } + @Override @Transactional - public void addLike(Long userId, Long reviewId) { - User user = userRepository.findById(userId) + public ReviewLikeDTO addLike(String userEmail, Long reviewId) { + System.out.println("Function(addLike) - userEmail: " + userEmail + ", reviewId: " + reviewId); + User user = userRepository.findByEmail(userEmail) .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + Long userId = user.getId(); Review review = reviewRepository.findById(reviewId) .orElseThrow(() -> new IllegalArgumentException("리뷰를 찾을 수 없습니다.")); Optional reviewLikeOptional = reviewLikeRepository.findByUserIdAndReviewId(userId, reviewId); + boolean isLiked; if (reviewLikeOptional.isPresent()) { // 이미 좋아요가 존재하면 좋아요 취소 (삭제) 로직 reviewLikeRepository.delete(reviewLikeOptional.get()); + review.setLikeCount(review.getLikeCount() - 1); + isLiked = false; } else { // 좋아요가 존재하지 않으면 새로운 좋아요 추가 ReviewLike reviewLike = ReviewLike.builder() @@ -41,7 +52,14 @@ public void addLike(Long userId, Long reviewId) { .review(review) .build(); reviewLikeRepository.save(reviewLike); + review.setLikeCount(review.getLikeCount() + 1); + isLiked = true; } + + return ReviewLikeDTO.builder() + .isLiked(isLiked) + .likeCount(review.getLikeCount()) + .build(); } @Override diff --git a/src/main/java/com/ssg/starroad/review/service/impl/ReviewReceiptServiceImpl.java b/src/main/java/com/ssg/starroad/review/service/impl/ReviewReceiptServiceImpl.java index ac4ff32..1d3395b 100644 --- a/src/main/java/com/ssg/starroad/review/service/impl/ReviewReceiptServiceImpl.java +++ b/src/main/java/com/ssg/starroad/review/service/impl/ReviewReceiptServiceImpl.java @@ -6,6 +6,8 @@ import com.ssg.starroad.review.entity.ReviewReceipt; import com.ssg.starroad.review.repository.ReviewReceiptRepository; import com.ssg.starroad.review.service.ReviewReceiptService; +import com.ssg.starroad.shop.repository.StoreRepository; +import com.ssg.starroad.shop.service.StoreService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,6 +25,8 @@ public class ReviewReceiptServiceImpl implements ReviewReceiptService { private final ReviewReceiptRepository reviewReceiptRepository; + private final StoreRepository storeRepository; + private final StoreService storeService; @Override public ReviewReceiptDTO saveReviewReceipt(ReviewReceiptDTO reviewReceiptDTO) { @@ -46,8 +50,9 @@ public ReviewReceiptDTO processAndSaveOcrResult(String ocrResultJson) { JsonNode paymentInfoNode = rootNode.path("images").get(0).path("receipt").path("result").path("paymentInfo"); // 상점 이름 추출 -// String shopName = storeInfoNode.path("name").path("text").asText(); - String shopName = "노브랜드"; + String shopName = storeService.findStoreName(storeInfoNode.path("name").path("text").asText()); + storeRepository.findByName(shopName).orElse(null); +// String shopName = "노브랜드"; // 지점명 추출 - 필요하다면 String subName = storeInfoNode.path("subName").path("text").asText(); // 주소 추출 - 배열의 첫 번째 주소 추출 @@ -94,11 +99,12 @@ public ReviewReceiptDTO processAndSaveOcrResult(String ocrResultJson) { // savedReviewReceipt.getAddress(), // 주소 반환 - 필요한 경우 savedReviewReceipt.getPaymentType(), savedReviewReceipt.getApprovalNumber(), savedReviewReceipt.getPurchaseDate()); System.out.println("resultDTO : " + resultDTO.toString()); + return resultDTO; } catch (IOException e) { e.printStackTrace(); + // 예외 발생 시 적절한 예외를 던지거나 처리 + throw new RuntimeException("OCR 처리 중 오류 발생", e); } - - return resultDTO; } diff --git a/src/main/java/com/ssg/starroad/review/service/impl/ReviewSentimentServiceImpl.java b/src/main/java/com/ssg/starroad/review/service/impl/ReviewSentimentServiceImpl.java index 48dbf8d..e7ac345 100644 --- a/src/main/java/com/ssg/starroad/review/service/impl/ReviewSentimentServiceImpl.java +++ b/src/main/java/com/ssg/starroad/review/service/impl/ReviewSentimentServiceImpl.java @@ -2,17 +2,18 @@ import com.ssg.starroad.review.DTO.ReviewSentimentDTO; import com.ssg.starroad.review.entity.ReviewSentiment; -import com.ssg.starroad.review.enums.ConfidenceType; import com.ssg.starroad.review.repository.ReviewRepository; import com.ssg.starroad.review.repository.ReviewSentimentRepository; import com.ssg.starroad.review.service.ReviewSentimentService; import lombok.RequiredArgsConstructor; +import lombok.ToString; import org.springframework.stereotype.Service; import java.util.Optional; @Service @RequiredArgsConstructor +@ToString public class ReviewSentimentServiceImpl implements ReviewSentimentService { private final ReviewSentimentRepository reviewSentimentRepository; private final ReviewRepository reviewRepository; @@ -23,8 +24,8 @@ public ReviewSentimentDTO createReviewSentiment(ReviewSentimentDTO reviewSentime .review(reviewRepository.findById(reviewSentimentDTO.getReviewId()) .orElseThrow(() -> new IllegalArgumentException("Review not found"))) .content(reviewSentimentDTO.getContent()) - .offset(reviewSentimentDTO.getOffset()) - .length(reviewSentimentDTO.getLength()) + .totalOffset(reviewSentimentDTO.getTotalOffset()) + .totalLength(reviewSentimentDTO.getTotalLength()) .confidence(reviewSentimentDTO.getConfidence()) .highlightOffset(reviewSentimentDTO.getHighlightOffset()) .highlightLength(reviewSentimentDTO.getHighlightLength()) @@ -67,8 +68,8 @@ private ReviewSentimentDTO convertToDTO(ReviewSentiment reviewSentiment) { .id(reviewSentiment.getId()) .reviewId(reviewSentiment.getReview().getId()) .content(reviewSentiment.getContent()) - .offset(reviewSentiment.getOffset()) - .length(reviewSentiment.getLength()) + .totalOffset(reviewSentiment.getTotalOffset()) + .totalLength(reviewSentiment.getTotalLength()) .confidence(reviewSentiment.getConfidence()) .highlightOffset(reviewSentiment.getHighlightOffset()) .highlightLength(reviewSentiment.getHighlightLength()) diff --git a/src/main/java/com/ssg/starroad/review/service/impl/ReviewServiceImpl.java b/src/main/java/com/ssg/starroad/review/service/impl/ReviewServiceImpl.java index c1b6509..f84777a 100644 --- a/src/main/java/com/ssg/starroad/review/service/impl/ReviewServiceImpl.java +++ b/src/main/java/com/ssg/starroad/review/service/impl/ReviewServiceImpl.java @@ -1,17 +1,26 @@ package com.ssg.starroad.review.service.impl; -import com.ssg.starroad.review.DTO.ResponseReviewDTO; -import com.ssg.starroad.review.DTO.ReviewDTO; -import com.ssg.starroad.review.DTO.ReviewFeedbackDTO; -import com.ssg.starroad.review.DTO.ReviewImageDTO; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ssg.starroad.common.service.S3Uploader; +import com.ssg.starroad.common.service.clova.Sentiment; +import com.ssg.starroad.review.DTO.*; import com.ssg.starroad.review.entity.Review; +import com.ssg.starroad.review.entity.ReviewImage; import com.ssg.starroad.review.repository.ReviewFollowRepository; +import com.ssg.starroad.review.repository.ReviewImageRepository; +import com.ssg.starroad.review.repository.ReviewLikeRepository; import com.ssg.starroad.review.repository.ReviewRepository; import com.ssg.starroad.review.service.ReviewFeedbackService; import com.ssg.starroad.review.service.ReviewImageService; +import com.ssg.starroad.review.service.ReviewSentimentService; import com.ssg.starroad.review.service.ReviewService; +import com.ssg.starroad.shop.entity.Store; +import com.ssg.starroad.shop.repository.StoreRepository; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.repository.UserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -36,13 +45,31 @@ public class ReviewServiceImpl implements ReviewService { private final ReviewRepository reviewRepository; private final ReviewFollowRepository ReviewFollowRepository; private final RestTemplate restTemplate; - private final ReviewImageService reviewImageService; // ReviewImageService 인젝션 + private final ReviewImageService reviewImageService; private final ReviewFeedbackService reviewFeedbackService; private final ReviewFollowRepository reviewFollowRepository; + private final UserRepository userRepository; + private final ReviewLikeRepository reviewLikeRepository; + private final ReviewImageRepository reviewImageRepository; + private final StoreRepository storeRepository; + private final S3Uploader s3Uploader; + private final Sentiment sentiment; + private final ReviewSentimentService reviewSentimentService; + + + @Value("${naver.openapi.ocr.secret-key}") + private String secretKey; + + @Value("${naver.openapi.summary.key}") + private String summaryKey; + + @Value("${naver.openapi.summary.key-id}") + private String summaryKeyID; + @Override public Long countReviewsByUserId(Long userId) { - return reviewRepository.countByUserId(userId); + return reviewRepository.countByUserId(userId); } @Override @@ -95,7 +122,7 @@ public ResponseEntity callOcrApi(MultipartFile imageFile) throws IOExcep // OCR API에 요청을 보낼 때 필요한 headers 설정 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("X-OCR-SECRET", "b1NCaWFVeEpKb0lIbGF3c0xKaHlZS0FnY0tudERDZHQ="); + headers.set("X-OCR-SECRET", secretKey); // 이미지 파일을 Base64 인코딩 String base64Image = Base64.getEncoder().encodeToString(imageFile.getBytes()); @@ -128,12 +155,12 @@ public ResponseEntity callOcrApi(MultipartFile imageFile) throws IOExcep } @Override - public ResponseReviewDTO findAllReview(int pageNo, int pageSize) { + public ResponseReviewDTO findAllReview(String userEmail, int pageNo, int pageSize) { // 최신순으로 정렬된 Pageable 객체 생성 - Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(Sort.Direction.DESC, "createdAt")); + Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(Sort.Direction.DESC, "likeCount")); // 정렬된 Pageable 객체를 사용하여 리뷰 페이지 가져오기 Page reviewPage = reviewRepository.findAll(pageable); - + Long userId = userRepository.findByEmail(userEmail).orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")).getId(); // Long totalReviewCount = reviewRepository.countByStoreId(id); List reviewDTOList = reviewPage.stream() @@ -141,6 +168,7 @@ public ResponseReviewDTO findAllReview(int pageNo, int pageSize) { List reviewImageDTOs = reviewImageService.getReviewImages(review.getId()); List reviewFeedbackDTOs = reviewFeedbackService.getReviewFeedback(review.getId()); Long userReviewCount = countReviewsByUserId(review.getUser().getId()); + boolean isLiked = reviewLikeRepository.existsByUser_IdAndReview_Id(userId, review.getId()); return ReviewDTO.builder() .id(review.getId()) @@ -157,9 +185,14 @@ public ResponseReviewDTO findAllReview(int pageNo, int pageSize) { .reviewcount(userReviewCount) .reviewImages(reviewImageDTOs) .reviewFeedbacks(reviewFeedbackDTOs) + .store(review.getStore()) .build(); }) .collect(Collectors.toList()); + List sortedList = reviewDTOList.stream() + .sorted(Comparator.comparingLong(ReviewDTO::getLikeCount).reversed()) + .collect(Collectors.toList()); + return ResponseReviewDTO.builder() .reviews(reviewDTOList) .pageNumber(reviewPage.getNumber()) @@ -169,17 +202,186 @@ public ResponseReviewDTO findAllReview(int pageNo, int pageSize) { } @Override - public ResponseReviewDTO findFollowingReview(Long id, int pageNo, int pageSize) { + public ResponseReviewDTO findFollowingReview(String userEmail, int pageNo, int pageSize) { // 최신순으로 정렬된 Pageable 객체 생성 Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(Sort.Direction.DESC, "createdAt")); // 정렬된 Pageable 객체를 사용하여 리뷰 페이지 가져오기 - List toUserIds = reviewFollowRepository.findToUserIdsByFromUserId(id); + // 사용자 이메일을 통해 사용자 ID를 조회합니다. + Long userId = userRepository.findByEmail(userEmail).orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")).getId(); + + List toUserIds = reviewFollowRepository.findToUserIdsByFromUserId(userId); Page reviewPage = reviewRepository.findAllByUserIds(toUserIds, pageable); + // Page reviewPage = reviewFollowRepository.findToUserIdsByFromUserId(id).stream().map(followId -> reviewRepository.findAllWithPageByUserId(followId, pageable)).toList().stream().findFirst().orElse(null); + List reviewDTOList = reviewPage.stream() + .map(review -> { + List reviewImageDTOs = reviewImageService.getReviewImages(review.getId()); + List reviewFeedbackDTOs = reviewFeedbackService.getReviewFeedback(review.getId()); + Long userReviewCount = countReviewsByUserId(review.getUser().getId()); + boolean isLiked = reviewLikeRepository.existsByUser_IdAndReview_Id(userId, review.getId()); + return ReviewDTO.builder() + .id(review.getId()) + .userId(review.getUser().getId()) + .userNickname(review.getUser().getNickname()) + .imagePath(review.getUser().getImagePath()) + .storeId(review.getStore().getId()) + .visible(review.isVisible()) + .createDate(review.getCreatedAt()) + .likeCount(review.getLikeCount()) + .contents(review.getContents()) + .summary(review.getSummary()) + .confidence(review.getConfidence()) + .reviewcount(userReviewCount) + .reviewImages(reviewImageDTOs) + .reviewFeedbacks(reviewFeedbackDTOs) + .isLiked(isLiked) + .store(review.getStore()) + .build(); + }) + .collect(Collectors.toList()); + return ResponseReviewDTO.builder() + .reviews(reviewDTOList) + .pageNumber(reviewPage.getNumber()) + .pageSize(reviewPage.getSize()) + .hasNext(reviewPage.hasNext()) + .build(); + } + + @Override + public String makeSummary(String content) { + System.out.println("makeSummary 진입"); + // OCR API 엔드포인트 + String summaryApiUrl = "https://naveropenapi.apigw.ntruss.com/text-summary/v1/summarize"; + + // OCR API에 요청을 보낼 때 필요한 headers 설정 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("X-NCP-APIGW-API-KEY-ID", summaryKeyID); + headers.set("X-NCP-APIGW-API-KEY", summaryKey); + + // 요청 바디 생성 + Map body = new HashMap<>(); + body.put("document", Map.of( + "title", "", + "content", content + )); + body.put("option", Map.of( + "language", "ko", + "model", "general", + "tone", 3, + "summaryCount", 3 + )); + + HttpEntity> entity = new HttpEntity<>(body, headers); + System.out.println("before makeSummary : " + entity); + // 텍스트 요약 API에 POST 요청 보내기 + ResponseEntity response = restTemplate.postForEntity(summaryApiUrl, entity, String.class); + System.out.println("makeSummary : " + response.getBody()); + + // JSON 응답 문자열 + ObjectMapper objectMapper = new ObjectMapper(); + String summary = null; + try { + // JSON 문자열을 JsonNode로 변환 + JsonNode rootNode = objectMapper.readTree(response.getBody()); + + // summary 값 추출 + JsonNode summaryNode = rootNode.path("summary"); + summary = summaryNode.asText(); + } catch (Exception e) { + e.printStackTrace(); + } + + // "summary" 키에 해당하는 값을 가져옴 + return summary; + } + + @Override + public ResponseEntity saveSurvey(ReviewDTO reviewDTO, List uploadedImages) { + System.out.println("reviewSelectionDTO toString : " + reviewDTO.toString()); + + User user = userRepository.findByEmail(reviewDTO.getUserEmail()).orElseThrow(() -> new IllegalArgumentException("User not found for the userEmail: " + reviewDTO.getUserEmail())); + System.out.println("sumbitSurvey user : " + user.toString()); + // Store 엔티티가 아직 저장되지 않은 경우 저장 + System.out.println("reviewDTO getShopName : " + reviewDTO.getShopName()); + Store store = storeRepository.findByName(reviewDTO.getShopName()).orElseThrow(() -> new IllegalArgumentException("Store not found for the shop name: " + reviewDTO.getShopName())); + System.out.printf("Store Id : " + store.getId()); + + String summary = makeSummary(reviewDTO.getContents()); + + SentimentDTO dto = sentiment.getSentiment(reviewDTO.getContents()); + + // Review 엔티티 생성 및 저장 + Review savedReview = createReview(Review.builder() + .user(user) + .store(store) + .confidence(dto.getDocumentConfidence()) + .paymentNum(reviewDTO.getPaymentNum()) + .contents(reviewDTO.getContents()) + .summary(summary) + .visible(true) + .likeCount(0L) + .build()); + + List sentimentDTOList = dto.getSentimentDetailDTOList().stream().map(x -> ReviewSentimentDTO.builder() + .reviewId(savedReview.getId()) + .content(x.getContent()) + .totalOffset(x.getOffset()) + .totalLength(x.getLength()) + .confidence(x.getConfidence()) + .highlightOffset(x.getHighlightOffset()) + .highlightLength(x.getHighlightLength()) + .build()).toList(); + + List reviewSentimentDTOList = sentimentDTOList.stream().map(reviewSentimentDTO -> { + System.out.println("reviewSentimentDTO : " + reviewSentimentDTO.toString()); + return reviewSentimentService.createReviewSentiment(reviewSentimentDTO); + }).toList(); + + System.out.println("리뷰 생성 완료"); + Long reviewId = savedReview.getId(); + System.out.println("submitSurvey reviewId : " + reviewId); + + List imageUrls = new ArrayList<>(); + if (uploadedImages != null && !uploadedImages.isEmpty()) { + imageUrls = s3Uploader.upload(uploadedImages.toArray(new MultipartFile[uploadedImages.size()]), "ssg/reviews"); + } + + System.out.println("================================================================"); + for (String imageUrl : imageUrls) { + System.out.println(imageUrl); + } + + // surveyData를 합쳐서 ReviewFeedbackDTO 생성 + String combinedSurveyData = reviewDTO.getCombinedSurveyData(); + ReviewFeedbackDTO reviewFeedbackDTO = ReviewFeedbackDTO.builder() + .id(user.getId()) + .reviewId(reviewId) + .reviewFeedbackSelection(combinedSurveyData) + .build(); + + System.out.println("submitSurvey reviewFeedbackDTO ID : " + reviewFeedbackDTO.getId()); + + imageUrls.forEach(url -> reviewImageRepository.save(ReviewImage.builder() + .review(savedReview) + .imagePath(url) + .build())); + + reviewFeedbackService.addReviewFeedback(reviewFeedbackDTO); + + return ResponseEntity.ok("설문이 성공적으로 제출되었습니다!"); + } + + public ResponseReviewDTO getUserReview(String email, int pageNo, int pageSize) { + + Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(Sort.Direction.DESC, "createdAt")); + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); + Page reviewPage = reviewRepository.findAllByUserIds(Collections.singletonList(userId), pageable); + List reviewDTOList = reviewPage.stream() .map(review -> { List reviewImageDTOs = reviewImageService.getReviewImages(review.getId()); @@ -210,5 +412,6 @@ public ResponseReviewDTO findFollowingReview(Long id, int pageNo, int pageSize) .pageSize(reviewPage.getSize()) .hasNext(reviewPage.hasNext()) .build(); + } } diff --git a/src/main/java/com/ssg/starroad/reward/DTO/RewardHistoryDTO.java b/src/main/java/com/ssg/starroad/reward/DTO/RewardHistoryDTO.java index af241ac..d630d9e 100644 --- a/src/main/java/com/ssg/starroad/reward/DTO/RewardHistoryDTO.java +++ b/src/main/java/com/ssg/starroad/reward/DTO/RewardHistoryDTO.java @@ -22,4 +22,6 @@ public class RewardHistoryDTO { @JsonProperty("reward_count") private Long count; + @JsonProperty("reward_image_path") + private String rewardImagePath; } \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/reward/DTO/RewardMemberDTO.java b/src/main/java/com/ssg/starroad/reward/DTO/RewardMemberDTO.java index c7c4e37..c733d0f 100644 --- a/src/main/java/com/ssg/starroad/reward/DTO/RewardMemberDTO.java +++ b/src/main/java/com/ssg/starroad/reward/DTO/RewardMemberDTO.java @@ -9,8 +9,8 @@ @AllArgsConstructor @ToString public class RewardMemberDTO { - @JsonProperty("member_id") - private Long memberId; + @JsonProperty("member_email") + private String email; @JsonProperty("reward_id") private Long rewardId; diff --git a/src/main/java/com/ssg/starroad/reward/controller/RewardController.java b/src/main/java/com/ssg/starroad/reward/controller/RewardController.java index 26dc7da..1c5dc21 100644 --- a/src/main/java/com/ssg/starroad/reward/controller/RewardController.java +++ b/src/main/java/com/ssg/starroad/reward/controller/RewardController.java @@ -29,6 +29,6 @@ public ResponseEntity RewardList (){ @PostMapping("/obtain") public ResponseEntity Rewardobtain(@RequestBody RewardMemberDTO rewardmemberDTO){ rewardHistoryService.addReward(rewardmemberDTO); - return ResponseEntity.ok("Reward issued successfully to user ID " + rewardmemberDTO.getMemberId()); + return ResponseEntity.ok("Reward issued successfully to user email " + rewardmemberDTO.getEmail()); } } diff --git a/src/main/java/com/ssg/starroad/reward/controller/RewardHistoryController.java b/src/main/java/com/ssg/starroad/reward/controller/RewardHistoryController.java index be399f2..aeec028 100644 --- a/src/main/java/com/ssg/starroad/reward/controller/RewardHistoryController.java +++ b/src/main/java/com/ssg/starroad/reward/controller/RewardHistoryController.java @@ -19,9 +19,9 @@ public class RewardHistoryController { private final RewardHistoryService rewardHistoryService; - @GetMapping("/user/{userId}/list") - public ResponseEntity> getRewardHistory(@PathVariable("userId") Long userId) { - List rewardHistoryDTOList = rewardHistoryService.getRewardHistory(userId); + @GetMapping("/user/{email}/list") + public ResponseEntity> getRewardHistory(@PathVariable("email") String email) { + List rewardHistoryDTOList = rewardHistoryService.getRewardHistory(email); if (rewardHistoryDTOList.isEmpty()) { return ResponseEntity.noContent().build(); // 내용이 없을 경우 No Content (204) 반환 } diff --git a/src/main/java/com/ssg/starroad/reward/controller/RewardProcessController.java b/src/main/java/com/ssg/starroad/reward/controller/RewardProcessController.java index 0427c70..d7385a9 100644 --- a/src/main/java/com/ssg/starroad/reward/controller/RewardProcessController.java +++ b/src/main/java/com/ssg/starroad/reward/controller/RewardProcessController.java @@ -15,10 +15,10 @@ public class RewardProcessController { private final RewardProcessService rewardProcessService; - @GetMapping("/{userId}/start") - public ResponseEntity startRewardProcess(@PathVariable Long userId) { + @GetMapping("/{email}/start") + public ResponseEntity startRewardProcess(@PathVariable String email) { // 여기서 userId와 DTO를 이용하여 처리 로직 호출 - rewardProcessService.startRewardProcess(userId); + rewardProcessService.startRewardProcess(email); return ResponseEntity.ok().build(); } @@ -32,38 +32,38 @@ public ResponseEntity updateCouponCount(@PathVariable Long userId) { } } - @PatchMapping("/{userId}/review") - public ResponseEntity updateReviewCount(@PathVariable Long userId) { + @PatchMapping("/{email}/review") + public ResponseEntity updateReviewCount(@PathVariable String email) { try { - rewardProcessService.updateReviewCount(userId); + rewardProcessService.updateReviewCount(email); return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.badRequest().body("Error updating review count: " + e.getMessage()); } } - @PatchMapping("/{userId}/completed") - public ResponseEntity updateStatus(@PathVariable Long userId) { + @PatchMapping("/{email}/completed") + public ResponseEntity updateStatus(@PathVariable String email) { try { - rewardProcessService.updateStatus(userId); + rewardProcessService.updateStatus(email); return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.badRequest().body("Error updating status: " + e.getMessage()); } } - @PatchMapping("/{userId}/new") - public ResponseEntity resetStatus(@PathVariable Long userId) { + @PatchMapping("/{email}/new") + public ResponseEntity resetStatus(@PathVariable String email) { try { - rewardProcessService.resetStatus(userId); + rewardProcessService.resetStatus(email); return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.badRequest().body("Error updating status: " + e.getMessage()); } } - @GetMapping("/{userId}/get") - public ResponseEntity getProcess(@PathVariable("userId") Long userId){ - RewardProcessDTO rewardProcessDTO=rewardProcessService.getProcess(userId); + @GetMapping("/{email}/get") + public ResponseEntity getProcess(@PathVariable("email") String email){ + RewardProcessDTO rewardProcessDTO=rewardProcessService.getProcess(email); if (rewardProcessDTO==null) { return ResponseEntity.noContent().build(); } diff --git a/src/main/java/com/ssg/starroad/reward/entity/Reward.java b/src/main/java/com/ssg/starroad/reward/entity/Reward.java index e7dfbed..0026bc3 100644 --- a/src/main/java/com/ssg/starroad/reward/entity/Reward.java +++ b/src/main/java/com/ssg/starroad/reward/entity/Reward.java @@ -23,4 +23,6 @@ public class Reward extends BaseTimeEntity { private Long id; private String name; + + private String rewardImagePath; } diff --git a/src/main/java/com/ssg/starroad/reward/repository/impl/RewardHistoryRepositoryCustom.java b/src/main/java/com/ssg/starroad/reward/repository/impl/RewardHistoryRepositoryCustom.java index 8c92147..f36277c 100644 --- a/src/main/java/com/ssg/starroad/reward/repository/impl/RewardHistoryRepositoryCustom.java +++ b/src/main/java/com/ssg/starroad/reward/repository/impl/RewardHistoryRepositoryCustom.java @@ -21,7 +21,7 @@ public List countRewardHistoryByUserId(Long userId) { QReward reward = QReward.reward; return queryFactory - .select(rewardHistory.rewardId, reward.name, rewardHistory.id.count()) + .select(rewardHistory.rewardId, reward.name ,rewardHistory.id.count(),reward.rewardImagePath) .from(rewardHistory) .leftJoin(reward).on(rewardHistory.rewardId.eq(reward.id)) // 명확한 조인 조건 사용 .where(rewardHistory.user.id.eq(userId)) diff --git a/src/main/java/com/ssg/starroad/reward/service/RewardHistoryService.java b/src/main/java/com/ssg/starroad/reward/service/RewardHistoryService.java index 2b386d5..ed64b86 100644 --- a/src/main/java/com/ssg/starroad/reward/service/RewardHistoryService.java +++ b/src/main/java/com/ssg/starroad/reward/service/RewardHistoryService.java @@ -7,5 +7,5 @@ public interface RewardHistoryService { public void addReward(RewardMemberDTO rewardmemberDTO); - public List getRewardHistory(Long userId); + public List getRewardHistory(String email); } diff --git a/src/main/java/com/ssg/starroad/reward/service/RewardProcessService.java b/src/main/java/com/ssg/starroad/reward/service/RewardProcessService.java index d3276bc..2b10877 100644 --- a/src/main/java/com/ssg/starroad/reward/service/RewardProcessService.java +++ b/src/main/java/com/ssg/starroad/reward/service/RewardProcessService.java @@ -3,16 +3,16 @@ import com.ssg.starroad.reward.DTO.RewardProcessDTO; public interface RewardProcessService { - public void startRewardProcess(Long userId); + public void startRewardProcess(String email); public void updateCouponCount(Long userId); - public void updateReviewCount(Long userId); + public void updateReviewCount(String email); - public void updateStatus(Long userId); - public void resetStatus(Long userId); + public void updateStatus(String email); + public void resetStatus(String email); - public RewardProcessDTO getProcess(Long userId); + public RewardProcessDTO getProcess(String email); } diff --git a/src/main/java/com/ssg/starroad/reward/service/impl/RewardHistoryServiceImpl.java b/src/main/java/com/ssg/starroad/reward/service/impl/RewardHistoryServiceImpl.java index bb9ea46..b46d6af 100644 --- a/src/main/java/com/ssg/starroad/reward/service/impl/RewardHistoryServiceImpl.java +++ b/src/main/java/com/ssg/starroad/reward/service/impl/RewardHistoryServiceImpl.java @@ -26,7 +26,7 @@ public class RewardHistoryServiceImpl implements RewardHistoryService { @Override public void addReward(RewardMemberDTO rewardmemberDTO) { - Optional user=userRepository.findById(rewardmemberDTO.getMemberId()); + Optional user=userRepository.findByEmail(rewardmemberDTO.getEmail()); user.ifPresent(value -> rewardHistoryRepository.save(RewardHistory.builder() .rewardId(rewardmemberDTO.getRewardId()) @@ -34,7 +34,8 @@ public void addReward(RewardMemberDTO rewardmemberDTO) { } @Override - public List getRewardHistory(Long userId) { + public List getRewardHistory(String email) { + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); List rewardCounts = rewardHistoryRepositoryCustom.countRewardHistoryByUserId(userId); List result = new ArrayList<>(); @@ -43,6 +44,7 @@ public List getRewardHistory(Long userId) { .rewardId(record.get(0, Long.class)) .rewardName(record.get(1, String.class)) .count(Objects.requireNonNull(record.get(2, Long.class))) + .rewardImagePath(record.get(3,String.class)) .build(); result.add(dto); } diff --git a/src/main/java/com/ssg/starroad/reward/service/impl/RewardProcessServiceImpl.java b/src/main/java/com/ssg/starroad/reward/service/impl/RewardProcessServiceImpl.java index caf4c4a..7edfd97 100644 --- a/src/main/java/com/ssg/starroad/reward/service/impl/RewardProcessServiceImpl.java +++ b/src/main/java/com/ssg/starroad/reward/service/impl/RewardProcessServiceImpl.java @@ -4,12 +4,15 @@ import com.ssg.starroad.reward.entity.RewardProcess; import com.ssg.starroad.reward.repository.RewardProcessRepository; import com.ssg.starroad.reward.service.RewardProcessService; +import com.ssg.starroad.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.DayOfWeek; import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; import java.util.Objects; import java.util.Optional; @@ -18,18 +21,16 @@ public class RewardProcessServiceImpl implements RewardProcessService { private final RewardProcessRepository rewardProcessRepository; + private final UserRepository userRepository; private final ModelMapper modelMapper; @Override @Transactional - public void startRewardProcess(Long userId) { + public void startRewardProcess(String email) { + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); RewardProcess rewardProcess = rewardProcessRepository.findById(userId) - .orElse(new RewardProcess(userId)); // 존재하지 않는 경우 새 객체 생성 - - rewardProcess.setRewardStatus(false); // 기존 로직대로 상태 설정 - rewardProcess.setExpiredAt(LocalDate.now().plusDays(7)); - rewardProcess.setReviewCount(0); - rewardProcess.setCouponCount(0); + .orElseThrow(); // 존재하지 않는 경우 새 객체 생성 + rewardProcess.setCouponCount(rewardProcess.getCouponCount()+1); rewardProcessRepository.save(rewardProcess); // 저장 } @@ -44,7 +45,8 @@ public void updateCouponCount(Long userId) { @Override @Transactional - public void updateReviewCount(Long userId) { + public void updateReviewCount(String email) { + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); RewardProcess rewardProcess = rewardProcessRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("Invalid user ID: " + userId)); rewardProcess.setReviewCount(rewardProcess.getReviewCount() + 1); @@ -53,7 +55,8 @@ public void updateReviewCount(Long userId) { @Override @Transactional - public void updateStatus(Long userId) { + public void updateStatus(String email) { + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); RewardProcess rewardProcess = rewardProcessRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("Invalid user ID: " + userId)); rewardProcess.setRewardStatus(true); @@ -61,7 +64,8 @@ public void updateStatus(Long userId) { } @Override - public void resetStatus(Long userId) { + public void resetStatus(String email) { + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); RewardProcess rewardProcess = rewardProcessRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("Invalid user ID: " + userId)); rewardProcess.setIssueStatus(false); @@ -71,9 +75,21 @@ public void resetStatus(Long userId) { @Override @Transactional - public RewardProcessDTO getProcess(Long userId){ - RewardProcess rewardProcess = rewardProcessRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("Invalid user ID: " + userId)); + public RewardProcessDTO getProcess(String email){ + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); + RewardProcess rewardProcess = rewardProcessRepository.findById(userId).orElseGet(() -> { + // RewardProcess가 존재하지 않을 경우 새로운 보상 프로세스를 생성합니다. + RewardProcess newRewardProcess = new RewardProcess(); + newRewardProcess.setUserId(userId); + newRewardProcess.setCouponCount(0); + newRewardProcess.setReviewCount(0); + newRewardProcess.setIssueStatus(false); + newRewardProcess.setUsageStatus(false); + newRewardProcess.setRewardStatus(false); + newRewardProcess.setExpiredAt(LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY))); // 예시로 만료일을 7일 후로 설정 + return rewardProcessRepository.save(newRewardProcess); + }); + if(Objects.equals(rewardProcess.getExpiredAt(), LocalDate.now())) { rewardProcessRepository.delete(rewardProcess); diff --git a/src/main/java/com/ssg/starroad/shop/DTO/StoreWithReviewDTO.java b/src/main/java/com/ssg/starroad/shop/DTO/StoreWithReviewDTO.java index 11fba61..0c09bbf 100644 --- a/src/main/java/com/ssg/starroad/shop/DTO/StoreWithReviewDTO.java +++ b/src/main/java/com/ssg/starroad/shop/DTO/StoreWithReviewDTO.java @@ -35,7 +35,7 @@ public class StoreWithReviewDTO { private String imagePath; - + private Long userId; // 로그인을 한 유저에 대한 필드 private String storeType; //프로그래스바 관련필드 private Long revisitCount; @@ -46,4 +46,6 @@ public class StoreWithReviewDTO { private int pageNumber; // 현재 페이지 번호 private int pageSize; // 페이지 크기 private boolean hasNext; // 다음 페이지 존재 여부 + + } diff --git a/src/main/java/com/ssg/starroad/shop/controller/StoreController.java b/src/main/java/com/ssg/starroad/shop/controller/StoreController.java index 192343f..e2b063d 100644 --- a/src/main/java/com/ssg/starroad/shop/controller/StoreController.java +++ b/src/main/java/com/ssg/starroad/shop/controller/StoreController.java @@ -1,5 +1,6 @@ package com.ssg.starroad.shop.controller; +import com.ssg.starroad.review.entity.ReviewKeyword; import com.ssg.starroad.shop.DTO.StoreDTO; import com.ssg.starroad.shop.DTO.StoreWithReviewDTO; import com.ssg.starroad.shop.service.StoreService; @@ -19,6 +20,21 @@ public class StoreController { private static final Logger log = LoggerFactory.getLogger(StoreController.class); private final StoreService storeService; + + + @GetMapping("/{id}/keywords") + public ResponseEntity> getStoreKeywords(@PathVariable Long id) { + try { + List keywords = storeService.getKeywordsByStoreCategory(id); + return ResponseEntity.ok(keywords); + } catch (RuntimeException e) { + log.error("Failed to fetch keywords for store ID: {}", id, e); + return ResponseEntity.notFound().build(); + } + } + + + // 메인 매장 목록을 검색하는 요청을 처리하는 메소드입니다. @GetMapping("/main") public ResponseEntity> StoreSearch() { @@ -32,30 +48,33 @@ public ResponseEntity> StoreSearch() { @GetMapping("/{id}/reviews") public ResponseEntity StoreGetWithReviewById( @PathVariable Long id, + @RequestParam String userEmail, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String filter, + @RequestParam(required = false) String sort, + @RequestParam(required = false) String keyword) { try { - StoreWithReviewDTO storeWithReviewDTO = storeService.findStoreWithReview(id, page, size); + log.info("Received request for store ID: {}, userEmail: {}, page: {}, size: {}, filter: {}, sort: {}, keyword: {}", id, userEmail, page, size, filter, sort, keyword); + StoreWithReviewDTO storeWithReviewDTO = storeService.findStoreWithReview(id, userEmail, page, size, filter, sort,keyword); return ResponseEntity.ok(storeWithReviewDTO); } catch (RuntimeException e) { + log.error("리뷰가 있는 상점을 가져오는데 실패했습니다", e); return ResponseEntity.notFound().build(); } } - // 특정 매장의 가이드 맵을 조회하는 요청을 처리하는 메소드입니다. + + @GetMapping("/{id}/guidemap") public ResponseEntity StoreGetGuidemapById(@PathVariable Long id) { - try { - // 매장 서비스를 통해 특정 매장의 가이드 맵을 조회합니다. + log.info("Fetching guide map for store ID: {}", id); StoreDTO storeDTO = storeService.findStore(id); - // 조회된 가이드 맵을 응답으로 반환합니다. - - - log.info(storeDTO.toString()); + log.info("StoreDTO: {}", storeDTO); return ResponseEntity.ok(storeDTO); } catch (RuntimeException e) { - // 조회 중에 예외가 발생한 경우 404 응답을 반환합니다. + log.error("Failed to fetch guide map for store ID: {}", id, e); return ResponseEntity.notFound().build(); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/shop/repository/StoreRepository.java b/src/main/java/com/ssg/starroad/shop/repository/StoreRepository.java index 6438936..a114e69 100644 --- a/src/main/java/com/ssg/starroad/shop/repository/StoreRepository.java +++ b/src/main/java/com/ssg/starroad/shop/repository/StoreRepository.java @@ -13,10 +13,12 @@ public interface StoreRepository extends JpaRepository { // complexShoppingmallId에 해당하는 Store 목록을 조회하는 메서드 // Optional>: 결과가 없을 경우 빈 Optional을 반환하고, 결과가 있을 경우 Store의 리스트를 Optional로 감싸서 반환 Optional> findByComplexShoppingmallId(Long id); + // store 정보를 id를 기반으로 조회해서 가져오는 메서드 +// 주어진 id를 기준으로 Store 엔티티를 조회하여 반환합니다. 존재하지 않을 경우 Optional.empty()를 반환합니다. Optional findById(Long id); + Optional findByName(String name); // shopName을 기준으로 storeType을 조회하는 새로운 메소드 @Query("SELECT s.storeType FROM Store s WHERE s.name = :name") String findStoreTypeByShopName(@Param("name") String name); - Optional findByName(String name); } \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/shop/service/StoreService.java b/src/main/java/com/ssg/starroad/shop/service/StoreService.java index 76ce745..a2723ba 100644 --- a/src/main/java/com/ssg/starroad/shop/service/StoreService.java +++ b/src/main/java/com/ssg/starroad/shop/service/StoreService.java @@ -1,16 +1,22 @@ package com.ssg.starroad.shop.service; +import com.ssg.starroad.review.entity.ReviewKeyword; import com.ssg.starroad.shop.DTO.StoreDTO; import com.ssg.starroad.shop.DTO.StoreWithReviewDTO; -import com.ssg.starroad.shop.entity.ComplexShoppingmall; -import com.ssg.starroad.shop.entity.Store; import java.util.List; public interface StoreService { - List searchStoreList(Long id); - StoreWithReviewDTO findStoreWithReview(Long id, int pageNo,int pageSize); - StoreDTO findStore(Long id); - String findStoreTypeByName(String name); + List getKeywordsByStoreCategory(Long storeId); + + List searchStoreList(Long id); + + StoreWithReviewDTO findStoreWithReview(Long storeId, String userEmail, int pageNo, int pageSize, String filter, String sort, String keyword); + + StoreDTO findStore(Long id); + + String findStoreTypeByName(String name); + + String findStoreName(String name); } diff --git a/src/main/java/com/ssg/starroad/shop/service/impl/StoreServiceImpl.java b/src/main/java/com/ssg/starroad/shop/service/impl/StoreServiceImpl.java index d6f720b..7f7649e 100644 --- a/src/main/java/com/ssg/starroad/shop/service/impl/StoreServiceImpl.java +++ b/src/main/java/com/ssg/starroad/shop/service/impl/StoreServiceImpl.java @@ -4,8 +4,9 @@ import com.ssg.starroad.review.DTO.ReviewFeedbackDTO; import com.ssg.starroad.review.DTO.ReviewImageDTO; import com.ssg.starroad.review.entity.Review; -import com.ssg.starroad.review.repository.ReviewImageRepository; -import com.ssg.starroad.review.repository.ReviewRepository; +import com.ssg.starroad.review.entity.ReviewKeyword; +import com.ssg.starroad.review.entity.ReviewSentiment; +import com.ssg.starroad.review.repository.*; import com.ssg.starroad.review.service.ReviewFeedbackService; import com.ssg.starroad.review.service.ReviewImageService; import com.ssg.starroad.review.service.ReviewService; @@ -14,13 +15,15 @@ import com.ssg.starroad.shop.entity.Store; import com.ssg.starroad.shop.repository.StoreRepository; import com.ssg.starroad.shop.service.StoreService; +import com.ssg.starroad.user.repository.FollowRepository; +import com.ssg.starroad.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; -import org.springframework.data.domain.Pageable; import java.util.List; import java.util.stream.Collectors; @@ -33,7 +36,21 @@ public class StoreServiceImpl implements StoreService { private final ReviewImageService reviewImageService; // ReviewImageService 인젝션 private final ReviewFeedbackService reviewFeedbackService; private final ReviewService reviewService; + private final ReviewSentimentRepository reviewSentimentRepository; + + + private final ReviewKeywordRepository reviewKeywordRepository; + private final ReviewLikeRepository reviewLikeRepository; + private final UserRepository userRepository; + private final FollowRepository followRepository; + + @Override + public List getKeywordsByStoreCategory(Long storeId) { + Store store = storeRepository.findById(storeId).orElseThrow(() -> new RuntimeException("존재하지 않는 스토어입니다.")); + + return reviewKeywordRepository.findAllByStoreType(store.getStoreType()); + } @Override public List searchStoreList(Long id) { // 주어진 complexShoppingmallId로 매장 목록을 조회합니다. @@ -44,34 +61,66 @@ public List searchStoreList(Long id) { .map(StoreDTO::toDTO).collect(Collectors.toList()); return storeDTOList; - }@Override - public StoreWithReviewDTO findStoreWithReview(Long id, int pageNo, int pageSize) { - Store store = storeRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 스토어입니다.")); - - // 최신순으로 정렬된 Pageable 객체 생성 - Pageable pageable = PageRequest.of(pageNo, pageSize, Sort.by(Sort.Direction.DESC, "createdAt")); - - // 정렬된 Pageable 객체를 사용하여 리뷰 페이지 가져오기 - Page reviewPage = reviewRepository.findAllWithPageByStoreId(id, pageable); - - Long totalReviewCount = reviewRepository.countByStoreId(id); + } - // 각 선택지에 대한 피드백 개수를 조회합니다. - long revisitCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(id, "재방문 하고 싶어요"); - long serviceSatisfactionCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(id, "서비스가 마음에 들어요"); - long reasonablePriceCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(id, "가격이 합리적입니다"); - long cleanlinessCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(id, "매장이 청결합니다"); + @Override + public StoreWithReviewDTO findStoreWithReview(Long storeId, String userEmail, int pageNo, int pageSize, String filter, String sort, String keyword) { + + // 주어진 storeId로 스토어를 조회합니다. + Store store = storeRepository.findById(storeId).orElseThrow(() -> new RuntimeException("존재하지 않는 스토어입니다.")); + + // 정렬 방식 설정 + Sort sortOption; + if ("likes".equalsIgnoreCase(sort)) { + sortOption = Sort.by(Sort.Direction.DESC, "likeCount"); + } else { + sortOption = Sort.by(Sort.Direction.DESC, "createdAt"); + } + + // 페이지 요청 객체를 생성합니다. + Pageable pageable = PageRequest.of(pageNo, pageSize, sortOption); + + // 스토어 ID와 필터 조건에 따라 리뷰 페이지를 조회합니다. + Page reviewPage; + if (filter == null || filter.isEmpty()) { + if (keyword == null || keyword.isEmpty()) { + // 필터와 키워드가 없으면 모든 리뷰를 조회합니다. + reviewPage = reviewRepository.findAllWithPageByStoreId(storeId, pageable); + } else { + // 키워드가 있으면 Review의 contents 필드를 사용하여 리뷰를 조회합니다. + reviewPage = reviewRepository.findByStoreIdAndContentsContaining(storeId, keyword, pageable); + } + } else { + // 필터가 있으면 필터 조건에 맞는 리뷰만 조회합니다. + reviewPage = reviewRepository.findAllWithPageByStoreIdAndReviewFeedbackSelection(storeId, filter, pageable); + } + + // 스토어 ID로 전체 리뷰 개수를 조회합니다. + Long totalReviewCount = reviewRepository.countByStoreId(storeId); + + // 특정 피드백을 받은 리뷰 개수를 조회합니다. + long revisitCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(storeId, "재방문 하고 싶어요"); + long serviceSatisfactionCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(storeId, "서비스가 마음에 들어요"); + long reasonablePriceCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(storeId, "가격이 합리적입니다"); + long cleanlinessCount = reviewRepository.countByStoreIdAndReviewFeedbackSelection(storeId, "매장이 청결합니다"); + + // 사용자 이메일을 통해 사용자 ID를 조회합니다. + Long userId = userRepository.findByEmail(userEmail).orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")).getId(); List reviewDTOList = reviewPage.stream() .map(review -> { List reviewImageDTOs = reviewImageService.getReviewImages(review.getId()); List reviewFeedbackDTOs = reviewFeedbackService.getReviewFeedback(review.getId()); Long userReviewCount = reviewService.countReviewsByUserId(review.getUser().getId()); + boolean isLiked = reviewLikeRepository.existsByUser_IdAndReview_Id(userId, review.getId()); + boolean following = followRepository.existsByFromUserIdAndToUserId(userId, review.getUser().getId()); return ReviewDTO.builder() .id(review.getId()) + .following(following) .userId(review.getUser().getId()) .userNickname(review.getUser().getNickname()) + .userEmail(review.getUser().getEmail()) .imagePath(review.getUser().getImagePath()) .storeId(review.getStore().getId()) .visible(review.isVisible()) @@ -83,6 +132,7 @@ public StoreWithReviewDTO findStoreWithReview(Long id, int pageNo, int pageSize) .reviewcount(userReviewCount) .reviewImages(reviewImageDTOs) .reviewFeedbacks(reviewFeedbackDTOs) + .isLiked(isLiked) // 로그인한 사용자가 좋아요를 눌렀는지 여부 추가 .build(); }) .collect(Collectors.toList()); @@ -110,7 +160,6 @@ public StoreWithReviewDTO findStoreWithReview(Long id, int pageNo, int pageSize) } - @Override public StoreDTO findStore(Long id) { Store store = storeRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않은 매장입니다")); @@ -121,9 +170,7 @@ public StoreDTO findStore(Long id) { .storeGuideMap(store.getStoreGuideMap()) .name(store.getName()) .floor(store.getFloor().getFloor()) - .build(); - - + .build(); return storeDTO; @@ -135,4 +182,12 @@ public String findStoreTypeByName(String name) { Store store = storeRepository.findByName(name).orElseThrow(() -> new RuntimeException("존재하지 않는 스토어입니다.")); return store.getStoreType(); } + + + public String findStoreName(String receiptShopName) { + return storeRepository.findByName(receiptShopName) + .map(Store::getName) // Store 객체에서 상점 이름을 추출 + .orElseThrow(() -> new RuntimeException("Store with name '" + receiptShopName + "' not found")); + } + } \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/controller/AuthController.java b/src/main/java/com/ssg/starroad/user/controller/AuthController.java new file mode 100644 index 0000000..ef51f48 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/controller/AuthController.java @@ -0,0 +1,44 @@ +package com.ssg.starroad.user.controller; + +import com.ssg.starroad.user.service.AuthService; +import jakarta.mail.MessagingException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + private final AuthService authService; + + @Autowired + public AuthController(AuthService authService) { + this.authService = authService; + } + + @PostMapping("/send-code") + public ResponseEntity sendAuthCode(@RequestBody Map payload) { + String email = payload.get("email"); + try { + authService.sendAuthCode(email); + return ResponseEntity.ok("인증 코드가 전송되었습니다."); + } catch (MessagingException e) { + return ResponseEntity.status(500).body("이메일 전송에 실패했습니다."); + } + } + + @PostMapping("/verify-code") + public ResponseEntity verifyAuthCode(@RequestBody Map payload) { + String email = (String) payload.get("email"); + int code = Integer.parseInt(payload.get("code").toString()); + boolean isVerified = authService.verifyAuthCode(email, code); + if (isVerified) { + return ResponseEntity.ok(true); + } else { + return ResponseEntity.status(400).body(false); + } + } +} diff --git a/src/main/java/com/ssg/starroad/user/controller/FollowController.java b/src/main/java/com/ssg/starroad/user/controller/FollowController.java index 20c3aed..4920acc 100644 --- a/src/main/java/com/ssg/starroad/user/controller/FollowController.java +++ b/src/main/java/com/ssg/starroad/user/controller/FollowController.java @@ -17,29 +17,29 @@ public class FollowController { private final FollowService followService; - @GetMapping("/mycount/{userId}") - public ResponseEntity getCountByFromUserId(@PathVariable Long userId) { - long followerCount = followService.getCountByFromUserId(userId); - long followingCount = followService.getCountByToUserId(userId); + @GetMapping("/mycount/{email}") + public ResponseEntity getCountByFromUserId(@PathVariable String email) { + long followerCount = followService.getCountByFromUserId(email); + long followingCount = followService.getCountByToUserId(email); FollowCountDTO followCountDTO = new FollowCountDTO(followerCount, followingCount); return ResponseEntity.ok(followCountDTO); } - @GetMapping("/from/{userId}") - public ResponseEntity> getFollowsByFromUserId(@PathVariable Long userId) { - List follows = followService.getFollowsByFromUserId(userId); + @GetMapping("/from/{email}") + public ResponseEntity> getFollowsByFromUserId(@PathVariable String email) { + List follows = followService.getFollowsByFromUserId(email); return ResponseEntity.ok(follows); } - @GetMapping("/to/{userId}") - public ResponseEntity> getFollowsByToUserId(@PathVariable Long userId) { - List follows = followService.getFollowsByToUserId(userId); + @GetMapping("/to/{email}") + public ResponseEntity> getFollowsByToUserId(@PathVariable String email) { + List follows = followService.getFollowsByToUserId(email); return ResponseEntity.ok(follows); } - @DeleteMapping("/{id}/deletefrom/{userId}") - public ResponseEntity deleteFollowingUser(@PathVariable Long userId, @PathVariable Long id) { - followService.deleteFollowingUser(userId, id); + @DeleteMapping("/{id}/deletefrom/{email}") + public ResponseEntity deleteFollowingUser(@PathVariable String email, @PathVariable Long id) { + followService.deleteFollowingUser(email, id); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/com/ssg/starroad/user/controller/OAuth2Controller.java b/src/main/java/com/ssg/starroad/user/controller/OAuth2Controller.java new file mode 100644 index 0000000..c8e8459 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/controller/OAuth2Controller.java @@ -0,0 +1,46 @@ +package com.ssg.starroad.user.controller; + +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.config.oauth.OAuth2SuccessHandler; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.service.CustomOAuth2UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.*; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/oauth2") +@RequiredArgsConstructor +public class OAuth2Controller { + + private final CustomOAuth2UserService customOAuth2UserService; + private final TokenProvider tokenProvider; + + + @PostMapping("/{provider}") + public ResponseEntity oauth2Login(@PathVariable String provider, @RequestBody Map body) { + String code = body.get("code"); + OAuth2User oAuth2User = customOAuth2UserService.loadUserByProvider(provider, code); + + Map attributes = oAuth2User.getAttributes(); + String accessToken = (String) attributes.get("accessToken"); + String refreshToken = (String) attributes.get("refreshToken"); + String email = (String) attributes.get("email"); + + // Create a response object + Map response = new HashMap<>(); + response.put("accessToken", accessToken); + response.put("refreshToken", refreshToken); + response.put("email", email); + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/controller/TokenApiController.java b/src/main/java/com/ssg/starroad/user/controller/TokenApiController.java new file mode 100644 index 0000000..1501162 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/controller/TokenApiController.java @@ -0,0 +1,43 @@ +package com.ssg.starroad.user.controller; + +import com.ssg.starroad.user.dto.CreateAccessTokenRequest; +import com.ssg.starroad.user.dto.CreateAccessTokenResponse; +import com.ssg.starroad.user.service.TokenService; +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; + +@RestController +// 클래스가 RESTful 웹 서비스의 컨트롤러로 작동함 +// 모든 메서드는 HTTP 응답 본문에 직접 작성되며, 객체는 자동으로 JSON 또는 XML로 직렬화 +@RequestMapping("/api/token") +public class TokenApiController { + + private final TokenService tokenService; + + public TokenApiController(TokenService tokenService) { + this.tokenService = tokenService; + } + + // 클라이언트는 리프레시 토큰을 포함한 CreateAccessTokenRequest 객체를 JSON 형태로 서버에 전송 + + // 새 액세스 토큰을 생성하는 요청을 담당 + @PostMapping("/access-token") + // CreateAccessTokenRequest 객체를 요청 본문에서 받아 처리하고, + // CreateAccessTokenResponse 객체를 반환 + // @RequestBody 어노테이션은 HTTP 요청 본문을 자바 객체로 변환하는 데 사용 + public ResponseEntity createAccessToken(@RequestBody CreateAccessTokenRequest request) { + String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken()); + // 서버는 요청을 받고 TokenService를 사용하여 리프레시 토큰의 유효성을 검증 + // 유효한 토큰인 경우, 관련된 사용자 정보를 기반으로 새 액세스 토큰을 생성 + + return ResponseEntity.ok(new CreateAccessTokenResponse(newAccessToken)); + // 생성된 액세스 토큰을 클라이언트에게 반환하는 데 사용 + // ok() 메서드는 HTTP 200 상태 코드를 설정 + // 응답 본문에는 생성된 토큰이 포함 + + // 클라이언트는 이 토큰을 사용하여 권한이 필요한 리소스에 접근 가능 + } +} diff --git a/src/main/java/com/ssg/starroad/user/controller/UserController.java b/src/main/java/com/ssg/starroad/user/controller/UserController.java index 4a9041a..3d88f7a 100644 --- a/src/main/java/com/ssg/starroad/user/controller/UserController.java +++ b/src/main/java/com/ssg/starroad/user/controller/UserController.java @@ -1,50 +1,205 @@ package com.ssg.starroad.user.controller; + +import com.ssg.starroad.common.util.CookieUtil; +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.user.dto.*; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.repository.RefreshTokenRepository; +import com.ssg.starroad.user.service.RefreshTokenService; import com.ssg.starroad.common.service.S3Uploader; import com.ssg.starroad.user.dto.MypageDTO; import com.ssg.starroad.user.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import java.util.Date; +import java.util.Map; +import java.util.Optional; import org.springframework.web.multipart.MultipartFile; + @RequestMapping("/user") @RestController @RequiredArgsConstructor public class UserController { private final UserService userService; + private final TokenProvider tokenProvider; + private final RefreshTokenService refreshTokenService; + private final RefreshTokenRepository refreshTokenRepository; private final S3Uploader s3Uploader; - @GetMapping("/mypage/{userId}") - public ResponseEntity getMypage(@PathVariable Long userId) { - MypageDTO mypageDTO=userService.getMypage(userId); + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + boolean isValidUser = userService.validateUser(request.getEmail(), request.getPassword()); + if (isValidUser) { + UserDetails userDetails = userService.loadUserByUsername(request.getEmail()); + User user = userService.findByEmail(userDetails.getUsername()).orElse(null); + if (user == null || !user.isEnabled()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new LoginResponse("비활성화 계정입니다. 이메일로 문의해주세요.")); + } + + Date expiry = new Date(System.currentTimeMillis() + 600 * 1000); // 10 minutes + String accessToken = tokenProvider.createToken(expiry, user); + String refreshToken = refreshTokenService.createRefreshToken(user.getId()); + + // 설정된 헤더를 응답에 추가 + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); // Bearer 타입의 Authorization 헤더 추가 + + LoginResponse loginResponse = new LoginResponse(accessToken, refreshToken,"환영합니다!"); + return ResponseEntity.ok().headers(headers).body(loginResponse); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new LoginResponse("이메일 또는 비밀번호를 잘못 입력했습니다")); + } + } + + @PostMapping("/join/email-check") + public ResponseEntity checkEmailDuplicate(@RequestBody Map request) { + boolean exists = userService.isEmailDuplicate(request.get("email")); + System.out.println(exists); + return ResponseEntity.ok(exists); + } + + @PostMapping("/join/nickname-check") + public ResponseEntity checkNicknameDuplicate(@RequestBody Map request) { + boolean exists = userService.isNicknameDuplicate(request.get("nickname")); + System.out.println(exists); + return ResponseEntity.ok(exists); + } + + @PostMapping("/join") + public ResponseEntity registerUser(@RequestBody UserDTO userDTO) { + try { + userService.registerUser(userDTO); + return ResponseEntity.ok("회원가입이 완료되었습니다."); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("회원가입 중 오류가 발생했습니다."); + } + } + + @PostMapping("/update-password") + public ResponseEntity updatePassword(@RequestBody PasswordUpdateRequest request) { + System.out.println("Received request to update password for email: " + request.getEmail()); // 로그 추가 + try { + boolean isUpdated = userService.updatePassword(request.getEmail(), request.getNewPassword()); + if (isUpdated) { + return ResponseEntity.ok("비밀번호가 성공적으로 변경되었습니다."); + } else { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("비밀번호 변경에 실패했습니다."); + } + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("비밀번호 변경 중 오류가 발생했습니다."); + } + } + + // 사용자 정보를 가져오는 메소드 + @PostMapping("/details") + public ResponseEntity getUserDetails(@RequestBody Map request) { + String email = request.get("email"); + Optional userOptional = userService.findByEmail(email); + if (userOptional.isPresent()) { + UserDTO userDTO = new UserDTO(userOptional.get()); + return ResponseEntity.ok(userDTO); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); + } + } + + @PostMapping("/update-profile") + public ResponseEntity updateProfile(@RequestBody UserDTO userDTO) { + try { + Optional userOptional = userService.findByEmail(userDTO.getEmail()); + if (userOptional.isPresent()) { + User user = userOptional.get(); + userService.updateUserProfile(user, userDTO); + return ResponseEntity.ok("프로필이 성공적으로 업데이트되었습니다."); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("사용자를 찾을 수 없습니다."); + } + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("프로필 업데이트 중 오류가 발생했습니다."); + } + } + + @Transactional + @PostMapping("/logout") + public ResponseEntity logout(@RequestBody LogoutRequest logoutRequest, HttpServletRequest request, HttpServletResponse response) { + String email = logoutRequest.getEmail(); + String accessToken = logoutRequest.getAccessToken(); + + // 로그 추가 + System.out.println("Logout request received"); + System.out.println("Email: " + email); + System.out.println("Access token: " + accessToken); + + if (tokenProvider.validToken(accessToken)) { + // 이메일과 토큰으로 사용자 인증 및 토큰 무효화 로직 추가 + refreshTokenRepository.deleteByUserEmail(email); + System.out.println("Token is valid. Proceeding with logout."); + }else { + System.out.println("Invalid token."); + return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).body("Invalid token"); + } + // 세션 무효화 + request.getSession().invalidate(); + + // JSESSIONID 쿠키 삭제 + CookieUtil.deleteCookie(request, response, "JSESSIONID"); + + + return ResponseEntity.ok().body("Logout successful"); + } + + @PostMapping("/inactive") + public ResponseEntity deactivateUser(@RequestBody Map request) { + String email = request.get("email"); + if (email == null || email.isEmpty()) { + return ResponseEntity.badRequest().body("Email is required"); + } + userService.inactiveUser(email); + return ResponseEntity.ok("User deactivated successfully"); + + } + + + + @GetMapping("/mypage/{email}") + public ResponseEntity getMypage(@PathVariable String email) { + MypageDTO mypageDTO=userService.getMypage(email); if (mypageDTO==null) { return ResponseEntity.noContent().build(); } return ResponseEntity.ok(mypageDTO); } - @PostMapping("/profile/upload/img/{userId}") - public ResponseEntity uploadImg(@PathVariable Long userId, @RequestParam("file") MultipartFile uploadImg) { + @PostMapping("/profile/upload/img/{email}") + public ResponseEntity uploadImg(@PathVariable String email, @RequestParam("file") MultipartFile uploadImg) { String path = s3Uploader.upload(uploadImg,"ssg/user/profile"); - userService.saveProfileimg(userId,path); + userService.saveProfileimg(email,path); return ResponseEntity.ok().build(); } - @GetMapping("/profile/get/img/{userId}") - public ResponseEntity getImg(@PathVariable Long userId) + @GetMapping("/profile/get/img/{email}") + public ResponseEntity getImg(@PathVariable String email) { - String path =userService.getProfileimg(userId); + String path =userService.getProfileimg(email); if (path==null) { return ResponseEntity.noContent().build(); } return ResponseEntity.ok(path.trim()); } - @DeleteMapping("/profile/delete/img/{userId}") - public ResponseEntity deleteImg(@PathVariable Long userId){ - userService.deleteProfileimg(userId); + @DeleteMapping("/profile/delete/img/{email}") + public ResponseEntity deleteImg(@PathVariable String email){ + userService.deleteProfileimg(email); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/ssg/starroad/user/dto/CreateAccessTokenRequest.java b/src/main/java/com/ssg/starroad/user/dto/CreateAccessTokenRequest.java new file mode 100644 index 0000000..b40dbb0 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/CreateAccessTokenRequest.java @@ -0,0 +1,24 @@ +package com.ssg.starroad.user.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor // 모든 필드를 매개변수로 받는 생성자를 추가 +@NoArgsConstructor +// 매개변수 없는 기본 생성자 생성 +// 데이터 바인딩 사용할 때 객체를 생성한 후 세터 메소드를 통해 +// 값을 주입할 수 있기 때문에 기본 생성자가 필요 +// 예를 들어, json 형태의 http 요청 바디를 객체로 변환할 때 사용 + +// 클라이언트로부터 리프레시 토큰을 받아 새로운 액세스 토큰을 생성하기 위한 +// 요청 정보를 담는 역할! 여기엔 보통 리프레스 토큰이 포함되어 있음 +// 사용자로부터 리프레시 토큰을 입력받는 용도! +public class CreateAccessTokenRequest { + private String refreshToken; // 클라이언트가 제공하는 리프레시 토큰 +} + diff --git a/src/main/java/com/ssg/starroad/user/dto/CreateAccessTokenResponse.java b/src/main/java/com/ssg/starroad/user/dto/CreateAccessTokenResponse.java new file mode 100644 index 0000000..7d967f8 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/CreateAccessTokenResponse.java @@ -0,0 +1,17 @@ +package com.ssg.starroad.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +// 모든 필드를 매개변수로 받는 생성자 생성 +// 모든 필드 값을 초기화해야 할 필요가 있음 +// 생성 시점에 모든 데이터를 받아 객체 상태를 완전히 정의해야 하는 경우 사용 +// 생성된 액세스 토큰을 필수적으로 포함해 인스턴스를 생성해야 해서 모든 인자를 받는 생성자가 필요함! + +// 새로 생성된 액세스 토큰을 클라이언트에게 반환할 때 사용 +// 클라이언트에게 액세스 토큰 정보를 제공하기 위한 목적! +public class CreateAccessTokenResponse { + private String accessToken; // 생성된 액세스 토큰 +} \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/dto/LoginRequest.java b/src/main/java/com/ssg/starroad/user/dto/LoginRequest.java new file mode 100644 index 0000000..7a1073f --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/LoginRequest.java @@ -0,0 +1,18 @@ +package com.ssg.starroad.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class LoginRequest { + private String email; + private String password; + + public LoginRequest(String email, String password) { + this.email = email; + this.password = password; + } +} diff --git a/src/main/java/com/ssg/starroad/user/dto/LoginResponse.java b/src/main/java/com/ssg/starroad/user/dto/LoginResponse.java new file mode 100644 index 0000000..c476e10 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/LoginResponse.java @@ -0,0 +1,25 @@ +package com.ssg.starroad.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoginResponse { + private String accessToken; + private String refreshToken; + private String message; + + + public LoginResponse(String message) { + this.message = message; + } + +} +// 응답에서 리프레시 토큰을 포함하는 이유 --- +// 사용자가 로그인할 때 마다 새로운 리프레시 토큰을 발급하는 것은, +// 토큰의 보안을 강화하고, 오래된 또는 잠재적으로 노출된 리프레시 토큰을 무효화하는 효과적인 방법 +// 일부 시스템에서는 리프레시 토큰의 재사용을 방지하기 위해 매 로그인 시 또는 매 토큰 갱신 시 +// 새로운 리프레시 토큰을 발급하고 이전 토큰을 무효화 +// 따라서, 로그인 응답에 리프레시 토큰을 포함하는 것은 +// 토큰 기반 인증 시스템에서 흔히 볼 수 있는 보안 관행이며, 이를 통해 사용자의 로그인 상태를 안전하게 유지 가능 \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/dto/LogoutRequest.java b/src/main/java/com/ssg/starroad/user/dto/LogoutRequest.java new file mode 100644 index 0000000..2ab9fb4 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/LogoutRequest.java @@ -0,0 +1,11 @@ +package com.ssg.starroad.user.dto; + +import lombok.Data; + +@Data +public class LogoutRequest { + private String email; + private String accessToken; + private String refreshToken; + +} diff --git a/src/main/java/com/ssg/starroad/user/dto/MypageDTO.java b/src/main/java/com/ssg/starroad/user/dto/MypageDTO.java index af7c5e6..c32d0d4 100644 --- a/src/main/java/com/ssg/starroad/user/dto/MypageDTO.java +++ b/src/main/java/com/ssg/starroad/user/dto/MypageDTO.java @@ -13,7 +13,5 @@ public class MypageDTO { private String profileImg; private int reviewExp; private String name; - private int following; - private int follower; private int point; } diff --git a/src/main/java/com/ssg/starroad/user/dto/OAuth2UserInfoDTO.java b/src/main/java/com/ssg/starroad/user/dto/OAuth2UserInfoDTO.java new file mode 100644 index 0000000..cdca740 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/OAuth2UserInfoDTO.java @@ -0,0 +1,97 @@ +package com.ssg.starroad.user.dto; + +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.enums.ActiveStatus; +import com.ssg.starroad.user.enums.ProviderType; +import lombok.*; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Map; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2UserInfoDTO { + private String email; + private String name; + private String nickname; + private String imagePath; + private ProviderType providerType; + private String providerId; + + // OAuth2User 인터페이스를 구현한 객체에서 필요한 정보를 추출하여 OAuth2UserInfoDTO 객체를 생성 + // OAuth2 인증 과정에서 제공된 사용자 정보 중 필요한 부분만을 선택하여 DTO에 저장 + public static OAuth2UserInfoDTO fromOAuth2User(OAuth2User oAuth2User, String providerType, String providerId) { + switch (providerType.toUpperCase()) { + case "GOOGLE": + return createGoogleDTO(oAuth2User, providerId); + case "KAKAO": + return createKakaoDTO(oAuth2User, providerId); + case "NAVER": + return createNaverDTO(oAuth2User, providerId); + default: + throw new IllegalArgumentException("Unsupported provider: " + providerType); + } + } + private static OAuth2UserInfoDTO createGoogleDTO(OAuth2User oAuth2User, String providerId) { + OAuth2UserInfoDTO dto = new OAuth2UserInfoDTO(); + dto.setEmail(oAuth2User.getAttribute("email")); + dto.setName(oAuth2User.getAttribute("name")); + dto.setNickname(oAuth2User.getAttribute("name")); + dto.setImagePath(oAuth2User.getAttribute("picture")); + dto.setProviderType(ProviderType.GOOGLE); + dto.setProviderId(providerId); + return dto; + } + + private static OAuth2UserInfoDTO createKakaoDTO(OAuth2User oAuth2User, String providerId) { + Map kakaoAccount = oAuth2User.getAttribute("kakao_account"); + OAuth2UserInfoDTO dto = new OAuth2UserInfoDTO(); + if (kakaoAccount != null) { + dto.setEmail((String) kakaoAccount.get("email")); + Map profile = (Map) kakaoAccount.get("profile"); + dto.setName((String) profile.get("nickname")); + dto.setImagePath((String) profile.get("thumbnail_image_url")); + } + dto.setProviderType(ProviderType.KAKAO); + dto.setProviderId(providerId); + return dto; + } + + private static OAuth2UserInfoDTO createNaverDTO(OAuth2User oAuth2User, String providerId) { + Map response = oAuth2User.getAttribute("response"); + OAuth2UserInfoDTO dto = new OAuth2UserInfoDTO(); + if (response != null) { + dto.setEmail((String) response.get("email")); + dto.setName((String) response.get("name")); + dto.setImagePath((String) response.get("profile_image")); + } + dto.setProviderType(ProviderType.NAVER); + dto.setProviderId(providerId); + return dto; + } + + // User 엔티티의 새 인스턴스를 생성 + // DTO가 서비스 레이어와 데이터 계층 사이에서 엔티티의 생성을 담당 + // 엔티티 인스턴스의 생성 로직과 비즈니스 로직을 분리 + public User toEntity() { + return User.builder() + .email(this.email) + .name(this.name) + .nickname(this.nickname != null ? this.nickname : generateTempNickname()) // 기본 닉네임 설정 + .imagePath(this.imagePath) + .providerType(this.providerType) + .providerId(this.providerId) + .activeStatus(ActiveStatus.ACTIVE) + .build(); + } + + private String generateTempNickname() { + long unixTimestamp = System.currentTimeMillis() / 1000L; + String timestampStr = String.valueOf(unixTimestamp); + String lastSixDigits = timestampStr.length() > 6 ? timestampStr.substring(timestampStr.length() - 6) : timestampStr; + return "star" + lastSixDigits; + } +} diff --git a/src/main/java/com/ssg/starroad/user/dto/PasswordUpdateRequest.java b/src/main/java/com/ssg/starroad/user/dto/PasswordUpdateRequest.java new file mode 100644 index 0000000..0fedd6c --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/PasswordUpdateRequest.java @@ -0,0 +1,16 @@ +package com.ssg.starroad.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class PasswordUpdateRequest { + private String email; + private String newPassword; + +} diff --git a/src/main/java/com/ssg/starroad/user/dto/RankUserDTO.java b/src/main/java/com/ssg/starroad/user/dto/RankUserDTO.java new file mode 100644 index 0000000..0110bc5 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/RankUserDTO.java @@ -0,0 +1,34 @@ +package com.ssg.starroad.user.dto; + +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.enums.ActiveStatus; +import lombok.*; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RankUserDTO { + private Long id; + private String name; + private String nickname; + private String email; + private String imagePath; + private int reviewExp; + private int point; + private ActiveStatus activeStatus; + private boolean isFollowed; + + public RankUserDTO(User user) { + this.id = user.getId(); + this.name = user.getName(); + this.nickname = user.getNickname(); + this.email = user.getEmail(); + this.imagePath = user.getImagePath(); + this.reviewExp = user.getReviewExp(); + this.point = user.getPoint(); + this.activeStatus = user.getActiveStatus(); + } +} diff --git a/src/main/java/com/ssg/starroad/user/dto/UserDTO.java b/src/main/java/com/ssg/starroad/user/dto/UserDTO.java new file mode 100644 index 0000000..632b816 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/dto/UserDTO.java @@ -0,0 +1,48 @@ +package com.ssg.starroad.user.dto; + +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.enums.ActiveStatus; +import com.ssg.starroad.user.enums.Gender; +import com.ssg.starroad.user.enums.ProviderType; +import lombok.*; + +import java.time.LocalDate; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class UserDTO { + private Long id; + private String password; + private String name; + private String nickname; + private String email; + private Gender gender; + private LocalDate birth; + private String phone; + private String imagePath; + private ProviderType providerType; + private String providerId; + private int reviewExp; + private int point; + private ActiveStatus activeStatus; + + public UserDTO(User user) { + this.id = user.getId(); + this.password = user.getPassword(); + this.name = user.getName(); + this.nickname = user.getNickname(); + this.email = user.getEmail(); + this.gender = user.getGender(); + this.birth = user.getBirth(); + this.phone = user.getPhone(); + this.imagePath = user.getImagePath(); + this.providerType = user.getProviderType(); + this.providerId = user.getProviderId(); + this.reviewExp = user.getReviewExp(); + this.point = user.getPoint(); + this.activeStatus = user.getActiveStatus(); + } +} diff --git a/src/main/java/com/ssg/starroad/user/entity/AuthCode.java b/src/main/java/com/ssg/starroad/user/entity/AuthCode.java new file mode 100644 index 0000000..d6c5ccc --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/entity/AuthCode.java @@ -0,0 +1,29 @@ +package com.ssg.starroad.user.entity; + + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class AuthCode { + + @Id + private String email; + private int code; + + // 기본 생성자 필요 + protected AuthCode() {} + + public AuthCode(String email, int code) { + this.email = email; + this.code = code; + } + + public String getEmail() { + return email; + } + + public int getCode() { + return code; + } +} \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/entity/Follow.java b/src/main/java/com/ssg/starroad/user/entity/Follow.java index 319cda5..633f9ed 100644 --- a/src/main/java/com/ssg/starroad/user/entity/Follow.java +++ b/src/main/java/com/ssg/starroad/user/entity/Follow.java @@ -2,6 +2,8 @@ import com.ssg.starroad.common.entity.BaseTimeEntity; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,6 +13,8 @@ @Getter @Entity @NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +@Builder public class Follow extends BaseTimeEntity { @Id diff --git a/src/main/java/com/ssg/starroad/user/entity/RefreshToken.java b/src/main/java/com/ssg/starroad/user/entity/RefreshToken.java new file mode 100644 index 0000000..0efe3a7 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/entity/RefreshToken.java @@ -0,0 +1,60 @@ +package com.ssg.starroad.user.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.Instant; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity + +// 1. 장기 인증 지원: 리프레시 토큰은 일반적으로 액세스 토큰보다 훨씬 긴 수명을 가짐. +// 그래서 사용자는 자주 로그인할 필요 없이 지속적으로 서비스를 이용 가능. +// 2. 보안성 향상: 액세스 토큰은 짧은 수명을 가지므로, 만약 이 토큰이 탈취당하더라도 사용할 수 있는 시간이 제한적. +// 리프레시 토큰을 이용해 새 액세스 토큰을 발급받는 방식은 액세스 토큰이 노출될 위험을 줄여줍니다. +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + // RefreshToken 인스턴스를 유일하게 식별하는 데 사용 + // 자동 생성된 ID는 토큰 관리를 더욱 효율적으로 + + @Column(nullable = false, unique = true) + private String token; + // 리프레시 토큰은 보안 상 중요한 정보 + // 각 토큰 값은 유일무이, 사용자 인증이나 세션 관리 시 안전하게 사용 가능 + + @Setter + @Column(nullable = false) + private Instant expiryDate; + // 만료 시간은 토큰의 유효성을 검증할 때 중요한 역할 + // 만료된 토큰은 자동으로 무효화 + // 세계 표준시(UTC)를 기준 + // 시간의 덧셈, 뺄셈과 같은 시간 연산을 수행하는 다양한 메소드를 제공 + // 예 : Instant.now().plusSeconds(3600)은 현재 시간으로부터 1시간 후 + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", referencedColumnName = "id") + private User user; + // RefreshToken과 User 엔티티 간의 일대일 관계를 설정 + // 토큰이 특정 사용자에게 속함을 명확히 + // 지연 로딩(FetchType.LAZY)을 사용해 성능을 최적화 + // User 엔티티는 실제로 접근할 때만 데이터베이스에서 로드 + + + public RefreshToken(User user, String token, Instant expiryDate) { + this.user = user; + this.token = token; + this.expiryDate = expiryDate; + } + public void updateToken(String newToken) { + this.token = newToken; + } + + // 새로운 리프레시 토큰이 발행될 때 사용 +} diff --git a/src/main/java/com/ssg/starroad/user/entity/User.java b/src/main/java/com/ssg/starroad/user/entity/User.java index b2a6cfb..5d26645 100644 --- a/src/main/java/com/ssg/starroad/user/entity/User.java +++ b/src/main/java/com/ssg/starroad/user/entity/User.java @@ -3,43 +3,79 @@ import com.ssg.starroad.common.entity.BaseTimeEntity; import com.ssg.starroad.user.enums.ActiveStatus; import com.ssg.starroad.user.enums.Gender; +import com.ssg.starroad.user.enums.ProviderType; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; import java.time.LocalDate; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; @Getter +@Builder(toBuilder = true) +// User 객체의 현재 상태를 기반으로 새로운 User 객체를 쉽게 생성 +// 예를 들어, 특정 속성만 변경하고 나머지는 기존 값으로 유지하려는 경우, +// toBuilder() 메소드를 사용하여 빌더를 얻고, 변경할 속성만 설정한 다음 build() 메소드를 호출하면 됨 @Entity @NoArgsConstructor(access = PROTECTED) @AllArgsConstructor @Embeddable -@Builder -public class User extends BaseTimeEntity { +@Setter +@ToString +public class User extends BaseTimeEntity implements UserDetails, OAuth2User { + @Id @GeneratedValue(strategy = IDENTITY) + @Column(updatable = false) + // 사용자가 자신의 정보를 업데이트하는 동안, + // 데이터베이스에서 식별자로 사용되는 id 값의 안정성을 보장 private Long id; + @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W).{8,}$", + message = "비밀번호는 8자 이상이며, 숫자, 문자, 특수문자를 각각 하나 이상 포함해야 합니다.") private String password; // bcrypt + + @Size(min = 2, max = 10, message = "이름은 2자 이상 10자 이하여야 합니다.") private String name; - @Column(unique = true) + + @Pattern(regexp = "^[가-힣a-zA-Z0-9\\p{Punct}]{3,10}$", + message = "닉네임은 3자 이상 10자 이하로, 영어, 한국어, 숫자, 특수문자를 포함해야 합니다.") + @Column(nullable = false, unique = true) private String nickname; + private String imagePath; // profile image + @Enumerated(EnumType.STRING) private Gender gender; + @Column + @Past(message = "생년월일은 과거 날짜여야 합니다.") private LocalDate birth; - private String phone; // xxx-xxxx-xxxx - @Column(unique = true) - private String email; // 이메일 - @Column(unique = true) - private String provider; // provider + providerId => unique + + @Pattern(regexp = "^[0-9]{11}$", message = "전화번호는 하이픈 없이 11자리여야 합니다.") + private String phone; // 하이픈 없이 11자리 + + @Column(nullable = false, unique = true) + @Email(message = "유효한 이메일 주소여야 합니다.") + private String email; + + @Enumerated(EnumType.STRING) + private ProviderType providerType; // 네이버, 카카오, 구글 + private String providerId; // (네이버, 카카오, 구글)에서 받아온 아이디 private int reviewExp; private int point; @@ -47,7 +83,116 @@ public class User extends BaseTimeEntity { @Enumerated(EnumType.STRING) private ActiveStatus activeStatus; - public void setProfileimgPath(String path){ - this.imagePath=path; + + // OAuth2User 메소드 + + @Override + public String getName() { + return this.providerId; // providerId를 반환 + } + + public String getUserName() { + return this.name; + } + + // 제네릭 타입 A를 사용하여, OAuth2User 인터페이스에서 + // 다양한 타입의 사용자 속성을 안전하게 가져오도록 설계(필수) + // String email = user.getAttribute("email")과 같이 사용할 때 + // 컴파일러가 반환 타입이 String임을 확인하고, 다른 타입으로 잘못 캐스팅하는 실수를 방지 + // 또 name 파라미터는 OAuth2User의 속성 중 하나를 지정하는 문자열 + // email, name, nickname 등과 같이 실제 속성 이름을 문자열로 전달받아 해당 속성 값을 반환 + @Override + public A getAttribute(String name) { + switch (name) { + case "email": + return (A) this.email; + case "name": + return (A) this.name; + case "nickname": + return (A) this.nickname; + case "imagePath": + return (A) this.imagePath; + default: + return null; // 해당하는 속성이 없을 경우 null 반환 + } + } + + // 메소드는 속성 맵 전체를 반환 + @Override + public Map getAttributes() { + // 실제 사용 시 필요한 모든 속성을 포함한 Map을 반환 + Map attributes = new HashMap<>(); + attributes.put("email", this.email); + attributes.put("name", this.name); + attributes.put("nickname", this.nickname); + attributes.put("imagePath", this.imagePath); + attributes.put("providerType", this.providerType != null ? this.providerType.name() : null); + attributes.put("providerId", this.providerId); + return attributes; + } + + // UserDetails 메소드 + @Override + public Collection getAuthorities() { + // 모든 사용자에게 'ROLE_USER' 권한 부여 + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getUsername() { + // 사용자의 아이디 반환 (고유한 값) + return email; + } + + @Override + public boolean isAccountNonExpired() { + // 계정 만료 여부 반환 + return true; + // true 만료되지 않았음 + } + + @Override + public boolean isAccountNonLocked() { + // 계정 잠금 여부 반환 + return true; + // true 잠금되지 않았음 + } + + @Override + public boolean isCredentialsNonExpired() { + // 패스워드 만료 여부 반환 + return true; + // true 만료되지 않았음 + } + + @Override + public boolean isEnabled() { + // activeStatus가 ACTIVE인 경우에만 true 반환 + return this.activeStatus == ActiveStatus.ACTIVE; + } + // 로그인을 할 수 있는 전제가 회원의 상태가 active여야 함 + // 이때 true를 반환 + + public void updatePassword(String newPassword) { + this.password = newPassword; + } + + // 닉네임, 비밀번호를 변경하는 메소드 추가 + public void updateProfile(String nickname, String password) { + if (nickname != null) { + this.nickname = nickname; + } + if (password != null) { + this.password = password; + } + } + + public void changeActiveStatus(ActiveStatus newStatus) { + this.activeStatus = newStatus; + } + + public void setProfileimgPath(String path) { + this.imagePath = path; + } } diff --git a/src/main/java/com/ssg/starroad/user/enums/ActiveStatus.java b/src/main/java/com/ssg/starroad/user/enums/ActiveStatus.java index 59aa212..b3b00d9 100644 --- a/src/main/java/com/ssg/starroad/user/enums/ActiveStatus.java +++ b/src/main/java/com/ssg/starroad/user/enums/ActiveStatus.java @@ -6,8 +6,9 @@ public enum ActiveStatus { ACTIVE("활성화"), INACTIVE("비활성화"), - SUSPENDED("정지"), + // 관리자에 의해 강제로 계정을 닫은 경우 CLOSED("탈퇴"), + // 유저가 자발적으로 탈퇴한 경우 WITHDRAW("탈퇴"); private String desc; diff --git a/src/main/java/com/ssg/starroad/user/enums/ProviderType.java b/src/main/java/com/ssg/starroad/user/enums/ProviderType.java new file mode 100644 index 0000000..dc0ea9b --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/enums/ProviderType.java @@ -0,0 +1,18 @@ +package com.ssg.starroad.user.enums; + +public enum ProviderType { + NAVER, KAKAO, GOOGLE, LOCAL; + + // 입력받은 문자열 providerId를 대문자로 변환 + // ProviderType의 valueOf 메소드를 사용하여 해당 문자열과 일치하는 열거형 상수를 반환 + public static ProviderType fromString(String providerId) { + if (providerId == null) { + throw new IllegalArgumentException("providerId is null"); + } + try { + return ProviderType.valueOf(providerId.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("providerId에 맞는 열거형 값이 없음: " + providerId, e); + } + } +} diff --git a/src/main/java/com/ssg/starroad/user/repository/AuthCodeRepository.java b/src/main/java/com/ssg/starroad/user/repository/AuthCodeRepository.java new file mode 100644 index 0000000..7e32e78 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/repository/AuthCodeRepository.java @@ -0,0 +1,7 @@ +package com.ssg.starroad.user.repository; + +import com.ssg.starroad.user.entity.AuthCode; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AuthCodeRepository extends JpaRepository { +} diff --git a/src/main/java/com/ssg/starroad/user/repository/FollowRepository.java b/src/main/java/com/ssg/starroad/user/repository/FollowRepository.java index a6dde75..ef9f5a1 100644 --- a/src/main/java/com/ssg/starroad/user/repository/FollowRepository.java +++ b/src/main/java/com/ssg/starroad/user/repository/FollowRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; public interface FollowRepository extends JpaRepository { @Query("SELECT COUNT(f) FROM Follow f WHERE f.fromUser.id = :userId") @@ -25,4 +26,8 @@ public interface FollowRepository extends JpaRepository { @Modifying @Query("DELETE FROM Follow f WHERE f.fromUser.id = :fromUserId AND f.toUser.id = :toUserId") void deleteByFromUserIdAndToUserId(@Param("fromUserId") Long fromUserId, @Param("toUserId") Long toUserId); + + Optional findByFromUserAndToUser(User fromUser, User toUser); + + boolean existsByFromUserIdAndToUserId(Long fromUserId, Long toUserId); } diff --git a/src/main/java/com/ssg/starroad/user/repository/RefreshTokenRepository.java b/src/main/java/com/ssg/starroad/user/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..6f4a683 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/repository/RefreshTokenRepository.java @@ -0,0 +1,26 @@ +package com.ssg.starroad.user.repository; + +import com.ssg.starroad.user.entity.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + + +public interface RefreshTokenRepository extends JpaRepository { + Optional findByUser_Id(Long userId); + + Optional findByToken(String refreshToken); + + void deleteByUserEmail(String email); + + // 테스트 용 + @Transactional + void deleteByUserId(Long userId); +} + +// findByUser_Id(Long userId) 메서드를 사용하는 것은 +// 일대일 관계를 통해 특정 사용자의 리프레시 토큰을 쉽게 찾기 위함 +// JPA에서는 연관된 엔티티의 필드를 조회하는 방법으로 +// _ (언더스코어)를 사용하여 엔티티의 필드 경로 지정 가능 +// User 엔티티의 id 값을 이용하여 해당 User의 RefreshToken 조회 가능 \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/repository/UserRepository.java b/src/main/java/com/ssg/starroad/user/repository/UserRepository.java index ce7ce32..8461f61 100644 --- a/src/main/java/com/ssg/starroad/user/repository/UserRepository.java +++ b/src/main/java/com/ssg/starroad/user/repository/UserRepository.java @@ -2,12 +2,32 @@ import com.ssg.starroad.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + + boolean existsByNickname(String nickname); Optional findByid(Long id); + Optional findBynickname(String nickName); + @Transactional + @Modifying + @Query("UPDATE User u SET u.reviewExp = :reviewExp WHERE u.id = :id") + int updateReviewExpById(@Param("id") Long id, @Param("reviewExp") int reviewExp); + + // point 필드를 업데이트하는 메소드 + @Transactional + @Modifying + @Query("UPDATE User u SET u.point = :point WHERE u.id = :id") + int updatePointById(@Param("id") Long id, @Param("point") int point); + + } diff --git a/src/main/java/com/ssg/starroad/user/service/AuthService.java b/src/main/java/com/ssg/starroad/user/service/AuthService.java new file mode 100644 index 0000000..d10582b --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/AuthService.java @@ -0,0 +1,8 @@ +package com.ssg.starroad.user.service; + +import jakarta.mail.MessagingException; + +public interface AuthService { + void sendAuthCode(String email) throws MessagingException; + boolean verifyAuthCode(String email, int authCode); +} diff --git a/src/main/java/com/ssg/starroad/user/service/CustomOAuth2UserService.java b/src/main/java/com/ssg/starroad/user/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..d06d788 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/CustomOAuth2UserService.java @@ -0,0 +1,29 @@ +package com.ssg.starroad.user.service; + +import com.ssg.starroad.user.entity.User; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; + +import java.util.Optional; + +public interface CustomOAuth2UserService extends OAuth2UserService { + OAuth2User loadUser(OAuth2UserRequest userRequest); + + User findUserByEmail(String email); + + Optional processOAuthPostLogin(OAuth2User oAuth2User); + + OAuth2User loadUserByProvider(String provider, String code); +} + +// OAuth2 프로세스는 사용자가 예를 들어 Google이나 Facebook 같은 외부 서비스를 사용하여 로그인할 때 발생함 +// 사용자가 이러한 서비스를 통해 로그인을 시도하면, +// 이 서비스는 사용자의 정보(예: 이름, 이메일, 프로필 사진 등)를 포함하는 OAuth2UserRequest를 생성하여 애플리케이션에 보냄 +// OAuth2UserRequest는 OAuth2UserService에 의해 처리되며, +// 이 서비스는 외부 시스템에서 제공하는 정보를 기반으로 OAuth2User 객체를 생성 +// 이 객체는 Spring Security 컨텍스트에서 사용자의 인증 정보로 활용 + +// OAuth2UserService를 상속받아 구현되어야 하는 이유 +// 외부 서비스로부터 받은 사용자 정보(OAuth2UserRequest)를 애플리케이션의 요구사항에 맞게 처리하고, +// 이를 OAuth2User 객체로 변환하여 Spring Security에 통합하기 위해 \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/service/EmailService.java b/src/main/java/com/ssg/starroad/user/service/EmailService.java new file mode 100644 index 0000000..b2bd066 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/EmailService.java @@ -0,0 +1,8 @@ +package com.ssg.starroad.user.service; + +import jakarta.mail.MessagingException; + +public interface EmailService { + void sendEmail(String to, String subject, String text) throws MessagingException; +} + diff --git a/src/main/java/com/ssg/starroad/user/service/FollowService.java b/src/main/java/com/ssg/starroad/user/service/FollowService.java index a843e3f..3886d2d 100644 --- a/src/main/java/com/ssg/starroad/user/service/FollowService.java +++ b/src/main/java/com/ssg/starroad/user/service/FollowService.java @@ -6,9 +6,9 @@ import java.util.List; public interface FollowService { - public long getCountByFromUserId(Long userId); - public long getCountByToUserId(Long userId); - public List getFollowsByToUserId(Long toUserId); - public List getFollowsByFromUserId(Long fromUserId); - public void deleteFollowingUser(Long userid,Long toUserId); + public long getCountByFromUserId(String email); + public long getCountByToUserId(String email); + public List getFollowsByToUserId(String email); + public List getFollowsByFromUserId(String email); + public void deleteFollowingUser(String email,Long toUserId); } diff --git a/src/main/java/com/ssg/starroad/user/service/RefreshTokenService.java b/src/main/java/com/ssg/starroad/user/service/RefreshTokenService.java new file mode 100644 index 0000000..84f25a3 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/RefreshTokenService.java @@ -0,0 +1,10 @@ +package com.ssg.starroad.user.service; + +import com.ssg.starroad.user.entity.RefreshToken; + +public interface RefreshTokenService { + + RefreshToken findByRefreshToken(String refreshToken); + + String createRefreshToken(Long userId); +} diff --git a/src/main/java/com/ssg/starroad/user/service/TokenService.java b/src/main/java/com/ssg/starroad/user/service/TokenService.java new file mode 100644 index 0000000..aebf71c --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/TokenService.java @@ -0,0 +1,5 @@ +package com.ssg.starroad.user.service; + +public interface TokenService { + String createNewAccessToken(String refreshToken); +} diff --git a/src/main/java/com/ssg/starroad/user/service/UserService.java b/src/main/java/com/ssg/starroad/user/service/UserService.java index 56a10b5..3036fa0 100644 --- a/src/main/java/com/ssg/starroad/user/service/UserService.java +++ b/src/main/java/com/ssg/starroad/user/service/UserService.java @@ -1,13 +1,50 @@ package com.ssg.starroad.user.service; -import java.util.Optional; + import com.ssg.starroad.user.dto.MypageDTO; -import org.springframework.web.multipart.MultipartFile; +import com.ssg.starroad.user.dto.RankUserDTO; +import com.ssg.starroad.user.dto.UserDTO; +import com.ssg.starroad.user.entity.User; +import org.springframework.security.core.userdetails.UserDetailsService; + +import java.util.List; +import java.util.Optional; + +public interface UserService extends UserDetailsService { + + User findById(Long userId); + + Optional findByEmail(String email); + + Long save(UserDTO userDTO); + + boolean validateUser(String email, String password); + + boolean isEmailDuplicate(String email); // 이메일 중복 검사 + + boolean isNicknameDuplicate(String nickname); // 닉네임 중복 검사 + + void registerUser(UserDTO userDTO); + + boolean updatePassword(String email, String newPassword); + + void updateUserProfile(User user, UserDTO userDTO); + + void inactiveUser(String email); -public interface UserService { Optional findNicknameById(Long id); - public MypageDTO getMypage(Long id); - public void saveProfileimg(Long id, String path); - public String getProfileimg(Long id); - public void deleteProfileimg(Long id); + + public MypageDTO getMypage(String email); + + public void saveProfileimg(String email, String path); + + public String getProfileimg(String email); + + public void deleteProfileimg(String email); + + public List getRankUser(String email); + + public List getAllUser(String email); + + public String addFollowUser(String userName, String email); } diff --git a/src/main/java/com/ssg/starroad/user/service/impl/AuthServiceImpl.java b/src/main/java/com/ssg/starroad/user/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..62f3056 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/impl/AuthServiceImpl.java @@ -0,0 +1,45 @@ +package com.ssg.starroad.user.service.impl; + +import com.ssg.starroad.user.entity.AuthCode; +import com.ssg.starroad.user.repository.AuthCodeRepository; +import com.ssg.starroad.user.service.AuthService; +import com.ssg.starroad.user.service.EmailService; +import jakarta.mail.MessagingException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.Random; + +@Service +public class AuthServiceImpl implements AuthService { + + private final AuthCodeRepository authCodeRepository; + private final EmailService emailService; + private final Random random = new Random(); + + @Autowired + public AuthServiceImpl(AuthCodeRepository authCodeRepository, EmailService emailService) { + this.authCodeRepository = authCodeRepository; + this.emailService = emailService; + } + + @Override + public void sendAuthCode(String email) throws MessagingException { + int authCode = random.nextInt(9000) + 1000; // 4자리 랜덤 숫자 생성 + AuthCode code = new AuthCode(email, authCode); + authCodeRepository.save(code); + + emailService.sendEmail(email, "인증 코드", "인증 코드는 " + authCode + " 입니다."); + } + + @Override + public boolean verifyAuthCode(String email, int authCode) { + Optional storedCode = authCodeRepository.findById(email); + if (storedCode.isPresent() && storedCode.get().getCode() == authCode) { + authCodeRepository.delete(storedCode.get()); + return true; + } + return false; + } +} diff --git a/src/main/java/com/ssg/starroad/user/service/impl/CustomOAuth2UserServiceImpl.java b/src/main/java/com/ssg/starroad/user/service/impl/CustomOAuth2UserServiceImpl.java new file mode 100644 index 0000000..7b3ed84 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/impl/CustomOAuth2UserServiceImpl.java @@ -0,0 +1,189 @@ +package com.ssg.starroad.user.service.impl; + +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.user.dto.OAuth2UserInfoDTO; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.enums.ActiveStatus; +import com.ssg.starroad.user.enums.ProviderType; +import com.ssg.starroad.user.repository.UserRepository; +import com.ssg.starroad.user.service.CustomOAuth2UserService; +import com.ssg.starroad.user.service.RefreshTokenService; +import com.ssg.starroad.user.service.oauth.OAuth2UserData; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.*; + +@RequiredArgsConstructor +@Service +public class CustomOAuth2UserServiceImpl extends DefaultOAuth2UserService implements CustomOAuth2UserService { + + private final UserRepository userRepository; + private final TokenProvider tokenProvider; + private final RefreshTokenService refreshTokenService; + private final ClientRegistrationRepository clientRegistrationRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + OAuth2User oAuth2User = super.loadUser(userRequest); + String provider = userRequest.getClientRegistration().getRegistrationId(); + String providerId = getProviderId(oAuth2User, provider); + + OAuth2UserInfoDTO userInfoDTO = OAuth2UserInfoDTO.fromOAuth2User(oAuth2User, provider, providerId); + + Optional existingUser = userRepository.findByEmail(userInfoDTO.getEmail()); + User user; + if (existingUser.isPresent()) { + user = updateExistingUser(existingUser.get(), userInfoDTO); + } else { + String nickname = userInfoDTO.getNickname() != null ? userInfoDTO.getNickname() : generateTempNickname(); + user = User.builder() + .email(userInfoDTO.getEmail()) + .name(userInfoDTO.getName()) + .nickname(nickname) + .providerId(userInfoDTO.getProviderId()) + .providerType(userInfoDTO.getProviderType()) + .activeStatus(ActiveStatus.ACTIVE) + .build(); + user = userRepository.save(user); + } + + String accessToken = tokenProvider.generateAccessToken(user); + String refreshToken = refreshTokenService.createRefreshToken(user.getId()); + + // Create a response object that includes the accessToken, refreshToken, and email + Map response = new HashMap<>(); + response.put("accessToken", accessToken); + response.put("refreshToken", refreshToken); + response.put("email", user.getEmail()); + + return new DefaultOAuth2User( + oAuth2User.getAuthorities(), + response, + "email" + ); + } + + private String generateTempNickname() { + long unixTimestamp = System.currentTimeMillis() / 1000L; + String timestampStr = String.valueOf(unixTimestamp); + String lastSixDigits = timestampStr.length() > 6 ? timestampStr.substring(timestampStr.length() - 6) : timestampStr; + return "star" + lastSixDigits; + } + + private String getProviderId(OAuth2User oAuth2User, String provider) { + switch (provider.toUpperCase()) { + case "GOOGLE": + return oAuth2User.getAttribute("sub"); + case "KAKAO": + Long kakaoId = oAuth2User.getAttribute("id"); + return kakaoId != null ? String.valueOf(kakaoId) : null; + case "NAVER": + Map response = oAuth2User.getAttribute("response"); + if (response != null) { + return (String) response.get("id"); + } else { + throw new IllegalArgumentException("OAuth2 제공자로부터 providerId를 가져올 수 없습니다."); + } + default: + throw new IllegalArgumentException("Unsupported provider: " + provider); + } + } + + @Override + public User findUserByEmail(String email) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("No user found with email " + email)); + } + + private User updateExistingUser(User existingUser, OAuth2UserInfoDTO userInfoDTO) { + return existingUser.toBuilder() + .name(userInfoDTO.getName()) + .nickname(userInfoDTO.getNickname() != null ? userInfoDTO.getNickname() : existingUser.getNickname()) + .imagePath(userInfoDTO.getImagePath()) + .providerType(userInfoDTO.getProviderType()) + .providerId(userInfoDTO.getProviderId()) + .build(); + } + + + @Override + public OAuth2User loadUserByProvider(String provider, String code) { + OAuth2AccessToken accessToken = getAccessToken(provider, code); + OAuth2UserRequest userRequest = createOAuth2UserRequest(provider, accessToken); + return loadUser(userRequest); + } + + private OAuth2AccessToken getAccessToken(String provider, String code) { + ClientRegistration clientRegistration = getClientRegistration(provider); + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", clientRegistration.getClientId()); + params.add("redirect_uri", clientRegistration.getRedirectUri()); + params.add("code", code); + params.add("client_secret", clientRegistration.getClientSecret()); + + HttpEntity> request = new HttpEntity<>(params, headers); + ResponseEntity response; + + try { + response = restTemplate.postForEntity(clientRegistration.getProviderDetails().getTokenUri(), request, Map.class); + System.out.println("Token request status: " + response.getStatusCode()); + System.out.println("Token request response body: " + response.getBody()); + } catch (Exception e) { + System.out.println("Failed to retrieve access token: " + e.getMessage()); + throw new OAuth2AuthenticationException("Failed to retrieve access token"); + } + + if (response.getStatusCode() == HttpStatus.OK) { + Map responseBody = response.getBody(); + System.out.println("Access token response: " + responseBody); // 응답 로그 추가 + String accessToken = (String) responseBody.get("access_token"); + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, accessToken, null, null); + } else { + System.out.println("Failed to retrieve access token: " + response.getStatusCode()); + throw new OAuth2AuthenticationException("Failed to retrieve access token"); + } + } + + private OAuth2UserRequest createOAuth2UserRequest(String provider, OAuth2AccessToken accessToken) { + ClientRegistration clientRegistration = getClientRegistration(provider); + return new OAuth2UserRequest(clientRegistration, accessToken); + } + + private ClientRegistration getClientRegistration(String provider) { + return clientRegistrationRepository.findByRegistrationId(provider); + } + + @Override + public Optional processOAuthPostLogin(OAuth2User oAuth2User) { + String email = oAuth2User.getAttribute("email"); + Optional userOptional = userRepository.findByEmail(email); + + if (userOptional.isEmpty()) { + OAuth2UserInfoDTO dto = OAuth2UserInfoDTO.fromOAuth2User(oAuth2User, oAuth2User.getAttribute("provider"), oAuth2User.getName()); + User user = dto.toEntity(); + user = userRepository.save(user); + return Optional.of(user); + } + + return userOptional; + } +} diff --git a/src/main/java/com/ssg/starroad/user/service/impl/EmailServiceImpl.java b/src/main/java/com/ssg/starroad/user/service/impl/EmailServiceImpl.java new file mode 100644 index 0000000..8b7d342 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/impl/EmailServiceImpl.java @@ -0,0 +1,30 @@ +package com.ssg.starroad.user.service.impl; + +import com.ssg.starroad.user.service.EmailService; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import jakarta.mail.MessagingException; + +@RequiredArgsConstructor +@Service +public class EmailServiceImpl implements EmailService { + + private final JavaMailSender javaMailSender; + + + @Override + public void sendEmail(String to, String subject, String text) throws MessagingException { + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true,"UTF-8"); + + helper.setTo(to); + helper.setSubject(subject); + helper.setText(text, true); + + javaMailSender.send(message); + } +} diff --git a/src/main/java/com/ssg/starroad/user/service/impl/FollowServiceImpl.java b/src/main/java/com/ssg/starroad/user/service/impl/FollowServiceImpl.java index 440f4dd..db8183b 100644 --- a/src/main/java/com/ssg/starroad/user/service/impl/FollowServiceImpl.java +++ b/src/main/java/com/ssg/starroad/user/service/impl/FollowServiceImpl.java @@ -4,6 +4,7 @@ import com.ssg.starroad.user.entity.Follow; import com.ssg.starroad.user.entity.User; import com.ssg.starroad.user.repository.FollowRepository; +import com.ssg.starroad.user.repository.UserRepository; import com.ssg.starroad.user.service.FollowService; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -17,22 +18,27 @@ public class FollowServiceImpl implements FollowService { private final FollowRepository followRepository; + private final UserRepository userRepository; - public long getCountByFromUserId(Long userId) { + public long getCountByFromUserId(String email) { + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); return followRepository.countByFromUserId(userId); } - public long getCountByToUserId(Long userId) { + public long getCountByToUserId(String email) { + Long userId = userRepository.findByEmail(email).orElseThrow().getId(); return followRepository.countByToUserId(userId); } - public List getFollowsByFromUserId(Long fromUserId) { + public List getFollowsByFromUserId(String email) { + Long fromUserId = userRepository.findByEmail(email).orElseThrow().getId(); List follows = followRepository.findToUsersByFromUserId(fromUserId); return follows.stream() .map(this::convertToDTO) .collect(Collectors.toList()); } - public List getFollowsByToUserId(Long toUserId) { + public List getFollowsByToUserId(String email) { + Long toUserId = userRepository.findByEmail(email).orElseThrow().getId(); List follows = followRepository.findFromUsersByToUserId(toUserId); return follows.stream() .map(this::convertToDTO) @@ -50,7 +56,8 @@ private FollowDTO convertToDTO(User user) { @Transactional @Override - public void deleteFollowingUser(Long userid,Long toUserId) { + public void deleteFollowingUser(String email,Long toUserId) { + Long userid = userRepository.findByEmail(email).orElseThrow().getId(); followRepository.deleteByFromUserIdAndToUserId(userid, toUserId); } diff --git a/src/main/java/com/ssg/starroad/user/service/impl/RefreshTokenServiceImpl.java b/src/main/java/com/ssg/starroad/user/service/impl/RefreshTokenServiceImpl.java new file mode 100644 index 0000000..cc215d2 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/impl/RefreshTokenServiceImpl.java @@ -0,0 +1,70 @@ +package com.ssg.starroad.user.service.impl; + +import com.ssg.starroad.common.util.TokenGenerator; +import com.ssg.starroad.user.entity.RefreshToken; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.repository.RefreshTokenRepository; +import com.ssg.starroad.user.repository.UserRepository; +import com.ssg.starroad.user.service.RefreshTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class RefreshTokenServiceImpl implements RefreshTokenService { + + private final RefreshTokenRepository refreshTokenRepository; + private final UserRepository userRepository; + + // RefreshToken에서 직접적으로 userId를 접근할 필요 없이 관련된 User 객체를 통해 필요한 정보 얻을 수 있음 + + @Override + public RefreshToken findByRefreshToken(String refreshToken) { + return refreshTokenRepository.findByToken(refreshToken) + .orElseThrow(() -> new IllegalArgumentException("Unexpected token")); + } + + // 사용자가 로그인할 때마다 이 메소드를 호출하여 + // 사용자의 세션에 대한 유효성을 연장하기 위한 새로운 리프레시 토큰을 발급 + @Override + public String createRefreshToken(Long userId) { + // User 객체를 가져오는 로직 + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + userId)); + + // 리프레시 토큰의 만료 시간 설정 + Instant expiryDate = Instant.now().plusSeconds(7 * 24 * 3600); // 7일 후 만료 + + // 고유한 토큰 값을 생성 + String newToken = generateUniqueToken(); + + // 기존의 리프레시 토큰이 있는지 확인 + Optional existingTokenOptional = refreshTokenRepository.findByUser_Id(userId); + + if (existingTokenOptional.isPresent()) { + // 기존 토큰이 존재하면 업데이트 + RefreshToken existingToken = existingTokenOptional.get(); + existingToken.updateToken(newToken); + existingToken.setExpiryDate(expiryDate); + refreshTokenRepository.save(existingToken); + return existingToken.getToken(); + } else { + // 기존 토큰이 존재하지 않으면 새로 생성 + RefreshToken refreshToken = new RefreshToken(user, newToken, expiryDate); + refreshTokenRepository.save(refreshToken); + return refreshToken.getToken(); + } + } + + // 고유한 토큰 값을 생성하는 데 사용. 특히 리프레시 토큰 내에서 고유성을 보장하는 데 사용 + // SecureRandom을 사용하여 예측이 불가능한 안전한 토큰을 생성 + // 이 토큰은 주로 리프레시 토큰의 일부로 사용되어 토큰 자체의 유일성을 보장 + public String generateUniqueToken() { + return TokenGenerator.generateUniqueToken(); + } +} + + diff --git a/src/main/java/com/ssg/starroad/user/service/impl/TokenServiceImpl.java b/src/main/java/com/ssg/starroad/user/service/impl/TokenServiceImpl.java new file mode 100644 index 0000000..176f487 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/impl/TokenServiceImpl.java @@ -0,0 +1,40 @@ +package com.ssg.starroad.user.service.impl; + +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.service.RefreshTokenService; +import com.ssg.starroad.user.service.TokenService; +import com.ssg.starroad.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@RequiredArgsConstructor +@Service +public class TokenServiceImpl implements TokenService { + private final TokenProvider tokenProvider; + private final RefreshTokenService refreshTokenService; + private final UserService userService; + +// RefreshToken 엔티티는 User 엔티티와 일대일 관계 +// user 필드를 통해 User 엔티티를 참조 +// userId를 직접 가져오는 대신, User 객체를 통해 ID를 가져와야 + @Override + public String createNewAccessToken(String refreshToken) { + + // 토큰 유효성 검사에 실패하면 예외 발생 + if(!tokenProvider.validToken(refreshToken)){ + throw new IllegalArgumentException("Unexpected token"); + } + + // RefreshToken 객체를 찾고, 연결된 User 객체의 ID를 가져오기 + User user = refreshTokenService.findByRefreshToken(refreshToken).getUser(); + // User ID로 User 객체를 조회하지 않고, 바로 User 객체를 사용하기 + return tokenProvider.generateToken(user, Duration.ofHours(2)); + } +} + +// 전달받은 리프레시 토큰으로 토큰 유효성 검사를 진행 +// 유효한 토큰일 때 리프레스 토큰으로 사용자 id를 찾음 +// 토큰 제공자의 generateToken 메서드 호출해 새로운 액세스 토큰 생성 \ No newline at end of file diff --git a/src/main/java/com/ssg/starroad/user/service/impl/UserServiceImpl.java b/src/main/java/com/ssg/starroad/user/service/impl/UserServiceImpl.java index 9aa3076..470c0db 100644 --- a/src/main/java/com/ssg/starroad/user/service/impl/UserServiceImpl.java +++ b/src/main/java/com/ssg/starroad/user/service/impl/UserServiceImpl.java @@ -1,21 +1,135 @@ package com.ssg.starroad.user.service.impl; -import com.ssg.starroad.user.entity.User; + import com.ssg.starroad.user.dto.MypageDTO; +import com.ssg.starroad.user.dto.RankUserDTO; +import com.ssg.starroad.user.dto.UserDTO; +import com.ssg.starroad.user.entity.Follow; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.enums.ActiveStatus; +import com.ssg.starroad.user.enums.ProviderType; +import com.ssg.starroad.user.repository.FollowRepository; import com.ssg.starroad.user.repository.UserRepository; import com.ssg.starroad.user.service.UserService; import lombok.RequiredArgsConstructor; import org.modelmapper.ModelMapper; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.Comparator; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + private final BCryptPasswordEncoder passwordEncoder; private final ModelMapper modelMapper; + private final UrlBasedCorsConfigurationSource corsConfigurationSource; + private final FollowRepository followRepository; + + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Optional userOptional = userRepository.findByEmail(email); + User user = userOptional.orElseThrow(() -> + new UsernameNotFoundException("User not found with email: " + email)); + + return user; // User 엔티티 반환 + } + + // 리프레시 토큰을 이용해 새로운 액세스 토큰을 생성할 때 해당 사용자의 정보를 db에서 조회하기 위해 + // User 클래스에서 id 필드에 해당하며, @Id 어노테이션으로 지정 + @Override + public User findById(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + userId)); + } + + // OAuth2에서 제공하는 이메일은 유일 값이므로 이 메서드를 사용해 유저 찾기 가능 + @Override + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + + @Override + public Long save(UserDTO userDTO) { + + User user = User.builder() + .email(userDTO.getEmail()) + .password(passwordEncoder.encode(userDTO.getPassword())) + .name(userDTO.getName()) + .nickname(userDTO.getNickname()) + .imagePath(userDTO.getImagePath()) + .gender(userDTO.getGender()) + .birth(userDTO.getBirth()) + .phone(userDTO.getPhone()) + .providerType(ProviderType.LOCAL) + .providerId(userDTO.getProviderId()) + .reviewExp(userDTO.getReviewExp()) + .point(userDTO.getPoint()) + .activeStatus(userDTO.getActiveStatus()) + .build(); + return userRepository.save(user).getId(); + } + + public boolean validateUser(String email, String password) { + + Optional userOpt = userRepository.findByEmail(email); + if (userOpt.isEmpty()) { + System.out.println("No user found with email: " + email); + return false; + } + + User user = userOpt.get(); + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + boolean passwordMatches = encoder.matches(password, user.getPassword()); + + if (!passwordMatches) { + System.out.println("Password mismatch for user: " + email); + } + + return passwordMatches; + } + + @Override + public boolean isEmailDuplicate(String email) { + return userRepository.findByEmail(email).isPresent(); + } + + @Override + public boolean isNicknameDuplicate(String nickname) { + return userRepository.existsByNickname(nickname); + } + + @Override + public void registerUser(UserDTO userDTO) { + User user = User.builder() + .name(userDTO.getName()) + .nickname(userDTO.getNickname()) + .email(userDTO.getEmail()) + .password(passwordEncoder.encode(userDTO.getPassword())) + .gender(userDTO.getGender()) + .birth(userDTO.getBirth()) + .phone(userDTO.getPhone()) + .providerType(ProviderType.LOCAL) + .providerId(null) + .reviewExp(0) + .point(0) + .activeStatus(ActiveStatus.ACTIVE) + .build(); + + userRepository.save(user); + } @Override public Optional findNicknameById(Long id) { @@ -23,27 +137,185 @@ public Optional findNicknameById(Long id) { return userOptional.map(User::getNickname); } - public MypageDTO getMypage(Long id) { - return userRepository.findById(id).map(info -> modelMapper.map(info, MypageDTO.class)).orElse(null); + public MypageDTO getMypage(String email) { + User user = userRepository.findByEmail(email).orElseThrow(); + return MypageDTO.builder() + .nickname(user.getNickname()) + .profileImg(user.getImagePath()) + .reviewExp(user.getReviewExp()) + .name(user.getUserName()) + .point(user.getPoint()) + .build(); } @Override - public void saveProfileimg(Long id, String path) { - User user=userRepository.findById(id).orElseThrow(); + public void saveProfileimg(String email, String path) { + + User user = userRepository.findByEmail(email).orElseThrow(); user.setProfileimgPath(path); userRepository.save(user); } @Override - public String getProfileimg(Long id) { - User user =userRepository.findById(id).orElseThrow(); + public boolean updatePassword(String email, String newPassword) { + System.out.println("Searching for user with email: " + email); // 로그 추가 + User user = userRepository.findByEmail(email).orElse(null); + if (user != null) { + System.out.println("User found: " + user); // 로그 추가 + user.updatePassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + return true; + } + System.out.println("User not found with email: " + email); // 로그 추가 + return false; + } + + @Transactional + @Override + public void updateUserProfile(User user, UserDTO userDTO) { + String encodedPassword = user.getPassword(); // 기존 비밀번호로 초기화 + + if (userDTO.getPassword() != null && !userDTO.getPassword().isEmpty()) { + // 비밀번호가 비어있지 않을 경우에만 비밀번호 업데이트 + encodedPassword = passwordEncoder.encode(userDTO.getPassword()); + } + + user.updateProfile(userDTO.getNickname(), encodedPassword); + + userRepository.save(user); + System.out.println("Updated User: " + user); + System.out.println("Updated Nickname: " + user.getNickname()); + System.out.println("Updated Password: " + user.getPassword()); + } + + @Override + public void inactiveUser(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + user.changeActiveStatus(ActiveStatus.INACTIVE); + userRepository.save(user); + } + + public String getProfileimg(String email) { + User user = userRepository.findByEmail(email).orElseThrow(); return user.getImagePath(); } @Override - public void deleteProfileimg(Long id) { - User user =userRepository.findById(id).orElseThrow(); + public void deleteProfileimg(String email) { + User user = userRepository.findByEmail(email).orElseThrow(); user.setProfileimgPath(""); userRepository.save(user); } + + @Override + public List getRankUser(String email) { + List users = userRepository.findAll(); + Long currentUserId = findByEmail(email).get().getId(); + + // id 값과 동일한 값을 제외하고 나머지 사용자들 선택 + List filteredUsers = users.stream() + .filter(user -> !user.getId().equals(currentUserId)) + .collect(Collectors.toList()); + + // reviewExp 필드를 기준으로 내림차순 정렬 + List topUsers = filteredUsers.stream() + .sorted(Comparator.comparingInt(User::getReviewExp).reversed()) + .collect(Collectors.toList()); + + topUsers.forEach(System.out::println); + + // User 객체를 RankUserDTO 객체로 변환 + List rankUserDTOS = topUsers.stream() + .map(user -> { + boolean isFollowed = followRepository.existsByFromUserIdAndToUserId(currentUserId, user.getId()); + return RankUserDTO.builder() + .id(user.getId()) + .name(user.getName()) + .nickname(user.getNickname()) + .email(user.getEmail()) + .imagePath(user.getImagePath()) + .reviewExp(user.getReviewExp()) + .point(user.getPoint()) + .activeStatus(user.getActiveStatus()) + .isFollowed(isFollowed) + .build(); + }) + .collect(Collectors.toList()); + + return rankUserDTOS; + } + + + @Override + public List getAllUser(String email) { + List users = userRepository.findAll(); + + Long id = findByEmail(email).get().getId(); + + // id 값과 동일한 값을 제외하고 나머지 사용자들 선택 + List filteredUsers = users.stream() + .filter(user -> !user.getId().equals(id)) + .collect(Collectors.toList()); + + // reviewExp 필드를 기준으로 상위 3명의 사용자만 선택 + List topUsers = users.stream() + .sorted(Comparator.comparingInt(User::getReviewExp).reversed()) + .collect(Collectors.toList()); + + // 상위 3명의 사용자 목록을 출력 또는 다른 작업 수행 + topUsers.forEach(System.out::println); + + // User 객체를 RankUserDTO 객체로 변환 + List rankUserDTOS = topUsers.stream() + .map(user -> RankUserDTO.builder() + .id(user.getId()) + .name(user.getName()) + .nickname(user.getNickname()) + .email(user.getEmail()) + .imagePath(user.getImagePath()) + .reviewExp(user.getReviewExp()) + .point(user.getPoint()) + .activeStatus(user.getActiveStatus()) + .build()) + .collect(Collectors.toList()); + + return rankUserDTOS; + } + + + @Transactional + public String addFollowUser(String userName, String email) { + System.out.println("userName, email : " + userName + ", " + email); + Optional fromUserOpt = userRepository.findByEmail(email); + Optional toUserOpt = userRepository.findBynickname(userName); + System.out.println("fromUserOpt, toUserOpt : " + fromUserOpt + ", " + toUserOpt); + if (!fromUserOpt.isPresent() || !toUserOpt.isPresent()) { + return "유효하지 않은 사용자 정보입니다."; + } + + User fromUser = fromUserOpt.get(); + User toUser = toUserOpt.get(); + + System.out.println("fromUser, toUser : " + fromUser + ", " + toUser); + System.out.println("toUser tostring : " + toUser.toString()); + + Optional existingFollow = followRepository.findByFromUserAndToUser(fromUser, toUser); + + // 기존 팔로우 관계가 존재하면 삭제, 그렇지 않으면 새로운 팔로우 관계 추가 + if (existingFollow.isPresent()) { + System.out.println("해제 실행"); + followRepository.delete(existingFollow.get()); + return "팔로우 관계가 해제되었습니다."; + } else { + Follow newFollow = Follow.builder() + .fromUser(fromUser) + .toUser(toUser) + .build(); + System.out.println("저장 실행"); + followRepository.save(newFollow); + return "새로운 팔로우 관계가 생성되었습니다."; + } + } } diff --git a/src/main/java/com/ssg/starroad/user/service/oauth/OAuth2UserData.java b/src/main/java/com/ssg/starroad/user/service/oauth/OAuth2UserData.java new file mode 100644 index 0000000..4fd6deb --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/oauth/OAuth2UserData.java @@ -0,0 +1,9 @@ +package com.ssg.starroad.user.service.oauth; + +import com.ssg.starroad.user.entity.User; +import org.springframework.security.oauth2.core.user.OAuth2User; + + +public interface OAuth2UserData { + User oauth2Info(OAuth2User oAuth2User, String providerType, String providerId); +} diff --git a/src/main/java/com/ssg/starroad/user/service/oauth/OAuth2UserInfo.java b/src/main/java/com/ssg/starroad/user/service/oauth/OAuth2UserInfo.java new file mode 100644 index 0000000..1859722 --- /dev/null +++ b/src/main/java/com/ssg/starroad/user/service/oauth/OAuth2UserInfo.java @@ -0,0 +1,15 @@ +package com.ssg.starroad.user.service.oauth; + +import com.ssg.starroad.user.dto.OAuth2UserInfoDTO; +import com.ssg.starroad.user.entity.User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +public class OAuth2UserInfo implements OAuth2UserData { + @Override + public User oauth2Info(OAuth2User oAuth2User, String providerType, String providerId) { + OAuth2UserInfoDTO dto = OAuth2UserInfoDTO.fromOAuth2User(oAuth2User, providerType, providerId); + return dto.toEntity(); + } +} \ No newline at end of file diff --git a/src/test/java/com/ssg/starroad/board/boardserviceTest.java b/src/test/java/com/ssg/starroad/board/boardserviceTest.java new file mode 100644 index 0000000..904d464 --- /dev/null +++ b/src/test/java/com/ssg/starroad/board/boardserviceTest.java @@ -0,0 +1,31 @@ +package com.ssg.starroad.board; + +import com.ssg.starroad.board.service.BoardImageService; +import com.ssg.starroad.board.service.BoardService; +import com.ssg.starroad.coupon.repository.CouponRepositoryTest; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class boardserviceTest { + private static final Logger log = LogManager.getLogger(CouponRepositoryTest.class); + + + @Autowired + private BoardService boardService; + + @Autowired + private BoardImageService boardImageService; + + @Test + public void testBoardlist(){ + log.info(boardService.getEventBoards()); + } + @Test + public void testBoardImage(){ + log.info(boardImageService.getEventContent(1L)); + } +} diff --git a/src/test/java/com/ssg/starroad/config/jwt/JwtFactory.java b/src/test/java/com/ssg/starroad/config/jwt/JwtFactory.java new file mode 100644 index 0000000..275d58d --- /dev/null +++ b/src/test/java/com/ssg/starroad/config/jwt/JwtFactory.java @@ -0,0 +1,57 @@ +package com.ssg.starroad.config.jwt; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.Builder; +import lombok.Getter; + +import java.time.Duration; +import java.util.Date; +import java.util.Map; + +import static java.util.Collections.emptyMap; + +// 테스트 실행용 Mocking(모킹) 객체 => 가짜 객체 +@Getter +public class JwtFactory { + + private String subject = "gildong@example.com"; + private Date issuedAt = new Date(); + private Date expiration = new Date(new Date().getTime() + Duration.ofDays(14).toMillis()); + private Map claims = emptyMap(); + // 비어 있는 맵 반환 클레임 포함하지 않는 기본 설정으로 토큰 생성할 때 사용됨 + // 테스트 시나리오에서 클레임 추가하기 전 기본 상태 제공 + + @Builder + // 빌더 패턴을 사용해 인스턴스 초기화 + // 파라미터가 null이 아닌 경우에만 해당 파라미터로 필드를 초기화하고 + // 그렇지 않으면 기본값을 사용 + public JwtFactory(String subject, Date issuedAt, Date expiration, Map claims) { + this.subject = subject != null ? subject : this.subject; + this.issuedAt = issuedAt != null ? issuedAt : this.issuedAt; + this.expiration = expiration != null ? expiration : this.expiration; + this.claims = claims != null ? claims : this.claims; + } + + // 기본 생성자를 통해 객체 생성 + // 추가 설정 없이 기본값으로 토큰 생성 + public static JwtFactory withDefaultValues() { + return JwtFactory.builder().build(); + } + + // jwt 토큰 생성 메소드 + // jwt 표준 필드와 함께 사용자 정의 클레임을 토큰에 추가 + public String createToken(JwtProperties jwtProperties) { + return Jwts.builder() + .setSubject(subject) + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer(jwtProperties.getIssuer()) + .setIssuedAt(issuedAt) + .setExpiration(expiration) + .addClaims(claims) // 수정된 부분: 다양한 사용자 정의 클레임을 포함할 수 있도록 수정 + .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey()) + .compact(); + } +} + diff --git a/src/test/java/com/ssg/starroad/config/jwt/TokenProviderTest.java b/src/test/java/com/ssg/starroad/config/jwt/TokenProviderTest.java new file mode 100644 index 0000000..092e869 --- /dev/null +++ b/src/test/java/com/ssg/starroad/config/jwt/TokenProviderTest.java @@ -0,0 +1,127 @@ +package com.ssg.starroad.config.jwt; + + +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.enums.ActiveStatus; +import com.ssg.starroad.user.enums.Gender; +import com.ssg.starroad.user.enums.ProviderType; +import com.ssg.starroad.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class TokenProviderTest { + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtProperties jwtProperties; + + @BeforeEach + @DisplayName("generateToken(): 유저 정보와 만료 기간을 전달해 토큰을 만들 수 있다.") + @Test + void generateToken() { + // given : 테스트를 위한 사용자 정보를 설정하고 데이터베이스에 저장 + User testUser = userRepository.save(User.builder() + .name("홍길동") + .nickname("gildong123") + .password("password@123") // 비밀번호 패턴을 준수해야 합니다. + .email("gildong@example.com") + .gender(Gender.MALE) + .birth(LocalDate.of(1990, 1, 1)) + .phone("01012345678") + .providerType(ProviderType.GOOGLE) + .providerId("1234567890") + .activeStatus(ActiveStatus.ACTIVE) + .build()); + // when : 토큰 생성 메서드를 호출하여 JWT 토큰을 생성 + String token = tokenProvider.generateToken(testUser, Duration.ofDays(14)); + + // then : 생성된 토큰에서 사용자 ID를 추출하여 저장된 사용자의 ID와 비교 + // 토큰 복호화. 토큰 만들 때 클레임으로 넣어둔 id 값이 given 절에서 만든 사용자 id랑 동일한지 확인 + Long userId = tokenProvider.getUserId(token); + assertThat(userId).isEqualTo(testUser.getId()); + } + + @DisplayName("validToken(): 만료된 토큰인 경우에 유효성 검증에 실패한다.") + @Test + void validToken_invalidToken() { + // given : 과거 날짜로 설정된 토큰의 만료 시간을 설정 + Date pastDate = new Date(new Date().getTime() - 86400000); // 하루 지났음 + JwtFactory jwtFactory = JwtFactory.builder() + .expiration(pastDate) + .build(); + String token = jwtFactory.createToken(jwtProperties); + + // when : 토큰의 유효성 검증을 수행 + boolean result = tokenProvider.validToken(token); + + // then : 토큰이 유효하지 않으므로 결과는 false + assertThat(result).isFalse(); + } + + + @DisplayName("validToken(): 유효한 토큰인 경우에 유효성 검증에 성공한다.") + @Test + void validToken_validToken() { + // given : 유효성 검증에 사용할 기본 설정의 토큰 생성 + JwtFactory jwtFactory = JwtFactory.withDefaultValues(); + String token = jwtFactory.createToken(jwtProperties); + + // when : 토큰의 유효성 검증을 수행 + boolean result = tokenProvider.validToken(token); + + // then : 토큰이 유효하므로 결과는 true + assertThat(result).isTrue(); + } + + + @DisplayName("getAuthentication(): 토큰 기반으로 인증정보를 가져올 수 있다.") + @Test + void getAuthentication() { + // given : 인증 정보 생성을 위해 사용자를 저장하고 토큰을 생성 + User testUser = userRepository.save(User.builder() + .email("gildong@example.com") + .password("password@123") + .build()); + + // when : 생성된 토큰을 사용하여 인증 정보를 얻음 + String token = JwtFactory.withDefaultValues().createToken(jwtProperties); + Authentication authentication = tokenProvider.getAuthentication(token); + + // then : 인증 정보에서 얻은 사용자 이름이 저장된 사용자의 이메일과 일치하는지 확인 + assertThat(((UserDetails) authentication.getPrincipal()).getUsername()).isEqualTo(testUser.getEmail()); + } + + @Rollback(false) // 특정 테스트에서 롤백 비활성화 예시 + @DisplayName("getUserId(): 토큰으로 유저 ID를 가져올 수 있다.") + @Test + void getUserId() { + // given : 테스트를 위해 사용자를 저장하고 토큰을 생성 + User testUser = userRepository.save(User.builder().email("gildong@example.com").build()); + String token = tokenProvider.generateToken(testUser, java.time.Duration.ofDays(1)); + // Duration expiredAt : java.time.Duration.ofDays(1)을 사용하여 토큰의 유효 기간을 1일로 설정 + // when : 생성된 토큰으로부터 사용자 ID를 추출 + Long userIdByToken = tokenProvider.getUserId(token); + + // then : 추출된 ID가 저장된 사용자의 ID와 일치하는지 확인 + assertThat(userIdByToken).isEqualTo(testUser.getId()); + } +} \ No newline at end of file diff --git a/src/test/java/com/ssg/starroad/coupon/repository/CouponRepositoryTest.java b/src/test/java/com/ssg/starroad/coupon/repository/CouponRepositoryTest.java index 952ae3f..7cc2d01 100644 --- a/src/test/java/com/ssg/starroad/coupon/repository/CouponRepositoryTest.java +++ b/src/test/java/com/ssg/starroad/coupon/repository/CouponRepositoryTest.java @@ -39,29 +39,29 @@ public class CouponRepositoryTest { @Autowired private UserRepository userRepository; +} +// @Test +// public void testSaveCoupon() { +// // 쿠폰 엔티티 생성 +// Coupon coupon = new Coupon(null, "Complex2", "Food", "Summer Sale", 1000, 10, 5000, 10000, "Active", LocalDate.now().plusDays(7)); +// +// // 쿠폰 저장 +// Coupon savedCoupon = couponRepository.save(coupon); +// +// // 로그 출력 +// log.info("Saved Coupon: {}", savedCoupon); +// +// // 검증 +// assertNotNull(savedCoupon); +// assertNotNull(savedCoupon.getId()); +// } - @Test - public void testSaveCoupon() { - // 쿠폰 엔티티 생성 - Coupon coupon = new Coupon(null, "Complex2", "Food", "Summer Sale", 1000, 10, 5000, 10000, "Active", LocalDate.now().plusDays(7)); - - // 쿠폰 저장 - Coupon savedCoupon = couponRepository.save(coupon); - - // 로그 출력 - log.info("Saved Coupon: {}", savedCoupon); - - // 검증 - assertNotNull(savedCoupon); - assertNotNull(savedCoupon.getId()); - } - - @Test - public void testIssueCoupon() { - couponHistoryService.CouponUserAdd(4L, 3L); +// @Test +// public void testIssueCoupon() { +// couponHistoryService.CouponUserAdd(4L, 3L); // log.info("check issue coupon: {}", couponHistoryRepository.findById(1L)); - } -} +// } +//} // @Test // public void testfindByUserId(){ //// Long userId = 1L; diff --git a/src/test/java/com/ssg/starroad/coupon/service/CouponHistorySeviceTest.java b/src/test/java/com/ssg/starroad/coupon/service/CouponHistorySeviceTest.java index 0c79902..5bd9d40 100644 --- a/src/test/java/com/ssg/starroad/coupon/service/CouponHistorySeviceTest.java +++ b/src/test/java/com/ssg/starroad/coupon/service/CouponHistorySeviceTest.java @@ -34,9 +34,9 @@ public void testupdateCouponUsage(){ couponHistoryService.CouponUsageModify(2L); log.info("coupon status: {} ",couponHistoryRepository.findById(2L)); } - @Test - public void testgetCouponsByUserID(){ - log.info("user coupons:{}",couponHistoryService.CouponsUserList(4L)); - } +// @Test +// public void testgetCouponsByUserID(){ +// log.info("user coupons:{}",couponHistoryService.CouponsUserList(4L)); +// } } diff --git a/src/test/java/com/ssg/starroad/review/repository/ReviewLikeRepositoryTest.java b/src/test/java/com/ssg/starroad/review/repository/ReviewLikeRepositoryTest.java new file mode 100644 index 0000000..da38167 --- /dev/null +++ b/src/test/java/com/ssg/starroad/review/repository/ReviewLikeRepositoryTest.java @@ -0,0 +1,20 @@ +package com.ssg.starroad.review.repository; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest +class ReviewLikeRepositoryTest { + + + @Autowired + private ReviewLikeRepository reviewLikeRepository; + + @Test + void booleanteest() { + assertThat(reviewLikeRepository.existsByUser_IdAndReview_Id(1L,43L)).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/com/ssg/starroad/review/service/impl/ReviewLikeServiceImplTest.java b/src/test/java/com/ssg/starroad/review/service/impl/ReviewLikeServiceImplTest.java new file mode 100644 index 0000000..52caddb --- /dev/null +++ b/src/test/java/com/ssg/starroad/review/service/impl/ReviewLikeServiceImplTest.java @@ -0,0 +1,30 @@ +package com.ssg.starroad.review.service.impl; + +import com.ssg.starroad.review.service.ReviewImageService; +import com.ssg.starroad.review.service.ReviewLikeService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class ReviewLikeServiceImplTest { + + + @Autowired + private ReviewLikeService reviewLikeService; + + private static final Logger log = LogManager.getLogger(ReviewLikeService.class); + @Test + @DisplayName("좋아요 누른 리뷰 체크하는테스트") + public void test(){ + log.info("좋아요테스트"); + System.out.println("좋아요테스트"); + log.info( reviewLikeService.getLikedReviewIdsByUserId(1L)); + log.info("!!!"); + } +} \ No newline at end of file diff --git a/src/test/java/com/ssg/starroad/reward/repository/RewardRepositoryTest.java b/src/test/java/com/ssg/starroad/reward/repository/RewardRepositoryTest.java index d7f4a01..1ad0096 100644 --- a/src/test/java/com/ssg/starroad/reward/repository/RewardRepositoryTest.java +++ b/src/test/java/com/ssg/starroad/reward/repository/RewardRepositoryTest.java @@ -17,9 +17,9 @@ public class RewardRepositoryTest { @Autowired private RewardRepository rewardRepository; - @Test - public void testSave() { - Reward reward = new Reward(null,"세번째별"); - rewardRepository.save(reward); - } +// @Test +// public void testSave() { +// Reward reward = new Reward(null,"세번째별"); +// rewardRepository.save(reward); +// } } diff --git a/src/test/java/com/ssg/starroad/reward/service/RewardServiceTest.java b/src/test/java/com/ssg/starroad/reward/service/RewardServiceTest.java index 86ace5b..f0ad81f 100644 --- a/src/test/java/com/ssg/starroad/reward/service/RewardServiceTest.java +++ b/src/test/java/com/ssg/starroad/reward/service/RewardServiceTest.java @@ -26,24 +26,24 @@ public class RewardServiceTest { @Autowired private ReviewImageService reviewImageService; - @Test - public void test() { - System.out.println("@@@@@@@"); - log.info(reviewImageService.getReviewImages(1L).toString()); - } - @Test - public void testRewardList(){ - log.info(rewardService.getRewardList()); - } - - @Test - public void testObtain(){ - RewardMemberDTO rewardMemberDTO = new RewardMemberDTO(4L,3L); - rewardHistoryService.addReward(rewardMemberDTO); - } - - @Test - public void testMyRewardList(){ - log.info(rewardHistoryService.getRewardHistory(4L).toString()); - } +// @Test +// public void test() { +// System.out.println("@@@@@@@"); +// log.info(reviewImageService.getReviewImages(1L).toString()); +// } +// @Test +// public void testRewardList(){ +// log.info(rewardService.getRewardList()); +// } +// +// @Test +// public void testObtain(){ +// RewardMemberDTO rewardMemberDTO = new RewardMemberDTO(4L,3L); +// rewardHistoryService.addReward(rewardMemberDTO); +// } +// +// @Test +// public void testMyRewardList(){ +// log.info(rewardHistoryService.getRewardHistory(4L).toString()); +// } } diff --git a/src/test/java/com/ssg/starroad/shop/controller/StoreControllerTest.java b/src/test/java/com/ssg/starroad/shop/controller/StoreControllerTest.java index df5106d..6c43487 100644 --- a/src/test/java/com/ssg/starroad/shop/controller/StoreControllerTest.java +++ b/src/test/java/com/ssg/starroad/shop/controller/StoreControllerTest.java @@ -22,11 +22,11 @@ public void teststoreController() { } - @Test - public void teststoreController2() { - log.info( - storeController.StoreGetWithReviewById(3L,1,20)); - } +// @Test +// public void teststoreController2() { +// log.info( +// storeController.StoreGetWithReviewById(1L,"hklee@example.com",0,20,)); +// } @Test public void teststoreController3() { diff --git a/src/test/java/com/ssg/starroad/shop/service/impl/StoreServiceImplTest.java b/src/test/java/com/ssg/starroad/shop/service/impl/StoreServiceImplTest.java index c508a11..027dcd0 100644 --- a/src/test/java/com/ssg/starroad/shop/service/impl/StoreServiceImplTest.java +++ b/src/test/java/com/ssg/starroad/shop/service/impl/StoreServiceImplTest.java @@ -39,12 +39,12 @@ void getStore() { log.info(stores.toString()); } - @Test - @DisplayName("Store단일 매장 전체 리뷰 작성메소드 테스트") - void StoreWithAllReviewTest() { - log.info(storeService.findStoreWithReview(2L,1,20)); - - } +// @Test +// @DisplayName("Store단일 매장 전체 리뷰 작성메소드 테스트") +// void StoreWithAllReviewTest() { +// log.info(storeService.findStoreWithReview(2L,"hklee@example.com",1,20)); +// +// } diff --git a/src/test/java/com/ssg/starroad/user/controller/TokenApiControllerTest.java b/src/test/java/com/ssg/starroad/user/controller/TokenApiControllerTest.java new file mode 100644 index 0000000..b5e03ca --- /dev/null +++ b/src/test/java/com/ssg/starroad/user/controller/TokenApiControllerTest.java @@ -0,0 +1,122 @@ +package com.ssg.starroad.user.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ssg.starroad.config.jwt.JwtFactory; +import com.ssg.starroad.config.jwt.JwtProperties; +import com.ssg.starroad.user.dto.CreateAccessTokenRequest; +import com.ssg.starroad.user.entity.RefreshToken; +import com.ssg.starroad.user.entity.User; +import com.ssg.starroad.user.enums.ActiveStatus; +import com.ssg.starroad.user.enums.Gender; +import com.ssg.starroad.user.enums.ProviderType; +import com.ssg.starroad.user.repository.RefreshTokenRepository; +import com.ssg.starroad.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.Map; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class TokenApiControllerTest { + + @Autowired + private MockMvc mockMvc; + // 웹 서버를 실행하지 않고도 스프링 MVC(웹 계층)의 동작을 시뮬레이션 할 수 있게 해주는 클래스 + + @Autowired + private ObjectMapper objectMapper; + // JSON 데이터와 Java 객체 간의 변환을 처리 + + @Autowired + private WebApplicationContext context; + // 스프링 애플리케이션 컨텍스트를 제공 + + @Autowired + private JwtProperties jwtProperties; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + // 테스트 실행 전에 필요한 초기 설정을 수행 + // MockMvc 인스턴스를 구성하고 데이터베이스를 초기화 + @BeforeEach + public void mockMvcSetUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + userRepository.deleteAll(); + refreshTokenRepository.deleteAll(); + } + + @DisplayName("createAccessToken: 새로운 액세스 토큰을 발급한다.") + @Test + public void createAccessToken() throws Exception { + // given + final String url = "/api/token/access-token"; + + // 테스트용 사용자를 생성하고 데이터베이스에 저장 + User testUser = userRepository.save(User.builder() + .email("user@example.com") + .password("Valid123$") // 비밀번호는 유효성 규칙에 맞춰 생성 + .name("John Doe") + .nickname("JD2024") // 닉네임은 유니크해야 함, 데이터베이스 상황에 따라 수정 필요 + .gender(Gender.MALE) // 성별은 ENUM 값 중 하나 + .birth(LocalDate.of(1990, 1, 1)) // LocalDate 객체로 생년월일 설정 + .phone("01012345678") // 하이픈 없는 전화번호 + .providerType(ProviderType.GOOGLE) // 인증 제공자 유형 + .providerId("123456") // 제공자 ID + .imagePath("/path/to/image.jpg") // 이미지 경로 + .reviewExp(0) // 리뷰 경험치 + .point(0) // 포인트 + .activeStatus(ActiveStatus.ACTIVE) // 활성 상태 + .build()); + + // 테스트 사용자의 ID를 사용하여 리프레시 토큰을 생성 + String refreshTokenString = JwtFactory.builder() + .claims(Map.of("id", testUser.getId())) + .build() + .createToken(jwtProperties); + + Instant expiryDate = Instant.now().plusSeconds(3600); // 만료 시간 설정 + + // 생성된 리프레시 토큰과 만료 시간을 설정하여 RefreshToken 인스턴스를 생성하고 저장 + RefreshToken refreshToken = new RefreshToken(testUser, refreshTokenString, expiryDate); + refreshTokenRepository.save(refreshToken); + + CreateAccessTokenRequest request = new CreateAccessTokenRequest(refreshTokenString); + final String requestBody = objectMapper.writeValueAsString(request); + + // when + ResultActions resultActions = mockMvc.perform(post(url) + .contentType(MediaType.APPLICATION_JSON_VALUE) // 요청의 컨텐트 타입을 JSON으로 설정 + .content(requestBody)); // 요청 본문에 CreateAccessTokenRequest 객체를 JSON 문자열로 변환하여 전달 + + // then + resultActions + .andExpect(status().isOk()) // HTTP 응답 상태 코드가 200(OK)인지 확인 + .andExpect(jsonPath("$.accessToken").isNotEmpty()); + // 응답 JSON 본문에서 accessToken 필드가 비어 있지 않은지 확인 + // $.accessToken은 JSON 응답에서 accessToken 속성에 접근하기 위한 경로 + + } +} + +// 클라이언트가 유효한 리프레시 토큰을 제공했을 때, +// 서버가 새 액세스 토큰을 생성하고 올바르게 반환하는지를 검증하는 과정을 자동화 \ No newline at end of file diff --git a/src/test/java/com/ssg/starroad/user/controller/UserControllerTest.java b/src/test/java/com/ssg/starroad/user/controller/UserControllerTest.java new file mode 100644 index 0000000..f5d0234 --- /dev/null +++ b/src/test/java/com/ssg/starroad/user/controller/UserControllerTest.java @@ -0,0 +1,143 @@ +package com.ssg.starroad.user.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ssg.starroad.config.jwt.TokenProvider; +import com.ssg.starroad.user.dto.LoginRequest; +import com.ssg.starroad.user.dto.UserDTO; +import com.ssg.starroad.user.enums.ActiveStatus; +import com.ssg.starroad.user.enums.Gender; +import com.ssg.starroad.user.enums.ProviderType; +import com.ssg.starroad.user.repository.RefreshTokenRepository; +import com.ssg.starroad.user.repository.UserRepository; +import com.ssg.starroad.user.service.RefreshTokenService; +import com.ssg.starroad.user.service.UserService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @Autowired + private UserService userService; + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private RefreshTokenService refreshTokenService; + + private ObjectMapper objectMapper = new ObjectMapper(); + private static final Logger logger = LoggerFactory.getLogger(UserControllerTest.class); + + @BeforeEach + public void setup() { +// 사용자 데이터를 삭제하기 전에 해당 사용자의 모든 리프레시 토큰을 삭제 + userRepository.findByEmail("testuser@example.com") + .ifPresent(user -> { + refreshTokenRepository.deleteByUserId(user.getId()); // 리프레시 토큰 먼저 삭제 + userRepository.delete(user); // 이후 사용자 삭제 + }); + + // 테스트용 사용자 데이터 생성 + UserDTO userDTO = new UserDTO(); + userDTO.setEmail("testuser@example.com"); + userDTO.setPassword("Valid123$!"); + userDTO.setName("TestUser"); + userDTO.setNickname("Tester"); + userDTO.setGender(Gender.MALE); + userDTO.setBirth(LocalDate.of(1990, 1, 1)); + userDTO.setPhone("01012345678"); + userDTO.setProviderType(ProviderType.GOOGLE); + userDTO.setProviderId("providerId123"); + userDTO.setImagePath("/path/to/image.jpg"); + userDTO.setReviewExp(0); + userDTO.setPoint(0); + userDTO.setActiveStatus(ActiveStatus.ACTIVE); + + userService.save(userDTO); + setupSecurityContext("testuser@example.com"); + } + private void setupSecurityContext(String email) { + UserDetails userDetails = userService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + @Test + public void testLoadUserByUsername() { + UserDetails userDetails = userService.loadUserByUsername("testuser@example.com"); + assertNotNull(userDetails); + assertEquals("testuser@example.com", userDetails.getUsername()); + } + @Test + public void testLogin() throws Exception { + LoginRequest loginRequest = new LoginRequest("testuser@example.com", "Valid123$!"); + String loginContent = objectMapper.writeValueAsString(loginRequest); + + logger.info("Sending login request for {}", loginRequest.getEmail()); + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/user/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginContent)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.accessToken").exists()) + .andExpect(jsonPath("$.refreshToken").exists()) + .andExpect(header().exists("Authorization")) + .andReturn(); + + logger.info("Login test passed for {}", loginRequest.getEmail()); + logger.info("Response: {}", result.getResponse().getContentAsString()); + } + + @Test + public void whenInvalidUser_thenUnauthorized() throws Exception { + LoginRequest loginRequest = new LoginRequest("wrong1@google.com", "password@456"); + + // 특정 이메일과 패스워드 조합에 대한 결과를 명시적으로 설정 + mockMvc.perform(post("/user/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isUnauthorized()); + } + @AfterEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } +} diff --git a/src/test/java/com/ssg/starroad/user/service/RefreshTokenServiceTest.java b/src/test/java/com/ssg/starroad/user/service/RefreshTokenServiceTest.java new file mode 100644 index 0000000..70fcb69 --- /dev/null +++ b/src/test/java/com/ssg/starroad/user/service/RefreshTokenServiceTest.java @@ -0,0 +1,4 @@ +package com.ssg.starroad.user.service; + +public class RefreshTokenServiceTest { +} diff --git a/src/test/java/com/ssg/starroad/user/service/TokenServiceTest.java b/src/test/java/com/ssg/starroad/user/service/TokenServiceTest.java new file mode 100644 index 0000000..594498f --- /dev/null +++ b/src/test/java/com/ssg/starroad/user/service/TokenServiceTest.java @@ -0,0 +1,4 @@ +package com.ssg.starroad.user.service; + +public class TokenServiceTest { +}