feat: 문제집 풀이 — 번호 스크롤 네비 추가

- 답안 패널 위에 가로 스크롤 번호 칩 바 추가
- 현재 문제는 인디고 글로우, 답안 완료는 보라색, 표시(★)는 주황
- 문제 전환 시 현재 번호로 자동 스크롤 (smooth center)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
reloop
2026-04-19 16:23:49 +09:00
parent 59da6a3578
commit e6f54d1dc4

View File

@@ -727,6 +727,8 @@ export default function ExamPage() {
</ProblemTextCard>
</ProblemPanel>
<ProblemScrollNav problems={problems} answers={answers} flagged={flagged} currentIndex={currentIndex} onJump={moveProblem} />
<AnswerPanel>
<AnswerPanelHeader>
<AnswerPanelTitle> </AnswerPanelTitle>
@@ -925,6 +927,54 @@ export default function ExamPage() {
return focusMode ? examContent : <AppShell>{examContent}</AppShell>;
}
function ProblemScrollNav({
problems,
answers,
flagged,
currentIndex,
onJump,
}: {
problems: ProblemSetDetail['problems'];
answers: AnswerMap;
flagged: Record<number, boolean>;
currentIndex: number;
onJump: (index: number) => void;
}) {
const scrollRef = useRef<HTMLDivElement>(null);
const activeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!activeRef.current || !scrollRef.current) return;
const container = scrollRef.current;
const chip = activeRef.current;
const left = chip.offsetLeft - container.offsetWidth / 2 + chip.offsetWidth / 2;
container.scrollTo({ left, behavior: 'smooth' });
}, [currentIndex]);
return (
<ScrollNavWrap ref={scrollRef}>
{problems.map((problem, index) => {
const answered = answers[problem.id] !== undefined && answers[problem.id] !== null;
const isFlagged = !!flagged[problem.id];
const isCurrent = index === currentIndex;
return (
<ScrollNavChip
key={problem.id}
ref={isCurrent ? activeRef : undefined}
type="button"
$current={isCurrent}
$answered={answered}
$flagged={isFlagged}
onClick={() => onJump(index)}
>
{problem.number}
</ScrollNavChip>
);
})}
</ScrollNavWrap>
);
}
function toWholeSeconds(ms: number): number {
if (ms <= 0) return 0;
return Math.max(1, Math.round(ms / 1000));
@@ -1296,6 +1346,59 @@ const AudioElement = styled.audio`
display: none;
`;
const ScrollNavWrap = styled.div`
display: flex;
gap: 6px;
overflow-x: auto;
padding: 4px 2px;
scroll-snap-type: x mandatory;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
`;
const ScrollNavChip = styled.button<{
$current: boolean;
$answered: boolean;
$flagged: boolean;
}>`
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 36px;
padding: 0 8px;
flex-shrink: 0;
scroll-snap-align: center;
border-radius: 10px;
border: 1px solid
${({ $current, $flagged, $answered }) =>
$current
? 'rgba(129, 140, 248, 0.7)'
: $flagged
? 'rgba(245, 158, 11, 0.55)'
: $answered
? 'rgba(99, 102, 241, 0.35)'
: theme.color.borderSoftAlpha};
background: ${({ $current, $answered }) =>
$current
? 'rgba(79, 70, 229, 0.28)'
: $answered
? 'rgba(79, 70, 229, 0.1)'
: 'rgba(255, 255, 255, 0.03)'};
color: ${({ $current }) =>
$current ? theme.color.textBright : theme.color.textSub};
font-size: 13px;
font-weight: ${({ $current }) => ($current ? 800 : 600)};
font-family: ${theme.font.mono};
transition: all 0.12s ease;
box-shadow: ${({ $current }) =>
$current ? theme.shadow.glowIndigo : 'none'};
`;
const Workspace = styled.main`
display: grid;
gap: 24px;