feat: 3단계 자기 평가 복습 스케줄 (어려움1d/보통7d/쉬움30d) + 샘플 ebook → StudyLog 연동

- backend study-logs: selfDifficulty 필드 추가, 고정 간격 스케줄링 (1/7/30일)
- backend reviews submit: selfDifficulty 전달 → 다음 복습 고정 간격
- frontend sample-ebook: 정답 확인 후 3단계 평가 → POST /study-logs → 복습 캘린더 등록
- frontend review: 정답/부분/오답 4버튼 → 어려웠어/괜찮았어/쉬웠어 3버튼으로 교체
- sample data: 180문항 전부 difficulty 책정 (문항별 easy/medium/hard)
This commit is contained in:
reloop
2026-04-16 12:24:41 +09:00
parent 21c31a07dc
commit 256d42dbd8
13 changed files with 528 additions and 62 deletions

View File

@@ -9,7 +9,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
import { IsEnum, IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { StudyResult } from '@prisma/client';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@@ -20,6 +20,10 @@ import { ReviewsService } from './reviews.service';
class SubmitDto {
@IsEnum(StudyResult)
result: StudyResult;
@IsOptional()
@IsIn(['hard', 'medium', 'easy'])
selfDifficulty?: 'hard' | 'medium' | 'easy';
}
class HistoryQuery {
@@ -66,7 +70,7 @@ export class ReviewsController {
@Param('id', ParseIntPipe) id: number,
@Body() dto: SubmitDto,
) {
return this.svc.submit(user.id, id, dto.result);
return this.svc.submit(user.id, id, dto.result, dto.selfDifficulty);
}
@Post(':id/skip')

View File

@@ -43,7 +43,12 @@ export class ReviewsService {
});
}
async submit(userId: number, reviewId: number, result: StudyResult) {
async submit(
userId: number,
reviewId: number,
result: StudyResult,
selfDifficulty?: 'hard' | 'medium' | 'easy',
) {
// Pre-tx: ownership check (safe to do outside tx — review ownership never changes)
const review = await this.prisma.reviewSchedule.findUnique({
where: { id: reviewId },
@@ -119,13 +124,29 @@ export class ReviewsService {
}
// d. Compute next schedule
const schedule = this.forget.schedule({
s0: newS0,
persona: user.persona,
intensity: user.reviewIntensity,
difficulty: D,
lastUpdatedAt: now,
});
const SELF_DIFFICULTY_DAYS: Record<string, number> = {
hard: 1,
medium: 7,
easy: 30,
};
let nextScheduledAt: Date;
let nextPredictedP: number | null = null;
if (selfDifficulty) {
const days = SELF_DIFFICULTY_DAYS[selfDifficulty];
nextScheduledAt = new Date(now.getTime() + days * 24 * 3_600_000);
} else {
const schedule = this.forget.schedule({
s0: newS0,
persona: user.persona,
intensity: user.reviewIntensity,
difficulty: D,
lastUpdatedAt: now,
});
nextScheduledAt = schedule.scheduledAt;
nextPredictedP = schedule.predictedP;
}
// e. Fetch the true latest iteration for this studyLog inside tx
const lastDone = await tx.reviewSchedule.findFirst({
@@ -139,8 +160,8 @@ export class ReviewsService {
data: {
userId,
studyLogId: review.studyLogId,
scheduledAt: schedule.scheduledAt,
predictedP: schedule.predictedP,
scheduledAt: nextScheduledAt,
predictedP: nextPredictedP,
iteration: nextIteration,
status: 'pending',
},

View File

@@ -14,6 +14,7 @@ import {
ArrayMinSize,
IsArray,
IsEnum,
IsIn,
IsInt,
IsNumber,
IsOptional,
@@ -76,6 +77,10 @@ class CreateStudyLogDto {
@IsInt()
@Min(0)
timeSpent?: number;
@IsOptional()
@IsIn(['hard', 'medium', 'easy'])
selfDifficulty?: 'hard' | 'medium' | 'easy';
}
class ListStudyLogQuery {
@@ -163,7 +168,19 @@ export class StudyLogsController {
@Post()
create(@CurrentUser() user: AuthUser, @Body() dto: CreateStudyLogDto) {
return this.svc.create(user.id, dto);
return this.svc.create(user.id, {
subjectId: dto.subjectId,
tagId: dto.tagId,
problemId: dto.problemId,
title: dto.title,
difficulty: dto.difficulty,
baseCorrectRate: dto.baseCorrectRate,
result: dto.result,
chosenAnswer: dto.chosenAnswer,
memo: dto.memo,
timeSpent: dto.timeSpent,
selfDifficulty: dto.selfDifficulty,
});
}
@Post('from-problem-set')

View File

@@ -25,6 +25,7 @@ export interface CreateStudyLogInput {
chosenAnswer?: number | null;
memo?: string;
timeSpent?: number;
selfDifficulty?: 'hard' | 'medium' | 'easy';
}
export interface UpdateStudyLogInput {
@@ -501,20 +502,36 @@ export class StudyLogsService {
});
}
const schedule = this.forget.schedule({
s0,
persona: user.persona,
intensity: user.reviewIntensity,
difficulty: D,
lastUpdatedAt: now,
});
const SELF_DIFFICULTY_DAYS: Record<string, number> = {
hard: 1,
medium: 7,
easy: 30,
};
let scheduledAt: Date;
let predictedP: number | null = null;
if (input.selfDifficulty) {
const days = SELF_DIFFICULTY_DAYS[input.selfDifficulty];
scheduledAt = new Date(now.getTime() + days * 24 * 3_600_000);
} else {
const schedule = this.forget.schedule({
s0,
persona: user.persona,
intensity: user.reviewIntensity,
difficulty: D,
lastUpdatedAt: now,
});
scheduledAt = schedule.scheduledAt;
predictedP = schedule.predictedP;
}
const reviewRow = await tx.reviewSchedule.create({
data: {
userId,
studyLogId: log.id,
scheduledAt: schedule.scheduledAt,
predictedP: schedule.predictedP,
scheduledAt,
predictedP,
iteration: 0,
status: "pending",
},

View File

@@ -9,9 +9,24 @@ import AppShell from '@/components/layout/AppShell';
import { Icon } from '@/components/ui/Icon';
import { Badge, Button, Card } from '@/components/ui/primitives';
import { theme } from '@/styles/theme';
import { api, type Subject } from '@/lib/api';
import { EBOOK_MAP } from '../data/index';
import type { SampleProblem } from '../data/types';
type SelfDifficulty = 'hard' | 'medium' | 'easy';
const SELF_DIFFICULTY_DAYS: Record<SelfDifficulty, number> = {
hard: 1,
medium: 7,
easy: 30,
};
const DIFFICULTY_SCORE: Record<SampleProblem['difficulty'], number> = {
hard: 0.9,
medium: 0.5,
easy: 0.1,
};
// ─── 페이지 컴포넌트 ────────────────────────────────────────────────────────────
export default function SampleEbookDynamicPage() {
@@ -27,6 +42,7 @@ export default function SampleEbookDynamicPage() {
<AppShell>
<EbookViewer
title={ebook.title}
grade={ebook.grade}
problems={ebook.problems}
/>
</AppShell>
@@ -37,18 +53,35 @@ export default function SampleEbookDynamicPage() {
interface EbookViewerProps {
title: string;
grade: string;
problems: SampleProblem[];
}
function EbookViewer({ title, problems }: EbookViewerProps) {
function EbookViewer({ title, grade, problems }: EbookViewerProps) {
const [pageIndex, setPageIndex] = useState(0);
const [selectedAnswers, setSelectedAnswers] = useState<Record<number, number>>({});
const [revealed, setRevealed] = useState<Record<number, boolean>>({});
const [fading, setFading] = useState(false);
// 자기 평가 관련 state
const [mathSubjectId, setMathSubjectId] = useState<number | null | undefined>(undefined);
const [registeredProblems, setRegisteredProblems] = useState<Set<number>>(new Set());
const [registeringProblem, setRegisteringProblem] = useState<number | null>(null);
const [registeredDates, setRegisteredDates] = useState<Record<number, Date>>({});
const total = problems.length;
const problem = problems[pageIndex];
// 마운트 시 수학 subject ID 조회
useEffect(() => {
api.get<Subject[]>('/subjects').then((res) => {
const math = res.data.find((s) => s.name === '수학');
setMathSubjectId(math?.id ?? null);
}).catch(() => {
setMathSubjectId(null);
});
}, []);
const goTo = useCallback(
(index: number) => {
if (index < 0 || index >= total || fading) return;
@@ -84,6 +117,33 @@ function EbookViewer({ title, problems }: EbookViewerProps) {
const isRevealed = revealed[problem.number] ?? false;
const isCorrect = chosen !== undefined && chosen === problem.answer;
const handleSelfDifficulty = useCallback(async (sd: SelfDifficulty) => {
if (!mathSubjectId || registeredProblems.has(problem.number) || registeringProblem !== null) return;
setRegisteringProblem(problem.number);
try {
const res = await api.post<{ nextReview: { scheduledAt: string } }>('/study-logs', {
subjectId: mathSubjectId,
title: `[${grade}] ${problem.question.slice(0, 50)}`,
difficulty: DIFFICULTY_SCORE[problem.difficulty],
result: isCorrect ? 'correct' : 'incorrect',
selfDifficulty: sd,
chosenAnswer: chosen,
});
const scheduledAt = new Date(res.data.nextReview.scheduledAt);
setRegisteredProblems((prev) => new Set(prev).add(problem.number));
setRegisteredDates((prev) => ({ ...prev, [problem.number]: scheduledAt }));
} catch {
// 등록 실패 시 조용히 무시 (서버 오류 등)
} finally {
setRegisteringProblem(null);
}
}, [mathSubjectId, registeredProblems, registeringProblem, problem, grade, isCorrect, chosen]);
const alreadyRegistered = registeredProblems.has(problem.number);
const scheduledDate = registeredDates[problem.number];
return (
<ViewerWrap>
{/* 상단 헤더 */}
@@ -188,6 +248,64 @@ function EbookViewer({ title, problems }: EbookViewerProps) {
<ExplanationText>{problem.explanation}</ExplanationText>
</ExplanationBox>
)}
{/* 자기 평가 UI */}
{isRevealed && mathSubjectId !== undefined && (
<SelfEvalBox>
{mathSubjectId === null ? (
<SelfEvalNoSubject>
<Icon name="info" size={14} />
'수학' .
</SelfEvalNoSubject>
) : alreadyRegistered ? (
<SelfEvalRegistered>
<Icon name="check-circle" size={15} />
!{' '}
{scheduledDate && (
<strong>
{scheduledDate.getMonth() + 1} {scheduledDate.getDate()} .
</strong>
)}
</SelfEvalRegistered>
) : (
<>
<SelfEvalTitle> ?</SelfEvalTitle>
<SelfEvalButtons>
<SelfEvalButton
type="button"
$tone="hard"
onClick={() => void handleSelfDifficulty('hard')}
disabled={registeringProblem !== null}
>
<SelfEvalEmoji>🔴</SelfEvalEmoji>
<SelfEvalLabel></SelfEvalLabel>
<SelfEvalInterval>1 </SelfEvalInterval>
</SelfEvalButton>
<SelfEvalButton
type="button"
$tone="medium"
onClick={() => void handleSelfDifficulty('medium')}
disabled={registeringProblem !== null}
>
<SelfEvalEmoji>🟡</SelfEvalEmoji>
<SelfEvalLabel></SelfEvalLabel>
<SelfEvalInterval>7 </SelfEvalInterval>
</SelfEvalButton>
<SelfEvalButton
type="button"
$tone="easy"
onClick={() => void handleSelfDifficulty('easy')}
disabled={registeringProblem !== null}
>
<SelfEvalEmoji>🟢</SelfEvalEmoji>
<SelfEvalLabel></SelfEvalLabel>
<SelfEvalInterval>30 </SelfEvalInterval>
</SelfEvalButton>
</SelfEvalButtons>
</>
)}
</SelfEvalBox>
)}
</BookPage>
</BookSpread>
@@ -526,6 +644,109 @@ const ExplanationText = styled.p`
color: ${theme.color.textSub};
`;
// ── 자기 평가 UI 스타일 ──────────────────────────────────────────────────────
const SelfEvalBox = styled.div`
padding: ${theme.space.md};
border-radius: ${theme.radius.md};
border: 1px solid ${theme.color.borderSoftAlpha};
background: rgba(255, 255, 255, 0.03);
display: flex;
flex-direction: column;
gap: ${theme.space.sm};
animation: ${fadeIn} 0.3s ease;
`;
const SelfEvalTitle = styled.span`
font-size: 13px;
font-weight: 600;
color: ${theme.color.textMain};
`;
const SelfEvalButtons = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
`;
const selfEvalToneStyles: Record<string, ReturnType<typeof css>> = {
hard: css`
border-color: rgba(239, 68, 68, 0.35);
&:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.6);
}
`,
medium: css`
border-color: rgba(234, 179, 8, 0.35);
&:hover:not(:disabled) {
background: rgba(234, 179, 8, 0.1);
border-color: rgba(234, 179, 8, 0.6);
}
`,
easy: css`
border-color: rgba(34, 197, 94, 0.35);
&:hover:not(:disabled) {
background: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.6);
}
`,
};
const SelfEvalButton = styled.button<{ $tone: 'hard' | 'medium' | 'easy' }>`
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 8px 6px;
border-radius: ${theme.radius.md};
border: 1px solid transparent;
background: rgba(255, 255, 255, 0.04);
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
${({ $tone }) => selfEvalToneStyles[$tone]}
`;
const SelfEvalEmoji = styled.span`
font-size: 18px;
line-height: 1;
`;
const SelfEvalLabel = styled.span`
font-size: 11px;
font-weight: 600;
color: ${theme.color.textMain};
`;
const SelfEvalInterval = styled.span`
font-size: 10px;
color: ${theme.color.textMute};
`;
const SelfEvalRegistered = styled.div`
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: ${theme.color.success};
`;
const SelfEvalNoSubject = styled.div`
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: ${theme.color.textMute};
`;
// ── 네비게이션 스타일 ───────────────────────────────────────────────────────
const PageNav = styled.div`
display: flex;
align-items: center;

View File

@@ -14,6 +14,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 1,
explanation: '(a+b)³ = a³ + 3a²b + 3ab² + b³. a=x, b=2 대입: x³ + 3·x²·2 + 3·x·4 + 8 = x³ + 6x² + 12x + 8.',
difficulty: 'medium',
},
{
number: 2,
@@ -21,6 +22,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '1', '1', '3', '5'],
answer: 3,
explanation: '나머지 정리: f(2) = 2·8 3·4 + 2 5 = 16 12 + 2 5 = 1.',
difficulty: 'medium',
},
{
number: 3,
@@ -28,6 +30,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['a² + b²', 'a² b²', 'a² + 2ab + b²', 'a² 2ab + b²', '2a² 2b²'],
answer: 2,
explanation: '합차 공식: (a+b)(ab) = a² b².',
difficulty: 'easy',
},
{
number: 4,
@@ -41,6 +44,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 1,
explanation: 'a³ b³ = (ab)(a²+ab+b²). x³8 = x³2³ = (x2)(x²+2x+4).',
difficulty: 'hard',
},
{
number: 5,
@@ -48,6 +52,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(x1)³', '(x+1)³', '(x1)(x²+x+1)', '(x+1)(x²x+1)', '(x1)²(x+1)'],
answer: 1,
explanation: '(ab)³ = a³ 3a²b + 3ab² b³. a=x, b=1 대입하면 (x1)³.',
difficulty: 'hard',
},
{
number: 6,
@@ -61,6 +66,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 3,
explanation: '나머지 정리: f(1) = (1)³ (1) + 2 = 1 + 1 + 2 = 2. 나머지 = 2.',
difficulty: 'medium',
},
// ── 방정식과 부등식 (7~12) ───────────────────────────────────────────────────
@@ -70,6 +76,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['x ≤ 1 또는 x ≥ 2', '1 ≤ x ≤ 2', 'x ≤ 1 또는 x ≥ 2', '2 ≤ x ≤ 1', 'x < 0'],
answer: 2,
explanation: '(x1)(x2) ≤ 0. 두 근 사이에서 성립: 1 ≤ x ≤ 2.',
difficulty: 'medium',
},
{
number: 8,
@@ -77,6 +84,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2 < x < 7', 'x > 2', 'x < 7', 'x ≥ 2', '2 ≤ x ≤ 7'],
answer: 1,
explanation: '첫 부등식: 2x > 4 → x > 2. 둘째: x < 7. 교집합: 2 < x < 7.',
difficulty: 'medium',
},
{
number: 9,
@@ -84,6 +92,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5/4', '25/4', '5', '25', '25/4'],
answer: 2,
explanation: '중근 조건: 판별식 D = 0. D = 25 4k = 0 → k = 25/4.',
difficulty: 'hard',
},
{
number: 10,
@@ -91,6 +100,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5', '2', '2', '5', '10'],
answer: 4,
explanation: '비에타 공식: 두 근의 곱 = c/a = 5/1 = 5.',
difficulty: 'medium',
},
{
number: 11,
@@ -98,6 +108,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1 < x < 4', 'x > 1', 'x < 4', '2 < x < 4', '1 < x < 4'],
answer: 1,
explanation: '5 < 2x 3 < 5 → 2 < 2x < 8 → 1 < x < 4.',
difficulty: 'hard',
},
{
number: 12,
@@ -105,6 +116,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2 < x < 3', 'x < 2 또는 x > 3', 'x > 3', 'x < 2', '3 < x < 2'],
answer: 2,
explanation: '(x3)(x+2) > 0. 두 근 바깥쪽: x < 2 또는 x > 3.',
difficulty: 'medium',
},
// ── 집합과 명제 (13~18) ──────────────────────────────────────────────────────
@@ -114,6 +126,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '4', '5', '7', '8'],
answer: 3,
explanation: 'A B = {1, 2, 3, 4, 5}. 원소 개수 = 5.',
difficulty: 'easy',
},
{
number: 14,
@@ -121,6 +134,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 2,
explanation: 'A ∩ B = {3, 4}. 원소 개수 = 2.',
difficulty: 'easy',
},
{
number: 15,
@@ -128,6 +142,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['p이면 ¬q이다', '¬p이면 q이다', '¬q이면 ¬p이다', 'q이면 p이다', '¬p이면 ¬q이다'],
answer: 3,
explanation: '명제 p→q의 대우는 ¬q→¬p. 대우는 원명제와 동치.',
difficulty: 'medium',
},
{
number: 16,
@@ -135,6 +150,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '5', '7', '2', '11'],
answer: 4,
explanation: '2는 소수이지만 짝수. 따라서 반례 = 2.',
difficulty: 'medium',
},
{
number: 17,
@@ -142,6 +158,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['{1,3,5}', '{2,4}', '{1,2,3,4,5}', '∅', '{2,3,4}'],
answer: 2,
explanation: 'Aᶜ = U A = {1,2,3,4,5} {1,3,5} = {2,4}.',
difficulty: 'easy',
},
{
number: 18,
@@ -155,6 +172,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 4,
explanation: '원명제와 대우는 항상 진릿값이 같다(동치). 역과 이도 서로 동치.',
difficulty: 'hard',
},
// ── 함수 (19~24) ─────────────────────────────────────────────────────────────
@@ -164,6 +182,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '0', '1', '2', '5'],
answer: 3,
explanation: 'f(1) = 2 × (1) + 3 = 2 + 3 = 1.',
difficulty: 'easy',
},
{
number: 20,
@@ -177,6 +196,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 3,
explanation: '전단사 = 단사(일대일) AND 전사(onto). 두 조건을 동시에 만족해야 한다.',
difficulty: 'hard',
},
{
number: 21,
@@ -184,6 +204,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['13', '17', '18', '19', '22'],
answer: 4,
explanation: '(g∘f)(3) = g(f(3)) = g(9) = 2·9 + 1 = 19.',
difficulty: 'medium',
},
{
number: 22,
@@ -197,6 +218,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 1,
explanation: 'y = 3x 2 → 3x = y + 2 → x = (y+2)/3. x와 y를 바꾸면 f⁻¹(x) = (x+2)/3.',
difficulty: 'medium',
},
{
number: 23,
@@ -210,6 +232,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 3,
explanation: 'y = 1/x는 x→0에서 y→±∞, y→0에서 x→±∞. 점근선은 x축(y=0)과 y축(x=0).',
difficulty: 'hard',
},
{
number: 24,
@@ -217,6 +240,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['x ≥ 0', 'x > 0', 'x ≥ 1', 'x > 1', '모든 실수'],
answer: 3,
explanation: '루트 안이 0 이상이어야 한다. x 1 ≥ 0 → x ≥ 1.',
difficulty: 'easy',
},
// ── 경우의 수 (25~30) ─────────────────────────────────────────────────────────
@@ -226,6 +250,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['10', '15', '20', '25', '30'],
answer: 3,
explanation: '순열: P(5,2) = 5 × 4 = 20.',
difficulty: 'easy',
},
{
number: 26,
@@ -233,6 +258,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5', '8', '10', '15', '20'],
answer: 3,
explanation: '조합: C(5,2) = 5!/(2!3!) = (5×4)/(2×1) = 10.',
difficulty: 'easy',
},
{
number: 27,
@@ -240,6 +266,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['120', '360', '480', '720', '1440'],
answer: 4,
explanation: '6! = 6 × 5 × 4 × 3 × 2 × 1 = 720.',
difficulty: 'medium',
},
{
number: 28,
@@ -247,6 +274,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '6', '8', '12', '24'],
answer: 2,
explanation: 'A가 고정되면 나머지 B, C, D를 3자리에 배열: 3! = 6.',
difficulty: 'medium',
},
{
number: 29,
@@ -254,6 +282,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '4', '6', '8', '12'],
answer: 4,
explanation: '각 동전이 2가지(앞/뒤). 2³ = 8.',
difficulty: 'easy',
},
{
number: 30,
@@ -261,5 +290,6 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['60', '90', '120', '180', '720'],
answer: 3,
explanation: 'C(10,3) = 10!/(3!·7!) = (10×9×8)/(3×2×1) = 720/6 = 120.',
difficulty: 'medium',
},
];

View File

@@ -8,6 +8,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2⁷', '2¹²', '4⁷', '8⁷', '2⁻¹'],
answer: 1,
explanation: '지수 법칙: aᵐ × aⁿ = aᵐ⁺ⁿ. 2³ × 2⁴ = 2⁷ = 128.',
difficulty: 'easy',
},
{
number: 2,
@@ -15,6 +16,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '8'],
answer: 3,
explanation: 'log₂ 8 = log₂ 2³ = 3.',
difficulty: 'easy',
},
{
number: 3,
@@ -22,6 +24,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 3,
explanation: 'log₁₀ 100 = 2, log₁₀ 10 = 1. 합 = 3. (로그의 덧셈 = log(곱): log(1000) = 3)',
difficulty: 'easy',
},
{
number: 4,
@@ -29,6 +32,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0.6020', '0.7525', '0.9030', '1.2040', '1.5050'],
answer: 3,
explanation: 'log 8 = log 2³ = 3 log 2 = 3 × 0.3010 = 0.9030.',
difficulty: 'medium',
},
{
number: 5,
@@ -36,6 +40,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '1/4', '1/4', '4', '8'],
answer: 3,
explanation: 'y = 2⁻² = 1/2² = 1/4.',
difficulty: 'easy',
},
{
number: 6,
@@ -43,6 +48,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1', 'e', '1/e', '2'],
answer: 2,
explanation: 'lnₑ e = log_e e = 1. 로그의 정의에 의해 eˣ = e이면 x = 1.',
difficulty: 'easy',
},
// ── 삼각함수 (7~12) ──────────────────────────────────────────────────────────
@@ -52,6 +58,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1/2', '1', '2', 'θ'],
answer: 3,
explanation: '피타고라스 항등식: sin²θ + cos²θ = 1 (항상 성립).',
difficulty: 'easy',
},
{
number: 8,
@@ -59,6 +66,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '0', '1/2', '√2/2', '1'],
answer: 5,
explanation: '단위원 정의: 90°일 때 y 좌표 = 1. sin 90° = 1.',
difficulty: 'easy',
},
{
number: 9,
@@ -66,6 +74,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['cos θ', 'cos θ', 'sin θ', 'sin θ', '1/cos θ'],
answer: 2,
explanation: '코사인은 우함수: cos(−θ) = cos θ.',
difficulty: 'medium',
},
{
number: 10,
@@ -73,6 +82,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['15°', '30°', '45°', '60°', '90°'],
answer: 2,
explanation: 'π 라디안 = 180°. π/6 × (180/π) = 30°.',
difficulty: 'medium',
},
{
number: 11,
@@ -80,6 +90,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['π/2', 'π', '2π', '4π', '1'],
answer: 3,
explanation: 'sin 함수의 주기 = 2π. y = sin(bx)의 주기는 2π/b.',
difficulty: 'medium',
},
{
number: 12,
@@ -87,6 +98,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3/4', '4/3', '3/5', '5/3', '5/4'],
answer: 1,
explanation: 'tan θ = (3/5) ÷ (4/5) = (3/5) × (5/4) = 3/4.',
difficulty: 'medium',
},
// ── 수열 (13~18) ─────────────────────────────────────────────────────────────
@@ -96,6 +108,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 3,
explanation: '연속하는 항의 차: 5 2 = 3. 공차 d = 3.',
difficulty: 'easy',
},
{
number: 14,
@@ -103,6 +116,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['25', '27', '28', '29', '31'],
answer: 3,
explanation: 'aₙ = a₁ + (n1)d. a₁₀ = 1 + 9 × 3 = 1 + 27 = 28.',
difficulty: 'medium',
},
{
number: 15,
@@ -110,6 +124,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '6'],
answer: 2,
explanation: '연속하는 항의 비: 6/3 = 2. 공비 r = 2.',
difficulty: 'easy',
},
{
number: 16,
@@ -117,6 +132,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['18', '27', '54', '81', '162'],
answer: 3,
explanation: 'aₙ = a₁ × r^(n1). a₄ = 2 × 3³ = 2 × 27 = 54.',
difficulty: 'medium',
},
{
number: 17,
@@ -124,6 +140,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['10', '12', '15', '18', '20'],
answer: 3,
explanation: '1 + 2 + 3 + 4 + 5 = 15. 공식: n(n+1)/2 = 5×6/2 = 15.',
difficulty: 'easy',
},
{
number: 18,
@@ -131,6 +148,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '5', '6', '7', '8'],
answer: 3,
explanation: 'aₙ = Sₙ S(n1). a₅ = S₅ S₄ = 20 14 = 6.',
difficulty: 'medium',
},
// ── 미분 (도함수) (19~24) ────────────────────────────────────────────────────
@@ -140,6 +158,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['x²', '2x²', '3x²', '3x³', '4x³'],
answer: 3,
explanation: '멱함수 미분: (x^n)의 도함수 = n*x^(n-1). (x³)의 도함수 = 3x².',
difficulty: 'easy',
},
{
number: 20,
@@ -147,6 +166,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2x 3', '4x 3', '4x + 3', '2x + 3', 'x² 3'],
answer: 2,
explanation: '각 항을 미분: (2x²)의 도함수 = 4x, (3x)의 도함수 = 3, (1)의 도함수 = 0. f´(x) = 4x 3.',
difficulty: 'medium',
},
{
number: 21,
@@ -154,6 +174,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '2', '0', '2', '3'],
answer: 3,
explanation: 'f´(x) = 3x² 3. f´(1) = 3 × 1 3 = 0.',
difficulty: 'medium',
},
{
number: 22,
@@ -161,6 +182,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5x', '5', '1', '0', '5'],
answer: 4,
explanation: '상수의 도함수는 0. 상수 c를 미분하면 항상 0.',
difficulty: 'easy',
},
{
number: 23,
@@ -168,6 +190,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '1', '0', '2', '3'],
answer: 4,
explanation: 'f´(x) = 3x² 6x = 3x(x2) = 0 → x = 0 또는 x = 2. x = 0은 극대, x = 2는 극소.',
difficulty: 'hard',
},
{
number: 24,
@@ -175,6 +198,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '3', '2', '0', '2'],
answer: 3,
explanation: 'f´(x) = 2x 4. x = 1: f´(1) = 2 4 = 2.',
difficulty: 'hard',
},
// ── 적분 (부정/정적분) (25~30) ───────────────────────────────────────────────
@@ -184,6 +208,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['x² + C', 'x³ + C', '6x + C', '3x³ + C', 'x³/3 + C'],
answer: 2,
explanation: '∫ x^n dx = x^(n+1)/(n+1) + C. ∫ 3x² dx = 3 × x³/3 + C = x³ + C.',
difficulty: 'medium',
},
{
number: 26,
@@ -191,6 +216,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '6'],
answer: 4,
explanation: '[x²]₀² = 4 0 = 4.',
difficulty: 'medium',
},
{
number: 27,
@@ -198,6 +224,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '5', '6', '7', '8'],
answer: 3,
explanation: '[x² x]₁³ = (9 3) (1 1) = 6 0 = 6.',
difficulty: 'hard',
},
{
number: 28,
@@ -205,6 +232,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(x+1)³/3 + C', '2(x+1) + C', '(x+1)² + C', '(x+1)³ + C', 'x²/2 + x + C'],
answer: 1,
explanation: '∫ (x+1)² dx. t = x+1로 치환하면 ∫t² dt = t³/3 + C = (x+1)³/3 + C.',
difficulty: 'hard',
},
{
number: 29,
@@ -212,6 +240,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/4', '1/3', '1/2', '1', '2'],
answer: 2,
explanation: '[x³/3]₀¹ = 1/3 0 = 1/3.',
difficulty: 'hard',
},
{
number: 30,
@@ -219,5 +248,6 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/6', '1/4', '1/3', '1/2', '1'],
answer: 1,
explanation: '교점: x² = x → x = 0, 1. 넓이 = ∫₀¹ (x x²) dx = [x²/2 x³/3]₀¹ = 1/2 1/3 = 1/6.',
difficulty: 'hard',
},
];

