diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5bfcde78..f357b51a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,6 +46,10 @@ jobs: run: echo "${{ secrets.DEV_OAUTH }}" > /home/runner/work/2023-POCHAK-server/2023-POCHAK-server/pochak/src/main/resources/application-OAUTH.properties shell: bash + - name: add test setting file + run: echo "${{ secrets.DEV_TEST }}" > /home/runner/work/2023-POCHAK-server/2023-POCHAK-server/pochak/src/test/resources/application-TEST.properties + shell: bash + - name: add authkey file run: echo "${{ secrets.DEV_AUTHKEY }}" > /home/runner/work/2023-POCHAK-server/2023-POCHAK-server/pochak/src/main/resources/static/AuthKey_D5ZQTHUQ4K.p8 shell: bash @@ -58,10 +62,6 @@ jobs: with: arguments: build build-root-directory: /home/runner/work/2023-POCHAK-server/2023-POCHAK-server/pochak - - - name: 빌드 확인 - run: ls -l /home/runner/work/2023-POCHAK-server/2023-POCHAK-server/pochak/build/libs - shell: bash - name: Make zip file run: zip -qq -r ./$GITHUB_SHA.zip . diff --git a/README.md b/README.md index 3afcacf6..65befa67 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ POCHAK은 다른 사용자들이 당신의 일상을 기록하도록 하는 독 POCHAK은 사용자들이 서로의 순간에 참여하고 상호 작용할 수 있도록 하는 기능을 강조합니다. 댓글, 좋아요, 그리고 공유를 통해 순간들을 더 특별하게 만들어보세요! ### 🌟 프로필의 다양성 -당신의 POCHAK 프로필은 다른 사람들이 기록한 당신의 순간들을 보여줍니다. 여러 시각으로부터의 사진들이 모여 하나의 아름다운 이야기를 만들어냅니다 +당신의 POCHAK 프로필은 다른 사람들이 기록한 당신의 순간들을 보여줍니다. 여러 시각으로부터의 사진들이 모여 하나의 아름다운 이야기를 만들어냅니다. ## Server Team > WWL, Troubleshooting, Team Rules 등은 [GitHub Wiki](https://github.com/APPS-sookmyung/2023-POCHAK-server/wiki)에서 확인할 수 있습니다. diff --git a/appspec.yml b/appspec.yml index c42a3908..a2eee2e2 100644 --- a/appspec.yml +++ b/appspec.yml @@ -23,4 +23,4 @@ hooks: # runas: root ApplicationStart: - location: scripts/start.sh - runas: root + runas: root \ No newline at end of file diff --git a/pochak/.gitignore b/pochak/.gitignore index 9003acd1..9b3eb508 100644 --- a/pochak/.gitignore +++ b/pochak/.gitignore @@ -19,6 +19,7 @@ AuthKey_D5ZQTHUQ4K.p8 application-API-KEY.properties application-OAUTH.properties application-JWT.properties +application-TEST.properties # deploy .tar diff --git a/pochak/build.gradle b/pochak/build.gradle index eda9f86c..ad14485f 100644 --- a/pochak/build.gradle +++ b/pochak/build.gradle @@ -13,10 +13,10 @@ java { } configurations { + asciidoctorExt compileOnly { extendsFrom annotationProcessor } - asciidoctorExt } repositories { @@ -54,26 +54,17 @@ dependencies { tasks.named('test') { useJUnitPlatform() + outputs.dir snippetsDir } ext { snippetsDir = file('build/generated-snippets') } -test { - outputs.dir snippetsDir -} - -bootJar { - dependsOn asciidoctor - copy { - from "${asciidoctor.outputDir}" - into 'BOOT-INF/classes/static/docs' - } -} asciidoctor { dependsOn test + configurations 'asciidoctorExt' inputs.dir snippetsDir } @@ -81,12 +72,30 @@ asciidoctor.doFirst { delete file('src/main/resources/static/docs') } -task copyDocument(type: Copy) { +bootJar { dependsOn asciidoctor - from file("build/docs/asciidoc") - into file("src/main/resources/static/docs") } -build { - dependsOn copyDocument +tasks.register('copyApiDocument') { + dependsOn asciidoctor + copy { + from "${asciidoctor.outputDir}" + into 'BOOT-INF/classes/static/docs' + } + copy { + from "${asciidoctor.outputDir}" + into 'src/main/resources/static/docs' + } + copy { + from file("build/docs/asciidoc") + into file("BOOT-INF/classes/static/docs") + } + copy { + from file("build/docs/asciidoc") + into file("src/main/resources/static/docs") + } } + +build { + dependsOn copyApiDocument +} \ No newline at end of file diff --git a/pochak/src/docs/asciidoc/index.adoc b/pochak/src/docs/asciidoc/index.adoc new file mode 100644 index 00000000..0fa87737 --- /dev/null +++ b/pochak/src/docs/asciidoc/index.adoc @@ -0,0 +1,37 @@ += POCHAK API Document +pochak server team~~ +:doctype: book +:icons: font +:source-highlighter: highlishtjs +:toc: left +:toclevels: 4 +:sectlinks: +:docinfo: shared-head + +== Http Status Code + +|=== +| Status code | Usage + +| `200 OK` +| The request completed successfully + +| `201 Created` +| A new resource has been created successfully. The resource's URI is available from the response's +`Location` header + +| `204 No Content` +| An update to an existing resource has been applied successfully + +| `400 Bad Request` +| The request was malformed. The response body will include an error providing further information + +| `404 Not Found` +| The requested resource did not exist +|=== + +== API List + +=== Post + +* link:post.html[Post API] \ No newline at end of file diff --git a/pochak/src/docs/asciidoc/post.adoc b/pochak/src/docs/asciidoc/post.adoc new file mode 100644 index 00000000..65aadae5 --- /dev/null +++ b/pochak/src/docs/asciidoc/post.adoc @@ -0,0 +1,49 @@ += POST API +:doctype: book +:icons: font +:source-highlighter: highlishtjs +:toc: left +:toclevels: 4 +:sectlinks: + +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + +== `POST` Upload Post API + +게시물 업로드 API + +=== Request + +include::{snippets}/upload-post/curl-request.adoc[] +include::{snippets}/upload-post/request-parts.adoc[] + +- request는 `application/json` 타입으로 다음과 같이 전달합니다. + +```json +{ + "caption" : "게시물 내용", + "taggedMemberHandleList" : ["habongee"] +} +``` + +include::{snippets}/upload-post/request-part-request-fields.adoc[] + +=== Response + +include::{snippets}/upload-post/response-body.adoc[] +include::{snippets}/upload-post/response-fields.adoc[] + +== `GET` Post Details Retrieval API + +게시물 상세 페이지 조회 API + +=== Request +include::{snippets}/get-detail-post/http-request.adoc[] +include::{snippets}/get-detail-post/path-parameters.adoc[] +include::{snippets}/get-detail-post/request-headers.adoc[] + +=== Response +include::{snippets}/get-detail-post/response-body.adoc[] +include::{snippets}/get-detail-post/response-fields.adoc[] \ No newline at end of file diff --git a/pochak/src/main/java/com/apps/pochak/alarm/domain/Alarm.java b/pochak/src/main/java/com/apps/pochak/alarm/domain/Alarm.java index e21768ac..ca5d5b90 100644 --- a/pochak/src/main/java/com/apps/pochak/alarm/domain/Alarm.java +++ b/pochak/src/main/java/com/apps/pochak/alarm/domain/Alarm.java @@ -3,21 +3,27 @@ import com.apps.pochak.global.BaseEntity; import com.apps.pochak.member.domain.Member; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; import static jakarta.persistence.InheritanceType.SINGLE_TABLE; +import static lombok.AccessLevel.*; @Entity @Getter +@DynamicInsert @BatchSize(size = 100) @Inheritance(strategy = SINGLE_TABLE) @SQLDelete(sql = "UPDATE alarm SET status = 'DELETED' WHERE id = ?") @SQLRestriction("status = 'ACTIVE'") +@NoArgsConstructor(access = PROTECTED) @DiscriminatorColumn(name = "alarmType") public abstract class Alarm extends BaseEntity { @Id @@ -28,5 +34,10 @@ public abstract class Alarm extends BaseEntity { @JoinColumn(name = "receiver_id") private Member receiver; + @Column(columnDefinition = "boolean default false") private Boolean isChecked; + + protected Alarm(Member receiver) { + this.receiver = receiver; + } } diff --git a/pochak/src/main/java/com/apps/pochak/alarm/domain/CommentAlarm.java b/pochak/src/main/java/com/apps/pochak/alarm/domain/CommentAlarm.java index d870a025..8eec06f2 100644 --- a/pochak/src/main/java/com/apps/pochak/alarm/domain/CommentAlarm.java +++ b/pochak/src/main/java/com/apps/pochak/alarm/domain/CommentAlarm.java @@ -7,6 +7,7 @@ import jakarta.persistence.OneToOne; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.FetchType.LAZY; @@ -14,6 +15,7 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @DiscriminatorValue("COMMENT") public class CommentAlarm extends Alarm { diff --git a/pochak/src/main/java/com/apps/pochak/alarm/domain/FollowAlarm.java b/pochak/src/main/java/com/apps/pochak/alarm/domain/FollowAlarm.java index 4e619fbe..37b96e69 100644 --- a/pochak/src/main/java/com/apps/pochak/alarm/domain/FollowAlarm.java +++ b/pochak/src/main/java/com/apps/pochak/alarm/domain/FollowAlarm.java @@ -7,6 +7,7 @@ import jakarta.persistence.OneToOne; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.FetchType.LAZY; @@ -14,6 +15,7 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @DiscriminatorValue("FOLLOW") public class FollowAlarm extends Alarm { diff --git a/pochak/src/main/java/com/apps/pochak/alarm/domain/LikeAlarm.java b/pochak/src/main/java/com/apps/pochak/alarm/domain/LikeAlarm.java index cbea0863..d9039394 100644 --- a/pochak/src/main/java/com/apps/pochak/alarm/domain/LikeAlarm.java +++ b/pochak/src/main/java/com/apps/pochak/alarm/domain/LikeAlarm.java @@ -1,12 +1,13 @@ package com.apps.pochak.alarm.domain; -import com.apps.pochak.likes.domain.LikeEntity; +import com.apps.pochak.like.domain.LikeEntity; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.FetchType.LAZY; @@ -14,6 +15,7 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @DiscriminatorValue("LIKE") public class LikeAlarm extends Alarm { diff --git a/pochak/src/main/java/com/apps/pochak/alarm/domain/TagApprovalAlarm.java b/pochak/src/main/java/com/apps/pochak/alarm/domain/TagApprovalAlarm.java index 67f2d06c..ba351bb7 100644 --- a/pochak/src/main/java/com/apps/pochak/alarm/domain/TagApprovalAlarm.java +++ b/pochak/src/main/java/com/apps/pochak/alarm/domain/TagApprovalAlarm.java @@ -1,12 +1,15 @@ package com.apps.pochak.alarm.domain; +import com.apps.pochak.member.domain.Member; import com.apps.pochak.tag.domain.Tag; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.FetchType.LAZY; @@ -14,10 +17,17 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @DiscriminatorValue("TAG_APPROVAL") public class TagApprovalAlarm extends Alarm { @OneToOne(fetch = LAZY, cascade = ALL) @JoinColumn(name = "tag_approval_id") private Tag tag; + + @Builder + public TagApprovalAlarm(Member receiver, Tag tag) { + super(receiver); + this.tag = tag; + } } diff --git a/pochak/src/main/java/com/apps/pochak/comment/domain/Comment.java b/pochak/src/main/java/com/apps/pochak/comment/domain/Comment.java index 943d2fac..16fd3f14 100644 --- a/pochak/src/main/java/com/apps/pochak/comment/domain/Comment.java +++ b/pochak/src/main/java/com/apps/pochak/comment/domain/Comment.java @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; @@ -19,6 +20,7 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @SQLDelete(sql = "UPDATE comment SET status = 'DELETED' WHERE id = ?") @SQLRestriction("status = 'ACTIVE'") diff --git a/pochak/src/main/java/com/apps/pochak/comment/domain/repository/CommentRepository.java b/pochak/src/main/java/com/apps/pochak/comment/domain/repository/CommentRepository.java index 04c334f4..9ea1553b 100644 --- a/pochak/src/main/java/com/apps/pochak/comment/domain/repository/CommentRepository.java +++ b/pochak/src/main/java/com/apps/pochak/comment/domain/repository/CommentRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Optional; public interface CommentRepository extends JpaRepository { @@ -13,4 +14,10 @@ public interface CommentRepository extends JpaRepository { @Query("update Comment c set c.status = 'DELETED' " + "where c.post = :post ") void bulkDeleteByPost(@Param("post") final Post post); + + @Query("select c from Comment c " + + "join fetch c.member " + + "where c.post = :post " + + "order by c.createdDate desc limit 1") + Optional findFirstByPost(@Param("post") final Post post); } diff --git a/pochak/src/main/java/com/apps/pochak/comment/dto/response/ChildCommentElement.java b/pochak/src/main/java/com/apps/pochak/comment/dto/response/ChildCommentElement.java deleted file mode 100644 index 7566799c..00000000 --- a/pochak/src/main/java/com/apps/pochak/comment/dto/response/ChildCommentElement.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.apps.pochak.comment.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -public class ChildCommentElement { -} diff --git a/pochak/src/main/java/com/apps/pochak/comment/dto/response/CommentElement.java b/pochak/src/main/java/com/apps/pochak/comment/dto/response/CommentElement.java new file mode 100644 index 00000000..5af2c493 --- /dev/null +++ b/pochak/src/main/java/com/apps/pochak/comment/dto/response/CommentElement.java @@ -0,0 +1,29 @@ +package com.apps.pochak.comment.dto.response; + +import com.apps.pochak.comment.domain.Comment; +import com.apps.pochak.member.domain.Member; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CommentElement { + private String profileImage; + private String handle; + private LocalDateTime createdDate; + private String content; + + @Builder(builderMethodName = "from") + public CommentElement(final Comment comment) { + final Member member = comment.getMember(); + this.profileImage = member.getProfileImage(); + this.handle = member.getHandle(); + this.createdDate = comment.getCreatedDate(); + this.content = comment.getContent(); + } +} diff --git a/pochak/src/main/java/com/apps/pochak/comment/dto/response/CommentElements.java b/pochak/src/main/java/com/apps/pochak/comment/dto/response/CommentElements.java deleted file mode 100644 index d25b743e..00000000 --- a/pochak/src/main/java/com/apps/pochak/comment/dto/response/CommentElements.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.apps.pochak.comment.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -public class CommentElements { -} diff --git a/pochak/src/main/java/com/apps/pochak/comment/dto/response/ParentCommentElement.java b/pochak/src/main/java/com/apps/pochak/comment/dto/response/ParentCommentElement.java deleted file mode 100644 index 19975b3a..00000000 --- a/pochak/src/main/java/com/apps/pochak/comment/dto/response/ParentCommentElement.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.apps.pochak.comment.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -public class ParentCommentElement { -} diff --git a/pochak/src/main/java/com/apps/pochak/follow/domain/Follow.java b/pochak/src/main/java/com/apps/pochak/follow/domain/Follow.java index 7a62293d..5a06059d 100644 --- a/pochak/src/main/java/com/apps/pochak/follow/domain/Follow.java +++ b/pochak/src/main/java/com/apps/pochak/follow/domain/Follow.java @@ -7,6 +7,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLDelete; import static com.apps.pochak.global.BaseEntityStatus.*; @@ -16,6 +17,7 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @SQLDelete(sql = "UPDATE follow SET status = 'DELETED' WHERE id = ?") public class Follow extends BaseEntity { diff --git a/pochak/src/main/java/com/apps/pochak/follow/domain/repository/FollowRepository.java b/pochak/src/main/java/com/apps/pochak/follow/domain/repository/FollowRepository.java index 32a1ed5c..40eaf7c0 100644 --- a/pochak/src/main/java/com/apps/pochak/follow/domain/repository/FollowRepository.java +++ b/pochak/src/main/java/com/apps/pochak/follow/domain/repository/FollowRepository.java @@ -13,13 +13,13 @@ public interface FollowRepository extends JpaRepository { - @Query(value = "select f from Follow f where f.receiver = :member and f.status = 'ACTIVE'") + @Query(value = "select count(f) from Follow f where f.receiver = :member and f.status = 'ACTIVE'") long countActiveFollowByReceiver(@Param("member") final Member member); - @Query(value = "select f from Follow f where f.sender = :member and f.status = 'ACTIVE'") + @Query(value = "select count(f) from Follow f where f.sender = :member and f.status = 'ACTIVE'") long countActiveFollowBySender(@Param("member") final Member member); - @Query(value = "select f from Follow f " + + @Query(value = "select count(f.id) > 0 from Follow f " + "where f.sender = :sender and f.receiver = :receiver and f.status = 'ACTIVE'") boolean existsBySenderAndReceiver(@Param("sender") final Member sender, @Param("receiver") final Member receiver); diff --git a/pochak/src/main/java/com/apps/pochak/global/apiPayload/code/status/ErrorStatus.java b/pochak/src/main/java/com/apps/pochak/global/apiPayload/code/status/ErrorStatus.java index eb61ca90..057e4ce9 100644 --- a/pochak/src/main/java/com/apps/pochak/global/apiPayload/code/status/ErrorStatus.java +++ b/pochak/src/main/java/com/apps/pochak/global/apiPayload/code/status/ErrorStatus.java @@ -18,6 +18,9 @@ public enum ErrorStatus implements BaseErrorCode { _UNAUTHORIZED(UNAUTHORIZED, "COMMON401", "인증이 필요합니다. 권한을 확인해주세요."), _FORBIDDEN(FORBIDDEN, "COMMON403", "금지된 요청입니다."), + // Global + IO_EXCEPTION(INTERNAL_SERVER_ERROR, "COMMON5001", "서버 IO Exception 발생, 관리자에게 문의 바랍니다"), + // Alarm // Comment @@ -48,6 +51,7 @@ public enum ErrorStatus implements BaseErrorCode { // Post INVALID_POST_ID(BAD_REQUEST, "POST4001", "유효하지 않은 게시물 아이디입니다."), NOT_YOUR_POST(UNAUTHORIZED, "POST4002", "해당 게시물의 삭제 권한이 없습니다."), + PRIVATE_POST(UNAUTHORIZED, "POST4003", "공개되지 않은 게시물입니다. 접근 권한이 없습니다."), // Tag diff --git a/pochak/src/main/java/com/apps/pochak/global/s3/DirName.java b/pochak/src/main/java/com/apps/pochak/global/s3/DirName.java new file mode 100644 index 00000000..8b48fa81 --- /dev/null +++ b/pochak/src/main/java/com/apps/pochak/global/s3/DirName.java @@ -0,0 +1,12 @@ +package com.apps.pochak.global.s3; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum DirName { + MEMBER("member"), POST("post"); + + private final String dirName; +} diff --git a/pochak/src/main/java/com/apps/pochak/global/s3/S3Service.java b/pochak/src/main/java/com/apps/pochak/global/s3/S3Service.java index 82d02693..d035d1cd 100644 --- a/pochak/src/main/java/com/apps/pochak/global/s3/S3Service.java +++ b/pochak/src/main/java/com/apps/pochak/global/s3/S3Service.java @@ -6,6 +6,7 @@ import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest; +import com.apps.pochak.global.apiPayload.exception.GeneralException; import com.apps.pochak.global.apiPayload.exception.handler.ImageException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -31,14 +32,18 @@ public class S3Service { @Value("${cloud.aws.s3.bucket}") private String bucket; - public String upload(MultipartFile multipartFile, String dirName) throws IOException { - File uploadFile = convert(multipartFile) - .orElseThrow(() -> new ImageException(CONVERT_FILE_ERROR)); - return upload(uploadFile, dirName); + public String upload(MultipartFile multipartFile, DirName dirName) { + try { + File uploadFile = convert(multipartFile) + .orElseThrow(() -> new ImageException(CONVERT_FILE_ERROR)); + return upload(uploadFile, dirName); + } catch (IOException e) { + throw new GeneralException(IO_EXCEPTION); + } } - private String upload(File uploadFile, String dirName) { - String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName(); + private String upload(File uploadFile, DirName dirName) { + String fileName = dirName.getDirName() + "/" + UUID.randomUUID() + uploadFile.getName(); String uploadImageUrl = putS3(uploadFile, fileName); deleteFile(uploadFile); return uploadImageUrl; diff --git a/pochak/src/main/java/com/apps/pochak/global/s3/ValidFile.java b/pochak/src/main/java/com/apps/pochak/global/s3/ValidFile.java new file mode 100644 index 00000000..5360a026 --- /dev/null +++ b/pochak/src/main/java/com/apps/pochak/global/s3/ValidFile.java @@ -0,0 +1,21 @@ +package com.apps.pochak.global.s3; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target(value = {ElementType.PARAMETER, ElementType.FIELD}) +@Retention(value = RUNTIME) +@Constraint(validatedBy = ValidFileValidator.class) +public @interface ValidFile { + String message() default "Invalid File"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/pochak/src/main/java/com/apps/pochak/global/s3/ValidFileValidator.java b/pochak/src/main/java/com/apps/pochak/global/s3/ValidFileValidator.java new file mode 100644 index 00000000..4256794c --- /dev/null +++ b/pochak/src/main/java/com/apps/pochak/global/s3/ValidFileValidator.java @@ -0,0 +1,12 @@ +package com.apps.pochak.global.s3; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.web.multipart.MultipartFile; + +public class ValidFileValidator implements ConstraintValidator { + @Override + public boolean isValid(MultipartFile file, ConstraintValidatorContext context) { + return file != null && !file.isEmpty(); + } +} diff --git a/pochak/src/main/java/com/apps/pochak/likes/domain/LikeEntity.java b/pochak/src/main/java/com/apps/pochak/like/domain/LikeEntity.java similarity index 90% rename from pochak/src/main/java/com/apps/pochak/likes/domain/LikeEntity.java rename to pochak/src/main/java/com/apps/pochak/like/domain/LikeEntity.java index c0e27a83..0d027e51 100644 --- a/pochak/src/main/java/com/apps/pochak/likes/domain/LikeEntity.java +++ b/pochak/src/main/java/com/apps/pochak/like/domain/LikeEntity.java @@ -1,4 +1,4 @@ -package com.apps.pochak.likes.domain; +package com.apps.pochak.like.domain; import com.apps.pochak.global.BaseEntity; import com.apps.pochak.member.domain.Member; @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; @@ -15,6 +16,7 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @SQLDelete(sql = "UPDATE like_entity SET status = 'DELETED' WHERE id = ?") @SQLRestriction("status = 'ACTIVE'") diff --git a/pochak/src/main/java/com/apps/pochak/like/domain/repository/LikeRepository.java b/pochak/src/main/java/com/apps/pochak/like/domain/repository/LikeRepository.java new file mode 100644 index 00000000..f7eb14c2 --- /dev/null +++ b/pochak/src/main/java/com/apps/pochak/like/domain/repository/LikeRepository.java @@ -0,0 +1,13 @@ +package com.apps.pochak.like.domain.repository; + +import com.apps.pochak.like.domain.LikeEntity; +import com.apps.pochak.member.domain.Member; +import com.apps.pochak.post.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeRepository extends JpaRepository { + int countByLikedPost(final Post post); + + Boolean existsByLikeMemberAndLikedPost(final Member member, + final Post post); +} diff --git a/pochak/src/main/java/com/apps/pochak/likes/dto/response/LikeElement.java b/pochak/src/main/java/com/apps/pochak/like/dto/response/LikeElement.java similarity index 56% rename from pochak/src/main/java/com/apps/pochak/likes/dto/response/LikeElement.java rename to pochak/src/main/java/com/apps/pochak/like/dto/response/LikeElement.java index 608cac5e..075c459a 100644 --- a/pochak/src/main/java/com/apps/pochak/likes/dto/response/LikeElement.java +++ b/pochak/src/main/java/com/apps/pochak/like/dto/response/LikeElement.java @@ -1,4 +1,4 @@ -package com.apps.pochak.likes.dto.response; +package com.apps.pochak.like.dto.response; import lombok.Data; diff --git a/pochak/src/main/java/com/apps/pochak/likes/dto/response/LikeElements.java b/pochak/src/main/java/com/apps/pochak/like/dto/response/LikeElements.java similarity index 56% rename from pochak/src/main/java/com/apps/pochak/likes/dto/response/LikeElements.java rename to pochak/src/main/java/com/apps/pochak/like/dto/response/LikeElements.java index b32e6f9d..cbc032dd 100644 --- a/pochak/src/main/java/com/apps/pochak/likes/dto/response/LikeElements.java +++ b/pochak/src/main/java/com/apps/pochak/like/dto/response/LikeElements.java @@ -1,4 +1,4 @@ -package com.apps.pochak.likes.dto.response; +package com.apps.pochak.like.dto.response; import lombok.Data; diff --git a/pochak/src/main/java/com/apps/pochak/likes/service/LikeService.java b/pochak/src/main/java/com/apps/pochak/like/service/LikeService.java similarity index 66% rename from pochak/src/main/java/com/apps/pochak/likes/service/LikeService.java rename to pochak/src/main/java/com/apps/pochak/like/service/LikeService.java index 985915aa..2a2bb37d 100644 --- a/pochak/src/main/java/com/apps/pochak/likes/service/LikeService.java +++ b/pochak/src/main/java/com/apps/pochak/like/service/LikeService.java @@ -1,6 +1,6 @@ -package com.apps.pochak.likes.service; +package com.apps.pochak.like.service; -import com.apps.pochak.likes.domain.repository.LikeRepository; +import com.apps.pochak.like.domain.repository.LikeRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/pochak/src/main/java/com/apps/pochak/likes/domain/repository/LikeRepository.java b/pochak/src/main/java/com/apps/pochak/likes/domain/repository/LikeRepository.java deleted file mode 100644 index afd88758..00000000 --- a/pochak/src/main/java/com/apps/pochak/likes/domain/repository/LikeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.apps.pochak.likes.domain.repository; - -import com.apps.pochak.likes.domain.LikeEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface LikeRepository extends JpaRepository { -} diff --git a/pochak/src/main/java/com/apps/pochak/login/oauth/OAuthService.java b/pochak/src/main/java/com/apps/pochak/login/oauth/OAuthService.java index 52423874..9e0c0c03 100644 --- a/pochak/src/main/java/com/apps/pochak/login/oauth/OAuthService.java +++ b/pochak/src/main/java/com/apps/pochak/login/oauth/OAuthService.java @@ -1,6 +1,5 @@ package com.apps.pochak.login.oauth; -import com.apps.pochak.global.apiPayload.ApiResponse; import com.apps.pochak.global.apiPayload.exception.GeneralException; import com.apps.pochak.global.s3.S3Service; import com.apps.pochak.login.dto.request.UserInfoRequest; @@ -17,7 +16,7 @@ import java.util.Optional; import static com.apps.pochak.global.apiPayload.code.status.ErrorStatus.EXIST_USER; -import static com.apps.pochak.global.apiPayload.code.status.ErrorStatus.INVALID_MEMBER_HANDLE; +import static com.apps.pochak.global.s3.DirName.MEMBER; @Service @RequiredArgsConstructor @@ -30,13 +29,13 @@ public class OAuthService { @Transactional public OAuthResponse signup(UserInfoRequest userInfoRequest) throws IOException { - Optional findMember = memberRepository.findMemberBySocialId(userInfoRequest.getSocialId()); + Optional findMember = memberRepository.findMemberBySocialId(userInfoRequest.getSocialId()); if (findMember.isPresent()) { throw new GeneralException(EXIST_USER); } - String profileImageUrl = awsS3Service.upload(userInfoRequest.getProfileImage(), "profile"); + String profileImageUrl = awsS3Service.upload(userInfoRequest.getProfileImage(), MEMBER); String refreshToken = jwtService.createRefreshToken(); String accessToken = jwtService.createAccessToken(userInfoRequest.getHandle()); diff --git a/pochak/src/main/java/com/apps/pochak/member/controller/MemberController.java b/pochak/src/main/java/com/apps/pochak/member/controller/MemberController.java index 07163b84..2d65f747 100644 --- a/pochak/src/main/java/com/apps/pochak/member/controller/MemberController.java +++ b/pochak/src/main/java/com/apps/pochak/member/controller/MemberController.java @@ -12,7 +12,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v2/members") +@RequestMapping("api/v2/members") public class MemberController { private final MemberService memberService; private final FollowService followService; diff --git a/pochak/src/main/java/com/apps/pochak/member/domain/Member.java b/pochak/src/main/java/com/apps/pochak/member/domain/Member.java index 2e5ef473..ab6ee10a 100644 --- a/pochak/src/main/java/com/apps/pochak/member/domain/Member.java +++ b/pochak/src/main/java/com/apps/pochak/member/domain/Member.java @@ -8,6 +8,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; @@ -16,6 +17,7 @@ @Entity @Getter +@DynamicInsert @NoArgsConstructor(access = PROTECTED) @SQLDelete(sql = "UPDATE member SET status = 'DELETED' WHERE id = ?") @SQLRestriction("status = 'ACTIVE'") diff --git a/pochak/src/main/java/com/apps/pochak/member/service/MemberService.java b/pochak/src/main/java/com/apps/pochak/member/service/MemberService.java index 9e539d94..c5882df3 100644 --- a/pochak/src/main/java/com/apps/pochak/member/service/MemberService.java +++ b/pochak/src/main/java/com/apps/pochak/member/service/MemberService.java @@ -4,7 +4,6 @@ import com.apps.pochak.login.jwt.JwtService; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.repository.MemberRepository; -import com.apps.pochak.member.dto.response.MemberElements; import com.apps.pochak.member.dto.response.ProfileResponse; import com.apps.pochak.post.domain.Post; import com.apps.pochak.post.domain.repository.PostRepository; diff --git a/pochak/src/main/java/com/apps/pochak/post/controller/PostController.java b/pochak/src/main/java/com/apps/pochak/post/controller/PostController.java index 67b8a9ce..d4c18883 100644 --- a/pochak/src/main/java/com/apps/pochak/post/controller/PostController.java +++ b/pochak/src/main/java/com/apps/pochak/post/controller/PostController.java @@ -1,22 +1,50 @@ package com.apps.pochak.post.controller; import com.apps.pochak.global.apiPayload.ApiResponse; +import com.apps.pochak.global.s3.ValidFile; +import com.apps.pochak.like.service.LikeService; +import com.apps.pochak.post.dto.request.PostUploadRequest; +import com.apps.pochak.post.dto.response.PostDetailResponse; import com.apps.pochak.post.service.PostService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; @RestController @RequiredArgsConstructor -@RequestMapping("/api/v2/posts") +@RequestMapping("api/v2/posts") public class PostController { private final PostService postService; + private final LikeService likeService; @DeleteMapping("/{postId}") public ApiResponse deletePost(@PathVariable final Long postId) { postService.deletePost(postId); return ApiResponse.onSuccess(null); } + + @GetMapping("/{postId}") + public ApiResponse getPostDetail( + @PathVariable("postId") final Long postId + ) { + return ApiResponse.onSuccess(postService.getPostDetail(postId)); + } + + @PostMapping(value = "", consumes = {APPLICATION_JSON_VALUE, MULTIPART_FORM_DATA_VALUE}) + public ApiResponse uploadPost( + @RequestPart(value = "postImage") + @ValidFile(message = "게시물 이미지는 필수로 전달해야 합니다.") final MultipartFile postImage, + @RequestPart("request") @Valid final PostUploadRequest request + ) { + postService.savePost(postImage, request); + return ApiResponse.onSuccess(null); + } } diff --git a/pochak/src/main/java/com/apps/pochak/post/domain/Post.java b/pochak/src/main/java/com/apps/pochak/post/domain/Post.java index 437c5bb4..5d570f3c 100644 --- a/pochak/src/main/java/com/apps/pochak/post/domain/Post.java +++ b/pochak/src/main/java/com/apps/pochak/post/domain/Post.java @@ -3,12 +3,14 @@ import com.apps.pochak.global.BaseEntity; import com.apps.pochak.member.domain.Member; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; +import static com.apps.pochak.post.domain.PostStatus.PRIVATE; import static jakarta.persistence.EnumType.STRING; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; @@ -34,5 +36,17 @@ public class Post extends BaseEntity { private Member owner; private String postImage; + private String caption; + + @Builder + public Post(Member owner, String postImage, String caption) { + this.owner = owner; + this.postImage = postImage; + this.caption = caption; + } + + public Boolean isPrivate() { + return getPostStatus().equals(PRIVATE); + } } diff --git a/pochak/src/main/java/com/apps/pochak/post/domain/repository/PostRepository.java b/pochak/src/main/java/com/apps/pochak/post/domain/repository/PostRepository.java index b02843cf..88583537 100644 --- a/pochak/src/main/java/com/apps/pochak/post/domain/repository/PostRepository.java +++ b/pochak/src/main/java/com/apps/pochak/post/domain/repository/PostRepository.java @@ -1,5 +1,6 @@ package com.apps.pochak.post.domain.repository; +import com.apps.pochak.global.apiPayload.exception.GeneralException; import com.apps.pochak.member.domain.Member; import com.apps.pochak.post.domain.Post; import com.apps.pochak.post.domain.PostStatus; @@ -9,6 +10,10 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Optional; + +import static com.apps.pochak.global.apiPayload.code.status.ErrorStatus.INVALID_POST_ID; + public interface PostRepository extends JpaRepository { @Query(value = "select p from Post p " + @@ -23,4 +28,13 @@ Page findTaggedPost(@Param("member") final Member member, Page findPostByOwnerAndPostStatusOrderByCreatedDateDesc(final Member owner, final PostStatus postStatus, final Pageable pageable); + + @Query("select p from Post p " + + "join fetch p.owner " + + "where p.id = :postId ") + Optional findById(@Param("postId") final Long postId); + + default Post findPostById(final Long postId) { + return findById(postId).orElseThrow(() -> new GeneralException(INVALID_POST_ID)); + } } diff --git a/pochak/src/main/java/com/apps/pochak/post/dto/request/PostUploadRequest.java b/pochak/src/main/java/com/apps/pochak/post/dto/request/PostUploadRequest.java index e522fab8..e725c787 100644 --- a/pochak/src/main/java/com/apps/pochak/post/dto/request/PostUploadRequest.java +++ b/pochak/src/main/java/com/apps/pochak/post/dto/request/PostUploadRequest.java @@ -1,9 +1,35 @@ package com.apps.pochak.post.dto.request; +import com.apps.pochak.member.domain.Member; +import com.apps.pochak.post.domain.Post; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + @Data +@NoArgsConstructor +@AllArgsConstructor public class PostUploadRequest { + private String caption; + + @Valid + @Size(min = 1, message = "태그된 한 명 이상의 유저의 아이디를 전달해야 합니다.") + @NotNull(message = "태그된 유저들의 아이디 리스트는 필수로 전달해야 합니다.") + private List taggedMemberHandleList; + + public Post toEntity( + final String postImage, + final Member owner + ) { + return Post.builder() + .caption(this.caption) + .postImage(postImage) + .owner(owner) + .build(); + } } diff --git a/pochak/src/main/java/com/apps/pochak/post/dto/response/PostDetailResponse.java b/pochak/src/main/java/com/apps/pochak/post/dto/response/PostDetailResponse.java index 95e0332e..1259d913 100644 --- a/pochak/src/main/java/com/apps/pochak/post/dto/response/PostDetailResponse.java +++ b/pochak/src/main/java/com/apps/pochak/post/dto/response/PostDetailResponse.java @@ -1,9 +1,56 @@ package com.apps.pochak.post.dto.response; +import com.apps.pochak.comment.domain.Comment; +import com.apps.pochak.comment.dto.response.CommentElement; +import com.apps.pochak.member.domain.Member; +import com.apps.pochak.post.domain.Post; +import com.apps.pochak.tag.domain.Tag; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; +import java.util.stream.Collectors; + @Data +@NoArgsConstructor +@AllArgsConstructor public class PostDetailResponse { + private String ownerHandle; + private String ownerProfileImage; + private List taggedMemberHandle; + private Boolean isFollow; + private String postImage; + private Boolean isLike; + private int likeCount; + private String caption; + private CommentElement recentComment; + + @Builder(builderMethodName = "of") + public PostDetailResponse( + final Post post, + final List tagList, + final Boolean isFollow, + final Boolean isLike, + final int likeCount, + final Comment recentComment + ) { + final Member owner = post.getOwner(); + this.ownerHandle = owner.getHandle(); + this.ownerProfileImage = owner.getProfileImage(); + this.taggedMemberHandle = tagList.stream().map( + tag -> tag.getMember().getHandle() + ).collect(Collectors.toList()); + this.isFollow = isFollow; + this.postImage = post.getPostImage(); + this.isLike = isLike; + this.likeCount = likeCount; + this.caption = post.getCaption(); + if (recentComment != null) { + this.recentComment = CommentElement.from() + .comment(recentComment) + .build(); + } + } } diff --git a/pochak/src/main/java/com/apps/pochak/post/service/PostService.java b/pochak/src/main/java/com/apps/pochak/post/service/PostService.java index 573716d8..25d13e45 100644 --- a/pochak/src/main/java/com/apps/pochak/post/service/PostService.java +++ b/pochak/src/main/java/com/apps/pochak/post/service/PostService.java @@ -6,9 +6,33 @@ import com.apps.pochak.member.domain.Member; import com.apps.pochak.post.domain.Post; import com.apps.pochak.post.domain.repository.PostRepository; +import com.apps.pochak.alarm.domain.TagApprovalAlarm; +import com.apps.pochak.alarm.domain.repository.AlarmRepository; +import com.apps.pochak.comment.domain.Comment; +import com.apps.pochak.comment.domain.repository.CommentRepository; +import com.apps.pochak.follow.domain.repository.FollowRepository; +import com.apps.pochak.global.apiPayload.exception.GeneralException; +import com.apps.pochak.global.s3.S3Service; +import com.apps.pochak.like.domain.repository.LikeRepository; +import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.member.domain.Member; +import com.apps.pochak.member.domain.repository.MemberRepository; +import com.apps.pochak.post.domain.Post; +import com.apps.pochak.post.domain.repository.PostRepository; +import com.apps.pochak.post.dto.request.PostUploadRequest; +import com.apps.pochak.post.dto.response.PostDetailResponse; +import com.apps.pochak.tag.domain.Tag; +import com.apps.pochak.tag.domain.repository.TagRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.apps.pochak.global.apiPayload.code.status.ErrorStatus.PRIVATE_POST; +import static com.apps.pochak.global.s3.DirName.POST; import static com.apps.pochak.global.apiPayload.code.status.ErrorStatus.INVALID_POST_ID; import static com.apps.pochak.global.apiPayload.code.status.ErrorStatus.NOT_YOUR_POST; @@ -16,8 +40,15 @@ @Service @RequiredArgsConstructor public class PostService { + private final MemberRepository memberRepository; private final PostRepository postRepository; + private final FollowRepository followRepository; + private final TagRepository tagRepository; private final CommentRepository commentRepository; + private final LikeRepository likeRepository; + private final AlarmRepository alarmRepository; + + private final S3Service s3Service; private final JwtService jwtService; @Transactional @@ -30,4 +61,75 @@ public void deletePost(final Long postId) { postRepository.delete(post); commentRepository.bulkDeleteByPost(post); } + + public PostDetailResponse getPostDetail(final Long postId) { + final Member loginMember = jwtService.getLoginMember(); + final Post post = postRepository.findPostById(postId); + final List tagList = tagRepository.findTagByPost(post); + if (post.isPrivate() && !isAccessAuthorized(post, tagList, loginMember)) { + throw new GeneralException(PRIVATE_POST); + } + final Boolean isFollow = isMyPost(post, loginMember) ? + null : followRepository.existsBySenderAndReceiver(loginMember, post.getOwner()); + final Boolean isLike = likeRepository.existsByLikeMemberAndLikedPost(loginMember, post); + final int likeCount = likeRepository.countByLikedPost(post); + final Comment comment = commentRepository.findFirstByPost(post).orElse(null); + + return PostDetailResponse.of() + .post(post) + .tagList(tagList) + .isFollow(isFollow) + .isLike(isLike) + .likeCount(likeCount) + .recentComment(comment) + .build(); + } + + private Boolean isAccessAuthorized(final Post post, + final List tagList, + final Member loginMember) { + final List taggedMemberHandleList = tagList.stream() + .map( + tag -> tag.getMember().getHandle() + ).collect(Collectors.toList()); + return isMyPost(post, loginMember) || taggedMemberHandleList.contains(loginMember.getHandle()); + } + + private Boolean isMyPost(final Post post, + final Member loginMember) { + return post.getOwner().getId().equals(loginMember.getId()); + } + + @Transactional + public void savePost( + final MultipartFile postImage, + final PostUploadRequest request + ) { + final Member loginMember = jwtService.getLoginMember(); + final String image = s3Service.upload(postImage, POST); + final Post post = request.toEntity(image, loginMember); + postRepository.save(post); + final List taggedMemberHandles = request.getTaggedMemberHandleList(); + + // TODO: N+1 고치기 + final List taggedMemberList = taggedMemberHandles.stream().map( + memberRepository::findByHandle + ).collect(Collectors.toList()); + + final List tagList = taggedMemberList.stream().map( + member -> Tag.builder() + .member(member) + .post(post) + .build() + ).collect(Collectors.toList()); + tagRepository.saveAll(tagList); + + final List tagApprovalAlarmList = tagList.stream().map( + tag -> TagApprovalAlarm.builder() + .tag(tag) + .receiver(tag.getMember()) + .build() + ).collect(Collectors.toList()); + alarmRepository.saveAll(tagApprovalAlarmList); + } } diff --git a/pochak/src/main/java/com/apps/pochak/tag/domain/Tag.java b/pochak/src/main/java/com/apps/pochak/tag/domain/Tag.java index 944062c5..e5febcc5 100644 --- a/pochak/src/main/java/com/apps/pochak/tag/domain/Tag.java +++ b/pochak/src/main/java/com/apps/pochak/tag/domain/Tag.java @@ -4,8 +4,10 @@ import com.apps.pochak.member.domain.Member; import com.apps.pochak.post.domain.Post; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; @@ -33,5 +35,13 @@ public class Tag extends BaseEntity { @JoinColumn(name = "member_id") private Member member; + @Setter + @Column(columnDefinition = "boolean default false") private Boolean isAccepted; + + @Builder + public Tag(Post post, Member member) { + this.post = post; + this.member = member; + } } diff --git a/pochak/src/main/java/com/apps/pochak/tag/domain/repository/TagRepository.java b/pochak/src/main/java/com/apps/pochak/tag/domain/repository/TagRepository.java index 34c42717..41332af5 100644 --- a/pochak/src/main/java/com/apps/pochak/tag/domain/repository/TagRepository.java +++ b/pochak/src/main/java/com/apps/pochak/tag/domain/repository/TagRepository.java @@ -1,8 +1,17 @@ package com.apps.pochak.tag.domain.repository; +import com.apps.pochak.post.domain.Post; import com.apps.pochak.tag.domain.Tag; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface TagRepository extends JpaRepository { + @Query("select t from Tag t " + + "join fetch t.member " + + "where t.post = :post ") + List findTagByPost(@Param("post") Post post); } diff --git a/pochak/src/main/resources/application.properties b/pochak/src/main/resources/application.properties index 2ca507b1..ee051970 100644 --- a/pochak/src/main/resources/application.properties +++ b/pochak/src/main/resources/application.properties @@ -1,5 +1,5 @@ spring.mvc.pathmatch.matching-strategy=ant_path_matcher -spring.profiles.include=API-KEY, OAUTH, JWT +spring.profiles.include=API-KEY, OAUTH, JWT, TEST server.port=8080 spring.servlet.multipart.maxFileSize=50MB diff --git a/pochak/src/main/resources/static/index.html b/pochak/src/main/resources/static/index.html index 4dbd3c2d..e4f618ec 100644 --- a/pochak/src/main/resources/static/index.html +++ b/pochak/src/main/resources/static/index.html @@ -5,6 +5,6 @@ POCHAK -

