From ab28c46ab05fe55eaabd4f33d962bd9686f77a77 Mon Sep 17 00:00:00 2001 From: JinWook Date: Tue, 13 Jan 2026 09:34:07 +0900 Subject: [PATCH 01/12] first pratice --- main.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index eb1f59f..370a220 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,10 @@ -def main(): - print("Hello from fastapi-community!") +from fastapi import FastAPI +app = FastAPI() -if __name__ == "__main__": - main() +@app.get("/itmes/") +async def read_items(q: str | None = None): + results = {"itmes":[{{"item_id":"Foo"},{"item_id":"Bar"}}]} + if q: + results.update({"q":q}) + return results \ No newline at end of file From bdc2ba7048b0bc5bcc253a225bf3253c0831aa19 Mon Sep 17 00:00:00 2001 From: JinWook Date: Tue, 13 Jan 2026 19:01:13 +0900 Subject: [PATCH 02/12] def list_deals --- app/routers/deals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/deals.py b/app/routers/deals.py index 0f3c170..9502303 100644 --- a/app/routers/deals.py +++ b/app/routers/deals.py @@ -15,7 +15,7 @@ ) async def list_deals(db: Session = Depends(get_db)): # TODO - return [] + return db.query(Deal).all() @router.post( "/", From fd5afbc53ce5885ad461d7812bcfe17cceb0cb73 Mon Sep 17 00:00:00 2001 From: JinWook Date: Tue, 13 Jan 2026 20:34:17 +0900 Subject: [PATCH 03/12] In deals.py, Before pytest --- app/routers/deals.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/routers/deals.py b/app/routers/deals.py index 9502303..36ed091 100644 --- a/app/routers/deals.py +++ b/app/routers/deals.py @@ -15,7 +15,7 @@ ) async def list_deals(db: Session = Depends(get_db)): # TODO - return db.query(Deal).all() + return db.query(Deal).all() # db.query()를 통해 Deal에 있는 모든 데이터들을 가져온다. @router.post( "/", @@ -25,7 +25,12 @@ async def list_deals(db: Session = Depends(get_db)): ) async def create_deal(deal: DealCreate, db: Session = Depends(get_db)): # TODO - raise NotImplementedError("TODO") + new_deal = Deal(**deal.dict()) # deal를 처리할 수 있는 형태로. + db.add(new_deal) # DB 세션에 추가. + db.commit() # 변경사항 저장. + db.refresh(new_deal) # 갱신 + + return new_deal @router.get( "/{deal_id}", @@ -34,7 +39,13 @@ async def create_deal(deal: DealCreate, db: Session = Depends(get_db)): ) async def get_deal(deal_id: int, db: Session = Depends(get_db)): # TODO - raise NotImplementedError("TODO") + deal = db.query(Deal).filter(Deal.id == deal_id).first() # deal_id인 멤버를 DB에서 찾기. + + if deal is None: + raise HTTPException(status_code=404, detail="Deal not found") + + return deal + @router.delete( "/{deal_id}", @@ -43,7 +54,14 @@ async def get_deal(deal_id: int, db: Session = Depends(get_db)): ) async def delete_deal(deal_id: int, db: Session = Depends(get_db)): # TODO - raise NotImplementedError("TODO") + + deal = db.query(Deal).filter(Deal.id == deal_id).first() + + if deal is None: + raise HTTPException(status_code=404, detail="Deal not found") + + db.delete(deal) + db.commit() # Special endpoint: Recommend Deal class DealRecommendInput(BaseModel): From 49f0956a4f1b7a87384e483a696001c26fabce79 Mon Sep 17 00:00:00 2001 From: JinWook Date: Tue, 13 Jan 2026 20:47:33 +0900 Subject: [PATCH 04/12] 01.13. 8:47, I need a test Db --- tests/test_deals.py | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_deals.py diff --git a/tests/test_deals.py b/tests/test_deals.py new file mode 100644 index 0000000..3077de5 --- /dev/null +++ b/tests/test_deals.py @@ -0,0 +1,54 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.db import Base, engine +from app.models.deal import Deal +from datetime import datetime, timedelta + +# Create tables for test +Base.metadata.create_all(bind=engine) + +client = TestClient(app) + +def test_deal_lifecycle(): + # 1. Create a fresh deal (POST) + # create_deal expects: event_id, discount_rate, starts_at, ends_at + payload = { + "event_id": 999, # 임의의 ID + "discount_rate": 25, + "starts_at": datetime.utcnow().isoformat(), + "ends_at": (datetime.utcnow() + timedelta(days=7)).isoformat() + } + + response = client.post("/deals/", json=payload) + assert response.status_code == 201 + data = response.json() + + # 생성된 데이터 검증 + deal_id = data["id"] + assert data["discount_rate"] == 25 + assert data["event_id"] == 999 + + # 2. List all deals (GET) should contain the new deal + response = client.get("/deals/") + assert response.status_code == 200 + deals = response.json() + # 방금 만든 deal_id가 리스트 안에 있는지 확인 + found = False + for d in deals: + if d["id"] == deal_id: + found = True + break + assert found is True + + # 3. Get specific deal (GET /{id}) + response = client.get(f"/deals/{deal_id}") + assert response.status_code == 200 + assert response.json()["id"] == deal_id + + # 4. Delete the deal (DELETE) + response = client.delete(f"/deals/{deal_id}") + assert response.status_code == 204 + + # 5. Verify it's gone (GET /{id} -> 404) + response = client.get(f"/deals/{deal_id}") + assert response.status_code == 404 From b319e7e2aa38806b1847aa117abc5e1e49db17a0 Mon Sep 17 00:00:00 2001 From: JinWook Date: Wed, 14 Jan 2026 09:11:15 +0900 Subject: [PATCH 05/12] def list_docents --- app/routers/docents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routers/docents.py b/app/routers/docents.py index 382db19..a078b9f 100644 --- a/app/routers/docents.py +++ b/app/routers/docents.py @@ -14,7 +14,8 @@ ) async def list_docents(db: Session = Depends(get_db)): # TODO - return [] + + return db.query(Docent).all() @router.post( "/", From b4b1292f87c5f9d3e6b8c6fbf37af9ea4dc2914f Mon Sep 17 00:00:00 2001 From: JinWook Date: Wed, 14 Jan 2026 09:14:32 +0900 Subject: [PATCH 06/12] def create_docents --- app/routers/docents.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/routers/docents.py b/app/routers/docents.py index a078b9f..57254e7 100644 --- a/app/routers/docents.py +++ b/app/routers/docents.py @@ -25,6 +25,12 @@ async def list_docents(db: Session = Depends(get_db)): ) async def create_docent(docent: DocentCreate, db: Session = Depends(get_db)): # TODO + + new_decent = Docent(**docent.dict) + db.add(new_decent) + db.commit() + db.refresh(new_decent) + raise NotImplementedError("TODO") @router.get( From 28bcebceaa17e0dcdd8ff25debf80337f6c3c726 Mon Sep 17 00:00:00 2001 From: JinWook Date: Wed, 14 Jan 2026 09:21:34 +0900 Subject: [PATCH 07/12] def get_docent and revise create_docents --- app/routers/docents.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/routers/docents.py b/app/routers/docents.py index 57254e7..23e92b7 100644 --- a/app/routers/docents.py +++ b/app/routers/docents.py @@ -26,12 +26,12 @@ async def list_docents(db: Session = Depends(get_db)): async def create_docent(docent: DocentCreate, db: Session = Depends(get_db)): # TODO - new_decent = Docent(**docent.dict) - db.add(new_decent) + new_docent = Docent(**docent.dict()) + db.add(new_docent) db.commit() - db.refresh(new_decent) + db.refresh(new_docent) - raise NotImplementedError("TODO") + return new_docent @router.get( "/{docent_id}", @@ -40,7 +40,13 @@ async def create_docent(docent: DocentCreate, db: Session = Depends(get_db)): ) async def get_docent(docent_id: int, db: Session = Depends(get_db)): # TODO - raise NotImplementedError("TODO") + + docent = db.query(Docent).filter(Docent.id == docent_id).first() + + if docent is None: + raise HTTPException(status_code=404, detail="Deal Not Found") + + return docent @router.delete( "/{docent_id}", From e31e800a36753cdc39472b928d07fc386d025f49 Mon Sep 17 00:00:00 2001 From: JinWook Date: Wed, 14 Jan 2026 09:27:10 +0900 Subject: [PATCH 08/12] def delete_docent --- app/routers/docents.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/routers/docents.py b/app/routers/docents.py index 23e92b7..e68b24d 100644 --- a/app/routers/docents.py +++ b/app/routers/docents.py @@ -44,7 +44,7 @@ async def get_docent(docent_id: int, db: Session = Depends(get_db)): docent = db.query(Docent).filter(Docent.id == docent_id).first() if docent is None: - raise HTTPException(status_code=404, detail="Deal Not Found") + raise HTTPException(status_code=404, detail="Docent Not Found") return docent @@ -55,7 +55,14 @@ async def get_docent(docent_id: int, db: Session = Depends(get_db)): ) async def delete_docent(docent_id: int, db: Session = Depends(get_db)): # TODO - raise NotImplementedError("TODO") + + docent = db.query(Docent).filter(Docent.id == docent_id).first() + + if docent is None: + raise HTTPException(status_code=404,detail="Docent Not Found") + + db.delete(docent) + db.commit() # Special endpoint: Generate Docent Content class DocentGenerateInput(BaseModel): From 245d82a3aa40567de3c175984a246ae7dc9bcd10 Mon Sep 17 00:00:00 2001 From: JinWook Date: Wed, 14 Jan 2026 10:05:48 +0900 Subject: [PATCH 09/12] Retry with tests folder --- tests/conftest.py | 50 +++++++++++++++++++++++------------------- tests/test_deals.py | 19 ++++------------ tests/test_docents.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 38 deletions(-) create mode 100644 tests/test_docents.py diff --git a/tests/conftest.py b/tests/conftest.py index 9d338eb..94b7f4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,39 +1,43 @@ import pytest -import os - -# app.db import 전에 환경변수 설정 (ImportError 방지) -# CI 환경에서 app/db.py가 DATABASE_URL을 요구하므로 dummy 값 설정 -os.environ.setdefault("DATABASE_URL", "sqlite:///./test_dummy.db") - from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from app.db import Base, get_db - -# Mock creation of tables in production DB (since we don't have ODBC driver in test env) -from unittest.mock import MagicMock -Base.metadata.create_all = MagicMock() +from sqlalchemy.pool import StaticPool +from fastapi.testclient import TestClient from app.main import app +from app.db import Base, get_db +# Ensure all models are imported so Base knows about them +from app.models.deal import Deal +from app.models.docent import Docent + +# Use an in-memory SQLite database for testing, with StaticPool +# StaticPool is important: it lets multiple threads share the same in-memory DB connection. +SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" -# 테스트 DB (in-memory SQLite, 또는 별도 테스트 Azure SQL) -# 로컬 개발 환경에서 빠르게 돌리기 위해 SQLite 사용 -SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" engine = create_engine( - SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool ) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base.metadata.create_all(bind=engine) -def override_get_db(): +@pytest.fixture(name="session") +def session_fixture(): + # Create tables + Base.metadata.create_all(bind=engine) + db = TestingSessionLocal() try: - db = TestingSessionLocal() yield db finally: db.close() + # Drop tables after test is done (optional, considering we use a fresh DB or memory) + Base.metadata.drop_all(bind=engine) -app.dependency_overrides[get_db] = override_get_db +@pytest.fixture(name="client") +def client_fixture(session): + def override_get_db(): + yield session -@pytest.fixture -def client(): - from fastapi.testclient import TestClient - return TestClient(app) + app.dependency_overrides[get_db] = override_get_db + yield TestClient(app) + app.dependency_overrides.clear() diff --git a/tests/test_deals.py b/tests/test_deals.py index 3077de5..3f2e8d1 100644 --- a/tests/test_deals.py +++ b/tests/test_deals.py @@ -1,19 +1,9 @@ -from fastapi.testclient import TestClient -from app.main import app -from app.db import Base, engine -from app.models.deal import Deal from datetime import datetime, timedelta -# Create tables for test -Base.metadata.create_all(bind=engine) - -client = TestClient(app) - -def test_deal_lifecycle(): +def test_deal_lifecycle(client): # 1. Create a fresh deal (POST) - # create_deal expects: event_id, discount_rate, starts_at, ends_at payload = { - "event_id": 999, # 임의의 ID + "event_id": 999, "discount_rate": 25, "starts_at": datetime.utcnow().isoformat(), "ends_at": (datetime.utcnow() + timedelta(days=7)).isoformat() @@ -23,16 +13,15 @@ def test_deal_lifecycle(): assert response.status_code == 201 data = response.json() - # 생성된 데이터 검증 deal_id = data["id"] assert data["discount_rate"] == 25 assert data["event_id"] == 999 - # 2. List all deals (GET) should contain the new deal + # 2. List all deals (GET) response = client.get("/deals/") assert response.status_code == 200 deals = response.json() - # 방금 만든 deal_id가 리스트 안에 있는지 확인 + found = False for d in deals: if d["id"] == deal_id: diff --git a/tests/test_docents.py b/tests/test_docents.py new file mode 100644 index 0000000..38895e2 --- /dev/null +++ b/tests/test_docents.py @@ -0,0 +1,51 @@ +def test_docent_lifecycle(client): + # 1. Create a fresh docent (POST) + payload = { + "place_id": 101, + "tone": "serene", + "content": "이곳은 아주 조용하고 평화로운 공간입니다." + } + + response = client.post("/docents/", json=payload) + assert response.status_code == 201 + data = response.json() + + docent_id = data["id"] + assert data["tone"] == "serene" + assert data["place_id"] == 101 + + # 2. List all docents (GET) + response = client.get("/docents/") + assert response.status_code == 200 + docents = response.json() + + found = False + for d in docents: + if d["id"] == docent_id: + found = True + break + assert found is True + + # 3. Get specific docent (GET /{id}) + response = client.get(f"/docents/{docent_id}") + assert response.status_code == 200 + assert response.json()["id"] == docent_id + + # 4. Delete the docent (DELETE) + response = client.delete(f"/docents/{docent_id}") + assert response.status_code == 204 + + # 5. Verify it's gone (GET /{id} -> 404) + response = client.get(f"/docents/{docent_id}") + assert response.status_code == 404 + +def test_generate_docent(client): + # Special endpoint test + payload = { + "place_id": 500, + "tone": "energetic" + } + response = client.post("/docents/generate", json=payload) + assert response.status_code == 200 + data = response.json() + assert "energetic" in data["content"] From de27b58c73dbbf641e5ceea6275b06018b00eaae Mon Sep 17 00:00:00 2001 From: JinWook Date: Thu, 15 Jan 2026 20:31:57 +0900 Subject: [PATCH 10/12] update confest.test --- tests/conftest.py | 66 +++++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 94b7f4e..03c2e2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,43 +1,59 @@ import pytest +import os +import sys + +# 1. 환경변수 설정 (app.db import 전) +# 실제 DB 연결을 시도하지 않도록 Dummy URL 설정 +os.environ.setdefault("DATABASE_URL", "sqlite:///./test_dummy.db") + from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool -from fastapi.testclient import TestClient -from app.main import app +# 2. Base 및 모델 Import from app.db import Base, get_db -# Ensure all models are imported so Base knows about them -from app.models.deal import Deal -from app.models.docent import Docent +# ★중요★: 테이블을 생성하려면 Base가 모델들을 알고 있어야 합니다. +# 만약 에러가 계속된다면 아래처럼 모델들을 여기서 import 해주세요. +# from app.models.deals import Deal +# from app.models.docents import Docent + +# ------------------------------------------------------------------ +# [수정됨] MagicMock 제거 +# Base.metadata.create_all 메서드를 Mocking 해버리면, +# 아래에서 create_all(bind=engine)을 호출해도 아무 일도 일어나지 않습니다. +# ------------------------------------------------------------------ -# Use an in-memory SQLite database for testing, with StaticPool -# StaticPool is important: it lets multiple threads share the same in-memory DB connection. -SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" +from app.main import app + +# 3. 테스트 DB 설정 (SQLite 사용) +SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" +# 메모리 DB를 원하면 "sqlite:///:memory:" 사용 가능 engine = create_engine( - SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False}, - poolclass=StaticPool + SQLALCHEMY_TEST_DATABASE_URL, + connect_args={"check_same_thread": False} ) + TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -@pytest.fixture(name="session") -def session_fixture(): - # Create tables - Base.metadata.create_all(bind=engine) +# 4. 테스트 시작 전 테이블 생성 +# MagicMock을 지웠으므로 이제 이 코드가 정상 작동하여 'deals', 'docents' 테이블을 만듭니다. +Base.metadata.create_all(bind=engine) + +def override_get_db(): db = TestingSessionLocal() try: yield db finally: db.close() - # Drop tables after test is done (optional, considering we use a fresh DB or memory) - Base.metadata.drop_all(bind=engine) -@pytest.fixture(name="client") -def client_fixture(session): - def override_get_db(): - yield session +app.dependency_overrides[get_db] = override_get_db - app.dependency_overrides[get_db] = override_get_db - yield TestClient(app) - app.dependency_overrides.clear() +@pytest.fixture +def client(): + from fastapi.testclient import TestClient + + # (선택 사항) 테스트 할 때마다 DB를 깨끗하게 비우고 싶다면 + # Base.metadata.drop_all(bind=engine) + # Base.metadata.create_all(bind=engine) + + return TestClient(app) \ No newline at end of file From 1be92184154e4ef377dcd881d39e9878483fa5b9 Mon Sep 17 00:00:00 2001 From: JinWook Date: Thu, 15 Jan 2026 20:45:57 +0900 Subject: [PATCH 11/12] =?UTF-8?q?=EC=A3=BC=EC=84=9D(SN=EC=95=84=EB=8B=98)?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/deals.py | 11 ++++++++++- app/routers/docents.py | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/routers/deals.py b/app/routers/deals.py index 36ed091..2f4cc67 100644 --- a/app/routers/deals.py +++ b/app/routers/deals.py @@ -15,6 +15,7 @@ ) async def list_deals(db: Session = Depends(get_db)): # TODO + ''' deal을 list 형태로 반환''' return db.query(Deal).all() # db.query()를 통해 Deal에 있는 모든 데이터들을 가져온다. @router.post( @@ -25,6 +26,9 @@ async def list_deals(db: Session = Depends(get_db)): ) async def create_deal(deal: DealCreate, db: Session = Depends(get_db)): # TODO + ''' + deal을 생성. + ''' new_deal = Deal(**deal.dict()) # deal를 처리할 수 있는 형태로. db.add(new_deal) # DB 세션에 추가. db.commit() # 변경사항 저장. @@ -39,6 +43,9 @@ async def create_deal(deal: DealCreate, db: Session = Depends(get_db)): ) async def get_deal(deal_id: int, db: Session = Depends(get_db)): # TODO + ''' + 해당되는 deal을 반환. + ''' deal = db.query(Deal).filter(Deal.id == deal_id).first() # deal_id인 멤버를 DB에서 찾기. if deal is None: @@ -54,7 +61,9 @@ async def get_deal(deal_id: int, db: Session = Depends(get_db)): ) async def delete_deal(deal_id: int, db: Session = Depends(get_db)): # TODO - + ''' + deal을 삭제. + ''' deal = db.query(Deal).filter(Deal.id == deal_id).first() if deal is None: diff --git a/app/routers/docents.py b/app/routers/docents.py index e68b24d..5d81049 100644 --- a/app/routers/docents.py +++ b/app/routers/docents.py @@ -14,6 +14,9 @@ ) async def list_docents(db: Session = Depends(get_db)): # TODO + ''' + docents를 리스트 형태로 반환. + ''' return db.query(Docent).all() @@ -26,6 +29,10 @@ async def list_docents(db: Session = Depends(get_db)): async def create_docent(docent: DocentCreate, db: Session = Depends(get_db)): # TODO + ''' + docent 생성. + ''' + new_docent = Docent(**docent.dict()) db.add(new_docent) db.commit() @@ -40,6 +47,7 @@ async def create_docent(docent: DocentCreate, db: Session = Depends(get_db)): ) async def get_docent(docent_id: int, db: Session = Depends(get_db)): # TODO + '''해당되는 docent를 반환''' docent = db.query(Docent).filter(Docent.id == docent_id).first() @@ -55,7 +63,9 @@ async def get_docent(docent_id: int, db: Session = Depends(get_db)): ) async def delete_docent(docent_id: int, db: Session = Depends(get_db)): # TODO - + ''' + 해당되는 docent를 삭제. + ''' docent = db.query(Docent).filter(Docent.id == docent_id).first() if docent is None: From c478d9a3908c709decbb3379048eb7a9953d3dec Mon Sep 17 00:00:00 2001 From: yeop <49216065+yeop-sang@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:23:45 +0900 Subject: [PATCH 12/12] Update requirements.txt --- requirements.txt | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index e67a454..243dd8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,28 +1,8 @@ -annotated-types==0.7.0 -anyio==3.7.1 -certifi==2026.1.4 -click==8.3.1 -fastapi==0.104.1 greenlet==3.3.0 -h11==0.16.0 -httpcore==1.0.9 -httptools==0.7.1 -httpx==0.25.2 -idna==3.11 -iniconfig==2.3.0 -packaging==25.0 -pluggy==1.6.0 -psycopg2-binary==2.9.9 -pydantic==2.5.0 -pydantic-core==2.14.1 -pytest==7.4.3 -python-dotenv==1.0.0 -pyyaml==6.0.3 -sniffio==1.3.1 -sqlalchemy==2.0.23 -starlette==0.27.0 +sqlalchemy==2.0.45 typing-extensions==4.15.0 -uvicorn==0.24.0 -uvloop==0.22.1 -watchfiles==1.1.1 -websockets==16.0 +fastapi>=0.110.0 +python-dotenv>=1.0.0 +uvicorn>=0.20.0 +httpx>=0.27.0 +pytest>=8.0.0