View File

@@ -8,6 +8,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '2', '4', '6', '수렴하지 않음'],
answer: 3,
explanation: '분자 인수분해: (x2)(x+2). 약분 후 lim(x→2) (x+2) = 2 + 2 = 4.',
difficulty: 'medium',
},
{
number: 2,
@@ -15,6 +16,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1', '2', '3', '∞'],
answer: 4,
explanation: '분자 분모를 최고차항 x²으로 나누면 lim = (3 + 2/x) / (1 1/x²) → 3/1 = 3.',
difficulty: 'medium',
},
{
number: 3,
@@ -22,6 +24,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1', 'π', '∞', '존재하지 않음'],
answer: 2,
explanation: '기본 극한: lim(x→0) sin x / x = 1 (중요 표준 극한).',
difficulty: 'medium',
},
{
number: 4,
@@ -29,6 +32,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', 'e', 'π'],
answer: 4,
explanation: '자연상수 e의 정의: e = lim(n→∞) (1 + 1/n)^n ≈ 2.718…',
difficulty: 'hard',
},
{
number: 5,
@@ -36,6 +40,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1', '2', '3', '4'],
answer: 3,
explanation: 'f(1+h) = (1+h)² = 1 + 2h + h². [f(1+h)f(1)]/h = (2h + h²)/h = 2 + h → h→0이면 2.',
difficulty: 'medium',
},
// ── 미분법 심화 (6~11) ────────────────────────────────────────────────────────
@@ -45,6 +50,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['e^(x1)', 'x·e^x', 'e^x', '1/e^x', 'e'],
answer: 3,
explanation: '지수함수의 미분: e^x의 도함수 = e^x. 자기 자신이 도함수.',
difficulty: 'easy',
},
{
number: 7,
@@ -52,6 +58,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['ln x', '1/x', 'x', 'e^x', '1/(x ln x)'],
answer: 2,
explanation: '자연로그 미분: ln x의 도함수 = 1/x.',
difficulty: 'easy',
},
{
number: 8,
@@ -59,6 +66,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['sin x', 'sin x', 'cos x', 'cos x', 'tan x'],
answer: 4,
explanation: '삼각함수 미분: sin x의 도함수 = cos x.',
difficulty: 'easy',
},
{
number: 9,
@@ -72,6 +80,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 2,
explanation: '곱의 미분: (uv)의 도함수 = u´v + uv´. u = x², u´ = 2x; v = sin x, v´ = cos x. 결과: 2x sin x + x² cos x.',
difficulty: 'hard',
},
{
number: 10,
@@ -79,6 +88,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3(2x+1)²', '6(2x+1)²', '3(2x+1)', '6(2x+1)', '2(2x+1)³'],
answer: 2,
explanation: '연쇄 법칙: (u³)의 도함수 = 3u² × u´. u = 2x+1, u´ = 2. 결과: 3(2x+1)² × 2 = 6(2x+1)².',
difficulty: 'hard',
},
{
number: 11,
@@ -86,6 +96,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '2', '4', '6', '9'],
answer: 3,
explanation: 'f´(x) = 3x² 12x + 9 = 3(x1)(x3) = 0 → x = 1(극대), x = 3(극소). f(1) = 1 6 + 9 = 4.',
difficulty: 'hard',
},
// ── 적분법 심화 (12~17) ───────────────────────────────────────────────────────
@@ -95,6 +106,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['e^(x+1) + C', 'e^x + C', 'x·e^x + C', '1/e^x + C', 'e^x/x + C'],
answer: 2,
explanation: '∫ e^x dx = e^x + C. e^x는 자신이 미분/적분의 고유함수.',
difficulty: 'easy',
},
{
number: 13,
@@ -102,6 +114,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/x² + C', 'x + C', 'ln x + C', '1/(x+1) + C', 'e^x + C'],
answer: 3,
explanation: '∫ (1/x) dx = ln |x| + C. x > 0이면 ln x + C.',
difficulty: 'medium',
},
{
number: 14,
@@ -109,6 +122,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['sin x + C', 'sin x + C', 'cos x + C', 'cos x + C', 'tan x + C'],
answer: 2,
explanation: '∫ cos x dx = sin x + C. sin x의 도함수가 cos x이므로 역연산.',
difficulty: 'medium',
},
{
number: 15,
@@ -116,6 +130,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1', '2', 'π', '2'],
answer: 3,
explanation: '[cos x]₀^π = cos π (cos 0) = (1) (1) = 1 + 1 = 2.',
difficulty: 'hard',
},
{
number: 16,
@@ -123,6 +138,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['x·e^x + C', 'x·e^x e^x + C', 'e^x + C', 'x²·e^x/2 + C', '(x1)·e^x + C'],
answer: 2,
explanation: '부분적분: u=x, dv=e^x dx → du=dx, v=e^x. ∫x·e^x dx = x·e^x ∫e^x dx = x·e^x e^x + C.',
difficulty: 'hard',
},
{
number: 17,
@@ -130,6 +146,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1/2', '1', '3/2', '2'],
answer: 2,
explanation: '|∫₋₁⁰ (x³x) dx| + |∫₀¹ (x³x) dx| = 1/4 + 1/4 = 1/2. 대칭 활용.',
difficulty: 'hard',
},
// ── 확률과 통계 (18~24) ───────────────────────────────────────────────────────
@@ -139,6 +156,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['24', '36', '48', '60', '72'],
answer: 3,
explanation: '2명을 묶어 1명으로 보면 4명 배열: 4! = 24. 묶음 내부 순서: 2! = 2. 24 × 2 = 48.',
difficulty: 'hard',
},
{
number: 19,
@@ -146,6 +164,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['10', '12', '15', '18', '30'],
answer: 3,
explanation: 'C(6,2) = 6!/(2!·4!) = (6×5)/2 = 15.',
difficulty: 'medium',
},
{
number: 20,
@@ -153,6 +172,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '4', '5', '6', '10'],
answer: 3,
explanation: '이항분포 B(n,p)의 평균 = np = 10 × 0.5 = 5.',
difficulty: 'medium',
},
{
number: 21,
@@ -160,6 +180,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0.3413', '0.5', '0.6826', '0.9544', '1'],
answer: 3,
explanation: 'Z = (X50)/10. P(40≤X≤60) = P(1≤Z≤1) = 2×P(0≤Z≤1) = 2×0.3413 = 0.6826.',
difficulty: 'hard',
},
{
number: 22,
@@ -167,6 +188,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0.1', '0.2', '0.4', '0.5', '0.9'],
answer: 2,
explanation: '독립사건: P(A∩B) = P(A) × P(B) = 0.4 × 0.5 = 0.2.',
difficulty: 'medium',
},
{
number: 23,
@@ -174,6 +196,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0.2', '0.3', '0.4', '0.5', '0.6'],
answer: 2,
explanation: 'P(A|B) = 0.12 / 0.4 = 0.3.',
difficulty: 'hard',
},
{
number: 24,
@@ -181,6 +204,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1.96', '2.96', '3.92', '4.90', '19.6'],
answer: 3,
explanation: '신뢰구간 폭 = 2 × z × σ/√n = 2 × 1.96 × 10/10 = 2 × 1.96 = 3.92.',
difficulty: 'hard',
},
// ── 벡터/공간좌표 (25~30) ─────────────────────────────────────────────────────
@@ -190,6 +214,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5', '6', '7', '12', '25'],
answer: 1,
explanation: '|a→| = √(3² + 4²) = √(9+16) = √25 = 5.',
difficulty: 'medium',
},
{
number: 26,
@@ -197,6 +222,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '0', '1', '2', '3'],
answer: 3,
explanation: 'a→ + b→ = (1+3, 2+(1)) = (4, 1). y 성분 = 1.',
difficulty: 'medium',
},
{
number: 27,
@@ -204,6 +230,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '4', '5', '6', '7'],
answer: 3,
explanation: 'a→·b→ = 2×1 + 1×3 = 2 + 3 = 5.',
difficulty: 'medium',
},
{
number: 28,
@@ -211,6 +238,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '0', '1', '√2', '2'],
answer: 2,
explanation: 'a→·b→ = 1×0 + 0×1 = 0. 수직(직교)이면 내적 = 0.',
difficulty: 'medium',
},
{
number: 29,
@@ -218,6 +246,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '4', '5', '6', '7'],
answer: 3,
explanation: 'd = √((41)² + (62)² + (33)²) = √(9+16+0) = √25 = 5.',
difficulty: 'hard',
},
{
number: 30,
@@ -225,5 +254,6 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(1,1,1)', '(1,2,3)', '(3,2,1)', '(2,3,1)', '(0,0,0)'],
answer: 2,
explanation: '대칭형 방정식 x/a = y/b = z/c에서 방향벡터 = (a, b, c) = (1, 2, 3).',
difficulty: 'hard',
},
];

