From e58d7a422593618c403e09b6ed62882626abbad3 Mon Sep 17 00:00:00 2001 From: 9898s Date: Sat, 28 Oct 2023 17:15:12 +0900 Subject: [PATCH 01/13] =?UTF-8?q?build:=20querydsl=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index 5c91753..fde6db1 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,12 @@ dependencies { runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + // querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' From 8b3663844fb398b20cc8bac8fba75c75419e0f60 Mon Sep 17 00:00:00 2001 From: 9898s Date: Sat, 28 Oct 2023 17:22:38 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EC=A7=81=EB=A0=AC=ED=99=94=20?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/snsIntegrationFeedService/hashtag/entity/Hashtag.java | 2 ++ .../java/com/snsIntegrationFeedService/post/entity/Post.java | 2 ++ .../postHashtag/entity/PostHashtag.java | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java b/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java index ac203a5..d852ff8 100644 --- a/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java +++ b/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java @@ -1,5 +1,6 @@ package com.snsIntegrationFeedService.hashtag.entity; +import com.fasterxml.jackson.annotation.JsonManagedReference; import com.snsIntegrationFeedService.postHashtag.entity.PostHashtag; import jakarta.persistence.*; import lombok.Getter; @@ -17,6 +18,7 @@ public class Hashtag { @Column(nullable = false, unique = true) private String name; + @JsonManagedReference @OneToMany(mappedBy = "hashtag", orphanRemoval = true) private List postHashtagList = new ArrayList<>(); } diff --git a/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java b/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java index a510892..2403ab3 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java +++ b/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonManagedReference; import com.snsIntegrationFeedService.common.entity.Timestamped; import com.snsIntegrationFeedService.postHashtag.entity.PostHashtag; import com.snsIntegrationFeedService.user.entity.User; @@ -54,6 +55,7 @@ public class Post extends Timestamped { @Column(nullable = false) private Long shareCount; + @JsonManagedReference @OneToMany(mappedBy = "post", orphanRemoval = true) private List postHashtagList = new ArrayList<>(); diff --git a/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java b/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java index 6fd99fd..5fb5685 100644 --- a/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java +++ b/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java @@ -1,5 +1,6 @@ package com.snsIntegrationFeedService.postHashtag.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.snsIntegrationFeedService.hashtag.entity.Hashtag; import com.snsIntegrationFeedService.post.entity.Post; import jakarta.persistence.*; @@ -12,10 +13,12 @@ public class PostHashtag { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @JsonBackReference @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; + @JsonBackReference @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "hashtag_id") private Hashtag hashtag; From 990ca2db47d34d60813de49cf92a0c69e3e9fca6 Mon Sep 17 00:00:00 2001 From: 9898s Date: Sat, 28 Oct 2023 17:23:31 +0900 Subject: [PATCH 03/13] =?UTF-8?q?complete:=20querydsl=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/QuerydslConfig.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java diff --git a/src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java b/src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java new file mode 100644 index 0000000..505423e --- /dev/null +++ b/src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java @@ -0,0 +1,21 @@ +package com.snsIntegrationFeedService.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +@Configuration +public class QuerydslConfig { + + private final EntityManager em; + + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(em); + } +} From 461fe6bf1d416946b38d97f69cb8ce303adeee56 Mon Sep 17 00:00:00 2001 From: 9898s Date: Sat, 28 Oct 2023 17:26:29 +0900 Subject: [PATCH 04/13] =?UTF-8?q?complete:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/controller/PostController.java | 19 +++++ .../post/dto/PostsResponseDto.java | 26 ++++++ .../post/repository/PostRepository.java | 2 +- .../post/repository/PostRepositoryCustom.java | 13 +++ .../post/repository/PostRepositoryImpl.java | 85 +++++++++++++++++++ .../post/service/PostService.java | 22 +++++ 6 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/snsIntegrationFeedService/post/dto/PostsResponseDto.java create mode 100644 src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java create mode 100644 src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java diff --git a/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java b/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java index e024f63..d750084 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java +++ b/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java @@ -3,9 +3,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.snsIntegrationFeedService.post.dto.PostDetailResponseDto; +import com.snsIntegrationFeedService.post.dto.PostsResponseDto; import com.snsIntegrationFeedService.post.service.PostService; import io.swagger.v3.oas.annotations.Operation; @@ -25,4 +27,21 @@ public ResponseEntity getPostDetail(@PathVariable String PostDetailResponseDto postDetailResponseDto = postService.getPostDetail(postId); return ResponseEntity.ok().body(postDetailResponseDto); } + + @Operation(summary = "게시글 목록", description = "유저가 검색한 게시글 목록을 보는 API") + @GetMapping("/api/posts") + public ResponseEntity getPosts( + @RequestParam(required = false) String hashtag, + @RequestParam(required = false) String type, + @RequestParam(name = "order_by", defaultValue = "created_at", required = false) String orderBy, + @RequestParam(name = "sort_by", defaultValue = "desc", required = false) String sortBy, + @RequestParam(name = "search_by", required = false) String searchBy, + @RequestParam(required = false) String search, + @RequestParam(name = "page_count", defaultValue = "10", required = false) int pageCount, + @RequestParam(name = "page", defaultValue = "0", required = false) int page + ) { + PostsResponseDto posts = + postService.getPosts(hashtag, type, orderBy, sortBy, searchBy, search, pageCount, page); + return ResponseEntity.ok().body(posts); + } } diff --git a/src/main/java/com/snsIntegrationFeedService/post/dto/PostsResponseDto.java b/src/main/java/com/snsIntegrationFeedService/post/dto/PostsResponseDto.java new file mode 100644 index 0000000..d4f0667 --- /dev/null +++ b/src/main/java/com/snsIntegrationFeedService/post/dto/PostsResponseDto.java @@ -0,0 +1,26 @@ +package com.snsIntegrationFeedService.post.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +public class PostsResponseDto { + private List data; + private int pageCount; + private int page; + + public static PostsResponseDto from(List list, int pageCount, int page) { + return PostsResponseDto.builder() + .data(list) + .pageCount(pageCount) + .page(page) + .build(); + } +} diff --git a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepository.java b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepository.java index 4b734a5..9065c59 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepository.java +++ b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepository.java @@ -6,7 +6,7 @@ import com.snsIntegrationFeedService.post.entity.Post; -public interface PostRepository extends JpaRepository { +public interface PostRepository extends JpaRepository, PostRepositoryCustom { Optional findByPostId(String postId); } \ No newline at end of file diff --git a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java new file mode 100644 index 0000000..6e11a35 --- /dev/null +++ b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java @@ -0,0 +1,13 @@ +package com.snsIntegrationFeedService.post.repository; + +import java.util.List; + +import com.snsIntegrationFeedService.post.entity.Post; + +public interface PostRepositoryCustom { + + List findWithFilter( + String hashtag, String type, String orderBy, String sortBy, String searchBy, String search, + int pageCount, int page + ); +} \ No newline at end of file diff --git a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java new file mode 100644 index 0000000..a11abb1 --- /dev/null +++ b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java @@ -0,0 +1,85 @@ +package com.snsIntegrationFeedService.post.repository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.snsIntegrationFeedService.hashtag.entity.QHashtag; +import com.snsIntegrationFeedService.post.entity.Post; +import com.snsIntegrationFeedService.post.entity.PostTypeEnum; +import com.snsIntegrationFeedService.post.entity.QPost; +import com.snsIntegrationFeedService.postHashtag.entity.QPostHashtag; +import com.snsIntegrationFeedService.user.entity.QUser; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Repository +public class PostRepositoryImpl implements PostRepositoryCustom { + private final JPAQueryFactory queryFactory; + + @Override + public List findWithFilter( + String hashtag, String type, String orderBy, String sortBy, String searchBy, String search, + int pageCount, int page + ) { + QPost qPost = QPost.post; + QPostHashtag qPostHashtag = QPostHashtag.postHashtag; + QHashtag qHashtag = QHashtag.hashtag; + QUser qUser = QUser.user; + + JPAQuery query = queryFactory.selectFrom(qPost) + .leftJoin(qPost.postHashtagList, qPostHashtag).fetchJoin() + .leftJoin(qPostHashtag.hashtag, qHashtag).fetchJoin(); + + // 해시태그 + if (hashtag != null) { + query.where(qHashtag.name.eq(hashtag)); + } else { + query.where(qPost.user.eq(qUser)); + } + + // 타입 + if (type != null) { + query.where(qPost.type.eq(PostTypeEnum.valueOf(type))); + } + + // 정렬 + OrderSpecifier orderSpecifier = getOrderSpecifier(qPost, orderBy, sortBy); + + // 검색 + BooleanExpression searchExpression = getSearchExpression(qPost, searchBy, search); + + return query.orderBy(orderSpecifier) + .where(searchExpression) + .offset((long)page * pageCount) + .limit(pageCount) + .fetch(); + } + + private OrderSpecifier getOrderSpecifier(QPost post, String orderBy, String sortBy) { + Map> orderMap = new HashMap<>(); + orderMap.put("created_at", sortBy.equals("desc") ? post.createdAt.desc() : post.createdAt.asc()); + orderMap.put("updated_at", sortBy.equals("desc") ? post.modifiedAt.desc() : post.modifiedAt.asc()); + orderMap.put("like_count", sortBy.equals("desc") ? post.likeCount.desc() : post.likeCount.asc()); + orderMap.put("share_count", sortBy.equals("desc") ? post.shareCount.desc() : post.shareCount.asc()); + orderMap.put("view_count", sortBy.equals("desc") ? post.viewCount.desc() : post.viewCount.asc()); + return orderMap.getOrDefault(orderBy, orderMap.get("created_at")); + } + + private BooleanExpression getSearchExpression(QPost qPost, String searchBy, String search) { + if ("title".equals(searchBy)) { + return qPost.title.contains(search); + } else if ("content".equals(searchBy)) { + return qPost.content.contains(search); + } else { + return qPost.title.contains(search).or(qPost.content.contains(search)); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java b/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java index 80d6a32..e470a22 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java +++ b/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java @@ -8,6 +8,7 @@ import com.snsIntegrationFeedService.common.error.CustomErrorCode; import com.snsIntegrationFeedService.common.exception.CustomException; import com.snsIntegrationFeedService.post.dto.PostDetailResponseDto; +import com.snsIntegrationFeedService.post.dto.PostsResponseDto; import com.snsIntegrationFeedService.post.entity.Post; import com.snsIntegrationFeedService.post.repository.PostRepository; @@ -37,4 +38,25 @@ public PostDetailResponseDto getPostDetail(String postId) { post.view(); return PostDetailResponseDto.from(post, hashTags); } + + @Transactional(readOnly = true) + public PostsResponseDto getPosts( + String hashtag, String type, String orderBy, String sortBy, String searchBy, String search, + int pageCount, int page + ) { + List posts = postRepository.findWithFilter( + hashtag, type, orderBy, sortBy, searchBy, search, pageCount, page + ); + + List postDetailResponseDtos = posts.stream() + .map(post -> { + List hashTags = post.getPostHashtagList().stream() + .map(postHashtag -> postHashtag.getHashtag().getName()) + .toList(); + return PostDetailResponseDto.from(post, hashTags); + }) + .toList(); + + return PostsResponseDto.from(postDetailResponseDtos, pageCount, page); + } } From 85c588e9bd23707e2744f081cf127805859b6cbd Mon Sep 17 00:00:00 2001 From: 9898s Date: Sun, 29 Oct 2023 13:11:44 +0900 Subject: [PATCH 05/13] =?UTF-8?q?docs:=20readme=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 305 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 303 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index edd421b..fe33dc3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,303 @@ -# SNS-Integration-Feed-Service -소셜 미디어 통합 Feed 서비스 +# ❤️ 소셜 미디어 통합 Feed 서비스 + +## 🌟 프로젝트 소개 + +- 본 서비스는 유저 계정의 해시태그를 기반으로 인스타그램, 스레드, 페이스북, 트위터 등 복수의 SNS에 게시된 게시물 중 유저의 해시태그가 포함된 게시물들을 하나의 서비스에서 확인할 수 있는 통합 Feed + 어플리케이션 입니다. + +## 💻 기술 스택 + +## 🖼 ERD + +![Wanted-Assignment-1](https://github.com/Wanted-Internship-Team-Careerly/SNS-Integration-Feed-Service/assets/46531692/678f1f27-70e4-4127-8f90-382b94098337) + +## 📐 아키텍처 + +![Blank board](https://github.com/Wanted-Internship-Team-Careerly/SNS-Integration-Feed-Service/assets/46531692/06ae2bc7-621f-4a9d-bdb9-837022f694bc) + +## 🗂 API 명세서 + +### 오류 + +
+더보기 +예외가 발생했을 때, 본문에 해당 문제를 기술한 JSON 객체가 담겨있습니다. + +| Path | Type | Description | +|-----------------|----------|-------------| +| `statusCode` | `int` | 상태 코드 | +| `statusMessage` | `String` | 상태 메세지 | + +예를 들어, 이미 가입된 계정이 존재할 경우 다음과 같은 응답을 받게 됩니다. + +``` http request +{ + "statusCode": "400", + "statusMessage": "이미 존재하는 사용자입니다." +} +``` + +
+ +### 사용자 + +
+더보기 + +> 사용자 리소스는 회원 가입, 로그인을 할 때 사용됩니다. + +### 가입 + +`POST` 요청을 사용해서 새 계정을 등록할 수 있습니다. + +#### Request fields + +| Path | Type | Description | +|------------|----------|-------------| +| `account` | `String` | 계정 | +| `email` | `String` | 이메일 | +| `password` | `String` | 비밀번호 | + +#### Example request + +``` http request +{ + "account": "test", + "email": "test@test.com", + "password": "test1234" +} +``` + +#### Response fields + +| Path | Type | Description | +|-----------------|----------|-------------| +| `statusCode` | `int` | 상태 코드 | +| `statusMessage` | `String` | 상태 메세지 | + +#### Example response + +``` http request +{ + "statusCode": "200", + "statusMessage": "회원 가입 완료" +} +``` + +
+ +### 게시물 + +
+더보기 + +> 게시물 리소스는 목록, 상세보기, 좋아요, 공유, 통계를 확인 할 때 사용됩니다. + +### 목록 + +`GET` 요청을 사용해서 게시물 목록을 확인할 수 있습니다. + +#### Parameter fields + +| Path | Type | Description | +|--------------|----------|-------------| +| `hashtag` | `String` | 해시태그 | +| `type` | `String` | 타입 | +| `order_by` | `String` | 정렬 기준 | +| `sort_by` | `String` | 정렬 순서 | +| `search_by` | `String` | 검색 기준 | +| `search` | `String` | 검색 키워드 | +| `page_count` | `int` | 페이지당 게시물 갯수 | +| `page` | `int` | 조회 하려는 페이지 | + +#### Example request + +> /api/posts/search_by=title&search=제목 + +#### Response fields + +| Path | Type | Description | +|--------------|-----------------|-------------| +| `postId` | `String` | 게시물 고유 인식값 | +| `type` | `String` | 게시물 게시 유형 | +| `title` | `String` | 게시물 제목 | +| `content` | `String` | 게시물 내용 | +| `hashtag` | `String` | 게시물 태그 | +| `viewCount` | `Long` | 게시글 조회 수 | +| `likeCount` | `Long` | 게시글 좋아요 수 | +| `shareCount` | `Long` | 게시글 공유 수 | +| `createdAt` | `LocalDateTime` | 게시글 생성일자 | +| `updatedAt` | `LocalDateTime` | 게시글 수정일자 | + +#### Example response + +``` http request +{ + "data": [ + { + "postId": "12345", + "type": "facebook", + "title": "맛집 추천", + "content": "성수동에 위치한 최고의...", + "hashtag": "맛집", + "viewCount": 500, + "likeCount": 100, + "shareCount": 50, + "createdAt": "2023-10-25 12:00:00", + "updatedAt": "2023-10-25 12:00:00" + } + ], + "page_count": 10, + "page": 0 +} +``` + +### 상세보기 + +`GET` 요청을 사용해서 게시물을 상세하게 확인할 수 있습니다. + +#### PathVariable fields + +| Path | Type | Description | +|----------|----------|-------------| +| `postId` | `String` | 게시물 고유 인식값 | + +#### Example request + +> /api/post/{postId} + +#### Response fields + +| Path | Type | Description | +|--------------|-----------------|-------------| +| `postId` | `String` | 게시물 고유 인식값 | +| `type` | `String` | 게시물 게시 유형 | +| `title` | `String` | 게시물 제목 | +| `content` | `String` | 게시물 내용 | +| `hashtag` | `String` | 게시물 태그 | +| `viewCount` | `Long` | 게시글 조회 수 | +| `likeCount` | `Long` | 게시글 좋아요 수 | +| `shareCount` | `Long` | 게시글 공유 수 | +| `createdAt` | `LocalDateTime` | 게시글 생성일자 | +| `updatedAt` | `LocalDateTime` | 게시글 수정일자 | + +#### Example response + +``` http request +{ + "postId": "12345", + "type": "facebook", + "title": "맛집 추천", + "content": "성수동에 위치한 최고의...", + "hashtag": "맛집", + "viewCount": 500, + "likeCount": 100, + "shareCount": 50, + "createdAt": "2023-10-25 12:00:00", + "updatedAt": "2023-10-25 12:00:00" +} +``` + +### 좋아요 + +`POST` 요청을 사용해서 게시물 좋아요를 할 수 있습니다. + +#### Pathvariable fields + +| Path | Type | Description | +|----------|----------|-------------| +| `postId` | `String` | 게시물 고유 인식값 | + +#### Example request + +> /api/post/like/{postId} + +#### Response fields + +| Path | Type | Description | +|-----------|----------|-------------| +| `message` | `String` | 응답 메세지 | + +#### Example response + +``` http request +{ + "message": "페이스북 게시물 좋아요 완료" +} +``` + +### 공유 + +`POST` 요청을 사용해서 게시물 공유 할 수 있습니다. + +#### Pathvariable fields + +| Path | Type | Description | +|----------|----------|-------------| +| `postId` | `String` | 게시물 고유 인식값 | + +#### Example request + +> /api/post/share/{postId} + +#### Response fields + +| Path | Type | Description | +|-----------|----------|-------------| +| `message` | `String` | 응답 메세지 | + +#### Example response + +``` http request +{ + "message": "페이스북 게시물 공유 완료" +} +``` + +### 통계 + +`GET` 요청을 사용해서 게시물 통계를 확인할 수 있습니다. + +#### Parameter fields + +| Path | Type | Description | +|-----------|----------|--------------------------------------------| +| `hashtag` | `String` | 해시태그 | +| `type` | `String` | date, hour | +| `start` | `Date` | 조회 기준 시작일 | +| `end` | `Date` | 정렬 기준 마지막일 | +| `value` | `String` | count, view_count, like_count, share_count | + +#### Example request + +> /api/posts/statics?hashtag=사과&type=date + +#### Example response + +``` http request +{ + "header": { + "code": 200, + "message": "SUCCESS" + }, + "body": { + "count" : { + "2023-10-01":0, + "2023-10-02":5 + } + } +} +``` + +
+ +## 😵‍💫 트러블 슈팅 + +## 🔥 Careerly + +| 이름 | Github | +|-----|-----------------------------| +| 김수환 | https://github.com/9898s | +| 김정석 | https://github.com/dyori04 | +| 이종훈 | https://github.com/rivkode | +| 표지수 | https://github.com/JisooPyo | From cebfabe6ee3ff49c3bc554ef5ecf0d82650336e3 Mon Sep 17 00:00:00 2001 From: dyori04 <65892441+dyori04@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:42:00 +0900 Subject: [PATCH 06/13] Update README.md docs : add tech stack Icon --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fe33dc3..38d0fa3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ 어플리케이션 입니다. ## 💻 기술 스택 - +
+ + +
## 🖼 ERD ![Wanted-Assignment-1](https://github.com/Wanted-Internship-Team-Careerly/SNS-Integration-Feed-Service/assets/46531692/678f1f27-70e4-4127-8f90-382b94098337) From e9fab45ce3db2963d464351e8d3474a72d27c126 Mon Sep 17 00:00:00 2001 From: dyori04 <65892441+dyori04@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:42:49 +0900 Subject: [PATCH 07/13] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 38d0fa3..7675ec0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@
+
+ ## 🖼 ERD ![Wanted-Assignment-1](https://github.com/Wanted-Internship-Team-Careerly/SNS-Integration-Feed-Service/assets/46531692/678f1f27-70e4-4127-8f90-382b94098337) From f558d33e83ad2e78c7ec2261eb1e38ced14a52b7 Mon Sep 17 00:00:00 2001 From: dyori04 <65892441+dyori04@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:55:19 +0900 Subject: [PATCH 08/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7675ec0..1d1dfe3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@
- +
## 🖼 ERD ![Wanted-Assignment-1](https://github.com/Wanted-Internship-Team-Careerly/SNS-Integration-Feed-Service/assets/46531692/678f1f27-70e4-4127-8f90-382b94098337) From b41fcb58070cf839da2dfd22b7e2abe74eb4cff3 Mon Sep 17 00:00:00 2001 From: dyori04 <65892441+dyori04@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:58:24 +0900 Subject: [PATCH 09/13] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 1d1dfe3..e278429 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,13 @@ + + + +
+ ## 🖼 ERD ![Wanted-Assignment-1](https://github.com/Wanted-Internship-Team-Careerly/SNS-Integration-Feed-Service/assets/46531692/678f1f27-70e4-4127-8f90-382b94098337) From 638481f268833b3dd3846c41da73520404fce332 Mon Sep 17 00:00:00 2001 From: dyori04 <65892441+dyori04@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:59:07 +0900 Subject: [PATCH 10/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e278429..3278936 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - +
From e175904004516a11704eb848d068bba24baf1110 Mon Sep 17 00:00:00 2001 From: 9898s Date: Wed, 8 Nov 2023 19:20:25 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor:=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/QuerydslConfig.java | 8 +-- .../post/controller/PostController.java | 9 ++- .../post/repository/PostRepositoryCustom.java | 7 +- .../post/repository/PostRepositoryImpl.java | 71 ++++++++++++++----- .../post/service/PostService.java | 29 ++++++-- 5 files changed, 92 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java b/src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java index 505423e..878f71e 100644 --- a/src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java +++ b/src/main/java/com/snsIntegrationFeedService/common/config/QuerydslConfig.java @@ -6,16 +6,16 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; -import lombok.AllArgsConstructor; +import jakarta.persistence.PersistenceContext; -@AllArgsConstructor @Configuration public class QuerydslConfig { - private final EntityManager em; + @PersistenceContext + private EntityManager em; @Bean public JPAQueryFactory queryFactory() { return new JPAQueryFactory(em); } -} +} \ No newline at end of file diff --git a/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java b/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java index d750084..ca93cdd 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java +++ b/src/main/java/com/snsIntegrationFeedService/post/controller/PostController.java @@ -1,11 +1,13 @@ package com.snsIntegrationFeedService.post.controller; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.snsIntegrationFeedService.common.security.UserDetailsImpl; import com.snsIntegrationFeedService.post.dto.PostDetailResponseDto; import com.snsIntegrationFeedService.post.dto.PostsResponseDto; import com.snsIntegrationFeedService.post.service.PostService; @@ -38,10 +40,11 @@ public ResponseEntity getPosts( @RequestParam(name = "search_by", required = false) String searchBy, @RequestParam(required = false) String search, @RequestParam(name = "page_count", defaultValue = "10", required = false) int pageCount, - @RequestParam(name = "page", defaultValue = "0", required = false) int page + @RequestParam(name = "page", defaultValue = "0", required = false) int page, + @AuthenticationPrincipal UserDetailsImpl userDetails ) { PostsResponseDto posts = - postService.getPosts(hashtag, type, orderBy, sortBy, searchBy, search, pageCount, page); + postService.getPosts(hashtag, type, orderBy, sortBy, searchBy, search, pageCount, page, userDetails); return ResponseEntity.ok().body(posts); } -} +} \ No newline at end of file diff --git a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java index 6e11a35..b34af0f 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java +++ b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryCustom.java @@ -1,5 +1,7 @@ package com.snsIntegrationFeedService.post.repository; +import com.snsIntegrationFeedService.post.dto.request.StaticsRequestDto; +import java.util.Date; import java.util.List; import com.snsIntegrationFeedService.post.entity.Post; @@ -8,6 +10,7 @@ public interface PostRepositoryCustom { List findWithFilter( String hashtag, String type, String orderBy, String sortBy, String searchBy, String search, - int pageCount, int page - ); + int pageCount, int page, String account); + + int findByStaticsRequest(StaticsRequestDto request, Date date); } \ No newline at end of file diff --git a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java index a11abb1..fda20b8 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java +++ b/src/main/java/com/snsIntegrationFeedService/post/repository/PostRepositoryImpl.java @@ -1,5 +1,15 @@ package com.snsIntegrationFeedService.post.repository; +import static com.snsIntegrationFeedService.hashtag.entity.QHashtag.hashtag; +import static com.snsIntegrationFeedService.post.entity.QPost.*; +import static com.snsIntegrationFeedService.postHashtag.entity.QPostHashtag.*; + +import com.snsIntegrationFeedService.post.dto.request.StaticsRequestDto; +import com.snsIntegrationFeedService.post.entity.QPost; +import com.snsIntegrationFeedService.postHashtag.entity.QPostHashtag; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -13,9 +23,6 @@ import com.snsIntegrationFeedService.hashtag.entity.QHashtag; import com.snsIntegrationFeedService.post.entity.Post; import com.snsIntegrationFeedService.post.entity.PostTypeEnum; -import com.snsIntegrationFeedService.post.entity.QPost; -import com.snsIntegrationFeedService.postHashtag.entity.QPostHashtag; -import com.snsIntegrationFeedService.user.entity.QUser; import lombok.RequiredArgsConstructor; @@ -27,34 +34,31 @@ public class PostRepositoryImpl implements PostRepositoryCustom { @Override public List findWithFilter( String hashtag, String type, String orderBy, String sortBy, String searchBy, String search, - int pageCount, int page + int pageCount, int page, String account ) { - QPost qPost = QPost.post; - QPostHashtag qPostHashtag = QPostHashtag.postHashtag; QHashtag qHashtag = QHashtag.hashtag; - QUser qUser = QUser.user; - JPAQuery query = queryFactory.selectFrom(qPost) - .leftJoin(qPost.postHashtagList, qPostHashtag).fetchJoin() - .leftJoin(qPostHashtag.hashtag, qHashtag).fetchJoin(); + JPAQuery query = queryFactory.selectFrom(post) + .leftJoin(post.postHashtagList, postHashtag) + .leftJoin(postHashtag.hashtag, qHashtag); // 해시태그 if (hashtag != null) { query.where(qHashtag.name.eq(hashtag)); } else { - query.where(qPost.user.eq(qUser)); + query.where(qHashtag.name.eq(account)); } // 타입 if (type != null) { - query.where(qPost.type.eq(PostTypeEnum.valueOf(type))); + query.where(post.type.eq(PostTypeEnum.valueOf(type))); } // 정렬 - OrderSpecifier orderSpecifier = getOrderSpecifier(qPost, orderBy, sortBy); + OrderSpecifier orderSpecifier = getOrderSpecifier(orderBy, sortBy); // 검색 - BooleanExpression searchExpression = getSearchExpression(qPost, searchBy, search); + BooleanExpression searchExpression = getSearchExpression(searchBy, search); return query.orderBy(orderSpecifier) .where(searchExpression) @@ -63,7 +67,36 @@ public List findWithFilter( .fetch(); } - private OrderSpecifier getOrderSpecifier(QPost post, String orderBy, String sortBy) { + // todo + // 1. hashtag의 id를 name=request.getHashtag() 를 통해 가져온다 + // 2. post_hashtag 테이블에서 해당 hashtag_id를 가진 post를 가져온다 + // 3. 해당 post들중 기간에 맞는 post의 개수를 출력한다 + @Override + public int findByStaticsRequest(StaticsRequestDto request, Date date) { + + String hashtagName = request.getHashtag(); // request에서 hashtag 이름을 가져옵니다. + // 기간의 시작 시간을 설정합니다. + LocalDateTime startOfDay = date.toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate().atStartOfDay(ZoneId.systemDefault()).toLocalDateTime(); + // 기간의 종료 시간을 설정합니다. + LocalDateTime endOfDay = date.toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate().atTime(23, 59, 59); + + Long hashtagId = queryFactory.select(hashtag.id) + .from(hashtag) + .where(hashtag.name.eq(hashtagName)) + .fetchOne(); + + return queryFactory.selectFrom(post) + .innerJoin(postHashtag) + .on(post.id.eq(postHashtag.post.id)) + .where(postHashtag.hashtag.id.eq(hashtagId) + .and(post.createdAt.between(startOfDay, endOfDay))) + .fetch().size(); + + } + + private OrderSpecifier getOrderSpecifier(String orderBy, String sortBy) { Map> orderMap = new HashMap<>(); orderMap.put("created_at", sortBy.equals("desc") ? post.createdAt.desc() : post.createdAt.asc()); orderMap.put("updated_at", sortBy.equals("desc") ? post.modifiedAt.desc() : post.modifiedAt.asc()); @@ -73,13 +106,13 @@ private OrderSpecifier getOrderSpecifier(QPost post, String orderBy, String s return orderMap.getOrDefault(orderBy, orderMap.get("created_at")); } - private BooleanExpression getSearchExpression(QPost qPost, String searchBy, String search) { + private BooleanExpression getSearchExpression(String searchBy, String search) { if ("title".equals(searchBy)) { - return qPost.title.contains(search); + return post.title.contains(search); } else if ("content".equals(searchBy)) { - return qPost.content.contains(search); + return post.content.contains(search); } else { - return qPost.title.contains(search).or(qPost.content.contains(search)); + return post.title.contains(search).or(post.content.contains(search)); } } } \ No newline at end of file diff --git a/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java b/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java index e470a22..a8018d6 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java +++ b/src/main/java/com/snsIntegrationFeedService/post/service/PostService.java @@ -1,5 +1,10 @@ package com.snsIntegrationFeedService.post.service; +import com.snsIntegrationFeedService.hashtag.entity.Hashtag; +import com.snsIntegrationFeedService.hashtag.service.HashtagService; +import com.snsIntegrationFeedService.post.dto.request.CreatePostRequestDto; +import com.snsIntegrationFeedService.postHashtag.service.PostHashtagService; +import com.snsIntegrationFeedService.user.entity.User; import java.util.List; import org.springframework.stereotype.Service; @@ -7,6 +12,7 @@ import com.snsIntegrationFeedService.common.error.CustomErrorCode; import com.snsIntegrationFeedService.common.exception.CustomException; +import com.snsIntegrationFeedService.common.security.UserDetailsImpl; import com.snsIntegrationFeedService.post.dto.PostDetailResponseDto; import com.snsIntegrationFeedService.post.dto.PostsResponseDto; import com.snsIntegrationFeedService.post.entity.Post; @@ -22,6 +28,9 @@ public class PostService { private final PostRepository postRepository; + private final HashtagService hashtagService; + private final PostHashtagService postHashtagService; + @Transactional public PostDetailResponseDto getPostDetail(String postId) { // 예외 처리 @@ -39,13 +48,25 @@ public PostDetailResponseDto getPostDetail(String postId) { return PostDetailResponseDto.from(post, hashTags); } + public Post createPost(User user, CreatePostRequestDto request) { + // post 생성 + Post savedPost = postRepository.save(request.toEntity(user, request)); + + // hashtag 생성 + Hashtag hashtag = hashtagService.createHashtag(request.getHashtag()); + + // postHashtag 생성 + postHashtagService.createPostHashtag(savedPost, hashtag); + + return savedPost; + } + @Transactional(readOnly = true) public PostsResponseDto getPosts( String hashtag, String type, String orderBy, String sortBy, String searchBy, String search, - int pageCount, int page - ) { + int pageCount, int page, UserDetailsImpl userDetails) { List posts = postRepository.findWithFilter( - hashtag, type, orderBy, sortBy, searchBy, search, pageCount, page + hashtag, type, orderBy, sortBy, searchBy, search, pageCount, page, userDetails.getAccount() ); List postDetailResponseDtos = posts.stream() @@ -59,4 +80,4 @@ public PostsResponseDto getPosts( return PostsResponseDto.from(postDetailResponseDtos, pageCount, page); } -} +} \ No newline at end of file From 00a9fcdf0b31ee1de54247c66d5cd72113263608 Mon Sep 17 00:00:00 2001 From: 9898s Date: Wed, 8 Nov 2023 19:30:34 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor:=20@JsonManagedReference,=20@Jso?= =?UTF-8?q?nBackReference=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hashtag/entity/Hashtag.java | 26 +++++++------------ .../post/entity/Post.java | 2 -- .../postHashtag/entity/PostHashtag.java | 12 ++++++--- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java b/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java index 39a515a..ebcb367 100644 --- a/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java +++ b/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java @@ -1,14 +1,18 @@ package com.snsIntegrationFeedService.hashtag.entity; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import com.snsIntegrationFeedService.postHashtag.entity.PostHashtag; -import jakarta.persistence.*; -import lombok.Builder; -import lombok.Getter; - import java.util.ArrayList; import java.util.List; +import com.snsIntegrationFeedService.postHashtag.entity.PostHashtag; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.Getter; + @Entity @Getter public class Hashtag { @@ -19,16 +23,6 @@ public class Hashtag { @Column(nullable = false, unique = true) private String name; - @JsonManagedReference @OneToMany(mappedBy = "hashtag", orphanRemoval = true) private List postHashtagList = new ArrayList<>(); - - @Builder - public Hashtag(String name) { - this.name = name; - } - - public Hashtag() { - - } } diff --git a/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java b/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java index 9d466b9..628934c 100644 --- a/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java +++ b/src/main/java/com/snsIntegrationFeedService/post/entity/Post.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.annotation.JsonManagedReference; import com.snsIntegrationFeedService.common.entity.Timestamped; import com.snsIntegrationFeedService.postHashtag.entity.PostHashtag; import com.snsIntegrationFeedService.user.entity.User; @@ -57,7 +56,6 @@ public class Post extends Timestamped { @Column(nullable = false) private Long shareCount; - @JsonManagedReference @OneToMany(mappedBy = "post", orphanRemoval = true) private List postHashtagList = new ArrayList<>(); diff --git a/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java b/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java index 4d2e2c1..f5c476c 100644 --- a/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java +++ b/src/main/java/com/snsIntegrationFeedService/postHashtag/entity/PostHashtag.java @@ -1,9 +1,15 @@ package com.snsIntegrationFeedService.postHashtag.entity; -import com.fasterxml.jackson.annotation.JsonBackReference; import com.snsIntegrationFeedService.hashtag.entity.Hashtag; import com.snsIntegrationFeedService.post.entity.Post; -import jakarta.persistence.*; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.Builder; import lombok.Getter; @@ -14,12 +20,10 @@ public class PostHashtag { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @JsonBackReference @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; - @JsonBackReference @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "hashtag_id") private Hashtag hashtag; From 2fddb4a834173efbfbaf8ac9d99c8d0ccbd6a4b5 Mon Sep 17 00:00:00 2001 From: 9898s Date: Wed, 8 Nov 2023 19:39:38 +0900 Subject: [PATCH 13/13] =?UTF-8?q?refactor:=20@Builder=20=EC=9B=90=EC=83=81?= =?UTF-8?q?=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hashtag/entity/Hashtag.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java b/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java index ebcb367..8bce04f 100644 --- a/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java +++ b/src/main/java/com/snsIntegrationFeedService/hashtag/entity/Hashtag.java @@ -11,6 +11,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import lombok.Builder; import lombok.Getter; @Entity @@ -24,5 +25,14 @@ public class Hashtag { private String name; @OneToMany(mappedBy = "hashtag", orphanRemoval = true) - private List postHashtagList = new ArrayList<>(); + private final List postHashtagList = new ArrayList<>(); + + @Builder + public Hashtag(String name) { + this.name = name; + } + + public Hashtag() { + + } }