어렵다...
학습시간 09:00~21:00(당일12H/누적2024H)
실습!!
response 모델의 기본구조를 만들어주세요
- 모든 response 에 공통적으로 포함될 필드를 정의
success : bool
timestamp : str (datetime.now().isoformat())
version : str ("1.0")
class BaseResponse(
from fastapi import FastAPI, Response, status
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Generic, TypeVar, Optional, List
app = FastAPI()
# --- 1. BaseResponse 모델 정의 ---
T = TypeVar('T') # 모든 타입을 받을 수 있는 제네릭 타입 변수 선언
class BaseResponse(BaseModel, Generic[T]):
success: bool = True
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
version: str = "1.0"
data: Optional[T] = None # 실제 데이터를 담을 필드, 제네릭 타입으로 선언
# --- 2. BaseResponse 모델 활용 예시 ---
class User(BaseModel):
id: int
name: str
email: str
# 샘플 데이터베이스
users_db = {
1: User(id=1, name="홍길동", email="hong@example.com"),
2: User(id=2, name="이순신", email="lee@example.com"),
}
@app.get("/users/{user_id}", response_model=BaseResponse[User])
def get_user(user_id: int):
user = users_db.get(user_id)
if not user:
# 실패 시 BaseResponse 형태를 직접 만들어 반환
return Response(
content=BaseResponse[User](success=False, data=None).model_dump_json(),
status_code=status.HTTP_404_NOT_FOUND,
media_type="application/json"
)
# 성공 시 데이터를 BaseResponse로 감싸서 반환
return BaseResponse[User](data=user)
@app.get("/users", response_model=BaseResponse[List[User]])
def get_all_users():
all_users = list(users_db.values())
# 성공 시 리스트 데이터를 BaseResponse로 감싸서 반환
return BaseResponse[List[User]](data=all_users)
BaseResponse 모델 상세Generic[T]BaseResponse 모델이 다양한 타입의 데이터를 담을 수 있도록 제네릭(Generic)을 사용T는 '타입 변수'로, User, List[User], str 등 어떤 타입이든 올 수 있음BaseResponse를 재사용 가능success: bool = TrueTrue로 설정하여 성공적인 경우를 표준으로 함timestamp: str = Field(default_factory=...)default_factory를 사용하여 BaseResponse 모델 객체가 생성될 때마다 datetime.now().isoformat() 함수가 동적으로 호출되도록 함version: str = "1.0"data: Optional[T] = NoneT를 사용하여 단일 객체(User), 객체 리스트(List[User]) 등 다양한 형태의 데이터를 담을 수 있음Optional과 기본값 None을 사용하여 데이터가 없는 경우(실패 등)도 표현 가능BaseResponse 활용response_model=BaseResponse[User]data 필드에 User 모델이 들어갈 것임을 명시response_model=BaseResponse[List[User]]data 필드에 User 모델의 리스트가 들어갈 것임을 명시return BaseResponse[User](data=user)user 객체를 BaseResponse로 감싸서 반환response_model 정의에 따라 자동으로 JSON 형식으로 변환해 줌Response 객체 직접 생성BaseResponse 모델의 success를 False로 설정하고 data를 None으로 하여 실패 응답 본문을 직접 생성Response 객체를 사용하여 상태 코드(404), 미디어 타입 등을 명시적으로 제어하여 반환product 도메인이랑 권한을 정의
1. 상품 상태 : active / inactive / out_of_stock
class ProductStatus
2. 상품의 생성/수정에 사용할 Product
: name : 1~100자
description: str
price : >0
category : str
status : active
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, ValidationError
# 1. 상품 상태(ProductStatus)를 Enum으로 정의
class ProductStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
OUT_OF_STOCK = "out_of_stock"
# 2. 상품(Product) Pydantic 모델 정의
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100)
description: Optional[str] = None
price: float = Field(gt=0)
category: str
status: ProductStatus = ProductStatus.ACTIVE
# --- 모델 동작 테스트 ---
if __name__ == "__main__":
print("--- 1. 정상 데이터 테스트 (상태 기본값 사용) ---")
try:
product1 = Product(
name="노트북",
price=1500000.0,
category="전자기기"
)
print("성공: 유효한 상품 데이터입니다.")
print(product1.model_dump_json(indent=2))
except ValidationError as e:
print(f"실패: {e}")
print("\n--- 2. 정상 데이터 테스트 (상태 명시적 지정) ---")
try:
product2 = Product(
name="키보드",
price=85000.0,
category="컴퓨터 주변기기",
status=ProductStatus.OUT_OF_STOCK # Enum 멤버로 상태 지정
)
print("성공: 유효한 상품 데이터입니다.")
print(product2.model_dump_json(indent=2))
except ValidationError as e:
print(f"실패: {e}")
print("\n--- 3. 유효성 검사 실패 테스트 (가격이 0 이하) ---")
try:
invalid_product = Product(
name="마우스",
price=0, # price는 0보다 커야 함
category="컴퓨터 주변기기"
)
except ValidationError as e:
print(f"성공 (에러 발생 예상):\n{e}")
print("\n--- 4. 유효성 검사 실패 테스트 (잘못된 상태 값) ---")
try:
invalid_status_product = Product(
name="모니터",
price=300000,
category="전자기기",
status="sold_out" # 허용되지 않은 상태 값
)
except ValidationError as e:
print(f"성공 (에러 발생 예상):\n{e}")
ProductStatus Enum 정의active, inactive, out_of_stock)로만 제한하기 위해 사용enum 라이브러리의 Enum을 상속받아 클래스 생성str을 함께 상속하여, Enum 멤버들이 JSON으로 변환될 때 "active"와 같은 일반 문자열로 처리되도록 함Product 모델 정의name: str = Field(min_length=1, max_length=100)name 필드는 문자열 타입이며, Field를 통해 1자 이상 100자 이하의 길이 제약조건을 부여description: Optional[str] = Nonedescription 필드는 선택 사항으로, 값이 없을 수도 있음을 Optional로 명시price: float = Field(gt=0)price 필드는 부동소수점 타입이며, Field의 gt(greater than) 옵션을 사용하여 값이 항상 0보다 커야 한다는 규칙을 적용status: ProductStatus = ProductStatus.ACTIVEstatus 필드의 타입을 위에서 정의한 ProductStatus Enum으로 지정ProductStatus에 정의된 세 가지 값 외에는 받을 수 없도록 강제ProductStatus.ACTIVE로 설정하여, status 값이 주어지지 않을 경우 자동으로 'active' 상태가 되도록 함상품 조회 API를 만들어주세요
GET /products/{product_id}
1. product_id : >=1
2. user_role : UserRole.user
3. 존재하지 않으면 : PRODUCT_NOT~~
class ErrorResponse(BaseResponse):
success : bool = False
error : str
error_code : int
details : Optional[Dict] = None
4. 역할별로 product 정보 노출을 다르게 해주세요
5. 성공하면 SuccessResponse(message = "", data = product)
from fastapi import FastAPI, HTTPException, Path, Header, Response, status
from pydantic import BaseModel, Field
from enum import Enum
from typing import Optional, Dict, Type, Any, Generic, TypeVar
app = FastAPI()
# --- Enum 및 기본 모델 정의 ---
class UserRole(str, Enum):
USER = "user"
ADMIN = "admin"
class ProductStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
OUT_OF_STOCK = "out_of_stock"
# --- 공통 응답 모델 정의 ---
T = TypeVar('T')
class BaseResponse(BaseModel):
success: bool
class ErrorResponse(BaseResponse):
success: bool = False
error: str
error_code: int
details: Optional[Dict[str, Any]] = None
class SuccessResponse(BaseResponse, Generic[T]):
success: bool = True
message: str = "성공"
data: T
# --- 상품 도메인 모델 정의 ---
# DB에 저장되는 전체 정보 모델
class ProductDB(BaseModel):
id: int
name: str
description: str
price: float
status: ProductStatus
# 일반 사용자에게 노출될 정보 모델
class ProductUserView(BaseModel):
name: str
price: float
status: ProductStatus
# 관리자에게 노출될 정보 모델 (모든 정보)
class ProductAdminView(ProductDB):
pass
# --- 인메모리 데이터베이스 ---
products_db: Dict[int, ProductDB] = {
1: ProductDB(id=1, name="노트북", description="고성능 노트북입니다.", price=1500000, status=ProductStatus.ACTIVE),
2: ProductDB(id=2, name="키보드", description="기계식 키보드입니다.", price=120000, status=ProductStatus.OUT_OF_STOCK),
3: ProductDB(id=3, name="마우스", description="게이밍 마우스입니다.", price=50000, status=ProductStatus.INACTIVE),
}
# --- API 엔드포인트 구현 ---
@app.get("/products/{product_id}", response_model=SuccessResponse[Any])
def get_product_by_id(
product_id: int = Path(..., ge=1, description="상품 ID는 1 이상이어야 합니다."),
user_role: UserRole = Header(UserRole.USER, description="사용자 역할 (user 또는 admin)")
):
product = products_db.get(product_id)
# 3. 존재하지 않을 경우 ErrorResponse 반환
if not product:
error_content = ErrorResponse(
error="PRODUCT_NOT_FOUND",
error_code=404,
details={"product_id": product_id}
).model_dump_json()
return Response(content=error_content, status_code=status.HTTP_404_NOT_FOUND, media_type="application/json")
# 4. 역할별로 다른 데이터 모델(View) 선택
ProductView: Type[BaseModel]
if user_role == UserRole.ADMIN:
ProductView = ProductAdminView
else: # user_role == UserRole.USER
ProductView = ProductUserView
# 선택된 View 모델로 데이터 변환
product_data = ProductView.model_validate(product)
# 5. 성공 시 SuccessResponse 반환
return SuccessResponse(data=product_data)
BaseResponse, ErrorResponse, SuccessResponse)ErrorResponse: 실패 응답을 위한 전용 모델. success는 항상 False이며, 에러 메시지, 코드 등 상세 정보를 포함SuccessResponse: 성공 응답을 위한 전용 모델. success는 항상 True이며, 제네릭(Generic[T])을 사용하여 data 필드에 다양한 타입의 데이터를 담을 수 있도록 설계ProductUserView, ProductAdminView)ProductUserView: 일반 사용자에게는 name, price, status 등 민감하지 않은 정보만 노출ProductAdminView: 관리자에게는 id, description을 포함한 모든 정보를 노출GET /products/{product_id})product_id: int = Path(..., ge=1, ...)Path를 사용하여 경로 매개변수 product_id가 1 이상(ge=1)이어야 한다는 제약조건을 추가user_role: UserRole = Header(UserRole.USER, ...)user-role 값을 읽어 user_role 변수에 할당UserRole.USER를 사용products_db에서 상품을 찾지 못하면, ErrorResponse 모델을 사용하여 표준화된 에러 메시지를 생성하고 404 Not Found 상태 코드로 응답user_role 값에 따라 ProductView 변수에 ProductAdminView 또는 ProductUserView 모델 클래스를 할당ProductView.model_validate(product)를 통해 DB에서 가져온 전체 데이터를 해당 역할에 맞는 모델로 변환 (불필요한 필드는 자동으로 걸러짐)product_data를 SuccessResponse로 감싸서 반환streamlit 에서 파일을 업로드하면
fastapi를 통해서 서버에 파일이 저장되는
어플리케이션을 만들어주세요
import shutil
from pathlib import Path
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
# 업로드된 파일을 저장할 디렉토리
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True) # 서버 시작 시 디렉토리가 없으면 생성
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 저장될 파일의 전체 경로
dest_path = UPLOAD_DIR / file.filename
try:
# 파일을 디스크에 저장
with dest_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
finally:
# 파일 핸들러 닫기
file.file.close()
return {"filename": file.filename, "saved_path": str(dest_path)}
import streamlit as st
import requests
# FastAPI 백엔드 서버 주소
FASTAPI_URL = "http://127.0.0.1:8000/uploadfile/"
st.title("Streamlit에서 FastAPI로 파일 업로드")
# 파일 업로더 위젯
uploaded_file = st.file_uploader("업로드할 파일을 선택하세요.")
if uploaded_file is not None:
# 업로드 버튼
if st.button("서버로 파일 업로드"):
with st.spinner("파일을 업로드 중입니다..."):
try:
# FastAPI 서버로 전송할 파일 데이터 준비
# 'file'이라는 키는 FastAPI의 매개변수 이름(file: UploadFile)과 일치해야 함
files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
# POST 요청 보내기
response = requests.post(FASTAPI_URL, files=files)
if response.status_code == 200:
st.success("파일이 성공적으로 업로드되었습니다.")
st.json(response.json())
else:
st.error(f"파일 업로드 실패. 상태 코드: {response.status_code}")
st.text(response.text)
except requests.exceptions.RequestException as e:
st.error(f"서버에 연결할 수 없습니다: {e}")
Path("uploads")pathlib을 사용하여 파일 경로를 객체로 관리UPLOAD_DIR.mkdir(exist_ok=True): 서버 실행 시 uploads 디렉토리가 없으면 자동으로 생성@app.post("/uploadfile/")/uploadfile/ 경로로 오는 POST 요청을 처리하는 엔드포인트 정의file: UploadFile = File(...)UploadFile 객체로 받음python-multipart 라이브러리가 이 기능을 위해 필요shutil.copyfileobj(file.file, buffer)file.file)의 내용을 서버의 디스크 파일(buffer)로 효율적으로 복사하여 저장st.file_uploader(...)requests.post(FASTAPI_URL, files=files)requests 라이브러리를 사용하여 FastAPI 서버에 POST 요청을 보냄files = {"file": ...}"file")는 FastAPI 엔드포인트 함수의 매개변수 이름(file)과 반드시 일치해야 함(파일명, 파일내용, 파일타입) 형식의 튜플로 구성st.spinner, st.success, st.errorFastAPI와 SQLModel을 사용하여 블로그 게시물을 관리하는 전체 CRUD API를 구현하세요.
1. 데이터베이스 모델 (Post):
* id: int, 기본 키(PK), 자동 증가
* title: str, 3자 이상 50자 이하
* content: str
* author: str, 2자 이상 20자 이하
* created_at: datetime, DB에 추가될 때 시간 자동 생성
2. 입력/출력 모델 분리:
* PostCreate: title, content, author 필드를 가짐 (게시물 생성용)
* PostUpdate: title, content 필드를 가짐 (게시물 수정용, 모두 선택 사항)
* PostRead: id, title, content, author, created_at 필드를 모두 가짐 (응답용)
3. API 엔드포인트 구현:
* POST /posts: 새 게시물 생성. (성공 시 201 Created)
* GET /posts: 모든 게시물 목록 조회.
* GET /posts/{post_id}: 특정 ID의 게시물 조회. (없으면 404 Not Found)
* PUT /posts/{post_id}: 특정 ID의 게시물 수정. (title, content만 수정)
* DELETE /posts/{post_id}: 특정 ID의 게시물 삭제. (성공 시 204 No Content)
4. 기타:
* lifespan을 사용하여 앱 시작 시 DB와 테이블을 생성하세요.
* Dependency Injection을 통해 DB 세션을 관리하세요.
# 필요 라이브러리: pip install fastapi "uvicorn[standard]" sqlmodel
from contextlib import asynccontextmanager
from datetime import datetime
from typing import List, Optional
from fastapi import FastAPI, Depends, HTTPException, Response, status
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- DB 설정 ---
DATABASE_URL = "sqlite:///blog.db"
# 실제 서비스에서는 check_same_thread는 False로 두지 않는 것이 좋음 (예제용)
engine = create_engine(DATABASE_URL, echo=True, connect_args={"check_same_thread": False})
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
# --- FastAPI Lifespan 설정 ---
@asynccontextmanager
async def lifespan(app: FastAPI):
print("애플리케이션 시작: DB 테이블 생성")
create_db_and_tables()
yield
print("애플리케이션 종료")
app = FastAPI(lifespan=lifespan)
def get_session():
with Session(engine) as session:
yield session
# --- 모델 정의 ---
# 2. 입력/출력 모델
class PostCreate(SQLModel):
title: str = Field(min_length=3, max_length=50)
content: str
author: str = Field(min_length=2, max_length=20)
class PostUpdate(SQLModel):
title: Optional[str] = Field(None, min_length=3, max_length=50)
content: Optional[str] = None
# 1. 데이터베이스 모델
class Post(PostCreate, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
# 응답 모델 (ID와 생성 시간 포함)
class PostRead(PostCreate):
id: int
created_at: datetime
# --- API 엔드포인트 구현 ---
# 3-1. CREATE
@app.post("/posts", response_model=PostRead, status_code=status.HTTP_201_CREATED)
def create_post(post_create: PostCreate, session: Session = Depends(get_session)):
db_post = Post.model_validate(post_create)
session.add(db_post)
session.commit()
session.refresh(db_post)
return db_post
# 3-2. READ (All)
@app.get("/posts", response_model=List[PostRead])
def read_posts(session: Session = Depends(get_session)):
posts = session.exec(select(Post)).all()
return posts
# 3-3. READ (One)
@app.get("/posts/{post_id}", response_model=PostRead)
def read_post(post_id: int, session: Session = Depends(get_session)):
db_post = session.get(Post, post_id)
if not db_post:
raise HTTPException(status_code=404, detail="게시물을 찾을 수 없습니다.")
return db_post
# 3-4. UPDATE
@app.put("/posts/{post_id}", response_model=PostRead)
def update_post(post_id: int, post_update: PostUpdate, session: Session = Depends(get_session)):
db_post = session.get(Post, post_id)
if not db_post:
raise HTTPException(status_code=404, detail="게시물을 찾을 수 없습니다.")
update_data = post_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_post, key, value)
session.add(db_post)
session.commit()
session.refresh(db_post)
return db_post
# 3-5. DELETE
@app.delete("/posts/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_post(post_id: int, session: Session = Depends(get_session)):
db_post = session.get(Post, post_id)
if not db_post:
raise HTTPException(status_code=404, detail="게시물을 찾을 수 없습니다.")
session.delete(db_post)
session.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)
SQLModel 및 Pydantic)Post: DB 테이블과 직접 매핑되는 모델, id와 created_at 같이 서버에서 관리하는 필드를 포함PostCreate: 게시물 생성 시 클라이언트로부터 받을 데이터의 규칙을 정의 (id 등 불필요한 필드 제외)PostUpdate: 게시물 수정 시 받을 데이터 규칙 정의. 모든 필드를 Optional로 만들어 부분 수정이 가능하도록 함PostRead: 클라이언트에게 응답을 보낼 때 사용할 모델. DB에 저장된 모든 정보를 포함lifespan 설정lifespan: FastAPI 앱이 시작될 때 create_db_and_tables 함수를 실행하여 DB 테이블이 존재하지 않으면 자동으로 생성get_session: API 함수가 호출될 때마다 독립적인 DB 세션을 생성하고, 함수 실행이 끝나면 세션을 닫아주는 의존성 주입 함수POST /posts (생성)PostCreate 모델로 데이터를 받아 유효성 검사 후 DB에 저장201 Created 상태 코드 반환GET /posts/{post_id} (조회)session.get()으로 특정 ID의 게시물을 효율적으로 조회404 Not Found 에러 발생PUT /posts/{post_id} (수정)PostUpdate 모델로 수정할 데이터만 받음model_dump(exclude_unset=True)를 사용하여 클라이언트가 보낸 필드만 업데이트DELETE /posts/{post_id} (삭제)session.get()으로 삭제할 게시물을 먼저 조회session.delete()로 삭제 후 session.commit()으로 DB에 반영204 No Content 상태 코드를 반환FastAPI, Streamlit, SQLModel을 사용하여 가상 주식 데이터를 저장하고 조회하는 웹 애플리케이션을 구현하세요.
1. 데이터베이스 모델 (StockData):
* id: int, 기본 키(PK), 자동 증가
* ticker: str, 주식 티커 (예: "하이닉스")
* date: datetime, 날짜
* close: float, 종가
* volume: int, 거래량
2. FastAPI 백엔드 (stock_api.py):
* 앱 시작 시(lifespan) DB와 테이블을 생성하고, "하이닉스"와 "삼성전자"에 대한 가상의 30일치 주가 데이터를 자동으로 생성하여 DB에 삽입하세요.
* GET /stocks/{ticker} 엔드포인트를 구현하세요.
* 경로 매개변수로 받은 ticker에 해당하는 모든 주가 데이터를 DB에서 조회합니다.
* 데이터는 날짜순으로 정렬하여 반환합니다.
* 해당 ticker의 데이터가 없으면 빈 리스트를 반환합니다.
3. Streamlit 프론트엔드 (dashboard.py):
* 사용자가 주식 티커를 입력할 수 있는 텍스트 입력창을 제공하세요.
* '차트 보기' 버튼을 누르면 FastAPI 백엔드에 데이터를 요청합니다.
* API로부터 받은 데이터를 아래와 같이 시각화하세요.
* 꺾은선 그래프: 시간의 흐름에 따른 종가(close) 변화를 표시합니다.
* 데이터 표: 최근 데이터가 가장 위에 오도록 정렬하여 date, close, volume을 표시합니다.
* 데이터가 없을 경우 "데이터를 찾을 수 없습니다." 메시지를 표시합니다.
import random
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from typing import List
from fastapi import FastAPI, Depends
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- DB 설정 ---
DATABASE_URL = "sqlite:///stock.db"
engine = create_engine(DATABASE_URL, echo=False, connect_args={"check_same_thread": False})
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
# --- 모델 정의 ---
class StockData(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
ticker: str = Field(index=True)
date: datetime
close: float
volume: int
# --- FastAPI Lifespan 및 앱 초기화 ---
@asynccontextmanager
async def lifespan(app: FastAPI):
print("DB 테이블 생성 및 초기 데이터 삽입")
create_db_and_tables()
# DB에 데이터가 비어있을 때만 샘플 데이터 생성
with Session(engine) as session:
if not session.exec(select(StockData)).first():
tickers = ["하이닉스", "삼성전자"]
today = datetime.now()
for ticker in tickers:
price = random.uniform(80000, 150000)
for i in range(30):
date = today - timedelta(days=i)
price *= (1 + random.uniform(-0.05, 0.05)) # 가격 변동 시뮬레이션
volume = random.randint(100, 1000)
sample_data = StockData(ticker=ticker, date=date, close=round(price, 2), volume=volume)
session.add(sample_data)
session.commit()
yield
print("애플리케이션 종료")
app = FastAPI(lifespan=lifespan)
# --- API 엔드포인트 ---
@app.get("/stocks/{ticker}", response_model=List[StockData])
def get_stock_data(ticker: str, session: Session = Depends(get_session)):
statement = select(StockData).where(StockData.ticker == ticker).order_by(StockData.date)
results = session.exec(statement).all()
return results
import streamlit as st
import requests
import pandas as pd
# FastAPI 백엔드 서버 주소
API_URL = "http://127.0.0.1:8000/stocks/"
st.title("가상 주식 차트")
# 사용자 입력
ticker = st.text_input("주식 티커를 입력하세요:", "하이닉스")
if st.button("차트 보기"):
if not ticker:
st.warning("티커를 입력해주세요.")
else:
with st.spinner(f"'{ticker}' 주가 데이터를 불러오는 중..."):
try:
# API 호출
response = requests.get(API_URL + ticker)
response.raise_for_status() # 200번대 상태 코드가 아니면 에러 발생
data = response.json()
if not data:
st.warning("해당 티커의 데이터를 찾을 수 없습니다.")
else:
# Pandas DataFrame으로 변환
df = pd.DataFrame(data)
df['date'] = pd.to_datetime(df['date'])
df = df.set_index('date')
# 차트 및 데이터 표시
st.subheader(f"{ticker} 주가")
st.line_chart(df['close'])
st.subheader("최근 데이터")
st.dataframe(df[['close', 'volume']].sort_index(ascending=False))
except requests.exceptions.RequestException as e:
st.error(f"API 서버에 연결할 수 없습니다: {e}")
lifespanGET /stocks/{ticker}ticker를 기준으로 DB에서 where 절을 이용해 데이터를 필터링order_by로 날짜순 정렬 후, 조회된 모든 데이터를 리스트 형태로 반환requests.getpandas.DataFramest.line_chartclose)을 꺾은선 그래프로 시각화st.dataframe