View File

@@ -8,6 +8,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['7', '1', '0', '1', '7'],
answer: 5,
explanation: '절댓값은 수직선에서 원점까지의 거리이므로 음수 기호를 제거한다. |7| = 7.',
difficulty: 'easy',
},
{
number: 2,
@@ -15,6 +16,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '2', '5', '0', '4'],
answer: 3,
explanation: '수직선에서 왼쪽에 있을수록 작다. 5 < 3 < 0 < 2 < 4 이므로 5가 가장 작다.',
difficulty: 'easy',
},
{
number: 3,
@@ -22,6 +24,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['11', '5', '5', '11', '24'],
answer: 1,
explanation: '부호가 같은 두 음수의 합은 절댓값의 합에 음수 부호를 붙인다. (3) + (8) = 11.',
difficulty: 'easy',
},
{
number: 4,
@@ -29,6 +32,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['24', '10', '10', '24', '48'],
answer: 4,
explanation: '음수 × 음수 = 양수. 4 × 6 = 24이므로 (4) × (6) = 24.',
difficulty: 'easy',
},
{
number: 5,
@@ -36,6 +40,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['10', '5/2', '5/2', '5', '10'],
answer: 1,
explanation: '÷ (1/2) = × (2). 5 × (2) = 10. 양수 ÷ 음수이므로 결과는 음수.',
difficulty: 'medium',
},
{
number: 6,
@@ -43,6 +48,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['9', '7', '6', '7', '9'],
answer: 2,
explanation: '2³ = 8, (1)⁴ = 1. 따라서 8 + 1 = 7.',
difficulty: 'medium',
},
// ── 일차방정식 (7~12) ────────────────────────────────────────────────────────
@@ -52,6 +58,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '4', '5', '7', '10'],
answer: 3,
explanation: '2x = 7 + 3 = 10. x = 10 ÷ 2 = 5.',
difficulty: 'easy',
},
{
number: 8,
@@ -59,6 +66,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 3,
explanation: '3x + 3 = 12. 3x = 9. x = 3.',
difficulty: 'easy',
},
{
number: 9,
@@ -66,6 +74,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '3', '5', '6', '8'],
answer: 4,
explanation: 'x/2 = 4 1 = 3. 양변에 2를 곱하면 x = 6.',
difficulty: 'easy',
},
{
number: 10,
@@ -73,6 +82,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '3', '4', '5', '6'],
answer: 3,
explanation: '5x 2x = 8 + 4. 3x = 12. x = 4.',
difficulty: 'medium',
},
{
number: 11,
@@ -80,6 +90,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5', '6', '7', '8', '9'],
answer: 3,
explanation: '3x 5 = 16. 3x = 21. x = 7.',
difficulty: 'medium',
},
{
number: 12,
@@ -87,6 +98,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5', '6', '7', '8', '9'],
answer: 3,
explanation: '2x 6 = x + 1. 2x x = 1 + 6. x = 7.',
difficulty: 'medium',
},
// ── 좌표평면과 그래프 (13~18) ────────────────────────────────────────────────
@@ -96,6 +108,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['제1사분면', '제2사분면', '제3사분면', '제4사분면', '어느 사분면도 아님'],
answer: 2,
explanation: 'x < 0, y > 0 이면 제2사분면. (3, 2)는 x = 3 < 0, y = 2 > 0이므로 제2사분면.',
difficulty: 'easy',
},
{
number: 14,
@@ -103,6 +116,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 3,
explanation: '4 1 = 3. x 좌표끼리의 차이를 구한다.',
difficulty: 'easy',
},
{
number: 15,
@@ -110,6 +124,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5', '6', '7', '8', '9'],
answer: 3,
explanation: 'y = 2 × 3 + 1 = 6 + 1 = 7.',
difficulty: 'easy',
},
{
number: 16,
@@ -117,6 +132,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(0, 4)', '(2, 2)', '(4, 0)', '(1, 3)', '(3, 0)'],
answer: 2,
explanation: 'x = 2일 때 y = 2 + 4 = 2. 따라서 점 (2, 2)를 지난다.',
difficulty: 'medium',
},
{
number: 17,
@@ -124,6 +140,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['제1사분면', '제2사분면', 'x 축', 'y 축', '원점'],
answer: 3,
explanation: 'y = 0인 점은 x 축 위에 있다. (5, 0)은 x 축 위의 점.',
difficulty: 'easy',
},
{
number: 18,
@@ -131,6 +148,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '0', '1', '3', '9'],
answer: 4,
explanation: 'y = mx 형태에서 m이 기울기. y = 3x에서 기울기는 3.',
difficulty: 'easy',
},
// ── 기본 도형 (19~24) ────────────────────────────────────────────────────────
@@ -140,6 +158,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['90°', '120°', '180°', '270°', '360°'],
answer: 3,
explanation: '삼각형 세 내각의 합은 항상 180°이다.',
difficulty: 'easy',
},
{
number: 20,
@@ -147,6 +166,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['45°', '60°', '90°', '108°', '120°'],
answer: 2,
explanation: '정삼각형은 세 각이 모두 같고 합이 180°. 180° ÷ 3 = 60°.',
difficulty: 'easy',
},
{
number: 21,
@@ -154,6 +174,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4π cm', '8π cm', '12π cm', '16π cm', '32π cm'],
answer: 2,
explanation: '원의 둘레 = 2πr. 2 × π × 4 = 8π cm.',
difficulty: 'easy',
},
{
number: 22,
@@ -161,6 +182,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['35°', '45°', '55°', '65°', '90°'],
answer: 3,
explanation: '세 각의 합 = 180°. 90° + 35° + ? = 180°. 나머지 각 = 55°.',
difficulty: 'easy',
},
{
number: 23,
@@ -168,6 +190,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['수직이등분', '수직', '서로 이등분', '같은 길이', '평행'],
answer: 3,
explanation: '평행사변형에서 두 대각선은 서로 이등분(교점에서 각각 반으로 나뉨)한다.',
difficulty: 'hard',
},
{
number: 24,
@@ -175,6 +198,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['10 cm²', '12 cm²', '18 cm²', '24 cm²', '48 cm²'],
answer: 2,
explanation: '삼각형 넓이 = (밑변 × 높이) ÷ 2 = (6 × 4) ÷ 2 = 12 cm².',
difficulty: 'easy',
},
// ── 비례/반비례 (25~30) ──────────────────────────────────────────────────────
@@ -184,6 +208,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['15', '18', '20', '24', '30'],
answer: 3,
explanation: '정비례: y = kx. 12 = k × 3 → k = 4. y = 4 × 5 = 20.',
difficulty: 'medium',
},
{
number: 26,
@@ -191,6 +216,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '3', '4', '6', '12'],
answer: 2,
explanation: '반비례: xy = k. 2 × 6 = 12 = k. x = 4이면 y = 12/4 = 3.',
difficulty: 'medium',
},
{
number: 27,
@@ -198,6 +224,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '6', '12'],
answer: 3,
explanation: 'y = 6/2 = 3.',
difficulty: 'easy',
},
{
number: 28,
@@ -205,6 +232,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['120 km', '150 km', '180 km', '200 km', '240 km'],
answer: 3,
explanation: '거리 = 속력 × 시간. 60 × 3 = 180 km.',
difficulty: 'easy',
},
{
number: 29,
@@ -212,6 +240,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['21', '28', '30', '35', '42'],
answer: 3,
explanation: '전체 비율 3 + 4 = 7. 3에 해당하는 양 = 70 × (3/7) = 30.',
difficulty: 'medium',
},
{
number: 30,
@@ -219,5 +248,6 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['12 cm', '13 cm', '14 cm', '15 cm', '18 cm'],
answer: 4,
explanation: '3:5 = 9:x. 3x = 45. x = 15 cm.',
difficulty: 'hard',
},
];

