Skip to content

Commit

Permalink
Feat: 커뮤니티 API 기초 구현 (#66)
Browse files Browse the repository at this point in the history
* Feat(#54): member extends baseTimeEntity

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

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

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

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

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

* Feat(#54): relocate s3Provider

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

* Feat(#54): rename CommunityParticipant
- to ArticleParticipant

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

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

* Feat(#54): removed Qclass

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

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

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

* Feat(#54): removed test api for fcm

* Feat(#54): removed test api for fcm

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

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

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

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

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

* Docs(#54): todo 작성

* Feat(#54): catch exception

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

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

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

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

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

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

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

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

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

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

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

* Docs(#54): update todo

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

* Hotfix: npe fix
  • Loading branch information
versatile0010 authored Nov 15, 2023
1 parent ca65cad commit 8f45616
Show file tree
Hide file tree
Showing 69 changed files with 2,133 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -27,10 +28,14 @@
)
@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("JWT", bearerAuth()));
.components(
new Components()
.addSecuritySchemes("JWT", bearerAuth()))
.addSecurityItem(new SecurityRequirement().addList("JWT"));
}

public SecurityScheme bearerAuth() {
Expand All @@ -41,4 +46,5 @@ public SecurityScheme bearerAuth() {
.in(SecurityScheme.In.HEADER)
.name(HttpHeaders.AUTHORIZATION);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.numberone.backend.domain.admin.dto.response.GetAddressResponse;
import com.numberone.backend.domain.shelter.dto.response.GetAllSheltersResponse;
import com.numberone.backend.domain.shelter.repository.ShelterRepository;
import com.numberone.backend.support.S3Provider;
import com.numberone.backend.support.s3.S3Provider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.numberone.backend.domain.article.controller;

import com.numberone.backend.domain.article.dto.request.ModifyArticleRequest;
import com.numberone.backend.domain.article.dto.request.UploadArticleRequest;
import com.numberone.backend.domain.article.dto.response.*;
import com.numberone.backend.domain.article.service.ArticleService;
import com.numberone.backend.domain.comment.dto.request.CreateCommentRequest;
import com.numberone.backend.domain.comment.dto.response.CreateCommentResponse;
import com.numberone.backend.support.redis.like.service.RedisLockLikeFacade;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;

@Slf4j
@RequestMapping("/api/articles")
@RequiredArgsConstructor
@RestController
public class ArticleController {

private final ArticleService articleService;
private final RedisLockLikeFacade redisLockLikeFacade;

@Operation(summary = "게시글 작성 API", description = """
동네생활 게시글 등록 api 입니다.
반드시 access token 을 헤더에 담아서 요청해주세요.
1. title 은 글 제목 입니다 (not null)
2. content 는 글 내용 입니다 (not null)
3. articleTag 는 게시글 태그 입니다. LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보)
4. imageList 는 이미지 (MultiPart) 리스트 입니다.
5. thumbNailImageIdx 는 썸네일 이미지의 인덱스 입니다. (0,1,2, ...
imageList 에 이미지를 담아서 보내는 경우,
idx 에 따라서 썸네일 이미지를 결정합니다.
""")

@PostMapping
public ResponseEntity<UploadArticleResponse> uploadArticle(@RequestBody @Valid UploadArticleRequest request) {
return ResponseEntity.created(URI.create("/api/articles"))
.body(articleService.uploadArticle(request));
}

@Operation(summary = "게시글을 삭제하는 API 입니다.", description = """
게시글 id 를 PathVariable 으로 넘겨주세요.
해당 게시글을 삭제 상태로 변경합니다.
""")
@PatchMapping("{article-id}/delete")
public ResponseEntity<DeleteArticleResponse> deleteArticle(@PathVariable("article-id") Long articleId) {
return ResponseEntity.ok(articleService.deleteArticle(articleId));
}

@Operation(summary = "게시글 상세 조회 API 입니다.", description = """
게시글 id 를 PathVariable 으로 넘겨주세요.
""")
@GetMapping("{article-id}")
public ResponseEntity<GetArticleDetailResponse> getArticleDetails(@PathVariable("article-id") Long articleId) {
return ResponseEntity.ok(articleService.getArticleDetail(articleId));
}


@Operation(summary = "게시글 리스트 조회 no offset Paging API 입니다.", description = """
요청 예시 url 은 다음과 같습니다.
`/api/articles?size=5`
size 는 페이지의 사이즈를 의미하고, default 는 20 입니다.
정렬 순서는 articleId 순입니다. ( = 생성 시간 순 )
ModelAttribute 로 lastArticleId 와 tag 를 넘겨주세요 ( 둘 다 nullable )
tag 가 null 이면, tag 상관 없이 전체 조회를 수행합니다.
tag 가 null 이 아니면, 해당 tag 에 해당하는 게시글만 조회합니다.
lastArticleId 는 직전에 조회한 게시글 중 가장 먀지막(작은) articleId 를 의미합니다.
- 첫 페이지를 요청할 경우에는 lastArticleId 를 null 로 보내야합니다.
- 첫 페이지 이후에 대한 요청은, 직전 페이지 요청에서 얻어온 lastArticleId 를 넣어서 보내면 그 다음 페이지를 호출합니다.
""")
@GetMapping
public ResponseEntity<Slice<GetArticleListResponse>> getArticlePages(
Pageable pageable,
@ModelAttribute ArticleSearchParameter param) { // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정
return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable));
}

@Operation(summary = "게시글에 댓글 작성하기", description = """
게시글에 댓글을 작성하는 API 입니다.
게시글 아이디는 Path variable 으로 넘겨주세요 (article-id)
""")
@PostMapping("comments/{article-id}")
public ResponseEntity<CreateCommentResponse> createComment(
@PathVariable("article-id") Long articleId,
@RequestBody @Valid CreateCommentRequest request) {
return ResponseEntity.created(
URI.create(String.format("/api/articles/comment/%s", articleId)))
.body(articleService.createComment(articleId, request));

}

@Operation(summary = "게시글 수정하기", description = """
게시글 내용을 수정하는 API 입니다.
반드시 access token 을 헤더에 포함해서 요청해주세요.
article-id 는 path variable 으로 넘겨주세요.
""")
@PutMapping("{article-id}/modify")
public ResponseEntity<ModifyArticleResponse> modifyArticle(
@PathVariable("article-id") Long articleId,
@RequestBody @Valid ModifyArticleRequest request ){
return ResponseEntity.ok(articleService.modifyArticle(articleId, request));
}

// todo: 게시글 신고 기능

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.numberone.backend.domain.article.dto.request;

import com.numberone.backend.domain.article.entity.ArticleTag;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@ToString
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ModifyArticleRequest {

// 글 관련
@NotNull(message = "글 제목은 null 일 수 없습니다.")
private String title; // 제목

@NotNull(message = "내용은 null 일 수 없습니다.")
private String content; // 내용

@NotNull(message = """
게시글의 태그를 하나 선택해주세요.
LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보)
""")
private ArticleTag articleTag; // 게시글 태그

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

private Double longitude;
private Double latitude;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.numberone.backend.domain.article.dto.request;

import com.numberone.backend.domain.article.entity.ArticleTag;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@ToString
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class UploadArticleRequest {

// 글 관련
@NotNull(message = "글 제목은 null 일 수 없습니다.")
private String title; // 제목

@NotNull(message = "내용은 null 일 수 없습니다.")
private String content; // 내용

@NotNull(message = """
게시글의 태그를 하나 선택해주세요.
LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보)
""")
private ArticleTag articleTag; // 게시글 태그

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

private Double longitude;
private Double latitude;

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

import com.numberone.backend.domain.article.entity.ArticleTag;
import lombok.*;

@ToString
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ArticleSearchParameter {
private ArticleTag tag;
private Long lastArticleId;
private Double longitude;
private Double latitude;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.numberone.backend.domain.article.dto.response;

import com.numberone.backend.domain.article.entity.Article;
import lombok.*;

import java.time.LocalDateTime;

@ToString
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class DeleteArticleResponse {

private Long id;
private LocalDateTime deletedAt;

public static DeleteArticleResponse of(Article article){
return DeleteArticleResponse.builder()
.id(article.getId())
.deletedAt(article.getModifiedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.numberone.backend.domain.article.dto.response;

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

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

@ToString
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class GetArticleDetailResponse {

// 게시글 관련
private Long articleId;
private Integer likeCount;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
private String title;
private String content;

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

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

public static GetArticleDetailResponse of(Article article, List<String> imageUrls, String thumbNailImageUrl, Member member){
return GetArticleDetailResponse.builder()
.articleId(article.getId())
.title(article.getTitle())
.content(article.getContent())
.likeCount(
Optional.ofNullable(
article.getLikeCount()
).orElse(0)
)
.createdAt(article.getCreatedAt())
.modifiedAt(article.getModifiedAt())
.ownerMemberId(member.getId())
.memberName(member.getRealName())
.memberNickName(member.getNickName())
.imageUrls(imageUrls)
.thumbNailImageUrl(thumbNailImageUrl)
.address("서울시 광진구 자양동") // 교체
.build();
}

}
Loading

0 comments on commit 8f45616

Please sign in to comment.