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:
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)(a−b) = a² − b².',
|
||||
difficulty: 'easy',
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
@@ -41,6 +44,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
],
|
||||
answer: 1,
|
||||
explanation: 'a³ − b³ = (a−b)(a²+ab+b²). x³−8 = x³−2³ = (x−2)(x²+2x+4).',
|
||||
difficulty: 'hard',
|
||||
},
|
||||
{
|
||||
number: 5,
|
||||
@@ -48,6 +52,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
choices: ['(x−1)³', '(x+1)³', '(x−1)(x²+x+1)', '(x+1)(x²−x+1)', '(x−1)²(x+1)'],
|
||||
answer: 1,
|
||||
explanation: '(a−b)³ = a³ − 3a²b + 3ab² − b³. a=x, b=1 대입하면 (x−1)³.',
|
||||
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: '(x−1)(x−2) ≤ 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: '(x−3)(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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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₁ + (n−1)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^(n−1). 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(n−1). 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(x−2) = 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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
choices: ['0', '2', '4', '6', '수렴하지 않음'],
|
||||
answer: 3,
|
||||
explanation: '분자 인수분해: (x−2)(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^(x−1)', '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(x−1)(x−3) = 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', '(x−1)·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 = (X−50)/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 = √((4−1)² + (6−2)² + (3−3)²) = √(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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
choices: ['(x+1)(x+6)', '(x+2)(x+3)', '(x−2)(x−3)', '(x+3)(x+2)', '(x−1)(x−6)'],
|
||||
answer: 2,
|
||||
explanation: '두 수의 합 = 5, 곱 = 6인 수: 2와 3. 따라서 (x+2)(x+3). ※ ②와 ④는 동일해서 ②가 정답.',
|
||||
difficulty: 'easy',
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
@@ -15,6 +16,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
choices: ['(x−3)²', '(x+3)²', '(x−3)(x+3)', '(x−9)(x+1)', '인수분해 불가'],
|
||||
answer: 3,
|
||||
explanation: '합차 공식: a² − b² = (a−b)(a+b). x² − 9 = x² − 3² = (x−3)(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: ['(x−3)(x+3)', '(x−3)²', '(x+3)²', '(x−9)(x+1)', '(x−1)(x−9)'],
|
||||
answer: 2,
|
||||
explanation: '완전제곱식: x² − 2·3·x + 3² = (x−3)². 완전제곱 공식 (a−b)² = a²−2ab+b².',
|
||||
difficulty: 'medium',
|
||||
},
|
||||
{
|
||||
number: 5,
|
||||
@@ -36,6 +40,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
choices: ['3(x²−4)', '3(x−2)(x+2)', '(3x−6)(x+2)', '3(x+2)²', '(x−2)(3x+6)'],
|
||||
answer: 2,
|
||||
explanation: '공통인수 3을 묶은 후 합차 공식: 3(x²−4) = 3(x−2)(x+2).',
|
||||
difficulty: 'medium',
|
||||
},
|
||||
|
||||
// ── 이차방정식 (6~10) ─────────────────────────────────────────────────────────
|
||||
@@ -45,6 +50,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
choices: ['−6', '−5', '1', '5', '6'],
|
||||
answer: 4,
|
||||
explanation: '(x−2)(x−3) = 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)(x−2) = 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 = (x−2)² − 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)(x−1) = 0 → x = −3 또는 x = 1. x 절편 2개.',
|
||||
difficulty: 'medium',
|
||||
},
|
||||
{
|
||||
number: 15,
|
||||
@@ -122,6 +136,7 @@ export const PROBLEMS: SampleProblem[] = [
|
||||
],
|
||||
answer: 3,
|
||||
explanation: 'y = a(x−p)² + q에 a=1, p=3, q=−2를 대입하면 y = (x−3)² − 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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,4 +4,5 @@ export interface SampleProblem {
|
||||
choices: string[];
|
||||
answer: number; // 1-based
|
||||
explanation: string;
|
||||
difficulty: 'hard' | 'medium' | 'easy';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user