Skip to content

Commit f308742

Browse files
authored
Merge pull request #8 from SKT-FlyAI-BoraMettugi/dev
Final merge
2 parents d3eb328 + b0a1046 commit f308742

26 files changed

+446
-116
lines changed

.github/workflows/deploy.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ jobs:
3131
echo "REDIS_URL=${{ secrets.REDIS_URL }}" >> .env
3232
echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
3333
echo "KAKAO_REDIRECT_URI=${{ secrets.KAKAO_REDIRECT_URI }}" >> .env
34+
echo "MINIO_USER=${{ secrets.MINIO_USER }}" >> .env
35+
echo "MINIO_PASSWORD=${{ secrets.MINIO_PASSWORD }}" >> .env
36+
echo "MINIO_HOST=${{ secrets.MINIO_HOST }}" >> .env
37+
echo "S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }}" >> .env
38+
3439
3540
- name: Generate deployment package
3641
run: zip -r deploy.zip . -x '*.git*' '*.github*' 'venv/*' '__pycache__/*'

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ __pycache__
66
*.bin
77
.idea/
88
venv/
9-
main_my.py
9+
main_my.py
10+
test_download/
11+
downloaded_model/

api/routes/answer.py

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
from sqlalchemy.orm import Session
33
from database.nolly import get_db
44
from crud.answer import get_answer_history, get_answer_scores
5-
from schemas.answer import AnswerResponse, AnswerScoreResponse
5+
from schemas.answer import AnswerResponse, AnswerScoreResponse, AnswerSubmit
6+
from dependencies import get_tokenizer, get_model
7+
from models.question import Question
8+
from models.answer import Answer
9+
import torch
10+
import json
11+
import re
612

713
router = APIRouter()
814