hi POCHAK! 📸

+

hi POCHAK Develop Server! 📸

\ No newline at end of file diff --git a/pochak/src/test/java/com/apps/pochak/post/controller/PostControllerTest.java b/pochak/src/test/java/com/apps/pochak/post/controller/PostControllerTest.java new file mode 100644 index 00000000..177be2eb --- /dev/null +++ b/pochak/src/test/java/com/apps/pochak/post/controller/PostControllerTest.java @@ -0,0 +1,209 @@ +package com.apps.pochak.post.controller; + +import com.apps.pochak.post.dto.request.PostUploadRequest; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import java.io.FileInputStream; +import java.util.ArrayList; + +import static com.apps.pochak.common.ApiDocumentUtils.getDocumentRequest; +import static com.apps.pochak.common.ApiDocumentUtils.getDocumentResponse; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureRestDocs +@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) +class PostControllerTest { + + @Value("${test.authorization}") + String authorization; + + @Autowired + MockMvc mockMvc; + + @Autowired + WebApplicationContext wac; + + ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .addFilter(new CharacterEncodingFilter("UTF-8", true)) + .apply(documentationConfiguration(restDocumentation)) + .build(); + } + + @Test + @Transactional + @DisplayName("Post Upload API Document") + void signUpTest() throws Exception { + final String fileName = "APPS_LOGO"; + final String fileType = "PNG"; + + final FileInputStream fileInputStream + = new FileInputStream("src/test/resources/static/" + fileName + "." + fileType); + final MockMultipartFile postImage = new MockMultipartFile( + "postImage", + fileName + "." + fileType, + "multipart/form-data", + fileInputStream + ); + + final String caption = "안녕하세요. 게시물 업로드를 테스트해보겠습니다."; + final ArrayList taggedMemberHandles = new ArrayList<>(); + taggedMemberHandles.add("_skf__11"); + taggedMemberHandles.add("habongee"); + + final PostUploadRequest postUploadRequest = new PostUploadRequest(caption, taggedMemberHandles); + + final String content = objectMapper.writeValueAsString(postUploadRequest); + final MockMultipartFile request + = new MockMultipartFile( + "request", + "request", + "application/json", + content.getBytes(UTF_8) + ); + + this.mockMvc.perform( + multipart("/api/v2/posts") + .file(postImage) + .file(request) + .header("Authorization", authorization) + ).andExpect(status().isOk()) + .andDo( + document("upload-post", + getDocumentRequest(), + getDocumentResponse(), + requestHeaders( + headerWithName("Authorization").description("Basic auth credentials") + ), + requestParts( + partWithName("postImage").description("업로드 할 게시물 사진 파일 : 빈 파일 전달 시 에러 발생"), + partWithName("request").description("게시물 업로드 DTO") + ), + requestPartFields( + "request", + fieldWithPath("caption").type(STRING) + .description( + "`request.content` 업로드 할 게시물 내용 \n" + + ": null 또는 empty 문자열 전달도 가능합니다." + ), + fieldWithPath("taggedMemberHandleList").type(ARRAY) + .description( + "`request.taggedMemberHandleList` 태그된 멤버 아이디 리스트 \n" + + ": 1개 이상의 아이디를 전달해야 합니다." + ) + ), + responseFields( + fieldWithPath("isSuccess").type(BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(STRING).description("결과 코드"), + fieldWithPath("message").type(STRING).description("결과 메세지") + ) + + ) + ); + } + + @Test + @Transactional + @DisplayName("Get Post Detail API Document") + void getPostDetailTest() throws Exception { + this.mockMvc.perform( + RestDocumentationRequestBuilders + .get("/api/v2/posts/{postId}", 2) + .header("Authorization", authorization) + .contentType(APPLICATION_JSON) + ).andExpect(status().isOk()) + .andDo( + document("get-detail-post", + getDocumentRequest(), + getDocumentResponse(), + requestHeaders( + headerWithName("Authorization") + .description( + "Basic auth credentials \n" + + ": 만약 아직 수락된 게시물이 아니라면 게시자와 태그된 사람만 접근 가능합니다." + ) + ), + pathParameters( + parameterWithName("postId").description("게시물 아이디") + ), + responseFields( + fieldWithPath("isSuccess").type(BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(STRING).description("결과 코드"), + fieldWithPath("message").type(STRING).description("결과 메세지"), + fieldWithPath("result").type(OBJECT).description("결과 데이터"), + fieldWithPath("result.ownerHandle").type(STRING).description("게시자 아이디 (handle)"), + fieldWithPath("result.ownerProfileImage").type(STRING).description("게시자 프로필 이미지"), + fieldWithPath("result.taggedMemberHandle").type(ARRAY).description("태그된 유저들의 아이디"), + fieldWithPath("result.isFollow").type(BOOLEAN) + .description( + "현재 로그인한 유저가 게시자를 팔로우하고 있는지 여부 \n" + + ": 만약 로그인한 유저가 게시자라면 null로 전달됨." + ), + fieldWithPath("result.postImage").type(STRING).description("게시물 이미지 URL"), + fieldWithPath("result.isLike").type(BOOLEAN) + .description( + "현재 로그인한 유저가 해당 게시물의 좋아요를 눌렀는지 여부" + ), + fieldWithPath("result.likeCount").type(NUMBER).description("게시물의 좋아요 개수"), + fieldWithPath("result.caption").type(STRING).description("게시물의 caption"), + fieldWithPath("result.recentComment").type(OBJECT) + .description( + "게시물의 가장 최근 댓글 : 댓글이 없는 경우 NULL이 전달됨." + ), + fieldWithPath("result.recentComment.profileImage").type(STRING) + .description( + "게시물의 가장 최근 댓글 : 댓글 게시자의 프로필 이미지" + ).optional(), + fieldWithPath("result.recentComment.handle").type(STRING) + .description( + "게시물의 가장 최근 댓글 : 댓글 게시자의 아이디 (handle)" + ).optional(), + fieldWithPath("result.recentComment.createdDate").type(STRING) + .description( + "게시물의 가장 최근 댓글 : 댓글 게시 시간" + ).optional(), + fieldWithPath("result.recentComment.content").type(STRING) + .description( + "게시물의 가장 최근 댓글 : 댓글 내용" + ).optional() + ) + + ) + ); + } + +} \ No newline at end of file diff --git a/pochak/src/test/resources/static/APPS_LOGO.PNG b/pochak/src/test/resources/static/APPS_LOGO.PNG new file mode 100644 index 00000000..893c4717 Binary files /dev/null and b/pochak/src/test/resources/static/APPS_LOGO.PNG differ diff --git a/scripts/initialize.sh b/scripts/initialize.sh deleted file mode 100644 index ec54e354..00000000 --- a/scripts/initialize.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -PROJECT_ROOT="/home/ubuntu/pochakapp" -JAR_FILE="$PROJECT_ROOT/spring-webapp.jar" - -DEPLOY_LOG="$PROJECT_ROOT/deploy.log" - -TIME_NOW=$(date +%c) - - -# 현재 구동 중인 애플리케이션 pid 확인 -CURRENT_PID=$(pgrep -f $JAR_FILE) diff --git a/scripts/start.sh b/scripts/start.sh index db860040..9061a49b 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash PROJECT_ROOT="/home/ubuntu/pochakapp" -JAR_FILE="$PROJECT_ROOT/spring-webapp.jar" APP_LOG="$PROJECT_ROOT/application.log" ERROR_LOG="$PROJECT_ROOT/error.log" @@ -14,9 +13,19 @@ echo "$TIME_NOW > $JAR_PATH 확인" >> $DEPLOY_LOG JAR_NAME=$(ls $PROJECT_ROOT/pochak/build/libs/ | grep 'SNAPSHOT.jar' | tail -n 1) JAR_PATH=$PROJECT_ROOT/pochak/build/libs/$JAR_NAME +CURRENT_PID=$(pgrep -f $JAR_NAME) + +if [ -z $CURRENT_PID ] +then + echo "> 종료할 애플리케이션이 없습니다." +else + echo "> kill -9 $CURRENT_PID" + sudo kill -15 $CURRENT_PID + sleep 5 +fi + # jar 파일 실행 -echo "$TIME_NOW > $JAR_FILE 파일 실행" >> $DEPLOY_LOG +echo "$TIME_NOW > $JAR_NAME 파일 실행" >> $DEPLOY_LOG nohup java -jar -Duser.timezone=Asia/Seoul $JAR_PATH --logging.level.org.hibernate.SQL=DEBUG > $APP_LOG 2> $ERROR_LOG & -CURRENT_PID=$(pgrep -f $JAR_FILE) echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG diff --git a/scripts/stop.sh b/scripts/stop.sh index 1b451862..506b30f6 100644 --- a/scripts/stop.sh +++ b/scripts/stop.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash PROJECT_ROOT="/home/ubuntu/pochakapp" -JAR_FILE="$PROJECT_ROOT/spring-webapp.jar" +JAR_FILE="$PROJECT_ROOT/pochak/build/libs/pochak-0.0.1-SNAPSHOT.jar" +JAR_ROOT="$PROJECT_ROOT/pochak/build/libs/" DEPLOY_LOG="$PROJECT_ROOT/deploy.log" @@ -15,5 +16,5 @@ if [ -z $CURRENT_PID ]; then echo "$TIME_NOW > 현재 실행중인 애플리케이션이 없습니다" >> $DEPLOY_LOG else echo "$TIME_NOW > 실행중인 $CURRENT_PID 애플리케이션 종료 " >> $DEPLOY_LOG - kill -15 $CURRENT_PID -fi + sudo kill -15 $CURRENT_PID +fi \ No newline at end of file