Skip to content

Commit 1acc0c3

Browse files
authored
Merge pull request #162 from prgrms-web-devcourse-final-project/feature/EA3-111-groupchat
[EA3-111] feature : 그룹채팅 과거 채팅 페이징 조회 API 구현 및 Swagger 문서 수정
2 parents d93c48a + 71ef388 commit 1acc0c3

15 files changed

+509
-214
lines changed

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ dependencies {
8686
// 배포 관련 의존성
8787
// runtimeOnly 'org.postgresql:postgresql'
8888
implementation 'org.springframework.boot:spring-boot-devtools'
89-
9089
implementation 'com.google.cloud:spring-cloud-gcp-starter-secretmanager:4.9.1'
9190
implementation 'com.google.cloud:google-cloud-storage:2.38.0'
91+
92+
// WebSocket + STOMP 통신용
93+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
9294
}
9395

9496
tasks.named('test') {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package grep.neogul_coder.domain.groupchat.controller;
2+
3+
import grep.neogul_coder.domain.groupchat.controller.dto.response.GroupChatMessageResponseDto;
4+
import grep.neogul_coder.domain.groupchat.service.GroupChatService;
5+
import grep.neogul_coder.global.response.ApiResponse;
6+
import grep.neogul_coder.global.response.PageResponse;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.web.bind.annotation.*;
9+
10+
@RestController
11+
@RequiredArgsConstructor
12+
@RequestMapping("/api/chat")
13+
public class GroupChatRestController implements GroupChatRestSpecification {
14+
15+
private final GroupChatService groupChatService;
16+
17+
// 과거 채팅 메시지 페이징 조회 (무한 스크롤용)
18+
@Override
19+
@GetMapping("/room/{roomId}/messages")
20+
public ApiResponse<PageResponse<GroupChatMessageResponseDto>> getMessages(
21+
@PathVariable("roomId") Long roomId,
22+
@RequestParam(defaultValue = "0") int page,
23+
@RequestParam(defaultValue = "20") int size
24+
) {
25+
// 서비스에서 페이징된 메시지 조회
26+
PageResponse<GroupChatMessageResponseDto> pageResponse =
27+
groupChatService.getMessages(roomId, page, size);
28+
29+
return ApiResponse.success(pageResponse);
30+
}
31+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package grep.neogul_coder.domain.groupchat.controller;
2+
3+
import grep.neogul_coder.domain.groupchat.controller.dto.response.GroupChatMessageResponseDto;
4+
import grep.neogul_coder.global.response.ApiResponse;
5+
import grep.neogul_coder.global.response.PageResponse;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.RequestParam;
11+
12+
@Tag(name = "GroupChat", description = "채팅 메시지 조회 API (무한 스크롤용)")
13+
public interface GroupChatRestSpecification {
14+
15+
@Operation(
16+
summary = "채팅 메시지 페이징 조회",
17+
description = """
18+
이 API는 **채팅방의 과거 메시지**를 페이지 단위로 가져오는 용도입니다.
19+
WebSocket의 실시간 수신(`/sub/chat/room/{roomId}`)과는 별개로,
20+
채팅방에 입장할 때 이전 대화 기록을 불러오는 데 사용됩니다.
21+
22+
---
23+
24+
**프론트엔드 연동 흐름 (권장 방식)**:
25+
1. **채팅방 입장 시** → `GET /api/chat/room/{roomId}/messages?page=0&size=20` 호출해 최신 메시지 20개 로드
26+
2. **스크롤 위로 올릴 때** → `page=1`, `page=2` ... 순차적으로 과거 메시지를 추가 로딩 (무한 스크롤)
27+
3. **동시에** → WebSocket(`wss://wibby.cedartodo.uk/ws-stomp`) 연결 후 `/sub/chat/room/{roomId}`를 **구독**해 실시간 메시지 수신
28+
29+
---
30+
31+
**파라미터 설명**:
32+
- `roomId`: 채팅방 ID
33+
- `page`: 페이지 번호 (0부터 시작, 0 = 최신 메시지 20개)
34+
- `size`: 한 페이지당 메시지 수 (기본값 20)
35+
- 메시지는 **오래된 순(오름차순)**으로 반환됩니다.
36+
37+
---
38+
39+
**응답 구조**:
40+
- `ApiResponse<PageResponse<GroupChatMessageResponseDto>>` 형태
41+
- `content()`로 메시지 목록 접근 가능
42+
- 페이지네이션 정보: `currentNumber()`, `prevPage()`, `nextPage()` 등
43+
44+
---
45+
46+
**예시 요청 URL**:
47+
```
48+
/api/chat/room/1/messages?page=0&size=20
49+
```
50+
51+
**예시 응답**:
52+
```json
53+
{
54+
"success": true,
55+
"data": {
56+
"content": [
57+
{
58+
"id": 101,
59+
"roomId": 1,
60+
"senderId": 10,
61+
"senderNickname": "유강현",
62+
"profileImageUrl": "https://example.com/profile.jpg",
63+
"message": "안녕하세요!",
64+
"sentAt": "2025-07-21T14:32:00"
65+
}
66+
],
67+
"currentNumber": 0,
68+
"nextPage": 1,
69+
"prevPage": null
70+
}
71+
}
72+
```
73+
"""
74+
)
75+
76+
ApiResponse<PageResponse<GroupChatMessageResponseDto>> getMessages(
77+
@Parameter(description = "채팅방 ID", example = "1")
78+
@PathVariable("roomId") Long roomId,
79+
80+
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0")
81+
@RequestParam(defaultValue = "0") int page,
82+
83+
@Parameter(description = "한 페이지당 메시지 수", example = "20")
84+
@RequestParam(defaultValue = "20") int size
85+
);
86+
}

src/main/java/grep/neogul_coder/domain/groupchat/controller/GroupChatSwaggerSpecification.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,61 @@
88

99
import java.util.List;
1010

11-
@Tag(name = "GroupChat-Swagger", description = "WebSocket 구조 설명용 Swagger 문서")
11+
@Tag(name = "GroupChat", description = "WebSocket 구조 설명용 Swagger 문서")
1212
public interface GroupChatSwaggerSpecification {
1313

14-
@Operation(summary = "Pub 채팅 메시지 전송 구조 예시", description = "실제 채팅 전송은 WebSocket을 통해 `/pub/chat/message`로 전송됩니다.")
14+
@Operation(summary = "채팅 메시지 전송 (WebSocket Pub)",
15+
description = """
16+
**실제 채팅 메시지 전송은 WebSocket 연결 후에 이루어집니다.**
17+
18+
**1. WebSocket 연결**
19+
- 먼저 `wss://wibby.cedartodo.uk/ws-stomp` 엔드포인트로 STOMP 연결을 맺습니다.
20+
21+
**2. 메시지 전송**
22+
- 연결이 완료된 후 `/pub/chat/message` 경로로 메시지를 보냅니다.
23+
24+
**예시 Request JSON**
25+
```json
26+
{
27+
"roomId": 1,
28+
"senderId": 10,
29+
"message": "안녕하세요!"
30+
}
31+
```
32+
33+
** Swagger에서 이 API를 실행해도 실제 전송은 되지 않으며,
34+
WebSocket 통신 구조를 이해하기 위한 문서 예시입니다.**
35+
""")
1536
ApiResponse<GroupChatSwaggerResponse> sendMessage(GroupChatSwaggerRequest request);
1637

17-
@Operation(summary = "Sub 채팅 메시지 수신 구조 예시", description = "실제 메시지 수신은 WebSocket을 통해 `/sub/chat/room/{roomId}`로 수신됩니다.")
38+
@Operation(summary = "채팅 메시지 수신 (WebSocket Sub)",
39+
description = """
40+
**실제 메시지 수신 또한 WebSocket 연결이 필수입니다.**
41+
42+
**1. WebSocket 연결**
43+
- 먼저 `wss://wibby.cedartodo.uk/ws-stomp` 엔드포인트로 STOMP 연결을 맺습니다.
44+
45+
**2. 메시지 구독**
46+
- 연결 후 `/sub/chat/room/{roomId}` 경로를 구독(subscribe)하면 해당 채팅방의 새로운 메시지를 실시간으로 수신할 수 있습니다.
47+
48+
**예시 Subscribe 경로**
49+
`/sub/chat/room/1`
50+
51+
**예시 수신 데이터(JSON)**
52+
```json
53+
{
54+
"id": 101,
55+
"roomId": 1,
56+
"senderId": 10,
57+
"senderNickname": "유강현",
58+
"profileImageUrl": "https://example.com/profile.jpg",
59+
"message": "안녕하세요!",
60+
"sentAt": "2025-07-21T14:32:00"
61+
}
62+
```
63+
64+
** Swagger에서는 WebSocket 구독을 테스트할 수 없으며,
65+
이 문서는 프론트엔드 구현 참고용입니다.**
66+
""")
1867
ApiResponse<List<GroupChatSwaggerResponse>> getMessages(Long roomId);
1968
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package grep.neogul_coder.domain.groupchat.controller;
2+
3+
import grep.neogul_coder.domain.groupchat.controller.dto.requset.GroupChatMessageRequestDto;
4+
import grep.neogul_coder.domain.groupchat.controller.dto.response.GroupChatMessageResponseDto;
5+
import grep.neogul_coder.domain.groupchat.service.GroupChatService;
6+
import io.swagger.v3.oas.annotations.Hidden;
7+
import org.springframework.messaging.handler.annotation.MessageMapping;
8+
import org.springframework.messaging.handler.annotation.SendTo;
9+
import org.springframework.messaging.simp.SimpMessagingTemplate;
10+
import org.springframework.stereotype.Controller;
11+
12+
// Swagger 문서에 노출되지 않도록 설정
13+
@Hidden
14+
@Controller
15+
public class GroupChatWebSocketController {
16+
17+
private final GroupChatService groupChatService;
18+
private final SimpMessagingTemplate messagingTemplate;
19+
20+
// 생성자 주입을 통해 필요한 서비스와 템플릿 객체 주입
21+
public GroupChatWebSocketController(GroupChatService groupChatService,
22+
SimpMessagingTemplate messagingTemplate) {
23+
this.groupChatService = groupChatService;
24+
this.messagingTemplate = messagingTemplate;
25+
}
26+
27+
// 클라이언트가 /pub/chat/message 로 보낼 때 처리됨
28+
@MessageMapping("/chat/message")
29+
public void handleMessage(GroupChatMessageRequestDto requestDto) {
30+
// 메시지를 DB에 저장하고, 응답 DTO 생성
31+
GroupChatMessageResponseDto responseDto = groupChatService.saveMessage(requestDto);
32+
33+
// 구독 중인 클라이언트에게 메시지 전송 (채팅방 구분)
34+
// 클라이언트는 /sub/chat/room/{roomId} 구독 중이어야 실시간으로 수신 가능
35+
messagingTemplate.convertAndSend(
36+
"/sub/chat/room/" + requestDto.getRoomId(), // 메시지를 받을 대상
37+
responseDto // 클라이언트에 전달할 응답 메시지 DTO
38+
);
39+
}
40+
}
Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,32 @@
1-
//package grep.neogul_coder.domain.groupchat.controller.dto;
2-
//
3-
//public class GroupChatMessageRequestDto {
4-
// private Long roomId;
5-
// private Long senderId;
6-
// private String message;
7-
//
8-
// public GroupChatMessageRequestDto() {}
9-
//
10-
// public GroupChatMessageRequestDto(Long roomId, Long senderId, String message) {
11-
// this.roomId = roomId;
12-
// this.senderId = senderId;
13-
// this.message = message;
14-
// }
15-
//
16-
// public Long getRoomId() {
17-
// return roomId;
18-
// }
19-
//
20-
// public void setRoomId(Long roomId) {
21-
// this.roomId = roomId;
22-
// }
23-
//
24-
// public Long getSenderId() {
25-
// return senderId;
26-
// }
27-
//
28-
// public void setSenderId(Long senderId) {
29-
// this.senderId = senderId;
30-
// }
31-
//
32-
// public String getMessage() {
33-
// return message;
34-
// }
35-
//
36-
// public void setMessage(String message) {
37-
// this.message = message;
38-
// }
39-
//}
1+
package grep.neogul_coder.domain.groupchat.controller.dto.requset;
2+
3+
import io.swagger.v3.oas.annotations.Hidden;
4+
import lombok.Getter;
5+
6+
@Hidden
7+
@Getter
8+
public class GroupChatMessageRequestDto {
9+
private Long roomId;
10+
private Long senderId;
11+
private String message;
12+
13+
public GroupChatMessageRequestDto() {}
14+
15+
public GroupChatMessageRequestDto(Long roomId, Long senderId, String message) {
16+
this.roomId = roomId;
17+
this.senderId = senderId;
18+
this.message = message;
19+
}
20+
21+
public void setRoomId(Long roomId) {
22+
this.roomId = roomId;
23+
}
24+
25+
public void setSenderId(Long senderId) {
26+
this.senderId = senderId;
27+
}
28+
29+
public void setMessage(String message) {
30+
this.message = message;
31+
}
32+
}

0 commit comments

Comments
 (0)