Skip to content

Commit c8131db

Browse files
committed
[Feat] 질문 상태 변경 SSE 이벤트 반영
1 parent 9faf6bf commit c8131db

5 files changed

Lines changed: 137 additions & 14 deletions

File tree

‎backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,19 @@ public record QuestionCreatedEvent(
243243
) {
244244
}
245245

246+
// 좋아요, 해결 상태, 본문 수정, 삭제처럼 기존 질문의 상태가 바뀔 때 내려가는 SSE 이벤트
247+
public record QuestionUpdatedEvent(
248+
String type,
249+
Long sessionId,
250+
Long questionId,
251+
String content,
252+
Boolean isResolved,
253+
Integer likeCount,
254+
Boolean isDeleted,
255+
LocalDateTime updatedAt
256+
) {
257+
}
258+
246259
// 운영진이 이해도 체크를 생성했을 때 SSE로 내려가는 이벤트.
247260
// 같은 세션 질문방을 보고 있는 모든 클라이언트에게 전파된다.
248261
public record UnderstandingCheckCreatedEvent(

‎backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionEventService.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ public void publishQuestionCreated(Long sessionId, QuestionResDTO.QuestionCreate
5050
broadcast(sessionId, "question-created", event);
5151
}
5252

53+
// 질문 상태 변경 이벤트를 같은 세션 질문방을 구독 중인 모든 클라이언트에게 전파한다.
54+
public void publishQuestionUpdated(Long sessionId, QuestionResDTO.QuestionUpdatedEvent event) {
55+
broadcast(sessionId, "question-updated", event);
56+
}
57+
5358
// 이해도 체크 생성 이벤트를 같은 세션 질문방을 구독 중인 모든 클라이언트에게 전파한다.
5459
public void publishUnderstandingCheckCreated(Long sessionId, QuestionResDTO.UnderstandingCheckCreatedEvent event) {
5560
broadcast(sessionId, "understanding-check-created", event);

‎backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ public QuestionResDTO.LikeRes toggleLike(Long questionId, Long userId) {
281281
Question question = findQuestion(questionId);
282282

283283
// 이미 좋아요를 눌렀는지 확인
284-
return questionLikeRepository.findByQuestionAndUser(question, loginUser)
284+
QuestionResDTO.LikeRes result = questionLikeRepository.findByQuestionAndUser(question, loginUser)
285285
.map(existingLike -> {
286286
// 이미 눌렀으면 → 취소 (삭제 + likeCount -1)
287287
questionLikeRepository.delete(existingLike);
@@ -298,6 +298,9 @@ public QuestionResDTO.LikeRes toggleLike(Long questionId, Long userId) {
298298
question.increaseLikeCount();
299299
return new QuestionResDTO.LikeRes(question.getId(), question.getLikeCount(), true);
300300
});
301+
302+
publishQuestionUpdatedEventAfterCommit(question, false);
303+
return result;
301304
}
302305

303306
// 질문 수정
@@ -313,6 +316,8 @@ public QuestionResDTO.UpdateDeleteRes updateQuestion(
313316

314317
question.updateContent(request.getContent());
315318

319+
publishQuestionUpdatedEventAfterCommit(question, false);
320+
316321
return new QuestionResDTO.UpdateDeleteRes(
317322
question.getId(), question.getContent(),
318323
question.getUpdatedAt(), question.getDeletedAt()
@@ -328,6 +333,8 @@ public QuestionResDTO.UpdateDeleteRes deleteQuestion(Long questionId, Long userI
328333

329334
question.softDelete();
330335

336+
publishQuestionUpdatedEventAfterCommit(question, true);
337+
331338
return new QuestionResDTO.UpdateDeleteRes(
332339
question.getId(), question.getContent(),
333340
question.getUpdatedAt(), question.getDeletedAt()
@@ -379,6 +386,8 @@ public QuestionResDTO.StatusUpdateRes updateQuestionStatus(Long questionId, Long
379386
Question question = findQuestion(questionId);
380387
question.markResolved();
381388

389+
publishQuestionUpdatedEventAfterCommit(question, false);
390+
382391
return new QuestionResDTO.StatusUpdateRes(
383392
question.getId(), question.getIsResolved(), question.getUpdatedAt()
384393
);
@@ -759,6 +768,23 @@ private void publishQuestionCreatedEventAfterCommit(Question question) {
759768
publishAfterCommit(() -> questionEventService.publishQuestionCreated(sessionId, event));
760769
}
761770

771+
private void publishQuestionUpdatedEventAfterCommit(Question question, boolean isDeleted) {
772+
Long sessionId = question.getSession().getId();
773+
774+
QuestionResDTO.QuestionUpdatedEvent event = new QuestionResDTO.QuestionUpdatedEvent(
775+
"QUESTION_UPDATED",
776+
sessionId,
777+
question.getId(),
778+
question.getContent(),
779+
question.getIsResolved(),
780+
question.getLikeCount(),
781+
isDeleted,
782+
question.getUpdatedAt()
783+
);
784+
785+
publishAfterCommit(() -> questionEventService.publishQuestionUpdated(sessionId, event));
786+
}
787+
762788
private void publishUnderstandingCheckCreatedEventAfterCommit(
763789
Long sessionId, UnderstandingCheck check, int attendanceCount
764790
) {

‎frontend/src/pages/qna/QnADetailPage.js‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import profileImg from '../../assets/images/profile.png';
1414
import { authFetch } from '../../utils/Api';
1515
import { subscribeQuestionEvents } from '../../utils/sse';
1616

17+
const POPULAR_LIKE_THRESHOLD = 5;
18+
1719
// 시간만 표시하는 포맷 함수 (HH:MM)
1820
const formatTime = (dateStr) => {
1921
if (!dateStr) return '';
@@ -129,15 +131,36 @@ function QnADetailPage() {
129131
}, [questionId, fetchQuestion]);
130132

131133
const handleQuestionEvent = useCallback((message) => {
132-
if (message.event !== 'comment-created') {
134+
if (String(message.data?.questionId) !== String(questionId)) {
133135
return;
134136
}
135-
if (String(message.data?.questionId) !== String(questionId)) {
137+
138+
if (message.event === 'comment-created') {
139+
void fetchQuestion();
136140
return;
137141
}
138142

139-
void fetchQuestion();
140-
}, [fetchQuestion, questionId]);
143+
if (message.event === 'question-updated') {
144+
if (message.data?.isDeleted) {
145+
navigate(-1);
146+
return;
147+
}
148+
149+
setQuestion(prev => {
150+
if (!prev) return prev;
151+
152+
return {
153+
...prev,
154+
content: message.data.content ?? prev.content,
155+
isResolved: message.data.isResolved ?? prev.isResolved,
156+
isPopular: message.data.isResolved === true
157+
? false
158+
: (message.data.likeCount ?? prev.likeCount) >= POPULAR_LIKE_THRESHOLD,
159+
likeCount: message.data.likeCount ?? prev.likeCount,
160+
};
161+
});
162+
}
163+
}, [fetchQuestion, navigate, questionId]);
141164

142165
useEffect(() => {
143166
if (!sessionId || !questionId) {

‎frontend/src/pages/qna/QnAListPage.js‎

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import {
1313
const MAX_VISIBLE_COMMENTS = 3;
1414
const POPULAR_LIKE_THRESHOLD = 5;
1515

16+
const getCreatedAtTime = (question) => new Date(question.createdAt ?? 0).getTime();
17+
18+
const sortQuestionGroups = (groups) => ({
19+
popularQuestions: [...groups.popularQuestions].sort(
20+
(a, b) => (b.likeCount ?? 0) - (a.likeCount ?? 0) || getCreatedAtTime(b) - getCreatedAtTime(a)
21+
),
22+
unresolvedQuestions: [...groups.unresolvedQuestions].sort(
23+
(a, b) => getCreatedAtTime(b) - getCreatedAtTime(a)
24+
),
25+
resolvedQuestions: [...groups.resolvedQuestions].sort(
26+
(a, b) => getCreatedAtTime(b) - getCreatedAtTime(a)
27+
),
28+
});
29+
1630
const getQuestionGroupKey = (question) => {
1731
if (question.isResolved) return 'resolvedQuestions';
1832
if ((question.likeCount ?? 0) >= POPULAR_LIKE_THRESHOLD) return 'popularQuestions';
@@ -33,7 +47,7 @@ const regroupQuestions = (questions) => {
3347
groups[getQuestionGroupKey(question)].push(question);
3448
});
3549

36-
return groups;
50+
return sortQuestionGroups(groups);
3751
};
3852

3953
const updateQuestionGroupsByCommentEvent = (groups, eventData) => {
@@ -62,6 +76,41 @@ const updateQuestionGroupsByCommentEvent = (groups, eventData) => {
6276
return hasUpdatedQuestion ? regroupQuestions(questions) : groups;
6377
};
6478

79+
const updateQuestionGroupsByQuestionEvent = (groups, eventData) => {
80+
if (!eventData?.questionId) return groups;
81+
82+
const questions = [
83+
...groups.popularQuestions,
84+
...groups.unresolvedQuestions,
85+
...groups.resolvedQuestions,
86+
];
87+
88+
if (eventData.isDeleted) {
89+
return regroupQuestions(questions.filter(question => question.questionId !== eventData.questionId));
90+
}
91+
92+
let hasUpdatedQuestion = false;
93+
const updatedQuestions = questions.map(question => {
94+
if (question.questionId !== eventData.questionId) return question;
95+
96+
hasUpdatedQuestion = true;
97+
const isResolved = eventData.isResolved ?? question.isResolved;
98+
const likeCount = eventData.likeCount ?? question.likeCount;
99+
100+
return {
101+
...question,
102+
content: eventData.content ?? question.content,
103+
isResolved,
104+
isPopular: !isResolved && (likeCount ?? 0) >= POPULAR_LIKE_THRESHOLD,
105+
likeCount,
106+
iLiked: eventData.isLiked ?? question.iLiked,
107+
isLiked: eventData.isLiked ?? question.isLiked,
108+
};
109+
});
110+
111+
return hasUpdatedQuestion ? regroupQuestions(updatedQuestions) : groups;
112+
};
113+
65114
const addQuestionToGroups = (groups, question) => {
66115
if (!question?.questionId) return groups;
67116

@@ -274,6 +323,11 @@ function QnAListPage() {
274323
applyQuestionGroups(nextGroups);
275324
}, [applyQuestionGroups, buildQuestionFromCreatedEvent]);
276325

326+
const handleQuestionUpdatedEvent = useCallback((eventData) => {
327+
const nextGroups = updateQuestionGroupsByQuestionEvent(questionGroupsRef.current, eventData);
328+
applyQuestionGroups(nextGroups);
329+
}, [applyQuestionGroups]);
330+
277331
const handleUnderstandingCheckCreatedEvent = useCallback((eventData) => {
278332
if (!eventData?.checkId) return;
279333
if (understandingRef.current?.current?.checkId === eventData.checkId) return;
@@ -335,6 +389,9 @@ function QnAListPage() {
335389
case 'question-created':
336390
void handleQuestionCreatedEvent(data);
337391
break;
392+
case 'question-updated':
393+
handleQuestionUpdatedEvent(data);
394+
break;
338395
case 'understanding-check-created':
339396
handleUnderstandingCheckCreatedEvent(data);
340397
break;
@@ -348,6 +405,7 @@ function QnAListPage() {
348405
}, [
349406
handleCommentCreatedEvent,
350407
handleQuestionCreatedEvent,
408+
handleQuestionUpdatedEvent,
351409
handleUnderstandingCheckCreatedEvent,
352410
handleUnderstandingResponseUpdatedEvent,
353411
]);
@@ -415,14 +473,12 @@ function QnAListPage() {
415473
if (!res.ok) throw new Error();
416474
const json = await res.json();
417475
if (json.isSuccess) {
418-
const update = (list) => list.map(q =>
419-
q.questionId === questionId
420-
? { ...q, likeCount: json.result.likeCount, iLiked: json.result.isLiked }
421-
: q
422-
);
423-
setPopularQuestions(update);
424-
setUnresolvedQuestions(update);
425-
setResolvedQuestions(update);
476+
const nextGroups = updateQuestionGroupsByQuestionEvent(questionGroupsRef.current, {
477+
questionId,
478+
likeCount: json.result.likeCount,
479+
isLiked: json.result.isLiked,
480+
});
481+
applyQuestionGroups(nextGroups);
426482
}
427483
} catch (err) {
428484
console.error('좋아요 실패:', err);

0 commit comments

Comments
 (0)