diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c2e53a3 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows.follow_deploy.yml b/.github/workflows.follow_deploy.yml new file mode 100644 index 0000000..334a289 --- /dev/null +++ b/.github/workflows.follow_deploy.yml @@ -0,0 +1,59 @@ +name: deploy_lambda + +on: + push: + branches: [ feat/follow_service#6 ] + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test Deploy' +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: JDK 세팅 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle without test + run: ./gradlew clean build buildZip + + - name: Create deployment package + run: | + mkdir -p deployment + cp build/libs/*.jar deployment/ + if [ -d "lib" ]; then + cp -R lib/* deployment/ + fi + cd deployment + zip -r ../follow_service.zip . + - name: AWS 계정 세팅 + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: S3에 패키지 업로드 + run: | + aws s3 cp follow_service.zip s3://gureumi-s3/lambda-function.zip + - name: 람다 배포 + run: | + aws lambda update-function-code \ + --function-name ${{ secrets.LAMBDA_FOLLOW_FUNCTION_NAME }} \ + --s3-bucket gureumi-s3 \ + --s3-key lambda-function.zip \ No newline at end of file diff --git a/.github/workflows/deploy_lambda.yml b/.github/workflows/deploy_lambda.yml new file mode 100644 index 0000000..311e319 --- /dev/null +++ b/.github/workflows/deploy_lambda.yml @@ -0,0 +1,61 @@ +name: deploy_lambda + +on: + push: + branches: [ feat/like_service ] + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test Deploy' +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: JDK 세팅 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle without test + run: ./gradlew clean build -x test buildZip + + - name: Create deployment package + run: | + mkdir -p deployment + cp build/distributions/lambda-function.zip deployment/ + if [ -d "lib" ]; then + cp -R lib/* deployment/ + fi + cd deployment + zip -r ../function.zip . + + - name: AWS 계정 세팅 + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: S3에 패키지 업로드 + run: | + aws s3 cp function.zip s3://gureumi-s3/deploy/function.zip + + - name: 람다 배포 + run: | + aws lambda update-function-code \ + --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ + --s3-bucket gureumi-s3 \ + --s3-key deploy/function.zip \ No newline at end of file diff --git a/.gitignore b/.gitignore index f208e71..c2065bc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ -/src/main/resources/application.yml ### STS ### .apt_generated diff --git a/build.gradle b/build.gradle index 9ea1e0a..ba35355 100644 --- a/build.gradle +++ b/build.gradle @@ -23,10 +23,36 @@ repositories { mavenCentral() } +jar { + manifest { + attributes( + 'Manifest-Version': '1.0', + 'Main-Class': 'com.goormy.hackathon.HackathonApplication' + ) + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-web' + + + implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1") + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs' + implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' + implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws:4.0.0' + implementation 'org.springframework.cloud:spring-cloud-starter-function-web:4.0.0' + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1") + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs' + implementation 'redis.clients:jedis:4.0.1' + implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' + implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws:4.0.0' + implementation 'org.springframework.cloud:spring-cloud-starter-function-web:4.0.0' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' @@ -38,3 +64,28 @@ dependencies { tasks.named('test') { useJUnitPlatform() } +jar { + manifest { + attributes( + 'Manifest-Version': '1.0', + 'Main-Class': 'com.goormy.hackathon.HackathonApplication' + ) + } +} + + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from configurations.runtimeClasspath + } + into('lib') { + from jar.archiveFile + } + archiveFileName = 'lambda-function.zip' + destinationDirectory = file("$buildDir/distributions") +} + +build.dependsOn buildZip + diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/goormy/hackathon/HackathonApplication.java b/src/main/java/com/goormy/hackathon/HackathonApplication.java index be8645a..80c6918 100644 --- a/src/main/java/com/goormy/hackathon/HackathonApplication.java +++ b/src/main/java/com/goormy/hackathon/HackathonApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class HackathonApplication { public static void main(String[] args) { diff --git a/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter_DS.java b/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter_DS.java new file mode 100644 index 0000000..5802268 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter_DS.java @@ -0,0 +1,25 @@ +package com.goormy.hackathon.common.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import org.springframework.stereotype.Component; + +@Component +public class LocalDateTimeConverter_DS { + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public LocalDateTime convertToLocalDateTime(String source) { + try { + return LocalDateTime.parse(source, formatter); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException( + "Invalid date time format. Please use this pattern: yyyy-MM-dd HH:mm:ss"); + } + } + + public String convertToString(LocalDateTime source) { + return source.format(formatter); + } +} \ No newline at end of file diff --git a/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter__SY.java b/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter__SY.java new file mode 100644 index 0000000..2ac5df3 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter__SY.java @@ -0,0 +1,15 @@ +package com.goormy.hackathon.common.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public final class LocalDateTimeConverter__SY { + + private LocalDateTimeConverter__SY() { + } + + public static String convert(LocalDateTime value) { + return value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + +} \ No newline at end of file diff --git a/src/main/java/com/goormy/hackathon/config/AwsConfig.java b/src/main/java/com/goormy/hackathon/config/AwsConfig.java new file mode 100644 index 0000000..811fab9 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/config/AwsConfig.java @@ -0,0 +1,32 @@ +package com.goormy.hackathon.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sqs.SqsClient; + +@Configuration +public class AwsConfig { + + + @Value("${spring.cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${spring.cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${spring.cloud.aws.region.static}") + private String region; + + @Bean + public SqsClient sqsClient() { + return SqsClient.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .build(); + } +} diff --git a/src/main/java/com/goormy/hackathon/config/dummy.txt b/src/main/java/com/goormy/hackathon/config/dummy.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/goormy/hackathon/controller/FollowController.java b/src/main/java/com/goormy/hackathon/controller/FollowController.java new file mode 100644 index 0000000..9c81ade --- /dev/null +++ b/src/main/java/com/goormy/hackathon/controller/FollowController.java @@ -0,0 +1,52 @@ +package com.goormy.hackathon.controller; + +import com.goormy.hackathon.dto.HashtagDto_sieun; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.service.FollowSQSService; +import com.goormy.hackathon.service.FollowService; +import com.goormy.hackathon.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +public class FollowController { + + @Autowired + private FollowSQSService followSQSService; + + @Autowired + private FollowService followService; + + @Autowired + private UserService userService; + + @PostMapping("/follow") + public ResponseEntity follow(@RequestHeader long userId, @RequestParam long hashtagId) { + followSQSService.sendFollowRequest(userId,hashtagId); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/unfollow") + public ResponseEntity unfollow(@RequestHeader long userId, @RequestParam long hashtagId) { + followSQSService.sendUnfollowRequest(userId,hashtagId); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/followings") + public List getFollowedHashtags(@RequestHeader("userId") Long userId) { + User user = userService.findById(userId); + + List hashtags = followService.getFollowedHashtags(user); + + return hashtags.stream() + .map(hashtag -> new HashtagDto_sieun(hashtag.getId(), hashtag.getName(), hashtag.getType().toString())) + .collect(Collectors.toList()); + } + + +} diff --git a/src/main/java/com/goormy/hackathon/controller/GetFeedController.java b/src/main/java/com/goormy/hackathon/controller/GetFeedController.java new file mode 100644 index 0000000..5005184 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/controller/GetFeedController.java @@ -0,0 +1,81 @@ +package com.goormy.hackathon.controller; + +import com.goormy.hackathon.common.util.LocalDateTimeConverter_DS; +import com.goormy.hackathon.dto.request.AddFeedUser; +import com.goormy.hackathon.dto.response.GetFeedResponseDto; +import com.goormy.hackathon.redis.entity.PostSimpleInfo; +import com.goormy.hackathon.repository.Redis.FeedHashtagRedisRepository; +import com.goormy.hackathon.repository.Redis.FeedUserRedisRepository; +import com.goormy.hackathon.service.GetFeedService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("") +public class GetFeedController { + + private final GetFeedService getFeedService; + private final LocalDateTimeConverter_DS localDateTimeConverter; + + private final FeedUserRedisRepository feedUserRedisRepository; + private final FeedHashtagRedisRepository feedHashtagRedisRepository; + + /** + * [GET] 사용자 맞춤형 피드 조회 기능 + * + * @param userId : 사용자 Id + * @param size : Pagenation size + * @return : size만큼의 게시글을 반환 + */ + @GetMapping("/feed") + public ResponseEntity> getFeedList( + @RequestHeader Long userId, + @RequestParam int size + ) { + // 피드 리스트 반환 + return ResponseEntity.ok(getFeedService.getFeedList(userId, size)); + } + + @PostMapping("/hashfeed") + public ResponseEntity addFeedHashtagData( + @RequestHeader Long userId, + @RequestBody AddFeedUser requestDto + ) { + requestDto.getInfoList().forEach( + info -> + feedHashtagRedisRepository.add(userId, + PostSimpleInfo.toEntity(info.getPostId(), + localDateTimeConverter.convertToString(info.getCreatedAt()))) + ); + + // 피드 리스트 반환 + return ResponseEntity.ok("ok"); + } + + @PostMapping("/userfeed") + public ResponseEntity addFeedUserData( + @RequestHeader Long userId, + @RequestBody AddFeedUser requestDto + ) { + requestDto.getInfoList().forEach( + info -> + feedUserRedisRepository.add(userId, + PostSimpleInfo.toEntity(info.getPostId(), + localDateTimeConverter.convertToString(info.getCreatedAt()))) + ); + + // 피드 리스트 반환 + return ResponseEntity.ok("ok"); + } + +} + diff --git a/src/main/java/com/goormy/hackathon/controller/LikeController.java b/src/main/java/com/goormy/hackathon/controller/LikeController.java new file mode 100644 index 0000000..ae988a7 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/controller/LikeController.java @@ -0,0 +1,40 @@ +package com.goormy.hackathon.controller; + +import com.goormy.hackathon.service.LikeSQSService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("") +public class LikeController { + + private final LikeSQSService likeService; + + @PostMapping("/likes") + public ResponseEntity addLike( + @RequestHeader(name = "userId") Long userId, + @RequestParam(name = "postId") Long postId) { + + likeService.sendLikeRequest(userId,postId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/likes") + public ResponseEntity cancelLike( + @RequestHeader(name = "userId") Long userId, + @RequestParam(name = "postId") Long postId) { + + likeService.sendCancelLikeRequest(userId,postId); + + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/com/goormy/hackathon/controller/PostController.java b/src/main/java/com/goormy/hackathon/controller/PostController.java new file mode 100644 index 0000000..33ba3b9 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/controller/PostController.java @@ -0,0 +1,37 @@ +package com.goormy.hackathon.controller; + + +import com.goormy.hackathon.dto.post.PostRequestDto_SY; +import com.goormy.hackathon.dto.post.PostResponseDto_SY; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/posts") +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + + @PostMapping + public PostResponseDto_SY create( + @RequestHeader("userId") Long userId, + @RequestBody PostRequestDto_SY postRequestDtoSY) { + return postService.createPost(userId, postRequestDtoSY); + } + + @GetMapping("/search") + public Page getPostsByHashtag( + @RequestParam(name = "hashtag") String hashtag, + @RequestParam(name = "page") int page, + @RequestParam(name = "size") int size) { + System.out.println("Received request to fetch posts by hashtag: " + hashtag); + Page postsPage = postService.getPostsByHashtag(hashtag, page, size); + System.out.println("Returning page " + postsPage.getNumber() + " of " + postsPage.getTotalPages() + " for hashtag: " + hashtag); + return postsPage; + } + +} diff --git a/src/main/java/com/goormy/hackathon/controller/dummy.txt b/src/main/java/com/goormy/hackathon/controller/dummy.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/goormy/hackathon/dto/HashtagDto_sieun.java b/src/main/java/com/goormy/hackathon/dto/HashtagDto_sieun.java new file mode 100644 index 0000000..1cba285 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/HashtagDto_sieun.java @@ -0,0 +1,18 @@ +package com.goormy.hackathon.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HashtagDto_sieun { + private Long id; + private String name; + private String type; + + public HashtagDto_sieun(Long id, String name, String type) { + this.id = id; + this.name = name; + this.type = type; + } +} diff --git a/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagRequestDto_SY.java b/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagRequestDto_SY.java new file mode 100644 index 0000000..40250de --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagRequestDto_SY.java @@ -0,0 +1,10 @@ +package com.goormy.hackathon.dto.hashtag; + +import com.goormy.hackathon.entity.Hashtag; + +public record PostHashtagRequestDto_SY( + String name, + Hashtag.Type type +) { + +} diff --git a/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagResponseDto_SY.java b/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagResponseDto_SY.java new file mode 100644 index 0000000..ef7046b --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagResponseDto_SY.java @@ -0,0 +1,10 @@ +package com.goormy.hackathon.dto.hashtag; + +import com.goormy.hackathon.entity.Hashtag; + +public record PostHashtagResponseDto_SY( + String name, + Hashtag.Type type +) { + +} diff --git a/src/main/java/com/goormy/hackathon/dto/post/PostRequestDto_SY.java b/src/main/java/com/goormy/hackathon/dto/post/PostRequestDto_SY.java new file mode 100644 index 0000000..6b7dae3 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/post/PostRequestDto_SY.java @@ -0,0 +1,14 @@ +package com.goormy.hackathon.dto.post; + +import com.goormy.hackathon.dto.hashtag.PostHashtagRequestDto_SY; + +import java.util.List; + +public record PostRequestDto_SY( + String content, + String imageUrl, + Integer star, + List postHashtags +) { + +} diff --git a/src/main/java/com/goormy/hackathon/dto/post/PostResponseDto_SY.java b/src/main/java/com/goormy/hackathon/dto/post/PostResponseDto_SY.java new file mode 100644 index 0000000..4cc17b6 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/post/PostResponseDto_SY.java @@ -0,0 +1,35 @@ +package com.goormy.hackathon.dto.post; + +import com.goormy.hackathon.dto.hashtag.PostHashtagResponseDto_SY; +import com.goormy.hackathon.entity.Post; + +import java.time.LocalDateTime; +import java.util.List; + +public record PostResponseDto_SY( + Long id, + String content, + String imageUrl, + Integer star, + List postHashtags, + LocalDateTime createdAt + +) { + + public PostResponseDto_SY(Post post) { + this( + post.getId(), + post.getContent(), + post.getImageUrl(), + post.getStar(), + mapHashtagsToDto(post), + post.getCreatedAt() + ); + } + + private static List mapHashtagsToDto(Post post) { + return post.getPostHashtags().stream() + .map(hashtag -> new PostHashtagResponseDto_SY(hashtag.getName(), hashtag.getType())) + .toList(); + } +} diff --git a/src/main/java/com/goormy/hackathon/dto/request/AddFeedUser.java b/src/main/java/com/goormy/hackathon/dto/request/AddFeedUser.java new file mode 100644 index 0000000..faaa1fa --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/request/AddFeedUser.java @@ -0,0 +1,26 @@ +package com.goormy.hackathon.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class AddFeedUser { + private List infoList; + + @Getter + public static class Info{ + private Long postId; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime createdAt; + } +} + + diff --git a/src/main/java/com/goormy/hackathon/dto/response/GetFeedResponseDto.java b/src/main/java/com/goormy/hackathon/dto/response/GetFeedResponseDto.java new file mode 100644 index 0000000..fd5d7d6 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/response/GetFeedResponseDto.java @@ -0,0 +1,35 @@ +package com.goormy.hackathon.dto.response; + +import com.goormy.hackathon.redis.entity.PostRedis_DS; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GetFeedResponseDto { + + private Long id; + private String content; + private String imgUrl; + private Integer star; + private Integer likeCount; + private Long userId; + private List postHashtags; + + public static GetFeedResponseDto toDto(PostRedis_DS postRedisDS) { + return GetFeedResponseDto.builder() + .id(postRedisDS.getId()) + .content(postRedisDS.getContent()) + .imgUrl(postRedisDS.getImgUrl()) + .star(postRedisDS.getStar()) + .likeCount(postRedisDS.getLikeCount()) + .userId(postRedisDS.getUserId()) + .postHashtags(postRedisDS.getPostHashtags()) + .build(); + } +} diff --git a/src/main/java/com/goormy/hackathon/entity/Follow.java b/src/main/java/com/goormy/hackathon/entity/Follow.java index aab67cc..c369754 100644 --- a/src/main/java/com/goormy/hackathon/entity/Follow.java +++ b/src/main/java/com/goormy/hackathon/entity/Follow.java @@ -1,15 +1,15 @@ package com.goormy.hackathon.entity; -import com.goormy.hackathon.common.entity.BaseTimeEntity; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @NoArgsConstructor @Getter -public class Follow extends BaseTimeEntity { +public class Follow { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/goormy/hackathon/entity/Hashtag.java b/src/main/java/com/goormy/hackathon/entity/Hashtag.java index fde4a7e..a1e5f9f 100644 --- a/src/main/java/com/goormy/hackathon/entity/Hashtag.java +++ b/src/main/java/com/goormy/hackathon/entity/Hashtag.java @@ -1,5 +1,6 @@ package com.goormy.hackathon.entity; +import com.goormy.hackathon.common.entity.BaseTimeEntity; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; @@ -11,7 +12,7 @@ @Entity @NoArgsConstructor @Getter -public class Hashtag { +public class Hashtag extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/goormy/hackathon/entity/Like.java b/src/main/java/com/goormy/hackathon/entity/Like.java index 250228a..672942c 100644 --- a/src/main/java/com/goormy/hackathon/entity/Like.java +++ b/src/main/java/com/goormy/hackathon/entity/Like.java @@ -10,7 +10,7 @@ @NoArgsConstructor @Getter @Table(name = "likes") -public class Like extends BaseTimeEntity { +public class Like { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/goormy/hackathon/entity/Post.java b/src/main/java/com/goormy/hackathon/entity/Post.java index 169705a..f3d132f 100644 --- a/src/main/java/com/goormy/hackathon/entity/Post.java +++ b/src/main/java/com/goormy/hackathon/entity/Post.java @@ -2,12 +2,15 @@ import com.goormy.hackathon.common.entity.BaseTimeEntity; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.List; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; @Entity @NoArgsConstructor @@ -36,6 +39,7 @@ public class Post extends BaseTimeEntity { @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) private List likes = new ArrayList<>(); + @Builder public Post(User user, String content, String imageUrl, Integer star, Integer likeCount) { this.user = user; @@ -44,4 +48,17 @@ public Post(User user, String content, String imageUrl, Integer star, Integer li this.star = star; this.likeCount = likeCount; } + + public List getPostHashtags() { + return postHashtags.stream() + .map(PostHashtag::getHashtag) + .toList(); + } + + public void setPostHashtags(List hashtags) { + this.postHashtags = hashtags.stream() + .map(hashtag -> new PostHashtag(this, hashtag)) + .toList(); + } + } diff --git a/src/main/java/com/goormy/hackathon/entity/PostHashtag.java b/src/main/java/com/goormy/hackathon/entity/PostHashtag.java index f56e1f2..86b8912 100644 --- a/src/main/java/com/goormy/hackathon/entity/PostHashtag.java +++ b/src/main/java/com/goormy/hackathon/entity/PostHashtag.java @@ -8,7 +8,7 @@ @Entity @NoArgsConstructor @Getter -public class PostHashtag { +public class PostHashtag{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/goormy/hackathon/entity/User.java b/src/main/java/com/goormy/hackathon/entity/User.java index 55182a8..f74957a 100644 --- a/src/main/java/com/goormy/hackathon/entity/User.java +++ b/src/main/java/com/goormy/hackathon/entity/User.java @@ -13,9 +13,10 @@ @NoArgsConstructor @Getter @Table(name="users") -public class User extends BaseTimeEntity { +public class User { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; diff --git a/src/main/java/com/goormy/hackathon/handler/FollowHandler.java b/src/main/java/com/goormy/hackathon/handler/FollowHandler.java new file mode 100644 index 0000000..b4cda30 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/handler/FollowHandler.java @@ -0,0 +1,12 @@ +package com.goormy.hackathon.handler; + +import org.springframework.cloud.function.adapter.aws.FunctionInvoker; + +public class FollowHandler extends FunctionInvoker { + + private static String FollowHandler = "followFunction"; + + public FollowHandler() { + super(FollowHandler); + } +} diff --git a/src/main/java/com/goormy/hackathon/handler/LikeHandler.java b/src/main/java/com/goormy/hackathon/handler/LikeHandler.java new file mode 100644 index 0000000..08f5e82 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/handler/LikeHandler.java @@ -0,0 +1,11 @@ +package com.goormy.hackathon.handler; + +import org.springframework.cloud.function.adapter.aws.FunctionInvoker; + +public class LikeHandler extends FunctionInvoker{ + private static String LikeHandler = "likeFunction"; + + public LikeHandler() { + super(LikeHandler); + } +} diff --git a/src/main/java/com/goormy/hackathon/handler/ScheduledHandler.java b/src/main/java/com/goormy/hackathon/handler/ScheduledHandler.java new file mode 100644 index 0000000..b13277b --- /dev/null +++ b/src/main/java/com/goormy/hackathon/handler/ScheduledHandler.java @@ -0,0 +1,12 @@ +package com.goormy.hackathon.handler; + +import org.springframework.cloud.function.adapter.aws.FunctionInvoker; + + +public class ScheduledHandler extends FunctionInvoker { + private static String ScheduledHandler = "scheduledFunction"; + + public ScheduledHandler() { + super(ScheduledHandler); + } +} diff --git a/src/main/java/com/goormy/hackathon/lambda/FollowFunction.java b/src/main/java/com/goormy/hackathon/lambda/FollowFunction.java new file mode 100644 index 0000000..3ca5030 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/lambda/FollowFunction.java @@ -0,0 +1,78 @@ +package com.goormy.hackathon.lambda; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.goormy.hackathon.entity.Follow; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.repository.JPA.FollowRepository; +import com.goormy.hackathon.repository.JPA.HashtagRepository; +import com.goormy.hackathon.repository.JPA.UserRepository; +import com.goormy.hackathon.repository.Redis.FollowListRedisRepository; +import com.goormy.hackathon.service.FollowService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +@RequiredArgsConstructor +@Configuration +@Slf4j +public class FollowFunction implements Consumer { + + private final FollowRepository followRepository; + private final UserRepository userRepository; + private final HashtagRepository hashtagRepository; + private final FollowListRedisRepository followListRedisRepository; + private final FollowService followService; + private final ObjectMapper objectMapper; + + @Override + public void accept(Object messageBody) { + try { + String messageString = new String((byte[]) messageBody, StandardCharsets.UTF_8); + + Map messageMap = objectMapper.readValue(messageString, Map.class); + List> records = (List>) messageMap.get("Records"); + String bodyString = (String) records.get(0).get("body"); + Map body = objectMapper.readValue(bodyString, Map.class); + + + // userId와 hashtagId를 Number로 파싱하고 long으로 변환 + long userId = ((Number) body.get("userId")).longValue(); + long hashtagId = ((Number) body.get("hashtagId")).longValue(); + String action = (String) body.get("action"); + log.info("userId: {}, hashtagId: {}, action: {}", userId, hashtagId, action); + + User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다. userId: " + userId)); + Hashtag hashtag = hashtagRepository.findById(hashtagId).orElseThrow(() -> new RuntimeException("존재하지 않는 해시태그입니다. hashtagId: " + hashtagId)); + + if ("follow".equals(action)) { + Follow follow = new Follow(user,hashtag); + // follow_list:{hashtagId} 저장 + followListRedisRepository.set(hashtagId, userId); + // follow_count:{hashtagId} 저장 + followService.followHashtag(hashtagId); + log.info("팔로우 성공: " + messageBody); + System.out.println("팔로우 성공: " + messageBody); + } else if ("unfollow".equals(action)) { + // follow_list:{hashtagId} 삭제 + Follow follow = followRepository.findByUserIdAndHashTagId(userId, hashtagId) + .orElseThrow(() -> new RuntimeException("존재하지 않는 팔로우입니다. userId: " + userId + " hashtagId: " + hashtagId)); + followListRedisRepository.delete(hashtagId, userId); + // follow_count:{hashtagId} 삭제 + followService.unfollowHashtag(hashtagId); + log.info("팔로우 취소 성공: " + messageBody); + } else { + log.warn("존재하지 않는 action입니다 : " + action); + } + } catch (Exception e) { + log.error("메시지 전송 실패: " + messageBody, e); + } + } +} + + + diff --git a/src/main/java/com/goormy/hackathon/lambda/LikeFunction.java b/src/main/java/com/goormy/hackathon/lambda/LikeFunction.java new file mode 100644 index 0000000..01f9021 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/lambda/LikeFunction.java @@ -0,0 +1,158 @@ +package com.goormy.hackathon.lambda; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.goormy.hackathon.entity.Like; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.repository.Redis.LikeRedisRepository; +import com.goormy.hackathon.repository.JPA.LikeRepository; +import com.goormy.hackathon.repository.JPA.PostRepository; +import com.goormy.hackathon.repository.JPA.UserRepository; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class LikeFunction implements Consumer{ + + private final LikeRedisRepository likeRedisRepository; + private final LikeRepository likeRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final ObjectMapper objectMapper; + + @Override + public void accept(Object messageBody) { + try { + String messageString = new String((byte[]) messageBody, StandardCharsets.UTF_8); + + Map messageMap = objectMapper.readValue(messageString, Map.class); + List> records = (List>) messageMap.get("Records"); + String bodyString = (String) records.get(0).get("body"); + Map body = objectMapper.readValue(bodyString, Map.class); + + long userId = ((Number) body.get("userId")).longValue(); + long postId = ((Number) body.get("postId")).longValue(); + String action = (String) body.get("action"); + + User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다. userId: " + userId)); + Post post = postRepository.findById(postId).orElseThrow(() -> new RuntimeException("존재하지 않는 포스트 입니다. postId: " + postId)); + + if ("like".equals(action)) { + addLike(postId,userId); + System.out.println("좋아요 성공: " + messageBody); + } else if ("unlike".equals(action)) { + cancelLike(postId,userId); + System.out.println("좋아요 취소 성공: " + messageBody); + } else { + System.out.println("존재하지 않는 action입니다 : " + action); + } + + } catch (Exception e) { + System.err.println("메시지 전송 실패: " + messageBody); + e.printStackTrace(); + } + } + + /** + * @description 좋아요 정보를 Redis 캐시에 업데이트 + * */ + @Transactional + public void addLike(Long postId, Long userId) { + // 캐시에서 좋아요에 대한 정보를 먼저 조회함 + Integer findPostLike = likeRedisRepository.findPostLikeByPostIdAndUserId(postId, userId); + + if (findPostLike == null) { + // 캐시에 좋아요에 대한 정보가 없다면, + // Key = postlike:{postId}, Field = {userId}, Value = 1 로 '좋아요' 정보 생성 + likeRedisRepository.set(postId, userId, 1); + }else if (findPostLike == -1) { + // '좋아요 취소' 정보가 있는 상태라면 + // '좋아요'를 다시 누른 것이기 때문에 '취소 정보'를 삭제 + likeRedisRepository.delete(postId,userId); + } + } + + /** + * @description 좋아요 취소 정보를 Redis 캐시에 업데이트 + * */ + @Transactional + public void cancelLike(Long postId, Long userId) { + // 캐시에서 좋아요 정보를 먼저 조회함 + Integer findPostLike = likeRedisRepository.findPostLikeByPostIdAndUserId(postId, userId); + + // 캐시에 좋아요에 대한 정보가 없다면, + // Key = postlike:{postId}, Field = {userId}, Value = -1 로 '좋아요 취소' 정보 생성 + if (findPostLike == null) { + likeRedisRepository.set(postId,userId,-1); + }else if (findPostLike == 1) { + // '좋아요'라는 정보가 있는 상태라면 + // '좋아요 취소'를 다시 누른 것이기 때문에 '좋아요' 정보를 삭젳 + likeRedisRepository.delete(postId,userId); + } + } + + /** + * @description 좋아요 정보가 있는지 조회 (1. Redis 조회 2. RDB 조회) + * */ + public boolean findLike(Long postId, Long userId) { + // 1. 캐시로부터 '좋아요'에 대한 정보를 조회함 + Integer value = likeRedisRepository.findPostLikeByPostIdAndUserId(postId, userId); + + if (value == null) { // 캐시에 정보가 없다면 DB에서 조회되는지 여부에 따라 true/false 리턴 + return likeRepository.isExistByPostIdAndUserId(postId, userId); + }else if (value == -1) { // 캐시에 '좋아요 삭제' 정보가 있다면 false 리턴 + return false; + }else{ // 캐시에 '좋아요 추가' 정보가 있다면 true 리턴 + return true; + } + } + + /** + * @description Redis에 있는 '좋아요' 정보들을 RDB에 반영하는 함수 + * */ + @Transactional + public void dumpToDB() { + // 1. "postlike:{postId} 형식의 모든 key 목록을 불러옴 + Set postLikeKeySet = likeRedisRepository.findAllKeys(); + + // 2. Key마다 postId, userId, value를 조회하는 과정 + for (String key: postLikeKeySet) { + + // 2-1. Key로 Hash 자료구조를 조회함. field = userId / value = 1 or -1 + Map result = likeRedisRepository.findPostLikeByKey(key); + + // 2-2. key를 파싱하여 postId를 구함 + String[] split = key.split(":"); + Long postId = Long.valueOf(split[1]); + + for (Map.Entry entry : result.entrySet()) { + // 2-3. field를 형변환하여 userId를 구함 + Long userId = Long.valueOf(String.valueOf(entry.getKey())); + // 2-4. value를 형변환하여 1 또는 -1 값을 얻게 됨 + Integer value = Integer.valueOf(String.valueOf(entry.getValue())); + + // 3. value 값에 따라 DB에 어떻게 반영할지 결정하여 처리함 + if (value == 1) { // 3-1. 좋아요를 추가한 상태였다면 RDB에 insert 쿼리 발생 + User user = userRepository.getReferenceById(userId); + Post post = postRepository.getReferenceById(postId); + likeRepository.save(new Like(user, post)); + }else if (value == -1) { // 3-2. 좋아요를 취소한 상태였다면 RDB에 delete 쿼리 발생 + likeRepository.deleteByPostIdAndUserId(postId, userId); + } + } + + // 4. 해당 Key에 대해 RDB에 반영하는 과정을 마쳤으므로, + likeRedisRepository.delete(key); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/goormy/hackathon/lambda/ScheduledFunction.java b/src/main/java/com/goormy/hackathon/lambda/ScheduledFunction.java new file mode 100644 index 0000000..6ee9d96 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/lambda/ScheduledFunction.java @@ -0,0 +1,99 @@ +package com.goormy.hackathon.lambda; + +import com.goormy.hackathon.entity.Follow; +import com.goormy.hackathon.entity.Like; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.repository.JPA.FollowRepository; +import com.goormy.hackathon.repository.JPA.LikeRepository; +import com.goormy.hackathon.repository.JPA.PostRepository; +import com.goormy.hackathon.repository.JPA.UserRepository; +import com.goormy.hackathon.repository.Redis.FollowListRedisRepository; +import com.goormy.hackathon.repository.Redis.LikeRedisRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ScheduledFunction implements Consumer{ + + private final LikeRedisRepository likeRedisRepository; + private final LikeRepository likeRepository; + private final UserRepository userRepository; + private final FollowListRedisRepository followListRedisRepository; + private final FollowRepository followRepository; + private final PostRepository postRepository; + + @Override + public void accept(Object o) { + dumpToDB(); + migrateDB(); + } + + /** + * @description Redis에 있는 '좋아요' 정보들을 RDB에 반영하는 함수 + * */ + @Transactional + public void dumpToDB() { + // 1. "postlike:{postId} 형식의 모든 key 목록을 불러옴 + Set postLikeKeySet = likeRedisRepository.findAllKeys(); + + // 2. Key마다 postId, userId, value를 조회하는 과정 + for (String key: postLikeKeySet) { + + // 2-1. Key로 Hash 자료구조를 조회함. field = userId / value = 1 or -1 + Map result = likeRedisRepository.findPostLikeByKey(key); + + // 2-2. key를 파싱하여 postId를 구함 + String[] split = key.split(":"); + Long postId = Long.valueOf(split[1]); + + for (Map.Entry entry : result.entrySet()) { + // 2-3. field를 형변환하여 userId를 구함 + Long userId = Long.valueOf(String.valueOf(entry.getKey())); + // 2-4. value를 형변환하여 1 또는 -1 값을 얻게 됨 + Integer value = Integer.valueOf(String.valueOf(entry.getValue())); + + // 3. value 값에 따라 DB에 어떻게 반영할지 결정하여 처리함 + if (value == 1) { // 3-1. 좋아요를 추가한 상태였다면 RDB에 insert 쿼리 발생 + User user = userRepository.getReferenceById(userId); + Post post = postRepository.getReferenceById(postId); + likeRepository.save(new Like(user, post)); + }else if (value == -1) { // 3-2. 좋아요를 취소한 상태였다면 RDB에 delete 쿼리 발생 + likeRepository.deleteByPostIdAndUserId(postId, userId); + } + } + + // 4. 해당 Key에 대해 RDB에 반영하는 과정을 마쳤으므로, + likeRedisRepository.delete(key); + } + } + + @Transactional(rollbackFor = Exception.class) + public void migrateDB() { + log.info("데이터 이전을 시작합니다."); + try { + List follows = followListRedisRepository.getAllFollows(); + followRepository.deleteAll(); + followRepository.saveAll(follows); + log.info("Redis 데이터를 RDBMS로 옮기고 Redis를 초기화했습니다."); + } + catch (Exception e) { + log.error("데이터 이전에 실패했습니다.",e); + } + } + + +} diff --git a/src/main/java/com/goormy/hackathon/redis/config/RedisConfig.java b/src/main/java/com/goormy/hackathon/redis/config/RedisConfig.java index a6fe5c7..d2f8ed4 100644 --- a/src/main/java/com/goormy/hackathon/redis/config/RedisConfig.java +++ b/src/main/java/com/goormy/hackathon/redis/config/RedisConfig.java @@ -1,16 +1,25 @@ package com.goormy.hackathon.redis.config; +import com.goormy.hackathon.redis.entity.PostRedis_DS; +import com.goormy.hackathon.redis.entity.PostSimpleInfo; +import com.goormy.hackathon.redis.entity.UserRedis; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; + @Configuration +@EnableRedisRepositories @EnableCaching public class RedisConfig { @@ -22,7 +31,9 @@ public class RedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(host, port); + RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration() + .clusterNode(host, port); + return new LettuceConnectionFactory(clusterConfig); } @Bean @@ -30,10 +41,83 @@ public RedisTemplate redisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>( + Object.class); + redisTemplate.setValueSerializer(serializer); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(serializer); + return redisTemplate; + } + + @Bean + public RedisTemplate userRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>( + UserRedis.class); + redisTemplate.setValueSerializer(serializer); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(serializer); + return redisTemplate; + } + + @Bean + public RedisTemplate postSimpleInfoRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>( + PostSimpleInfo.class); + redisTemplate.setValueSerializer(serializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + redisTemplate.setHashValueSerializer(serializer); return redisTemplate; } + @Bean + public RedisTemplate postRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>( + PostRedis_DS.class); + redisTemplate.setValueSerializer(serializer); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(serializer); + return redisTemplate; + } + + @Bean + public StringRedisTemplate stringRedisTemplate() { + final StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); + stringRedisTemplate.setKeySerializer(new StringRedisSerializer()); + stringRedisTemplate.setValueSerializer(new StringRedisSerializer()); + stringRedisTemplate.setConnectionFactory(redisConnectionFactory()); + return stringRedisTemplate; + } + + @Bean + public RedisTemplate longRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + final RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } + + // Integer 형식으로 저장 (FollowCount에서 사용) + @Bean + public RedisTemplate integerRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Integer.class)); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Integer.class)); + return redisTemplate; + } + + + } diff --git a/src/main/java/com/goormy/hackathon/redis/entity/FeedHashtagCache.java b/src/main/java/com/goormy/hackathon/redis/entity/FeedHashtagCache.java new file mode 100644 index 0000000..efd294d --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/FeedHashtagCache.java @@ -0,0 +1,16 @@ +package com.goormy.hackathon.redis.entity; + +import lombok.Getter; +import org.springframework.data.annotation.Id; + +import java.io.Serializable; +import java.util.List; + +@Getter +public class FeedHashtagCache implements Serializable { + + @Id + Long hashtagId; + private List postList; + +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/FeedSimpleInfo_SY.java b/src/main/java/com/goormy/hackathon/redis/entity/FeedSimpleInfo_SY.java new file mode 100644 index 0000000..edc9768 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/FeedSimpleInfo_SY.java @@ -0,0 +1,22 @@ +package com.goormy.hackathon.redis.entity; + +import com.goormy.hackathon.common.util.LocalDateTimeConverter__SY; +import com.goormy.hackathon.entity.Post; +import lombok.Getter; +import org.springframework.data.annotation.Id; + +import java.io.Serializable; + +@Getter +public class FeedSimpleInfo_SY implements Serializable { + + @Id + private Long id; + private String createdAt; + + public FeedSimpleInfo_SY(Post post) { + this.id = post.getId(); + this.createdAt = LocalDateTimeConverter__SY.convert(post.getCreatedAt()); + } + +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/FeedUserCache.java b/src/main/java/com/goormy/hackathon/redis/entity/FeedUserCache.java new file mode 100644 index 0000000..b4342b8 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/FeedUserCache.java @@ -0,0 +1,21 @@ +package com.goormy.hackathon.redis.entity; + +import com.goormy.hackathon.entity.Post; +import lombok.Getter; +import org.springframework.data.annotation.Id; + +import java.io.Serializable; +import java.util.List; + +@Getter +public class FeedUserCache implements Serializable { + + @Id + Long userId; + private List postList; + + public FeedUserCache(Post post) { + this.userId = post.getUser().getId(); + } + +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/FollowCountCache.java b/src/main/java/com/goormy/hackathon/redis/entity/FollowCountCache.java new file mode 100644 index 0000000..bc37040 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/FollowCountCache.java @@ -0,0 +1,29 @@ +package com.goormy.hackathon.redis.entity; + +import com.goormy.hackathon.entity.Hashtag; +import lombok.Getter; +import org.springframework.data.annotation.Id; + +import java.io.Serializable; + +@Getter +public class FollowCountCache implements Serializable { + + @Id + private Long hashtagId; + private Integer followCount; + + public FollowCountCache(Hashtag hashtag) { + this.hashtagId = hashtag.getId(); + this.followCount = 0; + } + + public String getKey() { + return "followcount:" + hashtagId; + } + + public String getField() { + return String.valueOf(hashtagId); + } + +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/FollowListCache.java b/src/main/java/com/goormy/hackathon/redis/entity/FollowListCache.java new file mode 100644 index 0000000..6557b8e --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/FollowListCache.java @@ -0,0 +1,16 @@ +package com.goormy.hackathon.redis.entity; + +import lombok.Getter; +import org.springframework.data.annotation.Id; + +import java.io.Serializable; +import java.util.List; + +@Getter +public class FollowListCache implements Serializable { + + @Id + private String hashtagId; + private List userIdList; + +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/PopularPostRedis.java b/src/main/java/com/goormy/hackathon/redis/entity/PopularPostRedis.java new file mode 100644 index 0000000..c58ec02 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/PopularPostRedis.java @@ -0,0 +1,14 @@ +package com.goormy.hackathon.redis.entity; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.redis.core.RedisHash; + +@Getter +@RedisHash("popular_post") +@AllArgsConstructor +public class PopularPostRedis { + + private List postIdList; // 최대 256개의 아티클 저장 -> 1KB +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/PostCache_SY.java b/src/main/java/com/goormy/hackathon/redis/entity/PostCache_SY.java new file mode 100644 index 0000000..61bcfee --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/PostCache_SY.java @@ -0,0 +1,43 @@ +package com.goormy.hackathon.redis.entity; + + +import com.goormy.hackathon.common.util.LocalDateTimeConverter__SY; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.Post; +import lombok.Getter; +import org.springframework.data.annotation.Id; + +import java.io.Serializable; +import java.util.List; + +@Getter +public class PostCache_SY implements Serializable { + + @Id + private Long postId; + private String content; + private String imageUrl; + private Integer star; + private Integer likeCount; + private Long userId; + private List postHashtags; + private String createdAt; + + public PostCache_SY(Post post) { + this.postId = post.getId(); + this.content = post.getContent(); + this.imageUrl = post.getImageUrl(); + this.star = post.getStar(); + this.likeCount = 0; + this.userId = post.getUser().getId(); + this.postHashtags = post.getPostHashtags().stream() + .map(Hashtag::getName) + .toList(); + this.createdAt = LocalDateTimeConverter__SY.convert(post.getCreatedAt()); + } + + public String getKey() { + return "post:" + postId; + } + +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/PostRedis_DS.java b/src/main/java/com/goormy/hackathon/redis/entity/PostRedis_DS.java new file mode 100644 index 0000000..3c07647 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/PostRedis_DS.java @@ -0,0 +1,64 @@ +package com.goormy.hackathon.redis.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.Id; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.redis.core.RedisHash; + +@Getter +@RedisHash("post") +@AllArgsConstructor +@Builder +public class PostRedis_DS { + + @Id + private Long id; + + private String content; + private String imgUrl; + private Integer star; + private Integer likeCount; + private Long userId; + private List postHashtags; + private String createdAt; + + + @JsonCreator + public PostRedis_DS( + @JsonProperty("id") Long id, + @JsonProperty("content") String content, + @JsonProperty("imgUrl") String imgUrl, + @JsonProperty("star") int star, + @JsonProperty("likeCount") int likeCount, + @JsonProperty("userId") Long userId, + @JsonProperty("postHashtags") List postHashtags, + @JsonProperty("createdAt") String createdAt) { + this.id = id; + this.content = content; + this.imgUrl = imgUrl; + this.star = star; + this.likeCount = likeCount; + this.userId = userId; + this.postHashtags = postHashtags; + this.createdAt = createdAt; + } + + public static PostRedis_DS toEntity(Long id, String content, String imgUrl, Integer star, + Integer likeCount, Long userId, List postHashtags, LocalDateTime createdAt) { + return PostRedis_DS.builder() + .id(id) + .content(content) + .imgUrl(imgUrl) + .star(star) + .likeCount(likeCount) + .userId(userId) + .postHashtags(postHashtags) + .createdAt(createdAt.toString()) + .build(); + } +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/PostSimpleInfo.java b/src/main/java/com/goormy/hackathon/redis/entity/PostSimpleInfo.java new file mode 100644 index 0000000..b8d2eba --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/PostSimpleInfo.java @@ -0,0 +1,31 @@ +package com.goormy.hackathon.redis.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PostSimpleInfo { + + private Long id; + private String createdAt; + + @JsonCreator + public PostSimpleInfo( + @JsonProperty("id") Long postId, + @JsonProperty("createdAt") String createdAt) { + this.id = postId; + this.createdAt = createdAt; + } + + public static PostSimpleInfo toEntity(Long postId, String createdAt) { + return PostSimpleInfo.builder() + .id(postId) + .createdAt(createdAt) + .build(); + } + + +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/RecentUpdateRedis.java b/src/main/java/com/goormy/hackathon/redis/entity/RecentUpdateRedis.java new file mode 100644 index 0000000..04d94f8 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/RecentUpdateRedis.java @@ -0,0 +1,27 @@ +package com.goormy.hackathon.redis.entity; + +import jakarta.persistence.Id; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.redis.core.RedisHash; + +@AllArgsConstructor +@RedisHash("recent_update") +@Getter +@Builder +public class RecentUpdateRedis { + + @Id + private Long id; + private LocalDateTime recentUpdateTime; + + + public static RecentUpdateRedis toEntity(Long id, LocalDateTime recentUpdateTime) { + return RecentUpdateRedis.builder() + .id(id) + .recentUpdateTime(recentUpdateTime) + .build(); + } +} diff --git a/src/main/java/com/goormy/hackathon/redis/entity/UserRedis.java b/src/main/java/com/goormy/hackathon/redis/entity/UserRedis.java new file mode 100644 index 0000000..ba5a7d3 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/UserRedis.java @@ -0,0 +1,56 @@ +package com.goormy.hackathon.redis.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class UserRedis implements Serializable { + + private Long id; + + private String name; + + private String password; + + private Integer followerCount; + + private Integer followingCount; + + private List followerIdList; + + @JsonCreator + public UserRedis( + @JsonProperty("id") Long id, + @JsonProperty("name") String name, + @JsonProperty("password") String password, + @JsonProperty("followerCount") Integer followerCount, + @JsonProperty("followingCount") Integer followingCount, + @JsonProperty("followerIdList") List followerIdList) { + this.id = id; + this.name = name; + this.password = password; + this.followerCount = followerCount; + this.followingCount = followingCount; + this.followerIdList = followerIdList; + } + + + public static UserRedis toEntity(Long id, String name, String password, Integer followerCount, + Integer followingCount, List followerIdList) { + return UserRedis.builder() + .id(id) + .name(name) + .password(password) + .followerCount(followerCount) + .followingCount(followingCount) + .followerIdList(followerIdList) + .build(); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/JPA/FollowRepository.java b/src/main/java/com/goormy/hackathon/repository/JPA/FollowRepository.java new file mode 100644 index 0000000..c4c81ae --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/JPA/FollowRepository.java @@ -0,0 +1,22 @@ +package com.goormy.hackathon.repository.JPA; + +import com.goormy.hackathon.entity.Follow; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface FollowRepository extends JpaRepository { + + @Query("SELECT f FROM Follow f WHERE f.user.id = :userId AND f.hashtag.id = :hashtagId") + Optional findByUserIdAndHashTagId(@Param("userId") Long userId, @Param("hashtagId") Long hashtagId); + + @Query("SELECT f.hashtag FROM Follow f WHERE f.user = :user") + List findHashtagsByUser(@Param("user") User user); +} diff --git a/src/main/java/com/goormy/hackathon/repository/JPA/HashtagRepository.java b/src/main/java/com/goormy/hackathon/repository/JPA/HashtagRepository.java new file mode 100644 index 0000000..ccb114a --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/JPA/HashtagRepository.java @@ -0,0 +1,12 @@ +package com.goormy.hackathon.repository.JPA; + +import com.goormy.hackathon.entity.Hashtag; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface HashtagRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/goormy/hackathon/repository/JPA/LikeRepository.java b/src/main/java/com/goormy/hackathon/repository/JPA/LikeRepository.java new file mode 100644 index 0000000..551da68 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/JPA/LikeRepository.java @@ -0,0 +1,17 @@ +package com.goormy.hackathon.repository.JPA; + +import com.goormy.hackathon.entity.Like; +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; + +public interface LikeRepository extends JpaRepository { + + @Modifying + @Query("delete from Like l where l.post.id = :postId and l.user.id = :userId") + void deleteByPostIdAndUserId(@Param("postId") Long postId, @Param("userId")Long userId); + + @Query("select count(l)>0 from Like l where l.post.id = :postId and l.user.id = :userId") + boolean isExistByPostIdAndUserId(@Param("postId") Long postId, @Param("userId")Long userId); +} diff --git a/src/main/java/com/goormy/hackathon/repository/JPA/PostRepository.java b/src/main/java/com/goormy/hackathon/repository/JPA/PostRepository.java new file mode 100644 index 0000000..22fd0dd --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/JPA/PostRepository.java @@ -0,0 +1,18 @@ +package com.goormy.hackathon.repository.JPA; + +import com.goormy.hackathon.entity.Post; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PostRepository extends JpaRepository { + List findAllByIdIn(List ids); + + @Query("SELECT p FROM Post p JOIN p.postHashtags ph JOIN ph.hashtag h WHERE h.name = :hashtagName") + Page findPostsByHashtagName(@Param("hashtagName") String hashtagName, Pageable pageable); + +} diff --git a/src/main/java/com/goormy/hackathon/repository/JPA/UserRepository.java b/src/main/java/com/goormy/hackathon/repository/JPA/UserRepository.java new file mode 100644 index 0000000..d22ed64 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/JPA/UserRepository.java @@ -0,0 +1,7 @@ +package com.goormy.hackathon.repository.JPA; + +import com.goormy.hackathon.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/FeedHashtagRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/FeedHashtagRedisRepository.java new file mode 100644 index 0000000..72120ac --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/FeedHashtagRedisRepository.java @@ -0,0 +1,42 @@ +package com.goormy.hackathon.repository.Redis; + +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.redis.entity.FeedSimpleInfo_SY; +import com.goormy.hackathon.redis.entity.PostSimpleInfo; +import jakarta.annotation.PostConstruct; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FeedHashtagRedisRepository { + + private final RedisTemplate redisTemplate_postSimple; + private static final String FEED_HASHTAG_KEY = "feedhashtag:"; + + private final RedisTemplate redisTemplate_postHashtag; + + private ListOperations listOperations; + + @PostConstruct + private void init() { + listOperations = redisTemplate_postSimple.opsForList(); + } + + public void add(Long hashtagId, PostSimpleInfo value) { + listOperations.leftPush(FEED_HASHTAG_KEY + hashtagId, value); + } + + public List getAll(Long hashtagId) { + return listOperations.range(FEED_HASHTAG_KEY + hashtagId, 0, -1); + } + + public void set(Long hashtagId, Post post) { + String key = "feedhashtag:" + hashtagId; + Object value = new FeedSimpleInfo_SY(post); + redisTemplate_postHashtag.opsForList().leftPush(key, value); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/FeedUserRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/FeedUserRedisRepository.java new file mode 100644 index 0000000..26a3b2a --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/FeedUserRedisRepository.java @@ -0,0 +1,46 @@ +package com.goormy.hackathon.repository.Redis; + +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.redis.entity.FeedSimpleInfo_SY; +import com.goormy.hackathon.redis.entity.PostSimpleInfo; +import jakarta.annotation.PostConstruct; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FeedUserRedisRepository { + + private final RedisTemplate redisTemplate; + private static final String FEED_USER_KEY = "feeduser:"; + private final RedisTemplate redisTemplate_feedUser; + + private ListOperations listOperations; + + @PostConstruct + private void init() { + listOperations = redisTemplate.opsForList(); + } + + public void add(Long userId, PostSimpleInfo value) { + listOperations.leftPush(FEED_USER_KEY + userId, value); + } + + public PostSimpleInfo get(Long userId) { + return listOperations.rightPop(FEED_USER_KEY + userId); + } + + public List getAll(Long userId) { + return listOperations.rightPop(FEED_USER_KEY + userId, Long.MAX_VALUE); + } + + + public void set(Long userId, Post post) { + String key = "feeduser:" + userId; + Object value = new FeedSimpleInfo_SY(post); + redisTemplate_feedUser.opsForList().leftPush(key, value); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/FeedUserSortRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/FeedUserSortRedisRepository.java new file mode 100644 index 0000000..33e5472 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/FeedUserSortRedisRepository.java @@ -0,0 +1,35 @@ +package com.goormy.hackathon.repository.Redis; + +import jakarta.annotation.PostConstruct; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class FeedUserSortRedisRepository { + + private final RedisTemplate redisTemplate; + private static final String FEED_USER_SORTED_KEY = "feedusersort:"; + + private ListOperations listOperations; + + @PostConstruct + private void init() { + listOperations = redisTemplate.opsForList(); + } + + public void add(Long userId, Long postId) { + listOperations.leftPush(FEED_USER_SORTED_KEY + userId, postId); + } + + public List getSome(Long userId, int size) { + return listOperations.rightPop(FEED_USER_SORTED_KEY + userId, size); + } + + public List getAll(Long userId) { + return listOperations.rightPop(FEED_USER_SORTED_KEY + userId, Long.MAX_VALUE); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/FollowCountRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/FollowCountRedisRepository.java new file mode 100644 index 0000000..6664407 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/FollowCountRedisRepository.java @@ -0,0 +1,52 @@ + +package com.goormy.hackathon.repository.Redis; + +import com.goormy.hackathon.redis.entity.FollowCountCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +public class FollowCountRedisRepository { + + private final RedisTemplate integerRedisTemplate; + private static final String FOLLOWING_COUNT_KEY = "follow_count:"; + private final RedisTemplate redisTemplate; + + @Autowired + public FollowCountRedisRepository(RedisTemplate integerRedisTemplate + , RedisTemplate redisTemplate) { + this.integerRedisTemplate = integerRedisTemplate; + this.redisTemplate = redisTemplate; + } + + public Integer getFollowCount(Long hashtagId) { + String key = FOLLOWING_COUNT_KEY + hashtagId; + return integerRedisTemplate.opsForValue().get(key); + } + + public void setFollowCount(Long hashtagId, int followCount) { + String key = FOLLOWING_COUNT_KEY + hashtagId; + integerRedisTemplate.opsForValue().set(key, followCount); + } + + public void incrementFollowCount(Long hashtagId) { + String key = FOLLOWING_COUNT_KEY + hashtagId; + integerRedisTemplate.opsForValue().increment(key, 1); + } + + public void decrementFollowCount(Long hashtagId) { + String key = FOLLOWING_COUNT_KEY + hashtagId; + integerRedisTemplate.opsForValue().decrement(key, 1); + } + + public void set(FollowCountCache followCountCacheSY) { + redisTemplate.opsForHash().put(followCountCacheSY.getKey(), followCountCacheSY.getField(), followCountCacheSY.getFollowCount()); + } + + public Integer findFollowCountByHashtagId(Long hashtagId) { + String key = "followcount:" + hashtagId; + String field = String.valueOf(hashtagId); + return (Integer) redisTemplate.opsForHash().get(key, field); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/FollowListRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/FollowListRedisRepository.java new file mode 100644 index 0000000..81a448f --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/FollowListRedisRepository.java @@ -0,0 +1,72 @@ +package com.goormy.hackathon.repository.Redis; + +import com.goormy.hackathon.entity.Follow; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.repository.JPA.HashtagRepository; +import com.goormy.hackathon.repository.JPA.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Repository +@Slf4j +@RequiredArgsConstructor +public class FollowListRedisRepository { + + private final RedisTemplate redisTemplate; + + private final HashtagRepository hashtagRepository; + + private final UserRepository userRepository; + + public void set(Long hashtagId, Long userId) { + String key = "hashtagId:" + hashtagId.toString(); + redisTemplate.opsForList().rightPush(key, userId); + } + + public void delete(Long hashtagId, Long userId) { + String key = "hashtagId:" + hashtagId.toString(); + redisTemplate.opsForList().remove(key, 0, userId); + } + + public List getAllFollows() { + Set keys = redisTemplate.keys("hashtagId:*"); + List follows = new ArrayList<>(); + if (keys != null) { + for (String key : keys) { + + List userIds = redisTemplate.opsForList().range(key, 0, -1); + if (userIds != null) { + for (Object userId : userIds) { + Long hashtagId = Long.parseLong(key.split(":")[1]); + Long userIdLong = Long.parseLong(String.valueOf(userId)); + + User user = userRepository.findById(userIdLong) + .orElseThrow(() -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다.")); + Hashtag hashtag = hashtagRepository.findById(hashtagId) + .orElseThrow(() -> new IllegalArgumentException("해당 해시태그 찾을 수 없습니다. ")); + Follow follow = new Follow(user, hashtag); + follows.add(follow); + } + } + } + } + return follows; + } + + // TODO: 수정 필요 - save 하는 쪽이 어떤 식으로 저장하느냐에 따라 호출 구현이 다를 듯 + public List findUserIdListByHashtagId(Long hashtagId) { + String key = "followlist:" + hashtagId; + return (List) redisTemplate.opsForValue().get(key); + } + + + + +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/LikeRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/LikeRedisRepository.java new file mode 100644 index 0000000..a7dc175 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/LikeRedisRepository.java @@ -0,0 +1,80 @@ +package com.goormy.hackathon.repository.Redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.*; +import org.springframework.stereotype.Repository; + +import java.util.Map; +import java.util.Set; + +@Repository +@RequiredArgsConstructor +public class LikeRedisRepository { + + private final RedisTemplate redisTemplate; + + /** + * @description '좋아요' 정보를 업데이트 하는 함수 + * */ + public void set(Long postId, Long userId, Integer value) { + String key = "postlike:" + postId.toString(); + String field = userId.toString(); + + redisTemplate.opsForHash().put(key, field, value); + } + + /** + * @description '좋아요' 혹은 '좋아요 취소' 정보를 삭제하는 함수 + * */ + public void delete(Long postId, Long userId){ + String key = "postlike:" + postId.toString(); + String field = userId.toString(); + + redisTemplate.opsForHash().delete(key, field); + } + + /** + * @description postId와 userId에 대한 value를 조회 + * */ + public Integer findPostLikeByPostIdAndUserId(Long postId, Long userId) { + String key = "postlike:" + postId.toString(); + String field = userId.toString(); + + return (Integer) redisTemplate.opsForHash().get(key, field); + } + + /** + * @description Key에 대한 모든 field와 value를 조회 + * */ + public Map findPostLikeByKey(String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * @description Key에 대한 모든 field와 value를 삭제 + * */ + public void delete(String key) { + redisTemplate.delete(key); + } + + + + + public Set findAllKeys() { + Set keys = redisTemplate.keys("postlike:*"); + + return keys; + } + + // # PostId로 조회하는 함수 +// public Map findByPostId(Long postId) { +// String key = "postlike:" + postId.toString(); +// Map entries = redisTemplate.opsForHash().entries(key); +// +// return entries.entrySet().stream() +// .collect(Collectors.toMap( +// entry -> Long.valueOf((String) entry.getKey()), +// entry -> (Integer) entry.getValue() +// )); +// } +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/PopularPostRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/PopularPostRedisRepository.java new file mode 100644 index 0000000..18d1449 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/PopularPostRedisRepository.java @@ -0,0 +1,5 @@ +package com.goormy.hackathon.repository.Redis; + +public class PopularPostRedisRepository { + +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/PostRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/PostRedisRepository.java new file mode 100644 index 0000000..7999a2e --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/PostRedisRepository.java @@ -0,0 +1,47 @@ +package com.goormy.hackathon.repository.Redis; + +import com.goormy.hackathon.redis.entity.PostCache_SY; +import com.goormy.hackathon.redis.entity.PostRedis_DS; +import jakarta.annotation.PostConstruct; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PostRedisRepository { + + private final RedisTemplate redisTemplate_postRedis; + private final RedisTemplate redisTemplate; + + private static final String POST_KEY = "post:"; + + private ValueOperations valueOperations; + + @PostConstruct + private void init() { + valueOperations = redisTemplate_postRedis.opsForValue(); + } + + public void set(Long postId, PostRedis_DS value) { + valueOperations.set(POST_KEY + postId, value); + } + + public Optional get(Long postId) { + PostRedis_DS post = valueOperations.get(POST_KEY + postId); + return Optional.ofNullable(post); + } + + public List getAll(List postIdList) { + return postIdList.stream() + .map(postId -> valueOperations.get(POST_KEY + postId)) + .toList(); + } + + public void set(PostCache_SY postCacheSY) { + redisTemplate.opsForList().leftPush(postCacheSY.getKey(), postCacheSY); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/RecentUpdateRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/RecentUpdateRedisRepository.java new file mode 100644 index 0000000..e4414a4 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/RecentUpdateRedisRepository.java @@ -0,0 +1,39 @@ +package com.goormy.hackathon.repository.Redis; + +import jakarta.annotation.PostConstruct; +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class RecentUpdateRedisRepository { + + private final StringRedisTemplate stringRedisTemplate; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + private static final String RECENT_UPDATE_KEY = "recentupdate:"; + private static final Duration TTL = Duration.ofDays(3); + + private ValueOperations valueOperations; + + @PostConstruct + private void init() { + valueOperations = stringRedisTemplate.opsForValue(); + } + + public void set(Long userId, LocalDateTime value) { + String formattedValue = value.format(FORMATTER); + valueOperations.set(RECENT_UPDATE_KEY + userId, formattedValue, TTL); + } + + public Optional get(Long userId) { + String value = valueOperations.get(RECENT_UPDATE_KEY + userId); + return Optional.ofNullable(value).map(v -> LocalDateTime.parse(v, FORMATTER)); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/Redis/UserRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/Redis/UserRedisRepository.java new file mode 100644 index 0000000..35e82b0 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/Redis/UserRedisRepository.java @@ -0,0 +1,33 @@ +package com.goormy.hackathon.repository.Redis; + +import com.goormy.hackathon.redis.entity.UserRedis; +import jakarta.annotation.PostConstruct; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserRedisRepository { + + private final RedisTemplate redisTemplate; + private static final String USER_KEY = "user:"; + + private ValueOperations valueOperations; + + @PostConstruct + private void init() { + valueOperations = redisTemplate.opsForValue(); + } + + public void set(Long userId, UserRedis value) { + valueOperations.set(USER_KEY + userId, value); + } + + public Optional get(Long userId) { + UserRedis user = valueOperations.get(USER_KEY + userId); + return Optional.ofNullable(user); + } +} diff --git a/src/main/java/com/goormy/hackathon/repository/dummy.txt b/src/main/java/com/goormy/hackathon/repository/dummy.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/goormy/hackathon/service/FollowSQSService.java b/src/main/java/com/goormy/hackathon/service/FollowSQSService.java new file mode 100644 index 0000000..a85389e --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/FollowSQSService.java @@ -0,0 +1,55 @@ +package com.goormy.hackathon.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.sqs.model.SendMessageResponse; + +import java.util.Map; + +@Service +public class FollowSQSService { + private static final Logger logger = LoggerFactory.getLogger(FollowSQSService.class); + + + @Autowired + private SqsClient sqsClient; + + @Value("${spring.cloud.aws.sqs.queue-url-follow}") + private String queueUrl; + + public void sendFollowRequest(long userId, long hashtagId) { + + sendRequest(userId, hashtagId, "follow"); + } + + public void sendUnfollowRequest(long userId, long hashtagId) { + sendRequest(userId, hashtagId, "unfollow"); + } + + public void sendRequest(long userId, long hashtagId, String action) { + try{ + ObjectMapper objectMapper = new ObjectMapper(); + String messageBody = objectMapper.writeValueAsString(Map.of( + "userId", userId, + "hashtagId", hashtagId, + "action",action + )); + SendMessageRequest sendMsgRequest = SendMessageRequest.builder() + .queueUrl(queueUrl) + .messageBody(messageBody) + .build(); + logger.info("메시지 송신 - action: {}, userId: {}, hashtagId: {}", action, userId, hashtagId); + SendMessageResponse sendMsgResponse = sqsClient.sendMessage(sendMsgRequest); + logger.info("메시지가 전달되었습니다: {}, Message ID: {}, HTTP Status: {}", + messageBody, sendMsgResponse.messageId(), sendMsgResponse.sdkHttpResponse().statusCode()); + } catch (Exception e) { + logger.error("메시지 전송 실패", e); + } + } +} diff --git a/src/main/java/com/goormy/hackathon/service/FollowService.java b/src/main/java/com/goormy/hackathon/service/FollowService.java new file mode 100644 index 0000000..cdfdf05 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/FollowService.java @@ -0,0 +1,40 @@ +package com.goormy.hackathon.service; + +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.repository.Redis.FollowCountRedisRepository; +import com.goormy.hackathon.repository.JPA.FollowRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FollowService { + + private final FollowRepository followRepository; + private final FollowCountRedisRepository followCountRedisRepositorySieun; + + // 유저가 팔로우하고 있는 해시태그 목록 조회 + public List getFollowedHashtags(User user) { + return followRepository.findHashtagsByUser(user); + } + + // 팔로우, 언팔로우 캐시 부분 + public void followHashtag(Long hashtagId) { + Integer currentCount = followCountRedisRepositorySieun.getFollowCount(hashtagId); + if (currentCount == null) { + followCountRedisRepositorySieun.setFollowCount(hashtagId, 1); // 처음 팔로우인 경우 초기화 + } else { + followCountRedisRepositorySieun.incrementFollowCount(hashtagId); // 기존 팔로우 수 증가 + } + } + + public void unfollowHashtag(Long hashtagId) { + Integer currentCount = followCountRedisRepositorySieun.getFollowCount(hashtagId); + if (currentCount != null && currentCount > 0) { + followCountRedisRepositorySieun.decrementFollowCount(hashtagId); // 팔로우 수 감소 + } + } +} diff --git a/src/main/java/com/goormy/hackathon/service/GetFeedService.java b/src/main/java/com/goormy/hackathon/service/GetFeedService.java new file mode 100644 index 0000000..2b03900 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/GetFeedService.java @@ -0,0 +1,184 @@ +package com.goormy.hackathon.service; + +import com.goormy.hackathon.common.util.LocalDateTimeConverter_DS; +import com.goormy.hackathon.dto.response.GetFeedResponseDto; +import com.goormy.hackathon.entity.Follow; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.redis.entity.PostRedis_DS; +import com.goormy.hackathon.redis.entity.PostSimpleInfo; +import com.goormy.hackathon.redis.entity.UserRedis; +import com.goormy.hackathon.repository.JPA.PostRepository; +import com.goormy.hackathon.repository.JPA.UserRepository; +import com.goormy.hackathon.repository.Redis.*; +import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GetFeedService { + + private final UserRepository userRespository; + private final PostRepository postRepository; + + private final LocalDateTimeConverter_DS localDateTimeConverter; + + private final FeedHashtagRedisRepository feedHashtagRedisRepository; + private final FeedUserRedisRepository feedUserRedisRepository; + private final FeedUserSortRedisRepository feedUserSortRedisRepository; + private final PostRedisRepository postRedisRepository; + private final RecentUpdateRedisRepository recentUpdateRedisRepository; + private final UserRedisRepository userRedisRepository; + + // 1. 사용자 정보를 가져옴 + private UserRedis getUser(Long userId) { + // 캐시 우선 검색 + Optional userCacheOptional = userRedisRepository.get(userId); + // 캐시에 없으면 RDB에서 검색 + return userCacheOptional.orElseGet(() -> { + User user = userRespository.findById(userId) + .orElseThrow(() -> new RuntimeException("유저 정보가 없습니다.")); // 적절한 예외로 변경 필요 + List followIdList = user.getFollows().stream() + .map(Follow::getId) + .toList(); + // RDB에서 검색하고, UserCache에 추가 + userRedisRepository.set(userId, + UserRedis.toEntity(user.getId(), user.getName(), user.getPassword(), + user.getFollowerCount(), user.getFollowingCount(), followIdList)); + return UserRedis.toEntity(user.getId(), user.getName(), user.getPassword(), + user.getFollowerCount(), user.getFollowingCount(), followIdList); + }); + } + + // 2, 3. Push & Pull 방식으로 피드를 가져옴 + private List getPushPullFeed(UserRedis userRedisDS) { + // 날짜 세팅 + // recentUpdatedTime - 가장 마지막으로 업데이트한 시간이랑 3일 중 현재와 가장 가까운 시간을 기준으로 잡음 + LocalDateTime now = LocalDateTime.now(); + LocalDateTime recentUpdatedTime = recentUpdateRedisRepository.get(userRedisDS.getId()) + .orElse(now.minusDays(3)); + + // *** 2. Push 방식 *** + // Push 방식으로 저장되어있는 포스트 가져오기 -> Created At 정보도 같이 저장해야함 + // (이유) 한개의 Key에 있는 각 값마다 TTL 설정이 불가능하기 때문 + // feedUser 접근 + List feedUserCache = feedUserRedisRepository.getAll(userRedisDS.getId()); + // push 캐시 가져오고 비우기 & 3일 전 포스트까지만 가져오기 + List postSimpleInfoDSList = new ArrayList<>(feedUserCache.stream() + .filter(simpleInfo -> localDateTimeConverter.convertToLocalDateTime( + simpleInfo.getCreatedAt()).isAfter(recentUpdatedTime)) + .toList()); + + // *** 3. Pull 방식 *** + // Pull 방식으로 저장되어있는 인플루언서 포스트 가져오기 -> 사용자가 최근에 업데이트한 시간 이후부터 진행 + // (이유) 한개의 Key에 있는 각 값마다 TTL 설정이 불가능하기 때문 + // user의 follow 리스트를 순회하면서 가져와야함 & 3일 전 포스트까지만 가져오기 + userRedisDS.getFollowerIdList().forEach( + followId -> { + List info = feedHashtagRedisRepository.getAll(followId); + postSimpleInfoDSList.addAll(info.stream() + .filter(Objects::nonNull) + .filter(simpleInfo -> localDateTimeConverter.convertToLocalDateTime( + simpleInfo.getCreatedAt()).isAfter(recentUpdatedTime)) + .toList() + ); + } + ); + + // 최신 업데이트 시간 반영 + recentUpdateRedisRepository.set(userRedisDS.getId(), now); + + return postSimpleInfoDSList.stream().filter(Objects::nonNull).toList(); + } + + // 5. size개의 post를 구분 + private List splitPostSimpleInfoList(UserRedis userRedisDS, + List postIdList, int size) { + // 초기 반환을 위한 Id 리스트 + + // 나머지 정렬한 값을 Redis에 저장 + List postIdListForRedis = postIdList.stream() + .skip(size).toList(); + postIdListForRedis.forEach( + postId -> feedUserSortRedisRepository.add(userRedisDS.getId(), postId)); + + // 사용자에게 반환할 게시글 리스트 + return postIdList.stream().limit(size).toList(); + } + + // 5. PostList를 가져옴 + private List getPostList(List postIdList) { + List postRedisDSList = new ArrayList<>(postRedisRepository.getAll(postIdList)); + + if (postRedisDSList.isEmpty()) { + return new ArrayList<>(); + } + + List postCacheIdList = postRedisDSList.stream() + .filter(Objects::nonNull) + .map(PostRedis_DS::getId) + .toList(); + + // cache에 없는 post id 리스트를 찾음 + List postIdListNotInCache = postIdList.stream() + .filter(id -> !postCacheIdList.contains(id)).toList(); + // RDS에서 가져온 이후 Cache에 업데이트 + List postList = postRepository.findAllByIdIn(postIdListNotInCache); + postList.forEach(post -> { + PostRedis_DS postRedisDS = PostRedis_DS.toEntity( + post.getId(), post.getContent(), post.getImageUrl(), post.getStar(), + post.getLikeCount(), post.getUser().getId(), + post.getPostHashtags().stream().map(Hashtag::getName) + .toList(), post.getCreatedAt() + ); + postRedisRepository.set(postRedisDS.getId(), postRedisDS); + postRedisDSList.add(postRedisDS); + }); + + // TODO: postId별로 like여부 받아오는 코드 추가 + + return postRedisDSList.stream().filter(Objects::nonNull).toList(); + } + + // 날짜 기준으로 정렬하는 것으로 우선순위 선정 + @Transactional + public List getFeedList(Long userId, int size) { + // 1. 사용자 정보 가져오기 (Redis -> RDS) + UserRedis userRedisDS = getUser(userId); + + // 2.0 이미 정리해둔 post가 있으면 가져오기 + List postIdList = feedUserSortRedisRepository.getSome(userId, size); + if (postIdList.size() == size) { + return getPostList(postIdList).stream().map(GetFeedResponseDto::toDto) + .toList(); + } else { + size -= postIdList.size(); + } + + // 2, 3. Push, Pull로 Feed List 가져오기 & 필터링 + // 나중에 (1)여기 먼저 조회 & 있으면 반환, 없으면 (2)push/pull 확인 후 부족하면 (3)인기 게시글 반환 + List pushPullFeedList = getPushPullFeed(userRedisDS); + // 여기서 Null 체크, 이후 리스트에 들어가는 모든 post는 null이 있을 수 없음 + + // 4. 최신순으로 정렬 + List sortedPushPullFeedList = pushPullFeedList.stream() + .sorted(Comparator.comparing(PostSimpleInfo::getCreatedAt)) + .map(PostSimpleInfo::getId) + .toList(); + + // 5. 반환할 데이터와, Redis에 저장할 데이터를 구분하고, Redis에 저장 + List postIdListForReturn = splitPostSimpleInfoList(userRedisDS, sortedPushPullFeedList, + size); + // 5. post를 조회해서 반환 + return getPostList(postIdListForReturn).stream().map(GetFeedResponseDto::toDto).toList(); + + } +} diff --git a/src/main/java/com/goormy/hackathon/service/HashtagService.java b/src/main/java/com/goormy/hackathon/service/HashtagService.java new file mode 100644 index 0000000..e1d89cb --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/HashtagService.java @@ -0,0 +1,43 @@ +package com.goormy.hackathon.service; + +import com.goormy.hackathon.dto.hashtag.PostHashtagRequestDto_SY; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.redis.entity.FollowCountCache; +import com.goormy.hackathon.repository.Redis.FollowCountRedisRepository; +import com.goormy.hackathon.repository.JPA.HashtagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class HashtagService { + + private final HashtagRepository hashtagRepository; + private final FollowCountRedisRepository followCountRedisRepositorySY; + + @Transactional + public List getOrCreateHashtags(List hashtagRequestDtos) { + List hashtags = new ArrayList<>(); + for (var hashtagRequestDto : hashtagRequestDtos) { + // 이미 존재하는 해시태그인 경우 + var hashtag = hashtagRepository.findByName(hashtagRequestDto.name()) + .orElseGet(() -> { + // 새로운 해시태그인 경우 + var newHashtag = new Hashtag(hashtagRequestDto.name(), hashtagRequestDto.type()); + hashtagRepository.save(newHashtag); + + var followCountCache = new FollowCountCache(newHashtag); + followCountRedisRepositorySY.set(followCountCache); + + return newHashtag; + }); + hashtags.add(hashtag); + } + return hashtags; + } + +} diff --git a/src/main/java/com/goormy/hackathon/service/LikeSQSService.java b/src/main/java/com/goormy/hackathon/service/LikeSQSService.java new file mode 100644 index 0000000..d35080e --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/LikeSQSService.java @@ -0,0 +1,55 @@ +package com.goormy.hackathon.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.goormy.hackathon.lambda.LikeFunction; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.awssdk.services.sqs.model.SendMessageResponse; + +@Service +public class LikeSQSService { + + private static final Logger logger = LoggerFactory.getLogger(LikeFunction.class); + + @Autowired + private SqsClient sqsClient; + + @Value("${spring.cloud.aws.sqs.queue-url-like}") + private String queueUrl; + + public void sendLikeRequest(Long userId, Long postId) { + sendRequest(userId, postId, "like"); + } + + public void sendCancelLikeRequest(Long userId, Long postId) { + sendRequest(userId, postId, "unlike"); + } + + public void sendRequest(Long userId, Long postId, String action) { + try{ + ObjectMapper objectMapper = new ObjectMapper(); + String messageBody = objectMapper.writeValueAsString(Map.of( + "userId", userId, + "postId", postId, + "action", action + )); + SendMessageRequest sendMsgRequest = SendMessageRequest.builder() + .queueUrl(queueUrl) + .messageBody(messageBody) + .build(); + + logger.info("메시지 송신 - action: {}, user Id: {}, postId: {}", action, userId, postId); + SendMessageResponse sendMsgResponse = sqsClient.sendMessage(sendMsgRequest); + logger.info("메시지가 전달되었습니다: {}, Message ID: {}, HTTP Status: {}", + messageBody, sendMsgResponse.messageId(), sendMsgResponse.sdkHttpResponse().statusCode()); + } catch (Exception e) { + logger.error("메시지 전송 실패", e); + } + } +} diff --git a/src/main/java/com/goormy/hackathon/service/PostCacheService.java b/src/main/java/com/goormy/hackathon/service/PostCacheService.java new file mode 100644 index 0000000..c9ee64a --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/PostCacheService.java @@ -0,0 +1,50 @@ +package com.goormy.hackathon.service; + +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.redis.entity.PostCache_SY; +import com.goormy.hackathon.repository.Redis.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PostCacheService { + + private final PostRedisRepository postRedisRepository; + private final FollowCountRedisRepository followCountRedisRepository; + private final FollowListRedisRepository followListRedisRepository; + private final FeedUserRedisRepository feedUserRedisRepository; + private final FeedHashtagRedisRepository feedHashtagRedisRepository; + + // TODO: 이벤트로 발행한 후 이벤트를 받아 Redis에 비동기로 저장 + public void cache(Post post) { + postRedisRepository.set(new PostCache_SY(post)); + for (var hashtag : post.getPostHashtags()) { + if (isPopular(hashtag)) { + pullModel(hashtag, post); + } else { + pushModel(hashtag, post); + } + } + } + + private boolean isPopular(Hashtag hashtag) { + var followCount = followCountRedisRepository.findFollowCountByHashtagId(hashtag.getId()); + if (followCount != null ) { + return followCount >= 5000; + } + return false; + } + + private void pushModel(Hashtag hashtag, Post post) { + var userIdList = followListRedisRepository.findUserIdListByHashtagId(hashtag.getId()); + userIdList.forEach(userId -> { + feedUserRedisRepository.set(Long.valueOf(userId), post); + }); + } + + private void pullModel(Hashtag hashtag, Post post) { + feedHashtagRedisRepository.set(hashtag.getId(), post); + } +} diff --git a/src/main/java/com/goormy/hackathon/service/PostService.java b/src/main/java/com/goormy/hackathon/service/PostService.java new file mode 100644 index 0000000..8918deb --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/PostService.java @@ -0,0 +1,66 @@ +package com.goormy.hackathon.service; + +import com.goormy.hackathon.dto.post.PostRequestDto_SY; +import com.goormy.hackathon.dto.post.PostResponseDto_SY; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.repository.JPA.PostRepository; +import com.goormy.hackathon.repository.JPA.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final UserRepository userRepository; + private final PostRepository postRepository; + private final HashtagService hashtagService; + private final PostCacheService postCacheService; + + @Transactional + public PostResponseDto_SY createPost(Long userId, PostRequestDto_SY postRequestDtoSY) { + // 사용자 찾기 + var user = userRepository.findById(userId).orElseThrow( + () -> new IllegalArgumentException("User not found")); // TODO: Custom exception + + // post 생성 + var post = Post.builder() + .user(user) + .content(postRequestDtoSY.content()) + .imageUrl(postRequestDtoSY.imageUrl()) + .star(postRequestDtoSY.star()) + .likeCount(0) + .build(); + + // hashtag 생성 + var postHashtags = hashtagService.getOrCreateHashtags(postRequestDtoSY.postHashtags()); + post.setPostHashtags(postHashtags); + + // DB 저장 + postRepository.save(post); + + // redis 저장 + postCacheService.cache(post); + + return new PostResponseDto_SY(post); + } + + public Page getPostsByHashtag(String hashtagName, int page, int size) { + System.out.println("Fetching posts with hashtag: " + hashtagName + ", page: " + page + ", size: " + size); + Page postsPage = postRepository.findPostsByHashtagName(hashtagName, PageRequest.of(page, size)); + + if (postsPage == null) { + throw new RuntimeException("No posts found for the given hashtag."); + } + + System.out.println("Total elements: " + postsPage.getTotalElements()); + System.out.println("Total pages: " + postsPage.getTotalPages()); + System.out.println("Current page: " + postsPage.getNumber()); + System.out.println("Number of posts on this page: " + postsPage.getNumberOfElements()); + return postsPage; + } + +} diff --git a/src/main/java/com/goormy/hackathon/service/UserService.java b/src/main/java/com/goormy/hackathon/service/UserService.java new file mode 100644 index 0000000..bd0772a --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/UserService.java @@ -0,0 +1,17 @@ +package com.goormy.hackathon.service; + +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.repository.JPA.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + public User findById(Long id) { + return userRepository.findById(id).orElse(null); + } +} diff --git a/src/main/java/com/goormy/hackathon/service/dummy.txt b/src/main/java/com/goormy/hackathon/service/dummy.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..8389b97 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,39 @@ +spring: + data: + redis: + host: localhost + port: 6379 + datasource: + url: ${MYSQL_URL:jdbc:mysql://gureumi-rds.cfgaqgkoy31u.ap-northeast-2.rds.amazonaws.com}:${MYSQL_PORT:3306}/${MYSQL_SCHEMA:gureumi-rds} + driver-class-name: com.mysql.cj.jdbc.Driver + username: ${MYSQL_USERNAME:admin} + password: ${MYSQL_PASSWORD:rnfmalelql3} + jpa: + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate: + format_sql: true + use_sql_comments: true + profiles: + include: + - redis + - swagger + cloud: + aws: + region: + static: ${{ secrets.AWS_REGION }} + credentials: + access-key: ${{ secrets.AWS_ACCESS_KEY_ID }} + secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + sqs: + queue-url-like: https://sqs.ap-northeast-2.amazonaws.com/008971650206/GoormySQSForLike + queue-url-follow: https://sqs.ap-northeast-2.amazonaws.com/008971650206/GoormySQS +logging: + level: + org: + hibernate: + type: + descriptor: + sql: trace diff --git a/src/test/java/com/goormy/hackathon/config/RedisConfigTest.java b/src/test/java/com/goormy/hackathon/config/RedisConfigTest.java new file mode 100644 index 0000000..fc3a8ed --- /dev/null +++ b/src/test/java/com/goormy/hackathon/config/RedisConfigTest.java @@ -0,0 +1,27 @@ +package com.goormy.hackathon.config; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +@SpringBootTest +class RedisConfigTest { + + @Autowired + RedisTemplate redisTemplate; + + @Test + void redisConnectionTest() { + final String key = "a"; + final String data = "1"; + + final ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, data); + + final String s = valueOperations.get(key); + Assertions.assertThat(s).isEqualTo(data); + } +} \ No newline at end of file diff --git a/src/test/java/com/goormy/hackathon/repository/LikeRedisRepositoryTest.java b/src/test/java/com/goormy/hackathon/repository/LikeRedisRepositoryTest.java new file mode 100644 index 0000000..107bcb0 --- /dev/null +++ b/src/test/java/com/goormy/hackathon/repository/LikeRedisRepositoryTest.java @@ -0,0 +1,26 @@ +package com.goormy.hackathon.repository; + +import com.goormy.hackathon.repository.Redis.LikeRedisRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class LikeRedisRepositoryTest { + + @Autowired + LikeRedisRepository likeRedisRepository; + + @Test + void addLike() { + } + + @Test + void cancelLike() { + } + + @Test + void findByKey() { + likeRedisRepository.findAllKeys(); + } +} \ No newline at end of file diff --git a/src/test/java/com/goormy/hackathon/service/LikeServiceTest.java b/src/test/java/com/goormy/hackathon/service/LikeServiceTest.java new file mode 100644 index 0000000..9516983 --- /dev/null +++ b/src/test/java/com/goormy/hackathon/service/LikeServiceTest.java @@ -0,0 +1,143 @@ +package com.goormy.hackathon.service; + +import com.goormy.hackathon.entity.Like; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.lambda.LikeFunction; +import com.goormy.hackathon.repository.Redis.LikeRedisRepository; +import com.goormy.hackathon.repository.JPA.LikeRepository; +import com.goormy.hackathon.repository.JPA.PostRepository; +import com.goormy.hackathon.repository.JPA.UserRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +@SpringBootTest +class LikeServiceTest { + + @Autowired + UserRepository userRepository; + + @Autowired + PostRepository postRepository; + + @Autowired + LikeFunction likeFunction; + + @Autowired + LikeRepository likeRepository; + + @Autowired + LikeRedisRepository likeRedisRepository; + + @BeforeEach + void 데이터_주입() { + User user1 = User.builder() + .name("test") + .followerCount(0) + .followingCount(0) + .password("abc").build(); + User user2 = User.builder() + .name("test") + .followerCount(0) + .followingCount(0) + .password("abc").build(); + User user3 = User.builder() + .name("test") + .followerCount(0) + .followingCount(0) + .password("abc").build(); + + userRepository.saveAll(List.of(user1, user2, user3)); + + Post post1 = Post.builder() + .user(user1) + .likeCount(0) + .content("null") + .star(3) + .build(); + Post post2 = Post.builder() + .user(user2) + .likeCount(0) + .content("null2") + .star(3) + .build(); + Post post3 = Post.builder() + .user(user1) + .likeCount(0) + .content("null3") + .star(3) + .build(); + Post post4 = Post.builder() + .user(user2) + .likeCount(0) + .content("null4") + .star(3) + .build(); + Post post5 = Post.builder() + .user(user3) + .likeCount(0) + .content("null5") + .star(3) + .build(); + + postRepository.saveAll(List.of(post1, post2, post3, post4, post5)); + + Like like = Like.builder() + .user(user3) + .post(post4) + .build(); + + likeRepository.save(like); + } + @Test + void 좋아요_추가() { + // given + + // when + likeFunction.addLike(2L,1L); + + } + + @Test + void 좋아요_삭제() { + // given + + // when + likeFunction.cancelLike(2L,1L); + + } + + @Test + void 좋아요여부_조회() { + + // given + + // when + boolean exist = likeFunction.findLike(4L, 3L); + boolean noExist = likeFunction.findLike(2L, 3L); + + // then + Assertions.assertEquals(exist, true); + Assertions.assertEquals(noExist, false); + } + + @Test + void 캐시로부터_DB로_반영() { + + // given + likeRedisRepository.set(3L, 1L, 1); + likeRedisRepository.set(3L, 2L, 1); + likeRedisRepository.set(3L, 3L, 1); + likeRedisRepository.set(4L, 3L, -1); + + // when + likeFunction.dumpToDB(); + + // then + } +} \ No newline at end of file