feat: 문제집 탭 통합 — 프리미엄/내 문제집/샘플 3섹션 구조

- exams 페이지를 3개 섹션으로 재구성:
  1. 프리미엄 문제집 (B2B 코드 등록)
  2. 내 문제집 (PDF 업로드 + 크롭 + 직접 등록)
  3. ReLoop 샘플 문제집
- SideNav에서 'PDF 문제집' 항목 제거, 문제집 탭으로 통합
- 빈 섹션에 안내 메시지 + 아이콘 표시

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
reloop
2026-04-19 16:28:24 +09:00
parent e6f54d1dc4
commit aa3ab3a1a7
2 changed files with 173 additions and 147 deletions

View File

@@ -182,62 +182,85 @@ function ExamsBody() {
const visibleSampleEbooks = EBOOK_LIST.filter((e) => !hiddenEbooks.includes(e.id));
const hasMyContent = myProblemSets.length > 0 || uploadedPdfs.length > 0;
return (
<Wrap>
<HeaderCard>
<PageHeader
eyebrow="Exams"
title="모의고사 문제집"
subtitle="학년별 샘플 ebook을 선택하거나, 코드로 프리미엄 문제집을 추가해봐"
title="문제집"
subtitle="프리미엄 · 내 문제집 · 샘플을 한눈에 관리하고 풀어보세요"
/>
</HeaderCard>
{/* 액션 카드 행 */}
<ActionRow>
<ActionCard onClick={() => setShowCodeModal(true)}>
<ActionIcon>
<Icon name="magnifying-glass" size={28} />
</ActionIcon>
<ActionContent>
<ActionTitle> </ActionTitle>
<ActionDesc>QR ebook이 </ActionDesc>
</ActionContent>
<Button type="button" $variant="primary" $size="sm" onClick={(e) => { e.stopPropagation(); setShowCodeModal(true); }}>
{/* ── 섹션 1: B2B 프리미엄 문제집 ─────────────────────────────────── */}
<Section>
<SectionHeader>
<SectionHeaderLeft>
<SectionIcon $tone="premium">
<Icon name="crown" size={18} />
</SectionIcon>
<div>
<SectionTitle2> </SectionTitle2>
<SectionDesc>QR B2B </SectionDesc>
</div>
</SectionHeaderLeft>
<Button type="button" $variant="primary" $size="sm" onClick={() => setShowCodeModal(true)}>
<Icon name="magnifying-glass" size={14} />
</Button>
</ActionCard>
</SectionHeader>
<ActionCard>
<ActionIcon>
<Icon name="pencil-simple" size={28} />
</ActionIcon>
<ActionContent>
<ActionTitle> </ActionTitle>
<ActionDesc> PDF를 </ActionDesc>
</ActionContent>
{premiumEbooks.length > 0 ? (
<Grid>
{premiumEbooks.map((ebook) => (
<PremiumEbookCard
key={ebook.id}
ebook={ebook}
router={router}
onDelete={handleDeletePremiumEbook}
/>
))}
</Grid>
) : (
<EmptySection>
<EmptySectionIcon>
<Icon name="crown" size={24} />
</EmptySectionIcon>
<EmptySectionText> </EmptySectionText>
<EmptySectionHint> ebook이 </EmptySectionHint>
</EmptySection>
)}
</Section>
{/* ── 섹션 2: 내 문제집 (PDF 업로드 + 크롭) ──────────────────────── */}
<Section>
<SectionHeader>
<SectionHeaderLeft>
<SectionIcon $tone="pdf">
<Icon name="file-pdf" size={18} />
</SectionIcon>
<div>
<SectionTitle2> </SectionTitle2>
<SectionDesc>PDF를 </SectionDesc>
</div>
</SectionHeaderLeft>
<ActionButtons>
<Button type="button" $variant="secondary" $size="sm" onClick={() => setShowProblemModal(true)}>
<Icon name="pencil-simple" size={14} />
</Button>
<Button type="button" $variant="secondary" $size="sm" onClick={() => setShowPdfModal(true)}>
<Icon name="file-pdf" size={14} />
PDF
</Button>
<Button type="button" $variant="primary" $size="sm" onClick={() => router.push('/study/import-pdf')}>
<Icon name="file-pdf" size={14} />
PDF
</Button>
</ActionButtons>
</ActionCard>
</ActionRow>
</SectionHeader>
{/* 내가 만든 문제집 (PDF 크롭) */}
{myProblemSets.length > 0 && (
<>
<SectionLabel>
<Icon name="file-pdf" size={14} />
</SectionLabel>
{hasMyContent ? (
<Grid>
{myProblemSets.map((ps) => (
<MyProblemSetCard
@@ -256,69 +279,51 @@ function ExamsBody() {
}}
/>
))}
</Grid>
</>
)}
{/* 프리미엄 ebook 섹션 */}
{premiumEbooks.length > 0 && (
<>
<SectionLabel>
<Icon name="crown" size={14} />
</SectionLabel>
<Grid>
{premiumEbooks.map((ebook) => (
<PremiumEbookCard
key={ebook.id}
ebook={ebook}
router={router}
onDelete={handleDeletePremiumEbook}
/>
))}
</Grid>
</>
)}
{/* 업로드된 PDF 섹션 */}
{uploadedPdfs.length > 0 && (
<>
<SectionLabel>
<Icon name="file-pdf" size={14} />
PDF
</SectionLabel>
<Grid>
{uploadedPdfs.map((pdf) => (
<PdfCard key={pdf.url} pdf={pdf} onDelete={handleDeletePdf} />
))}
</Grid>
</>
)}
) : (
<EmptySection>
<EmptySectionIcon>
<Icon name="file-pdf" size={24} />
</EmptySectionIcon>
<EmptySectionText> </EmptySectionText>
<EmptySectionHint> PDF를 </EmptySectionHint>
</EmptySection>
)}
</Section>
{/* 기본 샘플 ebook 그리드 */}
<SectionLabel>
<Icon name="book-open-text" size={14} />
</SectionLabel>
<Grid>
{visibleSampleEbooks.map((ebook) => (
<SampleEbookCard
key={ebook.id}
ebook={ebook}
router={router}
onDelete={handleDeleteSampleEbook}
/>
))}
</Grid>
{/* ── 섹션 3: ReLoop 샘플 문제집 ─────────────────────────────────── */}
<Section>
<SectionHeader>
<SectionHeaderLeft>
<SectionIcon $tone="sample">
<Icon name="book-open-text" size={18} />
</SectionIcon>
<div>
<SectionTitle2>ReLoop </SectionTitle2>
<SectionDesc> </SectionDesc>
</div>
</SectionHeaderLeft>
{hiddenEbooks.length > 0 && (
<RestoreLink type="button" onClick={handleRestoreAll}>
{hiddenEbooks.length}
</RestoreLink>
)}
</SectionHeader>
{/* 숨긴 ebook 복원 */}
{hiddenEbooks.length > 0 && (
<RestoreRow>
<RestoreLink type="button" onClick={handleRestoreAll}>
{hiddenEbooks.length} ·
</RestoreLink>
</RestoreRow>
)}
<Grid>
{visibleSampleEbooks.map((ebook) => (
<SampleEbookCard
key={ebook.id}
ebook={ebook}
router={router}
onDelete={handleDeleteSampleEbook}
/>
))}
</Grid>
</Section>
{/* 모달들 */}
{showCodeModal && (
@@ -821,73 +826,104 @@ const HeaderCard = styled(Card)`
border-color: ${theme.color.borderSoftAlpha};
`;
const ActionRow = styled.div`
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
@media (max-width: ${theme.breakpoint.tablet}) {
grid-template-columns: 1fr;
}
const Section = styled.section`
display: flex;
flex-direction: column;
gap: ${theme.space.md};
padding: 20px;
border: 1px solid ${theme.color.borderSoftAlpha};
border-radius: 20px;
background: rgba(255, 255, 255, 0.015);
`;
const ActionCard = styled(Card)`
const SectionHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: ${theme.space.md};
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
&:hover {
border-color: ${theme.color.accent};
background: ${theme.color.surfaceHover};
}
flex-wrap: wrap;
`;
const ActionIcon = styled.div`
color: ${theme.color.accent};
flex-shrink: 0;
`;
const ActionContent = styled.div`
flex: 1;
const SectionHeaderLeft = styled.div`
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
`;
const ActionTitle = styled.h3`
font-size: 15px;
font-weight: 700;
color: ${theme.color.textBright};
margin-bottom: 4px;
const SectionIcon = styled.div<{ $tone: 'premium' | 'pdf' | 'sample' }>`
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 12px;
color: ${({ $tone }) =>
$tone === 'premium'
? '#f59e0b'
: $tone === 'pdf'
? '#ef4444'
: theme.color.brandIndigo};
background: ${({ $tone }) =>
$tone === 'premium'
? 'rgba(245, 158, 11, 0.12)'
: $tone === 'pdf'
? 'rgba(239, 68, 68, 0.1)'
: 'rgba(79, 70, 229, 0.12)'};
`;
const ActionDesc = styled.p`
const SectionTitle2 = styled.h2`
margin: 0;
font-size: 17px;
font-weight: 700;
color: ${theme.color.textBright};
`;
const SectionDesc = styled.p`
margin: 2px 0 0;
font-size: 13px;
color: ${theme.color.textSub};
line-height: 1.5;
`;
const EmptySection = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 32px 16px;
border: 1px dashed ${theme.color.borderSoftAlpha};
border-radius: 14px;
text-align: center;
`;
const EmptySectionIcon = styled.div`
color: ${theme.color.textMute};
opacity: 0.5;
`;
const EmptySectionText = styled.span`
font-size: 14px;
font-weight: 600;
color: ${theme.color.textSub};
`;
const EmptySectionHint = styled.span`
font-size: 12px;
color: ${theme.color.textMute};
`;
const ActionButtons = styled.div`
display: flex;
gap: ${theme.space.sm};
flex-shrink: 0;
flex-wrap: wrap;
@media (max-width: ${theme.breakpoint.mobile}) {
flex-direction: column;
}
`;
const SectionLabel = styled.div`
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: ${theme.color.textSub};
`;
const Grid = styled.div`
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -953,12 +989,6 @@ const DeleteBtn = styled.button`
}
`;
const RestoreRow = styled.div`
display: flex;
justify-content: center;
padding: ${theme.space.sm} 0;
`;
const RestoreLink = styled.button`
background: none;
border: none;
@@ -966,7 +996,7 @@ const RestoreLink = styled.button`
font-size: 12px;
color: ${theme.color.textMute};
text-decoration: underline;
padding: 0;
padding: 4px 8px;
&:hover {
color: ${theme.color.textSub};

View File

@@ -33,13 +33,9 @@ const BASE_NAV_ITEMS: Array<{
label: '문제집',
icon: 'books',
match: (pathname) =>
pathname.startsWith('/exams') || pathname.startsWith('/study/exam'),
},
{
href: '/study/import-pdf',
label: 'PDF 문제집',
icon: 'file-pdf',
match: (pathname) => pathname.startsWith('/study/import-pdf'),
pathname.startsWith('/exams') ||
pathname.startsWith('/study/exam') ||
pathname.startsWith('/study/import-pdf'),
},
{
href: '/review',