Skip to content

Commit ef8f71b

Browse files
authored
Merge pull request #27 from FlipNoteTeam/feat/group-invitation
Feat: [FN-96][FN-97][FN-99][FN-110][FN-123] 그룹 초대 기능
2 parents 86e7ffb + d67c104 commit ef8f71b

30 files changed

Lines changed: 892 additions & 8 deletions

src/main/java/project/flipnote/auth/service/AuthService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import project.flipnote.auth.util.VerificationCodeGenerator;
3636
import project.flipnote.common.config.ClientProperties;
3737
import project.flipnote.common.dto.UserCreateCommand;
38+
import project.flipnote.common.event.UserRegisteredEvent;
3839
import project.flipnote.common.exception.BizException;
3940
import project.flipnote.common.security.dto.AuthPrinciple;
4041
import project.flipnote.common.security.jwt.JwtComponent;
@@ -77,6 +78,8 @@ public UserRegisterResponse register(UserRegisterRequest req) {
7778
.build();
7879
userAuthRepository.save(userAuth);
7980

81+
eventPublisher.publishEvent(new UserRegisteredEvent(userId, email));
82+
8083
return UserRegisterResponse.from(userId);
8184
}
8285

src/main/java/project/flipnote/common/config/SwaggerConfig.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package project.flipnote.common.config;
22

3+
import org.springframework.beans.factory.annotation.Value;
34
import org.springframework.context.annotation.Bean;
45
import org.springframework.context.annotation.Configuration;
56

@@ -8,14 +9,18 @@
89
import io.swagger.v3.oas.models.info.Info;
910
import io.swagger.v3.oas.models.security.SecurityRequirement;
1011
import io.swagger.v3.oas.models.security.SecurityScheme;
11-
import project.flipnote.common.security.jwt.JwtConstants;
12+
import io.swagger.v3.oas.models.servers.Server;
1213

1314
@Configuration
1415
public class SwaggerConfig {
1516

17+
@Value("${springdoc.server.url}")
18+
private String serverUrl;
19+
1620
@Bean
1721
public OpenAPI openApi() {
1822
return new OpenAPI()
23+
.addServersItem(new Server().url(serverUrl))
1924
.addSecurityItem(
2025
new SecurityRequirement()
2126
.addList("access-token")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package project.flipnote.common.event;
2+
3+
public record UserRegisteredEvent(
4+
Long userId,
5+
String email
6+
) {
7+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package project.flipnote.common.response;
2+
3+
import java.util.List;
4+
5+
import org.springframework.data.domain.Page;
6+
7+
public record PageResponse<T>(
8+
List<T> content,
9+
int page,
10+
int size,
11+
long totalElements,
12+
int totalPages,
13+
boolean first,
14+
boolean last,
15+
boolean hasNext,
16+
boolean hasPrevious
17+
) {
18+
19+
public static <T> PageResponse<T> from(Page<T> page) {
20+
return new PageResponse<>(
21+
page.getContent(),
22+
page.getNumber(),
23+
page.getSize(),
24+
page.getTotalElements(),
25+
page.getTotalPages(),
26+
page.isFirst(),
27+
page.isLast(),
28+
page.hasNext(),
29+
page.hasPrevious()
30+
);
31+
}
32+
}

src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@Getter
1010
@RequiredArgsConstructor
1111
public enum SecurityErrorCode implements ErrorCode {
12-
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "SECURITY_0021", "토큰이 만료되었습니다."),
12+
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "SECURITY_001", "토큰이 만료되었습니다."),
1313
NOT_VALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED.value(), "SECURITY_002", "올바르지 않은 토큰입니다."),
1414
UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "SECURITY_003", "인증이 필요합니다."),
1515
FORBIDDEN(HttpStatus.FORBIDDEN.value(), "SECURITY_004", "권한이 없습니다.");
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package project.flipnote.group.controller;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
6+
import org.springframework.web.bind.annotation.DeleteMapping;
7+
import org.springframework.web.bind.annotation.PatchMapping;
8+
import org.springframework.web.bind.annotation.PathVariable;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
import jakarta.validation.Valid;
15+
import lombok.RequiredArgsConstructor;
16+
import project.flipnote.common.security.dto.AuthPrinciple;
17+
import project.flipnote.group.controller.docs.GroupInvitationControllerDocs;
18+
import project.flipnote.group.model.GroupInvitationCreateRequest;
19+
import project.flipnote.group.model.GroupInvitationCreateResponse;
20+
import project.flipnote.group.model.GroupInvitationRespondRequest;
21+
import project.flipnote.group.service.GroupInvitationService;
22+
23+
@RequiredArgsConstructor
24+
@RestController
25+
@RequestMapping("/v1/groups/{groupId}/invitations")
26+
public class GroupInvitationController implements GroupInvitationControllerDocs {
27+
28+
private final GroupInvitationService groupInvitationService;
29+
30+
@PostMapping
31+
public ResponseEntity<GroupInvitationCreateResponse> createGroupInvitation(
32+
@PathVariable("groupId") Long groupId,
33+
@Valid @RequestBody GroupInvitationCreateRequest req,
34+
@AuthenticationPrincipal AuthPrinciple authPrinciple
35+
) {
36+
GroupInvitationCreateResponse res = groupInvitationService.createGroupInvitation(authPrinciple, groupId, req);
37+
38+
return ResponseEntity.status(HttpStatus.CREATED).body(res);
39+
}
40+
41+
@DeleteMapping("/{invitationId}")
42+
public ResponseEntity<Void> deleteGroupInvitation(
43+
@PathVariable("groupId") Long groupId,
44+
@PathVariable("invitationId") Long invitationId,
45+
@AuthenticationPrincipal AuthPrinciple authPrinciple
46+
) {
47+
groupInvitationService.deleteGroupInvitation(authPrinciple.userId(), groupId, invitationId);
48+
49+
return ResponseEntity.ok().build();
50+
}
51+
52+
@PatchMapping("/{invitationId}")
53+
public ResponseEntity<Void> respondToGroupInvitation(
54+
@PathVariable("groupId") Long groupId,
55+
@PathVariable("invitationId") Long invitationId,
56+
@Valid @RequestBody GroupInvitationRespondRequest req,
57+
@AuthenticationPrincipal AuthPrinciple authPrinciple
58+
) {
59+
groupInvitationService.respondToGroupInvitation(authPrinciple.userId(), groupId, invitationId, req);
60+
61+
return ResponseEntity.ok().build();
62+
}
63+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package project.flipnote.group.controller;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PathVariable;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RequestParam;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
import jakarta.validation.constraints.Min;
12+
import lombok.RequiredArgsConstructor;
13+
import project.flipnote.common.response.PageResponse;
14+
import project.flipnote.common.security.dto.AuthPrinciple;
15+
import project.flipnote.group.controller.docs.GroupInvitationQueryControllerDocs;
16+
import project.flipnote.group.model.IncomingGroupInvitationResponse;
17+
import project.flipnote.group.model.OutgoingGroupInvitationResponse;
18+
import project.flipnote.group.service.GroupInvitationService;
19+
20+
@RequiredArgsConstructor
21+
@RestController
22+
@RequestMapping("/v1")
23+
public class GroupInvitationQueryController implements GroupInvitationQueryControllerDocs {
24+
25+
private final GroupInvitationService groupInvitationService;
26+
27+
@GetMapping("/groups/{groupId}/invitations")
28+
public ResponseEntity<PageResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations(
29+
@PathVariable("groupId") Long groupId,
30+
@Min(0) @RequestParam(defaultValue = "0") int page,
31+
@Min(1) @Min(30) @RequestParam(defaultValue = "20") int size,
32+
@AuthenticationPrincipal AuthPrinciple authPrinciple
33+
) {
34+
PageResponse<OutgoingGroupInvitationResponse> res
35+
= groupInvitationService.getOutgoingInvitations(authPrinciple.userId(), groupId, page, size);
36+
37+
return ResponseEntity.ok(res);
38+
}
39+
40+
@GetMapping("/group-invitations")
41+
public ResponseEntity<PageResponse<IncomingGroupInvitationResponse>> getIncomingInvitations(
42+
@Min(0) @RequestParam(defaultValue = "0") int page,
43+
@Min(1) @Min(30) @RequestParam(defaultValue = "20") int size,
44+
@AuthenticationPrincipal AuthPrinciple authPrinciple
45+
) {
46+
PageResponse<IncomingGroupInvitationResponse> res
47+
= groupInvitationService.getIncomingInvitations(authPrinciple.userId(), page, size);
48+
49+
return ResponseEntity.ok(res);
50+
}
51+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package project.flipnote.group.controller.docs;
2+
3+
import org.springframework.http.ResponseEntity;
4+
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import project.flipnote.common.security.dto.AuthPrinciple;
9+
import project.flipnote.group.model.GroupInvitationCreateRequest;
10+
import project.flipnote.group.model.GroupInvitationCreateResponse;
11+
import project.flipnote.group.model.GroupInvitationRespondRequest;
12+
13+
@Tag(name = "Group Invitation", description = "Group Invitation API")
14+
public interface GroupInvitationControllerDocs {
15+
16+
@Operation(summary = "그룹 초대", security = {@SecurityRequirement(name = "access-token")})
17+
ResponseEntity<GroupInvitationCreateResponse> createGroupInvitation(
18+
Long groupId, GroupInvitationCreateRequest req, AuthPrinciple authPrinciple
19+
);
20+
21+
@Operation(summary = "그룹 초대 취소", security = {@SecurityRequirement(name = "access-token")})
22+
ResponseEntity<Void> deleteGroupInvitation(Long groupId, Long invitationId, AuthPrinciple authPrinciple);
23+
24+
@Operation(summary = "그룹 초대 응답", security = {@SecurityRequirement(name = "access-token")})
25+
ResponseEntity<Void> respondToGroupInvitation(
26+
Long groupId, Long invitationId, GroupInvitationRespondRequest req, AuthPrinciple authPrinciple
27+
);
28+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package project.flipnote.group.controller.docs;
2+
3+
import org.springframework.http.ResponseEntity;
4+
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import project.flipnote.common.response.PageResponse;
9+
import project.flipnote.common.security.dto.AuthPrinciple;
10+
import project.flipnote.group.model.IncomingGroupInvitationResponse;
11+
import project.flipnote.group.model.OutgoingGroupInvitationResponse;
12+
13+
@Tag(name = "Group Invitation Query", description = "Group Invitation Query API")
14+
public interface GroupInvitationQueryControllerDocs {
15+
16+
@Operation(summary = "그룹 초대 보낸 목록 조회", security = {@SecurityRequirement(name = "access-token")})
17+
ResponseEntity<PageResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations(
18+
Long groupId,
19+
int page,
20+
int size,
21+
AuthPrinciple authPrinciple
22+
);
23+
24+
@Operation(summary = "그룹 초대 받은 목록 조회", security = {@SecurityRequirement(name = "access-token")})
25+
ResponseEntity<PageResponse<IncomingGroupInvitationResponse>> getIncomingInvitations(
26+
int page,
27+
int size,
28+
AuthPrinciple authPrinciple
29+
);
30+
}

src/main/java/project/flipnote/group/entity/Group.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import lombok.Getter;
1717
import lombok.NoArgsConstructor;
1818
import project.flipnote.common.entity.BaseEntity;
19+
import project.flipnote.common.exception.BizException;
20+
import project.flipnote.group.exception.GroupErrorCode;
1921

2022
@Getter
2123
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -67,4 +69,10 @@ private Group(
6769
this.maxMember = maxMember;
6870
this.imageUrl = imageUrl;
6971
}
72+
73+
public void validateJoinable() {
74+
if (maxMember < 1 || maxMember >= 100) {
75+
throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER);
76+
}
77+
}
7078
}

0 commit comments

Comments
 (0)