feat: 2022 개정 교육과정 수학 프리셋 — 과목 생성 시 단원 자동 배치
- backend: curriculum-presets.ts에 중1~고3 + 공통수학/대수/미적분/확통/기하 단원 데이터 - backend: subjects.create() 시 과목명이 프리셋에 해당하면 태그 자동 createMany - frontend: 과목 생성 모달에 교육과정 프리셋 칩 13개 추가 (중1수학~고3수학, 공통수학1/2, 대수, 미적분I/II, 확통, 기하) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
244
backend/src/subjects/curriculum-presets.ts
Normal file
244
backend/src/subjects/curriculum-presets.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* 2022 개정 교육과정 수학 단원 프리셋
|
||||
* 과목명(예: "중1수학")을 입력하면 해당 단원이 태그로 자동 생성됨
|
||||
*/
|
||||
|
||||
export const CURRICULUM_PRESETS: Record<string, string[]> = {
|
||||
// ── 중학교 ──────────────────────────────────────────────────────
|
||||
중1수학: [
|
||||
'소인수분해',
|
||||
'정수와 유리수',
|
||||
'문자와 식',
|
||||
'일차방정식',
|
||||
'좌표평면과 그래프',
|
||||
'기본 도형',
|
||||
'평면도형의 성질',
|
||||
'입체도형의 성질',
|
||||
'자료의 정리와 해석',
|
||||
],
|
||||
중2수학: [
|
||||
'수와 식의 계산',
|
||||
'일차부등식',
|
||||
'연립일차방정식',
|
||||
'일차함수와 그래프',
|
||||
'도형의 성질',
|
||||
'도형의 닮음',
|
||||
'피타고라스 정리',
|
||||
'확률',
|
||||
],
|
||||
중3수학: [
|
||||
'실수와 그 계산',
|
||||
'다항식의 곱셈과 인수분해',
|
||||
'이차방정식',
|
||||
'이차함수와 그래프',
|
||||
'삼각비',
|
||||
'원의 성질',
|
||||
'통계',
|
||||
],
|
||||
|
||||
// ── 고등학교 공통 ───────────────────────────────────────────────
|
||||
'공통수학1': [
|
||||
'다항식의 연산',
|
||||
'나머지정리와 인수분해',
|
||||
'복소수',
|
||||
'이차방정식',
|
||||
'이차함수와 이차방정식',
|
||||
'고차방정식',
|
||||
'부등식과 이차부등식',
|
||||
'경우의 수',
|
||||
'순열과 조합',
|
||||
'행렬',
|
||||
],
|
||||
'공통수학2': [
|
||||
'직선의 방정식',
|
||||
'원의 방정식',
|
||||
'평행이동과 대칭이동',
|
||||
'집합',
|
||||
'명제와 조건',
|
||||
'함수',
|
||||
'합성함수와 역함수',
|
||||
'유리함수와 무리함수',
|
||||
],
|
||||
고1수학: [
|
||||
// 공통수학1+2 통합
|
||||
'다항식의 연산',
|
||||
'나머지정리와 인수분해',
|
||||
'복소수',
|
||||
'이차방정식',
|
||||
'이차함수와 이차방정식',
|
||||
'고차방정식',
|
||||
'부등식과 이차부등식',
|
||||
'경우의 수',
|
||||
'순열과 조합',
|
||||
'행렬',
|
||||
'직선의 방정식',
|
||||
'원의 방정식',
|
||||
'평행이동과 대칭이동',
|
||||
'집합',
|
||||
'명제와 조건',
|
||||
'함수',
|
||||
'합성함수와 역함수',
|
||||
'유리함수와 무리함수',
|
||||
],
|
||||
|
||||
// ── 고등학교 일반선택 ───────────────────────────────────────────
|
||||
대수: [
|
||||
'지수',
|
||||
'로그',
|
||||
'지수함수',
|
||||
'로그함수',
|
||||
'삼각함수',
|
||||
'삼각함수의 그래프',
|
||||
'삼각함수의 활용',
|
||||
'등차수열과 등비수열',
|
||||
'수열의 합',
|
||||
'수학적 귀납법',
|
||||
],
|
||||
'미적분I': [
|
||||
'함수의 극한',
|
||||
'함수의 연속',
|
||||
'미분계수와 도함수',
|
||||
'접선의 방정식',
|
||||
'함수의 증가와 감소',
|
||||
'함수의 극대와 극소',
|
||||
'최댓값과 최솟값',
|
||||
'부정적분',
|
||||
'정적분',
|
||||
'정적분의 활용',
|
||||
],
|
||||
미적분1: [
|
||||
'함수의 극한',
|
||||
'함수의 연속',
|
||||
'미분계수와 도함수',
|
||||
'접선의 방정식',
|
||||
'함수의 증가와 감소',
|
||||
'함수의 극대와 극소',
|
||||
'최댓값과 최솟값',
|
||||
'부정적분',
|
||||
'정적분',
|
||||
'정적분의 활용',
|
||||
],
|
||||
'확률과 통계': [
|
||||
'순열과 조합',
|
||||
'이항정리',
|
||||
'확률의 뜻과 활용',
|
||||
'조건부확률',
|
||||
'독립시행의 확률',
|
||||
'확률변수와 확률분포',
|
||||
'이항분포',
|
||||
'정규분포',
|
||||
'통계적 추정',
|
||||
],
|
||||
확통: [
|
||||
'순열과 조합',
|
||||
'이항정리',
|
||||
'확률의 뜻과 활용',
|
||||
'조건부확률',
|
||||
'독립시행의 확률',
|
||||
'확률변수와 확률분포',
|
||||
'이항분포',
|
||||
'정규분포',
|
||||
'통계적 추정',
|
||||
],
|
||||
고2수학: [
|
||||
// 대수 + 미적분I + 확통 통합
|
||||
'지수',
|
||||
'로그',
|
||||
'지수함수',
|
||||
'로그함수',
|
||||
'삼각함수',
|
||||
'삼각함수의 그래프',
|
||||
'등차수열과 등비수열',
|
||||
'수열의 합',
|
||||
'함수의 극한',
|
||||
'함수의 연속',
|
||||
'미분계수와 도함수',
|
||||
'함수의 극대와 극소',
|
||||
'부정적분',
|
||||
'정적분',
|
||||
'확률',
|
||||
'이항분포와 정규분포',
|
||||
'통계적 추정',
|
||||
],
|
||||
|
||||
// ── 고등학교 진로선택 ───────────────────────────────────────────
|
||||
'미적분II': [
|
||||
'수열의 극한',
|
||||
'급수',
|
||||
'지수함수와 로그함수의 미분',
|
||||
'삼각함수의 미분',
|
||||
'합성함수의 미분법',
|
||||
'음함수와 역함수의 미분',
|
||||
'이계도함수',
|
||||
'도함수의 활용',
|
||||
'치환적분법',
|
||||
'부분적분법',
|
||||
'정적분의 활용',
|
||||
],
|
||||
미적분2: [
|
||||
'수열의 극한',
|
||||
'급수',
|
||||
'지수함수와 로그함수의 미분',
|
||||
'삼각함수의 미분',
|
||||
'합성함수의 미분법',
|
||||
'음함수와 역함수의 미분',
|
||||
'이계도함수',
|
||||
'도함수의 활용',
|
||||
'치환적분법',
|
||||
'부분적분법',
|
||||
'정적분의 활용',
|
||||
],
|
||||
기하: [
|
||||
'포물선',
|
||||
'타원',
|
||||
'쌍곡선',
|
||||
'이차곡선의 접선',
|
||||
'공간도형',
|
||||
'공간좌표',
|
||||
'벡터의 연산',
|
||||
'벡터의 성분과 내적',
|
||||
],
|
||||
고3수학: [
|
||||
// 미적분II + 기하 통합
|
||||
'수열의 극한',
|
||||
'급수',
|
||||
'지수함수와 로그함수의 미분',
|
||||
'삼각함수의 미분',
|
||||
'합성함수의 미분법',
|
||||
'도함수의 활용',
|
||||
'치환적분법',
|
||||
'부분적분법',
|
||||
'정적분의 활용',
|
||||
'포물선',
|
||||
'타원',
|
||||
'쌍곡선',
|
||||
'이차곡선의 접선',
|
||||
'공간도형과 공간좌표',
|
||||
'벡터의 연산',
|
||||
'벡터의 내적',
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 과목명에서 프리셋 단원 목록을 찾는다.
|
||||
* 정확한 키 매칭 또는 공백/대소문자 무시 매칭
|
||||
*/
|
||||
export function findPresetUnits(subjectName: string): string[] | null {
|
||||
const trimmed = subjectName.trim();
|
||||
|
||||
// 정확한 키 매칭
|
||||
if (CURRICULUM_PRESETS[trimmed]) {
|
||||
return CURRICULUM_PRESETS[trimmed];
|
||||
}
|
||||
|
||||
// 공백/특수문자 제거 후 매칭
|
||||
const normalized = trimmed.replace(/[\s\-_·]/g, '').toLowerCase();
|
||||
for (const [key, units] of Object.entries(CURRICULUM_PRESETS)) {
|
||||
const normalizedKey = key.replace(/[\s\-_·]/g, '').toLowerCase();
|
||||
if (normalizedKey === normalized) {
|
||||
return units;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { findPresetUnits } from './curriculum-presets';
|
||||
|
||||
@Injectable()
|
||||
export class SubjectsService {
|
||||
@@ -22,10 +23,27 @@ export class SubjectsService {
|
||||
return subject;
|
||||
}
|
||||
|
||||
create(userId: number, name: string, color: string) {
|
||||
return this.prisma.subject.create({
|
||||
async create(userId: number, name: string, color: string) {
|
||||
const subject = await this.prisma.subject.create({
|
||||
data: { userId, name, color },
|
||||
});
|
||||
|
||||
// 교육과정 프리셋 단원 자동 생성
|
||||
const presetUnits = findPresetUnits(name);
|
||||
if (presetUnits && presetUnits.length > 0) {
|
||||
await this.prisma.tag.createMany({
|
||||
data: presetUnits.map((unitName) => ({
|
||||
subjectId: subject.id,
|
||||
name: unitName,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
return this.prisma.subject.findUniqueOrThrow({
|
||||
where: { id: subject.id },
|
||||
include: { tags: { orderBy: { name: 'asc' } } },
|
||||
});
|
||||
}
|
||||
|
||||
async update(userId: number, id: number, data: { name?: string; color?: string }) {
|
||||
|
||||
@@ -11,6 +11,22 @@ import { Button, Input, Label } from '@/components/ui/primitives';
|
||||
import { api, type Subject, type Tag } from '@/lib/api';
|
||||
import { animations, theme } from '@/styles/theme';
|
||||
|
||||
const MATH_PRESETS = [
|
||||
{ name: '중1수학', color: '#3b82f6' },
|
||||
{ name: '중2수학', color: '#0EA5E9' },
|
||||
{ name: '중3수학', color: '#06B6D4' },
|
||||
{ name: '공통수학1', color: '#6366f1' },
|
||||
{ name: '공통수학2', color: '#8B5CF6' },
|
||||
{ name: '고1수학', color: '#6366f1' },
|
||||
{ name: '대수', color: '#7C3AED' },
|
||||
{ name: '미적분I', color: '#EC4899' },
|
||||
{ name: '확률과 통계', color: '#F59E0B' },
|
||||
{ name: '고2수학', color: '#7C3AED' },
|
||||
{ name: '미적분II', color: '#EF4444' },
|
||||
{ name: '기하', color: '#10B981' },
|
||||
{ name: '고3수학', color: '#EF4444' },
|
||||
] as const;
|
||||
|
||||
const PRESET_COLORS = [
|
||||
theme.color.brandIndigo,
|
||||
theme.color.brandViolet,
|
||||
@@ -940,9 +956,26 @@ function SubjectModal({
|
||||
onChange={(event) =>
|
||||
onChange({ ...value, name: event.target.value })
|
||||
}
|
||||
placeholder="예) 공통 수학, 미적분"
|
||||
placeholder="예) 중1수학, 공통수학1, 대수"
|
||||
maxLength={40}
|
||||
/>
|
||||
{mode === 'create' && (
|
||||
<PresetSection>
|
||||
<PresetLabel>교육과정 프리셋 (선택하면 단원이 자동 생성돼요)</PresetLabel>
|
||||
<PresetChips>
|
||||
{MATH_PRESETS.map(({ name, color }) => (
|
||||
<PresetChip
|
||||
key={name}
|
||||
type="button"
|
||||
$active={value.name === name}
|
||||
onClick={() => onChange({ ...value, name, color })}
|
||||
>
|
||||
{name}
|
||||
</PresetChip>
|
||||
))}
|
||||
</PresetChips>
|
||||
</PresetSection>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -1549,6 +1582,44 @@ const FieldGroup = styled.div`
|
||||
gap: 18px;
|
||||
`;
|
||||
|
||||
const PresetSection = styled.div`
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const PresetLabel = styled.span`
|
||||
font-size: 12px;
|
||||
color: ${theme.color.textMute};
|
||||
`;
|
||||
|
||||
const PresetChips = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
const PresetChip = styled.button<{ $active: boolean }>`
|
||||
padding: 5px 12px;
|
||||
border-radius: ${theme.radius.pill};
|
||||
border: 1px solid
|
||||
${({ $active }) =>
|
||||
$active ? 'rgba(129, 140, 248, 0.6)' : theme.color.borderSoftAlpha};
|
||||
background: ${({ $active }) =>
|
||||
$active ? 'rgba(79, 70, 229, 0.18)' : 'rgba(255, 255, 255, 0.03)'};
|
||||
color: ${({ $active }) =>
|
||||
$active ? theme.color.textBright : theme.color.textSub};
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(129, 140, 248, 0.4);
|
||||
color: ${theme.color.textBright};
|
||||
}
|
||||
`;
|
||||
|
||||
const PickerWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user