Skip to content

Commit 593588a

Browse files
committed
test: add auto-tag-suggestions component test and DeepTutor assessment
- First React component test in codebase (5 cases, jsdom pragma) - Make globalSetup resilient to missing DB for component tests - Add DeepTutor feature integration assessment doc
1 parent 32f36ae commit 593588a

3 files changed

Lines changed: 241 additions & 13 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// @vitest-environment jsdom
2+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3+
import { render, fireEvent, cleanup } from "@testing-library/react";
4+
import { AutoTagSuggestions } from "../auto-tag-suggestions";
5+
6+
// ─────────────────────────────────────────────
7+
// Mock: tRPC
8+
// ─────────────────────────────────────────────
9+
const mockUseQuery = vi.fn();
10+
11+
vi.mock("@/lib/trpc", () => ({
12+
trpc: {
13+
item: {
14+
suggestMetadata: {
15+
useQuery: (...args: unknown[]) => mockUseQuery(...args),
16+
},
17+
},
18+
},
19+
}));
20+
21+
// ─────────────────────────────────────────────
22+
// Mock: shared constants (Node ESM 호환)
23+
// ─────────────────────────────────────────────
24+
vi.mock("@math-item-os/shared/constants/index", () => ({
25+
BLOOM_LEVEL: {
26+
1: { value: 1, label: "기억", order: 1 },
27+
2: { value: 2, label: "이해", order: 2 },
28+
3: { value: 3, label: "적용", order: 3 },
29+
4: { value: 4, label: "분석", order: 4 },
30+
5: { value: 5, label: "평가", order: 5 },
31+
6: { value: 6, label: "창조", order: 6 },
32+
},
33+
}));
34+
35+
// ─────────────────────────────────────────────
36+
// 테스트 데이터
37+
// ─────────────────────────────────────────────
38+
const mockData = {
39+
skills: [{ id: "sk1", title: "일차방정식", similarity: 0.92 }],
40+
standards: [{ id: "std1", code: "M8-01", title: "표준1" }],
41+
misconceptions: [{ id: "mc1", title: "부호 오류", typicalError: "부호 반전" }],
42+
bloomLevel: 3,
43+
};
44+
45+
function defaultProps(overrides: Partial<Parameters<typeof AutoTagSuggestions>[0]> = {}) {
46+
return {
47+
bodyLatex: "x + 1 = 0",
48+
schoolLevel: "middle" as const,
49+
grade: 2,
50+
selectedSkillIds: [] as string[],
51+
selectedStandardIds: [] as string[],
52+
selectedMisconceptionIds: [] as string[],
53+
onSkillSelect: vi.fn(),
54+
onStandardSelect: vi.fn(),
55+
onMisconceptionSelect: vi.fn(),
56+
...overrides,
57+
};
58+
}
59+
60+
// ─────────────────────────────────────────────
61+
// 테스트
62+
// ─────────────────────────────────────────────
63+
describe("AutoTagSuggestions", () => {
64+
afterEach(cleanup);
65+
66+
beforeEach(() => {
67+
vi.clearAllMocks();
68+
mockUseQuery.mockReturnValue({ data: undefined, isLoading: false, error: null });
69+
});
70+
71+
it("bodyLatex 빈 문자열 → null 렌더링", () => {
72+
mockUseQuery.mockReturnValue({ data: undefined, isLoading: false, error: null });
73+
const { container } = render(
74+
<AutoTagSuggestions {...defaultProps({ bodyLatex: "" })} />,
75+
);
76+
expect(container.innerHTML).toBe("");
77+
});
78+
79+
it("isLoading → 스켈레톤(animate-pulse) 표시", () => {
80+
mockUseQuery.mockReturnValue({ data: undefined, isLoading: true, error: null });
81+
const { container } = render(
82+
<AutoTagSuggestions {...defaultProps()} />,
83+
);
84+
expect(container.querySelector(".animate-pulse")).not.toBeNull();
85+
});
86+
87+
it("data 반환 → 스킬/성취기준/오개념 칩 렌더링", () => {
88+
mockUseQuery.mockReturnValue({ data: mockData, isLoading: false, error: null });
89+
const { getByText } = render(
90+
<AutoTagSuggestions {...defaultProps()} />,
91+
);
92+
expect(getByText("일차방정식")).toBeDefined();
93+
expect(getByText("M8-01 표준1")).toBeDefined();
94+
expect(getByText("부호 오류")).toBeDefined();
95+
// Bloom 레벨 배지
96+
expect(getByText("적용(3)")).toBeDefined();
97+
});
98+
99+
it("칩 클릭 → onSkillSelect 콜백 호출", () => {
100+
mockUseQuery.mockReturnValue({ data: mockData, isLoading: false, error: null });
101+
const onSkillSelect = vi.fn();
102+
const { getByText } = render(
103+
<AutoTagSuggestions {...defaultProps({ onSkillSelect })} />,
104+
);
105+
fireEvent.click(getByText("일차방정식"));
106+
expect(onSkillSelect).toHaveBeenCalledWith("sk1");
107+
});
108+
109+
it("선택된 항목 → 체크마크(✓) 표시", () => {
110+
mockUseQuery.mockReturnValue({ data: mockData, isLoading: false, error: null });
111+
const { container } = render(
112+
<AutoTagSuggestions {...defaultProps({ selectedSkillIds: ["sk1"] })} />,
113+
);
114+
// sk1 칩의 버튼 내부에 ✓ 문자 존재
115+
const buttons = container.querySelectorAll("button");
116+
const sk1Button = Array.from(buttons).find((b) => b.textContent?.includes("일차방정식"));
117+
expect(sk1Button?.textContent).toContain("\u2713");
118+
});
119+
});

