From 27fb73cd5d4c63d973e4f550190f20eb783e6184 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 10 Aug 2025 01:22:52 +0900 Subject: [PATCH 01/23] =?UTF-8?q?Feat:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20ser?= =?UTF-8?q?ver=20url=20=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/common/config/SwaggerConfig.java | 7 ++++++- src/main/resources/application.yml | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/common/config/SwaggerConfig.java b/src/main/java/project/flipnote/common/config/SwaggerConfig.java index 57e1f18d..bf7bae7a 100644 --- a/src/main/java/project/flipnote/common/config/SwaggerConfig.java +++ b/src/main/java/project/flipnote/common/config/SwaggerConfig.java @@ -1,5 +1,6 @@ package project.flipnote.common.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,14 +9,18 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; -import project.flipnote.common.security.jwt.JwtConstants; +import io.swagger.v3.oas.models.servers.Server; @Configuration public class SwaggerConfig { + @Value("${springdoc.server.url}") + private String serverUrl; + @Bean public OpenAPI openApi() { return new OpenAPI() + .addServersItem(new Server().url(serverUrl)) .addSecurityItem( new SecurityRequirement() .addList("access-token") diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d2cc7d20..9e9faa2d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -89,3 +89,7 @@ cloud: scope: - email - profile + +springdoc: + server: + url: http://localhost:8080 From a1f8858f9e74df9fc05e368b9ce2e8b745e9a900 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 10 Aug 2025 01:24:12 +0900 Subject: [PATCH 02/23] =?UTF-8?q?Chore:=20=EC=98=A4=ED=83=80=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 --- .../flipnote/common/security/exception/SecurityErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java b/src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java index 149c1de1..cb556b89 100644 --- a/src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java +++ b/src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java @@ -9,7 +9,7 @@ @Getter @RequiredArgsConstructor public enum SecurityErrorCode implements ErrorCode { - TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "SECURITY_0021", "토큰이 만료되었습니다."), + TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "SECURITY_001", "토큰이 만료되었습니다."), NOT_VALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "SECURITY_002", "올바르지 않은 토큰입니다."), UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "SECURITY_003", "인증이 필요합니다."), FORBIDDEN(HttpStatus.FORBIDDEN.value(), "SECURITY_004", "권한이 없습니다."); From 5734affd70802a0ead26a7fbaa9e2c161a0a3ad4 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 10 Aug 2025 01:25:08 +0900 Subject: [PATCH 03/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupInvitationController.java | 34 ++++++++++ .../docs/GroupInvitationControllerDocs.java | 17 +++++ .../group/entity/GroupGuestInvitation.java | 47 +++++++++++++ .../group/entity/GroupInvitationStatus.java | 5 ++ .../group/entity/GroupMemberInvitation.java | 47 +++++++++++++ .../group/exception/GroupErrorCode.java | 3 +- .../exception/GroupInvitationErrorCode.java | 24 +++++++ .../model/GroupInvitationCreateRequest.java | 10 +++ .../GroupGuestInvitationRepository.java | 10 +++ .../GroupMemberInvitationRepository.java | 10 +++ .../repository/GroupMemberRepository.java | 3 + .../group/service/GroupInvitationService.java | 67 +++++++++++++++++++ .../flipnote/group/service/GroupService.java | 15 +++++ .../repository/UserProfileRepository.java | 2 + .../flipnote/user/service/UserService.java | 15 +++-- 15 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 src/main/java/project/flipnote/group/controller/GroupInvitationController.java create mode 100644 src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java create mode 100644 src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java create mode 100644 src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java create mode 100644 src/main/java/project/flipnote/group/entity/GroupMemberInvitation.java create mode 100644 src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java create mode 100644 src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java create mode 100644 src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java create mode 100644 src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java create mode 100644 src/main/java/project/flipnote/group/service/GroupInvitationService.java diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java new file mode 100644 index 00000000..ef0bbf23 --- /dev/null +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java @@ -0,0 +1,34 @@ +package project.flipnote.group.controller; + +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.common.security.dto.AuthPrinciple; +import project.flipnote.group.controller.docs.GroupInvitationControllerDocs; +import project.flipnote.group.model.GroupInvitationCreateRequest; +import project.flipnote.group.service.GroupInvitationService; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/v1/groups/{groupId}/invitations") +public class GroupInvitationController implements GroupInvitationControllerDocs { + + private final GroupInvitationService groupInvitationService; + + @PostMapping + public ResponseEntity createGroupInvitation( + @PathVariable("groupId") Long groupId, + @Valid @RequestBody GroupInvitationCreateRequest req, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + groupInvitationService.createGroupInvitation(authPrinciple.userId(), groupId, req); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java new file mode 100644 index 00000000..da1c9f7d --- /dev/null +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java @@ -0,0 +1,17 @@ +package project.flipnote.group.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.common.security.dto.AuthPrinciple; +import project.flipnote.group.model.GroupInvitationCreateRequest; + +@Tag(name = "Group Invitation", description = "Group Invitation API") +public interface GroupInvitationControllerDocs { + + @Operation(summary = "그룹 초대") + ResponseEntity createGroupInvitation( + Long groupId, GroupInvitationCreateRequest req, AuthPrinciple authPrinciple + ); +} diff --git a/src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java b/src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java new file mode 100644 index 00000000..1e7ffd5b --- /dev/null +++ b/src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java @@ -0,0 +1,47 @@ +package project.flipnote.group.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import project.flipnote.common.entity.BaseEntity; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "group_guest_invitations") +public class GroupGuestInvitation extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long groupId; + + @Column(nullable = false) + private Long inviterUserId; + + @Column(nullable = false) + private String inviteeEmail; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private GroupInvitationStatus status; + + @Builder + public GroupGuestInvitation(Long groupId, Long inviterUserId, String inviteeEmail) { + this.groupId = groupId; + this.inviterUserId = inviterUserId; + this.inviteeEmail = inviteeEmail; + this.status = GroupInvitationStatus.PENDING; + } +} diff --git a/src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java b/src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java new file mode 100644 index 00000000..f4a0ffea --- /dev/null +++ b/src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java @@ -0,0 +1,5 @@ +package project.flipnote.group.entity; + +public enum GroupInvitationStatus { + PENDING, ACCEPTED, REJECTED +} diff --git a/src/main/java/project/flipnote/group/entity/GroupMemberInvitation.java b/src/main/java/project/flipnote/group/entity/GroupMemberInvitation.java new file mode 100644 index 00000000..4fe2b8cb --- /dev/null +++ b/src/main/java/project/flipnote/group/entity/GroupMemberInvitation.java @@ -0,0 +1,47 @@ +package project.flipnote.group.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import project.flipnote.common.entity.BaseEntity; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "group_member_invitations") +public class GroupMemberInvitation extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long groupId; + + @Column(nullable = false) + private Long inviterUserId; + + @Column(nullable = false) + private Long inviteeUserId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private GroupInvitationStatus status; + + @Builder + public GroupMemberInvitation(Long groupId, Long inviterUserId, Long inviteeUserId) { + this.groupId = groupId; + this.inviterUserId = inviterUserId; + this.inviteeUserId = inviteeUserId; + this.status = GroupInvitationStatus.PENDING; + } +} diff --git a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java index 3604efe4..5617905c 100644 --- a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java +++ b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java @@ -11,7 +11,8 @@ @RequiredArgsConstructor public enum GroupErrorCode implements ErrorCode { GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_002", "그룹이 존재하지 않습니다."), - INVALID_MAX_MEMBER(HttpStatus.BAD_REQUEST, "GROUP_001", "최대 인원 수는 1 이상 100 이하여야 합니다."); + INVALID_MAX_MEMBER(HttpStatus.BAD_REQUEST, "GROUP_001", "최대 인원 수는 1 이상 100 이하여야 합니다."), + USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_003", "그룹에 유저가 존재하지 않습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java b/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java new file mode 100644 index 00000000..e3bb0fa5 --- /dev/null +++ b/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java @@ -0,0 +1,24 @@ +package project.flipnote.group.exception; + + +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import project.flipnote.common.exception.ErrorCode; + +@Getter +@RequiredArgsConstructor +public enum GroupInvitationErrorCode implements ErrorCode { + ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."), + NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public int getStatus() { + return httpStatus.value(); + } +} diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java b/src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java new file mode 100644 index 00000000..30a0abf9 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java @@ -0,0 +1,10 @@ +package project.flipnote.group.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record GroupInvitationCreateRequest( + @Email @NotBlank + String email +) { +} diff --git a/src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java new file mode 100644 index 00000000..e4eb2d97 --- /dev/null +++ b/src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java @@ -0,0 +1,10 @@ +package project.flipnote.group.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import project.flipnote.group.entity.GroupGuestInvitation; + +public interface GroupGuestInvitationRepository extends JpaRepository { + + boolean existsByGroupIdAndInviteeEmail(Long groupId, String inviteeEmail); +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java new file mode 100644 index 00000000..5f30905f --- /dev/null +++ b/src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java @@ -0,0 +1,10 @@ +package project.flipnote.group.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import project.flipnote.group.entity.GroupMemberInvitation; + +public interface GroupMemberInvitationRepository extends JpaRepository { + + boolean existsByGroupIdAndInviteeUserId(Long groupId, Long inviteeUserId); +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java index c000389f..c5001b75 100644 --- a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java @@ -17,4 +17,7 @@ public interface GroupMemberRepository extends JpaRepository long countByGroup_Id(Long groupId); boolean existsByGroup_idAndUser_id(Long groupId, Long userId); + + Optional findByGroup_IdAndUser_Id(Long groupId, Long userId); + } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java new file mode 100644 index 00000000..bf274f7e --- /dev/null +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -0,0 +1,67 @@ +package project.flipnote.group.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import project.flipnote.common.exception.BizException; +import project.flipnote.group.entity.GroupGuestInvitation; +import project.flipnote.group.entity.GroupMemberInvitation; +import project.flipnote.group.entity.GroupPermissionStatus; +import project.flipnote.group.exception.GroupInvitationErrorCode; +import project.flipnote.group.model.GroupInvitationCreateRequest; +import project.flipnote.group.repository.GroupGuestInvitationRepository; +import project.flipnote.group.repository.GroupMemberInvitationRepository; +import project.flipnote.user.entity.UserProfile; +import project.flipnote.user.service.UserService; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class GroupInvitationService { + + private final UserService userService; + private final GroupMemberInvitationRepository memberInvitationRepository; + private final GroupGuestInvitationRepository guestInvitationRepository; + private final GroupService groupService; + + @Transactional + public void createGroupInvitation(Long inviterUserId, Long groupId, GroupInvitationCreateRequest req) { + if (!groupService.hasPermission(groupId, inviterUserId, GroupPermissionStatus.INVITE)) { + throw new BizException(GroupInvitationErrorCode.NO_INVITATION_PERMISSION); + } + + String inviteeEmail = req.email(); + + userService.findActiveUserByEmail(inviteeEmail).ifPresentOrElse( + inviteeUser -> handleMemberInvitation(inviterUserId, groupId, inviteeUser), + () -> handleGuestInvitation(inviterUserId, groupId, inviteeEmail) + ); + } + + private void handleMemberInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { + if (memberInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { + throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); + } + + GroupMemberInvitation invitation = GroupMemberInvitation.builder() + .groupId(groupId) + .inviterUserId(inviterUserId) + .inviteeUserId(inviteeUser.getId()) + .build(); + memberInvitationRepository.save(invitation); + } + + private void handleGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { + if (guestInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { + throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); + } + + GroupGuestInvitation invitation = GroupGuestInvitation.builder() + .groupId(groupId) + .inviterUserId(inviterUserId) + .inviteeEmail(inviteeEmail) + .build(); + guestInvitationRepository.save(invitation); + } +} diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index eba053c5..4e7049ac 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -14,6 +14,7 @@ import project.flipnote.group.entity.GroupMember; import project.flipnote.group.entity.GroupMemberRole; import project.flipnote.group.entity.GroupPermission; +import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.entity.GroupRolePermission; import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.model.GroupCreateRequest; @@ -68,6 +69,20 @@ public GroupCreateResponse create(AuthPrinciple authPrinciple, GroupCreateReques return GroupCreateResponse.from(group.getId()); } + public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) { + GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow( + () -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP) + ); + + GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); + + return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( + groupRepository.getReferenceById(groupId), + groupMember.getRole(), + groupPermission + ); + } + /* 최초 그룹 권한 설정 */ diff --git a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java index 19eb4141..6ac9eb9a 100644 --- a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -14,4 +14,6 @@ public interface UserProfileRepository extends JpaRepository boolean existsByPhone(String phone); Optional findByIdAndStatus(Long userId, UserStatus status); + + Optional findByEmailAndStatus(String email, UserStatus status); } diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 0cc8e713..c5750c63 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -1,6 +1,7 @@ package project.flipnote.user.service; import java.util.Objects; +import java.util.Optional; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -47,7 +48,7 @@ public Long createUser(UserCreateCommand command) { @Transactional public void withdraw(Long userId) { - UserProfile user = findActiveUserById(userId); + UserProfile user = findActiveUserByIdOrThrow(userId); user.withdraw(); eventPublisher.publishEvent(new UserWithdrawnEvent(userId)); @@ -55,7 +56,7 @@ public void withdraw(Long userId) { @Transactional public UserUpdateResponse update(Long userId, UserUpdateRequest req) { - UserProfile user = findActiveUserById(userId); + UserProfile user = findActiveUserByIdOrThrow(userId); String phone = req.getNormalizedPhone(); if (!Objects.equals(user.getPhone(), phone)) { @@ -68,22 +69,26 @@ public UserUpdateResponse update(Long userId, UserUpdateRequest req) { } public MyInfoResponse getMyInfo(Long userId) { - UserProfile user = findActiveUserById(userId); + UserProfile user = findActiveUserByIdOrThrow(userId); return MyInfoResponse.from(user); } public UserInfoResponse getUserInfo(Long userId) { - UserProfile user = findActiveUserById(userId); + UserProfile user = findActiveUserByIdOrThrow(userId); return UserInfoResponse.from(user); } - private UserProfile findActiveUserById(Long userId) { + private UserProfile findActiveUserByIdOrThrow(Long userId) { return userProfileRepository.findByIdAndStatus(userId, UserStatus.ACTIVE) .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); } + public Optional findActiveUserByEmail(String email) { + return userProfileRepository.findByEmailAndStatus(email, UserStatus.ACTIVE); + } + private void validateEmailDuplicate(String email) { if (userProfileRepository.existsByEmail(email)) { throw new BizException(UserErrorCode.DUPLICATE_EMAIL); From 389912fc1d5c4aaa86cb87e20d68933f708fece4 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 10 Aug 2025 17:25:01 +0900 Subject: [PATCH 04/23] =?UTF-8?q?Docs:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EA=B8=B0=EB=8A=A5=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20JavaDoc=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupInvitationController.java | 1 + .../group/service/GroupInvitationService.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java index ef0bbf23..ce154aa5 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java @@ -29,6 +29,7 @@ public ResponseEntity createGroupInvitation( @AuthenticationPrincipal AuthPrinciple authPrinciple ) { groupInvitationService.createGroupInvitation(authPrinciple.userId(), groupId, req); + return ResponseEntity.ok().build(); } } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index bf274f7e..fdd95890 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -25,6 +25,13 @@ public class GroupInvitationService { private final GroupGuestInvitationRepository guestInvitationRepository; private final GroupService groupService; + /** + * 그룹에 회원 혹은 비회원 초대 + * @param inviterUserId 초대한 회원 id + * @param groupId 초대한 그룹 id + * @param req 초대 대상 정보 + * @author 윤정환 + */ @Transactional public void createGroupInvitation(Long inviterUserId, Long groupId, GroupInvitationCreateRequest req) { if (!groupService.hasPermission(groupId, inviterUserId, GroupPermissionStatus.INVITE)) { @@ -39,6 +46,13 @@ public void createGroupInvitation(Long inviterUserId, Long groupId, GroupInvitat ); } + /** + * 그룹에 회원 초대 + * @param inviterUserId 초대한 회원 id + * @param groupId 초대한 그룹 id + * @param inviteeUser 초대 받는 user + * @author 윤정환 + */ private void handleMemberInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { if (memberInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); @@ -50,8 +64,17 @@ private void handleMemberInvitation(Long inviterUserId, Long groupId, UserProfil .inviteeUserId(inviteeUser.getId()) .build(); memberInvitationRepository.save(invitation); + + // TODO: 초대받은 회원한테 알림 전송 } + /** + * 그룹에 비회원 초대 + * @param inviterUserId 초대한 회원 id + * @param groupId 초대한 그룹 id + * @param inviteeEmail 초대 받는 비회원 email + * @author 윤정환 + */ private void handleGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { if (guestInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); @@ -63,5 +86,7 @@ private void handleGuestInvitation(Long inviterUserId, Long groupId, String invi .inviteeEmail(inviteeEmail) .build(); guestInvitationRepository.save(invitation); + + // TODO: 초대받은 비회원한테 이메일 전송 } } From 4f562fb47e03e199fd73bec6909279c45c688df4 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 10 Aug 2025 17:46:08 +0900 Subject: [PATCH 05/23] =?UTF-8?q?Refactor:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90/=EB=B9=84=ED=9A=8C=EC=9B=90=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/entity/GroupGuestInvitation.java | 47 -------------- ...erInvitation.java => GroupInvitation.java} | 10 +-- .../GroupGuestInvitationRepository.java | 10 --- .../repository/GroupInvitationRepository.java | 12 ++++ .../GroupMemberInvitationRepository.java | 10 --- .../group/service/GroupInvitationService.java | 61 +++++++++++-------- 6 files changed, 54 insertions(+), 96 deletions(-) delete mode 100644 src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java rename src/main/java/project/flipnote/group/entity/{GroupMemberInvitation.java => GroupInvitation.java} (80%) delete mode 100644 src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java create mode 100644 src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java delete mode 100644 src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java diff --git a/src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java b/src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java deleted file mode 100644 index 1e7ffd5b..00000000 --- a/src/main/java/project/flipnote/group/entity/GroupGuestInvitation.java +++ /dev/null @@ -1,47 +0,0 @@ -package project.flipnote.group.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import project.flipnote.common.entity.BaseEntity; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Entity -@Table(name = "group_guest_invitations") -public class GroupGuestInvitation extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private Long groupId; - - @Column(nullable = false) - private Long inviterUserId; - - @Column(nullable = false) - private String inviteeEmail; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private GroupInvitationStatus status; - - @Builder - public GroupGuestInvitation(Long groupId, Long inviterUserId, String inviteeEmail) { - this.groupId = groupId; - this.inviterUserId = inviterUserId; - this.inviteeEmail = inviteeEmail; - this.status = GroupInvitationStatus.PENDING; - } -} diff --git a/src/main/java/project/flipnote/group/entity/GroupMemberInvitation.java b/src/main/java/project/flipnote/group/entity/GroupInvitation.java similarity index 80% rename from src/main/java/project/flipnote/group/entity/GroupMemberInvitation.java rename to src/main/java/project/flipnote/group/entity/GroupInvitation.java index 4fe2b8cb..f34260c2 100644 --- a/src/main/java/project/flipnote/group/entity/GroupMemberInvitation.java +++ b/src/main/java/project/flipnote/group/entity/GroupInvitation.java @@ -17,8 +17,8 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -@Table(name = "group_member_invitations") -public class GroupMemberInvitation extends BaseEntity { +@Table(name = "group_invitations") +public class GroupInvitation extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -30,18 +30,20 @@ public class GroupMemberInvitation extends BaseEntity { @Column(nullable = false) private Long inviterUserId; - @Column(nullable = false) private Long inviteeUserId; + private String inviteeEmail; + @Enumerated(EnumType.STRING) @Column(nullable = false) private GroupInvitationStatus status; @Builder - public GroupMemberInvitation(Long groupId, Long inviterUserId, Long inviteeUserId) { + public GroupInvitation(Long groupId, Long inviterUserId, Long inviteeUserId, String inviteeEmail) { this.groupId = groupId; this.inviterUserId = inviterUserId; this.inviteeUserId = inviteeUserId; + this.inviteeEmail = inviteeEmail; this.status = GroupInvitationStatus.PENDING; } } diff --git a/src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java deleted file mode 100644 index e4eb2d97..00000000 --- a/src/main/java/project/flipnote/group/repository/GroupGuestInvitationRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package project.flipnote.group.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import project.flipnote.group.entity.GroupGuestInvitation; - -public interface GroupGuestInvitationRepository extends JpaRepository { - - boolean existsByGroupIdAndInviteeEmail(Long groupId, String inviteeEmail); -} diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java new file mode 100644 index 00000000..362d2919 --- /dev/null +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -0,0 +1,12 @@ +package project.flipnote.group.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import project.flipnote.group.entity.GroupInvitation; + +public interface GroupInvitationRepository extends JpaRepository { + + boolean existsByGroupIdAndInviteeUserId(Long groupId, Long inviteeUserId); + + boolean existsByGroupIdAndInviteeEmail(Long groupId, String inviteeEmail); +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java deleted file mode 100644 index 5f30905f..00000000 --- a/src/main/java/project/flipnote/group/repository/GroupMemberInvitationRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package project.flipnote.group.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import project.flipnote.group.entity.GroupMemberInvitation; - -public interface GroupMemberInvitationRepository extends JpaRepository { - - boolean existsByGroupIdAndInviteeUserId(Long groupId, 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 fdd95890..44632d0e 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -5,13 +5,11 @@ import lombok.RequiredArgsConstructor; import project.flipnote.common.exception.BizException; -import project.flipnote.group.entity.GroupGuestInvitation; -import project.flipnote.group.entity.GroupMemberInvitation; +import project.flipnote.group.entity.GroupInvitation; import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.exception.GroupInvitationErrorCode; import project.flipnote.group.model.GroupInvitationCreateRequest; -import project.flipnote.group.repository.GroupGuestInvitationRepository; -import project.flipnote.group.repository.GroupMemberInvitationRepository; +import project.flipnote.group.repository.GroupInvitationRepository; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.service.UserService; @@ -21,71 +19,84 @@ public class GroupInvitationService { private final UserService userService; - private final GroupMemberInvitationRepository memberInvitationRepository; - private final GroupGuestInvitationRepository guestInvitationRepository; + private final GroupInvitationRepository groupInvitationRepository; private final GroupService groupService; /** * 그룹에 회원 혹은 비회원 초대 + * * @param inviterUserId 초대한 회원 id - * @param groupId 초대한 그룹 id - * @param req 초대 대상 정보 + * @param groupId 초대한 그룹 id + * @param req 초대 대상 정보 * @author 윤정환 */ @Transactional public void createGroupInvitation(Long inviterUserId, Long groupId, GroupInvitationCreateRequest req) { - if (!groupService.hasPermission(groupId, inviterUserId, GroupPermissionStatus.INVITE)) { - throw new BizException(GroupInvitationErrorCode.NO_INVITATION_PERMISSION); - } + validateGroupInvitePermission(inviterUserId, groupId); String inviteeEmail = req.email(); userService.findActiveUserByEmail(inviteeEmail).ifPresentOrElse( - inviteeUser -> handleMemberInvitation(inviterUserId, groupId, inviteeUser), - () -> handleGuestInvitation(inviterUserId, groupId, inviteeEmail) + inviteeUser -> createUserInvitation(inviterUserId, groupId, inviteeUser), + () -> createGuestInvitation(inviterUserId, groupId, 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); + } + } + /** * 그룹에 회원 초대 + * * @param inviterUserId 초대한 회원 id - * @param groupId 초대한 그룹 id - * @param inviteeUser 초대 받는 user + * @param groupId 초대한 그룹 id + * @param inviteeUser 초대 받는 user * @author 윤정환 */ - private void handleMemberInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { - if (memberInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { + private void createUserInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { + if (groupInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } - GroupMemberInvitation invitation = GroupMemberInvitation.builder() + GroupInvitation invitation = GroupInvitation.builder() .groupId(groupId) .inviterUserId(inviterUserId) .inviteeUserId(inviteeUser.getId()) .build(); - memberInvitationRepository.save(invitation); + groupInvitationRepository.save(invitation); // TODO: 초대받은 회원한테 알림 전송 } /** * 그룹에 비회원 초대 + * * @param inviterUserId 초대한 회원 id - * @param groupId 초대한 그룹 id - * @param inviteeEmail 초대 받는 비회원 email + * @param groupId 초대한 그룹 id + * @param inviteeEmail 초대 받는 비회원 email * @author 윤정환 */ - private void handleGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { - if (guestInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { + private void createGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { + if (groupInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } - GroupGuestInvitation invitation = GroupGuestInvitation.builder() + GroupInvitation invitation = GroupInvitation.builder() .groupId(groupId) .inviterUserId(inviterUserId) .inviteeEmail(inviteeEmail) .build(); - guestInvitationRepository.save(invitation); + groupInvitationRepository.save(invitation); // TODO: 초대받은 비회원한테 이메일 전송 } From 1c8cf19ab68e1c96c87e93b12917423f420f3094 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 10 Aug 2025 18:11:52 +0900 Subject: [PATCH 06/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=B7=A8=EC=86=8C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupInvitationController.java | 14 +++++- .../exception/GroupInvitationErrorCode.java | 4 +- .../repository/GroupInvitationRepository.java | 5 +++ .../group/service/GroupInvitationService.java | 43 +++++++++++++++---- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java index ce154aa5..972d2056 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java @@ -2,6 +2,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -28,7 +29,18 @@ public ResponseEntity createGroupInvitation( @Valid @RequestBody GroupInvitationCreateRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { - groupInvitationService.createGroupInvitation(authPrinciple.userId(), groupId, req); + groupInvitationService.createGroupInvitation(authPrinciple, groupId, req); + + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{invitationId}") + public ResponseEntity deleteGroupInvitation( + @PathVariable("groupId") Long groupId, + @PathVariable("invitationId") Long invitationId, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + groupInvitationService.deleteGroupInvitation(authPrinciple.userId(), groupId, invitationId); return ResponseEntity.ok().build(); } diff --git a/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java b/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java index e3bb0fa5..21575f39 100644 --- a/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java +++ b/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java @@ -11,7 +11,9 @@ @RequiredArgsConstructor public enum GroupInvitationErrorCode implements ErrorCode { ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."), - NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."); + NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."), + INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."), + CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index 362d2919..79606633 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -1,12 +1,17 @@ package project.flipnote.group.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import project.flipnote.group.entity.GroupInvitation; +import project.flipnote.group.entity.GroupInvitationStatus; public interface GroupInvitationRepository extends JpaRepository { boolean existsByGroupIdAndInviteeUserId(Long groupId, Long inviteeUserId); boolean existsByGroupIdAndInviteeEmail(Long groupId, String inviteeEmail); + + Optional findByIdAndStatus(Long id, GroupInvitationStatus status); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 44632d0e..3e05f223 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -1,11 +1,15 @@ package project.flipnote.group.service; +import java.util.Objects; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import project.flipnote.common.exception.BizException; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.entity.GroupInvitation; +import project.flipnote.group.entity.GroupInvitationStatus; import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.exception.GroupInvitationErrorCode; import project.flipnote.group.model.GroupInvitationCreateRequest; @@ -25,16 +29,21 @@ public class GroupInvitationService { /** * 그룹에 회원 혹은 비회원 초대 * - * @param inviterUserId 초대한 회원 id - * @param groupId 초대한 그룹 id + * @param authPrinciple 초대한 회원 인증 정보 + * @param groupId 초대한 그룹 ID * @param req 초대 대상 정보 * @author 윤정환 */ @Transactional - public void createGroupInvitation(Long inviterUserId, Long groupId, GroupInvitationCreateRequest req) { + public void createGroupInvitation(AuthPrinciple authPrinciple, Long groupId, GroupInvitationCreateRequest req) { + Long inviterUserId = authPrinciple.userId(); validateGroupInvitePermission(inviterUserId, groupId); + String inviterUserEmail = authPrinciple.email(); String inviteeEmail = req.email(); + if (Objects.equals(inviterUserEmail, inviteeEmail)) { + throw new BizException(GroupInvitationErrorCode.CANNOT_INVITE_SELF); + } userService.findActiveUserByEmail(inviteeEmail).ifPresentOrElse( inviteeUser -> createUserInvitation(inviterUserId, groupId, inviteeUser), @@ -42,10 +51,28 @@ public void createGroupInvitation(Long inviterUserId, Long groupId, GroupInvitat ); } + /** + * 그룹 초대를 취소 + * + * @param userId 초대를 취소하는 회원 ID + * @param groupId 초대가 속한 그룹 ID + * @param invitationId 취소할 초대의 ID + */ + @Transactional + public void deleteGroupInvitation(Long userId, Long groupId, Long invitationId) { + validateGroupInvitePermission(userId, groupId); + + GroupInvitation invitation = groupInvitationRepository + .findByIdAndStatus(invitationId, GroupInvitationStatus.PENDING) + .orElseThrow(() -> new BizException(GroupInvitationErrorCode.INVITATION_NOT_FOUND)); + + groupInvitationRepository.delete(invitation); + } + /** * 그룹 초대 권한을 검증 * - * @param userId 권한을 검증할 사용자 ID + * @param userId 권한을 검증할 회원 ID * @param groupId 검증할 그룹 ID * @author 윤정환 */ @@ -58,8 +85,8 @@ private void validateGroupInvitePermission(Long userId, Long groupId) { /** * 그룹에 회원 초대 * - * @param inviterUserId 초대한 회원 id - * @param groupId 초대한 그룹 id + * @param inviterUserId 초대한 회원 ID + * @param groupId 초대한 그룹 ID * @param inviteeUser 초대 받는 user * @author 윤정환 */ @@ -81,8 +108,8 @@ private void createUserInvitation(Long inviterUserId, Long groupId, UserProfile /** * 그룹에 비회원 초대 * - * @param inviterUserId 초대한 회원 id - * @param groupId 초대한 그룹 id + * @param inviterUserId 초대한 회원 ID + * @param groupId 초대한 그룹 ID * @param inviteeEmail 초대 받는 비회원 email * @author 윤정환 */ From a83d96784a6ca63c177d14a9be99fe33a91f258c Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 10 Aug 2025 22:34:50 +0900 Subject: [PATCH 07/23] =?UTF-8?q?Docs:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=B7=A8=EC=86=8C=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/controller/docs/GroupInvitationControllerDocs.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java index da1c9f7d..ceb1a4a4 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java @@ -14,4 +14,7 @@ public interface GroupInvitationControllerDocs { ResponseEntity createGroupInvitation( Long groupId, GroupInvitationCreateRequest req, AuthPrinciple authPrinciple ); + + @Operation(summary = "그룹 초대 취소") + ResponseEntity deleteGroupInvitation(Long groupId, Long invitationId, AuthPrinciple authPrinciple); } From 0de90c6928d19139a256911ee2ca092338094cce Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 11 Aug 2025 15:11:39 +0900 Subject: [PATCH 08/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupInvitationController.java | 14 ++++++ .../group/entity/GroupInvitation.java | 4 ++ .../model/GroupInvitationRespondRequest.java | 17 +++++++ .../model/GroupInvitationResponseStatus.java | 5 +++ .../repository/GroupInvitationRepository.java | 3 ++ .../group/service/GroupInvitationService.java | 45 ++++++++++++++++++- 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java create mode 100644 src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java index 972d2056..0381f9d0 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java @@ -3,6 +3,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -14,6 +15,7 @@ import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.controller.docs.GroupInvitationControllerDocs; import project.flipnote.group.model.GroupInvitationCreateRequest; +import project.flipnote.group.model.GroupInvitationRespondRequest; import project.flipnote.group.service.GroupInvitationService; @RequiredArgsConstructor @@ -44,4 +46,16 @@ public ResponseEntity deleteGroupInvitation( return ResponseEntity.ok().build(); } + + @PatchMapping("/{invitationId}") + public ResponseEntity respondToGroupInvitation( + @PathVariable("groupId") Long groupId, + @PathVariable("invitationId") Long invitationId, + @Valid @RequestBody GroupInvitationRespondRequest req, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + groupInvitationService.respondToGroupInvitation(authPrinciple.userId(), groupId, invitationId, req); + + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/project/flipnote/group/entity/GroupInvitation.java b/src/main/java/project/flipnote/group/entity/GroupInvitation.java index f34260c2..652a3d99 100644 --- a/src/main/java/project/flipnote/group/entity/GroupInvitation.java +++ b/src/main/java/project/flipnote/group/entity/GroupInvitation.java @@ -46,4 +46,8 @@ public GroupInvitation(Long groupId, Long inviterUserId, Long inviteeUserId, Str this.inviteeEmail = inviteeEmail; this.status = GroupInvitationStatus.PENDING; } + + public void respond(GroupInvitationStatus status) { + this.status = status; + } } diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java b/src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java new file mode 100644 index 00000000..656e6510 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java @@ -0,0 +1,17 @@ +package project.flipnote.group.model; + +import jakarta.validation.constraints.NotNull; +import project.flipnote.group.entity.GroupInvitationStatus; + +public record GroupInvitationRespondRequest( + @NotNull + GroupInvitationResponseStatus status +) { + + public GroupInvitationStatus toEntityStatus() { + return switch (status) { + case ACCEPTED -> GroupInvitationStatus.ACCEPTED; + case REJECTED -> GroupInvitationStatus.REJECTED; + }; + } +} diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java b/src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java new file mode 100644 index 00000000..f74f710f --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java @@ -0,0 +1,5 @@ +package project.flipnote.group.model; + +public enum GroupInvitationResponseStatus { + ACCEPTED, REJECTED; +} diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index 79606633..fe602e7d 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -1,5 +1,6 @@ package project.flipnote.group.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -14,4 +15,6 @@ public interface GroupInvitationRepository extends JpaRepository findByIdAndStatus(Long id, GroupInvitationStatus status); + + Optional findByIdAndGroupIdAndInviteeUserIdAndStatus(Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 3e05f223..2b0a0f9e 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -5,15 +5,21 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import project.flipnote.common.exception.BizException; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.entity.GroupInvitation; import project.flipnote.group.entity.GroupInvitationStatus; +import project.flipnote.group.entity.GroupMember; +import project.flipnote.group.entity.GroupMemberRole; import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.exception.GroupInvitationErrorCode; import project.flipnote.group.model.GroupInvitationCreateRequest; +import project.flipnote.group.model.GroupInvitationRespondRequest; import project.flipnote.group.repository.GroupInvitationRepository; +import project.flipnote.group.repository.GroupMemberRepository; +import project.flipnote.group.repository.GroupRepository; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.service.UserService; @@ -25,6 +31,9 @@ public class GroupInvitationService { private final UserService userService; private final GroupInvitationRepository groupInvitationRepository; private final GroupService groupService; + private final GroupRepository groupRepository; + private final GroupMemberRepository groupMemberRepository; + private final EntityManager em; /** * 그룹에 회원 혹은 비회원 초대 @@ -55,7 +64,7 @@ public void createGroupInvitation(AuthPrinciple authPrinciple, Long groupId, Gro * 그룹 초대를 취소 * * @param userId 초대를 취소하는 회원 ID - * @param groupId 초대가 속한 그룹 ID + * @param groupId 초대한 그룹 ID * @param invitationId 취소할 초대의 ID */ @Transactional @@ -69,6 +78,40 @@ public void deleteGroupInvitation(Long userId, Long groupId, Long invitationId) groupInvitationRepository.delete(invitation); } + /** + * 그룹 초대에 응답 + * + * @param inviteeUserId 초대를 받은 회원 ID + * @param groupId 초대한 그룹 ID + * @param invitationId 응답할 초대의 ID + * @param req 초대에 응답할 정보 + */ + @Transactional + public void respondToGroupInvitation( + Long inviteeUserId, + Long groupId, + Long invitationId, + GroupInvitationRespondRequest req + ) { + GroupInvitation invitation = groupInvitationRepository.findByIdAndGroupIdAndInviteeUserIdAndStatus( + invitationId, groupId, inviteeUserId, GroupInvitationStatus.PENDING + ) + .orElseThrow(() -> new BizException(GroupInvitationErrorCode.INVITATION_NOT_FOUND)); + + invitation.respond(req.toEntityStatus()); + + if (Objects.equals(invitation.getStatus(), GroupInvitationStatus.ACCEPTED)) { + // TODO: GroupMember 에서 group과 user의 id만 가지고 있도록 수정 + GroupMember groupMember = GroupMember.builder() + .group(groupRepository.getReferenceById(groupId)) + .user(em.getReference(UserProfile.class, inviteeUserId)) + .role(GroupMemberRole.MEMBER) + .build(); + + groupMemberRepository.save(groupMember); + } + } + /** * 그룹 초대 권한을 검증 * From e26fe13ab82de9fce71ce14db39a8ba00246af8a Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 11 Aug 2025 15:16:19 +0900 Subject: [PATCH 09/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?id=EB=A5=BC=20=EC=9D=91=EB=8B=B5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupInvitationController.java | 8 +++++--- .../docs/GroupInvitationControllerDocs.java | 3 ++- .../model/GroupInvitationCreateResponse.java | 6 ++++++ .../group/service/GroupInvitationService.java | 20 ++++++++++++------- 4 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java index 0381f9d0..62af5c50 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java @@ -1,5 +1,6 @@ package project.flipnote.group.controller; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -15,6 +16,7 @@ import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.controller.docs.GroupInvitationControllerDocs; import project.flipnote.group.model.GroupInvitationCreateRequest; +import project.flipnote.group.model.GroupInvitationCreateResponse; import project.flipnote.group.model.GroupInvitationRespondRequest; import project.flipnote.group.service.GroupInvitationService; @@ -26,14 +28,14 @@ public class GroupInvitationController implements GroupInvitationControllerDocs private final GroupInvitationService groupInvitationService; @PostMapping - public ResponseEntity createGroupInvitation( + public ResponseEntity createGroupInvitation( @PathVariable("groupId") Long groupId, @Valid @RequestBody GroupInvitationCreateRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { - groupInvitationService.createGroupInvitation(authPrinciple, groupId, req); + GroupInvitationCreateResponse res = groupInvitationService.createGroupInvitation(authPrinciple, groupId, req); - return ResponseEntity.ok().build(); + return ResponseEntity.status(HttpStatus.CREATED).body(res); } @DeleteMapping("/{invitationId}") diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java index ceb1a4a4..ded61a13 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java @@ -6,12 +6,13 @@ import io.swagger.v3.oas.annotations.tags.Tag; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.model.GroupInvitationCreateRequest; +import project.flipnote.group.model.GroupInvitationCreateResponse; @Tag(name = "Group Invitation", description = "Group Invitation API") public interface GroupInvitationControllerDocs { @Operation(summary = "그룹 초대") - ResponseEntity createGroupInvitation( + ResponseEntity createGroupInvitation( Long groupId, GroupInvitationCreateRequest req, AuthPrinciple authPrinciple ); diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java b/src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java new file mode 100644 index 00000000..8f98f64e --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java @@ -0,0 +1,6 @@ +package project.flipnote.group.model; + +public record GroupInvitationCreateResponse( + Long invitationId +) { +} diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 2b0a0f9e..8481c384 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -16,6 +16,7 @@ import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.exception.GroupInvitationErrorCode; import project.flipnote.group.model.GroupInvitationCreateRequest; +import project.flipnote.group.model.GroupInvitationCreateResponse; import project.flipnote.group.model.GroupInvitationRespondRequest; import project.flipnote.group.repository.GroupInvitationRepository; import project.flipnote.group.repository.GroupMemberRepository; @@ -44,7 +45,7 @@ public class GroupInvitationService { * @author 윤정환 */ @Transactional - public void createGroupInvitation(AuthPrinciple authPrinciple, Long groupId, GroupInvitationCreateRequest req) { + public GroupInvitationCreateResponse createGroupInvitation(AuthPrinciple authPrinciple, Long groupId, GroupInvitationCreateRequest req) { Long inviterUserId = authPrinciple.userId(); validateGroupInvitePermission(inviterUserId, groupId); @@ -54,10 +55,11 @@ public void createGroupInvitation(AuthPrinciple authPrinciple, Long groupId, Gro throw new BizException(GroupInvitationErrorCode.CANNOT_INVITE_SELF); } - userService.findActiveUserByEmail(inviteeEmail).ifPresentOrElse( - inviteeUser -> createUserInvitation(inviterUserId, groupId, inviteeUser), - () -> createGuestInvitation(inviterUserId, groupId, inviteeEmail) - ); + Long invitationId = userService.findActiveUserByEmail(inviteeEmail) + .map(inviteeUser -> createUserInvitation(inviterUserId, groupId, inviteeUser)) + .orElseGet(() -> createGuestInvitation(inviterUserId, groupId, inviteeEmail)); + + return new GroupInvitationCreateResponse(invitationId); } /** @@ -133,7 +135,7 @@ private void validateGroupInvitePermission(Long userId, Long groupId) { * @param inviteeUser 초대 받는 user * @author 윤정환 */ - private void createUserInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { + private Long createUserInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { if (groupInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } @@ -146,6 +148,8 @@ private void createUserInvitation(Long inviterUserId, Long groupId, UserProfile groupInvitationRepository.save(invitation); // TODO: 초대받은 회원한테 알림 전송 + + return invitation.getId(); } /** @@ -156,7 +160,7 @@ private void createUserInvitation(Long inviterUserId, Long groupId, UserProfile * @param inviteeEmail 초대 받는 비회원 email * @author 윤정환 */ - private void createGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { + private Long createGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { if (groupInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } @@ -169,5 +173,7 @@ private void createGuestInvitation(Long inviterUserId, Long groupId, String invi groupInvitationRepository.save(invitation); // TODO: 초대받은 비회원한테 이메일 전송 + + return invitation.getId(); } } From 5d3a0a79a8521b35589690ba6962076a69973f00 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 11 Aug 2025 21:43:56 +0900 Subject: [PATCH 10/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/response/PageResponse.java | 32 +++++++++++++++++ .../controller/GroupInvitationController.java | 17 +++++++++ .../docs/GroupInvitationControllerDocs.java | 16 +++++++++ .../group/model/GroupInvitationResponse.java | 30 ++++++++++++++++ .../flipnote/group/model/UserIdNickname.java | 6 ++++ .../repository/GroupInvitationRepository.java | 5 ++- .../group/service/GroupInvitationService.java | 36 ++++++++++++++++++- .../repository/UserProfileRepository.java | 4 +++ .../flipnote/user/service/UserService.java | 11 ++++++ 9 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/main/java/project/flipnote/common/response/PageResponse.java create mode 100644 src/main/java/project/flipnote/group/model/GroupInvitationResponse.java create mode 100644 src/main/java/project/flipnote/group/model/UserIdNickname.java diff --git a/src/main/java/project/flipnote/common/response/PageResponse.java b/src/main/java/project/flipnote/common/response/PageResponse.java new file mode 100644 index 00000000..5539602b --- /dev/null +++ b/src/main/java/project/flipnote/common/response/PageResponse.java @@ -0,0 +1,32 @@ +package project.flipnote.common.response; + +import java.util.List; + +import org.springframework.data.domain.Page; + +public record PageResponse( + List content, + int page, + int size, + long totalElements, + int totalPages, + boolean first, + boolean last, + boolean hasNext, + boolean hasPrevious +) { + + public static PageResponse from(Page page) { + return new PageResponse<>( + page.getContent(), + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages(), + page.isFirst(), + page.isLast(), + page.hasNext(), + page.hasPrevious() + ); + } +} diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java index 62af5c50..abbfe472 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java @@ -4,20 +4,24 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.controller.docs.GroupInvitationControllerDocs; import project.flipnote.group.model.GroupInvitationCreateRequest; import project.flipnote.group.model.GroupInvitationCreateResponse; import project.flipnote.group.model.GroupInvitationRespondRequest; +import project.flipnote.group.model.GroupInvitationResponse; import project.flipnote.group.service.GroupInvitationService; @RequiredArgsConstructor @@ -60,4 +64,17 @@ public ResponseEntity respondToGroupInvitation( return ResponseEntity.ok().build(); } + + @GetMapping + public ResponseEntity> getGroupInvitations( + @PathVariable("groupId") Long groupId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + PageResponse res + = groupInvitationService.getGroupInvitations(authPrinciple.userId(), groupId, page, size); + + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java index ded61a13..bceea833 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java @@ -4,9 +4,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import project.flipnote.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.model.GroupInvitationCreateRequest; import project.flipnote.group.model.GroupInvitationCreateResponse; +import project.flipnote.group.model.GroupInvitationRespondRequest; +import project.flipnote.group.model.GroupInvitationResponse; @Tag(name = "Group Invitation", description = "Group Invitation API") public interface GroupInvitationControllerDocs { @@ -18,4 +21,17 @@ ResponseEntity createGroupInvitation( @Operation(summary = "그룹 초대 취소") ResponseEntity deleteGroupInvitation(Long groupId, Long invitationId, AuthPrinciple authPrinciple); + + @Operation(summary = "그룹 초대 응답") + ResponseEntity respondToGroupInvitation( + Long groupId, Long invitationId, GroupInvitationRespondRequest req, AuthPrinciple authPrinciple + ); + + @Operation(summary = "그룹 초대 목록 조회") + ResponseEntity> getGroupInvitations( + Long groupId, + int page, + int size, + AuthPrinciple authPrinciple + ); } diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationResponse.java b/src/main/java/project/flipnote/group/model/GroupInvitationResponse.java new file mode 100644 index 00000000..de5cba66 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInvitationResponse.java @@ -0,0 +1,30 @@ +package project.flipnote.group.model; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import project.flipnote.group.entity.GroupInvitation; + +public record GroupInvitationResponse( + Long invitationId, + Long inviterUserId, + Long inviteeUserId, + String inviteeEmail, + String inviteeNickname, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime createdAt +) { + + public static GroupInvitationResponse from(GroupInvitation invitation, String inviteeNickname) { + return new GroupInvitationResponse( + invitation.getId(), + invitation.getInviterUserId(), + invitation.getInviteeUserId(), + invitation.getInviteeEmail(), + inviteeNickname, + invitation.getCreatedAt() + ); + } +} diff --git a/src/main/java/project/flipnote/group/model/UserIdNickname.java b/src/main/java/project/flipnote/group/model/UserIdNickname.java new file mode 100644 index 00000000..595a2330 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/UserIdNickname.java @@ -0,0 +1,6 @@ +package project.flipnote.group.model; + +public interface UserIdNickname { + Long getId(); + String getNickname(); +} diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index fe602e7d..e7c5dae6 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -1,8 +1,9 @@ package project.flipnote.group.repository; -import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import project.flipnote.group.entity.GroupInvitation; @@ -17,4 +18,6 @@ public interface GroupInvitationRepository extends JpaRepository findByIdAndStatus(Long id, GroupInvitationStatus status); Optional findByIdAndGroupIdAndInviteeUserIdAndStatus(Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status); + + Page findAllByGroupIdAndStatus(Long groupId, GroupInvitationStatus status, Pageable pageable); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 8481c384..58b4729f 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -1,13 +1,18 @@ package project.flipnote.group.service; +import java.util.List; +import java.util.Map; import java.util.Objects; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import project.flipnote.common.exception.BizException; +import project.flipnote.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.entity.GroupInvitation; import project.flipnote.group.entity.GroupInvitationStatus; @@ -18,6 +23,7 @@ import project.flipnote.group.model.GroupInvitationCreateRequest; import project.flipnote.group.model.GroupInvitationCreateResponse; import project.flipnote.group.model.GroupInvitationRespondRequest; +import project.flipnote.group.model.GroupInvitationResponse; import project.flipnote.group.repository.GroupInvitationRepository; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupRepository; @@ -45,7 +51,8 @@ public class GroupInvitationService { * @author 윤정환 */ @Transactional - public GroupInvitationCreateResponse createGroupInvitation(AuthPrinciple authPrinciple, Long groupId, GroupInvitationCreateRequest req) { + public GroupInvitationCreateResponse createGroupInvitation(AuthPrinciple authPrinciple, Long groupId, + GroupInvitationCreateRequest req) { Long inviterUserId = authPrinciple.userId(); validateGroupInvitePermission(inviterUserId, groupId); @@ -114,6 +121,32 @@ public void respondToGroupInvitation( } } + public PageResponse getGroupInvitations(Long userId, Long groupId, int page, int size) { + 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 + .findAllByGroupIdAndStatus(groupId, GroupInvitationStatus.PENDING, pageRequest); + + List inviteeUserIds = invitationPage.getContent() + .stream() + .filter(Objects::nonNull) + .map(GroupInvitation::getInviteeUserId) + .toList(); + Map idAndNicknames = userService.getIdAndNicknames(inviteeUserIds); + + Page res = invitationPage.map( + (invitation) -> GroupInvitationResponse.from( + invitation, idAndNicknames.getOrDefault(invitation.getInviteeUserId(), "") + ) + ); + + return PageResponse.from(res); + } + /** * 그룹 초대 권한을 검증 * @@ -144,6 +177,7 @@ private Long createUserInvitation(Long inviterUserId, Long groupId, UserProfile .groupId(groupId) .inviterUserId(inviterUserId) .inviteeUserId(inviteeUser.getId()) + .inviteeEmail(inviteeUser.getEmail()) .build(); groupInvitationRepository.save(invitation); diff --git a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java index 6ac9eb9a..4d505131 100644 --- a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -1,9 +1,11 @@ package project.flipnote.user.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import project.flipnote.group.model.UserIdNickname; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; @@ -16,4 +18,6 @@ public interface UserProfileRepository extends JpaRepository Optional findByIdAndStatus(Long userId, UserStatus status); Optional findByEmailAndStatus(String email, UserStatus status); + + List findIdAndNicknameByIdIn(List ids); } diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index c5750c63..0ccb18a1 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -1,7 +1,10 @@ package project.flipnote.user.service; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -11,6 +14,7 @@ import project.flipnote.common.dto.UserCreateCommand; import project.flipnote.common.event.UserWithdrawnEvent; import project.flipnote.common.exception.BizException; +import project.flipnote.group.model.UserIdNickname; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; @@ -104,4 +108,11 @@ public void validatePhoneDuplicate(String phone) { throw new BizException(UserErrorCode.DUPLICATE_PHONE); } } + + public Map getIdAndNicknames(List inviteeUserIds) { + List idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds); + + return idAndNicknames.stream() + .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); + } } From e85cb58eb7f4a92236b550f54d96b1a086fdbd13 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 11 Aug 2025 22:06:22 +0900 Subject: [PATCH 11/23] =?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=EC=88=98=EB=9D=BD=EB=90=98?= =?UTF-8?q?=EB=8A=94=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 | 3 ++ .../common/event/UserRegisteredEvent.java | 7 ++++ .../listener/UserRegisteredEventListener.java | 36 ++++++++++++++++ .../repository/GroupInvitationRepository.java | 3 ++ .../group/service/GroupInvitationService.java | 42 +++++++++++++++---- 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/main/java/project/flipnote/common/event/UserRegisteredEvent.java create mode 100644 src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index 7bfabfc1..25eec76c 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -35,6 +35,7 @@ import project.flipnote.auth.util.VerificationCodeGenerator; import project.flipnote.common.config.ClientProperties; import project.flipnote.common.dto.UserCreateCommand; +import project.flipnote.common.event.UserRegisteredEvent; import project.flipnote.common.exception.BizException; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtComponent; @@ -77,6 +78,8 @@ public UserRegisterResponse register(UserRegisterRequest req) { .build(); userAuthRepository.save(userAuth); + eventPublisher.publishEvent(new UserRegisteredEvent(userId, email)); + return UserRegisterResponse.from(userId); } diff --git a/src/main/java/project/flipnote/common/event/UserRegisteredEvent.java b/src/main/java/project/flipnote/common/event/UserRegisteredEvent.java new file mode 100644 index 00000000..4ec670c9 --- /dev/null +++ b/src/main/java/project/flipnote/common/event/UserRegisteredEvent.java @@ -0,0 +1,7 @@ +package project.flipnote.common.event; + +public record UserRegisteredEvent( + Long userId, + String email +) { +} diff --git a/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java b/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java new file mode 100644 index 00000000..106b22a6 --- /dev/null +++ b/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java @@ -0,0 +1,36 @@ +package project.flipnote.group.listener; + +import org.springframework.context.event.EventListener; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.common.event.UserRegisteredEvent; +import project.flipnote.group.service.GroupInvitationService; + +@Slf4j +@RequiredArgsConstructor +@Component +public class UserRegisteredEventListener { + + private final GroupInvitationService groupInvitationService; + + @Async + @Retryable( + maxAttempts = 3, + backoff = @Backoff(delay = 2000, multiplier = 2) + ) + @EventListener + public void HandleUserRegisteredEvent(UserRegisteredEvent event) { + groupInvitationService.acceptPendingInvitationsOnRegister(event.userId(), event.email()); + } + + @Recover + public void recover(Exception ex, UserRegisteredEvent event) { + log.error("회원가입 후속 처리 예외 발생: userId={}, email={}", event.userId(), event.email(), ex); + } +} diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index e7c5dae6..7918fc31 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -1,5 +1,6 @@ package project.flipnote.group.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -20,4 +21,6 @@ public interface GroupInvitationRepository extends JpaRepository findByIdAndGroupIdAndInviteeUserIdAndStatus(Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status); Page findAllByGroupIdAndStatus(Long groupId, GroupInvitationStatus status, Pageable pageable); + + List findAllByInviteeEmailAndStatus(String InviteeEmail, GroupInvitationStatus status); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 58b4729f..5c22815f 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -111,13 +111,7 @@ public void respondToGroupInvitation( if (Objects.equals(invitation.getStatus(), GroupInvitationStatus.ACCEPTED)) { // TODO: GroupMember 에서 group과 user의 id만 가지고 있도록 수정 - GroupMember groupMember = GroupMember.builder() - .group(groupRepository.getReferenceById(groupId)) - .user(em.getReference(UserProfile.class, inviteeUserId)) - .role(GroupMemberRole.MEMBER) - .build(); - - groupMemberRepository.save(groupMember); + addGroupMember(inviteeUserId, groupId); } } @@ -147,6 +141,24 @@ public PageResponse getGroupInvitations(Long userId, Lo return PageResponse.from(res); } + /** + * 회원가입시 비회원 그룹 초대를 수락 + * + * @param inviteeUserId 초대 받은 회원 ID + * @param inviteeEmail 초대 받은 회원 email + */ + @Transactional + public void acceptPendingInvitationsOnRegister(Long inviteeUserId, String inviteeEmail) { + List invitations = groupInvitationRepository + .findAllByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING); + + for (GroupInvitation invitation : invitations) { + invitation.respond(GroupInvitationStatus.ACCEPTED); + + addGroupMember(inviteeUserId, invitation.getGroupId()); + } + } + /** * 그룹 초대 권한을 검증 * @@ -210,4 +222,20 @@ private Long createGuestInvitation(Long inviterUserId, Long groupId, String invi return invitation.getId(); } + + /** + * 그룹에 회원 추가 + * + * @param inviteeUserId 초대 받는 회원 ID + * @param groupId 초대한 그룹 ID + */ + private void addGroupMember(Long inviteeUserId, Long groupId) { + GroupMember groupMember = GroupMember.builder() + .group(groupRepository.getReferenceById(groupId)) + .user(em.getReference(UserProfile.class, inviteeUserId)) + .role(GroupMemberRole.MEMBER) + .build(); + + groupMemberRepository.save(groupMember); + } } From 933340dd911768dad0fdf21fec296d5444400569 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 11 Aug 2025 22:13:14 +0900 Subject: [PATCH 12/23] =?UTF-8?q?Docs:=20GroupInvitationService=20JavaDoc?= =?UTF-8?q?=20=EB=B9=A0=EC=A7=84=20=EB=B6=80=EB=B6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/service/GroupInvitationService.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 5c22815f..c83b9456 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -75,6 +75,7 @@ public GroupInvitationCreateResponse createGroupInvitation(AuthPrinciple authPri * @param userId 초대를 취소하는 회원 ID * @param groupId 초대한 그룹 ID * @param invitationId 취소할 초대의 ID + * @author 윤정환 */ @Transactional public void deleteGroupInvitation(Long userId, Long groupId, Long invitationId) { @@ -94,6 +95,7 @@ public void deleteGroupInvitation(Long userId, Long groupId, Long invitationId) * @param groupId 초대한 그룹 ID * @param invitationId 응답할 초대의 ID * @param req 초대에 응답할 정보 + * @author 윤정환 */ @Transactional public void respondToGroupInvitation( @@ -115,6 +117,16 @@ public void respondToGroupInvitation( } } + /** + * 그룹 초대 목록을 페이징하여 조회 + * + * @param userId 초대 목록을 조회하는 회원 ID + * @param groupId 초대한 그룹 ID + * @param page 페이지 번호 + * @param size 페이지 크기 + * @return 페이징된 그룹 초대 목록 응답 + * @author 윤정환 + */ public PageResponse getGroupInvitations(Long userId, Long groupId, int page, int size) { if (!groupService.hasPermission(groupId, userId, GroupPermissionStatus.INVITE)) { throw new BizException(GroupInvitationErrorCode.NO_INVITATION_PERMISSION); @@ -145,7 +157,8 @@ public PageResponse getGroupInvitations(Long userId, Lo * 회원가입시 비회원 그룹 초대를 수락 * * @param inviteeUserId 초대 받은 회원 ID - * @param inviteeEmail 초대 받은 회원 email + * @param inviteeEmail 초대 받은 회원 email + * @author 윤정환 */ @Transactional public void acceptPendingInvitationsOnRegister(Long inviteeUserId, String inviteeEmail) { @@ -227,7 +240,8 @@ private Long createGuestInvitation(Long inviterUserId, Long groupId, String invi * 그룹에 회원 추가 * * @param inviteeUserId 초대 받는 회원 ID - * @param groupId 초대한 그룹 ID + * @param groupId 초대한 그룹 ID + * @author 윤정환 */ private void addGroupMember(Long inviteeUserId, Long groupId) { GroupMember groupMember = GroupMember.builder() From c72531303d28cc7af3b89d62766c6b30b7f478e5 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 17:36:04 +0900 Subject: [PATCH 13/23] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=9B=84=EC=86=8D=20=EC=B2=98=EB=A6=AC=EA=B0=80=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=EC=9D=B4=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=ED=96=88=EC=9D=84=20=EB=95=8C=EB=A7=8C=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/group/listener/UserRegisteredEventListener.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java b/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java index 106b22a6..9c6264d7 100644 --- a/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java +++ b/src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java @@ -1,11 +1,12 @@ package project.flipnote.group.listener; -import org.springframework.context.event.EventListener; 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; @@ -24,7 +25,7 @@ public class UserRegisteredEventListener { maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2) ) - @EventListener + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void HandleUserRegisteredEvent(UserRegisteredEvent event) { groupInvitationService.acceptPendingInvitationsOnRegister(event.userId(), event.email()); } From 442f49a72b2f9e6dcbc3d704c7aaf1b4f3a94140 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 17:56:25 +0900 Subject: [PATCH 14/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EB=B0=9B=EC=9D=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=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/GroupInvitationController.java | 17 ------- .../GroupInvitationQueryController.java | 50 +++++++++++++++++++ .../docs/GroupInvitationControllerDocs.java | 10 ---- .../GroupInvitationQueryControllerDocs.java | 29 +++++++++++ .../group/model/GroupInvitationStatus.java | 18 +++++++ .../IncomingGroupInvitationResponse.java | 26 ++++++++++ ...a => OutgoingGroupInvitationResponse.java} | 8 +-- .../repository/GroupInvitationRepository.java | 4 +- .../group/service/GroupInvitationService.java | 36 +++++++++---- 9 files changed, 158 insertions(+), 40 deletions(-) create mode 100644 src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java create mode 100644 src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java create mode 100644 src/main/java/project/flipnote/group/model/GroupInvitationStatus.java create mode 100644 src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java rename src/main/java/project/flipnote/group/model/{GroupInvitationResponse.java => OutgoingGroupInvitationResponse.java} (65%) diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java index abbfe472..62af5c50 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationController.java @@ -4,24 +4,20 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.controller.docs.GroupInvitationControllerDocs; import project.flipnote.group.model.GroupInvitationCreateRequest; import project.flipnote.group.model.GroupInvitationCreateResponse; import project.flipnote.group.model.GroupInvitationRespondRequest; -import project.flipnote.group.model.GroupInvitationResponse; import project.flipnote.group.service.GroupInvitationService; @RequiredArgsConstructor @@ -64,17 +60,4 @@ public ResponseEntity respondToGroupInvitation( return ResponseEntity.ok().build(); } - - @GetMapping - public ResponseEntity> getGroupInvitations( - @PathVariable("groupId") Long groupId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, - @AuthenticationPrincipal AuthPrinciple authPrinciple - ) { - PageResponse res - = groupInvitationService.getGroupInvitations(authPrinciple.userId(), groupId, page, size); - - return ResponseEntity.ok(res); - } } diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java new file mode 100644 index 00000000..b5977223 --- /dev/null +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java @@ -0,0 +1,50 @@ +package project.flipnote.group.controller; + +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import project.flipnote.common.response.PageResponse; +import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.controller.docs.GroupInvitationQueryControllerDocs; +import project.flipnote.group.model.OutgoingGroupInvitationResponse; +import project.flipnote.group.model.IncomingGroupInvitationResponse; +import project.flipnote.group.service.GroupInvitationService; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/v1") +public class GroupInvitationQueryController implements GroupInvitationQueryControllerDocs { + + private final GroupInvitationService groupInvitationService; + + @GetMapping("/groups/{groupId}/invitations") + public ResponseEntity> getOutgoingInvitations( + @PathVariable("groupId") Long groupId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + PageResponse res + = groupInvitationService.getOutgoingInvitations(authPrinciple.userId(), groupId, page, size); + + return ResponseEntity.ok(res); + } + + @GetMapping("/group-invitations/incoming") + public ResponseEntity> getIncomingInvitations( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + PageResponse res + = groupInvitationService.getIncomingInvitations(authPrinciple.userId(), page, size); + + return ResponseEntity.ok(res); + } +} diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java index bceea833..2bb35b28 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java @@ -4,12 +4,10 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import project.flipnote.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.model.GroupInvitationCreateRequest; import project.flipnote.group.model.GroupInvitationCreateResponse; import project.flipnote.group.model.GroupInvitationRespondRequest; -import project.flipnote.group.model.GroupInvitationResponse; @Tag(name = "Group Invitation", description = "Group Invitation API") public interface GroupInvitationControllerDocs { @@ -26,12 +24,4 @@ ResponseEntity createGroupInvitation( ResponseEntity respondToGroupInvitation( Long groupId, Long invitationId, GroupInvitationRespondRequest req, AuthPrinciple authPrinciple ); - - @Operation(summary = "그룹 초대 목록 조회") - ResponseEntity> getGroupInvitations( - Long groupId, - int page, - int size, - AuthPrinciple authPrinciple - ); } diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java new file mode 100644 index 00000000..c63a151d --- /dev/null +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java @@ -0,0 +1,29 @@ +package project.flipnote.group.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.common.response.PageResponse; +import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.model.OutgoingGroupInvitationResponse; +import project.flipnote.group.model.IncomingGroupInvitationResponse; + +@Tag(name = "Group Invitation Query", description = "Group Invitation Query API") +public interface GroupInvitationQueryControllerDocs { + + @Operation(summary = "그룹 초대 보낸 목록 조회") + ResponseEntity> getOutgoingInvitations( + Long groupId, + int page, + int size, + AuthPrinciple authPrinciple + ); + + @Operation(summary = "그룹 초대 받은 목록 조회") + ResponseEntity> getIncomingInvitations( + int page, + int size, + AuthPrinciple authPrinciple + ); +} diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationStatus.java b/src/main/java/project/flipnote/group/model/GroupInvitationStatus.java new file mode 100644 index 00000000..07736e1d --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInvitationStatus.java @@ -0,0 +1,18 @@ +package project.flipnote.group.model; + +public enum GroupInvitationStatus { + PENDING, ACCEPTED, REJECTED; + + public static GroupInvitationStatus from(project.flipnote.group.entity.GroupInvitationStatus status) { + if (status == null) { + throw new IllegalArgumentException("GroupInvitationStatus is null"); + } + + return switch (status) { + case PENDING -> PENDING; + case ACCEPTED -> ACCEPTED; + case REJECTED -> REJECTED; + default -> throw new IllegalArgumentException("Unknown GroupInvitationStatus: " + status); + }; + } +} diff --git a/src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java b/src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java new file mode 100644 index 00000000..29b78a17 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java @@ -0,0 +1,26 @@ +package project.flipnote.group.model; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import project.flipnote.group.entity.GroupInvitation; + +public record IncomingGroupInvitationResponse( + Long invitationId, + Long groupId, + GroupInvitationStatus status, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime createdAt +) { + + public static IncomingGroupInvitationResponse from(GroupInvitation invitation) { + return new IncomingGroupInvitationResponse( + invitation.getId(), + invitation.getGroupId(), + GroupInvitationStatus.from(invitation.getStatus()), + invitation.getCreatedAt() + ); + } +} diff --git a/src/main/java/project/flipnote/group/model/GroupInvitationResponse.java b/src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java similarity index 65% rename from src/main/java/project/flipnote/group/model/GroupInvitationResponse.java rename to src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java index de5cba66..43ee3eeb 100644 --- a/src/main/java/project/flipnote/group/model/GroupInvitationResponse.java +++ b/src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java @@ -6,24 +6,26 @@ import project.flipnote.group.entity.GroupInvitation; -public record GroupInvitationResponse( +public record OutgoingGroupInvitationResponse( Long invitationId, Long inviterUserId, Long inviteeUserId, String inviteeEmail, String inviteeNickname, + GroupInvitationStatus status, @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt ) { - public static GroupInvitationResponse from(GroupInvitation invitation, String inviteeNickname) { - return new GroupInvitationResponse( + public static OutgoingGroupInvitationResponse from(GroupInvitation invitation, String inviteeNickname) { + return new OutgoingGroupInvitationResponse( invitation.getId(), invitation.getInviterUserId(), invitation.getInviteeUserId(), invitation.getInviteeEmail(), inviteeNickname, + GroupInvitationStatus.from(invitation.getStatus()), invitation.getCreatedAt() ); } diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index 7918fc31..59bfbdd4 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -20,7 +20,9 @@ public interface GroupInvitationRepository extends JpaRepository findByIdAndGroupIdAndInviteeUserIdAndStatus(Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status); - Page findAllByGroupIdAndStatus(Long groupId, GroupInvitationStatus status, Pageable pageable); + Page findAllByGroupId(Long groupId, Pageable pageable); List findAllByInviteeEmailAndStatus(String InviteeEmail, GroupInvitationStatus status); + + Page findAllByInviteeUserId(Long inviteeUserId, Pageable pageable); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index c83b9456..f96a09aa 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -23,7 +23,8 @@ import project.flipnote.group.model.GroupInvitationCreateRequest; import project.flipnote.group.model.GroupInvitationCreateResponse; import project.flipnote.group.model.GroupInvitationRespondRequest; -import project.flipnote.group.model.GroupInvitationResponse; +import project.flipnote.group.model.IncomingGroupInvitationResponse; +import project.flipnote.group.model.OutgoingGroupInvitationResponse; import project.flipnote.group.repository.GroupInvitationRepository; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupRepository; @@ -118,24 +119,24 @@ public void respondToGroupInvitation( } /** - * 그룹 초대 목록을 페이징하여 조회 + * 그룹 초대 보낸 목록을 페이징하여 조회 * - * @param userId 초대 목록을 조회하는 회원 ID + * @param userId 초대 보낸 목록을 조회하는 회원 ID * @param groupId 초대한 그룹 ID * @param page 페이지 번호 * @param size 페이지 크기 - * @return 페이징된 그룹 초대 목록 응답 + * @return 페이징된 그룹 초대 보낸 목록 응답 * @author 윤정환 */ - public PageResponse getGroupInvitations(Long userId, Long groupId, int page, int size) { + public PageResponse getOutgoingInvitations(Long userId, Long groupId, int page, + int size) { 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 - .findAllByGroupIdAndStatus(groupId, GroupInvitationStatus.PENDING, pageRequest); + Page invitationPage = groupInvitationRepository.findAllByGroupId(groupId, pageRequest); List inviteeUserIds = invitationPage.getContent() .stream() @@ -144,8 +145,8 @@ public PageResponse getGroupInvitations(Long userId, Lo .toList(); Map idAndNicknames = userService.getIdAndNicknames(inviteeUserIds); - Page res = invitationPage.map( - (invitation) -> GroupInvitationResponse.from( + Page res = invitationPage.map( + (invitation) -> OutgoingGroupInvitationResponse.from( invitation, idAndNicknames.getOrDefault(invitation.getInviteeUserId(), "") ) ); @@ -153,6 +154,23 @@ public PageResponse getGroupInvitations(Long userId, Lo return PageResponse.from(res); } + /** + * 그룹 초대 받은 목록을 페이징하여 조회 + * + * @param userId 초대 받은 목록을 조회하는 회원 ID + * @param page 페이지 번호 + * @param size 페이지 크기 + * @return 페이징된 그룹 초대 받은 목록 응답 + * @author 윤정환 + */ + public PageResponse getIncomingInvitations(Long userId, int page, int size) { + PageRequest pageRequest = PageRequest.of(page, size); + Page invitationPage = groupInvitationRepository.findAllByInviteeUserId(userId, pageRequest); + + Page res = invitationPage.map(IncomingGroupInvitationResponse::from); + return PageResponse.from(res); + } + /** * 회원가입시 비회원 그룹 초대를 수락 * From 4c632a5d7c9aaaee6348f93694b95cf12013e2c9 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 17:58:55 +0900 Subject: [PATCH 15/23] =?UTF-8?q?Docs:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20API=20Swagger=20Docs=EC=97=90=20=EB=B3=B4=EC=95=88?= =?UTF-8?q?=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/docs/GroupInvitationControllerDocs.java | 7 ++++--- .../docs/GroupInvitationQueryControllerDocs.java | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java index 2bb35b28..8b6c1cfb 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.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.common.security.dto.AuthPrinciple; import project.flipnote.group.model.GroupInvitationCreateRequest; @@ -12,15 +13,15 @@ @Tag(name = "Group Invitation", description = "Group Invitation API") public interface GroupInvitationControllerDocs { - @Operation(summary = "그룹 초대") + @Operation(summary = "그룹 초대", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity createGroupInvitation( Long groupId, GroupInvitationCreateRequest req, AuthPrinciple authPrinciple ); - @Operation(summary = "그룹 초대 취소") + @Operation(summary = "그룹 초대 취소", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity deleteGroupInvitation(Long groupId, Long invitationId, AuthPrinciple authPrinciple); - @Operation(summary = "그룹 초대 응답") + @Operation(summary = "그룹 초대 응답", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity respondToGroupInvitation( Long groupId, Long invitationId, GroupInvitationRespondRequest req, AuthPrinciple authPrinciple ); 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 c63a151d..72bb4335 100644 --- a/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java +++ b/src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java @@ -3,16 +3,17 @@ 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.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; -import project.flipnote.group.model.OutgoingGroupInvitationResponse; import project.flipnote.group.model.IncomingGroupInvitationResponse; +import project.flipnote.group.model.OutgoingGroupInvitationResponse; @Tag(name = "Group Invitation Query", description = "Group Invitation Query API") public interface GroupInvitationQueryControllerDocs { - @Operation(summary = "그룹 초대 보낸 목록 조회") + @Operation(summary = "그룹 초대 보낸 목록 조회", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity> getOutgoingInvitations( Long groupId, int page, @@ -20,7 +21,7 @@ ResponseEntity> getOutgoingInvitat AuthPrinciple authPrinciple ); - @Operation(summary = "그룹 초대 받은 목록 조회") + @Operation(summary = "그룹 초대 받은 목록 조회", security = {@SecurityRequirement(name = "access-token")}) ResponseEntity> getIncomingInvitations( int page, int size, From 418215fde97010f526a7be669993a6707829dff0 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:00:41 +0900 Subject: [PATCH 16/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupInvitationQueryController.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java index b5977223..75efe29b 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java @@ -8,12 +8,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import project.flipnote.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.controller.docs.GroupInvitationQueryControllerDocs; -import project.flipnote.group.model.OutgoingGroupInvitationResponse; import project.flipnote.group.model.IncomingGroupInvitationResponse; +import project.flipnote.group.model.OutgoingGroupInvitationResponse; import project.flipnote.group.service.GroupInvitationService; @RequiredArgsConstructor @@ -26,8 +27,8 @@ public class GroupInvitationQueryController implements GroupInvitationQueryContr @GetMapping("/groups/{groupId}/invitations") public ResponseEntity> getOutgoingInvitations( @PathVariable("groupId") Long groupId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, + @Min(0) @RequestParam(defaultValue = "0") int page, + @Min(1) @Min(30) @RequestParam(defaultValue = "20") int size, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { PageResponse res @@ -38,8 +39,8 @@ public ResponseEntity> getOutgoing @GetMapping("/group-invitations/incoming") public ResponseEntity> getIncomingInvitations( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, + @Min(0) @RequestParam(defaultValue = "0") int page, + @Min(1) @Min(30) @RequestParam(defaultValue = "20") int size, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { PageResponse res From 0eb2af2f8b9733d80132a8a310b744174f3f099c Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:02:30 +0900 Subject: [PATCH 17/23] =?UTF-8?q?Refactor:=20UserIdNickname=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=EC=85=98=20=EC=9C=84=EC=B9=98=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/{group => user}/model/UserIdNickname.java | 2 +- .../project/flipnote/user/repository/UserProfileRepository.java | 2 +- src/main/java/project/flipnote/user/service/UserService.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/project/flipnote/{group => user}/model/UserIdNickname.java (66%) diff --git a/src/main/java/project/flipnote/group/model/UserIdNickname.java b/src/main/java/project/flipnote/user/model/UserIdNickname.java similarity index 66% rename from src/main/java/project/flipnote/group/model/UserIdNickname.java rename to src/main/java/project/flipnote/user/model/UserIdNickname.java index 595a2330..d7bf56a3 100644 --- a/src/main/java/project/flipnote/group/model/UserIdNickname.java +++ b/src/main/java/project/flipnote/user/model/UserIdNickname.java @@ -1,4 +1,4 @@ -package project.flipnote.group.model; +package project.flipnote.user.model; public interface UserIdNickname { Long getId(); diff --git a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java index 4d505131..7c6370c3 100644 --- a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -5,7 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; -import project.flipnote.group.model.UserIdNickname; +import project.flipnote.user.model.UserIdNickname; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 0ccb18a1..3efebd09 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -14,7 +14,7 @@ import project.flipnote.common.dto.UserCreateCommand; import project.flipnote.common.event.UserWithdrawnEvent; import project.flipnote.common.exception.BizException; -import project.flipnote.group.model.UserIdNickname; +import project.flipnote.user.model.UserIdNickname; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; From 62214bff12eab3a98f5cde39c1427d7d44e33acd Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:12:18 +0900 Subject: [PATCH 18/23] =?UTF-8?q?Refactor:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=20=EB=B0=8F=20=EC=9C=A0=EB=8B=88=ED=81=AC=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/group/entity/GroupInvitation.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/group/entity/GroupInvitation.java b/src/main/java/project/flipnote/group/entity/GroupInvitation.java index 652a3d99..b9f4291f 100644 --- a/src/main/java/project/flipnote/group/entity/GroupInvitation.java +++ b/src/main/java/project/flipnote/group/entity/GroupInvitation.java @@ -7,7 +7,9 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Index; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -17,7 +19,19 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -@Table(name = "group_invitations") +@Table( + name = "group_invitation", + indexes = { + @Index(name = "idx_group_invitee_user", columnList = "group_id, invitee_user_id, status"), + @Index(name = "idx_group_invitee_email", columnList = "group_id, invitee_email, status"), + @Index(name = "idx_invitee_user_status", columnList = "invitee_user_id, status"), + @Index(name = "idx_invitee_email_status", columnList = "invitee_email, status") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uq_group_invitee_user", columnNames = {"group_id", "invitee_user_id"}), + @UniqueConstraint(name = "uq_group_invitee_email", columnNames = {"group_id", "invitee_email"}) + } +) public class GroupInvitation extends BaseEntity { @Id From f3ae3f3b4654ab26fa8455e4368a6e2ac5c46697 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:15:37 +0900 Subject: [PATCH 19/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=EC=97=90=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=B6=94=EA=B0=80=EC=8B=9C=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=EA=B2=80=EC=A6=9D=20=ED=9B=84?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=ED=95=98=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 --- .../flipnote/group/service/GroupInvitationService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index f96a09aa..545758ff 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -262,6 +262,10 @@ private Long createGuestInvitation(Long inviterUserId, Long groupId, String invi * @author 윤정환 */ private void addGroupMember(Long inviteeUserId, Long groupId) { + if (groupMemberRepository.existsByGroup_idAndUser_id(groupId, inviteeUserId)) { + return; + } + GroupMember groupMember = GroupMember.builder() .group(groupRepository.getReferenceById(groupId)) .user(em.getReference(UserProfile.class, inviteeUserId)) From e50dbe452733ccee353dac92880bc10a0f63738d Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:31:38 +0900 Subject: [PATCH 20/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=88=98=EB=9D=BD=EC=8B=9C=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=B5=9C=EB=8C=80=20=EA=B0=80=EC=9E=85=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=9B=84=20=EA=B0=80=EC=9E=85=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/group/entity/Group.java | 8 ++++ .../group/entity/GroupInvitation.java | 11 +++-- .../IncomingGroupInvitationResponse.java | 2 +- .../repository/GroupInvitationRepository.java | 41 +++++++++++++++---- .../group/service/GroupInvitationService.java | 27 +++++++----- 5 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/main/java/project/flipnote/group/entity/Group.java b/src/main/java/project/flipnote/group/entity/Group.java index bf9551e7..e87aafd2 100644 --- a/src/main/java/project/flipnote/group/entity/Group.java +++ b/src/main/java/project/flipnote/group/entity/Group.java @@ -16,6 +16,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import project.flipnote.common.entity.BaseEntity; +import project.flipnote.common.exception.BizException; +import project.flipnote.group.exception.GroupErrorCode; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -67,4 +69,10 @@ private Group( this.maxMember = maxMember; this.imageUrl = imageUrl; } + + public void validateJoinable() { + if (maxMember < 1 || maxMember >= 100) { + throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER); + } + } } diff --git a/src/main/java/project/flipnote/group/entity/GroupInvitation.java b/src/main/java/project/flipnote/group/entity/GroupInvitation.java index b9f4291f..140a4606 100644 --- a/src/main/java/project/flipnote/group/entity/GroupInvitation.java +++ b/src/main/java/project/flipnote/group/entity/GroupInvitation.java @@ -8,6 +8,8 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; @@ -38,8 +40,9 @@ public class GroupInvitation extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private Long groupId; + @ManyToOne + @JoinColumn(name = "group_id", nullable = false) + private Group group; @Column(nullable = false) private Long inviterUserId; @@ -53,8 +56,8 @@ public class GroupInvitation extends BaseEntity { private GroupInvitationStatus status; @Builder - public GroupInvitation(Long groupId, Long inviterUserId, Long inviteeUserId, String inviteeEmail) { - this.groupId = groupId; + public GroupInvitation(Group group, Long inviterUserId, Long inviteeUserId, String inviteeEmail) { + this.group = group; this.inviterUserId = inviterUserId; this.inviteeUserId = inviteeUserId; this.inviteeEmail = inviteeEmail; diff --git a/src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java b/src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java index 29b78a17..703ae0c9 100644 --- a/src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java +++ b/src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java @@ -18,7 +18,7 @@ public record IncomingGroupInvitationResponse( public static IncomingGroupInvitationResponse from(GroupInvitation invitation) { return new IncomingGroupInvitationResponse( invitation.getId(), - invitation.getGroupId(), + invitation.getGroup().getId(), GroupInvitationStatus.from(invitation.getStatus()), invitation.getCreatedAt() ); diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index 59bfbdd4..12b99ea1 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -6,23 +6,48 @@ 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 project.flipnote.group.entity.GroupInvitation; import project.flipnote.group.entity.GroupInvitationStatus; public interface GroupInvitationRepository extends JpaRepository { - boolean existsByGroupIdAndInviteeUserId(Long groupId, Long inviteeUserId); + boolean existsByGroup_IdAndInviteeUserId(Long groupId, Long inviteeUserId); - boolean existsByGroupIdAndInviteeEmail(Long groupId, String inviteeEmail); + boolean existsByGroup_IdAndInviteeEmail(Long groupId, String inviteeEmail); Optional findByIdAndStatus(Long id, GroupInvitationStatus status); - Optional findByIdAndGroupIdAndInviteeUserIdAndStatus(Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status); - - Page findAllByGroupId(Long groupId, Pageable pageable); - - List findAllByInviteeEmailAndStatus(String InviteeEmail, GroupInvitationStatus status); - + @Query(""" + SELECT gi + FROM GroupInvitation gi + JOIN FETCH gi.group g + WHERE gi.id = :id + AND g.id = :groupId + AND gi.inviteeUserId = :inviteeUserId + AND gi.status = :status + """) + Optional findWithGroupByIdAndGroup_IdAndInviteeUserIdAndStatus( + @Param("id") Long id, + @Param("groupId") Long groupId, + @Param("inviteeUserId") Long inviteeUserId, + @Param("status") GroupInvitationStatus status + ); + + Page findAllByGroup_Id(Long groupId, Pageable pageable); + + @Query(""" + SELECT gi + FROM GroupInvitation gi + JOIN FETCH gi.group g + WHERE gi.inviteeEmail = :inviteeEmail + AND gi.status = :status + """) + List findAllWithGroupByInviteeEmailAndStatus( + @Param("inviteeEmail") String inviteeEmail, + @Param("status") GroupInvitationStatus status + ); Page findAllByInviteeUserId(Long inviteeUserId, Pageable pageable); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 545758ff..6e9009de 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -14,6 +14,7 @@ import project.flipnote.common.exception.BizException; import project.flipnote.common.response.PageResponse; import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.entity.Group; import project.flipnote.group.entity.GroupInvitation; import project.flipnote.group.entity.GroupInvitationStatus; import project.flipnote.group.entity.GroupMember; @@ -105,15 +106,16 @@ public void respondToGroupInvitation( Long invitationId, GroupInvitationRespondRequest req ) { - GroupInvitation invitation = groupInvitationRepository.findByIdAndGroupIdAndInviteeUserIdAndStatus( + GroupInvitation invitation = groupInvitationRepository.findWithGroupByIdAndGroup_IdAndInviteeUserIdAndStatus( invitationId, groupId, inviteeUserId, GroupInvitationStatus.PENDING ) .orElseThrow(() -> new BizException(GroupInvitationErrorCode.INVITATION_NOT_FOUND)); + invitation.getGroup().validateJoinable(); + invitation.respond(req.toEntityStatus()); if (Objects.equals(invitation.getStatus(), GroupInvitationStatus.ACCEPTED)) { - // TODO: GroupMember 에서 group과 user의 id만 가지고 있도록 수정 addGroupMember(inviteeUserId, groupId); } } @@ -136,12 +138,12 @@ public PageResponse getOutgoingInvitations(Long // TODO: Projection 및 카운트 쿼리 튜닝 필요 PageRequest pageRequest = PageRequest.of(page, size); - Page invitationPage = groupInvitationRepository.findAllByGroupId(groupId, pageRequest); + Page invitationPage = groupInvitationRepository.findAllByGroup_Id(groupId, pageRequest); List inviteeUserIds = invitationPage.getContent() .stream() - .filter(Objects::nonNull) .map(GroupInvitation::getInviteeUserId) + .filter(Objects::nonNull) .toList(); Map idAndNicknames = userService.getIdAndNicknames(inviteeUserIds); @@ -164,6 +166,7 @@ public PageResponse getOutgoingInvitations(Long * @author 윤정환 */ public PageResponse getIncomingInvitations(Long userId, int page, int size) { + // TODO: Projection 및 카운트 쿼리 튜닝 필요 PageRequest pageRequest = PageRequest.of(page, size); Page invitationPage = groupInvitationRepository.findAllByInviteeUserId(userId, pageRequest); @@ -181,12 +184,16 @@ public PageResponse getIncomingInvitations(Long @Transactional public void acceptPendingInvitationsOnRegister(Long inviteeUserId, String inviteeEmail) { List invitations = groupInvitationRepository - .findAllByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING); + .findAllWithGroupByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING); for (GroupInvitation invitation : invitations) { + Group group = invitation.getGroup(); + + group.validateJoinable(); + invitation.respond(GroupInvitationStatus.ACCEPTED); - addGroupMember(inviteeUserId, invitation.getGroupId()); + addGroupMember(inviteeUserId, group.getId()); } } @@ -212,12 +219,12 @@ private void validateGroupInvitePermission(Long userId, Long groupId) { * @author 윤정환 */ private Long createUserInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { - if (groupInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { + if (groupInvitationRepository.existsByGroup_IdAndInviteeUserId(groupId, inviteeUser.getId())) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } GroupInvitation invitation = GroupInvitation.builder() - .groupId(groupId) + .group(groupRepository.getReferenceById(groupId)) .inviterUserId(inviterUserId) .inviteeUserId(inviteeUser.getId()) .inviteeEmail(inviteeUser.getEmail()) @@ -238,12 +245,12 @@ private Long createUserInvitation(Long inviterUserId, Long groupId, UserProfile * @author 윤정환 */ private Long createGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { - if (groupInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { + if (groupInvitationRepository.existsByGroup_IdAndInviteeEmail(groupId, inviteeEmail)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } GroupInvitation invitation = GroupInvitation.builder() - .groupId(groupId) + .group(groupRepository.getReferenceById(groupId)) .inviterUserId(inviterUserId) .inviteeEmail(inviteeEmail) .build(); From 48658f3af1386282c0a9b5e62c2b27b5919a7e1a Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:41:28 +0900 Subject: [PATCH 21/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=20=EA=B2=80=EC=A6=9D=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/exception/GroupInvitationErrorCode.java | 3 ++- .../group/repository/GroupInvitationRepository.java | 8 ++++---- .../group/service/GroupInvitationService.java | 12 ++++++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java b/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java index 21575f39..1bca0b37 100644 --- a/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java +++ b/src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java @@ -13,7 +13,8 @@ public enum GroupInvitationErrorCode implements ErrorCode { ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."), NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."), INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."), - CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."); + CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."), + ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_INVITATION_005", "이미 그룹 회원입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index 12b99ea1..8bff8219 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -14,10 +14,6 @@ public interface GroupInvitationRepository extends JpaRepository { - boolean existsByGroup_IdAndInviteeUserId(Long groupId, Long inviteeUserId); - - boolean existsByGroup_IdAndInviteeEmail(Long groupId, String inviteeEmail); - Optional findByIdAndStatus(Long id, GroupInvitationStatus status); @Query(""" @@ -50,4 +46,8 @@ List findAllWithGroupByInviteeEmailAndStatus( @Param("status") GroupInvitationStatus status ); Page findAllByInviteeUserId(Long inviteeUserId, Pageable pageable); + + boolean existsByGroup_IdAndInviteeUserIdAndStatus(Long groupId, Long inviteeUserId, GroupInvitationStatus status); + + boolean existsByGroup_IdAndInviteeEmailAndStatus(Long groupId, String inviteeEmail, GroupInvitationStatus status); } diff --git a/src/main/java/project/flipnote/group/service/GroupInvitationService.java b/src/main/java/project/flipnote/group/service/GroupInvitationService.java index 6e9009de..884f4e59 100644 --- a/src/main/java/project/flipnote/group/service/GroupInvitationService.java +++ b/src/main/java/project/flipnote/group/service/GroupInvitationService.java @@ -219,7 +219,13 @@ private void validateGroupInvitePermission(Long userId, Long groupId) { * @author 윤정환 */ private Long createUserInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { - if (groupInvitationRepository.existsByGroup_IdAndInviteeUserId(groupId, inviteeUser.getId())) { + if (groupMemberRepository.existsByGroup_idAndUser_id(groupId, inviteeUser.getId())) { + throw new BizException(GroupInvitationErrorCode.ALREADY_GROUP_MEMBER); + } + + if (groupInvitationRepository + .existsByGroup_IdAndInviteeUserIdAndStatus(groupId, inviteeUser.getId(), GroupInvitationStatus.PENDING) + ) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } @@ -245,7 +251,9 @@ private Long createUserInvitation(Long inviterUserId, Long groupId, UserProfile * @author 윤정환 */ private Long createGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { - if (groupInvitationRepository.existsByGroup_IdAndInviteeEmail(groupId, inviteeEmail)) { + if (groupInvitationRepository + .existsByGroup_IdAndInviteeEmailAndStatus(groupId, inviteeEmail, GroupInvitationStatus.PENDING) + ) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } From c1e0018be084965a74e2bc2152c4b1283863405f Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:44:21 +0900 Subject: [PATCH 22/23] =?UTF-8?q?Refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B9=88=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5/=EC=A4=91=EB=B3=B5=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20toMap=20=EC=B6=A9=EB=8F=8C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/user/service/UserService.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 3efebd09..de001b60 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -110,9 +110,17 @@ public void validatePhoneDuplicate(String phone) { } public Map getIdAndNicknames(List inviteeUserIds) { - List idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds); + if (inviteeUserIds == null || inviteeUserIds.isEmpty()) { + return java.util.Collections.emptyMap(); + } + List distinctIds = inviteeUserIds.stream().distinct().toList(); + List idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(distinctIds); return idAndNicknames.stream() - .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); + .collect(Collectors.toMap( + UserIdNickname::getId, + UserIdNickname::getNickname, + (a, b) -> a + )); } } From d67c10489e65b4f5ddb1725126880bffb49b6ddb Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 12 Aug 2025 18:48:36 +0900 Subject: [PATCH 23/23] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EB=B0=9B=EC=9D=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/controller/GroupInvitationQueryController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java index 75efe29b..19e45079 100644 --- a/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java +++ b/src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java @@ -37,7 +37,7 @@ public ResponseEntity> getOutgoing return ResponseEntity.ok(res); } - @GetMapping("/group-invitations/incoming") + @GetMapping("/group-invitations") public ResponseEntity> getIncomingInvitations( @Min(0) @RequestParam(defaultValue = "0") int page, @Min(1) @Min(30) @RequestParam(defaultValue = "20") int size,