View File

@@ -8,6 +8,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['√4', '0.333…', '5/3', '√7', '1.25'],
answer: 4,
explanation: '√4 = 2(유리수), 0.333… = 1/3(유리수), 5/3(유리수), 1.25(유리수). √7은 유한소수나 순환소수로 나타낼 수 없으므로 무리수.',
difficulty: 'medium',
},
{
number: 2,
@@ -15,6 +16,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3√2', '2√3', '3√6', '6√2', '9√2'],
answer: 1,
explanation: '√18 = √(9 × 2) = √9 × √2 = 3√2.',
difficulty: 'easy',
},
{
number: 3,
@@ -22,6 +24,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '4', '√10', '2√4', '8'],
answer: 2,
explanation: '√2 × √8 = √(2 × 8) = √16 = 4.',
difficulty: 'easy',
},
{
number: 4,
@@ -29,6 +32,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['7', '7√3', '10√3', '7√6', '√21'],
answer: 2,
explanation: '같은 무리수끼리 계수를 더한다. (2 + 5)√3 = 7√3.',
difficulty: 'easy',
},
{
number: 5,
@@ -36,6 +40,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5.618', '6.618', '6.708', '7.236', '8.236'],
answer: 3,
explanation: '3 × 2.236 = 6.708.',
difficulty: 'easy',
},
{
number: 6,
@@ -49,6 +54,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 2,
explanation: '유리수 + 무리수 = 무리수. 예: 1 + √2 = 1 + √2(무리수). 순환소수는 유리수, √9 = 3(유리수).',
difficulty: 'hard',
},
// ── 연립방정식 (7~12) ────────────────────────────────────────────────────────
@@ -58,6 +64,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 3,
explanation: '두 식을 더하면 2x = 6. x = 3. (y = 5 3 = 2 검증 가능)',
difficulty: 'easy',
},
{
number: 8,
@@ -65,6 +72,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 3,
explanation: '첫 식에서 y = 7 2x. 대입하면 x + 2(7 2x) = 8 → x + 14 4x = 8 → 3x = 6 → x = 2. y = 7 4 = 3.',
difficulty: 'medium',
},
{
number: 9,
@@ -72,6 +80,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2', '3', '4', '5', '6'],
answer: 3,
explanation: '더하면 3x = 12 → x = 4. y = 4 4 = 0. x + y = 4.',
difficulty: 'medium',
},
{
number: 10,
@@ -79,6 +88,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 3,
explanation: 'y = 7 x를 대입: 3x 2(7 x) = 1 → 3x 14 + 2x = 1 → 5x = 15 → x = 3.',
difficulty: 'medium',
},
{
number: 11,
@@ -86,6 +96,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3,000원', '4,000원', '5,000원', '6,000원', '7,000원'],
answer: 3,
explanation: 'a + c = 6000에서 c = 6000 a. 2a + 3(6000 a) = 13000 → 2a + 18000 3a = 13000 → a = 5000 → a = 5,000원.',
difficulty: 'hard',
},
{
number: 12,
@@ -93,6 +104,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0개(불능)', '1개', '2개', '무한히 많다(부정)', '판단 불가'],
answer: 4,
explanation: '두 번째 식은 첫 번째 식에 2를 곱한 것과 같다. 두 식이 일치하므로 해가 무한히 많다(부정).',
difficulty: 'hard',
},
// ── 일차함수 (13~18) ─────────────────────────────────────────────────────────
@@ -102,6 +114,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '2', '0', '2', '3'],
answer: 2,
explanation: 'x = 0 대입: y = 3 × 0 2 = 2. y 절편은 2.',
difficulty: 'easy',
},
{
number: 14,
@@ -109,6 +122,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['5', '2', '1', '2', '5'],
answer: 2,
explanation: 'y = mx + b 형태에서 m이 기울기. y = 2x + 5에서 기울기 = 2.',
difficulty: 'easy',
},
{
number: 15,
@@ -116,6 +130,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '5'],
answer: 2,
explanation: '기울기 = (y₂ y₁) / (x₂ x₁) = (7 3) / (3 1) = 4 / 2 = 2.',
difficulty: 'easy',
},
{
number: 16,
@@ -129,6 +144,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 3,
explanation: 'a > 0 이면 오른쪽 위로 증가, b < 0 이면 y 절편이 음수(원점 아래를 지남).',
difficulty: 'medium',
},
{
number: 17,
@@ -136,6 +152,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['한 점에서 만난다', '두 점에서 만난다', '평행하다', '일치한다', '수직이다'],
answer: 3,
explanation: '두 직선의 기울기가 같고 (2 = 2) y 절편이 다르므로 평행하다.',
difficulty: 'medium',
},
{
number: 18,
@@ -143,6 +160,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1', '2', '3', '4'],
answer: 2,
explanation: 'x + 2 = x + 4 → 2x = 2 → x = 1.',
difficulty: 'medium',
},
// ── 삼각형 성질 (19~24) ──────────────────────────────────────────────────────
@@ -152,6 +170,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['50°', '60°', '70°', '80°', '90°'],
answer: 3,
explanation: '세 각의 합 = 180°. 두 밑각이 같으므로 2a + 40° = 180° → 2a = 140° → a = 70°.',
difficulty: 'easy',
},
{
number: 20,
@@ -159,6 +178,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['같다', '크다', '작다', '두 배이다', '절반이다'],
answer: 1,
explanation: '삼각형 외각 정리: 한 외각의 크기 = 그 이웃하지 않는 두 내각의 합.',
difficulty: 'medium',
},
{
number: 21,
@@ -172,6 +192,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 3,
explanation: 'SSS(Side-Side-Side) 합동 조건: 세 쌍의 대응 변의 길이가 모두 같을 때.',
difficulty: 'easy',
},
{
number: 22,
@@ -179,6 +200,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3', '4', '5', '6', '7'],
answer: 3,
explanation: '피타고라스 정리: 3² + 4² = 9 + 16 = 25 = 5². 빗변 = 5.',
difficulty: 'easy',
},
{
number: 23,
@@ -186,6 +208,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['외심', '무게중심', '수심', '내심', '꼭짓점'],
answer: 4,
explanation: '내각의 이등분선 교점 = 내심(내접원의 중심). 외각이등분선 교점은 방심.',
difficulty: 'hard',
},
{
number: 24,
@@ -193,6 +216,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1:1', '1:2', '2:1', '3:1', '1:3'],
answer: 3,
explanation: '무게중심은 각 중선을 꼭짓점에서부터 2:1로 나눈다.',
difficulty: 'hard',
},
// ── 확률 (25~30) ─────────────────────────────────────────────────────────────
@@ -202,6 +226,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/6', '1/3', '1/2', '2/3', '5/6'],
answer: 3,
explanation: '짝수의 눈: 2, 4, 6 → 3가지. 전체 6가지. 확률 = 3/6 = 1/2.',
difficulty: 'easy',
},
{
number: 26,
@@ -209,6 +234,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/10', '2/10', '3/10', '4/10', '5/10'],
answer: 3,
explanation: '3의 배수: 3, 6, 9 → 3가지. 확률 = 3/10.',
difficulty: 'easy',
},
{
number: 27,
@@ -216,6 +242,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/4', '1/2', '3/4', '1/3', '2/3'],
answer: 1,
explanation: '전체 경우: HH, HT, TH, TT → 4가지. 둘 다 앞면: HH → 1가지. 확률 = 1/4.',
difficulty: 'easy',
},
{
number: 28,
@@ -223,6 +250,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/6', '1/4', '1/3', '2/3', '5/6'],
answer: 4,
explanation: '여사건 확률 = 1 P(A) = 1 1/3 = 2/3.',
difficulty: 'easy',
},
{
number: 29,
@@ -230,6 +258,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0.1', '0.3', '0.4', '0.7', '0.12'],
answer: 4,
explanation: '배반사건이면 P(A B) = P(A) + P(B) = 0.4 + 0.3 = 0.7.',
difficulty: 'medium',
},
{
number: 30,
@@ -237,5 +266,6 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2/5', '3/5', '4/10', '1/4', '1/2'],
answer: 2,
explanation: '파란 공 6개 / 전체 10개 = 6/10 = 3/5.',
difficulty: 'easy',
},
];

