Skip to content

Commit

Permalink
Feat: 커뮤니티 관련 API 요구사항 반영 (#71)
Browse files Browse the repository at this point in the history
* Feat(#54): resolved rebase conflicts

Feat(#54): 커뮤니티 API 기초 구현

Feat(#54): 게시글 상세 조회 시 태그 포함

Feat(#54): todo 작성

Feat(#54): 게시글 생성 시 이미지 업로드 bug fix

Feat(#54): 게시글 상세 조회 시 좋아요 여부와 작성자 프로필 사진 url 반환

Feat(#54): 게시글 상세 조회 시 좋아요 여부와 작성자 프로필 사진 url 반환

Feat(#54): 게시글 리스트 조회 시, 좋아요 누른 여부를 포함

Feat(#54): todo 주석 제거 및 더미데이터 제거

Feat(#54): 썸네일 이미지 관련 요구사항 적용
- 첫 이미지를 썸네일 이미지로 지정

Feat(#54): 게시물 카테고리 수정
- 일상, 교통, 치안, 기타

Hotfix: npe fix

Feat(#54): rename field for resolve rebase conflict

Docs(#54): update todo

Fix(#54): 좋아요 버그 수정

Fix(#54): 스웨거 jwt 버그 수정

Feat(#54): 게시글/댓글 좋아요 api 구현

Feat(#54): 알림 저장 시 닉네임도 저장

Feat(#54): 좋아요 관련 예외 로직

Feat(#54): 푸시 알림 태그 구분

Feat(#54): 푸시 알림 발송 시 db 에 저장

Feat(#54): 게시글 좋아요 api (wip)

Feat(#54): 게시글 좋아요 api (wip)

Feat(#54): improvement community apis
- 게시글 작성 시, 작성자의 주소 로깅
- 게시글에 댓글 등록 시 푸시 알림 전송

Feat(#54): improvement community apis
- 게시글 작성 시, 작성자의 주소 로깅
- 게시글에 댓글 등록 시 푸시 알림 전송

Feat(#54): catch exception

Docs(#54): todo 작성

Feat(#54): 게시글 수정하기 api 구현 및 게시글 삭제 http method 를 patch 로 변경

Feat(#54): 대댓글 계층형 조회 api 구현

Feat(#54): 대댓글 작성 api 구현

Feat(#54): 게시글에 댓글 작성하기 api 구현

Feat(#54): 게시글 조회 no offset paging api 구현

Feat(#54): removed test api for fcm

Feat(#54): removed test api for fcm

Feat(#54): 게시글 상세 조회 시, 제목 및 내용 포함

Feat(#54): 게시글 상세 조회 및 삭제 API 구현

Docs(#54): 게시글 작성 API

Feat(#54): removed Qclass

Feat(#54): 게시글 활성화 상태 필드 추가

Feat(#54): 게시글 작성 API 구현

Feat(#54): rename CommunityParticipant
- to ArticleParticipant

Feat(#54): 게시글, 댓글, 커뮤니티 참여자 레포지토리 구현

Feat(#54): relocate s3Provider

Docs(#54): 프로필 사진 업로드 api
- swagger api summary, description

Feat(#54): 커뮤니티 엔티티 설계
- Article: 동네생활 게시글
- ArticleTag: 게시글 태그
- CommentEntity: 댓글
- CommunityParticipant: 게시글 참여자

Feat(#54): 회원 프로필 사진 업로드 API 구현

Feat(#54): 회원 엔티티 필드 추가
- 프로필 사진, 닉네임, 실명, fcm 토큰

Feat(#54): implement SecurityContextProvider
- for parsing principal from jwt token

Feat(#54): member extends baseTimeEntity

* Feat(#54): fix invalid uri prefix

* Feat(#54): fix invalid uri prefix

* Feat(#54): 댓글 리스트 조회 시, 좋아요 여부 반환하도록 변경

* Feat(#54): 댓글 삭제 api 구현

* Feat(#54): 댓글 삭제 api docs 작성

* Feat(#54): comment count 관련 bug fix

* Feat(#54): removed unused field

* Feat(#54): 베스트 게시글 작성자에게 푸시알람 전송

* Feat(#54): 베스트 게시글 작성자에게 푸시알람 전송
  • Loading branch information
versatile0010 authored Nov 17, 2023
1 parent 9d418ad commit 2b297f3
Show file tree
Hide file tree
Showing 22 changed files with 165 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ public class ArticleController {
1. title 은 글 제목 입니다 (not null)
2. content 는 글 내용 입니다 (not null)
3. articleTag 는 게시글 태그 입니다. LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보)
3. articleTag 는 게시글 태그 입니다. LIFE(일상), TRAFFIC(교통), SAFETY(치안), NONE(기타) -> 영어로 보내주세요
4. imageList 는 이미지 (MultiPart) 리스트 입니다.
5. thumbNailImageIdx 는 썸네일 이미지의 인덱스 입니다. (0,1,2, ...
5. imageList 의 첫 원소를 썸네일로 지정합니다.
imageList 에 이미지를 담아서 보내는 경우,
idx 에 따라서 썸네일 이미지를 결정합니다.
""")

@PostMapping
public ResponseEntity<UploadArticleResponse> uploadArticle(@RequestBody @Valid UploadArticleRequest request) {
public ResponseEntity<UploadArticleResponse> uploadArticle(@ModelAttribute @Valid UploadArticleRequest request) {
return ResponseEntity.created(URI.create("/api/articles"))
.body(articleService.uploadArticle(request));
}
Expand Down Expand Up @@ -87,7 +87,7 @@ public ResponseEntity<GetArticleDetailResponse> getArticleDetails(@PathVariable(
@GetMapping
public ResponseEntity<Slice<GetArticleListResponse>> getArticlePages(
Pageable pageable,
@ModelAttribute ArticleSearchParameter param) { // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정
@ModelAttribute ArticleSearchParameter param) {
return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public class UploadArticleRequest {

// 이미지 관련
private List<MultipartFile> imageList; // 이미지 리스트
private Long thumbNailImageIdx; // 썸네일 이미지의 순서 (0,1,2,...)

private Double longitude;
private Double latitude;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.numberone.backend.domain.article.dto.response;

import com.numberone.backend.domain.article.entity.Article;
import com.numberone.backend.domain.article.entity.ArticleStatus;
import com.numberone.backend.domain.article.entity.ArticleTag;
import com.numberone.backend.domain.member.entity.Member;
import lombok.*;

Expand All @@ -22,18 +24,28 @@ public class GetArticleDetailResponse {
private LocalDateTime modifiedAt;
private String title;
private String content;
private boolean isLiked;
private ArticleTag articleTag;
private Long commentCount;

// 작성자 관련
private String memberName;
private String memberNickName;
private String address; // todo: 더미 데이터
private String ownerName;
private String ownerNickName;
private String address;
private Long ownerMemberId;
private String ownerProfileImageUrl;

// 이미지 관련
private List<String> imageUrls;
private String thumbNailImageUrl;

public static GetArticleDetailResponse of(Article article, List<String> imageUrls, String thumbNailImageUrl, Member member){
public static GetArticleDetailResponse of(
Article article,
List<String> imageUrls,
String thumbNailImageUrl,
Member owner,
List<Long> memberLikedArticleList,
Long commentCount ) {
return GetArticleDetailResponse.builder()
.articleId(article.getId())
.title(article.getTitle())
Expand All @@ -45,12 +57,16 @@ public static GetArticleDetailResponse of(Article article, List<String> imageUrl
)
.createdAt(article.getCreatedAt())
.modifiedAt(article.getModifiedAt())
.ownerMemberId(member.getId())
.memberName(member.getRealName())
.memberNickName(member.getNickName())
.ownerMemberId(owner.getId())
.ownerName(owner.getRealName())
.ownerNickName(owner.getNickName())
.imageUrls(imageUrls)
.thumbNailImageUrl(thumbNailImageUrl)
.address("서울시 광진구 자양동") // 교체
.address(article.getAddress())
.ownerProfileImageUrl(owner.getProfileImageUrl())
.isLiked(memberLikedArticleList.contains(article.getId()))
.articleTag(article.getArticleTag())
.commentCount(commentCount)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@ToString
Expand All @@ -31,7 +32,8 @@ public class GetArticleListResponse {
private Long thumbNailImageId;

private Integer articleLikeCount;
private Integer commentCount;
private Long commentCount;
private Boolean isLiked;


@QueryProjection
Expand All @@ -46,7 +48,6 @@ public GetArticleListResponse(Article article, Long ownerId, Long thumbNailImage
this.articleStatus = article.getArticleStatus();
this.thumbNailImageId = thumbNailImageId;
this.articleLikeCount = article.getLikeCount();
this.commentCount = article.getCommentCount();
}

public void setOwnerNickName(String nickName){
Expand All @@ -57,7 +58,14 @@ public void setThumbNailImageUrl(String thumbNailImageUrl){
this.thumbNailImageUrl = thumbNailImageUrl;
}

public void updateInfo(Optional<Member> owner, Optional<ArticleImage> articleImage){
public void setCommentCount(Long commentCount){
this.commentCount = commentCount;
}

public void updateInfo(Optional<Member> owner,
Optional<ArticleImage> articleImage,
List<Long> memberLikedArticleIdList,
Long commentCount ){
owner.ifPresentOrElse(
o -> setOwnerNickName(o.getNickName()),
() -> setOwnerNickName("알 수 없는 사용자")
Expand All @@ -66,7 +74,8 @@ public void updateInfo(Optional<Member> owner, Optional<ArticleImage> articleIma
image -> setThumbNailImageUrl(image.getImageUrl()),
() -> setThumbNailImageUrl("")
);
this.isLiked = memberLikedArticleIdList.contains(id);
this.commentCount = commentCount;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class ModifyArticleResponse {
private String thumbNailImageUrl;

// 작성자 주소
private String address; // todo: 더미 데이터
private String address;

public static ModifyArticleResponse of(Article article, List<String> imageUrls, String thumbNailImageUrl){
return ModifyArticleResponse.builder()
Expand All @@ -34,7 +34,7 @@ public static ModifyArticleResponse of(Article article, List<String> imageUrls,
.modifiedAt(article.getModifiedAt())
.imageUrls(imageUrls)
.thumbNailImageUrl(thumbNailImageUrl)
.address("서울시 광진구 자양동")
.address(article.getAddress())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class UploadArticleResponse {
private String thumbNailImageUrl;

// 작성자 주소
private String address; // todo: 더미 데이터
private String address;

public static UploadArticleResponse of(Article article, List<String> imageUrls, String thumbNailImageUrl){
return UploadArticleResponse.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ public class Article extends BaseTimeEntity {

@ColumnDefault("0")
@Comment("게시글 좋아요 개수")
private Integer likeCount; // todo: 동시성 처리

@ColumnDefault("0")
@Comment("게시글에 달린 댓글 개수")
private Integer commentCount;
private Integer likeCount;

@Comment("작성자 ID")
private Long articleOwnerId;
Expand All @@ -71,7 +67,6 @@ public Article(String title, String content, Long articleOwnerId, ArticleTag tag
this.articleOwnerId = articleOwnerId;
this.articleTag = tag;
this.articleStatus = ArticleStatus.ACTIVATED;
this.commentCount = 0;
this.likeCount = 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
@RequiredArgsConstructor
public enum ArticleTag {
LIFE, // 일상
FRAUD, // 사기
TRAFFIC, // 교통
SAFETY, // 치안
REPORT; // 제보
NONE; // 기타
private String value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import com.numberone.backend.domain.comment.dto.response.CreateCommentResponse;
import com.numberone.backend.domain.comment.entity.CommentEntity;
import com.numberone.backend.domain.comment.repository.CommentRepository;
import com.numberone.backend.domain.like.entity.ArticleLike;
import com.numberone.backend.domain.like.repository.ArticleLikeRepository;
import com.numberone.backend.domain.member.entity.Member;
import com.numberone.backend.domain.member.repository.MemberRepository;
import com.numberone.backend.domain.notification.entity.NotificationTag;
Expand Down Expand Up @@ -50,6 +52,7 @@ public class ArticleService {
private final ArticleParticipantRepository articleParticipantRepository;
private final ArticleImageRepository articleImageRepository;
private final CommentRepository commentRepository;
private final ArticleLikeRepository articleLikeRepository;
private final S3Provider s3Provider;
private final LocationProvider locationProvider;
private final FcmMessageProvider fcmMessageProvider;
Expand All @@ -68,6 +71,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) {
owner.getId(),
request.getArticleTag())
);

articleParticipantRepository.save(
new ArticleParticipant(article, owner)
);
Expand All @@ -89,7 +93,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) {
new ArticleImage(article, imageUrl)
);
articleImages.add(savedArticleImage);
if (Objects.equals(i, request.getThumbNailImageIdx())) {
if (i == 0) {
thumbNailImageUrl = imageUrl;
thumbNailImageId = savedArticleImage.getId();
}
Expand All @@ -104,6 +108,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) {
Double latitude = request.getLatitude();
Double longitude = request.getLongitude();
if (latitude != null && longitude != null) {
// 주소가 null 이 아닌 경우에만 api 요청하여 update
String address = locationProvider.pos2address(request.getLatitude(), request.getLongitude());
article.updateAddress(address);
}
Expand All @@ -122,10 +127,12 @@ public DeleteArticleResponse deleteArticle(Long articleId) {

public GetArticleDetailResponse getArticleDetail(Long articleId) {
String principal = SecurityContextProvider.getAuthenticatedUserEmail();
Member owner = memberRepository.findByEmail(principal)
Member member = memberRepository.findByEmail(principal) // 회원
.orElseThrow(NotFoundMemberException::new);
Article article = articleRepository.findById(articleId)
.orElseThrow(NotFoundArticleException::new);
Member owner = memberRepository.findById(article.getArticleOwnerId()) // 작성자
.orElseThrow(NotFoundMemberException::new);

List<String> imageUrls = articleImageRepository.findByArticle(article)
.stream()
Expand All @@ -134,32 +141,46 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) {


Optional<ArticleImage> thumbNailImage = articleImageRepository.findById(article.getThumbNailImageUrlId());
Long commentCount = commentRepository.countAllByArticle(articleId);

String thumbNailImageUrl = "";
if (thumbNailImage.isPresent()) {
thumbNailImageUrl = thumbNailImage.get().getImageUrl();
}

return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner);
// 내가 좋아요 한 게시글의 ID 리스트
List<Long> memberLikedArticleIdList = articleLikeRepository.findByMember(member)
.stream().map(ArticleLike::getArticleId)
.toList();

return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner, memberLikedArticleIdList, commentCount);
}

public Slice<GetArticleListResponse> getArticleListPaging(ArticleSearchParameter param, Pageable pageable) {
String principal = SecurityContextProvider.getAuthenticatedUserEmail();
Member member = memberRepository.findByEmail(principal)
.orElseThrow(NotFoundMemberException::new);
List<Long> memberLikedArticleIdList = articleLikeRepository.findByMember(member)
.stream().map(ArticleLike::getArticleId)
.toList();
return new SliceImpl<>(
articleRepository.getArticlesNoOffSetPaging(param, pageable)
.stream()
.peek(this::updateArticleInfo)
.toList()
);
.peek(article -> {
updateArticleInfo(article, memberLikedArticleIdList);
})
.toList());
}

public void updateArticleInfo(GetArticleListResponse articleInfo) {
public void updateArticleInfo(GetArticleListResponse articleInfo, List<Long> memberLikedArticleIdList) {
Long ownerId = articleInfo.getOwnerId();
Long thumbNailImageUrlId = articleInfo.getThumbNailImageId();

Optional<Member> owner = memberRepository.findById(ownerId);
Optional<ArticleImage> articleImage = articleImageRepository.findById(thumbNailImageUrlId);
Long commentCount = commentRepository.countAllByArticle(articleInfo.getId());

articleInfo.updateInfo(owner, articleImage);
articleInfo.updateInfo(owner, articleImage, memberLikedArticleIdList, commentCount);
}

@Transactional
Expand All @@ -174,8 +195,8 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest
);

articleParticipantRepository.save(new ArticleParticipant(article, member));
// 게시글 작성자에게 알림을 보낸다.
fcmMessageProvider.sendFcm(member, ARTICLE_COMMENT_FCM_ALARM, NotificationTag.COMMUNITY);

return CreateCommentResponse.of(savedComment);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest;
import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse;
import com.numberone.backend.domain.comment.dto.response.DeleteCommentResponse;
import com.numberone.backend.domain.comment.dto.response.GetCommentDto;
import com.numberone.backend.domain.comment.service.CommentService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.sql.Delete;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -56,10 +58,22 @@ public ResponseEntity<CreateChildCommentResponse> createChildComment(
""")
@GetMapping("{article-id}")
public ResponseEntity<List<GetCommentDto>> getCommentsByArticle(@PathVariable("article-id") Long articleId){
List<GetCommentDto> response = commentService.getCommentsByArticle(articleId); // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정
List<GetCommentDto> response = commentService.getCommentsByArticle(articleId);
return ResponseEntity.ok(response);
}

// todo: 댓글 삭제, 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능
@Operation(summary = "댓글 삭제 API 입니다", description = """
삭제할 댓글의 id 를 path variable 으로 보내주세요.
대댓글이 존재하는 댓글을 삭제 요청하는 경우에는, 대댓글까지 모두 삭제됩니다.
대댓글이 없는 댓글을 삭제 요청하는 경우에는 해당 댓글만 삭제됩니다.
""")
@DeleteMapping("{comment-id}")
public ResponseEntity<DeleteCommentResponse> deleteComment(@PathVariable("comment-id") Long commentId){
return ResponseEntity.ok(commentService.deleteComment(commentId));
}

// todo: 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.numberone.backend.domain.comment.dto.response;

import lombok.*;

@ToString
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class DeleteCommentResponse {
private Long commentId;
}
Loading

0 comments on commit 2b297f3

Please sign in to comment.