Skip to content

Commit c577e64

Browse files
authored
[Feature] 배틀 진행 현황판 UI 개선 (#140)
* feat: ProgressBoard 가로 크기 확장 및 6단계 표시 로직 구현 #140 - 가로 크기를 400px에서 850px로 확장 - 6단계 정의 추가 (의견공유 → 공격(1차) → 수비(1차) → 공격(2차) → 수비(2차) → 팀변경) - isActiveStage 함수로 현재 활성 단계 확인 로직 구현 - STAGES 배열을 map으로 순회하여 6개 아이콘 렌더링 * feat: 텍스트 라벨 제거 #140 - 배틀 진행 상황 텍스트 제거 - Round 텍스트 제거 - 아이콘 영역 상단 여백 조정 * feat: 라운드-페이즈 번호 표시 및 아이콘 라벨 제거 #140 - 맨 왼쪽에 현재 라운드-페이즈 번호 표시 - PENDING 상태일 때 배틀 대기중으로 표시 - StageIcon에서 아이콘 밑 라벨 텍스트 제거 * feat: 프로그레스 바를 ProgressBoard 하단에 통합 #140 - TimeProgressBar 로직을 ProgressBoard에 복사하여 통합 - 프로그레스 바를 아이콘 영역 하단에 배치 - 그라데이션 색상 적용 - shrink 애니메이션으로 남은 시간 시각화 * style: 레이아웃 크기 조정 및 라운드 표시 개선 #140 - 가로 크기 축소 (850px → 750px) - 라운드-페이즈 구분자에 공백 추가 - 라운드 표시 글자 크기 증가 및 높이 조정 * fix: 라운드-페이즈 번호 계산 로직 수정 #140 - phaseCount 기반으로 step 번호 계산 (1-6 순환) - turn 속성 제거하고 phaseCount 활용 - 각 라운드마다 1-1부터 1-6까지 표시되도록 수정 * fix: phaseCount 기반 step 계산 로직 수정 #140 - phase와 phaseCount를 조합하여 정확한 step 계산 - ATTACK: phaseCount * 2, DEFENSE: phaseCount * 2 + 1 * style: 대기 상태 텍스트 간소화 #140 - 배틀 대기중 → 대기중으로 변경 - 줄바꿈 방지를 위한 텍스트 길이 축소 * feat: 배틀 현황판 아이콘 활성화/비활성화 상태 및 크기 변화 효과 추가 #140 - 활성화된 아이콘: 원래 색상 유지 + 크기 증가 (48px → 56px) - 비활성화된 아이콘: 회색 배경(#0A0A1A) + 회색 테두리(#364153) + 회색 아이콘(#99A1AF) - 모든 아이콘 크기 통일 (기존 의견공유 아이콘만 컸던 문제 해결) - transition 효과 추가: duration-300 ease-in-out으로 부드러운 전환 * refactor: 배틀 헤더에서 TimeProgressBar 제거 #140 - TimeProgressBar 컴포넌트 import 및 렌더링 제거 - 프로그레스 바는 ProgressBoard에 통합되어 중복 제거 - 헤더 높이 조정 (185px → 179px) * style: sticky 옵션 개선 및 상단 주황색 줄 제거 #140 - z-index를 0에서 10으로 상향하여 sticky 동작 개선 - 상단 주황색 gradient 줄 제거 (프로그레스 바와 혼동 방지) - 패딩 조정 (pt-6 → pt-2) * style: 배틀 현황판 높이 최소화 #140 - 전체 높이 감소 (143.33px → 100px) - 하단 패딩 추가 (pb-3)로 프로그레스 바 하단 여백 최적화 - 상단 여백 감소 (top-[16px] → top-[12px]) - 요소 간 gap 감소 (gap-2 → gap-1.5) - 프로그레스 바 상단 여백 감소 (mt-4 → mt-1.5) * fix: 배틀 현황판 sticky 동작 수정 #140 - ProgressBoard를 div 래퍼 밖으로 이동하여 sticky 제대로 동작하도록 수정 - 마진 조정으로 기존 레이아웃 유지 (-mt-[10px]) * feat: 페이즈 아이콘에 상세 설명 툴팁 추가 #140 - StageIcon 컴포넌트에 tooltip prop 추가 - 호버 시 각 페이즈의 상세 설명을 아이콘 하단에 표시 - 툴팁 내용: - 의견공유: 대주제 확인 및 이의제기 준비 안내 - 1차/2차 공격: 상대 코드 문제점 지적 및 투표 안내 - 1차/2차 수비: 반박 논리 작성 및 방어 내용 선정 안내 - 팀변경: 진영 재선택 가능 안내 - 툴팁 디자인: 다크 테마 배경, 상단 화살표, 부드러운 페이드 효과 * feat: 대기 중 현황판 숨김 및 시작 시 슬라이드 다운 애니메이션 추가 #140 - PENDING 상태일 때 ProgressBoard 컴포넌트 완전히 숨김 처리 - 배틀 시작 시 슬라이드 다운 애니메이션 효과 추가 (0.5s ease-out) - 애니메이션: 상단에서 하단으로 슬라이드되며 페이드 인 * style: 라운드 표시 방식을 Round N 형태로 변경 및 글자 크기 조정 #140 - 기존 라운드-페이즈 형태에서 Round N으로 간소화 - 페이즈 정보는 활성화된 아이콘으로 시각적 표현 - 글자 크기 감소 (32px → 22px, 약 30% 축소) * refactor: 사용되지 않는 raiseZIndex prop 제거 #140 * style: 툴팁 화살표 테두리 추가 및 두께 조정 #140
1 parent 7235aa0 commit c577e64

File tree

4 files changed

+196
-74
lines changed

4 files changed

+196
-74
lines changed

frontend/src/pages/battlePage/components/header/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import ParticipantRatioBar from './ParticipantRatioBar';
22
import StageIndicator from './StageIndicator';
33
import BattleTimer from './BattleTimer';
44
import TeamCounter from './TeamCounter';
5-
import TimeProgressBar from './TimeProgressBar';
65
import {
76
useBattleStore,
87
selectBattleProgress,
@@ -33,10 +32,9 @@ export default function BattleHeader() {
3332

3433
return (
3534
<header
36-
className="h-[185px] w-[1800px] bg-[#1E1E2F] rounded-lg mb-2 overflow-hidden flex flex-col"
35+
className="h-[179px] w-[1800px] bg-[#1E1E2F] rounded-lg mb-2 overflow-hidden flex flex-col"
3736
data-tutorial="phase-guide"
3837
>
39-
<TimeProgressBar />
4038
<div className="px-8 flex-1 grid grid-cols-3 items-center">
4139
<StageIndicator />
4240
<div className="flex flex-col items-center justify-center" data-tutorial="timer">

frontend/src/pages/battlePage/components/progressBoard/ProgressBoard.tsx

Lines changed: 141 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useMemo } from 'react';
22
import { StageIcon } from './StageIcon';
33
import DownArrowIcon from '@/assets/icon/downArrow.svg?react';
44
import { selectBattleProgress, useBattleStore } from '../../stores/battleStore';
@@ -13,42 +13,121 @@ const COLOR_MAP = {
1313
PENDING: { bg: '#0A0A1A', border: '#364153', text: '#99A1AF' }
1414
};
1515

16-
interface BattleProgressBoardProps {
17-
raiseZIndex?: boolean;
18-
}
19-
20-
export default function BattleProgressBoard({ raiseZIndex = false }: BattleProgressBoardProps) {
16+
// 6단계 정의 (한 라운드당 6개 페이즈)
17+
const STAGES = [
18+
{
19+
step: 1,
20+
phase: 'OPINION_SHARE',
21+
icon: 'message',
22+
label: '의견공유',
23+
description:
24+
'의견공유 시간입니다. 현재 라운드의 대주제를 확인하고 라운지에서 자유롭게 의견을 나누며 이의제기를 준비해주세요!'
25+
},
26+
{
27+
step: 2,
28+
phase: 'ATTACK',
29+
icon: 'battle',
30+
label: '공격(1차)',
31+
description:
32+
'1차 이의제기 시간입니다. 주어진 3분 동안 상대 코드의 문제점을 찾고 투표를 통해 공격할 내용을 선정해보세요!'
33+
},
34+
{
35+
step: 3,
36+
phase: 'DEFENSE',
37+
icon: 'shield',
38+
label: '수비(1차)',
39+
description:
40+
'1차 반론 시간입니다. 주어진 4분 동안 상대의 공격에 대한 반박 논리를 작성하고 투표를 통해 방어할 내용을 선정해보세요!'
41+
},
42+
{
43+
step: 4,
44+
phase: 'ATTACK',
45+
icon: 'battle',
46+
label: '공격(2차)',
47+
description:
48+
'2차 이의제기 시간입니다. 주어진 3분 동안 상대 코드 혹은 반론에 대한 문제점을 찾고 투표를 통해 공격할 내용을 선정해보세요!'
49+
},
50+
{
51+
step: 5,
52+
phase: 'DEFENSE',
53+
icon: 'shield',
54+
label: '수비(2차)',
55+
description:
56+
'2차 반론 시간입니다. 주어진 4분 동안 상대의 공격에 대한 반박 논리를 작성하고 투표를 통해 최종적으로 방어할 내용을 선정해보세요!'
57+
},
58+
{
59+
step: 6,
60+
phase: 'TEAM_SWITCH',
61+
icon: 'switch',
62+
label: '팀변경',
63+
description:
64+
'진영 선택 시간입니다. 라운드를 진행하며 나눈 공격과 수비를 다시 확인해보고 지지하시는 팀으로 변경하실 수 있습니다!'
65+
}
66+
] as const;
67+
68+
export default function BattleProgressBoard() {
2169
const [collapsed, setCollapsed] = useState(false);
2270
const battleProgress = useBattleStore(selectBattleProgress);
71+
72+
// 프로그레스 바 duration 계산
73+
const duration = useMemo(() => {
74+
if (!battleProgress?.expiredAt || !battleProgress?.startedAt) return 60;
75+
return Math.max(0, (battleProgress.expiredAt - battleProgress.startedAt) / 1000);
76+
}, [battleProgress?.expiredAt, battleProgress?.startedAt]);
77+
2378
if (!battleProgress) return null;
2479

25-
const { round, phase } = battleProgress;
80+
const { round, phase, phaseCount } = battleProgress;
81+
82+
// PENDING 상태일 때는 아예 렌더링하지 않음
83+
if ((phase as string) === 'PENDING') {
84+
return null;
85+
}
2686

27-
const isActive = (stage: BattlePhase) => phase === stage;
87+
// 현재 페이즈의 step 번호 계산 (1-6)
88+
const getCurrentPhaseStep = () => {
89+
// phaseCount는 ATTACK/DEFENSE 반복 횟수 (1~BATTLE_MAX_PHASE_COUNT)
90+
// phase와 phaseCount로 step 계산
91+
if (phase === 'OPINION_SHARE') return 1;
92+
if (phase === 'ATTACK') return phaseCount * 2; // 1차: 2, 2차: 4
93+
if (phase === 'DEFENSE') return phaseCount * 2 + 1; // 1차: 3, 2차: 5
94+
if (phase === 'TEAM_SWITCH') return 6;
95+
return 1;
96+
};
2897

29-
const getStageColor = (stage: BattlePhase) => (isActive(stage) ? COLOR_MAP[stage] : COLOR_MAP.BASE);
98+
// 현재 활성 단계인지 확인
99+
const isActiveStage = (stageStep: number) => {
100+
return getCurrentPhaseStep() === stageStep;
101+
};
102+
103+
// 현재 라운드 번호 표시
104+
const getCurrentStageNumber = () => {
105+
return `Round ${round}`;
106+
};
107+
108+
const getStageColor = (stage: BattlePhase) => COLOR_MAP[stage] || COLOR_MAP.BASE;
30109

31110
return (
32111
<div
33-
className={`
34-
sticky top-0
112+
className="
113+
sticky top-0 z-10
35114
flex justify-center
36115
pointer-events-none
37-
${raiseZIndex ? 'z-[110]' : 'z-0'}
38-
`}
116+
animate-slideDown
117+
"
39118
>
40119
<div
41120
data-tutorial="progress-board"
42121
className={`
43122
pointer-events-auto
44123
relative
45-
w-[400px]
124+
w-[750px]
46125
rounded-lg
47126
bg-[#1a1a2ef2]
48127
border-b-[1.333px] border-b-[#1E2939]
49128
shadow-[0px_25px_50px_-12px_rgba(0,0,0,0.25)]
50129
transition-all duration-300 ease-in-out
51-
${collapsed ? '-translate-y-[120px] h-[32px]' : 'h-[143.33px]'}
130+
${collapsed ? '-translate-y-[120px] h-[32px]' : 'h-[100px] pb-3'}
52131
`}
53132
>
54133
{!collapsed && (
@@ -107,52 +186,62 @@ export default function BattleProgressBoard({ raiseZIndex = false }: BattleProgr
107186

108187
<div
109188
className={`
110-
absolute left-[26.21px] top-[16px]
111-
w-[347.58px]
112-
flex flex-col gap-2
189+
absolute left-[26.21px] top-[12px]
190+
w-[700px]
191+
flex flex-col gap-1.5
113192
transition-opacity duration-200
114193
${collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'}
115194
`}
116195
>
117-
<div
118-
className="absolute left-[6px] top-[-4px] h-[4px] w-[336px]
119-
bg-gradient-to-r from-[#FF6900] via-[#FF8904] to-[#F54900]"
120-
/>
121-
122-
<div className="text-center text-[12px] tracking-[0.6px] uppercase text-[#FF6900]">배틀 진행 상황</div>
123-
124-
<div className="flex items-center gap-4">
125-
<StageIcon
126-
label="의견 공유"
127-
icon="message"
128-
{...getStageColor('OPINION_SHARE')}
129-
active={isActive('OPINION_SHARE')}
130-
/>
131-
132-
<DownArrowIcon className="-rotate-90 opacity-40 -translate-y-2" />
133-
134-
<StageIcon label="이의제기" icon="battle" small {...getStageColor('ATTACK')} active={isActive('ATTACK')} />
135-
136-
<DownArrowIcon className="-rotate-90 opacity-40 -translate-y-2" />
137-
138-
<StageIcon label="반박" icon="shield" small {...getStageColor('DEFENSE')} active={isActive('DEFENSE')} />
139-
140-
<DownArrowIcon className="-rotate-90 opacity-40 -translate-y-2" />
141-
142-
<StageIcon
143-
label="팀 변경"
144-
icon="switch"
145-
small
146-
{...getStageColor('TEAM_SWITCH')}
147-
active={isActive('TEAM_SWITCH')}
148-
/>
196+
<div className="flex items-center gap-4 px-4">
197+
<div className="text-[22px] font-bold text-[#FF6900] min-w-[100px] flex items-center justify-center h-[56px]">
198+
{getCurrentStageNumber()}
199+
</div>
200+
{STAGES.map((stage, index) => (
201+
<div key={stage.step} className="flex items-center gap-3">
202+
<StageIcon
203+
icon={stage.icon as 'message' | 'battle' | 'shield' | 'switch'}
204+
{...getStageColor(stage.phase as BattlePhase)}
205+
active={isActiveStage(stage.step)}
206+
small={true}
207+
tooltip={stage.description}
208+
/>
209+
{index < STAGES.length - 1 && <DownArrowIcon className="-rotate-90 opacity-40" />}
210+
</div>
211+
))}
149212
</div>
150213

151-
<div className="relative mt-1 h-[15px]">
152-
<div className="text-center text-[10px] font-bold text-[#6A7282]">Round {round}</div>
214+
<div className="mt-1.5 h-[6px] bg-[#0A0A1A] rounded-full overflow-hidden">
215+
<div
216+
key={`${battleProgress?.expiredAt}-${battleProgress?.startedAt}`}
217+
className="h-full bg-gradient-to-r from-[#FF6900] via-[#FF8904] to-[#F54900]"
218+
style={{
219+
animation: `shrink ${duration}s linear`,
220+
animationFillMode: 'forwards'
221+
}}
222+
/>
153223
</div>
154224
</div>
155225
</div>
226+
<style>{`
227+
@keyframes shrink {
228+
from { width: 100%; }
229+
to { width: 0%; }
230+
}
231+
@keyframes slideDown {
232+
from {
233+
transform: translateY(-100%);
234+
opacity: 0;
235+
}
236+
to {
237+
transform: translateY(0);
238+
opacity: 1;
239+
}
240+
}
241+
.animate-slideDown {
242+
animation: slideDown 0.5s ease-out;
243+
}
244+
`}</style>
156245
</div>
157246
);
158247
}

frontend/src/pages/battlePage/components/progressBoard/StageIcon.tsx

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,28 @@ import ShieldIcon from '@/assets/icon/shield.svg?react';
44
import SwitchIcon from '@/assets/icon/switch.svg?react';
55

66
export function StageIcon({
7-
label,
87
icon,
98
bg,
109
border,
1110
text,
1211
active,
13-
small
12+
small,
13+
tooltip
1414
}: {
15-
label: string;
1615
icon: 'message' | 'battle' | 'shield' | 'switch';
1716
bg: string;
1817
border: string;
1918
text: string;
2019
active?: boolean;
2120
small?: boolean;
21+
tooltip?: string;
2222
}) {
23-
const size = small ? 'w-[48px] h-[48px]' : 'w-[52.8px] h-[52.8px]';
24-
const iconClass = `w-[20px] h-[20px] ${active ? '' : 'opacity-40'}`;
25-
const iconStyle = active ? { color: text } : undefined;
23+
const baseSize = small ? 48 : 52.8;
24+
const activeSize = small ? 56 : 60;
25+
const size = active ? activeSize : baseSize;
26+
27+
const iconClass = `w-[20px] h-[20px] transition-all`;
28+
const iconStyle = active ? { color: text } : { color: '#99A1AF' };
2629

2730
const renderIcon = () => {
2831
switch (icon) {
@@ -40,27 +43,59 @@ export function StageIcon({
4043
};
4144

4245
return (
43-
<div className="flex flex-col items-center gap-1">
46+
<div className="relative flex flex-col items-center gap-1 group">
4447
<div
4548
className={`
46-
${size}
4749
rounded-[12px]
4850
flex items-center justify-center
4951
border-[1.333px]
50-
transition-all
52+
transition-all duration-300 ease-in-out
5153
${active ? 'shadow-[0px_20px_25px_-5px_rgba(0,0,0,0.25)]' : ''}
5254
`}
5355
style={{
54-
background: bg,
55-
borderColor: border
56+
width: `${size}px`,
57+
height: `${size}px`,
58+
background: active ? bg : '#0A0A1A',
59+
borderColor: active ? border : '#364153'
5660
}}
5761
>
5862
{renderIcon()}
5963
</div>
60-
61-
<div className="text-[10px] font-bold text-center" style={{ color: active ? text : '#99A1AF' }}>
62-
{label}
63-
</div>
64+
{tooltip && (
65+
<div
66+
className="
67+
absolute top-full mt-2 px-3 py-2
68+
bg-[#1E1E2F] text-white text-sm rounded-lg
69+
shadow-lg border border-[#364153]
70+
opacity-0 group-hover:opacity-100
71+
pointer-events-none
72+
transition-opacity duration-200
73+
whitespace-nowrap z-20
74+
"
75+
>
76+
{tooltip}
77+
{/* 테두리용 화살표 (더 크게) */}
78+
<div
79+
className="
80+
absolute bottom-full left-1/2 -translate-x-1/2 translate-y-[0.5px]
81+
w-0 h-0
82+
border-l-[8px] border-l-transparent
83+
border-r-[8px] border-r-transparent
84+
border-b-[8px] border-b-[#364153]
85+
"
86+
/>
87+
{/* 배경색 화살표 (작게, 위에 겹침) */}
88+
<div
89+
className="
90+
absolute bottom-full left-1/2 -translate-x-1/2
91+
w-0 h-0
92+
border-l-[6px] border-l-transparent
93+
border-r-[6px] border-r-transparent
94+
border-b-[6px] border-b-[#1E1E2F]
95+
"
96+
/>
97+
</div>
98+
)}
6499
</div>
65100
);
66101
}

frontend/src/pages/battlePage/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ export default function BattlePage() {
111111
isSidebarOpen ? 'ml-sidebar' : 'ml-0'
112112
}`}
113113
>
114-
<div className={`transition-all duration-300 ${isSidebarOpen ? 'main-width-open' : 'main-width-closed'}`}>
115-
<div className="-mb-[10px]">
116-
<BattleProgressBoard raiseZIndex={isTutorialOpen && currentStep === 'progressBoard'} />
117-
</div>
114+
<BattleProgressBoard />
115+
<div
116+
className={`transition-all duration-300 ${isSidebarOpen ? 'main-width-open' : 'main-width-closed'} -mt-[10px]`}
117+
>
118118
<BattleHeader />
119119
</div>
120120
<main className={`transition-all duration-300 ${isSidebarOpen ? 'main-width-open' : 'main-width-closed'}`}>

0 commit comments

Comments
 (0)