Skip to content

Commit ab11004

Browse files
authored
Merge pull request #250 from JECT-Study/develop
release: develop → main
2 parents e5a18b3 + 2b390be commit ab11004

40 files changed

Lines changed: 1686 additions & 169 deletions
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
# 채팅방 API 개선안 — 선택지 배지 + 메시지 반응(이모지) + 답글
2+
3+
> 기반: "채팅_ 투표진행중_채팅 상세_개선" 5-1 ~ 5-6 요구사항
4+
> 버전: 1.0
5+
> 대상: 회원 (투표 참여자)
6+
7+
## 1. 요구사항 핵심 요약
8+
9+
### 5-1 선택지 배지 (본인)
10+
- 채팅방 진입 시 **본인이 선택한 A/B 배지**가 Chat Input **위 영역****상시 표시**
11+
- 본인 선택 변경 시 배지 정보 **실시간 반영**
12+
- A/B 선택지에 따라 시각적으로 다르게 표시
13+
14+
### 5-2 ~ 5-4 이모지 반응 (👍 👎)
15+
- **자신의 메시지**에는 Context Menu 호출 불가 (서버도 방어)
16+
- 말풍선 **0.5초 길게 누르기** → Context Menu (배경 dim)
17+
- 메뉴 항목: 👍 , 👎 , 답장
18+
- 반응
19+
- 동일 이모지 복수 사용자 → **누적 카운트**
20+
- 99개 이상 → `99+`
21+
- **본인이 누른 이모지 다시 누르면 취소**
22+
- 메뉴 자동 닫힘: 반응 선택 또는 "답장" 선택 시
23+
- "답장" 선택 → 메뉴 닫히고 **텍스트 입력 키패드 즉시 오픈**
24+
25+
### 5-5 ~ 5-6 답글 (Reply)
26+
- 메시지 길게 누르기 → 키패드 즉시 호출
27+
- 답글 작성 중 **원문 인용 박스** 상단 노출
28+
- X 버튼 → 답글 작성 취소
29+
- 인용 영역 선택 → 원문 메시지 위치로 스크롤 이동
30+
- **답글 메시지** 표시 규칙
31+
- 메시지 상단에 **원문 내용을 회색 + 말줄임**으로 표시
32+
33+
---
34+
35+
## 2. 데이터 모델 변경
36+
37+
### 2.1 ChatMessage 확장 (답글 지원)
38+
39+
```sql
40+
ALTER TABLE chat_message
41+
ADD COLUMN parent_message_id BIGINT NULL
42+
REFERENCES chat_message(id) ON DELETE SET NULL;
43+
44+
CREATE INDEX idx_chat_message_parent ON chat_message (parent_message_id);
45+
```
46+
47+
- `parent_message_id` = 답글 대상 원문 메시지 ID (null = 일반 메시지)
48+
49+
### 2.2 신규 테이블: chat_message_reaction
50+
51+
```sql
52+
CREATE TABLE chat_message_reaction (
53+
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
54+
message_id BIGINT NOT NULL REFERENCES chat_message(id) ON DELETE CASCADE,
55+
user_id BIGINT NOT NULL REFERENCES users(id),
56+
emoji VARCHAR(20) NOT NULL, -- THUMBS_UP | THUMBS_DOWN
57+
created_at TIMESTAMP NOT NULL DEFAULT now(),
58+
updated_at TIMESTAMP NOT NULL DEFAULT now(),
59+
60+
CONSTRAINT uq_chat_reaction UNIQUE (message_id, user_id)
61+
);
62+
63+
CREATE INDEX idx_chat_reaction_msg ON chat_message_reaction (message_id);
64+
CREATE INDEX idx_chat_reaction_user ON chat_message_reaction (user_id);
65+
```
66+
67+
**특징**
68+
- 회원 전용 (anonymous 없음) — 채팅은 투표 참여 회원만 가능
69+
- 1인 1반응 (동일 메시지). 다른 이모지 선택 시 row update, null 전송 시 삭제
70+
71+
### 2.3 신규 Enum (chat/domain)
72+
73+
```java
74+
package com.ject.vs.chat.domain;
75+
76+
public enum ChatReactionType {
77+
THUMBS_UP, // 👍
78+
THUMBS_DOWN // 👎
79+
}
80+
```
81+
82+
---
83+
84+
## 3. 기존 API 변경 사항
85+
86+
### 3.1 GET `/api/chats/{voteId}` — 채팅방 헤더 (배지 정보 제공)
87+
88+
**기존 응답에 추가**
89+
90+
```json
91+
{
92+
"voteId": 1,
93+
"title": "...",
94+
"status": "ONGOING",
95+
"participantCount": 25,
96+
"optionA": "...",
97+
"optionB": "...",
98+
"endAt": "...",
99+
"myVoteOption": "A" // ★ 신규: "A" | "B" | null
100+
}
101+
```
102+
103+
**변경 대상**
104+
- `ChatRoomResult``VoteOptionCode myVoteOption` 추가
105+
- `ChatRoomResponse` 에 동일 필드 + `@Schema`
106+
- `ChatService.getChatRoom(...)` 시그니처 변경 또는 내부에서 userId 사용
107+
- 현재 컨트롤러가 userId를 받지만 getChatRoom에 전달 안 함 → 수정 필요
108+
- 구현: `voteQueryUseCase.findSelectedOptionCode(voteId, userId).orElse(null)`
109+
110+
**5-1 배지 대응**: 이 응답으로 진입 시점 배지 렌더링. 선택 변경은 3.4 참고.
111+
112+
### 3.2 POST `/api/chats/{voteId}/messages` — 메시지 전송 (답글 지원)
113+
114+
**Request 변경 (하위 호환)**
115+
116+
```json
117+
{
118+
"content": "이것도 좋은 선택 같아요",
119+
"replyToMessageId": 1234 // optional. 없으면 일반 메시지
120+
}
121+
```
122+
123+
**변경 대상**
124+
- `SendMessageRequest`
125+
- `SendMessageCommand`
126+
- `ChatService.sendMessage(...)`
127+
128+
**서버 처리**
129+
- `replyToMessageId` 가 있으면:
130+
- 동일 `voteId` 소속 메시지인지 검증 (아니면 404 또는 400)
131+
- parent 메시지 내용 + 작성자 닉네임 스냅샷 준비 (preview 생성)
132+
- 저장 시 `ChatMessage.parentMessageId = replyToMessageId`
133+
134+
**응답** (`MessageResponse`)
135+
- 새 메시지에도 `replyTo` 정보 포함 (아래 3.3 참고)
136+
137+
### 3.3 GET `/api/chats/{voteId}/messages` — 메시지 목록 (reply + reaction 포함)
138+
139+
**각 메시지 응답 확장**
140+
141+
```json
142+
{
143+
"messageId": 1280,
144+
"content": "답글 내용입니다",
145+
"sentAt": "...",
146+
"senderNickname": "승부사",
147+
"senderProfileIcon": "GREEN",
148+
"senderVoteOption": "B",
149+
"isMine": true,
150+
151+
"replyTo": { // ★ 신규 (답글인 경우에만)
152+
"messageId": 1275,
153+
"senderNickname": "원작성자",
154+
"contentPreview": "원문 메시지 앞부분을 잘라서 보여줍니다..."
155+
},
156+
157+
"reactions": { // ★ 신규
158+
"THUMBS_UP": 14,
159+
"THUMBS_DOWN": 2
160+
},
161+
"myReaction": "THUMBS_UP" // ★ 신규 (내 반응, 없으면 null)
162+
}
163+
```
164+
165+
**변경 대상**
166+
- `MessageResult` record
167+
- `MessageResponse` record + from()
168+
- `MessagePageResponse`
169+
- `ChatService.getMessages(...)` 내부 enrichment 로직 대폭 수정
170+
171+
**Enrichment 구현 전략 (성능)**
172+
- 페이지 메시지 ID 목록 추출
173+
- parentMessageId 목록 별도 수집 → `chatMessageRepository.findAllById(...)` + User 정보
174+
- reaction:
175+
```java
176+
// ChatMessageReactionRepository
177+
List<ReactionCount> countByMessageIds(List<Long> messageIds);
178+
Optional<ChatReactionType> findMyReaction(Long messageId, Long userId);
179+
```
180+
- Map 으로 매핑 후 MessageResult 생성
181+
182+
**contentPreview**
183+
- 부모 메시지의 **전체 내용**을 그대로 내려줍니다.
184+
- 말줄임(ellipsis) 처리는 프론트엔드에서 담당합니다. (뷰의 관심사)
185+
186+
---
187+
188+
## 4. 신규 API
189+
190+
### 4.1 메시지 반응 추가/변경/취소
191+
192+
**PUT** `/api/chats/{voteId}/messages/{messageId}/reactions`
193+
194+
**Request**
195+
```json
196+
{ "emoji": "THUMBS_UP" } // "THUMBS_UP" | "THUMBS_DOWN" | null
197+
```
198+
199+
- `emoji` = 값 → 해당 반응 설정 (기존 반응이 있으면 교체)
200+
- `emoji` = `null` → 반응 취소
201+
202+
**Response** (204 No Content 대신 200 추천)
203+
```json
204+
{
205+
"messageId": 1280,
206+
"reactions": {
207+
"THUMBS_UP": 15,
208+
"THUMBS_DOWN": 2
209+
},
210+
"myReaction": "THUMBS_UP" // null 가능
211+
}
212+
```
213+
214+
**에러**
215+
- `403` : 자신의 메시지에 반응 시도 (`ChatForbiddenException` 재사용 또는 신규)
216+
- `403` : 투표 참여자가 아님
217+
- `404` : 메시지 또는 voteId 불일치
218+
219+
**구현 위치**
220+
- 신규: `ChatReactionCommandUseCase` (권장) 또는 `ChatCommandUseCase` 확장
221+
- `ChatService.react(...)`
222+
- `ChatMessageReactionRepository`
223+
224+
**트랜잭션 후**
225+
- `ChatReactionUpdatedEvent` 발행 → 리스너에서 WS 브로드캐스트
226+
227+
---
228+
229+
## 5. WebSocket (STOMP) 변경
230+
231+
### 5.1 기존 메시지 브로드캐스트 (`/topic/chat/{voteId}`)
232+
233+
기존 `MessageResult` 페이로드를 **3.3** 확장 형태로 전송 (replyTo + reactions 포함, `myReaction`은 항상 `null`).
234+
235+
### 5.2 반응 업데이트 브로드캐스트 (신규)
236+
237+
같은 토픽으로 다음 형태 전송:
238+
239+
```json
240+
{
241+
"event": "REACTION_UPDATED",
242+
"messageId": 1280,
243+
"reactions": {
244+
"THUMBS_UP": 15,
245+
"THUMBS_DOWN": 2
246+
}
247+
}
248+
```
249+
250+
클라이언트는 수신 즉시 해당 메시지의 reaction counts 를 갱신.
251+
252+
(개인 `myReaction`은 PUT 응답으로 이미 확정)
253+
254+
### 5.3 배지 실시간 업데이트 (권장)
255+
256+
**선택지 변경은 주로 Vote participate API에서 발생**하므로:
257+
258+
- 클라이언트가 `POST/PUT` participate 성공 시 응답의 `selectedOptionId``A/B` 변환 후 **로컬 배지 즉시 갱신** (가장 단순하고 정확)
259+
- 다중 탭/기기 동기화가 중요해지면 별도 개인 채널 고려:
260+
- `/user/topic/chat/{voteId}/my-option`
261+
- `{ "myVoteOption": "B" }`
262+
263+
현재 단계에서는 **헤더 + 클라이언트 상태 동기화**로 5-1 요구사항 충분히 만족 가능.
264+
265+
---
266+
267+
## 6. 아키텍처 / 코드 변경 포인트
268+
269+
| 위치 | 변경 내용 |
270+
|------|----------|
271+
| `chat/domain/ChatMessage.java` | `parentMessageId` 필드 + getter |
272+
| `chat/domain/ChatMessageRepository.java` | `findAllById(...)` (기본 제공), parent 조회용 추가 메서드 필요시 |
273+
| `chat/domain/` 신규 | `ChatMessageReaction.java`, `ChatMessageReactionRepository.java`, `ChatReactionType.java` |
274+
| `chat/port/in/dto/` | `MessageResult` 대폭 확장, `SendMessageCommand``replyToMessageId` 추가, 신규 `ReactMessageCommand`|
275+
| `chat/port/ChatService.java` | sendMessage reply 처리 + getMessages 대형 enrichment 로직 추가 + react 메서드 |
276+
| `chat/adapter/web/ChatController.java` + `ChatDocs.java` | 신규 반응 엔드포인트, 기존에 userId 전달 수정 |
277+
| `chat/adapter/web/dto/` | `MessageResponse`, `SendMessageRequest` 업데이트 |
278+
| `chat/adapter/event/` | `ChatMessageEventListener` : replyTo 포함 브로드캐스트 |
279+
| `vote/port/in/VoteQueryUseCase.java` | 기존 `findSelectedOptionCode` 재사용 (변경 없음) |
280+
| 신규 이벤트 | `ChatReactionUpdatedEvent` (선택) |
281+
282+
**메서드 네이밍 컨벤션 준수** (프로젝트 규칙)
283+
- `findAll...`, `findBy...`, `countBy...`, `existsBy...` 등 사용
284+
285+
---
286+
287+
## 7. 검증 / 엣지 케이스
288+
289+
- 자신의 메시지에 반응 → 거부
290+
- 삭제된 parent에 답글 → `replyTo.contentPreview = "(삭제된 메시지)"` (id는 유지하여 스크롤 이동 지원)
291+
- replyTo 메시지가 다른 vote → 400
292+
- 반응 99+ → 서버는 정확한 숫자 반환, 클라이언트에서 `count > 99 ? "99+" : count`
293+
- 시스템 메시지 (senderId=0) → 반응/답글 불가
294+
- 투표 종료 후에도 과거 메시지 반응/조회는 허용? (기존 정책 유지. 별도 확인 필요)
295+
- 동시에 여러 반응 race → UNIQUE 제약 + upsert 로 처리
296+
297+
---
298+
299+
## 8. 구현 우선순위 추천
300+
301+
1. **DB + 도메인** : ChatMessage parent + ChatMessageReaction 테이블/엔티티
302+
2. **메시지 전송 + 목록** : replyTo 기능 먼저 (5-5, 5-6)
303+
3. **채팅방 헤더** : myVoteOption 추가 (5-1)
304+
4. **반응 기능** : PUT API + 목록 enrichment + WS (5-2~5-4)
305+
5. **문서화 + 테스트** + 클라이언트 연동 확인
306+
6. **(선택)** 다중 기기 배지 동기화를 위한 WS 이벤트
307+
308+
---
309+
310+
## 9. OpenAPI / 문서 업데이트
311+
312+
- `ChatDocs.java` 에 신규 Operation 추가
313+
- `swagger.json` 재생성 또는 수동 반영
314+
- 기존 `docs/spec/2026-04-26-chat-api-spec.md` 에 "v2 개선" 섹션 또는 별도 이 문서 링크
315+
316+
---
317+
318+
## 10. 클라이언트 협업 포인트
319+
320+
- Context Menu / long-press 는 **클라이언트 전담**
321+
- 99+ formatting, 말줄임, X 버튼, 인용 박스 클릭 시 scroll-to-id → 클라이언트
322+
- 답글 작성 중 원문 quote box는 `replyTo` 데이터로 렌더
323+
- 배지 위치는 항상 Input 위 고정 (채팅방 진입 + myVoteOption 기준)
324+
325+
이 개선안을 적용하면 5-1~5-6 요구사항을 모두 만족하면서 기존 채팅 API와 호환성을 유지할 수 있습니다.

0 commit comments

Comments
 (0)