Ver3.5 Python으로 AI 글쓰기 품질 평가 시스템 구현

Python으로 AI 글쓰기 품질 평가 시스템 구현
✍️ 내 글이 진짜 잘 쓴 건지 AI한테 물어보자ㅋㅋ — 실전 코드와 함께하는 완전 정복 가이드
야 솔직히 말해봐ㅋㅋ 글 쓰고 나서 "이거 괜찮은 건가...?" 하고 혼자 멍하니 화면 바라본 적 있지 않아? 🤔
맞춤법 검사기는 있는데, "이 글이 얼마나 잘 쓰인 글인가"를 객관적으로 평가해주는 시스템은 생각보다 없거든.
그래서 오늘은 Python이랑 AI 기술을 활용해서 글쓰기 품질 평가 시스템을 직접 만들어보는 거 해볼 거야!
이거 단순히 맞춤법 체크 수준이 아니야. 가독성, 문장 구조, 감성 분석, 키워드 밀도, 논리 흐름까지 — 진짜 종합 평가 시스템이야.
재능넷 같은 플랫폼에서 글쓰기 재능을 판매하거나 콘텐츠 제작자로 활동하는 분들한테 특히 유용할 거야 😎
텍스트 전처리 → 가독성 지수 계산 → 감성 분석 → 키워드 분석 → GPT API 연동 → 종합 점수 산출까지!
Python 중급 이상이면 충분히 따라올 수 있어.
🛠️ 1. 개발 환경 세팅 — 일단 준비부터 빡세게
뭐든 환경 세팅이 제일 귀찮은 거 알지ㅋㅋ 근데 이거 제대로 안 하면 나중에 더 고생해. 차근차근 따라와봐.
먼저 가상환경 만들고 필요한 패키지들 설치해줘야 해. 아래 명령어 그대로 복붙해도 돼.
# 가상환경 생성 및 활성화
python -m venv writing_eval_env
source writing_eval_env/bin/activate # Windows: writing_eval_env\Scripts\activate
# 핵심 라이브러리 설치
pip install konlpy
pip install scikit-learn
pip install transformers torch
pip install openai
pip install fastapi uvicorn
pip install pandas numpy
pip install matplotlib seaborn
pip install python-dotenv
pip install textstat
pip install kiwipiepy # KoNLPy 대안, 더 빠름
KoNLPy는 Java 의존성이 있어서 JDK 1.7 이상이 설치되어 있어야 해.
java -version 으로 확인해봐. 없으면 OpenJDK 설치 먼저!Windows 환경에서는 kiwipiepy가 훨씬 설치 편하니까 대안으로 써도 돼.
OpenAI API 키는 절대 코드에 하드코딩하면 안 돼! 진짜로ㅋㅋ 깃허브에 올렸다가 키 털리는 사람 엄청 많아.
# .env 파일 생성
OPENAI_API_KEY=sk-your-api-key-here
MODEL_NAME=gpt-4o-mini
MAX_TOKENS=2000
TEMPERATURE=0.3
# config.py
from dotenv import load_dotenv
import os
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
MAX_TOKENS = int(os.getenv("MAX_TOKENS", 2000))
TEMPERATURE = float(os.getenv("TEMPERATURE", 0.3))
🔧 2. 텍스트 전처리 모듈 — 데이터 정제의 기술
AI한테 날 것의 텍스트 그대로 던져주면 결과가 엉망이 돼. 전처리가 품질 평가의 80%를 좌우한다고 해도 과언이 아님ㅋㅋ
# text_preprocessor.py
import re
import unicodedata
from kiwipiepy import Kiwi
from typing import List, Dict, Tuple
class TextPreprocessor:
"""한국어 텍스트 전처리 클래스"""
def __init__(self):
self.kiwi = Kiwi()
def clean_text(self, text: str) -> str:
"""기본 텍스트 정제"""
# 유니코드 정규화
text = unicodedata.normalize('NFC', text)
# HTML 태그 제거
text = re.sub(r'<[^>]+>', '', text)
# URL 제거
text = re.sub(r'http[s]?://\S+', '[URL]', text)
# 이메일 제거
text = re.sub(r'\S+@\S+\.\S+', '[EMAIL]', text)
# 과도한 공백 정리
text = re.sub(r'\s+', ' ', text).strip()
return text
def split_sentences(self, text: str) -> List[str]:
"""문장 분리"""
result = self.kiwi.split_into_sents(text)
sentences = [sent.text.strip() for sent in result
if len(sent.text.strip()) > 0]
return sentences
def tokenize(self, text: str) -> List[Dict]:
"""형태소 분석 및 토큰화"""
result = self.kiwi.analyze(text)
tokens = []
if result:
for token in result[0].tokens:
tokens.append({
'form': token.form,
'tag': token.tag,
'start': token.start,
'len': token.len
})
return tokens
def extract_morphemes(self, text: str) -> Dict[str, List[str]]:
"""품사별 형태소 추출"""
tokens = self.tokenize(text)
morphemes = {
'nouns': [], # 명사
'verbs': [], # 동사
'adjectives': [], # 형용사
'adverbs': [], # 부사
'all': [] # 전체
}
pos_map = {
'NNG': 'nouns', 'NNP': 'nouns', 'NNB': 'nouns',
'VV': 'verbs', 'VA': 'adjectives', 'MAG': 'adverbs'
}
for token in tokens:
morphemes['all'].append(token['form'])
category = pos_map.get(token['tag'])
if category:
morphemes[category].append(token['form'])
return morphemes
def get_basic_stats(self, text: str) -> Dict:
"""기본 통계 추출"""
sentences = self.split_sentences(text)
morphemes = self.extract_morphemes(text)
# 문장 길이 계산
sent_lengths = [len(s) for s in sentences]
return {
'total_chars': len(text),
'total_sentences': len(sentences),
'total_words': len(morphemes['all']),
'avg_sentence_length': sum(sent_lengths) / len(sent_lengths) if sent_lengths else 0,
'max_sentence_length': max(sent_lengths) if sent_lengths else 0,
'min_sentence_length': min(sent_lengths) if sent_lengths else 0,
'noun_count': len(morphemes['nouns']),
'verb_count': len(morphemes['verbs']),
'adj_count': len(morphemes['adjectives']),
}
kiwipiepy: 설치 쉬움, 속도 빠름, 최신 한국어 처리 우수, 추천!
KoNLPy (Okt, Komoran): 레퍼런스 많음, Java 필요, 대용량 처리 시 안정적
개인 프로젝트라면 kiwipiepy, 팀 프로젝트라면 KoNLPy 고려해봐.
📊 3. 가독성 분석 모듈 — 얼마나 읽기 쉬운 글인가
가독성은 글쓰기 품질에서 진짜 핵심이야. 아무리 내용이 좋아도 읽기 어려우면 독자가 이탈하거든ㅋㅋ
영어권에서는 Flesch-Kincaid 지수가 유명한데, 한국어는 음절 구조가 달라서 변형이 필요해.
# readability_analyzer.py
import math
from typing import Dict, List
from text_preprocessor import TextPreprocessor
class ReadabilityAnalyzer:
"""한국어 가독성 분석기"""
def __init__(self):
self.preprocessor = TextPreprocessor()
def count_syllables_korean(self, text: str) -> int:
"""한국어 음절 수 계산"""
# 한글 음절 범위: 0xAC00 ~ 0xD7A3
syllable_count = sum(1 for char in text
if '\uAC00' <= char <= '\uD7A3')
return syllable_count
def calculate_ari(self, text: str) -> float:
"""
한국어 변형 ARI (Automated Readability Index)
공식: 4.71 * (음절수/단어수) + 0.5 * (단어수/문장수) - 21.43
"""
stats = self.preprocessor.get_basic_stats(text)
syllables = self.count_syllables_korean(text)
if stats['total_words'] == 0 or stats['total_sentences'] == 0:
return 0
chars_per_word = syllables / stats['total_words']
words_per_sent = stats['total_words'] / stats['total_sentences']
ari = 4.71 * chars_per_word + 0.5 * words_per_sent - 21.43
return round(ari, 2)
def calculate_ttr(self, text: str) -> float:
"""
Type-Token Ratio (어휘 다양성 지수)
TTR = 고유 단어 수 / 전체 단어 수
1에 가까울수록 어휘가 다양함
"""
morphemes = self.preprocessor.extract_morphemes(text)
all_words = morphemes['all']
if not all_words:
return 0
unique_words = set(all_words)
ttr = len(unique_words) / len(all_words)
return round(ttr, 4)
def calculate_mtld(self, text: str, threshold: float = 0.72) -> float:
"""
MTLD (Measure of Textual Lexical Diversity)
TTR보다 텍스트 길이에 덜 민감한 어휘 다양성 지표
"""
morphemes = self.preprocessor.extract_morphemes(text)
words = morphemes['all']
if len(words) < 10:
return 0
def _mtld_forward(word_list):
factors = 0
start = 0
for end in range(1, len(word_list) + 1):
segment = word_list[start:end]
unique = len(set(segment))
ttr = unique / len(segment)
if ttr <= threshold:
factors += 1
start = end
# 마지막 불완전 세그먼트 처리
if start < len(word_list):
segment = word_list[start:]
unique = len(set(segment))
ttr = unique / len(segment)
factors += (1 - ttr) / (1 - threshold)
return len(word_list) / factors if factors > 0 else 0
forward = _mtld_forward(words)
backward = _mtld_forward(words[::-1])
return round((forward + backward) / 2, 2)
def analyze_sentence_variety(self, text: str) -> Dict:
"""문장 길이 다양성 분석"""
sentences = self.preprocessor.split_sentences(text)
lengths = [len(s) for s in sentences]
if not lengths:
return {}
avg = sum(lengths) / len(lengths)
variance = sum((l - avg) ** 2 for l in lengths) / len(lengths)
std_dev = math.sqrt(variance)
# 문장 길이 분포
short = sum(1 for l in lengths if l < 20)
medium = sum(1 for l in lengths if 20 <= l < 60)
long = sum(1 for l in lengths if l >= 60)
return {
'avg_length': round(avg, 1),
'std_deviation': round(std_dev, 2),
'variety_score': round(min(std_dev / avg * 100, 100), 1) if avg > 0 else 0,
'short_sentences': short,
'medium_sentences': medium,
'long_sentences': long,
'total_sentences': len(sentences)
}
def detect_repetition(self, text: str) -> Dict:
"""반복 표현 탐지"""
sentences = self.preprocessor.split_sentences(text)
morphemes = self.preprocessor.extract_morphemes(text)
# 단어 빈도 계산
from collections import Counter
word_freq = Counter(morphemes['nouns'] + morphemes['verbs'])
# 과도하게 반복되는 단어 (전체의 3% 이상)
total_words = len(morphemes['all'])
repeated = {
word: count for word, count in word_freq.items()
if count / total_words > 0.03 and len(word) > 1
}
return {
'repeated_words': repeated,
'repetition_score': max(0, 100 - len(repeated) * 10)
}
def get_readability_report(self, text: str) -> Dict:
"""종합 가독성 리포트"""
return {
'ari_score': self.calculate_ari(text),
'ttr': self.calculate_ttr(text),
'mtld': self.calculate_mtld(text),
'sentence_variety': self.analyze_sentence_variety(text),
'repetition': self.detect_repetition(text)
}
| 지표 | 설명 | 이상적인 범위 |
|---|---|---|
| ARI 점수 | 자동 가독성 지수 (낮을수록 쉬움) | 6~10 (일반 독자 기준) |
| TTR | 어휘 다양성 (1에 가까울수록 다양) | 0.4~0.7 |
| MTLD | 텍스트 어휘 다양성 (높을수록 좋음) | 70 이상 |
| 문장 다양성 | 문장 길이의 표준편차 | 표준편차 10~25 |
💬 4. 감성 분석 모듈 — 글의 톤앤매너 파악하기
글의 감성 톤은 독자 경험에 엄청난 영향을 줘. 너무 부정적이거나 너무 중립적이면 독자가 지루해하거든.
한국어 감성 분석은 Hugging Face의 사전학습 모델을 활용하면 꽤 정확하게 할 수 있어! 🎯
# sentiment_analyzer.py
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
import torch
from typing import Dict, List
from text_preprocessor import TextPreprocessor
class SentimentAnalyzer:
"""한국어 감성 분석기"""
def __init__(self, model_name: str = "snunlp/KR-FinBert-SC"):
"""
추천 한국어 감성 분석 모델:
- snunlp/KR-FinBert-SC (금융 도메인)
- monologg/koelectra-base-finetuned-sentiment
- klue/roberta-base (범용)
"""
self.preprocessor = TextPreprocessor()
try:
# 경량 모델 우선 시도
self.classifier = pipeline(
"text-classification",
model="monologg/koelectra-base-finetuned-sentiment",
device=0 if torch.cuda.is_available() else -1,
max_length=512,
truncation=True
)
self.model_loaded = True
except Exception as e:
print(f"모델 로드 실패: {e}")
print("규칙 기반 감성 분석으로 대체합니다.")
self.model_loaded = False
self._load_sentiment_dict()
def _load_sentiment_dict(self):
"""규칙 기반 감성 사전 (모델 로드 실패 시 대체)"""
self.positive_words = [
'좋다', '훌륭하다', '멋지다', '아름답다', '즐겁다',
'행복하다', '기쁘다', '뛰어나다', '완벽하다', '최고',
'유익하다', '도움', '감사', '사랑', '희망'
]
self.negative_words = [
'나쁘다', '싫다', '힘들다', '어렵다', '실망',
'문제', '오류', '실패', '부족', '불만',
'걱정', '두렵다', '슬프다', '화나다', '짜증'
]
def analyze_sentence_sentiment(self, sentence: str) -> Dict:
"""단일 문장 감성 분석"""
if self.model_loaded:
try:
result = self.classifier(sentence[:512])
label = result[0]['label']
score = result[0]['score']
# 레이블 정규화
if 'positive' in label.lower() or label == '1':
sentiment = 'positive'
elif 'negative' in label.lower() or label == '0':
sentiment = 'negative'
else:
sentiment = 'neutral'
return {'sentiment': sentiment, 'confidence': round(score, 4)}
except:
pass
# 규칙 기반 대체
return self._rule_based_sentiment(sentence)
def _rule_based_sentiment(self, text: str) -> Dict:
"""규칙 기반 감성 분석"""
pos_count = sum(1 for w in self.positive_words if w in text)
neg_count = sum(1 for w in self.negative_words if w in text)
if pos_count > neg_count:
return {'sentiment': 'positive', 'confidence': 0.6}
elif neg_count > pos_count:
return {'sentiment': 'negative', 'confidence': 0.6}
else:
return {'sentiment': 'neutral', 'confidence': 0.5}
def analyze_full_text(self, text: str) -> Dict:
"""전체 텍스트 감성 분석"""
sentences = self.preprocessor.split_sentences(text)
if not sentences:
return {}
results = []
for sent in sentences:
if len(sent.strip()) > 5: # 너무 짧은 문장 제외
result = self.analyze_sentence_sentiment(sent)
results.append(result)
# 감성 분포 계산
sentiment_counts = {'positive': 0, 'negative': 0, 'neutral': 0}
for r in results:
sentiment_counts[r['sentiment']] += 1
total = len(results)
sentiment_ratio = {
k: round(v / total * 100, 1)
for k, v in sentiment_counts.items()
} if total > 0 else {}
# 전반적 감성 결정
dominant = max(sentiment_counts, key=sentiment_counts.get)
# 감성 균형 점수 (너무 한쪽으로 치우치지 않을수록 좋음)
balance_score = self._calculate_balance_score(sentiment_ratio)
return {
'dominant_sentiment': dominant,
'sentiment_distribution': sentiment_ratio,
'balance_score': balance_score,
'analyzed_sentences': total,
'sentiment_details': results[:5] # 처음 5개만 샘플로
}
def _calculate_balance_score(self, ratio: Dict) -> float:
"""
감성 균형 점수 계산
- 정보성 글: 중립 60%+ 이상이 이상적
- 감성적 글: 긍정 50%+ 이상이 이상적
"""
positive = ratio.get('positive', 0)
negative = ratio.get('negative', 0)
neutral = ratio.get('neutral', 0)
# 부정 비율이 40% 이상이면 감점
if negative > 40:
return max(0, 100 - (negative - 40) * 2)
# 중립+긍정 비율이 높을수록 좋음
score = (neutral * 0.5 + positive * 0.8)
return round(min(score, 100), 1)
GPU가 없는 환경이라면
monologg/koelectra-base-finetuned-sentiment 모델이 CPU에서도 비교적 빠르게 돌아가.배치 처리가 필요하다면
pipeline의 batch_size 파라미터를 활용해봐!
🔍 5. 키워드 분석 & TF-IDF — 핵심 단어 뽑아내기
글에서 어떤 단어가 핵심인지 파악하는 건 SEO 최적화나 주제 일관성 평가에 필수야.
TF-IDF는 단순 빈도 분석보다 훨씬 스마트하게 중요 키워드를 뽑아줘 😎
# keyword_analyzer.py
from sklearn.feature_extraction.text import TfidfVectorizer
from collections import Counter
import numpy as np
from typing import Dict, List, Tuple
from text_preprocessor import TextPreprocessor
class KeywordAnalyzer:
"""키워드 분석 및 TF-IDF 계산기"""
def __init__(self):
self.preprocessor = TextPreprocessor()
def extract_keywords_tfidf(
self,
text: str,
top_n: int = 10,
reference_corpus: List[str] = None
) -> List[Tuple[str, float]]:
"""
TF-IDF 기반 키워드 추출
reference_corpus: 비교 문서 목록 (없으면 단일 문서 분석)
"""
morphemes = self.preprocessor.extract_morphemes(text)
# 명사 위주로 키워드 추출 (동사, 형용사도 포함 가능)
target_words = morphemes['nouns'] + morphemes['verbs']
processed_text = ' '.join(target_words)
if reference_corpus:
# 참조 코퍼스가 있을 때 - 진짜 TF-IDF
corpus = [processed_text] + [
' '.join(self.preprocessor.extract_morphemes(doc)['nouns'])
for doc in reference_corpus
]
else:
# 단일 문서 - 문장 단위로 분리해서 TF-IDF 계산
sentences = self.preprocessor.split_sentences(text)
corpus = [
' '.join(self.preprocessor.extract_morphemes(s)['nouns'])
for s in sentences
if len(s) > 10
]
if not corpus:
corpus = [processed_text]
# TF-IDF 계산
vectorizer = TfidfVectorizer(
max_features=100,
min_df=1,
ngram_range=(1, 2) # 단어 + 2-gram
)
try:
tfidf_matrix = vectorizer.fit_transform(corpus)
feature_names = vectorizer.get_feature_names_out()
# 첫 번째 문서(원문)의 TF-IDF 점수
scores = tfidf_matrix[0].toarray()[0]
# 점수 순으로 정렬
keyword_scores = [
(feature_names[i], round(float(scores[i]), 4))
for i in range(len(feature_names))
if scores[i] > 0
]
keyword_scores.sort(key=lambda x: x[1], reverse=True)
return keyword_scores[:top_n]
except Exception as e:
# 폴백: 단순 빈도 기반
return self._frequency_based_keywords(text, top_n)
def _frequency_based_keywords(
self, text: str, top_n: int = 10
) -> List[Tuple[str, float]]:
"""빈도 기반 키워드 추출 (폴백)"""
morphemes = self.preprocessor.extract_morphemes(text)
nouns = [n for n in morphemes['nouns'] if len(n) > 1]
freq = Counter(nouns)
total = sum(freq.values())
return [
(word, round(count / total, 4))
for word, count in freq.most_common(top_n)
]
def calculate_keyword_density(
self, text: str, target_keywords: List[str]
) -> Dict:
"""특정 키워드의 밀도 계산"""
morphemes = self.preprocessor.extract_morphemes(text)
all_words = morphemes['all']
total = len(all_words)
density_report = {}
for keyword in target_keywords:
count = all_words.count(keyword)
density = round(count / total * 100, 2) if total > 0 else 0
density_report[keyword] = {
'count': count,
'density': density,
'status': self._evaluate_density(density)
}
return density_report
def _evaluate_density(self, density: float) -> str:
"""키워드 밀도 평가"""
if density < 0.5:
return '부족 (0.5% 미만)'
elif density <= 2.5:
return '적정 (0.5~2.5%)'
elif density <= 4.0:
return '약간 과다 (2.5~4%)'
else:
return '과다 (4% 초과, 스팸 위험)'
def analyze_topic_coherence(self, text: str) -> Dict:
"""주제 일관성 분석"""
sentences = self.preprocessor.split_sentences(text)
if len(sentences) < 3:
return {'coherence_score': 100, 'note': '문장 수 부족'}
# 각 문장의 명사 집합 추출
sent_nouns = []
for sent in sentences:
morphemes = self.preprocessor.extract_morphemes(sent)
sent_nouns.append(set(morphemes['nouns']))
# 인접 문장 간 공유 단어 비율 계산
coherence_scores = []
for i in range(len(sent_nouns) - 1):
if sent_nouns[i] and sent_nouns[i+1]:
intersection = sent_nouns[i] & sent_nouns[i+1]
union = sent_nouns[i] | sent_nouns[i+1]
jaccard = len(intersection) / len(union) if union else 0
coherence_scores.append(jaccard)
avg_coherence = sum(coherence_scores) / len(coherence_scores) if coherence_scores else 0
return {
'coherence_score': round(avg_coherence * 100, 1),
'avg_sentence_overlap': round(avg_coherence, 4),
'evaluation': '높음' if avg_coherence > 0.15 else '보통' if avg_coherence > 0.05 else '낮음'
}
🤖 6. GPT API 연동 — AI의 눈으로 글 평가받기
이제 진짜 핵심이야! 수치 기반 분석만으로는 잡기 어려운 논리 흐름, 설득력, 창의성 같은 요소들을
GPT한테 평가받는 거야. 프롬프트 엔지니어링이 여기서 진짜 중요해 🎯
# gpt_evaluator.py
from openai import OpenAI
import json
from typing import Dict, Optional
from config import OPENAI_API_KEY, MODEL_NAME, MAX_TOKENS, TEMPERATURE
class GPTEvaluator:
"""GPT 기반 글쓰기 품질 평가기"""
def __init__(self):
self.client = OpenAI(api_key=OPENAI_API_KEY)
self.model = MODEL_NAME
def _build_evaluation_prompt(
self,
text: str,
writing_type: str = "일반",
target_audience: str = "일반 독자"
) -> str:
"""평가용 시스템 프롬프트 생성"""
system_prompt = """당신은 전문 글쓰기 평가 AI입니다.
주어진 텍스트를 다음 기준으로 엄격하고 객관적으로 평가하세요.
평가 기준 (각 항목 0~100점):
1. 논리적 흐름 (logical_flow): 내용의 논리적 연결성과 일관성
2. 설득력 (persuasiveness): 독자를 설득하는 능력
3. 명확성 (clarity): 표현의 명확함과 이해 용이성
4. 창의성 (creativity): 독창적인 표현과 관점
5. 구조 완성도 (structure): 서론-본론-결론의 구조적 완성도
6. 전문성 (expertise): 주제에 대한 깊이와 전문성
반드시 아래 JSON 형식으로만 응답하세요:
{
"scores": {
"logical_flow": <점수>,
"persuasiveness": <점수>,
"clarity": <점수>,
"creativity": <점수>,
"structure": <점수>,
"expertise": <점수>
},
"strengths": ["강점1", "강점2", "강점3"],
"improvements": ["개선점1", "개선점2", "개선점3"],
"overall_comment": "전반적인 평가 코멘트 (2~3문장)",
"grade": "S/A/B/C/D 중 하나"
}"""
user_prompt = f"""글쓰기 유형: {writing_type}
대상 독자: {target_audience}
평가할 텍스트:
---
{text[:3000]} # 토큰 제한으로 3000자 제한
---
위 텍스트를 평가해주세요."""
return system_prompt, user_prompt
def evaluate(
self,
text: str,
writing_type: str = "일반",
target_audience: str = "일반 독자"
) -> Dict:
"""GPT를 이용한 글쓰기 평가"""
system_prompt, user_prompt = self._build_evaluation_prompt(
text, writing_type, target_audience
)
try:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
max_tokens=MAX_TOKENS,
temperature=TEMPERATURE,
response_format={"type": "json_object"} # JSON 모드 강제
)
result_text = response.choices[0].message.content
result = json.loads(result_text)
# 점수 유효성 검증
result = self._validate_scores(result)
# 토큰 사용량 기록
result['token_usage'] = {
'prompt_tokens': response.usage.prompt_tokens,
'completion_tokens': response.usage.completion_tokens,
'total_tokens': response.usage.total_tokens
}
return result
except json.JSONDecodeError as e:
return self._fallback_evaluation(text)
except Exception as e:
print(f"GPT 평가 오류: {e}")
return self._fallback_evaluation(text)
def _validate_scores(self, result: Dict) -> Dict:
"""점수 유효성 검증 및 정규화"""
if 'scores' in result:
for key in result['scores']:
score = result['scores'][key]
result['scores'][key] = max(0, min(100, int(score)))
return result
def _fallback_evaluation(self, text: str) -> Dict:
"""API 실패 시 기본 평가 반환"""
return {
'scores': {
'logical_flow': 50,
'persuasiveness': 50,
'clarity': 50,
'creativity': 50,
'structure': 50,
'expertise': 50
},
'strengths': ['평가 불가'],
'improvements': ['API 연결 확인 필요'],
'overall_comment': 'GPT 평가를 완료하지 못했습니다.',
'grade': 'N/A'
}
def get_improvement_suggestions(
self, text: str, weak_areas: List[str]
) -> str:
"""약점 영역에 대한 구체적 개선 제안"""
areas_str = ', '.join(weak_areas)
response = self.client.chat.completions.create(
model=self.model,
messages=[{
"role": "user",
"content": f"""다음 텍스트의 [{areas_str}] 부분을 개선하기 위한
구체적이고 실행 가능한 제안을 3가지 제시해주세요.
각 제안은 실제 예시를 포함해야 합니다.
텍스트:
{text[:2000]}"""
}],
max_tokens=800,
temperature=0.5
)
return response.choices[0].message.content
gpt-4o-mini 기준 입력 $0.15/1M tokens, 출력 $0.60/1M tokens야.
텍스트를 3000자로 제한하고, 캐싱을 적극 활용해봐.
동일한 텍스트 반복 평가 방지를 위해 Redis나 SQLite로 결과 캐싱 추천!
🏆 7. 종합 평가 엔진 — 모든 걸 하나로 합치기
자, 이제 앞에서 만든 모듈들을 하나로 합쳐서 진짜 평가 시스템을 완성할 차례야!
각 지표에 가중치를 부여해서 최종 점수를 산출하는 거야 💪
# writing_quality_engine.py
from typing import Dict, List, Optional
from text_preprocessor import TextPreprocessor
from readability_analyzer import ReadabilityAnalyzer
from sentiment_analyzer import SentimentAnalyzer
from keyword_analyzer import KeywordAnalyzer
from gpt_evaluator import GPTEvaluator
import json
from datetime import datetime
class WritingQualityEngine:
"""글쓰기 품질 종합 평가 엔진"""
# 가중치 설정 (합계 = 100)
WEIGHTS = {
'readability': 25, # 가독성
'sentiment': 20, # 감성 분석
'keyword': 20, # 키워드 분석
'gpt_evaluation': 35 # GPT 평가
}
def __init__(self, use_gpt: bool = True):
self.preprocessor = TextPreprocessor()
self.readability = ReadabilityAnalyzer()
self.sentiment = SentimentAnalyzer()
self.keyword = KeywordAnalyzer()
self.use_gpt = use_gpt
if use_gpt:
self.gpt = GPTEvaluator()
def evaluate(
self,
text: str,
writing_type: str = "일반",
target_audience: str = "일반 독자",
target_keywords: Optional[List[str]] = None
) -> Dict:
"""종합 품질 평가 실행"""
print("📝 텍스트 전처리 중...")
basic_stats = self.preprocessor.get_basic_stats(text)
# 최소 텍스트 길이 체크
if basic_stats['total_chars'] < 100:
return {
'error': '텍스트가 너무 짧습니다. 최소 100자 이상 입력해주세요.',
'total_chars': basic_stats['total_chars']
}
print("📊 가독성 분석 중...")
readability_report = self.readability.get_readability_report(text)
readability_score = self._calc_readability_score(readability_report)
print("💬 감성 분석 중...")
sentiment_report = self.sentiment.analyze_full_text(text)
sentiment_score = sentiment_report.get('balance_score', 50)
print("🔍 키워드 분석 중...")
keywords = self.keyword.extract_keywords_tfidf(text, top_n=10)
coherence = self.keyword.analyze_topic_coherence(text)
keyword_score = self._calc_keyword_score(
coherence, keywords, text, target_keywords
)
gpt_report = {}
gpt_score = 50 # 기본값
if self.use_gpt:
print("🤖 GPT 평가 중... (시간이 걸릴 수 있어요)")
gpt_report = self.gpt.evaluate(text, writing_type, target_audience)
gpt_score = self._calc_gpt_score(gpt_report)
# 종합 점수 계산
final_score = (
readability_score * self.WEIGHTS['readability'] / 100 +
sentiment_score * self.WEIGHTS['sentiment'] / 100 +
keyword_score * self.WEIGHTS['keyword'] / 100 +
gpt_score * self.WEIGHTS['gpt_evaluation'] / 100
)
grade = self._get_grade(final_score)
return {
'metadata': {
'evaluated_at': datetime.now().isoformat(),
'writing_type': writing_type,
'target_audience': target_audience,
'text_length': basic_stats['total_chars']
},
'basic_stats': basic_stats,
'scores': {
'readability': round(readability_score, 1),
'sentiment': round(sentiment_score, 1),
'keyword': round(keyword_score, 1),
'gpt_evaluation': round(gpt_score, 1),
'final': round(final_score, 1)
},
'grade': grade,
'details': {
'readability': readability_report,
'sentiment': sentiment_report,
'keywords': keywords,
'coherence': coherence,
'gpt': gpt_report
},
'recommendations': self._generate_recommendations(
readability_score, sentiment_score,
keyword_score, gpt_score, gpt_report
)
}
def _calc_readability_score(self, report: Dict) -> float:
"""가독성 점수 계산 (0~100)"""
score = 70 # 기본 점수
# TTR 점수 (0.4~0.7이 이상적)
ttr = report.get('ttr', 0.5)
if 0.4 <= ttr <= 0.7:
score += 10
elif ttr < 0.3 or ttr > 0.8:
score -= 10
# 문장 다양성
variety = report.get('sentence_variety', {})
variety_score = variety.get('variety_score', 50)
score += (variety_score - 50) * 0.2
# 반복 표현
repetition = report.get('repetition', {})
rep_score = repetition.get('repetition_score', 80)
score += (rep_score - 80) * 0.2
return max(0, min(100, score))
def _calc_keyword_score(
self, coherence: Dict, keywords: List,
text: str, target_keywords: Optional[List[str]]
) -> float:
"""키워드 점수 계산"""
score = 60
# 주제 일관성
coherence_score = coherence.get('coherence_score', 50)
score += (coherence_score - 50) * 0.4
# 키워드 다양성 (상위 10개 키워드가 있으면 좋음)
if len(keywords) >= 5:
score += 10
# 타겟 키워드 밀도 체크
if target_keywords:
density_report = self.keyword.calculate_keyword_density(
text, target_keywords
)
optimal_count = sum(
1 for k, v in density_report.items()
if '적정' in v.get('status', '')
)
score += optimal_count * 5
return max(0, min(100, score))
def _calc_gpt_score(self, gpt_report: Dict) -> float:
"""GPT 평가 점수 계산"""
if not gpt_report or 'scores' not in gpt_report:
return 50
scores = gpt_report['scores']
weights = {
'logical_flow': 0.25,
'persuasiveness': 0.15,
'clarity': 0.25,
'creativity': 0.10,
'structure': 0.15,
'expertise': 0.10
}
weighted_score = sum(
scores.get(k, 50) * w
for k, w in weights.items()
)
return weighted_score
def _get_grade(self, score: float) -> str:
"""점수에 따른 등급 반환"""
if score >= 90: return 'S'
elif score >= 80: return 'A'
elif score >= 70: return 'B'
elif score >= 60: return 'C'
else: return 'D'
def _generate_recommendations(
self, read_score, sent_score,
key_score, gpt_score, gpt_report
) -> List[str]:
"""개선 추천사항 생성"""
recommendations = []
if read_score < 60:
recommendations.append(
"📖 가독성 개선: 문장을 더 짧고 다양하게 구성해보세요. "
"평균 문장 길이를 30~50자 사이로 유지하는 게 좋아요."
)
if sent_score < 60:
recommendations.append(
"💬 감성 균형: 부정적 표현이 많습니다. "
"긍정적이고 건설적인 표현으로 바꿔보세요."
)
if key_score < 60:
recommendations.append(
"🔍 주제 일관성: 글의 핵심 주제를 더 명확히 하고, "
"관련 키워드를 일관되게 사용해보세요."
)
if gpt_score < 60 and gpt_report:
improvements = gpt_report.get('improvements', [])
for imp in improvements[:2]:
recommendations.append(f"🤖 AI 제안: {imp}")
if not recommendations:
recommendations.append(
"✨ 전반적으로 잘 작성된 글입니다! "
"현재 수준을 유지하면서 더 발전시켜보세요."
)
return recommendations
🚀 8. FastAPI로 서비스 배포 — 실제로 쓸 수 있게 만들기
평가 엔진을 만들었으면 이제 API로 감싸서 실제 서비스로 만들어야지!
FastAPI는 자동 문서화도 되고 비동기 처리도 되니까 이런 AI 서비스에 딱이야 😎
# main.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional, List
import asyncio
from writing_quality_engine import WritingQualityEngine
import json
import hashlib
import time
app = FastAPI(
title="AI 글쓰기 품질 평가 API",
description="Python + GPT 기반 한국어 글쓰기 품질 평가 시스템",
version="1.0.0"
)
# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"]
)
# 간단한 메모리 캐시 (실제 서비스는 Redis 사용 권장)
cache = {}
CACHE_TTL = 3600 # 1시간
# 평가 엔진 초기화 (앱 시작 시 한 번만)
engine = WritingQualityEngine(use_gpt=True)
class EvaluationRequest(BaseModel):
text: str = Field(..., min_length=100, max_length=10000,
description="평가할 텍스트 (100~10000자)")
writing_type: str = Field(default="일반",
description="글쓰기 유형 (일반/블로그/학술/마케팅)")
target_audience: str = Field(default="일반 독자",
description="대상 독자")
target_keywords: Optional[List[str]] = Field(
default=None, description="타겟 키워드 목록"
)
use_cache: bool = Field(default=True, description="캐시 사용 여부")
class EvaluationResponse(BaseModel):
success: bool
data: dict
processing_time: float
def get_cache_key(request: EvaluationRequest) -> str:
"""캐시 키 생성"""
content = f"{request.text}{request.writing_type}{request.target_audience}"
return hashlib.md5(content.encode()).hexdigest()
@app.post("/evaluate", response_model=EvaluationResponse)
async def evaluate_writing(request: EvaluationRequest):
"""글쓰기 품질 평가 엔드포인트"""
start_time = time.time()
# 캐시 확인
if request.use_cache:
cache_key = get_cache_key(request)
if cache_key in cache:
cached_data, cached_time = cache[cache_key]
if time.time() - cached_time < CACHE_TTL:
return EvaluationResponse(
success=True,
data={**cached_data, 'from_cache': True},
processing_time=0.01
)
try:
# 비동기 실행 (블로킹 방지)
result = await asyncio.get_event_loop().run_in_executor(
None,
lambda: engine.evaluate(
request.text,
request.writing_type,
request.target_audience,
request.target_keywords
)
)
# 캐시 저장
if request.use_cache:
cache[cache_key] = (result, time.time())
processing_time = time.time() - start_time
return EvaluationResponse(
success=True,
data=result,
processing_time=round(processing_time, 2)
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
"""서버 상태 확인"""
return {"status": "healthy", "version": "1.0.0"}
@app.get("/")
async def root():
return {
"message": "AI 글쓰기 품질 평가 API",
"docs": "/docs",
"redoc": "/redoc"
}
# 서버 실행
# uvicorn main:app --reload --host 0.0.0.0 --port 8000
import requests
response = requests.post(
"http://localhost:8000/evaluate",
json={
"text": "여기에 평가할 텍스트를 입력하세요...",
"writing_type": "블로그",
"target_audience": "20~30대 직장인",
"target_keywords": ["Python", "AI", "개발"]
}
)
result = response.json()
print(f"최종 점수: {result['data']['scores']['final']}")
print(f"등급: {result['data']['grade']}")
print(f"처리 시간: {result['processing_time']}초")
📈 9. 결과 시각화 — 보기 좋은 리포트 만들기
숫자만 나오면 재미없잖아ㅋㅋ matplotlib으로 예쁜 리포트 차트도 만들어보자!
# visualizer.py
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
from typing import Dict
import matplotlib
matplotlib.rcParams['font.family'] = 'NanumGothic' # 한글 폰트
class EvaluationVisualizer:
"""평가 결과 시각화 클래스"""
def __init__(self):
self.colors = {
'primary': '#5a4a8a',
'secondary': '#7ab8d9',
'success': '#7ac99a',
'warning': '#e0a96d',
'danger': '#c07070'
}
def create_radar_chart(self, gpt_scores: Dict, save_path: str = None):
"""레이더 차트 (GPT 평가 점수)"""
categories = {
'logical_flow': '논리 흐름',
'persuasiveness': '설득력',
'clarity': '명확성',
'creativity': '창의성',
'structure': '구조',
'expertise': '전문성'
}
values = [gpt_scores.get(k, 50) for k in categories.keys()]
labels = list(categories.values())
# 레이더 차트 설정
angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False)
values_plot = values + [values[0]]
angles_plot = np.concatenate([angles, [angles[0]]])
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
ax.plot(angles_plot, values_plot, 'o-', linewidth=2,
color=self.colors['primary'])
ax.fill(angles_plot, values_plot, alpha=0.25,
color=self.colors['primary'])
ax.set_xticks(angles)
ax.set_xticklabels(labels, size=12, fontweight='bold')
ax.set_ylim(0, 100)
ax.set_yticks([20, 40, 60, 80, 100])
ax.set_yticklabels(['20', '40', '60', '80', '100'], size=9)
ax.grid(color='gray', linestyle='--', linewidth=0.5, alpha=0.7)
ax.set_title('AI 글쓰기 품질 평가\n레이더 차트',
size=14, fontweight='bold', pad=20)
if save_path:
plt.savefig(save_path, dpi=150, bbox_inches='tight')
plt.tight_layout()
return fig
def create_score_dashboard(self, result: Dict, save_path: str = None):
"""종합 점수 대시보드"""
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
fig.suptitle('글쓰기 품질 평가 대시보드',
fontsize=16, fontweight='bold', y=1.02)
scores = result.get('scores', {})
# 1. 종합 점수 게이지
ax1 = axes[0, 0]
final_score = scores.get('final', 0)
self._draw_gauge(ax1, final_score, '종합 점수')
# 2. 카테고리별 점수 바 차트
ax2 = axes[0, 1]
categories = ['가독성', '감성', '키워드', 'AI평가']
cat_scores = [
scores.get('readability', 0),
scores.get('sentiment', 0),
scores.get('keyword', 0),
scores.get('gpt_evaluation', 0)
]
colors = [self.colors['primary'], self.colors['secondary'],
self.colors['success'], self.colors['warning']]
bars = ax2.barh(categories, cat_scores, color=colors, height=0.5)
ax2.set_xlim(0, 100)
ax2.set_title('카테고리별 점수', fontweight='bold')
for bar, score in zip(bars, cat_scores):
ax2.text(bar.get_width() + 1, bar.get_y() + bar.get_height()/2,
f'{score:.1f}', va='center', fontweight='bold')
# 3. 감성 분포 파이 차트
ax3 = axes[1, 0]
sentiment = result.get('details', {}).get('sentiment', {})
dist = sentiment.get('sentiment_distribution',
{'positive': 33, 'neutral': 34, 'negative': 33})
pie_labels = ['긍정', '중립', '부정']
pie_values = [
dist.get('positive', 0),
dist.get('neutral', 0),
dist.get('negative', 0)
]
pie_colors = [self.colors['success'], self.colors['secondary'],
self.colors['danger']]
ax3.pie(pie_values, labels=pie_labels, colors=pie_colors,
autopct='%1.1f%%', startangle=90)
ax3.set_title('감성 분포', fontweight='bold')
# 4. 키워드 워드클라우드 대체 (상위 키워드 바 차트)
ax4 = axes[1, 1]
keywords = result.get('details', {}).get('keywords', [])[:8]
if keywords:
kw_labels = [k[0] for k in keywords]
kw_scores = [k[1] * 100 for k in keywords]
ax4.barh(kw_labels[::-1], kw_scores[::-1],
color=self.colors['primary'], alpha=0.7, height=0.5)
ax4.set_title('핵심 키워드 (TF-IDF)', fontweight='bold')
ax4.set_xlabel('TF-IDF 점수 (%)')
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=150, bbox_inches='tight')
return fig
def _draw_gauge(self, ax, value: float, title: str):
"""게이지 차트 그리기"""
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)
ax.set_aspect('equal')
ax.axis('off')
# 배경 반원
theta = np.linspace(np.pi, 0, 100)
ax.plot(np.cos(theta), np.sin(theta),
color='#e0e0e0', linewidth=20, solid_capstyle='round')
# 값 반원
fill_theta = np.linspace(np.pi, np.pi - (value/100) * np.pi, 100)
color = (self.colors['success'] if value >= 70
else self.colors['warning'] if value >= 50
else self.colors['danger'])
ax.plot(np.cos(fill_theta), np.sin(fill_theta),
color=color, linewidth=20, solid_capstyle='round')
ax.text(0, -0.3, f'{value:.1f}', ha='center', va='center',
fontsize=28, fontweight='bold', color=color)
ax.text(0, -0.7, title, ha='center', va='center',
fontsize=12, fontweight='bold')
💡 10. 실전 활용 팁 & 고도화 아이디어
기본 시스템은 완성했는데, 이걸 더 잘 써먹으려면 몇 가지 팁이 있어!
재능넷에서 글쓰기 서비스를 제공하는 분들이라면 이 시스템으로 자신의 글 품질을 객관적으로 검증하고 포트폴리오에 활용할 수 있어 😊
글쓰기 유형에 따라 평가 기준이 달라야 해. 아래처럼 설정 파일로 관리하면 편해.
# evaluation_profiles.py
EVALUATION_PROFILES = {
"블로그": {
"weights": {
"readability": 35, # 가독성 더 중요
"sentiment": 25, # 감성 중요
"keyword": 25, # SEO 키워드 중요
"gpt_evaluation": 15
},
"ideal_sentence_length": (20, 50),
"ideal_ttr": (0.45, 0.65),
"tone": "친근하고 대화체"
},
"학술": {
"weights": {
"readability": 15,
"sentiment": 10,
"keyword": 25,
"gpt_evaluation": 50 # AI 논리 평가 최중요
},
"ideal_sentence_length": (40, 80),
"ideal_ttr": (0.55, 0.75),
"tone": "객관적이고 형식적"
},
"마케팅": {
"weights": {
"readability": 25,
"sentiment": 35, # 감성 최중요
"keyword": 25,
"gpt_evaluation": 15
},
"ideal_sentence_length": (15, 40),
"ideal_ttr": (0.35, 0.55),
"tone": "설득적이고 행동 유도"
}
}
여러 글을 한 번에 평가하거나 A/B 테스트할 때 유용한 배치 처리 기능이야.
# batch_evaluator.py
import asyncio
from typing import List, Dict
from writing_quality_engine import WritingQualityEngine
async def batch_evaluate(
texts: List[str],
engine: WritingQualityEngine,
max_concurrent: int = 3
) -> List[Dict]:
"""비동기 배치 평가"""
semaphore = asyncio.Semaphore(max_concurrent)
async def evaluate_one(text: str, idx: int) -> Dict:
async with semaphore:
print(f"평가 중 [{idx+1}/{len(texts)}]...")
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, engine.evaluate, text
)
return {'index': idx, 'result': result}
tasks = [evaluate_one(text, i) for i, text in enumerate(texts)]
results = await asyncio.gather(*tasks)
return sorted(results, key=lambda x: x['index'])
def compare_texts(results: List[Dict]) -> Dict:
"""여러 텍스트 비교 분석"""
comparison = []
for i, r in enumerate(results):
scores = r['result'].get('scores', {})
comparison.append({
'index': i + 1,
'final_score': scores.get('final', 0),
'grade': r['result'].get('grade', 'N/A'),
'readability': scores.get('readability', 0),
'sentiment': scores.get('sentiment', 0),
'keyword': scores.get('keyword', 0),
'gpt': scores.get('gpt_evaluation', 0)
})
# 최고/최저 점수 텍스트 찾기
best = max(comparison, key=lambda x: x['final_score'])
worst = min(comparison, key=lambda x: x['final_score'])
return {
'comparison': comparison,
'best_text': best,
'worst_text': worst,
'avg_score': sum(c['final_score'] for c in comparison) / len(comparison)
}
🗄️ DB 연동 PostgreSQL로 평가 이력 저장 및 트렌드 분석
📱 웹 UI Streamlit이나 Gradio로 빠른 프로토타입 UI 구현
🔔 알림 점수 임계값 이하 시 Slack/이메일 알림
📊 대시보드 Grafana로 실시간 품질 모니터링
🤖 파인튜닝 도메인 특화 데이터로 감성 모델 파인튜닝
🌐 다국어 영어/일본어 텍스트 평가 확장
🎯 마무리 — 이 시스템으로 뭘 할 수 있냐고?
야 솔직히 이 시스템 완성하면 진짜 쓸모가 엄청 많아ㅋㅋ 정리해줄게!
✍️ 콘텐츠 크리에이터: 블로그 포스팅 올리기 전에 품질 체크 → 독자 이탈률 감소
📚 교육 플랫폼: 학생 글쓰기 과제 자동 채점 보조 도구로 활용
🏢 기업 마케팅팀: 카피라이팅 품질 일관성 유지 및 A/B 테스트
🔍 SEO 전문가: 키워드 밀도 최적화 및 가독성 개선
🤝 프리랜서: 납품 전 품질 검증으로 클라이언트 만족도 향상
특히 재능넷에서 글쓰기 관련 재능을 판매하는 분들한테 이 시스템은 진짜 차별화 포인트가 될 수 있어.
"AI 품질 검증 완료" 배지 같은 거 달 수 있잖아ㅋㅋ 클라이언트 신뢰도 올라가는 거 보장!
이 시스템은 보조 도구야. AI 점수가 높다고 무조건 좋은 글이 아니고,
낮다고 나쁜 글도 아니야. 창의적인 글쓰기는 수치로 완전히 평가할 수 없어.
특히 시, 소설 같은 문학 장르에는 이 평가 기준이 맞지 않을 수 있어!
총 8개 모듈 → 약 600줄의 Python 코드
처리 시간: GPT 포함 약 5~15초 / GPT 제외 약 0.5~2초
API 비용: gpt-4o-mini 기준 글 하나당 약 $0.001~0.003 (매우 저렴!)
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.

댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.