diff --git a/build.gradle b/build.gradle index 314edb13b..d7d543c9b 100644 --- a/build.gradle +++ b/build.gradle @@ -149,6 +149,9 @@ dependencies { //restAssured testImplementation 'io.rest-assured:rest-assured:4.4.0' testImplementation 'io.rest-assured:spring-mock-mvc:4.4.0' + + // Mongodb 의존성 추가 + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' } task prepareDirs { @@ -194,6 +197,7 @@ bootJar { querydsl { jpa = true querydslSourcesDir = querydslDir + springDataMongo = true } // build시 사용할 sourceSet 추가 설정 diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index fb788f7d8..76b74db1a 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -8,59 +8,59 @@ :sectnums: :docinfo: shared-head -[[overview]] -= Overview - -[[overview_http_verbs]] -== HTTP Method - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP verbs. - -|=== -| Verb | Usage - -| `GET` -| Used to retrieve a resource - -| `POST` -| Used to create a new resource - -| `PUT` -| PUT 설명 - -| `PATCH` -| Used to update an existing resource, including partial updates - -| `DELETE` -| Used to delete an existing resource -|=== - -[[overview_http_status_codes]] -== HTTP status codes - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP status codes. - -|=== -| 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 -|=== +// [[overview]] +// = Overview +// +// [[overview_http_verbs]] +// == HTTP Method +// +// RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +// use of HTTP verbs. + +// |=== +// | Verb | Usage +// +// | `GET` +// | Used to retrieve a resource +// +// | `POST` +// | Used to create a new resource +// +// | `PUT` +// | PUT 설명 +// +// | `PATCH` +// | Used to update an existing resource, including partial updates +// +// | `DELETE` +// | Used to delete an existing resource +// |=== + +// [[overview_http_status_codes]] +// == HTTP status codes +// +// RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +// use of HTTP status codes. + +// |=== +// | 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 +// |=== include::board.adoc[] include::post.adoc[] @@ -74,4 +74,5 @@ include::recruit.adoc[] include::recruitApplication.adoc[] include::recruitComment.adoc[] include::storage.adoc[] -include::report.adoc[] \ No newline at end of file +include::report.adoc[] +include::notification.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/notification.adoc b/src/docs/asciidoc/notification.adoc new file mode 100644 index 000000000..18dddb1e4 --- /dev/null +++ b/src/docs/asciidoc/notification.adoc @@ -0,0 +1,7 @@ +== 알림 + +=== 알림 목록 조회 +operation::notification/get-notifications-by-cursor[snippets='http-request,http-response,cookie,response-fields'] + +=== 새로운 알림 확인 +operation::notification/check-notification[snippets='http-request,http-response,cookie,response-fields'] \ No newline at end of file diff --git a/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java b/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java index 694e4a90e..152796429 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java +++ b/src/main/java/com/ssafy/ssafsound/domain/comment/domain/Comment.java @@ -1,7 +1,6 @@ package com.ssafy.ssafsound.domain.comment.domain; import com.ssafy.ssafsound.domain.BaseTimeEntity; -import com.ssafy.ssafsound.domain.comment.dto.PostCommentWriteReqDto; import com.ssafy.ssafsound.domain.member.domain.Member; import com.ssafy.ssafsound.domain.post.domain.Post; import lombok.AllArgsConstructor; @@ -65,4 +64,20 @@ public void updateComment(String content, Boolean anonymity) { this.content = content; this.anonymity = anonymity; } + + public Boolean isMine(Member member) { + return this.getMember().getId().equals(member.getId()); + } + + public Long getAuthorId() { + return this.getMember().getId(); + } + + public Boolean isAssociatedWithPost(Post post) { + return this.post.getId().equals(post.getId()); + } + + public Boolean isParent() { + return this.getId().equals(this.commentGroup.getId()); + } } diff --git a/src/main/java/com/ssafy/ssafsound/domain/comment/exception/CommentErrorInfo.java b/src/main/java/com/ssafy/ssafsound/domain/comment/exception/CommentErrorInfo.java index 9abb6b337..0f8f9f5a9 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/comment/exception/CommentErrorInfo.java +++ b/src/main/java/com/ssafy/ssafsound/domain/comment/exception/CommentErrorInfo.java @@ -7,7 +7,9 @@ public enum CommentErrorInfo { NOT_FOUND_COMMENT_NUMBER("806", "익명 번호를 찾을 수 없습니다."), NOT_FOUND_COMMENT("807", "댓글을 찾을 수 없습니다."), UNAUTHORIZED_UPDATE_COMMENT("808", "댓글을 수정할 권한이 없습니다"), - UNAUTHORIZED_DELETE_COMMENT("809", "댓글을 삭제할 권한이 없습니다"); + UNAUTHORIZED_DELETE_COMMENT("809", "댓글을 삭제할 권한이 없습니다"), + NOT_ASSOCIATED_WITH_POST("810", "댓글과 게시글이 일치하지 않습니다."), + FORBIDDEN_REPLY_SUB_COMMENT("811", "대댓글에는 답변을 작성할 수 없습니다."); private final String code; private final String message; diff --git a/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java b/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java index 6adee2a36..d90c9aa12 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java +++ b/src/main/java/com/ssafy/ssafsound/domain/comment/repository/CommentRepository.java @@ -1,6 +1,7 @@ package com.ssafy.ssafsound.domain.comment.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -26,4 +27,10 @@ public interface CommentRepository extends JpaRepository { + "where comment_id = :id", nativeQuery = true) void updateByCommentGroup(@Param("id") Long id); + @Query(value = "select c " + + "from comment c " + + "join fetch c.member " + + "join fetch c.commentGroup " + + "where c.id = :id") + Optional findByIdWithMemberAndCommentGroup(Long id); } diff --git a/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java b/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java index 8ac19cfe4..5bfd71fe1 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java +++ b/src/main/java/com/ssafy/ssafsound/domain/comment/service/CommentService.java @@ -4,7 +4,10 @@ import com.ssafy.ssafsound.domain.comment.domain.Comment; import com.ssafy.ssafsound.domain.comment.domain.CommentLike; import com.ssafy.ssafsound.domain.comment.domain.CommentNumber; -import com.ssafy.ssafsound.domain.comment.dto.*; +import com.ssafy.ssafsound.domain.comment.dto.CommentIdElement; +import com.ssafy.ssafsound.domain.comment.dto.GetCommentResDto; +import com.ssafy.ssafsound.domain.comment.dto.PatchCommentUpdateReqDto; +import com.ssafy.ssafsound.domain.comment.dto.PostCommentWriteReqDto; import com.ssafy.ssafsound.domain.comment.exception.CommentErrorInfo; import com.ssafy.ssafsound.domain.comment.exception.CommentException; import com.ssafy.ssafsound.domain.comment.repository.CommentLikeRepository; @@ -14,6 +17,10 @@ import com.ssafy.ssafsound.domain.member.exception.MemberErrorInfo; import com.ssafy.ssafsound.domain.member.exception.MemberException; import com.ssafy.ssafsound.domain.member.repository.MemberRepository; +import com.ssafy.ssafsound.domain.notification.domain.NotificationType; +import com.ssafy.ssafsound.domain.notification.domain.ServiceType; +import com.ssafy.ssafsound.domain.notification.event.NotificationEvent; +import com.ssafy.ssafsound.domain.notification.message.NotificationMessage; import com.ssafy.ssafsound.domain.post.domain.Post; import com.ssafy.ssafsound.domain.post.dto.PostCommonLikeResDto; import com.ssafy.ssafsound.domain.post.exception.PostErrorInfo; @@ -21,10 +28,13 @@ import com.ssafy.ssafsound.domain.post.repository.PostRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Service @Slf4j @@ -35,18 +45,16 @@ public class CommentService { private final CommentLikeRepository commentLikeRepository; private final PostRepository postRepository; private final MemberRepository memberRepository; - + private final ApplicationEventPublisher applicationEventPublisher; @Transactional public CommentIdElement writeComment(Long postId, Long loginMemberId, PostCommentWriteReqDto postCommentWriteReqDto) { - if (!postRepository.existsById(postId)) { - throw new PostException(PostErrorInfo.NOT_FOUND_POST); - } + Post post = postRepository.findByIdWithMember(postId) + .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); Member loginMember = memberRepository.findById(loginMemberId) .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - // 1. 익명 번호 부여 CommentNumber commentNumber = commentNumberRepository. findByPostIdAndMemberId(postId, loginMemberId).orElse(null); @@ -59,10 +67,9 @@ public CommentIdElement writeComment(Long postId, Long loginMemberId, PostCommen commentNumberRepository.save(commentNumber); } - // 2. 댓글 저장 Comment comment = Comment.builder() - .post(postRepository.getReferenceById(postId)) - .member(memberRepository.getReferenceById(loginMemberId)) + .post(post) + .member(loginMember) .content(postCommentWriteReqDto.getContent()) .anonymity(postCommentWriteReqDto.getAnonymity()) .commentNumber(commentNumber) @@ -72,18 +79,23 @@ public CommentIdElement writeComment(Long postId, Long loginMemberId, PostCommen comment = commentRepository.save(comment); commentRepository.updateByCommentGroup(comment.getId()); + if (!post.isMine(loginMember)) { + publishNotificationEventFromPostReply(post); + } + return new CommentIdElement(comment.getId()); } @Transactional(readOnly = true) public GetCommentResDto findComments(Long postId, AuthenticatedMember loginMember) { - if (!postRepository.existsById(postId)) { + if (!postRepository.existsById(postId)) { throw new PostException(PostErrorInfo.NOT_FOUND_POST); } - + List comments = commentRepository.findAllPostIdWithDetailsFetchOrderByCommentGroupId(postId); return GetCommentResDto.of(comments, loginMember); } + @Transactional public CommentIdElement updateComment(Long commentId, Long loginMemberId, PatchCommentUpdateReqDto patchCommentUpdateReqDto) { Comment comment = commentRepository.findById(commentId) @@ -102,41 +114,56 @@ public CommentIdElement updateComment(Long commentId, Long loginMemberId, PatchC @Transactional public CommentIdElement writeCommentReply(Long postId, Long commentId, Long loginMemberId, PostCommentWriteReqDto postCommentWriteReplyReqDto) { - if (!postRepository.existsById(postId)) { - throw new PostException(PostErrorInfo.NOT_FOUND_POST); - } - - if (!commentRepository.existsById(commentId)) { - throw new CommentException(CommentErrorInfo.NOT_FOUND_COMMENT); - } + Post post = postRepository.findByIdWithMember(postId) + .orElseThrow(() -> new PostException(PostErrorInfo.NOT_FOUND_POST)); Member loginMember = memberRepository.findById(loginMemberId) .orElseThrow(() -> new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID)); - // 1. 익명 번호 부여 + Comment parentComment = commentRepository.findByIdWithMemberAndCommentGroup(commentId) + .orElseThrow(() -> new CommentException(CommentErrorInfo.NOT_FOUND_COMMENT)); + + if (!parentComment.isAssociatedWithPost(post)) { + throw new CommentException(CommentErrorInfo.NOT_ASSOCIATED_WITH_POST); + } + + if (!parentComment.isParent()) { + throw new CommentException(CommentErrorInfo.FORBIDDEN_REPLY_SUB_COMMENT); + } + CommentNumber commentNumber = commentNumberRepository. findByPostIdAndMemberId(postId, loginMemberId).orElse(null); if (commentNumber == null) { commentNumber = CommentNumber.builder() - .post(postRepository.getReferenceById(postId)) + .post(post) .member(loginMember) .number(commentNumberRepository.countAllByPostId(postId) + 1) .build(); commentNumberRepository.save(commentNumber); } - // 2. 대댓글 저장 Comment comment = Comment.builder() - .post(postRepository.getReferenceById(postId)) + .post(post) .member(loginMember) .content(postCommentWriteReplyReqDto.getContent()) .anonymity(postCommentWriteReplyReqDto.getAnonymity()) .commentNumber(commentNumber) - .commentGroup(commentRepository.getReferenceById(commentId)) + .commentGroup(parentComment) .build(); comment = commentRepository.save(comment); + + Set visited = new HashSet<>(); + if (!parentComment.isMine(loginMember)) { + visited.add(parentComment.getAuthorId()); + publishNotificationEventFromCommentReply(post, parentComment); + } + if (!post.isMine(loginMember) && !visited.contains(post.getAuthorId())) { + visited.add(post.getAuthorId()); + publishNotificationEventFromPostReply(post); + } + return new CommentIdElement(comment.getId()); } @@ -192,4 +219,26 @@ public CommentIdElement deleteComment(Long commentId, Long loginMemberId) { commentRepository.delete(comment); return new CommentIdElement(comment.getId()); } + + private void publishNotificationEventFromPostReply(Post post) { + NotificationEvent notificationEvent = NotificationEvent.builder() + .ownerId(post.getAuthorId()) + .message(String.format(NotificationMessage.POST_REPLY_MESSAGE.getMessage(), post.getTitle())) + .contentId(post.getId()) + .serviceType(ServiceType.POST) + .notificationType(NotificationType.POST_REPLAY) + .build(); + applicationEventPublisher.publishEvent(notificationEvent); + } + + private void publishNotificationEventFromCommentReply(Post post, Comment comment) { + NotificationEvent notificationEvent = NotificationEvent.builder() + .ownerId(comment.getAuthorId()) + .message(String.format(NotificationMessage.COMMENT_REPLY_MESSAGE.getMessage(), post.getTitle())) + .contentId(post.getId()) + .serviceType(ServiceType.POST) + .notificationType(NotificationType.COMMENT_REPLAY) + .build(); + applicationEventPublisher.publishEvent(notificationEvent); + } } diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/controller/NotificationController.java b/src/main/java/com/ssafy/ssafsound/domain/notification/controller/NotificationController.java new file mode 100644 index 000000000..a0ef647b7 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/controller/NotificationController.java @@ -0,0 +1,40 @@ +package com.ssafy.ssafsound.domain.notification.controller; + +import com.ssafy.ssafsound.domain.auth.dto.AuthenticatedMember; +import com.ssafy.ssafsound.domain.auth.validator.Authentication; +import com.ssafy.ssafsound.domain.notification.dto.GetCheckNotificationResDto; +import com.ssafy.ssafsound.domain.notification.dto.GetNotificationReqDto; +import com.ssafy.ssafsound.domain.notification.dto.GetNotificationResDto; +import com.ssafy.ssafsound.domain.notification.service.NotificationService; +import com.ssafy.ssafsound.global.common.response.EnvelopeResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/notifications") +@RequiredArgsConstructor +@Slf4j +public class NotificationController { + private final NotificationService notificationService; + + @GetMapping() + public EnvelopeResponse getNotificationsByCursor(@Authentication AuthenticatedMember authenticatedMember, + @Valid @ModelAttribute GetNotificationReqDto getNotificationReqDto) { + return EnvelopeResponse.builder() + .data(notificationService.getNotifications(authenticatedMember, getNotificationReqDto)) + .build(); + } + + @GetMapping("/new") + public EnvelopeResponse checkNotification(@Authentication AuthenticatedMember authenticatedMember) { + return EnvelopeResponse.builder() + .data(notificationService.checkNotification(authenticatedMember)) + .build(); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/converter/NotificationTypeConverter.java b/src/main/java/com/ssafy/ssafsound/domain/notification/converter/NotificationTypeConverter.java new file mode 100644 index 000000000..7a06de0f2 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/converter/NotificationTypeConverter.java @@ -0,0 +1,17 @@ +package com.ssafy.ssafsound.domain.notification.converter; + +import com.ssafy.ssafsound.domain.notification.domain.NotificationType; + +import javax.persistence.AttributeConverter; + +public class NotificationTypeConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(NotificationType attribute) { + return attribute.getName(); + } + + @Override + public NotificationType convertToEntityAttribute(String dbData) { + return NotificationType.ofName(dbData); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/converter/ServiceTypeConverter.java b/src/main/java/com/ssafy/ssafsound/domain/notification/converter/ServiceTypeConverter.java new file mode 100644 index 000000000..2c66c421d --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/converter/ServiceTypeConverter.java @@ -0,0 +1,17 @@ +package com.ssafy.ssafsound.domain.notification.converter; + +import com.ssafy.ssafsound.domain.notification.domain.ServiceType; + +import javax.persistence.AttributeConverter; + +public class ServiceTypeConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(ServiceType attribute) { + return attribute.getName(); + } + + @Override + public ServiceType convertToEntityAttribute(String dbData) { + return ServiceType.ofName(dbData); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/domain/AutoIncrementSequence.java b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/AutoIncrementSequence.java new file mode 100644 index 000000000..f82b9a8e9 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/AutoIncrementSequence.java @@ -0,0 +1,16 @@ +package com.ssafy.ssafsound.domain.notification.domain; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Getter +@Setter +@Document(collection = "AutoIncrementSequences") +public class AutoIncrementSequence { + @Id + private String id; + + private Long sequence; +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/domain/Notification.java b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/Notification.java new file mode 100644 index 000000000..0c2ce1fed --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/Notification.java @@ -0,0 +1,65 @@ +package com.ssafy.ssafsound.domain.notification.domain; + +import com.ssafy.ssafsound.domain.notification.converter.NotificationTypeConverter; +import com.ssafy.ssafsound.domain.notification.converter.ServiceTypeConverter; +import com.ssafy.ssafsound.domain.notification.event.NotificationEvent; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import javax.persistence.Convert; +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Document(collection = "notifications") +public class Notification { + @Transient + public static final String SEQUENCE_NAME = "NOTIFICATIONS_SEQUENCE"; + + @Id + private Long id; + + @Indexed + private Long ownerId; + + private String message; + + private Long contentId; + + @Convert(converter = ServiceTypeConverter.class) + private ServiceType serviceType; + + @Convert(converter = NotificationTypeConverter.class) + private NotificationType notificationType; + + @Builder.Default + private Boolean read = Boolean.FALSE; + + @Indexed(name = "createdAtIndex", expireAfter = "30d") + @CreatedDate + private LocalDateTime createdAt; + + public void setId(Long id) { + this.id = id; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public static Notification from(NotificationEvent notificationEvent) { + return Notification.builder() + .ownerId(notificationEvent.getOwnerId()) + .message(notificationEvent.getMessage()) + .contentId(notificationEvent.getContentId()) + .serviceType(notificationEvent.getServiceType()) + .notificationType(notificationEvent.getNotificationType()) + .build(); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/domain/NotificationType.java b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/NotificationType.java new file mode 100644 index 000000000..ef3d2b8b0 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/NotificationType.java @@ -0,0 +1,30 @@ +package com.ssafy.ssafsound.domain.notification.domain; + +import com.ssafy.ssafsound.domain.notification.exception.NotificationErrorInfo; +import com.ssafy.ssafsound.domain.notification.exception.NotificationException; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum NotificationType { + SYSTEM(1, "SYSTEM"), + POST_REPLAY(2, "POST_REPLAY"), + COMMENT_REPLAY(3, "COMMENT_REPLAY"), + RECRUIT(4, "RECRUIT"); + + private final Integer id; + private final String name; + + NotificationType(Integer id, String name) { + this.id = id; + this.name = name; + } + + public static NotificationType ofName(String name) { + return Arrays.stream(NotificationType.values()) + .filter(value -> value.getName().equals(name)) + .findAny() + .orElseThrow(() -> new NotificationException(NotificationErrorInfo.NOT_FOUND_NOTIFICATION_TYPE)); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/domain/ServiceType.java b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/ServiceType.java new file mode 100644 index 000000000..ce2b3f0e0 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/domain/ServiceType.java @@ -0,0 +1,29 @@ +package com.ssafy.ssafsound.domain.notification.domain; + +import com.ssafy.ssafsound.domain.notification.exception.NotificationErrorInfo; +import com.ssafy.ssafsound.domain.notification.exception.NotificationException; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum ServiceType { + SYSTEM(1, "SYSTEM"), + POST(2, "POST"), + RECRUIT(3, "RECRUIT"); + + private final Integer id; + private final String name; + + ServiceType(Integer id, String name) { + this.id = id; + this.name = name; + } + + public static ServiceType ofName(String name) { + return Arrays.stream(ServiceType.values()) + .filter(value -> value.getName().equals(name)) + .findAny() + .orElseThrow(() -> new NotificationException(NotificationErrorInfo.NOT_FOUND_SERVICE_TYPE)); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetCheckNotificationResDto.java b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetCheckNotificationResDto.java new file mode 100644 index 000000000..8199e187d --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetCheckNotificationResDto.java @@ -0,0 +1,10 @@ +package com.ssafy.ssafsound.domain.notification.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GetCheckNotificationResDto { + private Boolean isNew; +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationElement.java b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationElement.java new file mode 100644 index 000000000..8e2b65568 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationElement.java @@ -0,0 +1,34 @@ +package com.ssafy.ssafsound.domain.notification.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ssafy.ssafsound.domain.notification.domain.Notification; +import com.ssafy.ssafsound.domain.notification.domain.NotificationType; +import com.ssafy.ssafsound.domain.notification.domain.ServiceType; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class GetNotificationElement { + private Long notificationId; + private String message; + private Long contentId; + private ServiceType serviceType; + private NotificationType notificationType; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createAt; + + public static GetNotificationElement from(Notification notification) { + return GetNotificationElement.builder() + .notificationId(notification.getId()) + .message(notification.getMessage()) + .contentId(notification.getContentId()) + .serviceType(notification.getServiceType()) + .notificationType(notification.getNotificationType()) + .createAt(notification.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationReqDto.java b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationReqDto.java new file mode 100644 index 000000000..b57af203a --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationReqDto.java @@ -0,0 +1,27 @@ +package com.ssafy.ssafsound.domain.notification.dto; + +import lombok.*; + +import javax.validation.constraints.Min; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GetNotificationReqDto { + @Builder.Default + private Long cursor = -1L; + + @Min(value = 10, message = "Size가 너무 작습니다.") + @Builder.Default + private Integer size = 10; + + public void setCursor(Long cursor) { + if (cursor <= -1) { + this.cursor = -1L; + return; + } + this.cursor = cursor; + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationResDto.java b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationResDto.java new file mode 100644 index 000000000..5b988d7ba --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/dto/GetNotificationResDto.java @@ -0,0 +1,28 @@ +package com.ssafy.ssafsound.domain.notification.dto; + +import com.ssafy.ssafsound.domain.notification.domain.Notification; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +public class GetNotificationResDto { + private List notifications; + private Long cursor; + + public static GetNotificationResDto of(List notifications, Integer pageSize) { + int size = notifications.size(); + boolean hasNext = size == pageSize + 1; + + return GetNotificationResDto.builder() + .notifications((hasNext ? notifications.subList(0, size - 1) : notifications) + .stream() + .map(GetNotificationElement::from) + .collect(Collectors.toList())) + .cursor(hasNext ? notifications.get(size - 1).getId() : null) + .build(); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/event/NotificationEvent.java b/src/main/java/com/ssafy/ssafsound/domain/notification/event/NotificationEvent.java new file mode 100644 index 000000000..698ecd5b5 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/event/NotificationEvent.java @@ -0,0 +1,16 @@ +package com.ssafy.ssafsound.domain.notification.event; + +import com.ssafy.ssafsound.domain.notification.domain.NotificationType; +import com.ssafy.ssafsound.domain.notification.domain.ServiceType; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class NotificationEvent { + private Long ownerId; + private String message; + private Long contentId; + private ServiceType serviceType; + private NotificationType notificationType; +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/event/NotificationEventListener.java b/src/main/java/com/ssafy/ssafsound/domain/notification/event/NotificationEventListener.java new file mode 100644 index 000000000..3153c7772 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/event/NotificationEventListener.java @@ -0,0 +1,31 @@ +package com.ssafy.ssafsound.domain.notification.event; + +import com.ssafy.ssafsound.domain.notification.domain.Notification; +import com.ssafy.ssafsound.domain.notification.service.NotificationService; +import com.ssafy.ssafsound.domain.notification.service.SequenceGeneratorService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +public class NotificationEventListener extends AbstractMongoEventListener { + private final NotificationService notificationService; + private final SequenceGeneratorService sequenceGeneratorService; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotificationEvent(NotificationEvent notificationEvent) { + notificationService.sendNotification(notificationEvent); + } + + @Override + public void onBeforeConvert(BeforeConvertEvent event) { + event.getSource().setId(sequenceGeneratorService.generateSequence(Notification.SEQUENCE_NAME)); + event.getSource().setCreatedAt(LocalDateTime.now()); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/exception/NotificationErrorInfo.java b/src/main/java/com/ssafy/ssafsound/domain/notification/exception/NotificationErrorInfo.java new file mode 100644 index 000000000..836ec5ef6 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/exception/NotificationErrorInfo.java @@ -0,0 +1,18 @@ +package com.ssafy.ssafsound.domain.notification.exception; + +import lombok.Getter; + +@Getter +public enum NotificationErrorInfo { + NOT_FOUND_SERVICE_TYPE("1200", "잘못된 서비스 타입입니다."), + NOT_FOUND_NOTIFICATION_TYPE("1201", "잘못된 알림 타입입니다."), + NOT_FOUND_NOTIFICATION_BY_OWNER("1202", "해당 사용자의 알림이 없습니다."); + + private final String code; + private final String message; + + NotificationErrorInfo(String code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/exception/NotificationException.java b/src/main/java/com/ssafy/ssafsound/domain/notification/exception/NotificationException.java new file mode 100644 index 000000000..67e281afb --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/exception/NotificationException.java @@ -0,0 +1,10 @@ +package com.ssafy.ssafsound.domain.notification.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class NotificationException extends RuntimeException{ + private NotificationErrorInfo info; +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/message/NotificationMessage.java b/src/main/java/com/ssafy/ssafsound/domain/notification/message/NotificationMessage.java new file mode 100644 index 000000000..db73314c1 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/message/NotificationMessage.java @@ -0,0 +1,15 @@ +package com.ssafy.ssafsound.domain.notification.message; + +import lombok.Getter; + +@Getter +public enum NotificationMessage { + POST_REPLY_MESSAGE("'%s' 게시글에 새로운 댓글이 달렸습니다."), + COMMENT_REPLY_MESSAGE("'%s' 게시글에 새로운 대댓글이 달렸습니다."); + + private final String message; + + NotificationMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationCustomRepository.java b/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationCustomRepository.java new file mode 100644 index 000000000..1d85a148d --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationCustomRepository.java @@ -0,0 +1,12 @@ +package com.ssafy.ssafsound.domain.notification.repository; + +import com.ssafy.ssafsound.domain.notification.domain.Notification; + +import java.util.List; + +public interface NotificationCustomRepository { + + List findAllByOwnerAndReadTrue(Long ownerId, Long cursor, Integer size); + + Boolean existsNewNotification(Long ownerId); +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationRepository.java b/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationRepository.java new file mode 100644 index 000000000..c19b3bdd0 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationRepository.java @@ -0,0 +1,9 @@ +package com.ssafy.ssafsound.domain.notification.repository; + +import com.ssafy.ssafsound.domain.notification.domain.Notification; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationRepository extends MongoRepository, NotificationCustomRepository { +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationRepositoryImpl.java b/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationRepositoryImpl.java new file mode 100644 index 000000000..5b87a1cd7 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/repository/NotificationRepositoryImpl.java @@ -0,0 +1,49 @@ +package com.ssafy.ssafsound.domain.notification.repository; + +import com.ssafy.ssafsound.domain.notification.domain.Notification; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class NotificationRepositoryImpl implements NotificationCustomRepository { + private final MongoOperations mongoOperations; + + @Override + public List findAllByOwnerAndReadTrue(Long ownerId, Long cursor, Integer size) { + Criteria criteria = Criteria.where("ownerId").is(ownerId); + if (cursor != -1) { + criteria.and("_id").lt(cursor); + } + Query query = new Query(criteria) + .limit(size + 1) + .with(Sort.by(Sort.Order.desc("_id"))); + + List notifications = mongoOperations.find(query, Notification.class); + + mongoOperations.updateMulti( + new Query(Criteria.where("ownerId").is(ownerId)), + new Update().set("read", true), + Notification.class + ); + + return notifications; + } + + @Override + public Boolean existsNewNotification(Long ownerId) { + Query query = new Query(Criteria.where("ownerId").is(ownerId).and("read").is(false)); + return mongoOperations.exists(query, Notification.class); + } + + +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/service/NotificationService.java b/src/main/java/com/ssafy/ssafsound/domain/notification/service/NotificationService.java new file mode 100644 index 000000000..9dc9b8703 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/service/NotificationService.java @@ -0,0 +1,52 @@ +package com.ssafy.ssafsound.domain.notification.service; + +import com.ssafy.ssafsound.domain.auth.dto.AuthenticatedMember; +import com.ssafy.ssafsound.domain.member.exception.MemberErrorInfo; +import com.ssafy.ssafsound.domain.member.exception.MemberException; +import com.ssafy.ssafsound.domain.member.repository.MemberRepository; +import com.ssafy.ssafsound.domain.notification.domain.Notification; +import com.ssafy.ssafsound.domain.notification.dto.GetCheckNotificationResDto; +import com.ssafy.ssafsound.domain.notification.dto.GetNotificationReqDto; +import com.ssafy.ssafsound.domain.notification.dto.GetNotificationResDto; +import com.ssafy.ssafsound.domain.notification.event.NotificationEvent; +import com.ssafy.ssafsound.domain.notification.repository.NotificationRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class NotificationService { + private final NotificationRepository notificationRepository; + private final MemberRepository memberRepository; + + @Transactional + public GetNotificationResDto getNotifications(AuthenticatedMember authenticatedMember, GetNotificationReqDto getNotificationReqDto) { + if (!memberRepository.existsById(authenticatedMember.getMemberId())) { + throw new MemberException(MemberErrorInfo.MEMBER_NOT_FOUND_BY_ID); + } + + Long ownerId = authenticatedMember.getMemberId(); + Long cursor = getNotificationReqDto.getCursor(); + int size = getNotificationReqDto.getSize(); + List notifications = notificationRepository.findAllByOwnerAndReadTrue(ownerId, cursor, size); + return GetNotificationResDto.of(notifications, size); + } + + @Transactional + public void sendNotification(NotificationEvent notificationEvent) { + Notification notification = Notification.from(notificationEvent); + notificationRepository.save(notification); + } + + @Transactional(readOnly = true) + public GetCheckNotificationResDto checkNotification(AuthenticatedMember authenticatedMember) { + Boolean isNew = notificationRepository.existsNewNotification(authenticatedMember.getMemberId()); + + return new GetCheckNotificationResDto(isNew); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/notification/service/SequenceGeneratorService.java b/src/main/java/com/ssafy/ssafsound/domain/notification/service/SequenceGeneratorService.java new file mode 100644 index 000000000..268dac273 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/notification/service/SequenceGeneratorService.java @@ -0,0 +1,28 @@ +package com.ssafy.ssafsound.domain.notification.service; + +import com.ssafy.ssafsound.domain.notification.domain.AutoIncrementSequence; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class SequenceGeneratorService { + private final MongoOperations mongoOperations; + + public Long generateSequence(String sequenceName) { + AutoIncrementSequence autoIncrementSequence = mongoOperations.findAndModify( + new Query(Criteria.where("_id").is(sequenceName)), + new Update().inc("sequence", 1), + FindAndModifyOptions.options().returnNew(true).upsert(true), + AutoIncrementSequence.class); + + return !Objects.isNull(autoIncrementSequence) ? autoIncrementSequence.getSequence() : 1; + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/post/domain/Post.java b/src/main/java/com/ssafy/ssafsound/domain/post/domain/Post.java index 3ec254ad1..57bfb69d1 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/post/domain/Post.java +++ b/src/main/java/com/ssafy/ssafsound/domain/post/domain/Post.java @@ -4,6 +4,9 @@ import com.ssafy.ssafsound.domain.board.domain.Board; import com.ssafy.ssafsound.domain.comment.domain.Comment; import com.ssafy.ssafsound.domain.member.domain.Member; +import com.ssafy.ssafsound.domain.notification.domain.NotificationType; +import com.ssafy.ssafsound.domain.notification.domain.ServiceType; +import com.ssafy.ssafsound.domain.notification.event.NotificationEvent; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -89,4 +92,12 @@ public int countComment() { return (int) this.getComments().stream() .filter(comment -> !comment.getDeletedComment()).count(); } + + public Boolean isMine(Member member) { + return this.getMember().getId().equals(member.getId()); + } + + public Long getAuthorId(){ + return this.getMember().getId(); + } } diff --git a/src/main/java/com/ssafy/ssafsound/global/advice/ExceptionControllerAdvice.java b/src/main/java/com/ssafy/ssafsound/global/advice/ExceptionControllerAdvice.java index 790512684..01dc71d02 100644 --- a/src/main/java/com/ssafy/ssafsound/global/advice/ExceptionControllerAdvice.java +++ b/src/main/java/com/ssafy/ssafsound/global/advice/ExceptionControllerAdvice.java @@ -6,6 +6,7 @@ import com.ssafy.ssafsound.domain.comment.exception.CommentException; import com.ssafy.ssafsound.domain.lunch.exception.LunchException; import com.ssafy.ssafsound.domain.member.exception.MemberException; +import com.ssafy.ssafsound.domain.notification.exception.NotificationException; import com.ssafy.ssafsound.domain.post.exception.PostException; import com.ssafy.ssafsound.domain.recruit.exception.RecruitException; import com.ssafy.ssafsound.global.common.exception.GlobalErrorInfo; @@ -198,6 +199,17 @@ public EnvelopeResponse HttpMessageNotReadableException(HttpMessageNotReadableEx .build(); } + @ExceptionHandler(NotificationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public EnvelopeResponse HttpMessageNotReadableException(NotificationException e) { + log.error(e.getMessage()); + + return EnvelopeResponse.builder() + .code(e.getInfo().getCode()) + .message(e.getInfo().getMessage()) + .build(); + } + private void extractLog(RuntimeException e) { String lineNumber = String.valueOf(e.getStackTrace()[0].getLineNumber()); diff --git a/src/main/java/com/ssafy/ssafsound/global/config/MongoConfig.java b/src/main/java/com/ssafy/ssafsound/global/config/MongoConfig.java new file mode 100644 index 000000000..82e4adba8 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/global/config/MongoConfig.java @@ -0,0 +1,33 @@ +package com.ssafy.ssafsound.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.config.EnableMongoAuditing; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +@Configuration +@EnableMongoAuditing +public class MongoConfig { + + @Bean + public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory mongoDatabaseFactory, MongoMappingContext mongoMappingContext) { + mongoMappingContext.setAutoIndexCreation(true); + + DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory); + MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext); + converter.setTypeMapper(new DefaultMongoTypeMapper(null)); + return converter; + } + + @Bean + public MongoTransactionManager transactionManager(MongoDatabaseFactory mongoDatabaseFactory) { + return new MongoTransactionManager(mongoDatabaseFactory); + } + +} diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index 58082d128..af0160516 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -445,253 +445,165 @@

SSAF-SOUND API Documentation

Table of Contents
-
-

Overview

-

1. HTTP Method

-
-
-

RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP verbs.

-
- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
VerbUsage

GET

Used to retrieve a resource

POST

Used to create a new resource

PUT

PUT 설명

PATCH

Used to update an existing resource, including partial updates

DELETE

Used to delete an existing resource

-
-
-
-

2. HTTP status codes

-
-
-

RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP status codes.

-
- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Status codeUsage

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

-
-
-
-

3. 게시판

+

1. 게시판

-

3.1. 게시판 목록 조회

+

1.1. 게시판 목록 조회

-

3.1.1. HTTP request

+

1.1.1. HTTP request

GET /boards HTTP/1.1
@@ -701,7 +613,7 @@ 

-

3.1.2. HTTP response

+

1.1.2. HTTP response

HTTP/1.1 200 OK
@@ -734,7 +646,7 @@ 

- +

@@ -755,7 +667,7 @@

1.1.4. Response fields

@@ -822,12 +734,12 @@

-

4. 익명 게시판

+

2. 익명 게시판

@@ -905,7 +817,7 @@

2.1.4. Request parameters

@@ -930,7 +842,7 @@

-

4.1.5. Response fields

+

2.1.5. Response fields

@@ -1035,9 +947,9 @@

-

4.2. 게시글 목록 조회(Cursor)

+

2.2. 게시글 목록 조회(Cursor)

-

4.2.1. HTTP request

+

2.2.1. HTTP request

GET /posts/cursor?boardId=1&cursor=-1&size=10 HTTP/1.1
@@ -1047,7 +959,7 @@ 

-

4.2.2. HTTP response

+

2.2.2. HTTP response

HTTP/1.1 200 OK
@@ -1055,7 +967,7 @@ 

- +

@@ -1114,7 +1026,7 @@

2.2.4. Request parameters

@@ -1143,7 +1055,7 @@

@@ -1243,9 +1155,9 @@

-

4.3. 게시글 목록 조회(Offset)

+

2.3. 게시글 목록 조회(Offset)

-

4.3.1. HTTP request

+

2.3.1. HTTP request

GET /posts/offset?boardId=1&page=1&size=10 HTTP/1.1
@@ -1255,7 +1167,7 @@ 

-

4.3.2. HTTP response

+

2.3.2. HTTP response

HTTP/1.1 200 OK
@@ -1263,7 +1175,7 @@ 

- +

@@ -1323,7 +1235,7 @@

2.3.4. Request parameters

@@ -1352,7 +1264,7 @@

@@ -1457,9 +1369,9 @@

-

4.4. 게시글 상세 보기(실명)

+

2.4. 게시글 상세 보기(실명)

-

4.4.1. HTTP request

+

2.4.1. HTTP request

GET /posts/1 HTTP/1.1
@@ -1469,7 +1381,7 @@ 

-

4.4.2. HTTP response

+

2.4.2. HTTP response

HTTP/1.1 200 OK
@@ -1519,7 +1431,7 @@ 

- +

@@ -1540,7 +1452,7 @@

2.4.4. Path parameters

@@ -1562,7 +1474,7 @@

-

4.4.5. Response fields

+

2.4.5. Response fields

Table 1. /posts/{postId}
@@ -1737,9 +1649,9 @@

-

4.5. 게시글 상세 보기(익명)

+

2.5. 게시글 상세 보기(익명)

-

4.5.1. HTTP request

+

2.5.1. HTTP request

GET /posts/2 HTTP/1.1
@@ -1749,7 +1661,7 @@ 

-

4.5.2. HTTP response

+

2.5.2. HTTP response

HTTP/1.1 200 OK
@@ -1794,7 +1706,7 @@ 

- +

@@ -1815,7 +1727,7 @@

2.5.4. Path parameters

@@ -1837,7 +1749,7 @@

-

4.5.5. Response fields

+

2.5.5. Response fields

Table 1. /posts/{postId}
@@ -2012,9 +1924,9 @@

-

4.6. 게시글 검색(Cursor)

+

2.6. 게시글 검색(Cursor)

-

4.6.1. HTTP request

+

2.6.1. HTTP request

GET /posts/search/cursor?boardId=1&keyword=%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94&cursor=-1&size=10 HTTP/1.1
@@ -2024,7 +1936,7 @@ 

-

4.6.2. HTTP response

+

2.6.2. HTTP response

HTTP/1.1 200 OK
@@ -2032,7 +1944,7 @@ 

- +

@@ -2079,7 +1991,7 @@

2.6.4. Request parameters

@@ -2112,7 +2024,7 @@

-

4.6.5. Response fields

+

2.6.5. Response fields

@@ -2212,9 +2124,9 @@

-

4.7. 게시글 검색(Offset)

+

2.7. 게시글 검색(Offset)

-

4.7.1. HTTP request

+

2.7.1. HTTP request

GET /posts/search/offset?boardId=1&keyword=%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94&page=1&size=10 HTTP/1.1
@@ -2224,7 +2136,7 @@ 

-

4.7.2. HTTP response

+

2.7.2. HTTP response

HTTP/1.1 200 OK
@@ -2232,7 +2144,7 @@ 

- +

@@ -2280,7 +2192,7 @@

2.7.4. Request parameters

@@ -2313,7 +2225,7 @@

-

4.7.5. Response fields

+

2.7.5. Response fields

@@ -2418,9 +2330,9 @@

-

4.8. 게시글 작성

+

2.8. 게시글 작성

-

4.8.1. HTTP request

+

2.8.1. HTTP request

POST /posts?boardId=1 HTTP/1.1
@@ -2440,7 +2352,7 @@ 

-

4.8.2. HTTP response

+

2.8.2. HTTP response

HTTP/1.1 200 OK
@@ -2461,7 +2373,7 @@ 

- +

@@ -2482,7 +2394,7 @@

2.8.4. Request parameters

@@ -2503,7 +2415,7 @@

-

4.8.5. Request fields

+

2.8.5. Request fields

@@ -2552,7 +2464,7 @@

-

4.8.6. Response fields

+

2.8.6. Response fields

@@ -2592,9 +2504,9 @@

-

4.9. 게시글 수정

+

2.9. 게시글 수정

-

4.9.1. HTTP request

+

2.9.1. HTTP request

PATCH /posts/1 HTTP/1.1
@@ -2614,7 +2526,7 @@ 

-

4.9.2. HTTP response

+

2.9.2. HTTP response

HTTP/1.1 200 OK
@@ -2635,7 +2547,7 @@ 

- +

@@ -2656,7 +2568,7 @@

2.9.4. Path parameters

@@ -2678,7 +2590,7 @@

-

4.9.5. Request fields

+

2.9.5. Request fields

Table 1. /posts/{postId}
@@ -2727,7 +2639,7 @@

-

4.9.6. Response fields

+

2.9.6. Response fields

@@ -2767,9 +2679,9 @@

-

4.10. 게시글 삭제

+

2.10. 게시글 삭제

-

4.10.1. HTTP request

+

2.10.1. HTTP request

DELETE /posts/1 HTTP/1.1
@@ -2779,7 +2691,7 @@ 

-

4.10.2. HTTP response

+

2.10.2. HTTP response

HTTP/1.1 200 OK
@@ -2800,7 +2712,7 @@ 

- +

@@ -2821,7 +2733,7 @@

2.10.4. Path parameters

@@ -2843,7 +2755,7 @@

-

4.10.5. Response fields

+

2.10.5. Response fields

Table 1. /posts/{postId}
@@ -2883,9 +2795,9 @@

-

4.11. 게시글 좋아요

+

2.11. 게시글 좋아요

-

4.11.1. HTTP request

+

2.11.1. HTTP request

POST /posts/1/like HTTP/1.1
@@ -2895,7 +2807,7 @@ 

-

4.11.2. HTTP response

+

2.11.2. HTTP response

HTTP/1.1 200 OK
@@ -2917,7 +2829,7 @@ 

- +

@@ -2938,7 +2850,7 @@

2.11.4. Path parameters

@@ -2960,7 +2872,7 @@

-

4.11.5. Response fields

+

2.11.5. Response fields

Table 1. /posts/{postId}/like
@@ -3005,9 +2917,9 @@

-

4.12. 게시글 스크랩

+

2.12. 게시글 스크랩

-

4.12.1. HTTP request

+

2.12.1. HTTP request

POST /posts/1/scrap HTTP/1.1
@@ -3017,7 +2929,7 @@ 

-

4.12.2. HTTP response

+

2.12.2. HTTP response

HTTP/1.1 200 OK
@@ -3039,7 +2951,7 @@ 

- +

@@ -3060,7 +2972,7 @@

2.12.4. Path parameters

@@ -3082,7 +2994,7 @@

-

4.12.5. Response fields

+

2.12.5. Response fields

Table 1. /posts/{postId}/scrap
@@ -3127,9 +3039,9 @@

-

4.13. Hot 게시글 목록 조회(Cursor)

+

2.13. Hot 게시글 목록 조회(Cursor)

-

4.13.1. HTTP request

+

2.13.1. HTTP request

GET /posts/hot/cursor?cursor=-1&size=10 HTTP/1.1
@@ -3139,7 +3051,7 @@ 

-

4.13.2. HTTP response

+

2.13.2. HTTP response

HTTP/1.1 200 OK
@@ -3147,7 +3059,7 @@ 

- +

@@ -3206,7 +3118,7 @@

2.13.4. Request parameters

@@ -3231,7 +3143,7 @@

@@ -3331,9 +3243,9 @@

@@ -3411,7 +3323,7 @@

2.14.4. Request parameters

@@ -3436,7 +3348,7 @@

@@ -3541,9 +3453,9 @@

-

4.15. Hot 게시글 검색(Cursor)

+

2.15. Hot 게시글 검색(Cursor)

-

4.15.1. HTTP request

+

2.15.1. HTTP request

GET /posts/hot/search/cursor?keyword=%EC%B7%A8%EC%97%85&cursor=-1&size=10 HTTP/1.1
@@ -3553,7 +3465,7 @@ 

-

4.15.2. HTTP response

+

2.15.2. HTTP response

HTTP/1.1 200 OK
@@ -3561,7 +3473,7 @@ 

- +

@@ -3608,7 +3520,7 @@

2.15.4. Request parameters

@@ -3637,7 +3549,7 @@

-

4.15.5. Response fields

+

2.15.5. Response fields

@@ -3737,9 +3649,9 @@

-

4.16. Hot 게시글 검색(Offset)

+

2.16. Hot 게시글 검색(Offset)

-

4.16.1. HTTP request

+

2.16.1. HTTP request

GET /posts/hot/search/offset?keyword=%EC%B7%A8%EC%97%85&page=1&size=10 HTTP/1.1
@@ -3749,7 +3661,7 @@ 

-

4.16.2. HTTP response

+

2.16.2. HTTP response

HTTP/1.1 200 OK
@@ -3757,7 +3669,7 @@ 

- +

@@ -3805,7 +3717,7 @@

2.16.4. Request parameters

@@ -3834,7 +3746,7 @@

-

4.16.5. Response fields

+

2.16.5. Response fields

@@ -3939,9 +3851,9 @@

-

4.17. 나의 게시글 목록 조회(Cursor)

+

2.17. 나의 게시글 목록 조회(Cursor)

-

4.17.1. HTTP request

+

2.17.1. HTTP request

GET /posts/my/cursor?cursor=-1&size=10 HTTP/1.1
@@ -3951,7 +3863,7 @@ 

@@ -4018,7 +3930,7 @@

2.17.4. Request parameters

@@ -4043,7 +3955,7 @@

-

4.17.5. Response fields

+

2.17.5. Response fields

@@ -4143,9 +4055,9 @@

@@ -4223,7 +4135,7 @@

2.18.4. Request parameters

@@ -4248,7 +4160,7 @@

-

4.18.5. Response fields

+

2.18.5. Response fields

@@ -4353,9 +4265,9 @@

@@ -4432,7 +4344,7 @@
@@ -4457,7 +4369,7 @@

@@ -4557,9 +4469,9 @@

@@ -4637,7 +4549,7 @@
@@ -4662,7 +4574,7 @@

@@ -4769,12 +4681,12 @@

@@ -4868,7 +4780,7 @@

3.1.4. Request parameters

@@ -4889,7 +4801,7 @@

-

5.1.5. Response fields

+

3.1.5. Response fields

@@ -5134,9 +5046,9 @@

-

5.2. 댓글 쓰기

+

3.2. 댓글 쓰기

-

5.2.1. HTTP request

+

3.2.1. HTTP request

POST /comments?postId=1 HTTP/1.1
@@ -5154,7 +5066,7 @@ 

-

5.2.2. HTTP response

+

3.2.2. HTTP response

HTTP/1.1 200 OK
@@ -5175,7 +5087,7 @@ 

- +

@@ -5196,7 +5108,7 @@
-

5.2.4. Request parameters

+

3.2.4. Request parameters

@@ -5217,7 +5129,7 @@

-

5.2.5. Request fields

+

3.2.5. Request fields

@@ -5246,7 +5158,7 @@

-

5.2.6. Response fields

+

3.2.6. Response fields

@@ -5286,9 +5198,9 @@

-

5.3. 대댓글 쓰기

+

3.3. 대댓글 쓰기

-

5.3.1. HTTP request

+

3.3.1. HTTP request

POST /comments/reply?commentId=1&postId=1 HTTP/1.1
@@ -5306,7 +5218,7 @@ 

-

5.3.2. HTTP response

+

3.3.2. HTTP response

HTTP/1.1 200 OK
@@ -5327,7 +5239,7 @@ 

- +

@@ -5348,7 +5260,7 @@

3.3.4. Request parameters

@@ -5373,7 +5285,7 @@

-

5.3.5. Request fields

+

3.3.5. Request fields

@@ -5402,7 +5314,7 @@

-

5.3.6. Response fields

+

3.3.6. Response fields

@@ -5442,9 +5354,9 @@

-

5.4. 댓글 수정

+

3.4. 댓글 수정

-

5.4.1. HTTP request

+

3.4.1. HTTP request

PATCH /comments/1 HTTP/1.1
@@ -5462,7 +5374,7 @@ 

-

5.4.2. HTTP response

+

3.4.2. HTTP response

HTTP/1.1 200 OK
@@ -5483,7 +5395,7 @@ 

- +

@@ -5504,7 +5416,7 @@
-

5.4.4. Path parameters

+

3.4.4. Path parameters

@@ -5526,7 +5438,7 @@

-

5.4.5. Request fields

+

3.4.5. Request fields

Table 1. /comments/{commentId}
@@ -5555,7 +5467,7 @@

-

5.4.6. Response fields

+

3.4.6. Response fields

@@ -5595,9 +5507,9 @@

-

5.5. 댓글 삭제

+

3.5. 댓글 삭제

-

5.5.1. HTTP request

+

3.5.1. HTTP request

DELETE /comments/1 HTTP/1.1
@@ -5607,7 +5519,7 @@ 

-

5.5.2. HTTP response

+

3.5.2. HTTP response

HTTP/1.1 200 OK
@@ -5628,7 +5540,7 @@ 

- +

@@ -5649,7 +5561,7 @@
-

5.5.4. Path parameters

+

3.5.4. Path parameters

@@ -5671,7 +5583,7 @@

-

5.5.5. Response fields

+

3.5.5. Response fields

Table 1. /comments/{commentId}
@@ -5711,9 +5623,9 @@

-

5.6. 댓글 좋아요

+

3.6. 댓글 좋아요

-

5.6.1. HTTP request

+

3.6.1. HTTP request

POST /comments/1/like HTTP/1.1
@@ -5723,7 +5635,7 @@ 

-

5.6.2. HTTP response

+

3.6.2. HTTP response

HTTP/1.1 200 OK
@@ -5745,7 +5657,7 @@ 

- +

@@ -5766,7 +5678,7 @@

3.6.4. Path parameters

@@ -5788,7 +5700,7 @@

-

5.6.5. Response fields

+

3.6.5. Response fields

Table 1. /comments/{commentId}/like
@@ -5835,12 +5747,12 @@

-

6. 메타데이터

+

4. 메타데이터

@@ -5930,9 +5842,9 @@

-

6.2. 스킬 목록 조회

+

4.2. 스킬 목록 조회

-

6.2.1. HTTP request

+

4.2.1. HTTP request

GET /meta/skills HTTP/1.1
@@ -5941,7 +5853,7 @@ 

-

6.2.2. HTTP response

+

4.2.2. HTTP response

HTTP/1.1 200 OK
@@ -6016,7 +5928,7 @@ 

-

6.2.3. Response fields

+

4.2.3. Response fields

@@ -6061,9 +5973,9 @@

-

6.3. 리크루트 목록 조회

+

4.3. 리크루트 목록 조회

-

6.3.1. HTTP request

+

4.3.1. HTTP request

GET /meta/recruit-types HTTP/1.1
@@ -6072,7 +5984,7 @@ 

-

6.3.2. HTTP response

+

4.3.2. HTTP response

HTTP/1.1 200 OK
@@ -6108,7 +6020,7 @@ 

-

6.3.3. Response fields

+

4.3.3. Response fields

@@ -6155,12 +6067,12 @@

-

7. 인증

+

5. 인증

@@ -6182,7 +6094,7 @@

-

7.1.2. HTTP request

+

5.1.2. HTTP request

GET /auth/kakao HTTP/1.1
@@ -6191,7 +6103,7 @@ 

Table 1. /auth/{oauthName}
@@ -6266,15 +6178,15 @@

-

7.2.3. HTTP response

+

5.2.3. HTTP response

HTTP/1.1 200 OK
 Vary: Origin
 Vary: Access-Control-Request-Method
 Vary: Access-Control-Request-Headers
-Set-Cookie: accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Path=/; Domain=ssafsound.com; Max-Age=2629744; Expires=Sat, 2 Dec 2023 20:51:14 GMT; Secure; HttpOnly
-Set-Cookie: refreshToken=syJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IsdfserwetweSflKxwRJeSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Path=/auth/reissue; Domain=ssafsound.com; Max-Age=2629744; Expires=Sat, 2 Dec 2023 20:51:14 GMT; Secure; HttpOnly
+Set-Cookie: accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Path=/; Domain=ssafsound.com; Max-Age=2629744; Expires=Sun, 10 Dec 2023 19:01:16 GMT; Secure; HttpOnly
+Set-Cookie: refreshToken=syJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IsdfserwetweSflKxwRJeSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Path=/auth/reissue; Domain=ssafsound.com; Max-Age=2629744; Expires=Sun, 10 Dec 2023 19:01:16 GMT; Secure; HttpOnly
 Content-Type: application/json
 Content-Length: 422
 
@@ -6291,9 +6203,9 @@ 

-

7.3. 로그아웃시(accessToken 또는 refreshToken이 존재하는 경우)

+

5.3. 로그아웃시(accessToken 또는 refreshToken이 존재하는 경우)

-

7.3.1. HTTP request

+

5.3.1. HTTP request

DELETE /auth/logout HTTP/1.1
@@ -6304,7 +6216,7 @@ 

-

7.3.2. HTTP response

+

5.3.2. HTTP response

HTTP/1.1 200 OK
@@ -6325,7 +6237,7 @@ 

-

7.3.3. Response fields

+

5.3.3. Response fields

@@ -6360,9 +6272,9 @@

-

7.4. 로그아웃시(토큰 존재하지 않을 경우)

+

5.4. 로그아웃시(토큰 존재하지 않을 경우)

-

7.4.1. HTTP request

+

5.4.1. HTTP request

DELETE /auth/logout HTTP/1.1
@@ -6371,7 +6283,7 @@ 

@@ -6427,9 +6339,9 @@

@@ -6478,7 +6390,7 @@

-

7.5.3. HTTP response

+

5.5.3. HTTP response

@@ -6529,7 +6441,7 @@

-

8.1.2. HTTP request

+

6.1.2. HTTP request

GET /members HTTP/1.1
@@ -6539,7 +6451,7 @@ 

-

8.1.3. HTTP response

+

6.1.3. HTTP response

HTTP/1.1 200 OK
@@ -6565,7 +6477,7 @@ 

-

8.1.4. Response fields

+

6.1.4. Response fields

@@ -6630,9 +6542,9 @@

-

8.2. 일반 멤버 정보 조회

+

6.2. 일반 멤버 정보 조회

@@ -6653,7 +6565,7 @@

6.2.2. HTTP request

GET /members HTTP/1.1
@@ -6663,7 +6575,7 @@ 

-

8.2.3. HTTP response

+

6.2.3. HTTP response

HTTP/1.1 200 OK
@@ -6689,7 +6601,7 @@ 

-

8.2.4. Response fields

+

6.2.4. Response fields

@@ -6754,9 +6666,9 @@

-

8.3. 회원 탈퇴

+

6.3. 회원 탈퇴

@@ -6777,7 +6689,7 @@
-

8.3.2. HTTP request

+

6.3.2. HTTP request

DELETE /members HTTP/1.1
@@ -6787,7 +6699,7 @@ 

-

8.3.3. HTTP response

+

6.3.3. HTTP response

HTTP/1.1 200 OK
@@ -6808,7 +6720,7 @@ 

-

8.3.4. Response fields

+

6.3.4. Response fields

@@ -6843,9 +6755,9 @@

-

8.4. 싸피생 멤버 정보 조회

+

6.4. 싸피생 멤버 정보 조회

@@ -6866,7 +6778,7 @@

6.4.2. HTTP request

GET /members HTTP/1.1
@@ -6876,7 +6788,7 @@ 

-

8.4.3. HTTP response

+

6.4.3. HTTP response

HTTP/1.1 200 OK
@@ -6907,7 +6819,7 @@ 

-

8.4.4. Response fields

+

6.4.4. Response fields

@@ -6992,9 +6904,9 @@

-

8.5. 회원가입 시 일반 멤버 정보 입력

+

6.5. 회원가입 시 일반 멤버 정보 입력

@@ -7015,7 +6927,7 @@
@@ -7101,7 +7013,7 @@

-

8.5.5. HTTP response

+

6.5.5. HTTP response

HTTP/1.1 200 OK
@@ -7127,7 +7039,7 @@ 

-

8.5.6. Response fields

+

6.5.6. Response fields

@@ -7212,9 +7124,9 @@

@@ -7235,7 +7147,7 @@
@@ -7321,7 +7233,7 @@

-

8.6.5. HTTP response

+

6.6.5. HTTP response

HTTP/1.1 200 OK
@@ -7352,7 +7264,7 @@ 

-

8.6.6. Response fields

+

6.6.6. Response fields

@@ -7437,9 +7349,9 @@

@@ -7460,7 +7372,7 @@

6.7.2. HTTP request

POST /members/ssafy-certification HTTP/1.1
@@ -7479,7 +7391,7 @@ 

-

8.7.3. Request fields

+

6.7.3. Request fields

@@ -7513,7 +7425,7 @@

-

8.7.4. HTTP response

+

6.7.4. HTTP response

HTTP/1.1 200 OK
@@ -7535,7 +7447,7 @@ 

-

8.7.5. Response fields

+

6.7.5. Response fields

@@ -7580,9 +7492,9 @@

-

8.8. 나의 포트폴리오 조회

+

6.8. 나의 포트폴리오 조회

@@ -7603,7 +7515,7 @@

6.8.2. HTTP request

GET /members/portfolio HTTP/1.1
@@ -7613,7 +7525,7 @@ 

-

8.8.3. HTTP response

+

6.8.3. HTTP response

HTTP/1.1 200 OK
@@ -7644,7 +7556,7 @@ 

-

8.8.4. Response fields

+

6.8.4. Response fields

@@ -7704,9 +7616,9 @@

-

8.9. 다른 멤버 포트폴리오 조회

+

6.9. 다른 멤버 포트폴리오 조회

@@ -7727,7 +7639,7 @@

6.9.2. HTTP request

GET /members/99/portfolio HTTP/1.1
@@ -7736,7 +7648,7 @@ 

@@ -7758,7 +7670,7 @@

Table 1. /members/{memberId}/portfolio
@@ -7849,9 +7761,9 @@

@@ -7872,7 +7784,7 @@

6.10.2. HTTP request

PUT /members/portfolio HTTP/1.1
@@ -7897,7 +7809,7 @@ 

-

8.10.3. Request fields

+

6.10.3. Request fields

@@ -7941,7 +7853,7 @@

-

8.10.4. HTTP response

+

6.10.4. HTTP response

HTTP/1.1 200 OK
@@ -7960,7 +7872,7 @@ 

-

8.10.5. Response fields

+

6.10.5. Response fields

@@ -7995,9 +7907,9 @@

-

8.11. 멤버의 기본 정보 조회

+

6.11. 멤버의 기본 정보 조회

@@ -8018,7 +7930,7 @@

6.11.2. HTTP request

GET /members/99/default-information HTTP/1.1
@@ -8027,7 +7939,7 @@ 

-

8.11.3. Path parameters

+

6.11.3. Path parameters

@@ -8049,7 +7961,7 @@

-

8.11.4. HTTP response

+

6.11.4. HTTP response

HTTP/1.1 200 OK
@@ -8078,7 +7990,7 @@ 

-

8.11.5. Response fields

+

6.11.5. Response fields

Table 1. /members/{memberId}/default-information
@@ -8153,9 +8065,9 @@

-

8.12. 멤버의 기본 정보 수정

+

6.12. 멤버의 기본 정보 수정

@@ -8176,7 +8088,7 @@

6.12.2. HTTP request

PATCH /members/default-information HTTP/1.1
@@ -8195,7 +8107,7 @@ 

-

8.12.3. Request fields

+

6.12.3. Request fields

@@ -8229,7 +8141,7 @@

-

8.12.4. HTTP response

+

6.12.4. HTTP response

HTTP/1.1 200 OK
@@ -8248,7 +8160,7 @@ 

-

8.12.5. Response fields

+

6.12.5. Response fields

@@ -8283,9 +8195,9 @@

-

8.13. 나의 프로필 공개 여부 조회

+

6.13. 나의 프로필 공개 여부 조회

@@ -8306,7 +8218,7 @@

6.13.2. HTTP request

GET /members/public-profile HTTP/1.1
@@ -8316,7 +8228,7 @@ 

@@ -8377,9 +8289,9 @@

@@ -8400,7 +8312,7 @@
@@ -8432,7 +8344,7 @@

Table 1. /members/{memberId}/public-profile
@@ -8493,9 +8405,9 @@

@@ -8516,7 +8428,7 @@

6.15.2. HTTP request

PATCH /members/public-profile HTTP/1.1
@@ -8533,7 +8445,7 @@ 

-

8.15.3. Request fields

+

6.15.3. Request fields

@@ -8557,7 +8469,7 @@

-

8.15.4. HTTP response

+

6.15.4. HTTP response

HTTP/1.1 200 OK
@@ -8576,7 +8488,7 @@ 

-

8.15.5. Response fields

+

6.15.5. Response fields

@@ -8611,9 +8523,9 @@

-

8.16. 닉네임 중복 검사

+

6.16. 닉네임 중복 검사

@@ -8634,7 +8546,7 @@

6.16.2. HTTP request

POST /members/nickname HTTP/1.1
@@ -8650,7 +8562,7 @@ 

-

8.16.3. Request fields

+

6.16.3. Request fields

@@ -8674,7 +8586,7 @@

-

8.16.4. HTTP response

+

6.16.4. HTTP response

HTTP/1.1 200 OK
@@ -8695,7 +8607,7 @@ 

-

8.16.5. Response fields

+

6.16.5. Response fields

@@ -8735,9 +8647,9 @@

-

8.17. 닉네임 변경

+

6.17. 닉네임 변경

@@ -8758,7 +8670,7 @@

6.17.2. HTTP request

PATCH /members/nickname HTTP/1.1
@@ -8775,7 +8687,7 @@ 

-

8.17.3. Request fields

+

6.17.3. Request fields

@@ -8799,7 +8711,7 @@

-

8.17.4. HTTP response

+

6.17.4. HTTP response

HTTP/1.1 200 OK
@@ -8818,7 +8730,7 @@ 

-

8.17.5. Response fields

+

6.17.5. Response fields

@@ -8853,9 +8765,9 @@

-

8.18. 멤버의 전공자 여부 수정

+

6.18. 멤버의 전공자 여부 수정

@@ -8876,7 +8788,7 @@

6.18.2. HTTP request

PATCH /members/major HTTP/1.1
@@ -8893,7 +8805,7 @@ 

-

8.18.3. Request fields

+

6.18.3. Request fields

@@ -8917,7 +8829,7 @@

@@ -8971,9 +8883,9 @@

@@ -8994,7 +8906,7 @@

6.19.2. HTTP request

PATCH /members/major-track HTTP/1.1
@@ -9011,7 +8923,7 @@ 

-

8.19.3. Request fields

+

6.19.3. Request fields

@@ -9035,7 +8947,7 @@

-

8.19.4. HTTP response

+

6.19.4. HTTP response

HTTP/1.1 200 OK
@@ -9054,7 +8966,7 @@ 

-

8.19.5. Response fields

+

6.19.5. Response fields

@@ -9091,12 +9003,12 @@

-

9. 약관

+

7. 약관

@@ -9117,7 +9029,7 @@

7.1.2. Response fields

@@ -9176,7 +9088,7 @@

-

9.1.3. HTTP response

+

7.1.3. HTTP response

HTTP/1.1 200 OK
@@ -9218,12 +9130,12 @@ 

-

10. 점심

+

8. 점심

-

10.1. 점심 목록 조회

+

8.1. 점심 목록 조회

-

10.1.1. HTTP request

+

8.1.1. HTTP request

GET /lunch?campus=%EC%84%9C%EC%9A%B8&date=2023-08-12 HTTP/1.1
@@ -9233,7 +9145,7 @@ 

- +

@@ -9254,7 +9166,7 @@

8.1.3. Request parameters

@@ -9279,7 +9191,7 @@

-

10.1.4. HTTP response

+

8.1.4. HTTP response

HTTP/1.1 200 OK
@@ -9323,7 +9235,7 @@ 

-

10.1.5. Response fields

+

8.1.5. Response fields

@@ -9403,9 +9315,9 @@

-

10.2. 점심 투표하기

+

8.2. 점심 투표하기

-

10.2.1. HTTP request

+

8.2.1. HTTP request

POST /lunch/poll/2 HTTP/1.1
@@ -9415,7 +9327,7 @@ 

- +

@@ -9436,7 +9348,7 @@

8.2.3. Path parameters

@@ -9458,7 +9370,7 @@

-

10.2.4. HTTP response

+

8.2.4. HTTP response

HTTP/1.1 200 OK
@@ -9479,7 +9391,7 @@ 

-

10.2.5. Response fields

+

8.2.5. Response fields

Table 1. /lunch/poll/{lunchId}
@@ -9519,9 +9431,9 @@

-

10.3. 점심 투표 취소하기

+

8.3. 점심 투표 취소하기

-

10.3.1. HTTP request

+

8.3.1. HTTP request

POST /lunch/poll/revert/2 HTTP/1.1
@@ -9531,7 +9443,7 @@ 

- +

@@ -9552,7 +9464,7 @@

8.3.3. Path parameters

@@ -9574,7 +9486,7 @@

-

10.3.4. HTTP response

+

8.3.4. HTTP response

HTTP/1.1 200 OK
@@ -9595,7 +9507,7 @@ 

-

10.3.5. Response fields

+

8.3.5. Response fields

Table 1. /lunch/poll/revert/{lunchId}
@@ -9637,12 +9549,12 @@

-

11. 리크루트

+

9. 리크루트

@@ -9745,7 +9657,7 @@

- +

@@ -9766,7 +9678,7 @@

9.1.4. HTTP response

HTTP/1.1 200 OK
@@ -9787,7 +9699,7 @@ 

-

11.1.5. Response fields

+

9.1.5. Response fields

@@ -9827,9 +9739,9 @@

-

11.2. 리크루트 스크랩 토글

+

9.2. 리크루트 스크랩 토글

-

11.2.1. HTTP request

+

9.2.1. HTTP request

POST /recruits/1/scrap HTTP/1.1
@@ -9839,7 +9751,7 @@ 

- +

@@ -9860,7 +9772,7 @@

9.2.3. Path parameters

@@ -9882,7 +9794,7 @@

-

11.2.4. HTTP response

+

9.2.4. HTTP response

HTTP/1.1 200 OK
@@ -9904,7 +9816,7 @@ 

-

11.2.5. Response fields

+

9.2.5. Response fields

Table 1. /recruits/{recruitId}/scrap
@@ -9949,9 +9861,9 @@

-

11.3. 리크루트 완료

+

9.3. 리크루트 완료

-

11.3.1. HTTP request

+

9.3.1. HTTP request

POST /recruits/1/expired HTTP/1.1
@@ -9961,7 +9873,7 @@ 

- +

@@ -9982,7 +9894,7 @@

9.3.3. Path parameters

@@ -10004,7 +9916,7 @@

-

11.3.4. HTTP response

+

9.3.4. HTTP response

HTTP/1.1 200 OK
@@ -10023,7 +9935,7 @@ 

-

11.3.5. Response fields

+

9.3.5. Response fields

Table 1. /recruits/{recruitId}/expired
@@ -10058,9 +9970,9 @@

-

11.4. 리크루트 상세 조회

+

9.4. 리크루트 상세 조회

-

11.4.1. HTTP request

+

9.4.1. HTTP request

GET /recruits/1 HTTP/1.1
@@ -10069,7 +9981,7 @@ 

- +

@@ -10090,7 +10002,7 @@

9.4.3. Path parameters

@@ -10112,7 +10024,7 @@

-

11.4.4. HTTP response

+

9.4.4. HTTP response

HTTP/1.1 200 OK
@@ -10133,8 +10045,8 @@ 

-

11.4.5. Response fields

+

9.4.5. Response fields

Table 1. /recruits/{recruitId}
@@ -10355,9 +10267,9 @@

-

11.5. 리크루트 업데이트

+

9.5. 리크루트 업데이트

-

11.5.1. HTTP request

+

9.5.1. HTTP request

PATCH /recruits/1 HTTP/1.1
@@ -10387,7 +10299,7 @@ 

- +

@@ -10408,7 +10320,7 @@

9.5.3. Path parameters

@@ -10430,7 +10342,7 @@

-

11.5.4. Request fields

+

9.5.4. Request fields

Table 1. /recruits/{recruitId}
@@ -10494,7 +10406,7 @@

-

11.5.5. HTTP response

+

9.5.5. HTTP response

HTTP/1.1 200 OK
@@ -10513,7 +10425,7 @@ 

-

11.5.6. Response fields

+

9.5.6. Response fields

@@ -10548,9 +10460,9 @@

-

11.6. 리크루트 삭제

+

9.6. 리크루트 삭제

-

11.6.1. HTTP request

+

9.6.1. HTTP request

DELETE /recruits/1 HTTP/1.1
@@ -10560,7 +10472,7 @@ 

- +

@@ -10581,7 +10493,7 @@

9.6.3. Path parameters

@@ -10603,7 +10515,7 @@

-

11.6.4. HTTP response

+

9.6.4. HTTP response

HTTP/1.1 200 OK
@@ -10622,7 +10534,7 @@ 

-

11.6.5. Response fields

+

9.6.5. Response fields

Table 1. /recruits/{recruitId}
@@ -10657,9 +10569,9 @@

-

11.7. 리크루트 목록 조회(Cursor)

+

9.7. 리크루트 목록 조회(Cursor)

-

11.7.1. HTTP request

+

9.7.1. HTTP request

GET /recruits/cursor?size=20&category=project&keyword=%EC%82%AC%EC%9D%B4%EB%93%9C&isFinished=false&recruitTypes=%EB%B0%B1%EC%97%94%EB%93%9C&recruitTypes=%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C&skills=Spring&skills=React HTTP/1.1
@@ -10669,7 +10581,7 @@ 

-

11.7.2. Request parameters

+

9.7.2. Request parameters

@@ -10714,7 +10626,7 @@

@@ -10879,9 +10791,9 @@

-

11.8. 리크루트 목록 조회(Offset)

+

9.8. 리크루트 목록 조회(Offset)

-

11.8.1. HTTP request

+

9.8.1. HTTP request

GET /recruits/offset?size=20&category=project&keyword=%EC%82%AC%EC%9D%B4%EB%93%9C&isFinished=false&recruitTypes=%EB%B0%B1%EC%97%94%EB%93%9C&recruitTypes=%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C&skills=Spring&skills=React HTTP/1.1
@@ -10891,7 +10803,7 @@ 

-

11.8.2. Request parameters

+

9.8.2. Request parameters

@@ -10936,7 +10848,7 @@

@@ -11101,9 +11013,9 @@

@@ -11134,7 +11046,7 @@

9.9.3. Request parameters

@@ -11167,7 +11079,7 @@

<

-

11.9.4. HTTP response

+

9.9.4. HTTP response

HTTP/1.1 200 OK
@@ -11186,7 +11098,7 @@ 

-

11.10.4. HTTP response

+

9.10.4. HTTP response

HTTP/1.1 200 OK
@@ -11429,7 +11341,7 @@ 

-

11.10.5. Response fields

+

9.10.5. Response fields

@@ -11587,9 +11499,9 @@

@@ -11620,7 +11532,7 @@

9.11.3. Request parameters

@@ -11653,7 +11565,7 @@

-

11.11.4. HTTP response

+

9.11.4. HTTP response

HTTP/1.1 200 OK
@@ -11672,7 +11584,7 @@ 

@@ -11818,9 +11730,9 @@

<

@@ -11851,7 +11763,7 @@

9.12.3. Request parameters

@@ -11884,7 +11796,7 @@

-

11.12.4. HTTP response

+

9.12.4. HTTP response

HTTP/1.1 200 OK
@@ -11903,7 +11815,7 @@ 

@@ -12049,9 +11961,9 @@

<

@@ -12082,7 +11994,7 @@

9.13.3. Request parameters

@@ -12107,7 +12019,7 @@

-

11.13.4. HTTP response

+

9.13.4. HTTP response

HTTP/1.1 200 OK
@@ -12126,7 +12038,7 @@ 

@@ -12272,9 +12184,9 @@

<

@@ -12305,7 +12217,7 @@

9.14.3. Request parameters

@@ -12330,7 +12242,7 @@

-

11.14.4. HTTP response

+

9.14.4. HTTP response

HTTP/1.1 200 OK
@@ -12349,7 +12261,7 @@ 

@@ -12497,12 +12409,12 @@

<

@@ -12541,7 +12453,7 @@

10.1.3. Path parameters

@@ -12563,7 +12475,7 @@

-

12.1.4. Request fields

+

10.1.4. Request fields

Table 1. /recruits/{recruitId}/application
@@ -12592,7 +12504,7 @@

-

12.1.5. HTTP response

+

10.1.5. HTTP response

HTTP/1.1 200 OK
@@ -12614,7 +12526,7 @@ 

-

12.1.6. Response fields

+

10.1.6. Response fields

@@ -12659,9 +12571,9 @@

-

12.2. 리크루트 등록자 참여 신청 수락

+

10.2. 리크루트 등록자 참여 신청 수락

-

12.2.1. HTTP request

+

10.2.1. HTTP request

PATCH /recruit-applications/1/approve HTTP/1.1
@@ -12671,7 +12583,7 @@ 

- +

@@ -12692,7 +12604,7 @@
@@ -12714,7 +12626,7 @@

-

12.2.4. HTTP response

+

10.2.4. HTTP response

HTTP/1.1 200 OK
@@ -12736,7 +12648,7 @@ 

-

12.2.5. Response fields

+

10.2.5. Response fields

Table 1. /recruit-applications/{recruitApplicationId}/approve
@@ -12781,9 +12693,9 @@

-

12.3. 리크루트 참여 신청 거절

+

10.3. 리크루트 참여 신청 거절

-

12.3.1. HTTP request

+

10.3.1. HTTP request

PATCH /recruit-applications/1/reject HTTP/1.1
@@ -12793,7 +12705,7 @@ 

- +

@@ -12814,7 +12726,7 @@

10.3.3. Path parameters

@@ -12836,7 +12748,7 @@

Table 1. /recruit-applications/{recruitApplicationId}/reject
@@ -12903,9 +12815,9 @@

@@ -12936,7 +12848,7 @@
@@ -12958,7 +12870,7 @@

-

12.4.4. HTTP response

+

10.4.4. HTTP response

HTTP/1.1 200 OK
@@ -12980,7 +12892,7 @@ 

-

12.4.5. Response fields

+

10.4.5. Response fields

Table 1. /recruit-applications/{recruitApplicationId}/cancel
@@ -13025,9 +12937,9 @@

-

12.5. 리크루트 참여자 목록 조회

+

10.5. 리크루트 참여자 목록 조회

-

12.5.1. HTTP request

+

10.5.1. HTTP request

GET /recruits/1/members HTTP/1.1
@@ -13036,7 +12948,7 @@ 

@@ -13058,7 +12970,7 @@

Table 1. /recruits/{recruitId}/members
@@ -13197,9 +13109,9 @@

@@ -13230,7 +13142,7 @@

10.6.3. Request parameters

@@ -13251,7 +13163,7 @@

<

-

12.6.4. HTTP response

+

10.6.4. HTTP response

HTTP/1.1 200 OK
@@ -13288,7 +13200,7 @@ 

-

12.6.5. Response fields

+

10.6.5. Response fields

@@ -13422,9 +13334,9 @@

@@ -13455,7 +13367,7 @@
@@ -13477,7 +13389,7 @@

Table 1. /recruit-applications/{recruitApplicationId}/like
@@ -13538,9 +13450,9 @@

@@ -13571,7 +13483,7 @@

10.8.3. Path parameters

@@ -13593,7 +13505,7 @@

-

12.8.4. HTTP response

+

10.8.4. HTTP response

HTTP/1.1 200 OK
@@ -13627,14 +13539,14 @@ 

Table 1. /recruit-applications/{recruitApplicationId}
@@ -13754,9 +13666,9 @@

@@ -13787,7 +13699,7 @@
@@ -13808,7 +13720,7 @@

-

12.9.4. HTTP response

+

10.9.4. HTTP response

@@ -13971,9 +13883,9 @@

@@ -14004,7 +13916,7 @@

10.10.3. Request parameters

@@ -14025,7 +13937,7 @@

-

12.10.4. HTTP response

+

10.10.4. HTTP response

HTTP/1.1 200 OK
@@ -14059,14 +13971,14 @@ 

@@ -14188,12 +14100,12 @@

-

13. 리크루트 덧글

+

11. 리크루트 덧글

@@ -14232,7 +14144,7 @@

11.1.3. Path parameters

@@ -14254,7 +14166,7 @@

-

13.1.4. Request fields

+

11.1.4. Request fields

Table 1. /recruits/{recruitId}/comments
@@ -14283,7 +14195,7 @@

-

13.1.5. HTTP response

+

11.1.5. HTTP response

HTTP/1.1 200 OK
@@ -14308,7 +14220,7 @@ 

-

13.1.6. Response fields

+

11.1.6. Response fields

@@ -14368,9 +14280,9 @@

-

13.2. 리크루트 덧글 삭제

+

11.2. 리크루트 덧글 삭제

-

13.2.1. HTTP request

+

11.2.1. HTTP request

DELETE /recruit-comments/1 HTTP/1.1
@@ -14380,7 +14292,7 @@ 

- +

@@ -14401,7 +14313,7 @@

11.2.3. HTTP response

HTTP/1.1 200 OK
@@ -14420,7 +14332,7 @@ 

-

13.2.4. Response fields

+

11.2.4. Response fields

@@ -14455,9 +14367,9 @@

-

13.3. 리크루트 덧글 업데이트

+

11.3. 리크루트 덧글 업데이트

-

13.3.1. HTTP request

+

11.3.1. HTTP request

PATCH /recruit-comments/1 HTTP/1.1
@@ -14474,7 +14386,7 @@ 

- +

@@ -14495,7 +14407,7 @@

11.3.3. Path parameters

@@ -14517,7 +14429,7 @@

Table 1. /recruit-comments/{recruitCommentId}
@@ -14541,7 +14453,7 @@

-

13.3.5. HTTP response

+

11.3.5. HTTP response

HTTP/1.1 200 OK
@@ -14560,7 +14472,7 @@ 

-

13.3.6. Response fields

+

11.3.6. Response fields

@@ -14595,9 +14507,9 @@

@@ -14628,7 +14540,7 @@

11.4.3. Path parameters

@@ -14650,7 +14562,7 @@

-

13.4.4. HTTP response

+

11.4.4. HTTP response

HTTP/1.1 200 OK
@@ -14672,7 +14584,7 @@ 

-

13.4.5. Response fields

+

11.4.5. Response fields

Table 1. /recruit-comments/{recruitCommentId}/like
@@ -14717,9 +14629,9 @@

-

13.5. 리크루트 덧글 목록 조회

+

11.5. 리크루트 덧글 목록 조회

-

13.5.1. HTTP request

+

11.5.1. HTTP request

GET /recruits/1/comments HTTP/1.1
@@ -14728,7 +14640,7 @@ 

- +

@@ -14749,7 +14661,7 @@

11.5.3. Path parameters

@@ -14771,7 +14683,7 @@

Table 1. /recruits/{recruitId}/comments
@@ -14948,12 +14860,12 @@

@@ -14984,7 +14896,7 @@

12.1.3. HTTP response

HTTP/1.1 200 OK
@@ -15007,7 +14919,7 @@ 

-

14.1.4. Response fields

+

12.1.4. Response fields

@@ -15059,12 +14971,12 @@

-

15. 신고

+

13. 신고

@@ -15124,7 +15036,7 @@

- +

@@ -15145,7 +15057,7 @@
-

15.1.4. HTTP response

+

13.1.4. HTTP response

HTTP/1.1 200 OK
@@ -15164,7 +15076,245 @@ 

-

15.1.5. Response fields

+

13.1.5. Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

code

String

응답 코드

message

String

응답 메시지

data

Object

응답 데이터

+
+
+
+
+
+

14. 알림

+
+
+

14.1. 알림 목록 조회

+
+

14.1.1. HTTP request

+
+
+
GET /notifications?cursor=-1&size=10 HTTP/1.1
+Host: api.ssafsound.com
+Cookie: accessToken=accessTokenValue
+
+
+
+
+

14.1.2. HTTP response

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+Content-Length: 674
+
+{
+  "code" : "200",
+  "message" : "success",
+  "data" : {
+    "notifications" : [ {
+      "notificationId" : 2,
+      "message" : "'취업 하고싶다~~' 게시글에 새로운 대댓글이 달렸습니다.",
+      "contentId" : 2,
+      "serviceType" : "POST",
+      "notificationType" : "COMMENT_REPLAY",
+      "createAt" : "2023-11-10 18:32:15"
+    }, {
+      "notificationId" : 1,
+      "message" : "'오늘 점심 추천좀' 게시글에 새로운 댓글이 달렸습니다.",
+      "contentId" : 1,
+      "serviceType" : "POST",
+      "notificationType" : "POST_REPLAY",
+      "createAt" : "2023-11-10 17:32:15"
+    } ],
+    "cursor" : null
+  }
+}
+
+
+
+
+ + ++++ + + + + + + + + + + + + +
CookieDescription

accessToken

액세스 토큰 필수

+
+
+

14.1.4. Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

code

String

응답 코드

message

String

응답 메시지

data

Object

응답 데이터

data.notifications

Array

알림 목록

data.cursor

Number

다음에 요청할 cursor값, 응답되는 cursor값이 null이면 다음 페이지는 없음을 의미

data.notifications[].notificationId

Number

알림의 고유 ID

data.notifications[].message

String

알림 메시지

data.notifications[].contentId

Number

알림 클릭 시 이동할 해당 컨텐츠의 고유 ID, EX) ServiceType이 POST이면 게시글의 id, RECRUIT면 리쿠르트의 id 단, SYSTEM이면 null이 올 수도 있음.

