diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a8e3ea8..fce7e63 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,13 +2,11 @@ - Resolve: #{이슈번호} # 변경 유형 -* [ ] 기능 추가 +* [ ] 기능 추가 * [ ] 버그 수정 * [ ] 성능 개선 * [ ] TC 추가 * [ ] 기타 설정(환경 설정, CI/CD, 문서...) # 수정 내용 - - # 레퍼런스 diff --git a/build.gradle b/build.gradle index 2b28269..bbe2ef1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,26 +1,26 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.2' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '3.3.2' + id 'io.spring.dependency-management' version '1.1.6' } group = 'com.goormy' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } jar { @@ -33,12 +33,10 @@ jar { } 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 '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' @@ -50,17 +48,20 @@ dependencies { 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' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + + // s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.523' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } jar { manifest { diff --git a/src/main/java/com/goormy/hackathon/HackathonApplication.java b/src/main/java/com/goormy/hackathon/HackathonApplication.java index 80c6918..57ab0a6 100644 --- a/src/main/java/com/goormy/hackathon/HackathonApplication.java +++ b/src/main/java/com/goormy/hackathon/HackathonApplication.java @@ -2,14 +2,12 @@ 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) { - SpringApplication.run(HackathonApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(HackathonApplication.class, args); + } } diff --git a/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter.java b/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter.java new file mode 100644 index 0000000..92c0f7f --- /dev/null +++ b/src/main/java/com/goormy/hackathon/common/util/LocalDateTimeConverter.java @@ -0,0 +1,15 @@ +package com.goormy.hackathon.common.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public final class LocalDateTimeConverter { + + private LocalDateTimeConverter() { + } + + public static String convert(LocalDateTime value) { + return value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + +} diff --git a/src/main/java/com/goormy/hackathon/config/JpaAuditingConfig.java b/src/main/java/com/goormy/hackathon/config/JpaAuditingConfig.java index b458164..7618b0c 100644 --- a/src/main/java/com/goormy/hackathon/config/JpaAuditingConfig.java +++ b/src/main/java/com/goormy/hackathon/config/JpaAuditingConfig.java @@ -6,4 +6,5 @@ @Configuration @EnableJpaAuditing public class JpaAuditingConfig { + } diff --git a/src/main/java/com/goormy/hackathon/config/S3config.java b/src/main/java/com/goormy/hackathon/config/S3config.java new file mode 100644 index 0000000..1851989 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/config/S3config.java @@ -0,0 +1,38 @@ +package com.goormy.hackathon.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class S3config { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + @Primary + public BasicAWSCredentials awsCredentialsProvider() { + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + return basicAWSCredentials; + } + + @Bean + public AmazonS3 amazonS3() { + AmazonS3 s3Builder = AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider())) + .build(); + return s3Builder; + } + +} 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..39d037b --- /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.PostCreateRequestDto; +import com.goormy.hackathon.dto.post.PostResponseDto; +import com.goormy.hackathon.redis.entity.PostRedis; +import com.goormy.hackathon.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/posts") +public class PostController { + + private final PostService postService; + + @PostMapping + public ResponseEntity createPost( + @RequestHeader("userId") Long userId, + @RequestBody PostCreateRequestDto request) { + return ResponseEntity.ok(postService.createPost(userId, request)); + } + + @DeleteMapping("/{post_id}") + public ResponseEntity deletePost( + @PathVariable("post_id") Long postId) { + postService.deletePost(postId); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{post_id}") + public ResponseEntity getPost( + @PathVariable("post_id") Long postId) { + return ResponseEntity.ok(postService.getPost(postId)); + } +} diff --git a/src/main/java/com/goormy/hackathon/dto/hashtag/HashtagResponseDto.java b/src/main/java/com/goormy/hackathon/dto/hashtag/HashtagResponseDto.java new file mode 100644 index 0000000..074580d --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/hashtag/HashtagResponseDto.java @@ -0,0 +1,23 @@ +package com.goormy.hackathon.dto.hashtag; + +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.PostHashtag; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class HashtagResponseDto { + + private Long id; + private String name; + private Hashtag.Type type; + + public static HashtagResponseDto toDto(PostHashtag hashtag) { + return HashtagResponseDto.builder() + .id(hashtag.getId()) + .name(hashtag.getHashtag().getName()) + .type(hashtag.getHashtag().getType()) + .build(); + } +} diff --git a/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagRequestDto.java b/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagRequestDto.java new file mode 100644 index 0000000..dcb1210 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/hashtag/PostHashtagRequestDto.java @@ -0,0 +1,10 @@ +package com.goormy.hackathon.dto.hashtag; + +import com.goormy.hackathon.entity.Hashtag; + +public record PostHashtagRequestDto( + String name, + Hashtag.Type type +) { + +} diff --git a/src/main/java/com/goormy/hackathon/dto/post/PostCreateRequestDto.java b/src/main/java/com/goormy/hackathon/dto/post/PostCreateRequestDto.java new file mode 100644 index 0000000..979176d --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/post/PostCreateRequestDto.java @@ -0,0 +1,50 @@ +package com.goormy.hackathon.dto.post; + +import com.goormy.hackathon.common.util.LocalDateTimeConverter; +import com.goormy.hackathon.dto.hashtag.HashtagResponseDto; +import com.goormy.hackathon.dto.hashtag.PostHashtagRequestDto; +import com.goormy.hackathon.dto.user.UserResponseDto; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.PostHashtag; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.redis.entity.PostRedis; + +import java.util.List; + +import lombok.Getter; + +@Getter +public class PostCreateRequestDto { + + private String content; + private Integer star; + private String imageName; + private List hashtags; + + public Post toEntity(User user) { + return Post.builder() + .content(content) + .star(star) + .user(user) + .likeCount(0) + .imageUrl(imageName) + .build(); + } + + public PostRedis toRedisEntity(Post post) { + return PostRedis.builder() + .postId(post.getId().toString()) + .content(content) + .star(star) + .user(UserResponseDto.builder() + .userId(post.getUser().getId()) + .name(post.getUser().getName()) + .build()) + .likeCount(0) + .imageName(imageName) + .postHashtags(post.getPostHashtags().stream().map(HashtagResponseDto::toDto).toList()) + .createdAt(LocalDateTimeConverter.convert(post.getCreatedAt())) + .build(); + } +} diff --git a/src/main/java/com/goormy/hackathon/dto/post/PostDetailResponseDto.java b/src/main/java/com/goormy/hackathon/dto/post/PostDetailResponseDto.java new file mode 100644 index 0000000..7d5943f --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/post/PostDetailResponseDto.java @@ -0,0 +1,25 @@ +package com.goormy.hackathon.dto.post; + +import com.goormy.hackathon.dto.hashtag.HashtagResponseDto; +import com.goormy.hackathon.dto.user.UserResponseDto; +import lombok.Builder; + +import java.util.ArrayList; +import java.util.List; + +@Builder +public class PostDetailResponseDto { + + private Long id; + private UserResponseDto user; + private String content; + private long likeCount; + private String imageUrl; + private int str; + + private List hashtags = new ArrayList<>(); + + // created_at 추가 + + private boolean isLike; +} diff --git a/src/main/java/com/goormy/hackathon/dto/post/PostRedisSaveDto.java b/src/main/java/com/goormy/hackathon/dto/post/PostRedisSaveDto.java new file mode 100644 index 0000000..5b7b4c1 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/post/PostRedisSaveDto.java @@ -0,0 +1,45 @@ +package com.goormy.hackathon.dto.post; + +import com.goormy.hackathon.dto.hashtag.HashtagResponseDto; +import com.goormy.hackathon.dto.user.UserResponseDto; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.PostHashtag; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; + +@Builder +@Getter +public class PostRedisSaveDto implements Serializable { + + @Id + private String postId; + private String content; + private Integer star; + private int likeCount; + private UserResponseDto user; + private String imageName; + private List postHashtags = new ArrayList<>(); + + public static PostRedisSaveDto toDto(Post post, List hashtags) { + return PostRedisSaveDto.builder() + .postId(post.getId() + "") + .content(post.getContent()) + .star(post.getStar()) + .likeCount(post.getLikeCount()) + .user(UserResponseDto.builder() + .userId(post.getUser().getId()) + .name(post.getUser().getName()) + .build()) + .imageName(post.getImageName()) + .postHashtags(hashtags.stream().map(HashtagResponseDto::toDto).toList()) + .build(); + } + + +} diff --git a/src/main/java/com/goormy/hackathon/dto/post/PostResponseDto.java b/src/main/java/com/goormy/hackathon/dto/post/PostResponseDto.java new file mode 100644 index 0000000..4214c0d --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/post/PostResponseDto.java @@ -0,0 +1,61 @@ +package com.goormy.hackathon.dto.post; + +import com.goormy.hackathon.dto.hashtag.HashtagResponseDto; +import com.goormy.hackathon.dto.user.UserResponseDto; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.redis.entity.PostRedis; + +import java.util.Collections; +import java.util.List; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class PostResponseDto { + + private Long postId; + private String content; + private Integer star; + private int likeCount; + private UserResponseDto user; + private String imageUrl; + private List postHashtags = Collections.emptyList(); + + public static PostResponseDto toDto(T post, String imageUrl) { + if (post instanceof Post data) { + return PostResponseDto.builder() + .postId(Long.parseLong(data.getId().toString())) + .content(data.getContent()) + .star(data.getStar()) + .likeCount(data.getLikeCount()) + .user(UserResponseDto.builder() + .userId(data.getUser().getId()) + .name(data.getUser().getName()) + .build()) + .imageUrl(imageUrl) + .postHashtags(data.getPostHashtags().stream().map(HashtagResponseDto::toDto).toList()) + .build(); + } else { + PostRedis data = (PostRedis) post; + return PostResponseDto.builder() + .postId(Long.parseLong(data.getPostId().toString())) + .content(data.getContent()) + .star(data.getStar()) + .likeCount(data.getLikeCount()) + .user(UserResponseDto.builder() + .userId(data.getUser().getUserId()) + .name(data.getUser().getName()) + .build()) + .imageUrl(imageUrl) + .postHashtags(data.getPostHashtags()) + .build(); + } + } + + public void updatePhoto(String imageUrl) { + this.imageUrl = imageUrl; + } + +} diff --git a/src/main/java/com/goormy/hackathon/dto/user/UserResponseDto.java b/src/main/java/com/goormy/hackathon/dto/user/UserResponseDto.java new file mode 100644 index 0000000..f1377b1 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/dto/user/UserResponseDto.java @@ -0,0 +1,16 @@ +package com.goormy.hackathon.dto.user; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.Serializable; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class UserResponseDto { + + private Long userId; + private String name; +} diff --git a/src/main/java/com/goormy/hackathon/entity/Hashtag.java b/src/main/java/com/goormy/hackathon/entity/Hashtag.java index fde4a7e..3cf6d21 100644 --- a/src/main/java/com/goormy/hackathon/entity/Hashtag.java +++ b/src/main/java/com/goormy/hackathon/entity/Hashtag.java @@ -32,8 +32,7 @@ public class Hashtag { public enum Type { LOCATION, FOOD, - RESTAURANT - ; + RESTAURANT; } @Builder diff --git a/src/main/java/com/goormy/hackathon/entity/Post.java b/src/main/java/com/goormy/hackathon/entity/Post.java index 169705a..07bf757 100644 --- a/src/main/java/com/goormy/hackathon/entity/Post.java +++ b/src/main/java/com/goormy/hackathon/entity/Post.java @@ -14,13 +14,14 @@ @Getter public class Post extends BaseTimeEntity { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "post_id") private Long id; private String content; - private String imageUrl; + private String imageName; private Integer star; @@ -36,11 +37,18 @@ public class Post extends BaseTimeEntity { @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) private List likes = new ArrayList<>(); + public void setPostHashtags(List hashtags) { + this.postHashtags = hashtags.stream() + .map(hashtag -> new PostHashtag(this, hashtag)) + .toList(); + } + + @Builder public Post(User user, String content, String imageUrl, Integer star, Integer likeCount) { this.user = user; this.content = content; - this.imageUrl = imageUrl; + this.imageName = imageUrl; this.star = star; this.likeCount = likeCount; } diff --git a/src/main/java/com/goormy/hackathon/entity/User.java b/src/main/java/com/goormy/hackathon/entity/User.java index de36e63..2be6a97 100644 --- a/src/main/java/com/goormy/hackathon/entity/User.java +++ b/src/main/java/com/goormy/hackathon/entity/User.java @@ -12,10 +12,11 @@ @Entity @NoArgsConstructor @Getter -@Table(name="users") +@Table(name = "users") 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/redis/entity/FollowCountCache.java b/src/main/java/com/goormy/hackathon/redis/entity/FollowCountCache.java new file mode 100644 index 0000000..741e101 --- /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); + } + +} \ No newline at end of file diff --git a/src/main/java/com/goormy/hackathon/redis/entity/PostRedis.java b/src/main/java/com/goormy/hackathon/redis/entity/PostRedis.java new file mode 100644 index 0000000..46fb036 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/redis/entity/PostRedis.java @@ -0,0 +1,33 @@ +package com.goormy.hackathon.redis.entity; + +import com.goormy.hackathon.common.util.LocalDateTimeConverter; +import com.goormy.hackathon.dto.hashtag.HashtagResponseDto; +import com.goormy.hackathon.dto.user.UserResponseDto; +import com.goormy.hackathon.entity.Post; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@RedisHash(value = "post_", timeToLive = 60 * 60 * 24) +@Builder +@Getter +@Data +public class PostRedis implements Serializable { + + @Id + private String postId; + private String content; + private Integer star; + private int likeCount; + private UserResponseDto user; + private String imageName; + + private List postHashtags = new ArrayList<>(); + private String createdAt; +} diff --git a/src/main/java/com/goormy/hackathon/repository/FollowCountRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/FollowCountRedisRepository.java new file mode 100644 index 0000000..4c0170c --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/FollowCountRedisRepository.java @@ -0,0 +1,25 @@ +package com.goormy.hackathon.repository; + +import com.goormy.hackathon.redis.entity.FollowCountCache; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FollowCountRedisRepository { + + private final RedisTemplate redisTemplate; + + public void save(FollowCountCache followCountCache) { + redisTemplate.opsForHash().put(followCountCache.getKey(), followCountCache.getField(), + followCountCache.getFollowCount()); + } + + public Integer findFollowCount(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/HashtagRepository.java b/src/main/java/com/goormy/hackathon/repository/HashtagRepository.java index f13623c..4105cef 100644 --- a/src/main/java/com/goormy/hackathon/repository/HashtagRepository.java +++ b/src/main/java/com/goormy/hackathon/repository/HashtagRepository.java @@ -1,9 +1,14 @@ package com.goormy.hackathon.repository; import com.goormy.hackathon.entity.Hashtag; + +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface HashtagRepository extends JpaRepository { + + Optional findByName(String name); } diff --git a/src/main/java/com/goormy/hackathon/repository/PostHashtagRepository.java b/src/main/java/com/goormy/hackathon/repository/PostHashtagRepository.java new file mode 100644 index 0000000..70da3d4 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/PostHashtagRepository.java @@ -0,0 +1,8 @@ +package com.goormy.hackathon.repository; + +import com.goormy.hackathon.entity.PostHashtag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostHashtagRepository extends JpaRepository { + +} diff --git a/src/main/java/com/goormy/hackathon/repository/PostRedisRepository.java b/src/main/java/com/goormy/hackathon/repository/PostRedisRepository.java new file mode 100644 index 0000000..adf0191 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/repository/PostRedisRepository.java @@ -0,0 +1,21 @@ +package com.goormy.hackathon.repository; + +import com.goormy.hackathon.redis.entity.PostRedis; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class PostRedisRepository { + + private final RedisTemplate redisTemplate; + + public void save(PostRedis postRedis) { + redisTemplate.opsForList().leftPush(postRedis.getPostId().toString(), postRedis); + } + + public void findById() { + + } +} \ No newline at end of file diff --git a/src/main/java/com/goormy/hackathon/repository/PostRepository.java b/src/main/java/com/goormy/hackathon/repository/PostRepository.java index 2e2bda3..d49b26b 100644 --- a/src/main/java/com/goormy/hackathon/repository/PostRepository.java +++ b/src/main/java/com/goormy/hackathon/repository/PostRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface PostRepository extends JpaRepository { + } diff --git a/src/main/java/com/goormy/hackathon/repository/UserRepository.java b/src/main/java/com/goormy/hackathon/repository/UserRepository.java index 2e801fc..44fc012 100644 --- a/src/main/java/com/goormy/hackathon/repository/UserRepository.java +++ b/src/main/java/com/goormy/hackathon/repository/UserRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { + } 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..19d8f43 --- /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; +import com.goormy.hackathon.entity.Hashtag; +import com.goormy.hackathon.redis.entity.FollowCountCache; +import com.goormy.hackathon.repository.FollowCountRedisRepository; +import com.goormy.hackathon.repository.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 followCountRedisRepository; + + @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); + followCountRedisRepository.save(followCountCache); + + return newHashtag; + }); + hashtags.add(hashtag); + } + return hashtags; + } + +} \ No newline at end of file diff --git a/src/main/java/com/goormy/hackathon/service/PhotoService.java b/src/main/java/com/goormy/hackathon/service/PhotoService.java new file mode 100644 index 0000000..b67bbec --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/PhotoService.java @@ -0,0 +1,66 @@ +package com.goormy.hackathon.service; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.Headers; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; + +import java.util.Date; +import java.util.UUID; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PhotoService { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + + /* + url 생성 함수 + */ + public String getPreSignedUrl(Long prefix, String imageName, HttpMethod httpMethod) { + imageName = createPath(prefix.toString(), imageName); + GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePresignedUrlRequest(bucket, + imageName, httpMethod); + return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString(); + } + + private GeneratePresignedUrlRequest getGeneratePresignedUrlRequest(String bucket, String fileName, + HttpMethod httpMethod) { + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucket, fileName) + .withMethod(httpMethod) + .withExpiration(getPresignedUrlExpiration()); +// generatePresignedUrlRequest.addRequestParameter( +// Headers.S3_CANNED_ACL, +// CannedAccessControlList.PublicRead.toString()); + return generatePresignedUrlRequest; + } + + private String createFileId() { + return UUID.randomUUID().toString(); + } + + private String createPath(String prefix, String fileName) { + String fileId = createFileId(); + return String.format("%s/%s", prefix, fileId + fileName); + } + + /* + 유효기간 설정 + */ + private Date getPresignedUrlExpiration() { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 2; + expiration.setTime(expTimeMillis); + return expiration; + } +} 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..0422d96 --- /dev/null +++ b/src/main/java/com/goormy/hackathon/service/PostService.java @@ -0,0 +1,109 @@ +package com.goormy.hackathon.service; + +import com.amazonaws.HttpMethod; +import com.goormy.hackathon.dto.post.PostCreateRequestDto; +import com.goormy.hackathon.dto.post.PostResponseDto; +import com.goormy.hackathon.dto.post.PostRedisSaveDto; +import com.goormy.hackathon.entity.Post; +import com.goormy.hackathon.entity.User; +import com.goormy.hackathon.redis.entity.PostRedis; +import com.goormy.hackathon.repository.PostRedisRepository; +import com.goormy.hackathon.repository.PostRepository; +import com.goormy.hackathon.repository.UserRepository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; + + +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + private final RedisTemplate postRedisTemplate; + private final UserRepository userRepository; + private final PostRedisRepository postRedisRepository; + private final PhotoService photoService; + + private final HashtagService hashtagService; + + // 포스트 생성 + @Transactional + public PostResponseDto createPost(Long userId, PostCreateRequestDto request) { + + User user = userRepository.findById(userId). + orElseThrow(() -> new IllegalArgumentException("해당 회원을 찾을 수 없습니다. ")); + + // rds에 저장 + + Post post = request.toEntity(user); + + postRepository.save(post); + + var postHashtags = hashtagService.getOrCreateHashtags(request.getHashtags()); + post.setPostHashtags(postHashtags); + + // redis 저장 Dto(photoUrl 포함) + + PostRedis redis = request.toRedisEntity(post); + postRedisRepository.save(redis); +// postRedisTemplate.opsForValue().set(post.getId() + "", saveRedisDto); + + // redis 반환 Dto(photoName 포함) + String imageUrl = photoService.getPreSignedUrl(post.getId(), request.getImageName(), + HttpMethod.PUT); + + return PostResponseDto.toDto(post, imageUrl); + } + + + // 포스트 삭제 + @Transactional + public void deletePost(Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("해당 게시물을 찾을 수 없습니다. ")); + postRepository.delete(post); + postRedisTemplate.delete(postId.toString()); + } + + // 포스트 단일 조회 + @Transactional(readOnly = true) + public PostResponseDto getPost(Long postId) { + System.out.println(postRedisTemplate.opsForValue() + .get(postId + "")); + + Object redisData = postRedisTemplate.opsForValue() + .get(postId + ""); + + String imageUrl; + if (redisData == null) { + return getPostInRDB(postId); + } + + return getPostInRedis(postId, (PostRedis) redisData); + } + + // rdb에서 조회 + private PostResponseDto getPostInRDB(Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("해당 포스트를 조회할 수 없습니다. ")); + String imageUrl = photoService.getPreSignedUrl(postId, post.getImageName(), HttpMethod.GET); + + postRedisTemplate.opsForValue().set( + post.getId().toString(), + PostRedisSaveDto.toDto(post, post.getPostHashtags()), + Duration.ofHours(24)); + + return PostResponseDto.toDto(post, imageUrl); + } + + + private PostResponseDto getPostInRedis(Long postId, PostRedis data) { + String imageUrl = photoService.getPreSignedUrl(postId, data.getImageName(), HttpMethod.GET); + return PostResponseDto.toDto(data, imageUrl); + } +} diff --git a/src/test/java/com/goormy/hackathon/HackathonApplicationTests.java b/src/test/java/com/goormy/hackathon/HackathonApplicationTests.java index b092eda..2edcf4d 100644 --- a/src/test/java/com/goormy/hackathon/HackathonApplicationTests.java +++ b/src/test/java/com/goormy/hackathon/HackathonApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class HackathonApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } }