Skip to content

Commit a06d0ea

Browse files
Refactor: 그룹 스웨거 추가 및 그룹 가입 신청 리팩토링
Refactor: 그룹 스웨거 추가 및 그룹 가입 신청 리팩토링
2 parents b467330 + 068df6b commit a06d0ea

19 files changed

Lines changed: 612 additions & 43 deletions

src/main/java/project/flipnote/group/controller/GroupController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import lombok.RequiredArgsConstructor;
1919
import project.flipnote.common.model.response.CursorPagingResponse;
2020
import project.flipnote.common.security.dto.AuthPrinciple;
21+
import project.flipnote.group.controller.docs.GroupControllerDocs;
2122
import project.flipnote.group.model.FindGroupMemberResponse;
2223
import project.flipnote.group.model.GroupCreateRequest;
2324
import project.flipnote.group.model.GroupCreateResponse;
@@ -31,7 +32,7 @@
3132
@RequiredArgsConstructor
3233
@RestController
3334
@RequestMapping("/v1/groups")
34-
public class GroupController {
35+
public class GroupController implements GroupControllerDocs {
3536
private final GroupService groupService;
3637

3738
//그룹 생성 API
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package project.flipnote.group.controller.docs;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
5+
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
8+
import io.swagger.v3.oas.annotations.Parameters;
9+
import io.swagger.v3.oas.annotations.media.Content;
10+
import io.swagger.v3.oas.annotations.media.ExampleObject;
11+
import io.swagger.v3.oas.annotations.media.Schema;
12+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
13+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
14+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
15+
import io.swagger.v3.oas.annotations.tags.Tag;
16+
import jakarta.validation.Valid;
17+
import project.flipnote.common.model.response.CursorPagingResponse;
18+
import project.flipnote.common.security.dto.AuthPrinciple;
19+
import project.flipnote.group.model.FindGroupMemberResponse;
20+
import project.flipnote.group.model.GroupCreateRequest;
21+
import project.flipnote.group.model.GroupCreateResponse;
22+
import project.flipnote.group.model.GroupDetailResponse;
23+
import project.flipnote.group.model.GroupInfo;
24+
import project.flipnote.group.model.GroupListRequest;
25+
import project.flipnote.group.model.GroupPutRequest;
26+
import project.flipnote.group.model.GroupPutResponse;
27+
28+
@Tag(name = "그룹", description = "그룹 생성/수정/상세/삭제/멤버/목록 API")
29+
@SecurityRequirement(name = "access-token")
30+
public interface GroupControllerDocs {
31+
32+
//그룹 생성
33+
@Operation(
34+
summary = "그룹 생성",
35+
description = "새 그룹을 생성하고 생성자를 OWNER로 등록합니다."
36+
)
37+
@ApiResponses({
38+
@ApiResponse(responseCode = "201", description = "생성 성공",
39+
content = @Content(mediaType = "application/json",
40+
schema = @Schema(implementation = GroupCreateResponse.class),
41+
examples = @ExampleObject(name = "성공", value = "{\"groupId\":123}")
42+
)
43+
),
44+
@ApiResponse(responseCode = "400", description = "잘못된 요청(최대 인원/카테고리 등)"),
45+
@ApiResponse(responseCode = "401", description = "인증 실패")
46+
})
47+
ResponseEntity<GroupCreateResponse> create(
48+
@Parameter(hidden = true) @AuthenticationPrincipal AuthPrinciple authPrinciple,
49+
@io.swagger.v3.oas.annotations.parameters.RequestBody(
50+
description = "그룹 생성 요청 바디",
51+
required = true,
52+
content = @Content(mediaType = "application/json",
53+
schema = @Schema(implementation = GroupCreateRequest.class),
54+
examples = @ExampleObject(name = "요청 예시", value = """
55+
{
56+
"name": "백엔드 스터디",
57+
"category": "IT",
58+
"description": "스프링/인프라 중심의 백엔드 스터디 그룹입니다.",
59+
"applicationRequired": true,
60+
"publicVisible": true,
61+
"maxMember": 20,
62+
"image": "https://cdn.example.com/group/cover.jpg"
63+
}
64+
""")
65+
)
66+
)
67+
@Valid GroupCreateRequest req
68+
);
69+
70+
//그룹 수정
71+
@Operation(
72+
summary = "그룹 수정",
73+
description = "기존 그룹 정보를 수정합니다. 오너만 가능합니다."
74+
)
75+
@ApiResponses({
76+
@ApiResponse(responseCode = "200", description = "수정 성공",
77+
content = @Content(mediaType = "application/json",
78+
schema = @Schema(implementation = GroupPutResponse.class),
79+
examples = @ExampleObject(name = "응답 예시", value = """
80+
{
81+
"name": "백엔드 스터디(수정)",
82+
"category": "IT",
83+
"description": "소개 수정",
84+
"applicationRequired": false,
85+
"publicVisible": true,
86+
"maxMember": 30,
87+
"imageUrl": "https://cdn.example.com/group/cover_v2.png",
88+
"createdAt": "2025-08-20T12:34:56",
89+
"modifiedAt": "2025-08-31T16:10:00"
90+
}
91+
""")
92+
)
93+
),
94+
@ApiResponse(responseCode = "400", description = "잘못된 요청(최대 인원/카테고리 등)"),
95+
@ApiResponse(responseCode = "401", description = "인증 실패"),
96+
@ApiResponse(responseCode = "403", description = "권한 없음(오너 아님)"),
97+
@ApiResponse(responseCode = "404", description = "그룹 없음")
98+
})
99+
ResponseEntity<GroupPutResponse> changeGroup(
100+
@Parameter(hidden = true) @AuthenticationPrincipal AuthPrinciple authPrinciple,
101+
@io.swagger.v3.oas.annotations.parameters.RequestBody(
102+
description = "그룹 수정 요청 바디",
103+
required = true,
104+
content = @Content(mediaType = "application/json",
105+
schema = @Schema(implementation = GroupPutRequest.class),
106+
examples = @ExampleObject(name = "요청 예시", value = """
107+
{
108+
"name": "백엔드 스터디(수정)",
109+
"category": "IT",
110+
"description": "소개 수정",
111+
"applicationRequired": false,
112+
"publicVisible": true,
113+
"maxMember": 30,
114+
"image": "https://cdn.example.com/group/cover_v2.png"
115+
}
116+
""")
117+
)
118+
)
119+
@Valid GroupPutRequest req,
120+
@Parameter(description = "그룹 ID", required = true, example = "1") Long groupId
121+
);
122+
123+
@Operation(summary = "그룹 상세", description = "그룹 상세 정보를 조회합니다. 그룹 멤버만 접근 가능합니다.")
124+
@ApiResponses({
125+
@ApiResponse(responseCode = "200", description = "조회 성공",
126+
content = @Content(mediaType = "application/json",
127+
schema = @Schema(implementation = GroupDetailResponse.class)
128+
)
129+
),
130+
@ApiResponse(responseCode = "401", description = "인증 실패"),
131+
@ApiResponse(responseCode = "404", description = "그룹 없음/그룹 내 유저 없음")
132+
})
133+
ResponseEntity<GroupDetailResponse> findGroupDetail(
134+
@Parameter(hidden = true) @AuthenticationPrincipal AuthPrinciple authPrinciple,
135+
@Parameter(description = "그룹 ID", required = true, example = "1") Long groupId
136+
);
137+
138+
@Operation(summary = "그룹 삭제", description = "오너만 그룹을 삭제할 수 있습니다. 오너 외 멤버가 존재하면 삭제 불가입니다.")
139+
@ApiResponses({
140+
@ApiResponse(responseCode = "204", description = "삭제 성공"),
141+
@ApiResponse(responseCode = "401", description = "인증 실패"),
142+
@ApiResponse(responseCode = "403", description = "권한 없음(오너 아님)"),
143+
@ApiResponse(responseCode = "404", description = "그룹 없음"),
144+
@ApiResponse(responseCode = "409", description = "오너 외 멤버 존재")
145+
})
146+
ResponseEntity<Void> deleteGroup(
147+
@Parameter(hidden = true) @AuthenticationPrincipal AuthPrinciple authPrinciple,
148+
@Parameter(description = "그룹 ID", required = true, example = "123") Long groupId
149+
);
150+
151+
@Operation(summary = "그룹내 멤버 조회", description = "그룹 멤버 목록을 조회합니다. 그룹 멤버만 접근 가능합니다.")
152+
@ApiResponses({
153+
@ApiResponse(responseCode = "200", description = "조회 성공",
154+
content = @Content(mediaType = "application/json",
155+
schema = @Schema(implementation = FindGroupMemberResponse.class)
156+
)
157+
),
158+
@ApiResponse(responseCode = "401", description = "인증 실패"),
159+
@ApiResponse(responseCode = "404", description = "그룹 없음/그룹 내 유저 없음")
160+
})
161+
ResponseEntity<FindGroupMemberResponse> findGroupMembers(
162+
@Parameter(hidden = true) @AuthenticationPrincipal AuthPrinciple authPrinciple,
163+
@Parameter(description = "그룹 ID", required = true, example = "123") Long groupId
164+
);
165+
166+
@Operation(summary = "그룹 전체 조회(커서 페이징)", description = "카테고리/커서/사이즈로 그룹 목록을 조회합니다.")
167+
@Parameters({
168+
@Parameter(
169+
name = "cursor",
170+
description = "커서 ID (이전 응답의 nextCursor). 기본값: null",
171+
example = "40",
172+
schema = @Schema(nullable = true)
173+
),
174+
@Parameter(
175+
name = "size",
176+
description = "페이지 크기. 기본값: 10",
177+
example = "10",
178+
schema = @Schema(defaultValue = "10", minimum = "1")
179+
),
180+
@Parameter(
181+
name = "category",
182+
description = "카테고리 필터 (예: IT). 기본값: null",
183+
example = "IT",
184+
schema = @Schema(nullable = true)
185+
),
186+
@Parameter(
187+
name = "sortBy",
188+
description = "(더미) 현재 미사용",
189+
example = "string",
190+
deprecated = true
191+
),
192+
@Parameter(
193+
name = "order",
194+
description = "(더미) 현재 미사용",
195+
example = "string",
196+
deprecated = true
197+
)
198+
})
199+
@ApiResponses({
200+
@ApiResponse(responseCode = "200", description = "조회 성공",
201+
content = @Content(mediaType = "application/json",
202+
schema = @Schema(implementation = CursorPagingResponse.class)
203+
)
204+
),
205+
@ApiResponse(responseCode = "400", description = "잘못된 요청(카테고리 등)"),
206+
@ApiResponse(responseCode = "401", description = "인증 실패")
207+
})
208+
ResponseEntity<CursorPagingResponse<GroupInfo>> findGroup(
209+
@Parameter(hidden = true) @AuthenticationPrincipal AuthPrinciple authPrinciple,
210+
@org.springdoc.core.annotations.ParameterObject
211+
@Valid GroupListRequest req
212+
);
213+
214+
@Operation(summary = "내 그룹 전체 조회(커서 페이징)", description = "현재 사용자 기준으로 가입한 그룹 목록을 커서 페이징으로 조회합니다.")
215+
@Parameters({
216+
@Parameter(
217+
name = "cursor",
218+
description = "커서 ID (이전 응답의 nextCursor). 기본값: null",
219+
example = "40",
220+
schema = @Schema(nullable = true)
221+
),
222+
@Parameter(
223+
name = "size",
224+
description = "페이지 크기. 기본값: 10",
225+
example = "10",
226+
schema = @Schema(defaultValue = "10", minimum = "1")
227+
),
228+
@Parameter(
229+
name = "category",
230+
description = "카테고리 필터 (예: IT). 기본값: null",
231+
example = "IT",
232+
schema = @Schema(nullable = true)
233+
),
234+
@Parameter(
235+
name = "sortBy",
236+
description = "(더미) 현재 미사용",
237+
example = "string",
238+
deprecated = true
239+
),
240+
@Parameter(
241+
name = "order",
242+
description = "(더미) 현재 미사용",
243+
example = "string",
244+
deprecated = true
245+
)
246+
})
247+
@ApiResponses({
248+
@ApiResponse(responseCode = "200", description = "조회 성공",
249+
content = @Content(mediaType = "application/json",
250+
schema = @Schema(implementation = CursorPagingResponse.class)
251+
)
252+
),
253+
@ApiResponse(responseCode = "400", description = "잘못된 요청(카테고리 등)"),
254+
@ApiResponse(responseCode = "401", description = "인증 실패")
255+
})
256+
ResponseEntity<CursorPagingResponse<GroupInfo>> findMyGroup(
257+
@Parameter(hidden = true) @AuthenticationPrincipal AuthPrinciple authPrinciple,
258+
@org.springdoc.core.annotations.ParameterObject
259+
@Valid GroupListRequest req
260+
);
261+
}
Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
11
package project.flipnote.group.model;
22

3+
import org.hibernate.validator.constraints.URL;
4+
35
import jakarta.validation.constraints.*;
46
import project.flipnote.group.entity.Category;
57

68
public record GroupCreateRequest(
7-
@NotBlank
8-
@Size(max = 50)
9-
String name,
9+
@NotBlank(message = "그룹 이름을 입력해주세요.")
10+
@Size(max = 50, message = "그룹 이름은 최대 50자까지 입력할 수 있습니다.")
11+
String name,
1012

11-
@NotNull
12-
Category category,
13+
@NotNull(message = "그룹 카테고리를 선택해야 합니다.")
14+
Category category,
1315

14-
@NotBlank
15-
@Size(max = 150)
16-
String description,
16+
@NotBlank(message = "그룹 설명을 입력해주세요.")
17+
@Size(max = 150, message = "그룹 설명은 최대 150자까지 입력할 수 있습니다.")
18+
String description,
1719

18-
@NotNull
19-
Boolean applicationRequired,
20+
@NotNull(message = "가입 승인 필요 여부를 선택해주세요.")
21+
Boolean applicationRequired,
2022

21-
@NotNull
22-
Boolean publicVisible,
23+
@NotNull(message = "공개 여부를 선택해주세요.")
24+
Boolean publicVisible,
2325

24-
@NotNull
25-
Integer maxMember,
26+
@NotNull(message = "최대 인원 수를 입력해주세요.")
27+
@Min(value = 1, message = "최대 인원 수는 1명 이상이어야 합니다.")
28+
@Max(value = 100, message = "최대 인원 수는 100명을 초과할 수 없습니다.")
29+
Integer maxMember,
2630

27-
String image
31+
@URL(message = "이미지 URL 형식이 올바르지 않습니다.")
32+
String image
2833
) {
2934
}
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
package project.flipnote.group.model;
22

3+
import org.hibernate.validator.constraints.URL;
4+
5+
import jakarta.validation.constraints.Max;
6+
import jakarta.validation.constraints.Min;
37
import jakarta.validation.constraints.NotBlank;
48
import jakarta.validation.constraints.NotNull;
59
import jakarta.validation.constraints.Size;
610
import project.flipnote.group.entity.Category;
711

812
public record GroupPutRequest(
9-
@NotBlank
10-
@Size(max = 50)
13+
@NotBlank(message = "그룹 이름을 입력해주세요.")
14+
@Size(max = 50, message = "그룹 이름은 최대 50자까지 입력할 수 있습니다.")
1115
String name,
1216

13-
@NotNull
17+
@NotNull(message = "그룹 카테고리를 선택해야 합니다.")
1418
Category category,
1519

16-
@NotBlank
17-
@Size(max = 150)
20+
@NotBlank(message = "그룹 설명을 입력해주세요.")
21+
@Size(max = 150, message = "그룹 설명은 최대 150자까지 입력할 수 있습니다.")
1822
String description,
1923

20-
@NotNull
24+
@NotNull(message = "가입 승인 필요 여부를 선택해주세요.")
2125
Boolean applicationRequired,
2226

23-
@NotNull
27+
@NotNull(message = "공개 여부를 선택해주세요.")
2428
Boolean publicVisible,
2529

26-
@NotNull
30+
@NotNull(message = "최대 인원 수를 입력해주세요.")
31+
@Min(value = 1, message = "최대 인원 수는 1명 이상이어야 합니다.")
32+
@Max(value = 100, message = "최대 인원 수는 100명을 초과할 수 없습니다.")
2733
Integer maxMember,
2834

35+
@URL(message = "이미지 URL 형식이 올바르지 않습니다.")
2936
String image
3037
) {
3138
}

0 commit comments

Comments
 (0)