data.notifications[].serviceType

String

알림을 저장한 서비스 타입, SYSTEM | POST | RECRUIT

data.notifications[].notificationType

String

구체적인 알림 타입, SYSTEM | POST_REPLAY | COMMENT_REPLAY | RECRUIT~ 단, RECRUIT는 추가될 수 있음.

data.notifications[].createAt

String

알림이 저장된 시간, yyyy-MM-dd HH:mm:ss

+
+
+
+

14.2. 새로운 알림 확인

+
+

14.2.1. HTTP request

+
+
+
GET /notifications/new HTTP/1.1
+Host: api.ssafsound.com
+Cookie: accessToken=accessTokenValue
+
+
+
+
+

14.2.2. HTTP response

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+Content-Length: 88
+
+{
+  "code" : "200",
+  "message" : "success",
+  "data" : {
+    "isNew" : true
+  }
+}
+
+
+
+
+ + ++++ + + + + + + + + + + + + +
CookieDescription

accessToken

액세스 토큰 필수

+
+
+

14.2.4. Response fields

@@ -15194,6 +15344,11 @@

Object

+ + + + +

응답 데이터

data.isNew

Boolean

새로운 알림이 있는지 여부, true면 확인하지 않은 알림이 있다는 의미.

@@ -15204,7 +15359,7 @@

