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:
@@ -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};
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user