From fb204a7b68804a602d27455ee51dff6988338681 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 12:18:16 +0900 Subject: [PATCH 01/11] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=EC=85=8B=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C(=EA=B2=80=EC=83=89)=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cardset/controller/CardSetController.java | 30 ++++++------- .../controller/GroupCardSetController.java | 36 ++++++++++++++++ .../docs/CardSetControllerDocs.java | 16 +++++++ .../cardset/model/CardSetSearchRequest.java | 35 ++++++++++++++++ .../cardset/model/CardSetSummaryResponse.java | 24 +++++++++++ .../cardset/repository/CardSetRepository.java | 17 ++++++++ .../cardset/service/CardSetService.java | 29 +++++++++++-- ...eRequest.java => CursorPagingRequest.java} | 24 ++++++++++- .../common/model/request/PagingRequest.java | 42 +++++++++++++++++++ ...{PageResponse.java => PagingResponse.java} | 6 +-- .../GroupInvitationQueryController.java | 24 +++++------ .../GroupInvitationQueryControllerDocs.java | 15 +++---- .../flipnote/group/entity/Category.java | 13 +++++- .../model/GroupInvitationListRequest.java | 14 +++++++ .../group/service/GroupInvitationService.java | 34 ++++++++------- .../model/NotificationListRequest.java | 12 +++++- .../service/NotificationService.java | 3 +- 17 files changed, 307 insertions(+), 67 deletions(-) create mode 100644 src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java create mode 100644 src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java create mode 100644 src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java create mode 100644 src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java rename src/main/java/project/flipnote/common/model/request/{CursorPageRequest.java => CursorPagingRequest.java} (53%) create mode 100644 src/main/java/project/flipnote/common/model/request/PagingRequest.java rename src/main/java/project/flipnote/common/model/response/{PageResponse.java => PagingResponse.java} (79%) create mode 100644 src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java diff --git a/src/main/java/project/flipnote/cardset/controller/CardSetController.java b/src/main/java/project/flipnote/cardset/controller/CardSetController.java index 88e0c0fd..9464a6c3 100644 --- a/src/main/java/project/flipnote/cardset/controller/CardSetController.java +++ b/src/main/java/project/flipnote/cardset/controller/CardSetController.java @@ -1,36 +1,32 @@ package project.flipnote.cardset.controller; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +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 jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.cardset.model.CreateCardSetRequest; -import project.flipnote.cardset.model.CreateCardSetResponse; +import project.flipnote.cardset.controller.docs.CardSetControllerDocs; +import project.flipnote.cardset.model.CardSetSearchRequest; +import project.flipnote.cardset.model.CardSetSummaryResponse; import project.flipnote.cardset.service.CardSetService; -import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.common.model.response.PagingResponse; @RequiredArgsConstructor @RestController -@RequestMapping("/v1/groups/{groupId}/card-sets") -public class CardSetController { +@RequestMapping("/v1/card-sets") +public class CardSetController implements CardSetControllerDocs { private final CardSetService cardSetService; - @PostMapping("") - public ResponseEntity createCardSet( - @AuthenticationPrincipal AuthPrinciple authPrinciple, - @PathVariable("groupId") Long groupId, - @RequestBody @Valid CreateCardSetRequest req + @GetMapping + public ResponseEntity> getCardSets( + @Valid @ModelAttribute CardSetSearchRequest req ) { - CreateCardSetResponse res = cardSetService.createCardSet(groupId, authPrinciple, req); + PagingResponse res = cardSetService.getCardSets(req); - return ResponseEntity.status(HttpStatus.CREATED).body(res); + return ResponseEntity.ok(res); } } diff --git a/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java new file mode 100644 index 00000000..6b8fef25 --- /dev/null +++ b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java @@ -0,0 +1,36 @@ +package project.flipnote.cardset.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import project.flipnote.cardset.model.CreateCardSetRequest; +import project.flipnote.cardset.model.CreateCardSetResponse; +import project.flipnote.cardset.service.CardSetService; +import project.flipnote.common.security.dto.AuthPrinciple; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/v1/groups/{groupId}/card-sets") +public class GroupCardSetController { + + private final CardSetService cardSetService; + + @PostMapping("") + public ResponseEntity createCardSet( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId, + @RequestBody @Valid CreateCardSetRequest req + ) { + CreateCardSetResponse res = cardSetService.createCardSet(groupId, authPrinciple, req); + + return ResponseEntity.status(HttpStatus.CREATED).body(res); + } +} diff --git a/src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java b/src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java new file mode 100644 index 00000000..e632c2e0 --- /dev/null +++ b/src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java @@ -0,0 +1,16 @@ +package project.flipnote.cardset.controller.docs; + +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import project.flipnote.cardset.model.CardSetSearchRequest; +import project.flipnote.cardset.model.CardSetSummaryResponse; +import project.flipnote.common.model.response.PagingResponse; + +@Tag(name = "CardSet", description = "CardSet API") +public interface CardSetControllerDocs { + + @Operation(summary = "카드셋 목록 조회(검색)") + ResponseEntity> getCardSets(CardSetSearchRequest req); +} diff --git a/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java b/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java new file mode 100644 index 00000000..5833b52a --- /dev/null +++ b/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java @@ -0,0 +1,35 @@ +package project.flipnote.cardset.model; + +import java.util.Set; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import lombok.Getter; +import lombok.Setter; +import project.flipnote.common.model.request.PagingRequest; + +@Getter +@Setter +public class CardSetSearchRequest extends PagingRequest { + + private static final Set ALLOWED_SORT_FIELDS = Set.of("id"); + + private String keyword; + private String category; + + @Override + public PageRequest getPageRequest() { + String sortBy = this.getSortBy(); + String effectiveSortBy = (sortBy != null && ALLOWED_SORT_FIELDS.contains(sortBy)) ? sortBy : "id"; + + Sort.Direction direction; + try { + direction = Sort.Direction.fromString(this.getOrder()); + } catch (IllegalArgumentException e) { + direction = Sort.Direction.DESC; + } + + return PageRequest.of(getPage() - 1, getSize(), Sort.by(direction, effectiveSortBy)); + } +} diff --git a/src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java b/src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java new file mode 100644 index 00000000..e3c44689 --- /dev/null +++ b/src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java @@ -0,0 +1,24 @@ +package project.flipnote.cardset.model; + +import project.flipnote.cardset.entity.CardSet; + +public record CardSetSummaryResponse( + Long cardSetId, + Long groupId, + String name, + String category, + String hashtag, + String imageUrl +) { + + public static CardSetSummaryResponse from(CardSet cardSet) { + return new CardSetSummaryResponse( + cardSet.getId(), + cardSet.getGroup().getId(), + cardSet.getName(), + cardSet.getCategory().name(), + cardSet.getHashtag(), + cardSet.getImageUrl() + ); + } +} diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java index fd04f29c..0ff2a866 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java @@ -1,10 +1,27 @@ package project.flipnote.cardset.repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import project.flipnote.cardset.entity.CardSet; +import project.flipnote.group.entity.Category; @Repository public interface CardSetRepository extends JpaRepository { + + @Query(""" + SELECT c FROM CardSet c + WHERE (:name IS NULL OR c.name LIKE %:name%) + AND (:category IS NULL OR c.category = :category) + """) + Page findByNameContainingAndCategory( + @Param("name") String name, + @Param("category") Category category, + Pageable pageable + ); + } diff --git a/src/main/java/project/flipnote/cardset/service/CardSetService.java b/src/main/java/project/flipnote/cardset/service/CardSetService.java index 948fc867..7daec866 100644 --- a/src/main/java/project/flipnote/cardset/service/CardSetService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetService.java @@ -1,25 +1,28 @@ package project.flipnote.cardset.service; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.cardset.entity.CardSet; import project.flipnote.cardset.entity.CardSetManager; import project.flipnote.cardset.exception.CardSetErrorCode; +import project.flipnote.cardset.model.CardSetSearchRequest; +import project.flipnote.cardset.model.CardSetSummaryResponse; import project.flipnote.cardset.model.CreateCardSetRequest; import project.flipnote.cardset.model.CreateCardSetResponse; import project.flipnote.cardset.repository.CardSetManagerRepository; import project.flipnote.cardset.repository.CardSetRepository; import project.flipnote.common.exception.BizException; +import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.entity.Category; import project.flipnote.group.entity.Group; import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupRepository; -import project.flipnote.groupjoin.exception.GroupJoinErrorCode; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; @@ -61,7 +64,7 @@ public CreateCardSetResponse createCardSet(Long groupId, AuthPrinciple authPrinc //그룹 정보 찾기 Group group = findGroup(groupId); - + //그룹 내 유저 있는지 확인 if (!existGroupMember(group, user)) { throw new BizException(CardSetErrorCode.GROUP_MEMBER_NOT_FOUND); @@ -91,7 +94,25 @@ public CreateCardSetResponse createCardSet(Long groupId, AuthPrinciple authPrinc cardSetManagerRepository.save(cardSetManager); - return CreateCardSetResponse.from(cardSet.getId()); } + + /** + * 카드셋 목록을 페이지 단위로 조회 + * + * @param req 조회 조건 및 페이징 정보를 포함한 요청 DTO + * @return 페이지 단위로 조회된 카드셋 목록 + * @author 윤정환 + */ + public PagingResponse getCardSets(CardSetSearchRequest req) { + + // TODO: Projection 및 카운트 쿼리 튜닝 필요, 좋아요 수 및 즐겨찾기 수 등 다양한 정렬 조건 추가 필요 + Page CardSetPage = cardSetRepository.findByNameContainingAndCategory( + req.getKeyword(), Category.from(req.getCategory()), req.getPageRequest() + ); + + Page res = CardSetPage.map(CardSetSummaryResponse::from); + + return PagingResponse.from(res); + } } diff --git a/src/main/java/project/flipnote/common/model/request/CursorPageRequest.java b/src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java similarity index 53% rename from src/main/java/project/flipnote/common/model/request/CursorPageRequest.java rename to src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java index 182786bb..7cbe84a6 100644 --- a/src/main/java/project/flipnote/common/model/request/CursorPageRequest.java +++ b/src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java @@ -1,5 +1,7 @@ package project.flipnote.common.model.request; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.util.StringUtils; import io.swagger.v3.oas.annotations.media.Schema; @@ -10,7 +12,7 @@ @Getter @Setter -public class CursorPageRequest { +public class CursorPagingRequest { private String cursor; @@ -18,6 +20,10 @@ public class CursorPageRequest { @Max(30) private Integer size = 10; + private String sortBy; + + private String order = "desc"; + @Schema(hidden = true) public Long getCursorId() { if (!StringUtils.hasText(cursor)) { @@ -35,4 +41,20 @@ public Long getCursorId() { return null; } } + + @Schema(hidden = true) + public PageRequest getPageRequest() { + if (sortBy == null || sortBy.isEmpty()) { + return PageRequest.of(0, size); + } else { + Sort.Direction direction; + try { + direction = Sort.Direction.fromString(order); + } catch (IllegalArgumentException e) { + direction = Sort.Direction.DESC; + } + + return PageRequest.of(0, size, Sort.by(direction, sortBy)); + } + } } diff --git a/src/main/java/project/flipnote/common/model/request/PagingRequest.java b/src/main/java/project/flipnote/common/model/request/PagingRequest.java new file mode 100644 index 00000000..aff8542a --- /dev/null +++ b/src/main/java/project/flipnote/common/model/request/PagingRequest.java @@ -0,0 +1,42 @@ +package project.flipnote.common.model.request; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PagingRequest { + + @Min(1) + private Integer page; + + @Min(1) + @Max(30) + private Integer size = 10; + + private String sortBy; + + private String order = "desc"; + + @Schema(hidden = true) + public PageRequest getPageRequest() { + if (sortBy == null || sortBy.isEmpty()) { + return PageRequest.of(page - 1, size); + } else { + Sort.Direction direction; + try { + direction = Sort.Direction.fromString(order); + } catch (IllegalArgumentException e) { + direction = Sort.Direction.DESC; + } + + return PageRequest.of(page - 1, size, Sort.by(direction, sortBy)); + } + } +} diff --git a/src/main/java/project/flipnote/common/model/response/PageResponse.java b/src/main/java/project/flipnote/common/model/response/PagingResponse.java similarity index 79% rename from src/main/java/project/flipnote/common/model/response/PageResponse.java rename to src/main/java/project/flipnote/common/model/response/PagingResponse.java index 5f33e044..207a5bf0 100644 --- a/src/main/java/project/flipnote/common/model/response/PageResponse.java +++ b/src/main/java/project/flipnote/common/model/response/PagingResponse.java @@ -4,7 +4,7 @@ import org.springframework.data.domain.Page; -public record PageResponse( +public record PagingResponse( List content, int page, int size, @@ -16,8 +16,8 @@ public record PageResponse( boolean hasPrevious ) { - public static PageResponse from(Page page) { - return new PageResponse<>( + public static PagingResponse from(Page page) { + return new PagingResponse<>( page.getContent(), page.getNumber(), page.getSize(), diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java index 5f9da72f..1d29b849 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java @@ -3,13 +3,15 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.model.response.PageResponse; +import project.flipnote.common.model.request.PagingRequest; +import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.controller.docs.GroupInvitationQueryControllerDocs; import project.flipnote.group.model.IncomingGroupInvitationResponse; @@ -24,26 +26,24 @@ public class GroupInvitationQueryController implements GroupInvitationQueryContr private final GroupInvitationService groupInvitationService; @GetMapping("/groups/{groupId}/invitations") - public ResponseEntity> getOutgoingInvitations( + public ResponseEntity> getOutgoingInvitations( @PathVariable("groupId") Long groupId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, + @Valid @ModelAttribute PagingRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { - PageResponse res - = groupInvitationService.getOutgoingInvitations(authPrinciple.userId(), groupId, page, size); + PagingResponse res + = groupInvitationService.getOutgoingInvitations(authPrinciple.userId(), groupId, req); return ResponseEntity.ok(res); } @GetMapping("/group-invitations") - public ResponseEntity> getIncomingInvitations( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, + public ResponseEntity> getIncomingInvitations( + @Valid @ModelAttribute PagingRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { - PageResponse res - = groupInvitationService.getIncomingInvitations(authPrinciple.userId(), page, size); + PagingResponse res + = groupInvitationService.getIncomingInvitations(authPrinciple.userId(), req); return ResponseEntity.ok(res); } diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java index b726b4f5..d655f49f 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java @@ -5,9 +5,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import project.flipnote.common.model.response.PageResponse; +import project.flipnote.common.model.request.PagingRequest; +import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.model.IncomingGroupInvitationResponse; import project.flipnote.group.model.OutgoingGroupInvitationResponse; @@ -16,17 +15,15 @@ public interface GroupInvitationQueryControllerDocs { @Operation(summary = "그룹 초대 보낸 목록 조회", security = {@SecurityRequirement(name = "access-token")}) - ResponseEntity> getOutgoingInvitations( + ResponseEntity> getOutgoingInvitations( Long groupId, - @Min(0) int page, - @Min(1) @Max(30) int size, + PagingRequest req, AuthPrinciple authPrinciple ); @Operation(summary = "그룹 초대 받은 목록 조회", security = {@SecurityRequirement(name = "access-token")}) - ResponseEntity> getIncomingInvitations( - @Min(0) int page, - @Min(1) @Max(30) int size, + ResponseEntity> getIncomingInvitations( + PagingRequest req, AuthPrinciple authPrinciple ); } diff --git a/src/main/java/project/flipnote/group/entity/Category.java b/src/main/java/project/flipnote/group/entity/Category.java index 26e8563e..eec50bf9 100644 --- a/src/main/java/project/flipnote/group/entity/Category.java +++ b/src/main/java/project/flipnote/group/entity/Category.java @@ -1,5 +1,16 @@ package project.flipnote.group.entity; public enum Category { - IT, ENGLISH, MATH, SCIENCE, HISTORY, GEOGRAPHY, KOREAN + IT, ENGLISH, MATH, SCIENCE, HISTORY, GEOGRAPHY, KOREAN; + + public static Category from(String category) { + if (category == null || category.isEmpty()) { + return null; + } + try { + return Category.valueOf(category); + } catch (IllegalArgumentException e) { + return null; + } + } } diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java b/src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java new file mode 100644 index 00000000..869376e5 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java @@ -0,0 +1,14 @@ +package project.flipnote.group.model; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import project.flipnote.common.model.request.PagingRequest; + +public class GroupInvitationListRequest extends PagingRequest { + + @Override + public PageRequest getPageRequest() { + return PageRequest.of(getPage() - 1, getSize(), Sort.by(Sort.Direction.DESC, "id")); + } +} diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index d8e70385..1f9bcb31 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -6,14 +6,14 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import project.flipnote.common.exception.BizException; import project.flipnote.common.model.event.GroupInvitationCreatedEvent; -import project.flipnote.common.model.response.PageResponse; +import project.flipnote.common.model.request.PagingRequest; +import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.entity.GroupInvitation; import project.flipnote.group.entity.GroupInvitationStatus; @@ -135,20 +135,22 @@ public void respondToGroupInvitation( * * @param userId 초대 보낸 목록을 조회하는 회원 ID * @param groupId 초대한 그룹 ID - * @param page 페이지 번호 - * @param size 페이지 크기 + * @param req 페이징을 위한 정보 * @return 페이징된 그룹 초대 보낸 목록 응답 * @author 윤정환 */ - public PageResponse getOutgoingInvitations(Long userId, Long groupId, int page, - int size) { + public PagingResponse getOutgoingInvitations( + Long userId, + Long groupId, + PagingRequest req + ) { if (!groupService.hasPermission(groupId, userId, GroupPermissionStatus.INVITE)) { throw new BizException(GroupInvitationErrorCode.NO_INVITATION_PERMISSION); } // TODO: Projection 및 카운트 쿼리 튜닝 필요 - PageRequest pageRequest = PageRequest.of(page, size); - Page invitationPage = groupInvitationRepository.findAllByGroup_Id(groupId, pageRequest); + Page invitationPage + = groupInvitationRepository.findAllByGroup_Id(groupId, req.getPageRequest()); List inviteeUserIds = invitationPage.getContent() .stream() @@ -163,25 +165,24 @@ public PageResponse getOutgoingInvitations(Long ) ); - return PageResponse.from(res); + return PagingResponse.from(res); } /** * 그룹 초대 받은 목록을 페이징하여 조회 * * @param userId 초대 받은 목록을 조회하는 회원 ID - * @param page 페이지 번호 - * @param size 페이지 크기 + * @param req 페이징을 위한 정보 * @return 페이징된 그룹 초대 받은 목록 응답 * @author 윤정환 */ - public PageResponse getIncomingInvitations(Long userId, int page, int size) { + public PagingResponse getIncomingInvitations(Long userId, PagingRequest req) { // TODO: Projection 및 카운트 쿼리 튜닝 필요 - PageRequest pageRequest = PageRequest.of(page, size); - Page invitationPage = groupInvitationRepository.findAllByInviteeUserId(userId, pageRequest); + Page invitationPage + = groupInvitationRepository.findAllByInviteeUserId(userId, req.getPageRequest()); Page res = invitationPage.map(IncomingGroupInvitationResponse::from); - return PageResponse.from(res); + return PagingResponse.from(res); } /** @@ -207,7 +208,8 @@ public void acceptPendingInvitationsOnRegister(String inviteeEmail) { if (ex.getErrorCode() == GroupErrorCode.ALREADY_GROUP_MEMBER) { invitation.respond(GroupInvitationStatus.ACCEPTED); } - } catch (Exception ignored) { } + } catch (Exception ignored) { + } } } diff --git a/src/main/java/project/flipnote/notification/model/NotificationListRequest.java b/src/main/java/project/flipnote/notification/model/NotificationListRequest.java index 490d93cf..55b216b2 100644 --- a/src/main/java/project/flipnote/notification/model/NotificationListRequest.java +++ b/src/main/java/project/flipnote/notification/model/NotificationListRequest.java @@ -1,16 +1,24 @@ package project.flipnote.notification.model; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + import jakarta.validation.constraints.Min; import lombok.Getter; import lombok.Setter; -import project.flipnote.common.model.request.CursorPageRequest; +import project.flipnote.common.model.request.CursorPagingRequest; @Getter @Setter -public class NotificationListRequest extends CursorPageRequest { +public class NotificationListRequest extends CursorPagingRequest { @Min(1) private Long groupId; private Boolean read; + + @Override + public PageRequest getPageRequest() { + return PageRequest.of(0, getSize(), Sort.by(Sort.Direction.DESC, "id")); + } } diff --git a/src/main/java/project/flipnote/notification/service/NotificationService.java b/src/main/java/project/flipnote/notification/service/NotificationService.java index e7819d58..94c8b025 100644 --- a/src/main/java/project/flipnote/notification/service/NotificationService.java +++ b/src/main/java/project/flipnote/notification/service/NotificationService.java @@ -63,9 +63,8 @@ public class NotificationService { * @author 윤정환 */ public CursorPageResponse getNotifications(Long userId, NotificationListRequest req) { - Pageable pageable = PageRequest.of(0, req.getSize() + 1, Sort.by("id").descending()); List notifications = notificationRepository.findNotificationsByReceiverIdAndCursor( - userId, req.getCursorId(), req.getGroupId(), req.getRead(), pageable + userId, req.getCursorId(), req.getGroupId(), req.getRead(), req.getPageRequest() ); boolean hasNext = notifications.size() > req.getSize(); From 9d554c3e94f8ac912a5730cde6905e428b4c28a7 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 15:55:30 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Feat:=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=EC=85=8B=EC=9D=80=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/cardset/repository/CardSetRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java index 0ff2a866..e2f77356 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java @@ -17,6 +17,7 @@ public interface CardSetRepository extends JpaRepository { SELECT c FROM CardSet c WHERE (:name IS NULL OR c.name LIKE %:name%) AND (:category IS NULL OR c.category = :category) + AND c.publicVisible = TRUE """) Page findByNameContainingAndCategory( @Param("name") String name, From b0e44958a435354b1f0cd2adfb5d1b8f775d5915 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 15:56:02 +0900 Subject: [PATCH 03/11] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=EC=85=8B=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=BF=BC=EB=A6=AC=EB=AC=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/cardset/repository/CardSetRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java index e2f77356..53b9b1e2 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java @@ -15,7 +15,7 @@ public interface CardSetRepository extends JpaRepository { @Query(""" SELECT c FROM CardSet c - WHERE (:name IS NULL OR c.name LIKE %:name%) + WHERE (:name IS NULL OR c.name LIKE CONCAT('%', :name, '%')) AND (:category IS NULL OR c.category = :category) AND c.publicVisible = TRUE """) From 05704893e874a7d7d3788a71e969b6f31459df67 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 16:56:57 +0900 Subject: [PATCH 04/11] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=EC=85=8B=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupCardSetController.java | 18 ++++++++- .../docs/CardSetControllerDocs.java | 7 +++- .../docs/GroupCardSetControllerDocs.java | 23 +++++++++++ .../cardset/exception/CardSetErrorCode.java | 4 +- .../cardset/model/CardSetDetailResponse.java | 38 +++++++++++++++++++ .../cardset/repository/CardSetRepository.java | 4 ++ .../cardset/service/CardSetService.java | 27 +++++++++++++ .../group/controller/GroupController.java | 2 - .../flipnote/group/service/GroupService.java | 15 +++++++- 9 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java create mode 100644 src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java diff --git a/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java index 6b8fef25..7d44fb05 100644 --- a/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java +++ b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -11,6 +12,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.cardset.controller.docs.GroupCardSetControllerDocs; +import project.flipnote.cardset.model.CardSetDetailResponse; import project.flipnote.cardset.model.CreateCardSetRequest; import project.flipnote.cardset.model.CreateCardSetResponse; import project.flipnote.cardset.service.CardSetService; @@ -19,7 +22,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/v1/groups/{groupId}/card-sets") -public class GroupCardSetController { +public class GroupCardSetController implements GroupCardSetControllerDocs { private final CardSetService cardSetService; @@ -33,4 +36,17 @@ public ResponseEntity createCardSet( return ResponseEntity.status(HttpStatus.CREATED).body(res); } + + @GetMapping("/{cardSetId}") + public ResponseEntity getCardSet( + @PathVariable("groupId") Long groupId, + @PathVariable("cardSetId") Long cardSetId, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + CardSetDetailResponse res = cardSetService.getCardSet(authPrinciple.userId(), groupId, cardSetId); + + return ResponseEntity.ok(res); + } + + } diff --git a/src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java b/src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java index e632c2e0..81ed5ac3 100644 --- a/src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java +++ b/src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java @@ -3,6 +3,7 @@ import org.springframework.http.ResponseEntity; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import project.flipnote.cardset.model.CardSetSearchRequest; import project.flipnote.cardset.model.CardSetSummaryResponse; @@ -11,6 +12,8 @@ @Tag(name = "CardSet", description = "CardSet API") public interface CardSetControllerDocs { - @Operation(summary = "카드셋 목록 조회(검색)") - ResponseEntity> getCardSets(CardSetSearchRequest req); + @Operation(summary = "카드셋 목록 조회(검색)", security = {@SecurityRequirement(name = "access-token")}) + ResponseEntity> getCardSets( + CardSetSearchRequest req + ); } diff --git a/src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java b/src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java new file mode 100644 index 00000000..c2993c9b --- /dev/null +++ b/src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java @@ -0,0 +1,23 @@ +package project.flipnote.cardset.controller.docs; + +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import project.flipnote.cardset.model.CardSetDetailResponse; +import project.flipnote.cardset.model.CreateCardSetRequest; +import project.flipnote.cardset.model.CreateCardSetResponse; +import project.flipnote.common.security.dto.AuthPrinciple; + +@Tag(name = "CardSet", description = "CardSet API") +public interface GroupCardSetControllerDocs { + + @Operation(summary = "카드셋 생성", security = {@SecurityRequirement(name = "access-token")}) + ResponseEntity createCardSet( + AuthPrinciple authPrinciple, Long groupId, CreateCardSetRequest req + ); + + @Operation(summary = "카드셋 상세 조회", security = {@SecurityRequirement(name = "access-token")}) + ResponseEntity getCardSet(Long groupId, Long cardSetId, AuthPrinciple authPrinciple); +} diff --git a/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java b/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java index 2ebcf229..dcab8d49 100644 --- a/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java +++ b/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java @@ -10,7 +10,9 @@ @RequiredArgsConstructor public enum CardSetErrorCode implements ErrorCode { - GROUP_MEMBER_NOT_FOUND(HttpStatus.FORBIDDEN, "CARDSET_001", "해당 그룹의 멤버가 아닙니다."); + GROUP_MEMBER_NOT_FOUND(HttpStatus.FORBIDDEN, "CARDSET_001", "해당 그룹의 멤버가 아닙니다."), + CARD_SET_NOT_FOUND(HttpStatus.NOT_FOUND, "CARDSET_002", "카드셋이 존재하지 않습니다."), + CARD_SET_PRIVATE(HttpStatus.FORBIDDEN, "CARDSET_003", "비공개 카드셋입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java b/src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java new file mode 100644 index 00000000..351e922b --- /dev/null +++ b/src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java @@ -0,0 +1,38 @@ +package project.flipnote.cardset.model; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import project.flipnote.cardset.entity.CardSet; + +public record CardSetDetailResponse( + Long cardSetId, + Long groupId, + String name, + String category, + String hashtag, + String imageUrl, + boolean isPublicVisible, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime createdAt, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime modifiedAt +) { + + public static CardSetDetailResponse from(CardSet cardSet) { + return new CardSetDetailResponse( + cardSet.getId(), + cardSet.getGroup().getId(), + cardSet.getName(), + cardSet.getCategory().name(), + cardSet.getHashtag(), + cardSet.getImageUrl(), + cardSet.getPublicVisible(), + cardSet.getCreatedAt(), + cardSet.getModifiedAt() + ); + } +} diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java index 53b9b1e2..42a30727 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java @@ -1,5 +1,7 @@ package project.flipnote.cardset.repository; +import java.util.Optional; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -25,4 +27,6 @@ Page findByNameContainingAndCategory( Pageable pageable ); + Optional findByIdAndGroup_Id(Long id, Long groupId); + } diff --git a/src/main/java/project/flipnote/cardset/service/CardSetService.java b/src/main/java/project/flipnote/cardset/service/CardSetService.java index 7daec866..171a7714 100644 --- a/src/main/java/project/flipnote/cardset/service/CardSetService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetService.java @@ -9,6 +9,7 @@ import project.flipnote.cardset.entity.CardSet; import project.flipnote.cardset.entity.CardSetManager; import project.flipnote.cardset.exception.CardSetErrorCode; +import project.flipnote.cardset.model.CardSetDetailResponse; import project.flipnote.cardset.model.CardSetSearchRequest; import project.flipnote.cardset.model.CardSetSummaryResponse; import project.flipnote.cardset.model.CreateCardSetRequest; @@ -23,6 +24,7 @@ import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupRepository; +import project.flipnote.group.service.GroupService; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; @@ -39,6 +41,7 @@ public class CardSetService { private final GroupRepository groupRepository; private final GroupMemberRepository groupMemberRepository; private final CardSetManagerRepository cardSetManagerRepository; + private final GroupService groupService; private UserProfile validateUser(Long userId) { return userProfileRepository.findByIdAndStatus(userId, UserStatus.ACTIVE).orElseThrow( @@ -115,4 +118,28 @@ public PagingResponse getCardSets(CardSetSearchRequest r return PagingResponse.from(res); } + + /** + * 카드셋 상세 조회 + * + * @param userId 카드셋 상세 조회하는 회원 ID + * @param groupId 카드셋을 생성한 그룹 ID + * @param cardSetId 상세 조회하려는 카드셋 ID + * @return 카드셋 상세 조회 정보 + * @author 윤정환 + */ + public CardSetDetailResponse getCardSet(Long userId, Long groupId, Long cardSetId) { + CardSet cardSet = findByIdAndGroupIdOrThrow(groupId, cardSetId); + + if (!cardSet.getPublicVisible() && !groupService.existsMember(groupId, userId)) { + throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE); + } + + return CardSetDetailResponse.from(cardSet); + } + + private CardSet findByIdAndGroupIdOrThrow(Long groupId, Long cardSetId) { + return cardSetRepository.findByIdAndGroup_Id(cardSetId, groupId) + .orElseThrow(() -> new BizException(CardSetErrorCode.CARD_SET_NOT_FOUND)); + } } diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index f9204460..980970cf 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -6,7 +6,6 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,7 +16,6 @@ import lombok.RequiredArgsConstructor; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.model.FindGroupMemberResponse; -import project.flipnote.group.model.FindGroupMemberResponse; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; import project.flipnote.group.model.GroupDetailResponse; diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index d964890e..b0cbc229 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -6,7 +6,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.common.exception.BizException; @@ -61,7 +60,7 @@ public UserProfile validateUser(AuthPrinciple authPrinciple) { 그룹 내 유저 검증 */ public void validateGroupInUser(UserProfile user, Long groupId) { - if(!groupMemberRepository.existsByGroup_IdAndUser_Id(groupId, user.getId())) { + if (!groupMemberRepository.existsByGroup_IdAndUser_Id(groupId, user.getId())) { throw new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP); } } @@ -305,4 +304,16 @@ public FindGroupMemberResponse findGroupMembers(AuthPrinciple authPrinciple, Lon return FindGroupMemberResponse.from(groupMembers); } + + /** + * 해당 회원에 그룹에 존재하는지 확인 + * + * @param groupId 검증할 그룹의 ID + * @param userId 검증할 회원의 ID + * @return 회원이 그룹 멤버인지 여부 + * @author 윤정환 + */ + public boolean existsMember(Long groupId, Long userId) { + return groupMemberRepository.existsByGroup_IdAndUser_Id(groupId, userId); + } } From 1fca7bc81d5197b4f9205c2d19721da0e7f5a31e Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 17:42:38 +0900 Subject: [PATCH 05/11] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=EC=85=8B=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupCardSetController.java | 13 ++++ .../docs/GroupCardSetControllerDocs.java | 9 +++ .../flipnote/cardset/entity/CardSet.java | 9 +++ .../cardset/exception/CardSetErrorCode.java | 3 +- .../cardset/model/CardSetUpdatePayload.java | 22 +++++++ .../cardset/model/CardSetUpdateRequest.java | 36 +++++++++++ .../repository/CardSetManagerRepository.java | 2 + .../cardset/service/CardSetPolicyService.java | 60 +++++++++++++++++++ .../cardset/service/CardSetService.java | 35 ++++++++--- 9 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/main/java/project/flipnote/cardset/model/CardSetUpdatePayload.java create mode 100644 src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java create mode 100644 src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java diff --git a/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java index 7d44fb05..7e4c91f4 100644 --- a/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java +++ b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -14,6 +15,7 @@ import lombok.RequiredArgsConstructor; import project.flipnote.cardset.controller.docs.GroupCardSetControllerDocs; import project.flipnote.cardset.model.CardSetDetailResponse; +import project.flipnote.cardset.model.CardSetUpdateRequest; import project.flipnote.cardset.model.CreateCardSetRequest; import project.flipnote.cardset.model.CreateCardSetResponse; import project.flipnote.cardset.service.CardSetService; @@ -48,5 +50,16 @@ public ResponseEntity getCardSet( return ResponseEntity.ok(res); } + @PutMapping("/{cardSetId}") + public ResponseEntity updateCardSet( + @PathVariable("groupId") Long groupId, + @PathVariable("cardSetId") Long cardSetId, + @Valid @RequestBody CardSetUpdateRequest req, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + CardSetDetailResponse res = cardSetService.updateCardSet(authPrinciple.userId(), groupId, cardSetId, req); + + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java b/src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java index c2993c9b..0b450cc4 100644 --- a/src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java +++ b/src/main/java/project/flipnote/cardset/controller/docs/GroupCardSetControllerDocs.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import project.flipnote.cardset.model.CardSetDetailResponse; +import project.flipnote.cardset.model.CardSetUpdateRequest; import project.flipnote.cardset.model.CreateCardSetRequest; import project.flipnote.cardset.model.CreateCardSetResponse; import project.flipnote.common.security.dto.AuthPrinciple; @@ -20,4 +21,12 @@ ResponseEntity createCardSet( @Operation(summary = "카드셋 상세 조회", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity getCardSet(Long groupId, Long cardSetId, AuthPrinciple authPrinciple); + + @Operation(summary = "카드셋 수정", security = {@SecurityRequirement(name = "access-token")}) + ResponseEntity updateCardSet( + Long groupId, + Long cardSetId, + CardSetUpdateRequest req, + AuthPrinciple authPrinciple + ); } diff --git a/src/main/java/project/flipnote/cardset/entity/CardSet.java b/src/main/java/project/flipnote/cardset/entity/CardSet.java index 3e4ac665..37b13020 100644 --- a/src/main/java/project/flipnote/cardset/entity/CardSet.java +++ b/src/main/java/project/flipnote/cardset/entity/CardSet.java @@ -14,6 +14,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import project.flipnote.cardset.model.CardSetUpdatePayload; import project.flipnote.common.entity.BaseEntity; import project.flipnote.group.entity.Category; import project.flipnote.group.entity.Group; @@ -57,4 +58,12 @@ private CardSet(String name, Group group, Boolean publicVisible, Category catego this.hashtag = hashtag; this.imageUrl = imageUrl; } + + public void update(CardSetUpdatePayload payload) { + this.name = payload.name(); + this.publicVisible = payload.publicVisible(); + this.category = payload.category(); + this.hashtag = payload.hashtag(); + this.imageUrl = payload.imageUrl(); + } } diff --git a/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java b/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java index dcab8d49..4b60fd4a 100644 --- a/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java +++ b/src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java @@ -12,7 +12,8 @@ public enum CardSetErrorCode implements ErrorCode { GROUP_MEMBER_NOT_FOUND(HttpStatus.FORBIDDEN, "CARDSET_001", "해당 그룹의 멤버가 아닙니다."), CARD_SET_NOT_FOUND(HttpStatus.NOT_FOUND, "CARDSET_002", "카드셋이 존재하지 않습니다."), - CARD_SET_PRIVATE(HttpStatus.FORBIDDEN, "CARDSET_003", "비공개 카드셋입니다."); + CARD_SET_PRIVATE(HttpStatus.FORBIDDEN, "CARDSET_003", "비공개 카드셋입니다."), + CARD_SET_NO_EDIT_PERMISSION(HttpStatus.FORBIDDEN, "CARDSET_004", "카드셋 수정 권한이 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/cardset/model/CardSetUpdatePayload.java b/src/main/java/project/flipnote/cardset/model/CardSetUpdatePayload.java new file mode 100644 index 00000000..f38330c7 --- /dev/null +++ b/src/main/java/project/flipnote/cardset/model/CardSetUpdatePayload.java @@ -0,0 +1,22 @@ +package project.flipnote.cardset.model; + +import project.flipnote.group.entity.Category; + +public record CardSetUpdatePayload( + String name, + Boolean publicVisible, + Category category, + String hashtag, + String imageUrl +) { + + public static CardSetUpdatePayload from(CardSetUpdateRequest req) { + return new CardSetUpdatePayload( + req.name(), + req.publicVisible(), + req.category(), + req.getHashTag(), + req.image() + ); + } +} diff --git a/src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java b/src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java new file mode 100644 index 00000000..58a18469 --- /dev/null +++ b/src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java @@ -0,0 +1,36 @@ +package project.flipnote.cardset.model; + +import java.util.List; + +import org.hibernate.validator.constraints.URL; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import project.flipnote.group.entity.Category; + +public record CardSetUpdateRequest( + + @NotBlank + @Size(max = 50) + String name, + + @NotNull + Boolean publicVisible, + + @NotNull + Category category, + + @NotNull + List hashtag, + + @URL + String image +) { + + @Schema(hidden = true) + public String getHashTag() { + return hashtag != null && hashtag.isEmpty() ? String.join(",", hashtag) : null; + } +} diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java b/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java index 429eb960..dc314f25 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java @@ -7,4 +7,6 @@ @Repository public interface CardSetManagerRepository extends JpaRepository { + + boolean existsByIdAndCardSet_Id(Long userId, Long cardSetId); } diff --git a/src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java b/src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java new file mode 100644 index 00000000..8d7b431e --- /dev/null +++ b/src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java @@ -0,0 +1,60 @@ +package project.flipnote.cardset.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import project.flipnote.cardset.entity.CardSet; +import project.flipnote.cardset.exception.CardSetErrorCode; +import project.flipnote.cardset.repository.CardSetManagerRepository; +import project.flipnote.cardset.repository.CardSetRepository; +import project.flipnote.common.exception.BizException; +import project.flipnote.group.service.GroupService; + +@RequiredArgsConstructor +@Service +public class CardSetPolicyService { + + private final CardSetRepository cardSetRepository; + private final CardSetManagerRepository cardSetManagerRepository; + private final GroupService groupService; + + /** + * 그룹 ID와 카드셋 ID로 카드셋을 조회 + * + * @param groupId 조회할 카드셋이 속한 그룹의 ID + * @param cardSetId 조회할 카드셋의 ID + * @return 조회된 카드셋 엔티티 + * @author 윤정환 + */ + public CardSet findByIdAndGroupIdOrThrow(Long groupId, Long cardSetId) { + return cardSetRepository.findByIdAndGroup_Id(cardSetId, groupId) + .orElseThrow(() -> new BizException(CardSetErrorCode.CARD_SET_NOT_FOUND)); + } + + /** + * 특정 회원이 해당 카드셋을 수정할 수 있는 권한이 있는지 검증 + * + * @param userId 수정 권한을 검증할 회원의 ID + * @param cardSetId 수정 권한이 요구되는 카드셋의 ID + * @author 윤정환 + */ + public void validateCardSetEditable(Long userId, Long cardSetId) { + if (!cardSetManagerRepository.existsByIdAndCardSet_Id(userId, cardSetId)) { + throw new BizException(CardSetErrorCode.CARD_SET_NO_EDIT_PERMISSION); + } + } + + /** + * 특정 회원이 해당 카드셋을 조회할 수 있는 권한이 있는지 검증 + * + * @param cardSet 조회 대상 카드셋 엔티티 + * @param userId 조회 권한을 검증할 회원의 ID + * @param groupId 카드셋이 속한 그룹의 ID + * @author 윤정환 + */ + public void validateCardSetViewable(CardSet cardSet, Long userId, Long groupId) { + if (!cardSet.getPublicVisible() && !groupService.existsMember(groupId, userId)) { + throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE); + } + } +} diff --git a/src/main/java/project/flipnote/cardset/service/CardSetService.java b/src/main/java/project/flipnote/cardset/service/CardSetService.java index 171a7714..31732ed3 100644 --- a/src/main/java/project/flipnote/cardset/service/CardSetService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetService.java @@ -12,6 +12,8 @@ import project.flipnote.cardset.model.CardSetDetailResponse; import project.flipnote.cardset.model.CardSetSearchRequest; import project.flipnote.cardset.model.CardSetSummaryResponse; +import project.flipnote.cardset.model.CardSetUpdatePayload; +import project.flipnote.cardset.model.CardSetUpdateRequest; import project.flipnote.cardset.model.CreateCardSetRequest; import project.flipnote.cardset.model.CreateCardSetResponse; import project.flipnote.cardset.repository.CardSetManagerRepository; @@ -21,6 +23,7 @@ import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.entity.Category; import project.flipnote.group.entity.Group; +import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupRepository; @@ -42,6 +45,7 @@ public class CardSetService { private final GroupMemberRepository groupMemberRepository; private final CardSetManagerRepository cardSetManagerRepository; private final GroupService groupService; + private final CardSetPolicyService cardSetPolicyService; private UserProfile validateUser(Long userId) { return userProfileRepository.findByIdAndStatus(userId, UserStatus.ACTIVE).orElseThrow( @@ -129,17 +133,34 @@ public PagingResponse getCardSets(CardSetSearchRequest r * @author 윤정환 */ public CardSetDetailResponse getCardSet(Long userId, Long groupId, Long cardSetId) { - CardSet cardSet = findByIdAndGroupIdOrThrow(groupId, cardSetId); + CardSet cardSet = cardSetPolicyService.findByIdAndGroupIdOrThrow(groupId, cardSetId); - if (!cardSet.getPublicVisible() && !groupService.existsMember(groupId, userId)) { - throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE); - } + cardSetPolicyService.validateCardSetViewable(cardSet, userId, groupId); return CardSetDetailResponse.from(cardSet); } - private CardSet findByIdAndGroupIdOrThrow(Long groupId, Long cardSetId) { - return cardSetRepository.findByIdAndGroup_Id(cardSetId, groupId) - .orElseThrow(() -> new BizException(CardSetErrorCode.CARD_SET_NOT_FOUND)); + /** + * 카드셋 수정 + * + * @param userId 카드셋 수정하는 회원 ID + * @param groupId 카드셋을 생성한 그룹 ID + * @param cardSetId 수정하려는 카드셋 ID + * @param req 카드셋의 수정 내용을 담은 요청 정보 + * @return 수정된 카드셋 정보 + * @author 윤정환 + */ + @Transactional + public CardSetDetailResponse updateCardSet(Long userId, Long groupId, Long cardSetId, CardSetUpdateRequest req) { + CardSet cardSet = cardSetPolicyService.findByIdAndGroupIdOrThrow(groupId, cardSetId); + + cardSetPolicyService.validateCardSetEditable(userId, cardSetId); + + CardSetUpdatePayload updatePayload = CardSetUpdatePayload.from(req); + cardSet.update(updatePayload); + + cardSetRepository.saveAndFlush(cardSet); + + return CardSetDetailResponse.from(cardSet); } } From 2c7d5c0cdc87b8c04c26250718c4cb01e7426457 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 17:53:34 +0900 Subject: [PATCH 06/11] =?UTF-8?q?Refactor:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EA=B4=80=EB=A0=A8=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/GroupInvitationPolicyService.java | 43 +++++++++++++++++++ .../group/service/GroupInvitationService.java | 22 ++-------- 2 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 src/main/java/project/flipnote/group/service/GroupInvitationPolicyService.java diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationPolicyService.java b/src/main/java/project/flipnote/group/service/GroupInvitationPolicyService.java new file mode 100644 index 00000000..ed79e75b --- /dev/null +++ b/src/main/java/project/flipnote/group/service/GroupInvitationPolicyService.java @@ -0,0 +1,43 @@ +package project.flipnote.group.service; + +import java.util.Objects; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import project.flipnote.common.exception.BizException; +import project.flipnote.group.entity.GroupPermissionStatus; +import project.flipnote.group.exception.GroupInvitationErrorCode; + +@RequiredArgsConstructor +@Service +public class GroupInvitationPolicyService { + + private final GroupService groupService; + + /** + * 그룹 초대 권한을 검증 + * + * @param userId 권한을 검증할 회원 ID + * @param groupId 검증할 그룹 ID + * @author 윤정환 + */ + public void validateGroupInvitePermission(Long userId, Long groupId) { + if (!groupService.hasPermission(groupId, userId, GroupPermissionStatus.INVITE)) { + throw new BizException(GroupInvitationErrorCode.NO_INVITATION_PERMISSION); + } + } + + /** + * 자기 자신을 초대했는지 검증 + * + * @param inviterUserEmail 초대 보낸 회원 이메일 + * @param inviteeEmail 초대 받은 회원 이메일 + * @author 윤정환 + */ + public void validateSelfInvitation(String inviterUserEmail, String inviteeEmail) { + if (Objects.equals(inviterUserEmail, inviteeEmail)) { + throw new BizException(GroupInvitationErrorCode.CANNOT_INVITE_SELF); + } + } +} diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 1f9bcb31..1eb0f667 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -43,6 +43,7 @@ public class GroupInvitationService { private final GroupMemberRepository groupMemberRepository; private final GroupMemberPolicyService groupMemberPolicyService; private final ApplicationEventPublisher eventPublisher; + private final GroupInvitationPolicyService groupInvitationPolicyService; /** * 그룹에 회원 혹은 비회원 초대 @@ -59,13 +60,11 @@ public GroupInvitationCreateResponse createGroupInvitation( GroupInvitationCreateRequest req ) { Long inviterUserId = authPrinciple.userId(); - validateGroupInvitePermission(inviterUserId, groupId); + groupInvitationPolicyService.validateGroupInvitePermission(inviterUserId, groupId); String inviterUserEmail = authPrinciple.email(); String inviteeEmail = req.email(); - if (Objects.equals(inviterUserEmail, inviteeEmail)) { - throw new BizException(GroupInvitationErrorCode.CANNOT_INVITE_SELF); - } + groupInvitationPolicyService.validateSelfInvitation(inviterUserEmail, inviteeEmail); Long invitationId = userService.findActiveUserByEmail(inviteeEmail) .map(inviteeUser -> createUserInvitation(inviterUserId, groupId, inviteeUser)) @@ -84,7 +83,7 @@ public GroupInvitationCreateResponse createGroupInvitation( */ @Transactional public void deleteGroupInvitation(Long userId, Long groupId, Long invitationId) { - validateGroupInvitePermission(userId, groupId); + groupInvitationPolicyService.validateGroupInvitePermission(userId, groupId); GroupInvitation invitation = groupInvitationRepository .findByIdAndStatus(invitationId, GroupInvitationStatus.PENDING) @@ -213,19 +212,6 @@ public void acceptPendingInvitationsOnRegister(String inviteeEmail) { } } - /** - * 그룹 초대 권한을 검증 - * - * @param userId 권한을 검증할 회원 ID - * @param groupId 검증할 그룹 ID - * @author 윤정환 - */ - private void validateGroupInvitePermission(Long userId, Long groupId) { - if (!groupService.hasPermission(groupId, userId, GroupPermissionStatus.INVITE)) { - throw new BizException(GroupInvitationErrorCode.NO_INVITATION_PERMISSION); - } - } - /** * 그룹에 회원 초대 * From c7363b24415af0aad5efff8e604fdcf961021f75 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 18:22:21 +0900 Subject: [PATCH 07/11] =?UTF-8?q?Fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=82=B4=EC=9A=A9=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/cardset/model/CardSetDetailResponse.java | 2 +- .../flipnote/cardset/model/CardSetSearchRequest.java | 2 +- .../flipnote/cardset/model/CardSetUpdateRequest.java | 2 +- .../cardset/repository/CardSetManagerRepository.java | 2 +- .../flipnote/cardset/service/CardSetPolicyService.java | 2 +- .../flipnote/common/model/request/PagingRequest.java | 6 +++--- .../group/controller/GroupInvitationQueryController.java | 6 +++--- .../docs/GroupInvitationQueryControllerDocs.java | 6 +++--- .../flipnote/group/model/GroupInvitationListRequest.java | 2 +- .../flipnote/group/service/GroupInvitationService.java | 9 ++++++--- 10 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java b/src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java index 351e922b..4a7ed142 100644 --- a/src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java +++ b/src/main/java/project/flipnote/cardset/model/CardSetDetailResponse.java @@ -13,7 +13,7 @@ public record CardSetDetailResponse( String category, String hashtag, String imageUrl, - boolean isPublicVisible, + boolean publicVisible, @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, diff --git a/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java b/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java index 5833b52a..a2450404 100644 --- a/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java +++ b/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java @@ -30,6 +30,6 @@ public PageRequest getPageRequest() { direction = Sort.Direction.DESC; } - return PageRequest.of(getPage() - 1, getSize(), Sort.by(direction, effectiveSortBy)); + return PageRequest.of(getPage() - 1, getSize() + 1, Sort.by(direction, effectiveSortBy)); } } diff --git a/src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java b/src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java index 58a18469..551eae86 100644 --- a/src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java +++ b/src/main/java/project/flipnote/cardset/model/CardSetUpdateRequest.java @@ -31,6 +31,6 @@ public record CardSetUpdateRequest( @Schema(hidden = true) public String getHashTag() { - return hashtag != null && hashtag.isEmpty() ? String.join(",", hashtag) : null; + return hashtag != null && !hashtag.isEmpty() ? String.join(",", hashtag) : null; } } diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java b/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java index dc314f25..02367726 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java @@ -8,5 +8,5 @@ @Repository public interface CardSetManagerRepository extends JpaRepository { - boolean existsByIdAndCardSet_Id(Long userId, Long cardSetId); + boolean existsByUser_IdAndCardSet_Id(Long userId, Long cardSetId); } diff --git a/src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java b/src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java index 8d7b431e..406d4cf2 100644 --- a/src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java @@ -39,7 +39,7 @@ public CardSet findByIdAndGroupIdOrThrow(Long groupId, Long cardSetId) { * @author 윤정환 */ public void validateCardSetEditable(Long userId, Long cardSetId) { - if (!cardSetManagerRepository.existsByIdAndCardSet_Id(userId, cardSetId)) { + if (!cardSetManagerRepository.existsByUser_IdAndCardSet_Id(userId, cardSetId)) { throw new BizException(CardSetErrorCode.CARD_SET_NO_EDIT_PERMISSION); } } diff --git a/src/main/java/project/flipnote/common/model/request/PagingRequest.java b/src/main/java/project/flipnote/common/model/request/PagingRequest.java index aff8542a..1fd7d51b 100644 --- a/src/main/java/project/flipnote/common/model/request/PagingRequest.java +++ b/src/main/java/project/flipnote/common/model/request/PagingRequest.java @@ -14,7 +14,7 @@ public class PagingRequest { @Min(1) - private Integer page; + private Integer page = 1; @Min(1) @Max(30) @@ -27,7 +27,7 @@ public class PagingRequest { @Schema(hidden = true) public PageRequest getPageRequest() { if (sortBy == null || sortBy.isEmpty()) { - return PageRequest.of(page - 1, size); + return PageRequest.of(page - 1, size + 1); } else { Sort.Direction direction; try { @@ -36,7 +36,7 @@ public PageRequest getPageRequest() { direction = Sort.Direction.DESC; } - return PageRequest.of(page - 1, size, Sort.by(direction, sortBy)); + return PageRequest.of(page - 1, size + 1, Sort.by(direction, sortBy)); } } } diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java index 1d29b849..cc5b29c6 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java @@ -10,10 +10,10 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.model.request.PagingRequest; import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.controller.docs.GroupInvitationQueryControllerDocs; +import project.flipnote.group.model.GroupInvitationListRequest; import project.flipnote.group.model.IncomingGroupInvitationResponse; import project.flipnote.group.model.OutgoingGroupInvitationResponse; import project.flipnote.group.service.GroupInvitationService; @@ -28,7 +28,7 @@ public class GroupInvitationQueryController implements GroupInvitationQueryContr @GetMapping("/groups/{groupId}/invitations") public ResponseEntity> getOutgoingInvitations( @PathVariable("groupId") Long groupId, - @Valid @ModelAttribute PagingRequest req, + @Valid @ModelAttribute GroupInvitationListRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { PagingResponse res @@ -39,7 +39,7 @@ public ResponseEntity> getOutgoi @GetMapping("/group-invitations") public ResponseEntity> getIncomingInvitations( - @Valid @ModelAttribute PagingRequest req, + @Valid @ModelAttribute GroupInvitationListRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { PagingResponse res diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java index d655f49f..c102c7d2 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java @@ -5,9 +5,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import project.flipnote.common.model.request.PagingRequest; import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.model.GroupInvitationListRequest; import project.flipnote.group.model.IncomingGroupInvitationResponse; import project.flipnote.group.model.OutgoingGroupInvitationResponse; @@ -17,13 +17,13 @@ public interface GroupInvitationQueryControllerDocs { @Operation(summary = "그룹 초대 보낸 목록 조회", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity> getOutgoingInvitations( Long groupId, - PagingRequest req, + GroupInvitationListRequest req, AuthPrinciple authPrinciple ); @Operation(summary = "그룹 초대 받은 목록 조회", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity> getIncomingInvitations( - PagingRequest req, + GroupInvitationListRequest req, AuthPrinciple authPrinciple ); } diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java b/src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java index 869376e5..68a1c9b4 100644 --- a/src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java +++ b/src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java @@ -9,6 +9,6 @@ public class GroupInvitationListRequest extends PagingRequest { @Override public PageRequest getPageRequest() { - return PageRequest.of(getPage() - 1, getSize(), Sort.by(Sort.Direction.DESC, "id")); + return PageRequest.of(getPage() - 1, getSize() + 1, Sort.by(Sort.Direction.DESC, "id")); } } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 1eb0f667..810f87b1 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -12,7 +12,6 @@ import lombok.RequiredArgsConstructor; import project.flipnote.common.exception.BizException; import project.flipnote.common.model.event.GroupInvitationCreatedEvent; -import project.flipnote.common.model.request.PagingRequest; import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.entity.GroupInvitation; @@ -22,6 +21,7 @@ import project.flipnote.group.exception.GroupInvitationErrorCode; import project.flipnote.group.model.GroupInvitationCreateRequest; import project.flipnote.group.model.GroupInvitationCreateResponse; +import project.flipnote.group.model.GroupInvitationListRequest; import project.flipnote.group.model.GroupInvitationRespondRequest; import project.flipnote.group.model.IncomingGroupInvitationResponse; import project.flipnote.group.model.OutgoingGroupInvitationResponse; @@ -141,7 +141,7 @@ public void respondToGroupInvitation( public PagingResponse getOutgoingInvitations( Long userId, Long groupId, - PagingRequest req + GroupInvitationListRequest req ) { if (!groupService.hasPermission(groupId, userId, GroupPermissionStatus.INVITE)) { throw new BizException(GroupInvitationErrorCode.NO_INVITATION_PERMISSION); @@ -175,7 +175,10 @@ public PagingResponse getOutgoingInvitations( * @return 페이징된 그룹 초대 받은 목록 응답 * @author 윤정환 */ - public PagingResponse getIncomingInvitations(Long userId, PagingRequest req) { + public PagingResponse getIncomingInvitations( + Long userId, + GroupInvitationListRequest req + ) { // TODO: Projection 및 카운트 쿼리 튜닝 필요 Page invitationPage = groupInvitationRepository.findAllByInviteeUserId(userId, req.getPageRequest()); From da0ea912bc732839382715e7eaa48e4969ae9bac Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 19:13:28 +0900 Subject: [PATCH 08/11] =?UTF-8?q?Chore:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EB=8E=81=EC=8A=A4=20=EA=BC=AC=EC=9D=B8=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7a8051c8..1aeb9fda 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -60,18 +60,6 @@ app: encryption: key: ${APP_ENCRYPTION_KEY} -cloud: - aws: - region: ${S3_BUCKET_REGION} - credentials: - access-key: ${S3_ACCESS_KEY} - secret-key: ${S3_SECRET_KEY} - stack: - auto: false - s3: - bucket: ${S3_BUCKET_NAME} - - client: url: ${APP_CLIENT_URL:https://flipnote.site} paths: @@ -95,6 +83,17 @@ cloud: - email - profile +cloud: + aws: + region: ${S3_BUCKET_REGION} + credentials: + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} + stack: + auto: false + s3: + bucket: ${S3_BUCKET_NAME} + springdoc: server: url: http://localhost:8080 From c93ace02161add1e8a054064bd9ca22d7e93c9c3 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 19:13:45 +0900 Subject: [PATCH 09/11] =?UTF-8?q?Feat:=20=EB=B9=84=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9=20=EC=B4=88=EB=8C=80=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GuestGroupInvitationEventListener.java | 39 +++++++++++++++ .../GuestGroupInvitationCreateEvent.java | 7 +++ .../group/service/GroupInvitationService.java | 5 +- .../flipnote/infra/email/EmailService.java | 2 + .../infra/email/ResendEmailService.java | 23 +++++++++ .../email/guest-group-invitation.html | 47 +++++++++++++++++++ 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java create mode 100644 src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java create mode 100644 src/main/resources/templates/email/guest-group-invitation.html diff --git a/src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java b/src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java new file mode 100644 index 00000000..3cbb0965 --- /dev/null +++ b/src/main/java/project/flipnote/group/listener/GuestGroupInvitationEventListener.java @@ -0,0 +1,39 @@ +package project.flipnote.group.listener; + +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.common.config.ClientProperties; +import project.flipnote.group.model.event.GuestGroupInvitationCreateEvent; +import project.flipnote.infra.email.EmailService; + +@Slf4j +@RequiredArgsConstructor +@Component +public class GuestGroupInvitationEventListener { + + private final EmailService emailService; + private final ClientProperties clientProperties; + + @Async + @Retryable( + maxAttempts = 3, + backoff = @Backoff(delay = 2000, multiplier = 2) + ) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleGuestGroupInvitationCreateEvent(GuestGroupInvitationCreateEvent event) { + emailService.sendGuestGroupInvitation(event.email(), event.groupName(), clientProperties.getUrl()); + } + + @Recover + public void recover(Exception ex, GuestGroupInvitationCreateEvent event) { + log.error("비회원 그룹 초대 전송 처리 예외 발생: email={}, groupName={}", event.email(), event.groupName(), ex); + } +} diff --git a/src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java b/src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java new file mode 100644 index 00000000..b9b594ff --- /dev/null +++ b/src/main/java/project/flipnote/group/model/event/GuestGroupInvitationCreateEvent.java @@ -0,0 +1,7 @@ +package project.flipnote.group.model.event; + +public record GuestGroupInvitationCreateEvent( + String email, + String groupName +) { +} diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 810f87b1..87d031c6 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -25,6 +25,7 @@ import project.flipnote.group.model.GroupInvitationRespondRequest; import project.flipnote.group.model.IncomingGroupInvitationResponse; import project.flipnote.group.model.OutgoingGroupInvitationResponse; +import project.flipnote.group.model.event.GuestGroupInvitationCreateEvent; import project.flipnote.group.repository.GroupInvitationRepository; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupRepository; @@ -269,7 +270,9 @@ private Long createGuestInvitation(Long inviterUserId, Long groupId, String invi .build(); groupInvitationRepository.save(invitation); - // TODO: 초대받은 비회원한테 이메일 전송 + String groupName = groupRepository.findGroupNameById(groupId) + .orElseThrow(() -> new BizException(GroupErrorCode.GROUP_NOT_FOUND)); + eventPublisher.publishEvent(new GuestGroupInvitationCreateEvent(inviteeEmail, groupName)); return invitation.getId(); } diff --git a/src/main/java/project/flipnote/infra/email/EmailService.java b/src/main/java/project/flipnote/infra/email/EmailService.java index c2cf3f6f..fa8ecbed 100644 --- a/src/main/java/project/flipnote/infra/email/EmailService.java +++ b/src/main/java/project/flipnote/infra/email/EmailService.java @@ -5,4 +5,6 @@ public interface EmailService { void sendEmailVerificationCode(String to, String code, int ttl); void sendPasswordResetLink(String to, String link, int ttl); + + void sendGuestGroupInvitation(String to, String groupName, String registerUrl); } diff --git a/src/main/java/project/flipnote/infra/email/ResendEmailService.java b/src/main/java/project/flipnote/infra/email/ResendEmailService.java index 78ba7b57..13dff543 100644 --- a/src/main/java/project/flipnote/infra/email/ResendEmailService.java +++ b/src/main/java/project/flipnote/infra/email/ResendEmailService.java @@ -67,4 +67,27 @@ public void sendPasswordResetLink(String to, String link, int ttl) { throw new EmailSendException(e); } } + + @Override + public void sendGuestGroupInvitation(String to, String groupName, String registerUrl) { + Context context = new Context(); + context.setVariable("groupName", groupName); + context.setVariable("registerUrl", registerUrl); + + String html = templateEngine.process("email/guest-group-invitation", context); + + CreateEmailOptions params = CreateEmailOptions.builder() + .from(resendProperties.getFromEmail()) + .to(to) + .subject("그룹 초대 안내") + .html(html) + .build(); + + try { + resend.emails().send(params); + } catch (ResendException e) { + log.error("비회원 그룹 초대 발송 실패: email={}", to, e); + throw new EmailSendException(e); + } + } } diff --git a/src/main/resources/templates/email/guest-group-invitation.html b/src/main/resources/templates/email/guest-group-invitation.html new file mode 100644 index 00000000..b09b54a2 --- /dev/null +++ b/src/main/resources/templates/email/guest-group-invitation.html @@ -0,0 +1,47 @@ + + + + + + + 그룹 초대 안내 + + +
+ 그룹 초대 메시지입니다. 아래 내용을 확인해 주세요. +
+ + + + + +
+ + + + +
+

+ 그룹 초대 안내 +

+

+ 안녕하세요.
+ [그룹명] 그룹에서 회원님을 초대하였습니다.
+ 아래 버튼을 눌러 회원가입 후 그룹에 참여해 주세요. +

+ +

+ 본 메일은 발신전용입니다. +

+
+
+ + From 770186721c63ceb309ad8832af3230d72712ce50 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 19:23:57 +0900 Subject: [PATCH 10/11] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20=EB=B9=84=ED=9A=8C=EC=9B=90=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=20=EC=B4=88=EB=8C=80=20=ED=9A=8C=EC=9B=90=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=ED=99=98=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/auth/service/AuthService.java | 2 +- .../model/event/UserRegisteredEvent.java | 3 ++- .../listener/UserRegisteredEventListener.java | 4 ++-- .../repository/GroupInvitationRepository.java | 11 +++++++--- .../group/service/GroupInvitationService.java | 21 ++----------------- 5 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index a25e5049..d01db712 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -78,7 +78,7 @@ public UserRegisterResponse register(UserRegisterRequest req) { .build(); userAuthRepository.save(userAuth); - eventPublisher.publishEvent(new UserRegisteredEvent(email)); + eventPublisher.publishEvent(new UserRegisteredEvent(email, userId)); return UserRegisterResponse.from(userId); } diff --git a/src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java b/src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java index 3ca1e498..c19b4619 100644 --- a/src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java +++ b/src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java @@ -1,6 +1,7 @@ package project.flipnote.common.model.event; public record UserRegisteredEvent( - String email + String email, + Long userId ) { } diff --git a/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java b/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java index a12c4899..56e4729e 100644 --- a/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java +++ b/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java @@ -27,11 +27,11 @@ public class UserRegisteredEventListener { ) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleUserRegisteredEvent(UserRegisteredEvent event) { - groupInvitationService.acceptPendingInvitationsOnRegister(event.email()); + groupInvitationService.convertGuestInvitationToMember(event.email(), event.userId()); } @Recover public void recover(Exception ex, UserRegisteredEvent event) { - log.error("회원가입 후속 처리 예외 발생: email={}", event.email(), ex); + log.error("회원가입 후 비회원 그룹 초대 회원으로 변환 중 예외 발생: email={}, userId={}", event.email(), event.userId(), ex); } } diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index e4bff22c..80fecb6c 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -1,7 +1,6 @@ package project.flipnote.group.repository; import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -24,8 +23,6 @@ Optional findByIdAndGroup_IdAndInviteeUserIdAndStatus( Page findAllByGroup_Id(Long groupId, Pageable pageable); - List findAllByInviteeEmailAndStatus(String inviteeEmail, GroupInvitationStatus status); - Page findAllByInviteeUserId(Long inviteeUserId, Pageable pageable); boolean existsByGroup_IdAndInviteeUserIdAndStatus(Long groupId, Long inviteeUserId, GroupInvitationStatus status); @@ -40,4 +37,12 @@ Optional findByIdAndGroup_IdAndInviteeUserIdAndStatus( AND gi.expiredAt < :now """) int bulkExpire(@Param("now") LocalDateTime now); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + UPDATE GroupInvitation gi + SET gi.inviteeUserId = :inviteeUserId + WHERE gi.inviteeEmail = :inviteeEmail + """) + int bulkUpdateInviteeUserId(@Param("inviteeEmail") String inviteeEmail, @Param("inviteeUserId") Long inviteeUserId); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 87d031c6..83b47da6 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -195,25 +195,8 @@ public PagingResponse getIncomingInvitations( * @author 윤정환 */ @Transactional - public void acceptPendingInvitationsOnRegister(String inviteeEmail) { - List invitations = groupInvitationRepository - .findAllByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING); - - for (GroupInvitation invitation : invitations) { - if (invitation.isExpired()) { - continue; - } - - try { - groupMemberPolicyService.addGroupMember(invitation.getInviteeUserId(), invitation.getGroup().getId()); - invitation.respond(GroupInvitationStatus.ACCEPTED); - } catch (BizException ex) { - if (ex.getErrorCode() == GroupErrorCode.ALREADY_GROUP_MEMBER) { - invitation.respond(GroupInvitationStatus.ACCEPTED); - } - } catch (Exception ignored) { - } - } + public void convertGuestInvitationToMember(String inviteeEmail, Long inviteeUserId) { + groupInvitationRepository.bulkUpdateInviteeUserId(inviteeEmail, inviteeUserId); } /** From a213dddc71c91c91271e522a56edaa39ec74f978 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 26 Aug 2025 19:28:37 +0900 Subject: [PATCH 11/11] Chore: Rename CursorPageRequest to CursorPagingRequest --- .../model/response/CursorPageResponse.java | 21 ------------------- .../model/response/CursorPagingResponse.java | 21 +++++++++++++++++++ .../controller/NotificationController.java | 6 +++--- .../docs/NotificationControllerDocs.java | 4 ++-- .../service/NotificationService.java | 9 +++----- 5 files changed, 29 insertions(+), 32 deletions(-) delete mode 100644 src/main/java/project/flipnote/common/model/response/CursorPageResponse.java create mode 100644 src/main/java/project/flipnote/common/model/response/CursorPagingResponse.java diff --git a/src/main/java/project/flipnote/common/model/response/CursorPageResponse.java b/src/main/java/project/flipnote/common/model/response/CursorPageResponse.java deleted file mode 100644 index 3f50df64..00000000 --- a/src/main/java/project/flipnote/common/model/response/CursorPageResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package project.flipnote.common.model.response; - -import java.util.List; -import java.util.Objects; - -public record CursorPageResponse( - List content, - boolean hasNext, - String nextCursor, - int size -) { - - public static CursorPageResponse of(List content, boolean hasNext, String nextCursor) { - return new CursorPageResponse<>(content, hasNext, hasNext ? nextCursor : null, content.size()); - } - - public static CursorPageResponse of(List content, boolean hasNext, Long nextCursorId) { - String nextCursor = Objects.toString(nextCursorId, null); - return of(content, hasNext, nextCursor); - } -} diff --git a/src/main/java/project/flipnote/common/model/response/CursorPagingResponse.java b/src/main/java/project/flipnote/common/model/response/CursorPagingResponse.java new file mode 100644 index 00000000..af9c6fac --- /dev/null +++ b/src/main/java/project/flipnote/common/model/response/CursorPagingResponse.java @@ -0,0 +1,21 @@ +package project.flipnote.common.model.response; + +import java.util.List; +import java.util.Objects; + +public record CursorPagingResponse( + List content, + boolean hasNext, + String nextCursor, + int size +) { + + public static CursorPagingResponse of(List content, boolean hasNext, String nextCursor) { + return new CursorPagingResponse<>(content, hasNext, hasNext ? nextCursor : null, content.size()); + } + + public static CursorPagingResponse of(List content, boolean hasNext, Long nextCursorId) { + String nextCursor = Objects.toString(nextCursorId, null); + return of(content, hasNext, nextCursor); + } +} diff --git a/src/main/java/project/flipnote/notification/controller/NotificationController.java b/src/main/java/project/flipnote/notification/controller/NotificationController.java index 7aaa3e96..0349a76a 100644 --- a/src/main/java/project/flipnote/notification/controller/NotificationController.java +++ b/src/main/java/project/flipnote/notification/controller/NotificationController.java @@ -13,7 +13,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.model.response.CursorPageResponse; +import project.flipnote.common.model.response.CursorPagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.notification.controller.docs.NotificationControllerDocs; import project.flipnote.notification.model.NotificationListRequest; @@ -29,11 +29,11 @@ public class NotificationController implements NotificationControllerDocs { private final NotificationService notificationService; @GetMapping - public ResponseEntity> getNotifications( + public ResponseEntity> getNotifications( @Valid @ModelAttribute NotificationListRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { - CursorPageResponse res + CursorPagingResponse res = notificationService.getNotifications(authPrinciple.userId(), req); return ResponseEntity.ok(res); diff --git a/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java b/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java index 232e1860..e25f84fc 100644 --- a/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java +++ b/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import project.flipnote.common.model.response.CursorPageResponse; +import project.flipnote.common.model.response.CursorPagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.notification.model.NotificationListRequest; import project.flipnote.notification.model.NotificationResponse; @@ -14,7 +14,7 @@ public interface NotificationControllerDocs { @Operation(summary = "알림 목록 조회") - ResponseEntity> getNotifications( + ResponseEntity> getNotifications( NotificationListRequest req, AuthPrinciple authPrinciple ); diff --git a/src/main/java/project/flipnote/notification/service/NotificationService.java b/src/main/java/project/flipnote/notification/service/NotificationService.java index 94c8b025..ecf41c71 100644 --- a/src/main/java/project/flipnote/notification/service/NotificationService.java +++ b/src/main/java/project/flipnote/notification/service/NotificationService.java @@ -11,9 +11,6 @@ import org.apache.commons.text.StringSubstitutor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.MessageSource; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,7 +21,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.common.exception.BizException; -import project.flipnote.common.model.response.CursorPageResponse; +import project.flipnote.common.model.response.CursorPagingResponse; import project.flipnote.group.service.GroupService; import project.flipnote.infra.firebase.FcmErrorCode; import project.flipnote.infra.firebase.FirebaseService; @@ -62,7 +59,7 @@ public class NotificationService { * @return 커서 기반 페이징된 알림 목록 * @author 윤정환 */ - public CursorPageResponse getNotifications(Long userId, NotificationListRequest req) { + public CursorPagingResponse getNotifications(Long userId, NotificationListRequest req) { List notifications = notificationRepository.findNotificationsByReceiverIdAndCursor( userId, req.getCursorId(), req.getGroupId(), req.getRead(), req.getPageRequest() ); @@ -81,7 +78,7 @@ public CursorPageResponse getNotifications(Long userId, No })) .toList(); - return CursorPageResponse.of(content, hasNext, nextCursor); + return CursorPagingResponse.of(content, hasNext, nextCursor); } /**