"""
FGI (Focus Group Interview) survey tools - 대화형 심층 인터뷰
"""
import os
import uuid
import json
from datetime import datetime, timedelta
from groq import Groq
from ...db.database import get_session
from ...db.models import FGISurvey, FGIResponse, Persona
from ...utils.cache import get_cache
# ========================================
# 1단계: 기본 프로필 질문 (선택적)
# ========================================
BASIC_PROFILE_QUESTIONS = {
"age_range": {
"text": "나이대를 선택해주세요",
"type": "choice",
"options": ["10대", "20대 초반", "20대 후반", "30대 초반", "30대 후반", "40대 이상"]
},
"gender": {
"text": "성별을 선택해주세요",
"type": "choice",
"options": ["남성", "여성", "기타", "답변 안 함"]
},
"location": {
"text": "거주 지역을 선택해주세요",
"type": "choice",
"options": ["서울", "경기", "인천", "부산", "대구", "광주", "대전", "기타"]
},
"occupation_category": {
"text": "직업 분야를 선택해주세요",
"type": "choice",
"options": ["IT/개발", "디자인", "마케팅", "교육", "서비스", "학생", "기타"]
},
"income_range": {
"text": "월 소득 범위는? (선택사항)",
"type": "choice",
"optional": True,
"options": ["200만원 미만", "200-300만원", "300-500만원", "500만원 이상", "답변 안 함"]
}
}
# ========================================
# 2단계: 카테고리별 오프닝 질문
# ========================================
OPENING_QUESTIONS = {
"technology": "요즘 기술 분야에서 가장 관심있는 주제나 배우고 싶은 것이 있나요?",
"lifestyle": "평소 여가 시간을 어떻게 보내시나요? 가장 좋아하는 활동은?",
"fashion": "당신의 패션 스타일을 어떻게 설명하시겠어요? 어떤 옷을 즐겨 입나요?",
"food": "평소 식사는 주로 어떻게 해결하시나요? 좋아하는 음식 종류는?",
"culture": "최근에 본 영화, 드라마, 책 중에 인상 깊었던 게 있나요?",
"general": "요즘 당신의 일상이나 삶에서 가장 중요한 것은 무엇인가요?"
}
def get_basic_profile_questions():
"""
기본 프로필 질문 목록을 반환합니다.
:return: 기본 프로필 질문 딕셔너리
"""
return {
"questions": BASIC_PROFILE_QUESTIONS,
"description": "페르소나의 기본 정보를 수집하기 위한 질문 목록입니다.",
"usage": "각 질문에 대한 답변을 수집한 후 update_basic_profile_tool을 사용하여 저장하세요.",
"required_fields": ["age_range", "gender", "location", "occupation_category"],
"optional_fields": ["income_range"]
}
def check_basic_profile_complete(persona_id: str = None):
"""
기본 프로필이 완전한지 확인합니다.
:param persona_id: 페르소나 ID (None이면 모든 페르소나 확인)
:return: {
"is_complete": bool,
"missing_fields": list,
"persona_id": str or None,
"message": str
}
"""
required_fields = ["age_range", "gender", "location", "occupation_category"]
try:
with get_session() as session:
if persona_id:
# 특정 페르소나 확인
persona = session.query(Persona).filter_by(id=persona_id).first()
if not persona:
return {
"is_complete": False,
"missing_fields": required_fields,
"persona_id": persona_id,
"message": f"페르소나 '{persona_id}'가 존재하지 않습니다. 먼저 기본 프로필을 설정해주세요.",
"exists": False
}
missing = [field for field in required_fields if not getattr(persona, field, None)]
return {
"is_complete": len(missing) == 0,
"missing_fields": missing,
"persona_id": persona_id,
"message": f"기본 프로필이 {'완전합니다' if len(missing) == 0 else '불완전합니다'}. 누락된 필드: {', '.join(missing) if missing else '없음'}",
"exists": True
}
else:
# 모든 페르소나 확인 (최소 하나라도 완전한 프로필이 있는지)
personas = session.query(Persona).all()
if not personas:
return {
"is_complete": False,
"missing_fields": required_fields,
"persona_id": None,
"message": "등록된 페르소나가 없습니다. 먼저 기본 프로필을 설정해주세요.",
"persona_count": 0
}
complete_personas = []
for persona in personas:
missing = [field for field in required_fields if not getattr(persona, field, None)]
if len(missing) == 0:
complete_personas.append(persona.id)
if not complete_personas:
return {
"is_complete": False,
"missing_fields": required_fields,
"persona_id": None,
"message": f"등록된 페르소나 {len(personas)}개 중 완전한 기본 프로필을 가진 페르소나가 없습니다. 먼저 기본 프로필을 설정해주세요.",
"persona_count": len(personas),
"complete_count": 0
}
return {
"is_complete": True,
"missing_fields": [],
"persona_id": None,
"message": f"완전한 기본 프로필을 가진 페르소나 {len(complete_personas)}개가 있습니다.",
"persona_count": len(personas),
"complete_count": len(complete_personas),
"complete_persona_ids": complete_personas
}
except Exception as e:
return {
"is_complete": False,
"missing_fields": required_fields,
"persona_id": persona_id,
"message": f"기본 프로필 확인 중 오류: {str(e)}",
"error": str(e)
}
def update_basic_profile(persona_id: str, profile_data: dict):
"""
1단계: 기본 프로필 업데이트
:param persona_id: 페르소나 ID
:param profile_data: {
"age_range": "20대 후반",
"gender": "남성",
"location": "서울",
"occupation_category": "IT/개발",
"income_range": "300-500만원" (선택)
}
:return: success, persona_id, profile, freshness
"""
try:
with get_session() as session:
# 페르소나 조회 또는 생성
persona = session.query(Persona).filter_by(id=persona_id).first()
old_freshness = 0.0
if not persona:
persona = Persona(
id=persona_id,
freshness_score=0.0,
created_at=datetime.now(),
last_updated=datetime.now()
)
session.add(persona)
else:
old_freshness = persona.freshness_score
# 기본 프로필 업데이트
persona.age_range = profile_data.get("age_range")
persona.gender = profile_data.get("gender")
persona.location = profile_data.get("location")
persona.occupation_category = profile_data.get("occupation_category")
persona.income_range = profile_data.get("income_range")
# 기본 프로필 점수 계산 (최대 25점)
profile_score = 0
if persona.age_range: profile_score += 5
if persona.gender: profile_score += 5
if persona.location: profile_score += 5
if persona.occupation_category: profile_score += 5
if persona.income_range: profile_score += 5
# 신선도 업데이트 (무제한 누적)
persona.freshness_score = old_freshness + profile_score
persona.last_updated = datetime.now()
session.commit()
return {
"success": True,
"persona_id": persona_id,
"profile": {
"age_range": persona.age_range,
"gender": persona.gender,
"location": persona.location,
"occupation_category": persona.occupation_category,
"income_range": persona.income_range
},
"old_freshness": old_freshness,
"new_freshness": persona.freshness_score,
"points_earned": profile_score,
"message": f"기본 프로필 저장! 데이터 가치 +{profile_score}점 (현재: {persona.freshness_score:.0f}점)"
}
except Exception as e:
return {"error": f"프로필 업데이트 중 오류: {str(e)}", "success": False}
def create_fgi_survey(category: str, persona_id: str = None):
"""
2단계: 카테고리별 오프닝 질문으로 설문 시작
:param category: 설문 카테고리 (technology, lifestyle, culture, fashion, food, general)
:param persona_id: 페르소나 ID (선택사항)
:return: survey_id, opening_question, created_at
"""
try:
survey_id = f"survey_{uuid.uuid4().hex[:12]}"
# 오프닝 질문
opening_question = {
"id": "q1",
"type": "open",
"text": OPENING_QUESTIONS.get(category, OPENING_QUESTIONS["general"])
}
# DB에 설문 저장
with get_session() as session:
survey = FGISurvey(
id=survey_id,
category=category,
questions=[opening_question],
news_source={"type": "adaptive", "category": category},
created_at=datetime.now()
)
session.add(survey)
return {
"survey_id": survey_id,
"category": category,
"question": opening_question,
"created_at": datetime.now().isoformat(),
"message": "설문이 시작되었습니다! 답변을 주시면 다음 질문을 생성합니다."
}
except Exception as e:
return {"error": f"설문 생성 중 오류 발생: {str(e)}"}
def generate_next_question(survey_id: str, previous_qa: list, category: str = "general", max_questions: int = None):
"""
3단계: 이전 답변을 분석해서 다음 질문을 동적으로 생성
**최소한의 질문으로 최대한의 정보를 수집**하는 것이 목표입니다.
:param survey_id: 설문 ID
:param previous_qa: 이전 질문/답변 쌍 [{"question": "...", "answer": "..."}]
:param category: 카테고리
:param max_questions: 최대 질문 수 (선택사항, None이면 무제한)
:return: next_question, question_number, is_final, info_completeness
"""
try:
# 캐시 확인 (질문 생성은 유사한 컨텍스트에서 재사용 가능)
cache = get_cache()
cache_key = cache._generate_cache_key(
"question",
survey_id=survey_id,
qa_count=len(previous_qa),
category=category,
max_questions=max_questions,
# previous_qa의 마지막 2개만 포함 (최근 컨텍스트)
recent_qa=previous_qa[-2:] if len(previous_qa) >= 2 else previous_qa
)
cached_result = cache.get(cache_key, ttl=timedelta(hours=12))
if cached_result is not None:
return cached_result
groq_api_key = os.getenv("GROQ_API_KEY")
if not groq_api_key:
return {"error": "GROQ_API_KEY가 설정되지 않았습니다"}
client = Groq(api_key=groq_api_key)
# 대화 컨텍스트 구성
qa_context = "\n\n".join([
f"Q{i+1}: {qa['question']}\nA{i+1}: {qa['answer']}"
for i, qa in enumerate(previous_qa)
])
question_num = len(previous_qa) + 1
# 최대 질문 수 체크
if max_questions and question_num > max_questions:
return {
"success": True,
"question": None,
"question_number": question_num - 1,
"is_final": True,
"info_completeness": "최대 질문 수 도달",
"message": f"최대 {max_questions}개 질문을 완료했습니다. 충분한 정보가 수집되었습니다."
}
# 이전 질문들의 주제 분석 (중복 방지용)
previous_topics = []
for qa in previous_qa:
# 먼저 저장된 target_area 확인 (더 정확)
if 'target_area' in qa and qa['target_area']:
previous_topics.append(qa['target_area'])
else:
# 없으면 키워드 기반 추출 (하위 호환성)
q_text = qa.get('question', '').lower()
if any(word in q_text for word in ['기술', 'ai', '개발', '코딩', '프로그래밍', '소프트웨어', '직업']):
previous_topics.append('기술')
elif any(word in q_text for word in ['여가', '취미', '시간', '활동', '운동', '여행', '일상']):
previous_topics.append('라이프스타일')
elif any(word in q_text for word in ['패션', '옷', '스타일', '브랜드']):
previous_topics.append('패션')
elif any(word in q_text for word in ['음식', '식사', '맛집', '요리']):
previous_topics.append('음식')
elif any(word in q_text for word in ['영화', '드라마', '책', '문화', '예술']):
previous_topics.append('문화')
elif any(word in q_text for word in ['소비', '구매', '가격', '비용', '투자']):
previous_topics.append('소비패턴')
elif any(word in q_text for word in ['가치', '중요', '생각', '믿음']):
previous_topics.append('가치관')
elif any(word in q_text for word in ['성격', '성향', 'mbti', '에너지', '결정', '스트레스', '계획', '즉흥', '논리', '감정']):
previous_topics.append('성향')
elif any(word in q_text for word in ['관계', '친구', '사람', '소통', '사회']):
previous_topics.append('관계')
elif any(word in q_text for word in ['미래', '목표', '계획', '꿈']):
previous_topics.append('미래')
# 주제별 빈도 계산 (같은 주제를 몇 번 다뤘는지)
from collections import Counter
topic_counts = Counter(previous_topics)
topics_summary = ", ".join([f"{topic}({count}회)" for topic, count in topic_counts.most_common()]) if previous_topics else "없음"
# 가장 많이 다룬 주제 (피해야 할 주제)
most_covered_topic = topic_counts.most_common(1)[0][0] if topic_counts else None
# 정보 수집 완료도 평가 (LLM으로)
info_completeness_prompt = f"""다음 질문/답변을 분석하여 페르소나 파악에 필요한 정보가 얼마나 수집되었는지 평가해주세요.
질문/답변:
{qa_context}
평가 기준:
- 기본 정보 (나이, 직업, 관심사): 0-25%
- 라이프스타일/일상 패턴: 0-25%
- 가치관/성향/성격: 0-25%
- 소비 패턴/구매 행동: 0-25%
JSON 형식:
{{
"completeness": 0-100,
"covered_areas": ["이미 파악한 영역들"],
"missing_areas": ["아직 파악하지 못한 영역들"],
"enough_info": true/false,
"reason": "평가 이유"
}}"""
try:
completeness_response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": info_completeness_prompt}],
response_format={"type": "json_object"},
max_tokens=300,
temperature=0.3
)
completeness_data = json.loads(completeness_response.choices[0].message.content)
info_completeness = completeness_data.get("completeness", 0)
enough_info = completeness_data.get("enough_info", False)
missing_areas = completeness_data.get("missing_areas", [])
except:
info_completeness = (question_num - 1) * 15 # 간단한 추정
enough_info = False
missing_areas = []
# 충분한 정보가 모였고, 최대 질문 수에 도달했으면 종료
if enough_info and (not max_questions or question_num >= max_questions):
return {
"success": True,
"question": None,
"question_number": question_num - 1,
"is_final": True,
"info_completeness": f"{info_completeness}%",
"message": f"충분한 정보가 수집되었습니다 ({info_completeness}% 완료). 페르소나 예측이 가능합니다."
}
prompt = f"""당신은 전문 시장조사 인터뷰어입니다. **최소한의 질문으로 사용자의 전체적인 페르소나를 빠르게 파악**하는 것이 목표입니다.
카테고리: {category}
진행 상황: {question_num-1}개 질문 완료
정보 수집 완료도: {info_completeness}%
{f"최대 질문 수: {max_questions}개 (남은 질문: {max_questions - question_num + 1}개)" if max_questions else ""}
{f"아직 파악하지 못한 영역: {', '.join(missing_areas) if missing_areas else '없음'}" if missing_areas else ""}
**지금까지의 대화:**
{qa_context}
**핵심 원칙: 효율적인 정보 수집**
- **최소한의 질문으로 최대한의 정보 수집**이 목표입니다
- 이미 충분히 파악한 주제는 피하세요 (이미 다룬 주제: {topics_summary})
{f"- ⚠️ 특히 '{most_covered_topic}' 주제는 이미 충분히 다뤘으니 절대 반복하지 마세요!" if most_covered_topic and topic_counts[most_covered_topic] >= 2 else ""}
- 같은 주제를 계속 파고들지 마세요 (예: 기술 질문만 반복 ❌)
- **전체적인 페르소나 그림**을 그리기 위해 다양한 영역을 탐색하세요
- **정보 밀도 높은 질문**: 하나의 질문으로 여러 측면을 파악할 수 있는 질문을 만드세요
- 예: "평소 여가 시간을 어떻게 보내시나요? 그런 활동을 선택하는 기준은 무엇인가요?" (라이프스타일 + 가치관)
- 예: "중요한 결정을 내릴 때 어떤 과정을 거치시나요? 논리적으로 분석하시나요, 아니면 감정과 가치를 중시하시나요?" (의사결정 + 성향)
**페르소나 파악을 위한 필수 영역:**
1. **기술/직업**: 기술 스택, 업무 방식, 전문성
2. **라이프스타일**: 일상 패턴, 여가 활동, 생활 습관
3. **가치관/성격**: 중요하게 생각하는 것, 결정 기준, 성격 특성
4. **소비 패턴**: 구매 행동, 가격 민감도, 브랜드 선호도
5. **관계/사회**: 인간관계, 소통 방식, 사회적 활동
6. **문화/취미**: 문화 소비, 취미 활동, 관심사
7. **미래/목표**: 계획, 목표, 우려사항
8. **성향/성격 유형**: MBTI 스타일 성향 파악 (외향/내향, 감각/직관, 사고/감정, 판단/인식)
**질문 전략 (효율성 우선):**
1. **정보 밀도 높은 질문**: 하나의 질문으로 여러 측면을 동시에 파악
- 예: "평소 여가 시간을 어떻게 보내시나요? 그런 활동을 선택하는 기준은 무엇인가요?"
→ 라이프스타일 + 가치관 + 소비 패턴을 한 번에
- 예: "중요한 결정을 내릴 때 어떤 과정을 거치시나요? 논리적으로 분석하시나요, 아니면 감정과 가치를 중시하시나요?"
→ 의사결정 스타일 + 성향(사고/감정) + 가치관을 한 번에
- 예: "스트레스를 받을 때 어떻게 대처하시나요? 혼자 해결하시나요, 아니면 사람들과 상담하시나요?"
→ 스트레스 대처 + 성향(외향/내향) + 관계 패턴을 한 번에
2. **아직 파악하지 못한 영역 우선**: {', '.join(missing_areas) if missing_areas else '모든 영역'}에 집중
- 빠르게 다양한 영역을 커버하세요
- 이미 충분히 파악한 영역은 건너뛰세요
3. **연결고리 만들기**: 이전 답변과 새 영역을 자연스럽게 연결
- 예: "기술에 관심 많으시는 것 같은데, 일상에서도 기술을 어떻게 활용하시나요?"
- 예: "개발 일 하시면서, 퇴근 후에는 어떻게 스트레스를 푸시나요?"
4. **깊이 있는 탐색**: 새 영역으로 전환한 후에는 그 영역을 깊게 파악
- "왜?" (동기, 이유)
- "구체적인 경험" (최근에 ~한 적?)
- "선택 기준" (A vs B, 무엇이 중요한가?)
- "성향 파악" (MBTI 스타일: 외향/내향, 계획/즉흥, 논리/감정 등)
**다음 질문 생성 규칙 (절대 규칙):**
- 이전 질문들이 {topics_summary}에 집중했다면, **반드시 다른 영역**으로 전환
- 같은 주제를 2번 이상 다뤘다면, **무조건 다른 주제**로 이동
- 전체적인 페르소나를 그리기 위해 **균형잡힌 질문** 생성
- **절대로** 이전 질문과 같은 주제를 반복하지 마세요
- 기술 질문만 하지 말고, 라이프스타일, 가치관, 소비패턴 등 **다양한 영역**을 탐색하세요
**성향/성격 파악 질문 예시 (MBTI 스타일):**
- 외향/내향: "에너지를 얻는 방식은? (사람들과 어울리기 vs 혼자 시간)"
- 감각/직관: "정보 처리 방식은? (구체적 사실 vs 가능성과 패턴)"
- 사고/감정: "결정할 때 무엇을 중시하나요? (논리와 객관성 vs 가치와 조화)"
- 판단/인식: "일상 스타일은? (계획적이고 체계적 vs 유연하고 즉흥적)"
- 스트레스 대처: "스트레스를 받을 때 어떻게 하나요?"
- 의사결정: "중요한 결정을 내릴 때 어떤 과정을 거치나요?"
**다음 질문 1개를 생성해주세요:**
JSON 형식:
{{
"question": "질문 내용 (자연스럽고 대화하듯이, 이전 주제와 다른 영역으로 전환)",
"purpose": "이 질문으로 파악하려는 것 (어떤 영역의 어떤 측면?)",
"target_area": "목표 영역 (기술/라이프스타일/가치관/소비패턴/관계/문화/미래/성향 중 하나)",
"type": "질문 타입 (choice/open/scenario)"
}}
"""
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
max_tokens=400,
temperature=0.8
)
result = json.loads(response.choices[0].message.content)
next_question = {
"id": f"q{question_num}",
"type": result.get("type", "open"),
"text": result["question"],
"purpose": result.get("purpose", ""),
"target_area": result.get("target_area", "general") # 질문이 다루는 영역 저장
}
# DB 업데이트: 설문에 질문 추가
with get_session() as session:
survey = session.query(FGISurvey).filter_by(id=survey_id).first()
if survey:
questions = survey.questions or []
questions.append(next_question)
survey.questions = questions
session.commit()
result = {
"success": True,
"question": next_question,
"question_number": question_num,
"is_final": False,
"info_completeness": f"{info_completeness}%",
"message": f"{question_num}번째 질문입니다. (정보 수집: {info_completeness}% 완료)"
}
# 캐시 저장 (성공한 경우만)
if result.get("success") and not result.get("error"):
cache.set(cache_key, result, ttl=timedelta(hours=12))
return result
except Exception as e:
return {"error": f"다음 질문 생성 중 오류: {str(e)}", "success": False}
def evaluate_response_quality(question: str, answer: str) -> dict:
"""
응답 품질을 LLM으로 평가
:param question: 질문 내용
:param answer: 사용자 답변
:return: {"score": 0-15, "evaluation": {...}}
"""
try:
# 캐시 확인 (유사한 질문/답변은 재사용)
cache = get_cache()
cache_key = cache._generate_cache_key(
"quality",
question_hash=hash(question[:100]), # 질문의 해시
answer_length=len(answer),
answer_hash=hash(answer[:200]) if len(answer) > 50 else hash(answer) # 답변의 해시
)
cached_result = cache.get(cache_key, ttl=timedelta(hours=24))
if cached_result is not None:
return cached_result
groq_api_key = os.getenv("GROQ_API_KEY")
if not groq_api_key:
return {"score": 8, "evaluation": {"error": "API key not found"}}
client = Groq(api_key=groq_api_key)
prompt = f"""당신은 데이터 품질 평가 전문가입니다.
사용자의 설문 응답이 **페르소나 파악에 얼마나 유용한지** 평가해주세요.
질문: {question}
답변: {answer}
평가 기준:
1. **구체성** (0-5점): 구체적이고 상세한 정보를 포함하는가?
- 5점: 매우 구체적 (예: 숫자, 브랜드명, 구체적 경험)
- 3점: 보통
- 1점: 모호하거나 짧음
2. **정보량** (0-5점): 페르소나 파악에 유용한 정보가 많은가?
- 5점: 다양한 인사이트 (직업, 관심사, 소비 패턴 등 복합)
- 3점: 기본 정보
- 1점: 정보 부족
3. **진정성** (0-3점): 진솔하고 개인적 경험이 담겨있는가?
- 3점: 개인적 경험과 감정이 담김
- 2점: 일반적 답변
- 1점: 피상적
4. **활용도** (0-2점): 마케팅/타겟팅에 활용 가능한가?
- 2점: 즉시 활용 가능
- 1점: 부분 활용
- 0점: 활용 어려움
JSON 형식으로 반환:
{{
"specificity": 숫자,
"information": 숫자,
"authenticity": 숫자,
"usability": 숫자,
"total": 총점,
"reason": "평가 이유 (한 줄)"
}}
"""
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
max_tokens=300,
temperature=0.3
)
result = json.loads(response.choices[0].message.content)
evaluation_result = {
"score": result.get("total", 8),
"evaluation": result
}
# 캐시 저장
cache.set(cache_key, evaluation_result, ttl=timedelta(hours=24))
return evaluation_result
except Exception as e:
return {"score": 8, "evaluation": {"error": str(e)}}
def submit_fgi_response(persona_id: str, survey_id: str, responses: list):
"""
사용자 응답 저장 + 신선도 점수 업데이트 (응답 품질 기반)
:param persona_id: 페르소나 ID
:param survey_id: 설문 ID
:param responses: 응답 리스트 [{"question_id": "q1", "answer": "답변"}]
:return: success, old_freshness, new_freshness, points_earned, quality_details
"""
try:
with get_session() as session:
# 1. 페르소나 조회 또는 생성
persona = session.query(Persona).filter_by(id=persona_id).first()
if not persona:
persona = Persona(
id=persona_id,
freshness_score=0.0,
created_at=datetime.now(),
last_updated=datetime.now()
)
session.add(persona)
session.flush()
old_freshness = 0.0
else:
old_freshness = persona.freshness_score
# 2. 설문 조회 (질문 정보 필요)
survey = session.query(FGISurvey).filter_by(id=survey_id).first()
questions_dict = {}
if survey and survey.questions:
questions_dict = {q["id"]: q["text"] for q in survey.questions}
# 3. 각 응답의 품질 평가
total_points = 0
quality_details = []
for resp in responses:
question_id = resp.get("question_id", "")
answer = resp.get("answer", "")
question_text = questions_dict.get(question_id, "질문 내용 없음")
quality = evaluate_response_quality(question_text, answer)
score = quality["score"]
total_points += score
quality_details.append({
"question_id": question_id,
"score": score,
"evaluation": quality.get("evaluation", {})
})
# 4. 응답 저장
response_id = f"resp_{uuid.uuid4().hex[:12]}"
fgi_response = FGIResponse(
id=response_id,
persona_id=persona_id,
survey_id=survey_id,
responses=responses,
submitted_at=datetime.now()
)
session.add(fgi_response)
# 5. 신선도 점수 업데이트
new_freshness = persona.freshness_score + total_points
persona.freshness_score = new_freshness
persona.last_updated = datetime.now()
session.commit()
# 신선도 등급 계산
if new_freshness >= 500:
grade = "SS급 (완벽한 데이터)"
elif new_freshness >= 300:
grade = "S급 (최고 가치 데이터)"
elif new_freshness >= 200:
grade = "A급 (고가치 데이터)"
elif new_freshness >= 100:
grade = "B급 (중간 가치 데이터)"
elif new_freshness >= 50:
grade = "C급 (기본 데이터)"
else:
grade = "D급 (불완전 데이터)"
return {
"success": True,
"response_id": response_id,
"old_freshness": old_freshness,
"new_freshness": new_freshness,
"points_earned": total_points,
"grade": grade,
"quality_details": quality_details,
"message": f"데이터 가치가 {old_freshness:.1f}에서 {new_freshness:.1f}로 상승! ({grade})"
}
except Exception as e:
return {"error": f"응답 제출 중 오류 발생: {str(e)}", "success": False}