diff --git a/src/test/java/com/ssafy/ssafsound/domain/notification/controller/NotificationControllerTest.java b/src/test/java/com/ssafy/ssafsound/domain/notification/controller/NotificationControllerTest.java new file mode 100644 index 000000000..273df00e2 --- /dev/null +++ b/src/test/java/com/ssafy/ssafsound/domain/notification/controller/NotificationControllerTest.java @@ -0,0 +1,82 @@ +package com.ssafy.ssafsound.domain.notification.controller; + +import com.ssafy.ssafsound.domain.notification.dto.GetCheckNotificationResDto; +import com.ssafy.ssafsound.domain.notification.dto.GetNotificationResDto; +import com.ssafy.ssafsound.global.docs.ControllerTest; +import com.ssafy.ssafsound.global.util.fixture.NotificationFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.payload.JsonFieldType; + +import java.util.List; + +import static com.ssafy.ssafsound.global.docs.snippet.CookieDescriptionSnippet.requestCookieAccessTokenMandatory; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; + +class NotificationControllerTest extends ControllerTest { + private final NotificationFixture notificationFixture = new NotificationFixture(); + + @Test + @DisplayName("알림 목록 조회(Cursor), cursor와 size를 기준으로 커서 기반 페이지네이션이 수행됨.") + void getNotificationsByCursor() { + GetNotificationResDto response = GetNotificationResDto.of( + List.of(notificationFixture.createCommentReplyNotification(), + notificationFixture.createPostReplyNotification()), + 10 + ); + + doReturn(response) + .when(notificationService) + .getNotifications(any(), any()); + + restDocs.cookie(ACCESS_TOKEN) + .when().get("/notifications?cursor={cursor}&size={pageSize}", -1, 10) + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .apply(document("notification/get-notifications-by-cursor", + requestCookieAccessTokenMandatory(), + requestParameters( + parameterWithName("cursor").description("cursor값은 다음 페이지를 가져올 마지막 페이지 번호를 의미함, 초기 cursor는 -1, 이후 cursor값은 응답 데이터로 제공되는 cursor값을 사용."), + parameterWithName("size").description("cursor를 기준으로 다음에 불러올 페이지의 size를 의미, 최소 size는 10") + ), + getEnvelopPatternWithData().andWithPrefix("data.", + fieldWithPath("notifications").type(JsonFieldType.ARRAY).description("알림 목록"), + fieldWithPath("cursor").type(JsonFieldType.NUMBER).description("다음에 요청할 cursor값, 응답되는 cursor값이 null이면 다음 페이지는 없음을 의미").optional() + ).andWithPrefix("data.notifications[].", + fieldWithPath("notificationId").type(JsonFieldType.NUMBER).description("알림의 고유 ID"), + fieldWithPath("message").type(JsonFieldType.STRING).description("알림 메시지"), + fieldWithPath("contentId").type(JsonFieldType.NUMBER).description("알림 클릭 시 이동할 해당 컨텐츠의 고유 ID, EX) ServiceType이 POST이면 게시글의 id, RECRUIT면 리쿠르트의 id 단, SYSTEM이면 null이 올 수도 있음."), + fieldWithPath("serviceType").type(JsonFieldType.STRING).description("알림을 저장한 서비스 타입, SYSTEM | POST | RECRUIT"), + fieldWithPath("notificationType").type(JsonFieldType.STRING).description("구체적인 알림 타입, SYSTEM | POST_REPLAY | COMMENT_REPLAY | RECRUIT~~~ 단, RECRUIT는 추가될 수 있음."), + fieldWithPath("createAt").type(JsonFieldType.STRING).description("알림이 저장된 시간, yyyy-MM-dd HH:mm:ss") + ) + ) + ); + } + + @Test + @DisplayName("새로운 알림 확인") + void checkNewNotification() { + doReturn(new GetCheckNotificationResDto(true)) + .when(notificationService) + .checkNotification(any()); + + restDocs.cookie(ACCESS_TOKEN) + .when().get("/notifications/new") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .apply(document("notification/check-notification", + requestCookieAccessTokenMandatory(), + getEnvelopPatternWithData().andWithPrefix("data.", + fieldWithPath("isNew").type(JsonFieldType.BOOLEAN).description("새로운 알림이 있는지 여부, true면 확인하지 않은 알림이 있다는 의미.") + ) + ) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/ssafy/ssafsound/global/docs/ControllerTest.java b/src/test/java/com/ssafy/ssafsound/global/docs/ControllerTest.java index 2a8634b37..dc2d8d0d7 100644 --- a/src/test/java/com/ssafy/ssafsound/global/docs/ControllerTest.java +++ b/src/test/java/com/ssafy/ssafsound/global/docs/ControllerTest.java @@ -18,6 +18,8 @@ import com.ssafy.ssafsound.domain.member.validator.SemesterValidator; import com.ssafy.ssafsound.domain.meta.controller.MetaDataController; import com.ssafy.ssafsound.domain.meta.service.EnumMetaDataConsumer; +import com.ssafy.ssafsound.domain.notification.controller.NotificationController; +import com.ssafy.ssafsound.domain.notification.service.NotificationService; import com.ssafy.ssafsound.domain.post.controller.PostController; import com.ssafy.ssafsound.domain.post.service.PostService; import com.ssafy.ssafsound.domain.recruit.controller.RecruitController; @@ -43,7 +45,6 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; - import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -63,6 +64,7 @@ LunchController.class, MemberController.class, MetaDataController.class, + NotificationController.class, PostController.class, RecruitController.class, RecruitApplicationController.class, @@ -101,6 +103,9 @@ public class ControllerTest { @MockBean protected EnumMetaDataConsumer enumMetaDataConsumer; + @MockBean + protected NotificationService notificationService; + @MockBean protected PostService postService; diff --git a/src/test/java/com/ssafy/ssafsound/global/util/fixture/NotificationFixture.java b/src/test/java/com/ssafy/ssafsound/global/util/fixture/NotificationFixture.java new file mode 100644 index 000000000..b8c845ce1 --- /dev/null +++ b/src/test/java/com/ssafy/ssafsound/global/util/fixture/NotificationFixture.java @@ -0,0 +1,38 @@ +package com.ssafy.ssafsound.global.util.fixture; + +import com.ssafy.ssafsound.domain.notification.domain.Notification; +import com.ssafy.ssafsound.domain.notification.domain.NotificationType; +import com.ssafy.ssafsound.domain.notification.domain.ServiceType; + +import java.time.LocalDateTime; + +public class NotificationFixture { + + public Notification createPostReplyNotification() { + return Notification.builder() + .id(1L) + .ownerId(1L) + .message("'오늘 점심 추천좀' 게시글에 새로운 댓글이 달렸습니다.") + .contentId(1L) + .serviceType(ServiceType.POST) + .notificationType(NotificationType.POST_REPLAY) + .createdAt(LocalDateTime.now()) + .build(); + + } + + public Notification createCommentReplyNotification() { + return Notification.builder() + .id(2L) + .ownerId(1L) + .message("'취업 하고싶다~~' 게시글에 새로운 대댓글이 달렸습니다.") + .contentId(2L) + .serviceType(ServiceType.POST) + .notificationType(NotificationType.COMMENT_REPLAY) + .createdAt(LocalDateTime.now().plusHours(1)) + .build(); + + } + + +}