Skip to content

Commit ea597bb

Browse files
authored
Merge pull request #300 from TTORANG/develop
deploy: 3.2.0 배포
2 parents 19e0882 + 59ed8a7 commit ea597bb

4 files changed

Lines changed: 257 additions & 124 deletions

File tree

docs/api-connect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<aside>
22

33
## 전체 레이어 구조
4-
4+
```
55
src/
66
├── api/
77
│ ├── dto/ # [서버 규격] Request/Response 인터페이스 정의함
@@ -21,7 +21,7 @@ src/
2121
2222
└── types/ # [도메인 모델] 앱 내부에서 공통으로 쓰는 순수 데이터 타입임
2323
└── presentation.ts # camelCase로 정제된 프로젝트 모델임
24-
24+
```
2525
</aside>
2626

2727
## 1. 레이어 구조 및 역할

src/components/comment/Comment.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function Comment({ comment, isIndented = false, rootCommentId }: CommentProps) {
131131
className={clsx(
132132
'flex gap-3 py-3 pr-4 transition-colors',
133133
isIndented ? 'pl-15' : 'pl-4',
134-
isEditing ? 'bg-gray-100' : isActive ? 'bg-gray-200' : 'bg-gray-100',
134+
isEditing ? 'bg-gray-100' : isActive ? 'bg-gray-200' : '',
135135
)}
136136
>
137137
<div className="w-8 shrink-0">
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @file ReactionBubble.tsx
3+
* @description 영상 재생바 위에 현재 구간 리액션을 요약하여 보여주는 버블 컴포넌트
4+
*
5+
* - 현재 재생시간 ±windowMs 범위 내 리액션을 표시
6+
* - 상위 3개까지 노출, 4개 이상이면 상위 3개 + "..." 로 축약
7+
* - "..." 클릭 시 팝오버로 전체 5종 표시
8+
*/
9+
import { useCallback, useEffect, useRef, useState } from 'react';
10+
11+
import { REACTION_CONFIG, REACTION_TYPES } from '@/constants/reaction';
12+
import { useVideoReactionWindow } from '@/hooks/queries/useVideoReactionQueries';
13+
import type { ReactionType } from '@/types/script';
14+
15+
interface ReactionBubbleProps {
16+
videoId: string | undefined;
17+
currentTimeMs: number;
18+
windowMs?: number;
19+
}
20+
21+
export default function ReactionBubble({
22+
videoId,
23+
currentTimeMs,
24+
windowMs = 5000,
25+
}: ReactionBubbleProps) {
26+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
27+
const popoverRef = useRef<HTMLDivElement>(null);
28+
29+
// 500ms 단위로 쿼리 키를 스냅하여 과도한 리패치 방지
30+
const snappedMs = Math.round(currentTimeMs / 500) * 500;
31+
32+
const { data: reactions } = useVideoReactionWindow(videoId, snappedMs, windowMs);
33+
34+
useEffect(() => {
35+
if (!isPopoverOpen) return;
36+
const handleClick = (e: MouseEvent) => {
37+
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
38+
setIsPopoverOpen(false);
39+
}
40+
};
41+
document.addEventListener('mousedown', handleClick);
42+
return () => document.removeEventListener('mousedown', handleClick);
43+
}, [isPopoverOpen]);
44+
45+
const handleTogglePopover = useCallback(() => {
46+
setIsPopoverOpen((prev) => !prev);
47+
}, []);
48+
49+
// 리액션 데이터를 정규화
50+
const reactionMap = new Map<ReactionType, number>();
51+
if (reactions) {
52+
for (const r of reactions) {
53+
reactionMap.set(r.emojiType, (reactionMap.get(r.emojiType) ?? 0) + r.count);
54+
}
55+
}
56+
57+
// count > 0인 항목만 추출, count 내림차순
58+
const activeReactions = REACTION_TYPES.map((type) => ({
59+
type,
60+
emoji: REACTION_CONFIG[type].emoji,
61+
count: reactionMap.get(type) ?? 0,
62+
}))
63+
.filter((r) => r.count > 0)
64+
.sort((a, b) => b.count - a.count);
65+
66+
if (activeReactions.length === 0) return null;
67+
68+
const displayItems = activeReactions.slice(0, 3);
69+
const hasMore = activeReactions.length >= 4;
70+
71+
// 전체 5종 목록 (팝오버용)
72+
const allReactions = REACTION_TYPES.map((type) => ({
73+
type,
74+
emoji: REACTION_CONFIG[type].emoji,
75+
label: REACTION_CONFIG[type].label,
76+
count: reactionMap.get(type) ?? 0,
77+
}));
78+
79+
return (
80+
<div ref={popoverRef} className="relative inline-flex">
81+
<button
82+
type="button"
83+
onClick={hasMore ? handleTogglePopover : undefined}
84+
className="flex items-center gap-1 md:gap-2 rounded-full bg-black/70 px-2 py-1 md:px-3 md:py-1.5 text-white backdrop-blur-sm text-caption md:text-body-s"
85+
>
86+
{displayItems.map((item) => (
87+
<span key={item.type} className="inline-flex items-center gap-0.5 md:gap-1">
88+
<span className="text-xs md:text-sm">{item.emoji}</span>
89+
<span>{item.count}</span>
90+
</span>
91+
))}
92+
{hasMore && <span className="text-gray-300">···</span>}
93+
</button>
94+
95+
{isPopoverOpen && (
96+
<div className="absolute bottom-full left-0 mb-2 w-40 md:w-52 rounded-lg bg-black/85 p-2 md:p-3 text-white backdrop-blur-sm shadow-lg">
97+
<p className="mb-1.5 md:mb-2 text-caption-bold md:text-body-s-bold text-gray-300">
98+
전체 이모지 반응 보기
99+
</p>
100+
<div className="flex flex-col gap-1 md:gap-1.5">
101+
{allReactions.map((item) => (
102+
<div key={item.type} className="flex items-center justify-between">
103+
<span className="flex items-center gap-1 md:gap-1.5">
104+
<span className="text-xs md:text-sm">{item.emoji}</span>
105+
<span className="text-caption md:text-body-s text-gray-300">{item.label}</span>
106+
</span>
107+
<span className="text-caption-bold md:text-body-s-bold">{item.count}</span>
108+
</div>
109+
))}
110+
</div>
111+
</div>
112+
)}
113+
</div>
114+
);
115+
}

0 commit comments

Comments
 (0)