diff --git a/src/main/java/com/api/trip/common/scheduler/SchedulerRunner.java b/src/main/java/com/api/trip/common/scheduler/SchedulerRunner.java index ea4af5e..27d0b9b 100644 --- a/src/main/java/com/api/trip/common/scheduler/SchedulerRunner.java +++ b/src/main/java/com/api/trip/common/scheduler/SchedulerRunner.java @@ -21,6 +21,7 @@ import org.springframework.web.client.HttpClientErrorException; import java.util.List; +import java.util.Set; @RequiredArgsConstructor @Component @@ -55,8 +56,8 @@ public void updateData() List createItemRequests = naverApiService.toCreateItemRequest(shoppingItems); for (CreateItemRequest createItemRequest : createItemRequests) { Item item = itemService.createItem(createItemRequest); - notificationService.createNotification(item, createItemRequest.getTagNames()); - sseEmitterMap.sendToAll("notification",new SseNotificationResponse(item.getId(), createItemRequest.getTagNames())); + Set memberIds = notificationService.createNotifications(item, createItemRequest.getTagNames()); + sseEmitterMap.send(memberIds,"notification",new SseNotificationResponse(item.getId(), createItemRequest.getTagNames())); /** * * 알림이 가져야할 데이터가 itemId, memberId diff --git a/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java b/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java index 4664b2a..7dfeb75 100644 --- a/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java +++ b/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java @@ -5,6 +5,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; +import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -15,22 +16,22 @@ @Slf4j public class SseEmitterMap { - private final Map sseEmitterMap = new ConcurrentHashMap<>(); + private final Map sseEmitterMap = new ConcurrentHashMap<>(); - public void put(String email, SseEmitter sseEmitter) { - sseEmitter.onCompletion(() -> remove(email)); + public void put(Long memberId, SseEmitter sseEmitter) { + sseEmitter.onCompletion(() -> remove(memberId)); sseEmitter.onTimeout(sseEmitter::complete); - sseEmitterMap.put(email, sseEmitter); - log.info("connected with {}, the number of connections is {}", email, sseEmitterMap.size()); + sseEmitterMap.put(memberId, sseEmitter); + log.info("connected with {}, the number of connections is {}", memberId, sseEmitterMap.size()); } - public void remove(String email) { - sseEmitterMap.remove(email); - log.info("disconnected with {}, the number of connections is {}", email, sseEmitterMap.size()); + public void remove(Long memberId) { + sseEmitterMap.remove(memberId); + log.info("disconnected with {}, the number of connections is {}", memberId, sseEmitterMap.size()); } - public void send(String email, String eventName, Object eventData) { - SseEmitter sseEmitter = sseEmitterMap.get(email); + public void send(Long memberId, String eventName, Object eventData) { + SseEmitter sseEmitter = sseEmitterMap.get(memberId); try { sseEmitter.send( event() @@ -38,17 +39,19 @@ public void send(String email, String eventName, Object eventData) { .data(eventData) ); } catch (IOException | IllegalStateException e) { - remove(email); + remove(memberId); } } - public void sendToAll(String eventName, Object eventData) { + public void send(Collection memberIds, String eventName, Object eventData) { SseEventBuilder sseEventBuilder = event().name(eventName).data(eventData); - sseEmitterMap.forEach((email, sseEmitter) -> { - try { - sseEmitter.send(sseEventBuilder); - } catch (IOException | IllegalStateException e) { - remove(email); + sseEmitterMap.forEach((memberId, sseEmitter) -> { + if (memberIds.contains(memberId)) { + try { + sseEmitter.send(sseEventBuilder); + } catch (IOException | IllegalStateException e) { + remove(memberId); + } } }); } diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java index 2860e62..9d720d0 100644 --- a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -2,6 +2,7 @@ import com.api.trip.domain.article.controller.dto.*; import com.api.trip.domain.article.service.ArticleService; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -17,12 +18,14 @@ public class ArticleController { private final ArticleService articleService; + @Operation(summary = "게시글 등록") @PostMapping public ResponseEntity createArticle(@RequestBody @Valid CreateArticleRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(articleService.createArticle(request, email)); } + @Operation(summary = "게시글 수정") @PatchMapping("/{articleId}") public ResponseEntity updateArticle(@PathVariable Long articleId, @RequestBody @Valid UpdateArticleRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -30,6 +33,7 @@ public ResponseEntity updateArticle(@PathVariable Long articleId, @Request return ResponseEntity.ok().build(); } + @Operation(summary = "게시글 삭제") @DeleteMapping("/{articleId}") public ResponseEntity deleteArticle(@PathVariable Long articleId) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -37,12 +41,14 @@ public ResponseEntity deleteArticle(@PathVariable Long articleId) { return ResponseEntity.ok().build(); } + @Operation(summary = "게시글 상세 조회") @GetMapping("/{articleId}") public ResponseEntity readArticle(@PathVariable Long articleId) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(articleService.readArticle(articleId, email)); } + @Operation(summary = "게시글 목록 조회 (게시글 검색)") @GetMapping public ResponseEntity getArticles( @PageableDefault(size = 8) Pageable pageable, @@ -54,6 +60,7 @@ public ResponseEntity getArticles( return ResponseEntity.ok(articleService.getArticles(pageable, sortCode, category, title, tagName)); } + @Operation(summary = "내가 쓴 게시글 목록 조회") @GetMapping("/me") public ResponseEntity getMyArticles() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java index ffdbc5e..908ba53 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -20,6 +20,7 @@ public class ReadArticleResponse { private String writerNickname; private String writerRole; private String writerProfileImg; + private String writerIntro; private List tags; private String content; private long viewCount; @@ -34,6 +35,7 @@ public static ReadArticleResponse of(Article article, List articleTa .title(article.getTitle()) .writerId(writer.getId()) .writerNickname(writer.getNickname()) + .writerIntro(writer.getIntro()) .writerProfileImg(writer.getProfileImg()) .writerRole(writer.getRole().name()) .tags(articleTags.stream().map(articleTag -> articleTag.getTag().getName()).toList()) diff --git a/src/main/java/com/api/trip/domain/articlefile/controller/ArticleFileController.java b/src/main/java/com/api/trip/domain/articlefile/controller/ArticleFileController.java index 19d0d5f..1ea20ba 100644 --- a/src/main/java/com/api/trip/domain/articlefile/controller/ArticleFileController.java +++ b/src/main/java/com/api/trip/domain/articlefile/controller/ArticleFileController.java @@ -1,6 +1,7 @@ package com.api.trip.domain.articlefile.controller; import com.api.trip.domain.articlefile.service.ArticleFileService; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; @@ -16,6 +17,7 @@ public class ArticleFileController { private final ArticleFileService articleFileService; + @Operation(summary = "게시글 파일(이미지) 업로드") @PostMapping public ResponseEntity upload(MultipartFile file) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); diff --git a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java index 78e9c80..67381aa 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java +++ b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java @@ -5,6 +5,7 @@ import com.api.trip.domain.comment.controller.dto.GetMyCommentsResponse; import com.api.trip.domain.comment.controller.dto.UpdateCommentRequest; import com.api.trip.domain.comment.service.CommentService; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -18,12 +19,14 @@ public class CommentController { private final CommentService commentService; + @Operation(summary = "(대)댓글 등록") @PostMapping public ResponseEntity createComment(@RequestBody @Valid CreateCommentRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(commentService.createComment(request, email)); } + @Operation(summary = "(대)댓글 수정") @PatchMapping("/{commentId}") public ResponseEntity updateComment(@PathVariable Long commentId, @RequestBody @Valid UpdateCommentRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -31,6 +34,7 @@ public ResponseEntity updateComment(@PathVariable Long commentId, @Request return ResponseEntity.ok().build(); } + @Operation(summary = "(대)댓글 삭제") @DeleteMapping("/{commentId}") public ResponseEntity deleteComment(@PathVariable Long commentId) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -38,11 +42,13 @@ public ResponseEntity deleteComment(@PathVariable Long commentId) { return ResponseEntity.ok().build(); } + @Operation(summary = "(대)댓글 목록 조회") @GetMapping public ResponseEntity getComments(Long articleId) { return ResponseEntity.ok(commentService.getComments(articleId)); } + @Operation(summary = "내가 쓴 (대)댓글 목록 조회") @GetMapping("/me") public ResponseEntity getMyComments() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); diff --git a/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java index 76ba35b..9383040 100644 --- a/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java +++ b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java @@ -3,6 +3,7 @@ import com.api.trip.domain.interestarticle.controller.dto.CreateInterestArticleRequest; import com.api.trip.domain.interestarticle.controller.dto.GetMyInterestArticlesResponse; import com.api.trip.domain.interestarticle.service.InterestArticleService; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -16,12 +17,14 @@ public class InterestArticleController { private final InterestArticleService interestArticleService; + @Operation(summary = "게시글 좋아요") @PostMapping public ResponseEntity createInterestArticle(@RequestBody @Valid CreateInterestArticleRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(interestArticleService.createInterestArticle(request, email)); } + @Operation(summary = "게시글 좋아요 취소") @DeleteMapping("/{interestArticleId}") public ResponseEntity deleteInterestArticle(@PathVariable Long interestArticleId) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -29,6 +32,7 @@ public ResponseEntity deleteInterestArticle(@PathVariable Long interestArt return ResponseEntity.ok().build(); } + @Operation(summary = "내가 좋아한 게시글 목록 조회") @GetMapping("/me") public ResponseEntity getMyInterestArticles() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); diff --git a/src/main/java/com/api/trip/domain/interesttag/service/InterestTagService.java b/src/main/java/com/api/trip/domain/interesttag/service/InterestTagService.java index a6cbb85..ce4363e 100644 --- a/src/main/java/com/api/trip/domain/interesttag/service/InterestTagService.java +++ b/src/main/java/com/api/trip/domain/interesttag/service/InterestTagService.java @@ -5,13 +5,14 @@ import com.api.trip.domain.member.model.Member; import com.api.trip.domain.tag.model.Tag; import com.api.trip.domain.tag.repository.TagRepository; -import com.api.trip.domain.tag.service.TagService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service @Slf4j @@ -45,8 +46,8 @@ public List getInterestTag(Member member) { } @Transactional(readOnly = true) - public List getMemberByTags(List tagNames){ + public Set getMembersByTagNames(List tagNames) { List interestTags = interestTagRepository.findInterestTagsByTagNames(tagNames); - return interestTags.stream().map(InterestTag::getMember).toList(); + return interestTags.stream().map(InterestTag::getMember).collect(Collectors.toSet()); } } diff --git a/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java b/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java index 75b151c..9be0759 100644 --- a/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java @@ -1,10 +1,15 @@ package com.api.trip.domain.notification.controller; +import com.api.trip.common.exception.CustomException; +import com.api.trip.common.exception.ErrorCode; import com.api.trip.common.sse.emitter.SseEmitterMap; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; import com.api.trip.domain.notification.controller.dto.DeleteNotificationRequest; import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse; import com.api.trip.domain.notification.controller.dto.ReadNotificationRequest; import com.api.trip.domain.notification.service.NotificationService; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -19,24 +24,31 @@ @RequiredArgsConstructor public class NotificationController { + private final MemberRepository memberRepository; private final NotificationService notificationService; private final SseEmitterMap sseEmitterMap; + @Operation(summary = "SSE 연결") @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseEntity connect() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); + SseEmitter sseEmitter = new SseEmitter(3600000L); - sseEmitterMap.put(email, sseEmitter); - sseEmitterMap.send(email, "connect", LocalDateTime.now()); + sseEmitterMap.put(member.getId(), sseEmitter); + sseEmitterMap.send(member.getId(), "connect", LocalDateTime.now()); return ResponseEntity.ok(sseEmitter); } + @Operation(summary = "알림 목록 조회") @GetMapping("/me") public ResponseEntity getMyNotifications() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(notificationService.getMyNotifications(email)); } + @Operation(summary = "알림 읽음 처리") @PatchMapping public ResponseEntity readNotification(@RequestBody ReadNotificationRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -44,6 +56,7 @@ public ResponseEntity readNotification(@RequestBody ReadNotificationReques return ResponseEntity.ok().build(); } + @Operation(summary = "알림 삭제") @DeleteMapping public ResponseEntity deleteNotification(@RequestBody DeleteNotificationRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -51,6 +64,7 @@ public ResponseEntity deleteNotification(@RequestBody DeleteNotificationRe return ResponseEntity.ok().build(); } + @Operation(summary = "알림 목록 삭제") @DeleteMapping("/me") public ResponseEntity deleteMyNotifications() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); diff --git a/src/main/java/com/api/trip/domain/notification/service/NotificationService.java b/src/main/java/com/api/trip/domain/notification/service/NotificationService.java index 2b66787..d3625d4 100644 --- a/src/main/java/com/api/trip/domain/notification/service/NotificationService.java +++ b/src/main/java/com/api/trip/domain/notification/service/NotificationService.java @@ -17,6 +17,8 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service @Transactional @@ -27,8 +29,8 @@ public class NotificationService { private final NotificationRepository notificationRepository; private final InterestTagService interestTagService; - public void createNotification(Item item, List tagNames) { - List receivers = interestTagService.getMemberByTags(tagNames); + public Set createNotifications(Item item, List tagNames) { + Set receivers = interestTagService.getMembersByTagNames(tagNames); receivers.forEach(member -> { notificationRepository.save( @@ -38,6 +40,8 @@ public void createNotification(Item item, List tagNames) { .build() ); }); + + return receivers.stream().map(Member::getId).collect(Collectors.toSet()); } @Transactional(readOnly = true)