apps/web/src/test-setup.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@ import { PrismaClient } from "@math-item-os/db";
99
const prisma = new PrismaClient();
1010

1111
export async function setup(): Promise<void> {
12-
await prisma.$executeRawUnsafe(
13-
`DROP RULE IF EXISTS audit_log_no_update ON audit_logs`,
14-
);
15-
await prisma.$executeRawUnsafe(
16-
`DROP RULE IF EXISTS audit_log_no_delete ON audit_logs`,
17-
);
18-
await prisma.$executeRawUnsafe(
19-
`DROP RULE IF EXISTS item_version_no_update ON item_versions`,
20-
);
21-
await prisma.$executeRawUnsafe(
22-
`DROP RULE IF EXISTS item_version_no_delete ON item_versions`,
23-
);
24-
await prisma.$disconnect();
12+
try {
13+
await prisma.$executeRawUnsafe(
14+
`DROP RULE IF EXISTS audit_log_no_update ON audit_logs`,
15+
);
16+
await prisma.$executeRawUnsafe(
17+
`DROP RULE IF EXISTS audit_log_no_delete ON audit_logs`,
18+
);
19+
await prisma.$executeRawUnsafe(
20+
`DROP RULE IF EXISTS item_version_no_update ON item_versions`,
21+
);
22+
await prisma.$executeRawUnsafe(
23+
`DROP RULE IF EXISTS item_version_no_delete ON item_versions`,
24+
);
25+
await prisma.$disconnect();
26+
} catch {
27+
// DB 미가동 시 무시 — 컴포넌트 테스트 등 DB 불필요 테스트 허용
28+
await prisma.$disconnect().catch(() => {});
29+
}
2530
}
2631

