fastAPI 심화

- Chart.js
- pdf, csv 파일 업로드 후 데이터 정제하여 llm으로 처리 후 결과 도출
- sqlite로 데이터 저장
- ORM - SQLAlchemy
This commit is contained in:
2026-06-16 18:03:02 +09:00
parent ccfdac1286
commit 06eb3c57ab
43 changed files with 1912 additions and 39 deletions
@@ -0,0 +1,9 @@
from langchain_ibm import WatsonxEmbeddings
from backend.config.settings import settings
watson_embedding = WatsonxEmbeddings(
model_id="ibm/granite-embedding-278m-multilingual",
url=f"{settings.watsonx_url}",
api_key=f"{settings.watsonx_api_key}",
project_id=f"{settings.watsonx_project_id}",
)
+13
View File
@@ -0,0 +1,13 @@
from langchain_ibm import ChatWatsonx
from backend.config.settings import settings
watson_llm = ChatWatsonx(
model_id="ibm/granite-4-h-small",
url=f"{settings.watsonx_url}",
api_key=f"{settings.watsonx_api_key}",
project_id=f"{settings.watsonx_project_id}",
max_tokens=2000,
params={
"temperature": 0
}
)
@@ -0,0 +1,11 @@
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file="backend/.env", extra="ignore")
# 사용할 모델
watsonx_api_key: str = Field(alias="WATSONX_API_KEY")
watsonx_project_id: str = Field(alias="WATSONX_PROJECT_ID")
watsonx_url: str = Field(alias="WATSONX_URL")
settings = Settings()
@@ -0,0 +1,25 @@
from fastapi import APIRouter, Request, UploadFile
from backend.schemas.card_schema import AnalysisRequest
from backend.services.card_service import upload_csv, card_history, get_dashboard, card_analysis
from fastapi.templating import Jinja2Templates
router = APIRouter(prefix="/api/card")
templates = Jinja2Templates(directory="backend/templates")
@router.post("/upload")
async def upload_file(file: UploadFile):
return await upload_csv(file)
@router.get("/history")
async def history(request : Request):
card_infos = card_history()
return templates.TemplateResponse(request, name = "history.html", context = {"history": card_infos})
@router.get("/dashboard")
async def dashboard():
return get_dashboard()
@router.post("/analysis")
async def sql_llm_analysis(request : AnalysisRequest):
return card_analysis(request.question)
@@ -0,0 +1,22 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
router = APIRouter()
templates = Jinja2Templates(directory="backend/templates")
# http://127.0.0.1:8000
@router.get("/")
async def home(request : Request):
return templates.TemplateResponse(request = request, name="index.html")
@router.get("/card/upload")
async def rag(request : Request):
return templates.TemplateResponse(request = request, name="card.html")
@router.get("/card/dashboard")
async def dashboard(request : Request):
return templates.TemplateResponse(request = request, name="dashboard.html")
@router.get("/card/analysis")
async def analysis(request : Request):
return templates.TemplateResponse(request = request, name="analysis.html")
@@ -0,0 +1,8 @@
from pydantic import BaseModel
class AnalysisRequest(BaseModel):
question: str
class AnalysisResponse(BaseModel):
message: str
@@ -0,0 +1,140 @@
from fastapi import UploadFile
from langchain_core.runnables import RunnablePassthrough
from backend.services.db_service import get_connection
from backend.services.db_service import get_table_columns
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from backend.ai.llm import watson_llm
import csv
async def upload_csv(file :UploadFile):
"""
csv 파일을 테이블에 저장
"""
conn =get_connection()
cursor = conn.cursor()
# file.read() : 비동기 함수임
contents = await file.read()
csv_text = contents.decode('utf-8')
reader = csv.DictReader(csv_text.splitlines())
count = 0
for row in reader:
cursor.execute("""
INSERT INTO transactions (date, category, merchant, amount)
VALUES (?, ?,?, ?)"""
,
(row['date'], row['category'], row['merchant'], row['amount'])
)
count += 1
conn.commit()
conn.close()
return {"message": f"{count} 건 저장 완료"}
def card_history():
"""
db에서 카드 정보 조회
"""
conn =get_connection()
cursor = conn.cursor()
try :
cursor.execute("""
SELECT * FROM transactions ORDER BY date DESC
""")
rows = cursor.fetchall()
query_result = [dict(row) for row in rows]
conn.close()
except Exception as e:
query_result = [f"SQL 실행 오류 : {e}"]
return query_result
def get_dashboard():
"""
db에서 대시보드 정보 조회
"""
conn = get_connection()
cursor = conn.cursor()
# 카테고리 별 사용금액
cursor.execute(
"""
SELECT category, SUM(amount)
FROM transactions
GROUP BY category
ORDER BY SUM(amount) DESC
"""
)
category_rows = cursor.fetchall()
# 월별 사용금액
cursor.execute(
"""
SELECT strftime('%Y-%m', date), SUM(amount)
FROM transactions
GROUP BY strftime('%Y-%m', date)
ORDER BY strftime('%Y-%m', date)
"""
)
month_rows = cursor.fetchall()
conn.close()
return {
"category": [{"category":row[0], "amount":row[1]} for row in category_rows],
"monthly": [{"month":row[0], "amount":row[1]} for row in month_rows],
}
def sql_generate_llm(question):
"""자연어 -> SQL"""
sql_prompt = ChatPromptTemplate.from_template("""
당신은 SQLite 전문가입니다.
테이블명 : transactions
컬럼 : {columns}
질문 : {question}
SQL 만 출력하세요
""")
columns = get_table_columns("transactions")
sql_chain = sql_prompt | watson_llm | StrOutputParser()
sql = sql_chain.invoke({"columns": columns, "question": question})
print(f"sql: {sql}")
sql = sql.replace("```sql","").replace("```", "").strip()
# sql 문 실제 실행
conn = get_connection()
cursor = conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
query_result = [dict(row) for row in rows]
conn.close()
return query_result
def card_analysis(question):
query_result = sql_generate_llm(question)
analysis_prompt = ChatPromptTemplate.from_template("""
사용자 질문 : {question}
SQL 결과
{result}
결과를 설명하고 소비 습관을 분석하고 절약 팁을 제시해 주세요.
""")
analysis_chain = analysis_prompt | watson_llm | StrOutputParser()
answer = analysis_chain.invoke({"question": question, "result": query_result})
return {"message": answer}
@@ -0,0 +1,44 @@
import sqlite3
import os
DB_PATH = "db/history.db"
"""
cursor.execute('select id, name for users')
cursor.fetchone() => (1, 'alice', '010-1234-5678')
row['id']
"""
def get_connection():
coon = sqlite3.connect(DB_PATH, isolation_level=None)
# 조회 결과를 어떻게 반활할지 설정
coon.row_factory = sqlite3.Row
return coon
def init_db():
# 디렉토리 생성
os.makedirs("db", exist_ok=True)
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""create table if not exists transactions (
id integer primary key AUTOINCREMENT,
date text,
category text,
merchant text,
amount integer
)
""")
conn.commit()
conn.close()
print("DB 초기화")
def get_table_columns(table_name):
conn = get_connection()
cursor = conn.cursor()
cursor.execute("PRAGMA table_info({})".format(table_name))
columns = cursor.fetchall()
conn.close()
return [column[1] for column in columns]
@@ -0,0 +1,17 @@
document.querySelector("button").addEventListener("click", ask)
async function ask() {
// 사용자가 질문 입력 시 질문을 서버로 전송
const question = document.querySelector('#question').value
const response = await fetch("/api/card/analysis", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({question : question})
})
// 전송 후 answer 도착 시 answer 화면에 보여주기
const answer = await response.json()
document.querySelector('#answer').textContent = answer.message
}
@@ -0,0 +1,24 @@
// 파일 업로드
document.querySelector("#uploadBtn").addEventListener("click",uploadFile)
async function uploadFile() {
const fileInput = document.querySelector("#file")
// 첨부파일 정보 가져오기
const file = fileInput.files[0]
if(!file){
alert('파일을 선택하세요');
return;
}
// form 만들어 전송
const formData = new FormData();
formData.append("file",file);
const response = await fetch("/api/card/upload",{
method:"POST",
body:formData
})
// 전송 후 answer 도착 시 answer 화면에 보여주기
const answer = await response.json()
document.querySelector('#result').textContent = answer.message
}
@@ -0,0 +1,65 @@
const drawCategoryChart = (data) => {
new Chart(document.querySelector("#categoryChart"), {
type: 'pie',
data: {
labels : data.map((x) => x.category),
datasets:[
{
data:data.map((x)=>x.amount)
}
]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: '카테고리별 소비내역'
}
}
},
})
}
const drawMonthlyChart = (data) => {
new Chart(document.querySelector("#monthlyChart"), {
type: 'line',
data: {
labels : data.map((x) => x.month),
datasets:[
{
label:"월별 소비",
data:data.map((x)=>x.amount)
}
]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: '월별 소비내역'
}
}
},
})
}
async function loadDashboard() {
const response = await fetch("/api/card/dashboard");
const data = await response.json();
console.log(data);
drawCategoryChart(data.category);
drawMonthlyChart(data.monthly)
}
window.onload = loadDashboard;
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<H2>신용카드 사용내역 분석</H2>
<div>
<input type = "text" name = "question" id = "question" size = "50">
<button type="button">질문하기</button>
</div>
<hr>
<div id="answer"></div>
<script src="{{ url_for('static', path='js/analysis.js')}}"></script>
</body>
</html>
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>카드 정보 추가</h2>
<div>
<input type="file" name="file" id="file">
<button id="uploadBtn">업로드</button>
</div>
<hr>
<div id="result"></div>
<script src="{{ url_for('static', path='js/card.js')}}"></script>
</body>
</html>
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>소비내역 대시보드</h2>
<div style ="width: 700px; height: 300px">
<canvas id="categoryChart"></canvas>
</div>
<div style ="width: 700px; height: 300px">
<canvas id="monthlyChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', path='js/dashboard.js')}}"></script>
</body>
</html>
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>카드 소비 내역</h2>
<table border = "1">
<tr>
<th>날짜</th>
<th>카테고리</th>
<th>가맹점</th>
<th>금액</th>
</tr>
<tbody>
{% for row in history %}
<tr>
<td>{{ row.date }}</td>
<td>{{ row.category }}</td>
<td>{{ row.merchant }}</td>
<td>{{ row.amount }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>신용카드 분석</h1>
<ul>
<li>
<a href="/card/upload">1. 카드 사용 정보 업로드</a>
</li>
<li>
<a href="/api/card/history">2. 카드 사용 내역 확인</a>
</li>
<li>
<a href="/card/dashboard">3. 카드 소비 내역 대시보드</a>
</li>
<li>
<a href="/card/analysis">4. 카드 소비 내역 분석 및 조언</a>
</li>
</ul>
</body>
</html>