View File

@@ -8,6 +8,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(x+1)(x+6)', '(x+2)(x+3)', '(x2)(x3)', '(x+3)(x+2)', '(x1)(x6)'],
answer: 2,
explanation: '두 수의 합 = 5, 곱 = 6인 수: 2와 3. 따라서 (x+2)(x+3). ※ ②와 ④는 동일해서 ②가 정답.',
difficulty: 'easy',
},
{
number: 2,
@@ -15,6 +16,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(x3)²', '(x+3)²', '(x3)(x+3)', '(x9)(x+1)', '인수분해 불가'],
answer: 3,
explanation: '합차 공식: a² b² = (ab)(a+b). x² 9 = x² 3² = (x3)(x+3).',
difficulty: 'easy',
},
{
number: 3,
@@ -22,6 +24,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2(x+2)', '2x(x+2)', 'x(2x+4)', '2(x²+2x)', '(2x+1)(x+2)'],
answer: 2,
explanation: '공통인수 2x를 묶는다. 2x² + 4x = 2x(x + 2).',
difficulty: 'easy',
},
{
number: 4,
@@ -29,6 +32,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(x3)(x+3)', '(x3)²', '(x+3)²', '(x9)(x+1)', '(x1)(x9)'],
answer: 2,
explanation: '완전제곱식: x² 2·3·x + 3² = (x3)². 완전제곱 공식 (ab)² = a²2ab+b².',
difficulty: 'medium',
},
{
number: 5,
@@ -36,6 +40,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3(x²4)', '3(x2)(x+2)', '(3x6)(x+2)', '3(x+2)²', '(x2)(3x+6)'],
answer: 2,
explanation: '공통인수 3을 묶은 후 합차 공식: 3(x²4) = 3(x2)(x+2).',
difficulty: 'medium',
},
// ── 이차방정식 (6~10) ─────────────────────────────────────────────────────────
@@ -45,6 +50,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['6', '5', '1', '5', '6'],
answer: 4,
explanation: '(x2)(x3) = 0 → x = 2 또는 x = 3. 두 근의 합 = 2 + 3 = 5. (비에타: (5)/1 = 5)',
difficulty: 'medium',
},
{
number: 7,
@@ -52,6 +58,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['x = 2', 'x = 2', 'x = ±2', 'x = 4', 'x = ±4'],
answer: 3,
explanation: 'x² = 4. x = ±√4 = ±2.',
difficulty: 'easy',
},
{
number: 8,
@@ -59,6 +66,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '2', '2', '4', '8'],
answer: 3,
explanation: '(x+4)(x2) = 0 → x = 4 또는 x = 2. 큰 값은 2.',
difficulty: 'medium',
},
{
number: 9,
@@ -66,6 +74,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['x = 2 또는 x = 1/2', 'x = 1 또는 x = 2', 'x = 3 또는 x = 1', 'x = 2 또는 x = 1', 'x = 2 또는 x = 1/2'],
answer: 1,
explanation: 'x = (3 ± √(9+16)) / 4 = (3 ± 5) / 4. x = 8/4 = 2 또는 x = 2/4 = 1/2.',
difficulty: 'hard',
},
{
number: 10,
@@ -73,6 +82,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0개(허수)', '1개(중근)', '2개(서로 다른 실근)', '무한개', '판단 불가'],
answer: 3,
explanation: 'D > 0이면 서로 다른 두 실근, D = 0이면 중근(1개), D < 0이면 실근 없음.',
difficulty: 'medium',
},
// ── 이차함수 (11~15) ──────────────────────────────────────────────────────────
@@ -82,6 +92,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['(2, 1)', '(2, 1)', '(4, 3)', '(2, 1)', '(4, 3)'],
answer: 1,
explanation: 'y = (x2)² 1로 변형. 꼭짓점은 (2, 1).',
difficulty: 'hard',
},
{
number: 12,
@@ -89,6 +100,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '1', '1', '4', '5'],
answer: 4,
explanation: '위로 볼록 포물선. 꼭짓점 (1, 4)에서 최댓값 = 4.',
difficulty: 'medium',
},
{
number: 13,
@@ -102,6 +114,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 1,
explanation: 'a = 2 > 0이므로 아래로 볼록, 꼭짓점은 (0, 0). a의 절댓값이 클수록 폭이 좁다.',
difficulty: 'medium',
},
{
number: 14,
@@ -109,6 +122,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['0', '1', '2', '3', '무한'],
answer: 3,
explanation: 'x² + 2x 3 = (x+3)(x1) = 0 → x = 3 또는 x = 1. x 절편 2개.',
difficulty: 'medium',
},
{
number: 15,
@@ -122,6 +136,7 @@ export const PROBLEMS: SampleProblem[] = [
],
answer: 3,
explanation: 'y = a(xp)² + q에 a=1, p=3, q=2를 대입하면 y = (x3)² 2.',
difficulty: 'easy',
},
// ── 피타고라스 / 삼각비 (16~22) ──────────────────────────────────────────────
@@ -131,6 +146,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['10', '11', '13', '14', '15'],
answer: 3,
explanation: '5² + 12² = 25 + 144 = 169 = 13². 빗변 = 13.',
difficulty: 'easy',
},
{
number: 17,
@@ -138,6 +154,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['4', '6', '7', '8', '9'],
answer: 4,
explanation: '6² + b² = 10² → 36 + b² = 100 → b² = 64 → b = 8.',
difficulty: 'medium',
},
{
number: 18,
@@ -145,6 +162,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3/5', '4/5', '3/4', '5/3', '5/4'],
answer: 1,
explanation: 'sin = (맞은편 변) / (빗변) = 3/5.',
difficulty: 'easy',
},
{
number: 19,
@@ -152,6 +170,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3/5', '4/5', '3/4', '5/4', '5/3'],
answer: 2,
explanation: 'cos = (인접변) / (빗변) = 4/5.',
difficulty: 'easy',
},
{
number: 20,
@@ -159,6 +178,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['√3', '1/√3 (= √3/3)', '√2/2', '1', '√3/2'],
answer: 2,
explanation: 'tan 30° = sin 30° / cos 30° = (1/2) / (√3/2) = 1/√3 = √3/3.',
difficulty: 'medium',
},
{
number: 21,
@@ -166,6 +186,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1/2', '√2/2', '√3/2', '1', '√2'],
answer: 2,
explanation: '45-45-90 삼각형에서 sin 45° = 1/√2 = √2/2.',
difficulty: 'medium',
},
{
number: 22,
@@ -173,6 +194,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['3/5', '4/5', '3/4', '4/3', '5/4'],
answer: 2,
explanation: 'tan A = 맞은편/인접 = 4/3. 빗변 = √(4²+3²) = 5. sin A = 4/5.',
difficulty: 'hard',
},
// ── 원의 성질 (23~30) ─────────────────────────────────────────────────────────
@@ -182,6 +204,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['15°', '30°', '60°', '90°', '120°'],
answer: 2,
explanation: '원주각 = 중심각 / 2. 60° / 2 = 30°.',
difficulty: 'medium',
},
{
number: 24,
@@ -189,6 +212,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2π cm', '3π cm', '4π cm', '6π cm', '9π cm'],
answer: 2,
explanation: '호의 길이 = 2πr × (중심각/360°) = 2π × 6 × (90/360) = 12π × 1/4 = 3π cm.',
difficulty: 'medium',
},
{
number: 25,
@@ -196,6 +220,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['호의 길이에 비례', '항상 같다', '중심각의 2배', '중심각과 같다', '호의 길이와 무관'],
answer: 2,
explanation: '같은 호에 대한 원주각은 모두 같다(원주각의 크기 일정성).',
difficulty: 'medium',
},
{
number: 26,
@@ -203,6 +228,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['90°', '120°', '180°', '270°', '360°'],
answer: 3,
explanation: '원에 내접하는 사각형(원내접 사각형)에서 대각의 합 = 180°.',
difficulty: 'hard',
},
{
number: 27,
@@ -210,6 +236,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['중심각과 같다', '원주각과 같다', '원주각의 2배', '중심각의 절반', '접선의 길이와 같다'],
answer: 2,
explanation: '접선과 현의 각(접현각) = 그 현이 대하는 원주각. 접선-현의 각 정리.',
difficulty: 'hard',
},
{
number: 28,
@@ -217,6 +244,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1', '2', '3', '4', '0'],
answer: 3,
explanation: '두 원이 외접(외부에서 접)할 때 공통 접선은 3개(외부 공통 접선 2 + 내부 공통 접선 1).',
difficulty: 'hard',
},
{
number: 29,
@@ -224,6 +252,7 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['1:2로', '2:1로', '이등분한다', '황금비로', '나누지 않는다'],
answer: 3,
explanation: '원의 중심에서 현에 내린 수선은 현을 이등분한다(원과 현의 성질).',
difficulty: 'easy',
},
{
number: 30,
@@ -231,5 +260,6 @@ export const PROBLEMS: SampleProblem[] = [
choices: ['2 cm', '3 cm', '4 cm', '5 cm', '6 cm'],
answer: 2,
explanation: '현의 절반 = 4 cm. 피타고라스: 5² = 4² + d² → d² = 9 → d = 3 cm.',
difficulty: 'hard',
},
];

