This commit is contained in:
root
2026-07-05 02:06:37 +00:00
parent 104107bf1d
commit c3596c12f3
12 changed files with 1132 additions and 5 deletions
+231
View File
@@ -14,6 +14,8 @@ import (
"strings"
"time"
"changeme/antigravity"
"github.com/google/generative-ai-go/genai"
"github.com/wailsapp/wails/v3/pkg/application"
"google.golang.org/api/option"
@@ -121,6 +123,20 @@ func NewQuizService() *QuizService {
log.Fatalf("Failed to create QuizExplain table: %v", err)
}
// Create StudyElements table if it doesn't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS StudyElements (
question_id INTEGER NOT NULL,
source TEXT NOT NULL,
content TEXT,
PRIMARY KEY (source, question_id),
FOREIGN KEY (source, question_id) REFERENCES QuizQuestions (source, id) ON DELETE CASCADE
)
`)
if err != nil {
log.Fatalf("Failed to create StudyElements table: %v", err)
}
// Create Users table if it doesn't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS Users (
@@ -1377,3 +1393,218 @@ func (s *QuizService) UpdateUserInfoForce(username, nickname, phone, password st
}
}
type StudyElementDB struct {
QuestionID int `json:"questionId"`
Source string `json:"source"`
QuestionText string `json:"questionText"`
Content string `json:"content"`
Category string `json:"category"`
}
// GetStudyElements retrieves all extracted study elements for a category.
func (s *QuizService) GetStudyElements(category string) ([]StudyElementDB, error) {
query := `
SELECT se.question_id, se.source, se.content, q.question_text, q.category
FROM StudyElements se
JOIN QuizQuestions q ON se.source = q.source AND se.question_id = q.id
WHERE q.category = ?
ORDER BY se.question_id ASC
`
rows, err := s.db.Query(query, category)
if err != nil {
return nil, err
}
defer rows.Close()
var list []StudyElementDB
for rows.Next() {
var se StudyElementDB
err := rows.Scan(&se.QuestionID, &se.Source, &se.Content, &se.QuestionText, &se.Category)
if err == nil {
list = append(list, se)
}
}
return list, nil
}
// ExtractStudyElementsForCategory extracts study elements for all questions in the category.
// It skips questions that already have an extracted study element.
func (s *QuizService) ExtractStudyElementsForCategory(category string) (string, error) {
// 1. Get questions in this category that don't have study elements yet.
query := `
SELECT q.id, q.source, q.question_text, q.choice1, q.choice2, q.choice3, q.choice4, q.correct_answer_str, q.explanation
FROM QuizQuestions q
LEFT JOIN StudyElements se ON q.source = se.source AND q.id = se.question_id
WHERE q.category = ? AND se.question_id IS NULL
`
rows, err := s.db.Query(query, category)
if err != nil {
return "", err
}
defer rows.Close()
type Task struct {
ID int
Source string
QuestionText string
Choice1 string
Choice2 string
Choice3 string
Choice4 string
CorrectAnswerStr string
Explanation string
}
var tasks []Task
for rows.Next() {
var t Task
err := rows.Scan(&t.ID, &t.Source, &t.QuestionText, &t.Choice1, &t.Choice2, &t.Choice3, &t.Choice4, &t.CorrectAnswerStr, &t.Explanation)
if err == nil {
tasks = append(tasks, t)
}
}
if len(tasks) == 0 {
return "이미 모든 문제의 학습요소가 추출되어 있습니다.", nil
}
// 2. Start a background goroutine to process with limited concurrency.
go func() {
ctx := context.Background()
sem := make(chan struct{}, 3) // limit to 3 concurrent agy calls
total := len(tasks)
for i, t := range tasks {
sem <- struct{}{}
go func(idx int, task Task) {
defer func() { <-sem }()
// Update progress
s.emitEvent("study:progress", map[string]interface{}{
"current": idx + 1,
"total": total,
"message": fmt.Sprintf("문제 %d 추출 중...", task.ID),
})
content, err := antigravity.ExtractStudyElement(
ctx,
task.QuestionText,
task.Choice1,
task.Choice2,
task.Choice3,
task.Choice4,
task.CorrectAnswerStr,
task.Explanation,
)
if err != nil {
log.Printf("Failed to extract study element for question %d: %v", task.ID, err)
return
}
// Save to DB
_, err = s.db.Exec(`
INSERT INTO StudyElements (question_id, source, content)
VALUES (?, ?, ?)
ON CONFLICT(source, question_id) DO UPDATE SET content=excluded.content
`, task.ID, task.Source, content)
if err != nil {
log.Printf("Failed to save study element to DB: %v", err)
}
}(i, t)
}
// Wait for all goroutines to finish
for i := 0; i < 3; i++ {
sem <- struct{}{}
}
s.emitEvent("study:done", map[string]interface{}{
"category": category,
"message": "학습요소 추출이 완료되었습니다.",
})
}()
return "학습요소 추출을 시작합니다.", nil
}
// GenerateGoPracticalExams creates new exam questions based on existing ones.
func (s *QuizService) GenerateGoPracticalExams(count int) (string, error) {
if count <= 0 {
count = 5
}
// 1. Fetch sample real exams as references for the AI
rows, err := s.db.Query(`
SELECT question_text, explanation
FROM QuizQuestions
WHERE category = '정보보안기사 실기' AND explanation != ''
LIMIT 10
`)
if err != nil {
return "", err
}
defer rows.Close()
var references []string
for rows.Next() {
var qText, explanation string
if err := rows.Scan(&qText, &explanation); err == nil {
references = append(references, fmt.Sprintf("질문:\n%s\n\n모범답안/해설:\n%s", qText, explanation))
}
}
if len(references) == 0 {
references = []string{
"질문:\n다음 지문을 읽고 빈칸 A, B에 들어갈 적절한 용어를 쓰시오.\n- ( A )은 네트워크 상의 패킷을 가로채어 분석하는 행위 또는 도구를 말한다.\n- ( B )은 IP 주소를 스푸핑하여 정상적인 호스트인 것처럼 위장하는 공격이다.\n\n모범답안/해설:\nA: 스니핑(Sniffing)\nB: IP 스푸핑(IP Spoofing)",
}
}
// 2. Start a background goroutine to generate questions
go func() {
ctx := context.Background()
category := "정보보호기사 실기(Go)"
source := "qple"
difficulty := "실기(Go)"
subject := "만든 자료"
for i := 0; i < count; i++ {
s.emitEvent("exam:progress", map[string]interface{}{
"current": i + 1,
"total": count,
"message": fmt.Sprintf("%d번째 실기 문제 생성 중...", i+1),
})
gq, err := antigravity.GenerateDescriptiveQuestion(ctx, references)
if err != nil {
log.Printf("Failed to generate exam question %d: %v", i+1, err)
continue
}
// Generate next ID
var nextID int
err = s.db.QueryRow("SELECT COALESCE(MAX(id), 0) + 1 FROM QuizQuestions WHERE source = ?", source).Scan(&nextID)
if err != nil {
log.Printf("Failed to get next ID: %v", err)
continue
}
// Insert into QuizQuestions
_, err = s.db.Exec(`
INSERT INTO QuizQuestions (id, source, category, question_text, choice1, choice2, choice3, choice4, correct_answer, explanation, difficulty, subject)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nextID, source, category, gq.QuestionText, gq.Choice1, gq.Choice2, gq.Choice3, gq.Choice4, gq.CorrectAnswer, gq.Explanation, difficulty, subject)
if err != nil {
log.Printf("Failed to insert generated question: %v", err)
}
}
s.emitEvent("exam:done", map[string]interface{}{
"message": fmt.Sprintf("%d개의 실기 문제가 생성 및 저장되었습니다.", count),
})
}()
return "실기 문제 생성을 시작합니다.", nil
}