2732
export async function teardown(): Promise<void> {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# DeepTutor Feature 통합 평가
2+
3+
> 작성일: 2026-04-12 | 대상: [HKUDS/DeepTutor](https://github.com/HKUDS/DeepTutor) v1.0.2 (Apache-2.0)
4+
5+
## 개요
6+
7+
DeepTutor는 Agent-Native 개인화 튜터링 플랫폼(Python 3.11+ / Next.js 16)으로,
8+
Two-layer 플러그인 모델(Tools + Capabilities)을 기반으로 한다.
9+
10+
hwp-to-html(math-item-os)에 통합할 가치가 있는 Capability를 평가한다.
11+
12+
## Capability 분석
13+
14+
| Capability | 설명 | 관련성 | 난이도 |
15+
|-----------|------|--------|--------|
16+
| **Visualize** | SVG/Chart.js 3단계 파이프라인 (분석→생성→리뷰) | **높음** | 낮음-중간 |
17+
| **Math Animator** | Manim 5-agent 파이프라인, 비디오/이미지 출력 | **높음** | 높음 |
18+
| **Deep Solve** | 멀티에이전트 문제풀이 (Plan→ReAct→Write) | **중간** | 중간 |
19+
| Deep Question | 퀴즈 생성 | 낮음 | 중간 |
20+
| Guided Learning | 구조화 학습 여정 | 중간 | 높음 |
21+
| Knowledge Hub | RAG 지식 베이스 | 낮음 | 낮음 |
22+
| Co-Writer | AI 협업 에디터 | 낮음 | 중간 |
23+
24+
## Effort/Impact 매트릭스
25+
26+
```
27+
High Impact
28+
|
29+
Visualize | Math Animator
30+
(Low Eff.) | (High Eff.)
31+
|
32+
─────────────┼─────────────
33+
|
34+
Deep Solve | Guided Learning
35+
(Med Eff.) | (High Eff.)
36+
|
37+
Low Impact
38+
```
39+
40+
## Top 3 통합 후보
41+
42+
### 1순위: Visualize (SVG/Chart.js)
43+
44+
**선택 이유**: 인프라 추가 불필요, 스키마 변경 없이 시작 가능, 즉각적 가치.
45+
46+
**통합 경로**:
47+
- `services/math-ai/app/routers/visualize.py` — 3단계 파이프라인 (분석→생성→리뷰)
48+
- 입력: LaTeX 본문 → LLM SVG/Chart.js 코드 생성
49+
- 출력: SVG 문자열 또는 Chart.js spec JSON
50+
- 저장: `Solution.steps` JSON에 `{ type: "svg", content: "..." }` 추가
51+
- 프론트: `apps/web/src/components/items/visual-solution.tsx`
52+
- tRPC: `item.generateVisualization` mutation
53+
54+
**기존 자산 활용**:
55+
- `anthropic-generation.service.ts` — LLM 호출 패턴
56+
- `services/math-ai/app/services/sympy_solver.py` — 수식 파싱 재사용
57+
- DeepTutor `deeptutor/capabilities/visualize.py` — 파이프라인 설계 참조
58+
59+
**예상 기간**: 1주 (기본), 2주 (Chart.js 인터랙션 포함)
60+
61+
### 2순위: Math Animator (Manim)
62+
63+
**선택 이유**: 높은 시각적 영향력. `SolutionMethod.visual` enum이 이미 존재.
64+
65+
**통합 경로**:
66+
- `services/math-ai/app/routers/animate.py` — 5-agent 파이프라인
67+
- DB: `Solution` 모델에 `videoUrl String?` 추가
68+
- 인프라: Docker sidecar (Manim + LaTeX + ffmpeg) 또는 BullMQ 비동기 워커
69+
- 객체 스토리지: S3/R2에 비디오 저장
70+
71+
**리스크**: Manim 시스템 의존성(LaTeX, ffmpeg, Cairo), 코드 생성 불안정성 (RetryManager 필수)
72+
73+
**DeepTutor 참조**: `math_animator.py`, `pipeline.py`, `renderer.py`, `retry_manager.py`
74+
75+
**예상 기간**: 2-4주
76+
77+
### 3순위: Deep Solve (멀티에이전트 풀이)
78+
79+
**선택 이유**: `generation.service`의 SymPy/LLM 전략을 보완하는 고급 모드.
80+
81+
**통합 경로**:
82+
- `generation.service.ts``"deep-solve"` 전략 추가
83+
- `services/math-ai/app/routers/solve.py` — Plan→ReAct→Write 파이프라인
84+
- 기존 `sympy_solver.py``verify_answer()`, `solve_equation()`을 ReAct 도구로 활용
85+
- 스키마 변경 없음
86+
87+
**예상 기간**: 1-2주
88+
89+
## 의존성 결정 사항 (구현 전 확정 필요)
90+
91+
| 항목 | 선택지 | 영향 범위 |
92+
|------|--------|----------|
93+
| 객체 스토리지 | S3, R2, 로컬(dev) | Math Animator 비디오 저장 |
94+
| LLM 프로바이더 | Z.ai/GLM-4.7, Claude 직접 | agent 파이프라인 |
95+
| Manim 렌더링 | 동기, 비동기(BullMQ) | Math Animator 아키텍처 |
96+
| 라이선스 | Apache-2.0 | 코드 참조/포팅 가능 확인됨 |
97+
98+
## 실행 로드맵
99+
100+
```
101+
[다음 스프린트] Visualize SVG/Chart.js 프로토타입
102+
[2-4주] Math Animator Docker + 기본 파이프라인
103+
[2-4주] Deep Solve 전략 추가
104+
```

0 commit comments

Comments
 (0)