View File

@@ -4,4 +4,5 @@ export interface SampleProblem {
choices: string[];
answer: number; // 1-based
explanation: string;
difficulty: 'hard' | 'medium' | 'easy';
}

View File

@@ -27,7 +27,13 @@ import {
import { clearToken, hasToken } from '@/lib/auth';
import { animations, theme } from '@/styles/theme';
type ReviewAction = StudyResult | 'skip';
type ReviewAction = 'hard' | 'medium' | 'easy' | 'skip';
const SELF_DIFFICULTY_RESULT: Record<'hard' | 'medium' | 'easy', StudyResult> = {
hard: 'incorrect',
medium: 'partial',
easy: 'correct',
};
interface SessionHistoryEntry {
action: ReviewAction;
@@ -37,16 +43,16 @@ interface SessionHistoryEntry {
}
interface SessionResultSummary {
correct: number;
incorrect: number;
partial: number;
hard: number;
medium: number;
easy: number;
skipped: number;
}
const KEYBOARD_HINTS = [
{ keys: ['1'], label: '틀림' },
{ keys: ['2'], label: '부분' },
{ keys: ['3'], label: '맞음' },
{ keys: ['1'], label: '어려웠어' },
{ keys: ['2'], label: '괜찮았어' },
{ keys: ['3'], label: '쉬웠어' },
{ keys: ['S'], label: '스킵' },
{ keys: ['←'], label: '이전' },
] as const;
@@ -150,22 +156,21 @@ export default function ReviewPage() {
const sessionResults = useMemo<SessionResultSummary>(() => {
return history.reduce<SessionResultSummary>(
(acc, entry) => {
if (entry.action === 'correct') acc.correct += 1;
else if (entry.action === 'incorrect') acc.incorrect += 1;
else if (entry.action === 'partial') acc.partial += 1;
if (entry.action === 'hard') acc.hard += 1;
else if (entry.action === 'medium') acc.medium += 1;
else if (entry.action === 'easy') acc.easy += 1;
else acc.skipped += 1;
return acc;
},
{ correct: 0, incorrect: 0, partial: 0, skipped: 0 },
{ hard: 0, medium: 0, easy: 0, skipped: 0 },
);
}, [history]);
const sessionAccuracy = useMemo(() => {
const total =
sessionResults.correct + sessionResults.incorrect + sessionResults.partial;
const total = sessionResults.hard + sessionResults.medium + sessionResults.easy;
if (total === 0) return null;
return (
((sessionResults.correct + sessionResults.partial * 0.5) / total) * 100
((sessionResults.easy + sessionResults.medium * 0.5) / total) * 100
);
}, [sessionResults]);
@@ -175,7 +180,7 @@ export default function ReviewPage() {
const completionAccuracy = sessionAccuracy ?? weeklyAccuracy;
const streakDays = dashboardSummary
? Math.max(activityStreakDays(activityLogs), 1)
: sessionResults.correct + sessionResults.partial + sessionResults.incorrect > 0
: sessionResults.hard + sessionResults.medium + sessionResults.easy > 0
? 1
: 0;
@@ -286,7 +291,10 @@ export default function ReviewPage() {
if (action === 'skip') {
await api.post(`/reviews/${item.id}/skip`);
} else {
await api.post(`/reviews/${item.id}/submit`, { result: action });
await api.post(`/reviews/${item.id}/submit`, {
result: SELF_DIFFICULTY_RESULT[action],
selfDifficulty: action,
});
}
setQueue(nextQueue);
@@ -342,19 +350,19 @@ export default function ReviewPage() {
if (event.key === '1' && currentItem) {
event.preventDefault();
void handleResult(currentItem, 'incorrect');
void handleResult(currentItem, 'hard');
return;
}
if (event.key === '2' && currentItem) {
event.preventDefault();
void handleResult(currentItem, 'partial');
void handleResult(currentItem, 'medium');
return;
}
if (event.key === '3' && currentItem) {
event.preventDefault();
void handleResult(currentItem, 'correct');
void handleResult(currentItem, 'easy');
return;
}
@@ -454,18 +462,16 @@ export default function ReviewPage() {
<CompletionStats>
<CompletionStat>
<span> </span>
<strong>
{completionAccuracy === null ? '--' : `${Math.round(completionAccuracy)}%`}
</strong>
<span></span>
<strong>{sessionResults.hard}</strong>
</CompletionStat>
<CompletionStat>
<span> </span>
<strong>{streakDays || 1}</strong>
<span></span>
<strong>{sessionResults.medium}</strong>
</CompletionStat>
<CompletionStat>
<span> </span>
<strong>{sessionResults.partial}</strong>
<span></span>
<strong>{sessionResults.easy}</strong>
</CompletionStat>
</CompletionStats>
@@ -683,14 +689,13 @@ export default function ReviewPage() {
type="button"
$tone="incorrect"
disabled={pending.has(currentItem.id)}
onClick={() => void handleResult(currentItem, 'incorrect')}
onClick={() => void handleResult(currentItem, 'hard')}
>
<ActionCopy>
<ActionLabel>
<Icon name="x" size={20} weight="bold" />
🔴
</ActionLabel>
<ActionHelper>, </ActionHelper>
<ActionHelper>1 </ActionHelper>
</ActionCopy>
<ActionShortcut>1</ActionShortcut>
</ActionButton>
@@ -699,14 +704,13 @@ export default function ReviewPage() {
type="button"
$tone="partial"
disabled={pending.has(currentItem.id)}
onClick={() => void handleResult(currentItem, 'partial')}
onClick={() => void handleResult(currentItem, 'medium')}
>
<ActionCopy>
<ActionLabel>
<Icon name="triangle" size={20} weight="bold" />
🟡
</ActionLabel>
<ActionHelper> . </ActionHelper>
<ActionHelper>7 </ActionHelper>
</ActionCopy>
<ActionShortcut>2</ActionShortcut>
</ActionButton>
@@ -715,14 +719,13 @@ export default function ReviewPage() {
type="button"
$tone="correct"
disabled={pending.has(currentItem.id)}
onClick={() => void handleResult(currentItem, 'correct')}
onClick={() => void handleResult(currentItem, 'easy')}
>
<ActionCopy>
<ActionLabel>
<Icon name="check" size={20} weight="bold" />
🟢
</ActionLabel>
<ActionHelper>, </ActionHelper>
<ActionHelper>30 </ActionHelper>
</ActionCopy>
<ActionShortcut>3</ActionShortcut>
</ActionButton>
@@ -792,6 +795,8 @@ function computeWeeklyAccuracy(weekly: DashboardSummary['weekly']): number | nul
return ((weekly.correct + weekly.partial * 0.5) / total) * 100;
}
// streakDays 계산용 — weekly는 그대로 DashboardSummary 구조를 참조
function activityStreakDays(logs: StudyLog[]): number {
if (logs.length === 0) return 0;