@@ -22,4 +28,133 @@ async def get_answer_scores_api(user_id: int, question_id: int, db: Session = De
2228
if not answer_scores:
2329
raise HTTPException(status_code=404, detail="채점 결과를 찾을 수 없습니다.")
2430

25-
return answer_scores
31+
return answer_scores
32+
33+
@router.post("grade/{user_id}/{question_id}")
34+
async def grade_answers(
35+
user_id: int, question_id: int,
36+
answer_data: AnswerSubmit, tokenizer = Depends(get_tokenizer), model = Depends(get_model),
37+
db: Session = Depends(get_db)
38+
):
39+
question = db.query(Question).filter(
40+
Question.question_id == question_id
41+
).first()
42+
43+
if not question:
44+
raise HTTPException(status_code=404, detail="해당 문제를 찾을 수 없습니다.")
45+
46+
description = question.description # 문제 설명
47+
48+
# ✅ 패딩 토큰 명확하게 설정 (경고 방지)
49+
if tokenizer.pad_token is None:
50+
tokenizer.pad_token = tokenizer.eos_token
51+
52+
# 1. 사용자 답변 불러오기 (JSON 출력을 강제하는 프롬프트 추가)
53+
input_text = [
54+
{"role": "system", "content": """답변을 다음 항목으로 나누어 JSON 형식으로 평가해 주세요.
55+
반드시 JSON 객체 하나로 반환하세요.
56+
예시:
57+
{
58+
"논리력": {"점수": 8, "해설": "설명이 논리적입니다."},
59+
"사고력": {"점수": 7, "해설": "분석력이 뛰어납니다."},
60+
"창의력": {"점수": 9, "해설": "새로운 아이디어가 포함되었습니다."},
61+
"설득력": {"점수": 6, "해설": "설명이 명확합니다."},
62+
"추론의 깊이": {"점수": 7, "해설": "근거가 논리적입니다."}
63+
}
64+
""".strip()},
65+
{"role": "assistant", "content": f"{description}"},
66+
{"role": "user", "content": f"{answer_data.answer}"}
67+
]
68+
69+
# 2. 모델에 넣을 수 있도록 토큰화
70+
inputs = tokenizer.apply_chat_template(
71+
input_text,
72+
add_generation_prompt=True,
73+
return_tensors="pt",
74+
padding=True,
75+
truncation=True
76+
).to(model.device)
77+
78+
terminators = [
79+
tokenizer.convert_tokens_to_ids("<|end_of_text|>"),
80+
tokenizer.convert_tokens_to_ids("<|eot_id|>")
81+
]
82+
83+
# 3. 모델 추론 수행
84+
outputs = model.generate(
85+
inputs,
86+
max_new_tokens=1024,
87+
pad_token_id=tokenizer.pad_token_id,
88+
eos_token_id=tokenizer.eos_token_id,
89+
do_sample=True,
90+
temperature=0.6,
91+
top_p=0.9
92+
)
93+
94+
# 4. 모델 결과 디코딩
95+
generated_text = tokenizer.decode(outputs[:, inputs.shape[1]:][0], skip_special_tokens=True)
96+
print("🔍 모델 생성 결과:", generated_text)
97+
98+
# ✅ JSON 자동 보정 (불완전한 JSON을 수정)
99+
try:
100+
# 1️⃣ JSON 내부 개별 `{}` 블록을 하나의 JSON 객체로 병합
101+
json_str = re.sub(r"}\s*{", "},{", generated_text.strip()) # 중괄호 사이 개행 문제 수정
102+
json_str = f"{{{json_str.strip()}}}" if not json_str.startswith("{") else json_str # 중괄호 감싸기
103+
json_str = json_str.replace("\n", "").replace("\t", "") # 불필요한 줄바꿈 제거
104+
json_str = re.sub(r",\s*}", "}", json_str) # 마지막 쉼표 제거
105+
106+
result = json.loads(json_str) # JSON 변환
107+
108+
except json.JSONDecodeError:
109+
raise HTTPException(status_code=500, detail=f"모델 응답을 JSON으로 변환하는데 실패했습니다. 출력된 텍스트: {generated_text}")
110+
111+
# 5. 점수와 리뷰 추출
112+
scores = [
113+
result.get("논리력", {}).get("점수", 0),
114+
result.get("사고력", {}).get("점수", 0),
115+
result.get("창의력", {}).get("점수", 0),
116+
result.get("설득력", {}).get("점수", 0),
117+
result.get("추론의 깊이", {}).get("점수", 0)
118+
]
119+
120+
reviews = [
121+
result.get("논리력", {}).get("해설", ""),
122+
result.get("사고력", {}).get("해설", ""),
123+
result.get("창의력", {}).get("해설", ""),
124+
result.get("설득력", {}).get("해설", ""),
125+
result.get("추론의 깊이", {}).get("해설", "")
126+
]
127+
128+
total_score = sum(scores) / len(scores) if scores else 0
129+
130+
# 6. DB 저장
131+
existing_answer = db.query(Answer).filter(
132+
Answer.user_id == user_id,
133+
Answer.question_id == question_id
134+
).first()
135+
136+
if existing_answer:
137+
existing_answer.content = answer_data.answer
138+
existing_answer.creativity = scores[2]
139+
existing_answer.logic = scores[0]
140+
existing_answer.thinking = scores[1]
141+
existing_answer.persuasion = scores[3]
142+
existing_answer.depth = scores[4]
143+
existing_answer.creativity_review = reviews[2]
144+
existing_answer.logic_review = reviews[0]
145+
existing_answer.thinking_review = reviews[1]
146+
existing_answer.persuasion_review = reviews[3]
147+
existing_answer.depth_review = reviews[4]
148+
existing_answer.total_score = total_score
149+
else:
150+
db.add(Answer(
151+
user_id=user_id, question_id=question_id, content=answer_data.answer,
152+
creativity=scores[2], logic=scores[0], thinking=scores[1],
153+
persuasion=scores[3], depth=scores[4],
154+
creativity_review=reviews[2], logic_review=reviews[0],
155+
thinking_review=reviews[1], persuasion_review=reviews[3], depth_review=reviews[4],
156+
total_score=total_score
157+
))
158+
159+
db.commit()
160+
return {"message":"답변 제출 완료"}

api/routes/comment.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from fastapi import APIRouter, Depends
1+
from fastapi import APIRouter, Depends, HTTPException
22
from sqlalchemy.orm import Session
33
from database.nolly import get_db
4-
from crud.comment import create_comment, add_like_to_comment
4+
from crud.comment import create_comment, add_like_to_comment, get_liked_comments_by_user, get_comments_by_discussion_id
55
from schemas.comment import CommentCreate, CommentResponse, CommentLikeResponse
6+
from typing import List
67

78
router = APIRouter()
89

@@ -16,4 +17,19 @@ async def post_comment(user_id: int, discussion_id: int, comment_data: CommentCr
1617
@router.patch("/like/{comment_id}/{user_id}", response_model=CommentLikeResponse)
1718
async def like_comment(comment_id: int, user_id: int, db: Session = Depends(get_db)):
1819
comment = add_like_to_comment(db, comment_id, user_id)
19-
return comment
20+
return comment
21+
22+
# 좋아요 누른 답글 조회
23+
@router.get("/like/{user_id}")
24+
async def get_liked_comments(user_id: int, db: Session = Depends(get_db)):
25+
return get_liked_comments_by_user(db, user_id)
26+
27+
# 토론에 대한 모든 답글 조회
28+
@router.get("/{discussion_id}", response_model=List[CommentResponse])
29+
async def get_comments(discussion_id: int, db: Session = Depends(get_db)):
30+
comments = get_comments_by_discussion_id(db, discussion_id)
31+
32+
if not comments:
33+
raise HTTPException(status_code=404, detail="해당 토론에 대한 답글이 없습니다.")
34+
35+
return comments

api/routes/discussion.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from fastapi import APIRouter, Depends, HTTPException
22
from sqlalchemy.orm import Session
33
from database.nolly import get_db
4-
from crud.discussion import create_discussion, get_discussions_by_question, add_like_to_discussion
4+
from crud.discussion import create_discussion, get_discussions_by_question, add_like_to_discussion, get_liked_discussions_by_user
55
from schemas.discussion import DiscussionCreate, DiscussionResponse, DiscussionLikeResponse
6+
from typing import List
67

78
router = APIRouter()
89

@@ -28,4 +29,13 @@ async def like_discussion(discussion_id: int, user_id: int, db: Session = Depend
2829
result = add_like_to_discussion(db, discussion_id, user_id)
2930
return result
3031

32+
# 좋아요 누른 토론 조회
33+
@router.get("/like/{user_id}", response_model=List[DiscussionResponse])
34+
async def get_liked_discussions(user_id: int, db: Session = Depends(get_db)):
35+
liked_discussions = get_liked_discussions_by_user(db, user_id)
36+
37+
if not liked_discussions:
38+
raise HTTPException(status_code=404, detail="좋아요 누른 토론이 없습니다.")
39+
40+
return liked_discussions
3141

api/routes/question.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ async def get_question(question_id: int, db: Session = Depends(get_db)):
2121
if not question:
2222
raise HTTPException(status_code=404, detail="해당 문제를 찾을 수 없습니다.")
2323

24-
return question
24+
return question
25+
26+

api/routes/user.py

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
from fastapi import APIRouter, Depends, HTTPException
22
from sqlalchemy.orm import Session
33
from database.nolly import get_db
4-
from crud.user import get_user_info, update_user_nickname, update_kakao_login, logout_kakao_user
4+
from crud.user import get_user_info, update_user_nickname, get_user_by_kakao_id, create_user, update_kakao_login, logout_kakao_user, get_all_users
55
from schemas.nickname import Nickname
66
from schemas.login import KakaoLoginRequest
7+
from schemas.user import UserResponse
8+
from typing import List
79
from core.kakao_api import KakaoAPI
10+
import random
811

912
router = APIRouter()
1013
kakao_api = KakaoAPI()
1114

15+
# 전체 유저 조회
16+
@router.get("/all", response_model=List[UserResponse])
17+
async def get_all_users_api(db: Session = Depends(get_db)):
18+
users = get_all_users(db)
19+
return users
20+
1221
@router.get("/{user_id}")
1322
async def get_user(user_id: int, db: Session = Depends(get_db)):
1423
user = get_user_info(user_id, db)
@@ -19,42 +28,25 @@ async def update_nickname(nickname: Nickname, db: Session = Depends(get_db)):
1928
update_user_nickname(nickname, db)
2029
return nickname
2130

22-
# user 생성
23-
24-
# 카카오 로그인 url 반환
25-
@router.get("/auth/kakao/login-url")
26-
def get_kakao_login_url():
27-
return {"login_url": kakao_api.get_auth_url()}
28-
29-
# 카카오 로그인 후 받은 code를 JSON으로 반환
30-
@router.get("/auth/kakao/callback")
31-
async def kakao_callback(code: str):
32-
return {"code": code}
33-
3431
# 프론트에서 code 보냄
35-
@router.patch("/login/{user_id}")
36-
async def kakao_login(user_id: int, request: KakaoLoginRequest, db: Session = Depends(get_db)):
37-
38-
# 인가 코드로 카카오에서 액세스 토큰 요청
39-
token_data = await kakao_api.get_access_token(request.code)
40-
access_token = token_data.get("access_token")
41-
print(" 액세스 토큰:", access_token)
32+
@router.patch("/login")
33+
async def kakao_login(request: KakaoLoginRequest, db: Session = Depends(get_db)):
34+
kakao_id = request.kakao_id
35+
nickname = request.nickname
36+
profile_image = request.profile_img
4237

43-
if not access_token:
44-
raise HTTPException(status_code=400, detail="카카오 액세스 토큰 발급 실패")
38+
# DB에서 기존 유저 확인
39+
user = get_user_by_kakao_id(db, kakao_id)
4540

46-
# 액세스 토큰으로 사용자 정보 요청
47-
user_data = await kakao_api.get_user_info(access_token)
48-
kakao_id = user_data["id"]
49-
nickname = user_data["kakao_account"]["profile"].get("nickname", "No Nickname")
50-
profile_image = user_data["kakao_account"]["profile"].get("profile_image_url", None)
51-
# DB 업데이트 수행
52-
db_user = update_kakao_login(db, user_id, access_token, nickname, profile_image)
41+
if user:
42+
# 기존 유저: 로그인 처리
43+
update_kakao_login(db, user.user_id, None, nickname, profile_image)
44+
return {"message": "카카오 로그인 성공", "user_id": user.user_id, "nickname": nickname}
5345

54-
if not db_user:
55-
raise HTTPException(status_code=404, detail="User not found")
46+
# 신규 유저: 회원가입 후 로그인 처리
47+
new_user = create_user(db, kakao_id, nickname, profile_image, None)
5648

57-
return {"message": "카카오 로그인 성공", "user_id": user_id, "nickname": nickname}
49+
return {"message": "회원가입 및 로그인 성공", "user_id": new_user.user_id, "nickname": nickname}
5850

5951
# 카카오 로그아웃
6052
@router.patch("/logout/{user_id}")

core/kakao_api.py

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from dotenv import load_dotenv
55

66
# 환경 변수 로드
7-
load_dotenv(override=True)
7+
load_dotenv()
88

99
class KakaoAPI:
1010
def __init__(self):
@@ -14,45 +14,6 @@ def __init__(self):
1414
self.kakao_token_url = "https://kauth.kakao.com/oauth/token"
1515
self.kakao_user_info_url = "https://kapi.kakao.com/v2/user/me"
1616

17-
# 카카오 로그인 url 생성
18-
def get_auth_url(self):
19-
return {
20-
f"{self.kakao_auth_url}?response_type=code"
21-
f"&client_id={self.client_id}"
22-
f"&redirect_uri={self.redirect_uri}"
23-
}
24-
# 인가 코드로 액세스 토큰 요청
25-
async def get_access_token(self, code: str):
26-
async with httpx.AsyncClient() as client:
27-
response = await client.post(
28-
self.kakao_token_url,
29-
data={
30-
"grant_type": "authorization_code",
31-
"client_id": self.client_id,
32-
"redirect_uri": self.redirect_uri,
33-
"code": code,
34-
},
35-
headers={"Content-Type": "application/x-www-form-urlencoded"},
36-
)
37-
38-
# 🔹 카카오 API 응답 로그 출력
39-
print("카카오 토큰 응답:", response.json())
40-
41-
# ✅ HTTP 상태 코드 체크 추가 (잘못된 응답 처리)
42-
if response.status_code != 200:
43-
raise HTTPException(status_code=400, detail=f"카카오 액세스 토큰 요청 실패: {response.text}")
44-
45-
return response.json()
46-
47-
# 액세스 토큰으로 사용자 정보 요청
48-
async def get_user_info(self, access_token: str):
49-
async with httpx.AsyncClient() as client:
50-
response = await client.get(
51-
self.kakao_user_info_url,
52-
headers={"Authorization": f"Bearer {access_token}"},
53-
)
54-
return response.json()
55-
5617
# 카카오 로그아웃
5718
async def logout_user(self, access_token: str):
5819
async with httpx.AsyncClient() as client:

0 commit comments

Comments
 (0)