Skip to content

Commit d088860

Browse files
finished product api
1 parent 81334f5 commit d088860

File tree

14 files changed

+409
-242
lines changed

14 files changed

+409
-242
lines changed

carrot/app/auction/models.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from sqlalchemy import String, Integer, ForeignKey, Boolean, DateTime
55
from sqlalchemy.orm import Mapped, mapped_column, relationship
66
from sqlalchemy.sql import func
7-
from typing import TYPE_CHECKING
7+
from typing import TYPE_CHECKING, Optional
88

99
from carrot.db.common import Base
1010

@@ -24,7 +24,7 @@ class Auction(Base):
2424
id: Mapped[str] = mapped_column(String(36), primary_key=True, index=True, default=lambda: str(uuid.uuid4()))
2525
product_id: Mapped[str] = mapped_column(String(36), ForeignKey("product.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
2626

27-
starting_price: Mapped[int] = mapped_column(Integer, nullable=False) # 시작가
27+
# starting_price: Mapped[int] = mapped_column(Integer, nullable=False) # 시작가
2828
current_price: Mapped[int] = mapped_column(Integer, nullable=False) # 현재가
2929
# is_sold: Mapped[bool] = mapped_column(Boolean, default=False) # Product의 is_sold와 중복
3030
end_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) # 경매 종료 시간
@@ -34,11 +34,13 @@ class Auction(Base):
3434
product: Mapped["Product"] = relationship("Product", back_populates="auction", uselist=False)
3535
bids: Mapped[list["Bid"]] = relationship("Bid", back_populates="auction", cascade="all, delete-orphan")
3636

37-
@property
38-
def top_bid(self):
39-
if not self.bids:
40-
return None
41-
return max(self.bids, key=lambda bid: bid.bid_price)
37+
top_bid: Mapped[Optional["Bid"]] = relationship(
38+
"Bid",
39+
primaryjoin="and_(Auction.id==Bid.auction_id)",
40+
order_by="desc(Bid.bid_price)",
41+
uselist=False,
42+
viewonly=True,
43+
)
4244

4345
@property
4446
def owner_id(self):

carrot/app/auction/repositories.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,29 @@ async def get_auction_by_id(self, auction_id: str) -> Optional[Auction]:
5353
return auction
5454

5555
async def get_active_auctions(
56-
self,
57-
category_id: Optional[str] = None,
58-
region_id: Optional[str] = None
59-
)-> List[Auction]:
56+
self,
57+
category_id: Optional[str] = None,
58+
region_id: Optional[str] = None,
59+
) -> List[Auction]:
6060
stmt = (
6161
select(Auction)
6262
.where(Auction.status == AuctionStatus.ACTIVE)
6363
.options(selectinload(Auction.product))
64+
.order_by(Auction.end_at.asc())
6465
)
6566

66-
if category_id:
67-
stmt = stmt.join(Auction.product).where(Product.category_id == category_id)
68-
69-
if region_id:
70-
stmt = stmt.join(Auction.product).where(Product.region_id == region_id)
71-
72-
stmt = stmt.order_by(Auction.end_at.asc())
67+
# ✅ 필터가 있을 때만 join 한 번
68+
if category_id or region_id:
69+
stmt = stmt.join(Auction.product)
70+
71+
if category_id:
72+
stmt = stmt.where(Product.category_id == category_id)
73+
74+
if region_id:
75+
stmt = stmt.where(Product.region_id == region_id)
7376

7477
result = await self.session.execute(stmt)
75-
return result.scalars().all()
78+
return result.scalars().unique().all()
7679

7780
async def delete_auction(self, auction: Auction) -> None:
7881
await self.session.delete(auction)

carrot/app/auction/router.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,48 @@
66
from carrot.app.auth.utils import login_with_header
77

88
from carrot.app.auction.services import AuctionService
9-
from carrot.app.auction.schemas import AuctionCreate, AuctionResponse, BidCreate, BidResponse # Pydantic 모델 가정
9+
from carrot.app.auction.schemas import AuctionListResponse, AuctionResponse, BidCreate, BidResponse # Pydantic 모델 가정
1010

1111
from carrot.app.user.models import User
1212

1313
from carrot.app.product.schemas import ProductPostRequest
1414

1515
auction_router = APIRouter()
1616

17-
# 1. 경매 등록
18-
@auction_router.post("/", response_model=AuctionResponse)
19-
async def create_new_auction(
20-
owner: Annotated[User, Depends(login_with_header)],
21-
product_data: ProductPostRequest,
22-
auction_data: AuctionCreate,
23-
service: Annotated[AuctionService, Depends(AuctionService.create)],
24-
) -> AuctionResponse:
25-
auction = await service.create_auction_with_product(
26-
owner_id=owner.id,
27-
region_id=owner.region_id,
28-
product_data=product_data,
29-
auction_data=auction_data
30-
)
31-
return AuctionResponse.model_validate(auction)
17+
# # 1. 경매 등록
18+
# @auction_router.post("/", response_model=AuctionResponse)
19+
# async def create_new_auction(
20+
# owner: Annotated[User, Depends(login_with_header)],
21+
# product_data: ProductPostRequest,
22+
# auction_data: AuctionCreate,
23+
# service: Annotated[AuctionService, Depends(AuctionService.create)],
24+
# ) -> AuctionResponse:
25+
# auction = await service.create_auction_with_product(
26+
# owner_id=owner.id,
27+
# region_id=owner.region_id,
28+
# product_data=product_data,
29+
# auction_data=auction_data
30+
# )
31+
# return AuctionResponse.model_validate(auction)
3232

3333
# 2. 경매 목록 조회 (카테고리, 지역 필터링)
34-
@auction_router.get("/", response_model=List[AuctionResponse])
34+
@auction_router.get("/", response_model=List[AuctionListResponse])
3535
async def get_auctions(
3636
service: Annotated[AuctionService, Depends(AuctionService.create)],
3737
category_id: Optional[str] = Query(None, description="카테고리 ID로 필터링"),
3838
region_id: Optional[str] = Query(None, description="지역 ID로 필터링"),
39-
) -> List[AuctionResponse]:
40-
39+
) -> List[AuctionListResponse]:
4140
auctions = await service.list_auctions(category_id, region_id)
42-
return auctions
41+
return [AuctionListResponse.model_validate(a) for a in auctions]
4342

44-
# 3. 경매 상세 조회
45-
@auction_router.get("/{auction_id}", response_model=AuctionResponse)
46-
async def get_auction_detail(
47-
auction_id: str,
48-
service: Annotated[AuctionService, Depends(AuctionService.create)],
49-
) -> AuctionResponse:
50-
auction = await service.get_auction_details(auction_id)
51-
return AuctionResponse.model_validate(auction)
43+
# # 3. 경매 상세 조회
44+
# @auction_router.get("/{auction_id}", response_model=AuctionResponse)
45+
# async def get_auction_detail(
46+
# auction_id: str,
47+
# service: Annotated[AuctionService, Depends(AuctionService.create)],
48+
# ) -> AuctionResponse:
49+
# auction = await service.get_auction_details(auction_id)
50+
# return AuctionResponse.model_validate(auction)
5251

5352
# 4. 입찰하기
5453
@auction_router.post("/{auction_id}/bids", response_model=BidResponse)

carrot/app/auction/schemas.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
from typing import List, Optional
44
from enum import Enum
55

6-
from carrot.app.product.schemas import ProductResponse
7-
86
from sqlalchemy import func
97

108
# 경매 상태 Enum (모델과 맞춤)
@@ -15,7 +13,7 @@ class AuctionStatus(str, Enum):
1513
CANCELED = "canceled"
1614

1715
class AuctionCreate(BaseModel):
18-
starting_price: int = Field(..., gt=0, description="경매 시작 가격")
16+
# starting_price: int = Field(..., gt=0, description="경매 시작 가격")
1917
end_at: datetime = Field(..., description="경매 종료 시간")
2018

2119
@field_validator("end_at")
@@ -32,15 +30,50 @@ def validate_end_at(cls, v: datetime) -> datetime:
3230
raise ValueError("경매 종료 시간은 현재 시간보다 미래여야 합니다.")
3331
return v
3432

33+
class AuctionProductSummary(BaseModel):
34+
id: str
35+
owner_id: str
36+
title: str
37+
image_ids: List[str]
38+
content: str | None
39+
price: int
40+
like_count: int
41+
category_id: str
42+
region_id: str
43+
is_sold: bool
44+
45+
class Config:
46+
from_attributes = True
47+
48+
class BidSummaryResponse(BaseModel):
49+
id: str
50+
bidder_id: str
51+
bid_price: int
52+
bid_at: datetime
53+
54+
class Config:
55+
from_attributes = True
56+
57+
class AuctionListResponse(BaseModel):
58+
id: str
59+
product_id: str
60+
current_price: int
61+
end_at: datetime
62+
bid_count: int
63+
status: AuctionStatus
64+
65+
product: AuctionProductSummary
66+
67+
class Config:
68+
from_attributes = True
69+
3570
class AuctionResponse(BaseModel):
3671
id: str
3772
product_id: str
38-
starting_price: int
3973
current_price: int
4074
end_at: datetime
4175
bid_count: int
4276
status: AuctionStatus
43-
product: ProductResponse
4477

4578
class Config:
4679
from_attributes = True
@@ -56,4 +89,5 @@ class BidResponse(BaseModel):
5689
bid_at: datetime
5790

5891
class Config:
59-
from_attributes = True
92+
from_attributes = True
93+

carrot/app/image/repositories.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
from typing import Annotated
2-
3-
from fastapi import Depends
4-
from sqlalchemy import select, and_
1+
from sqlalchemy import select
52
from sqlalchemy.ext.asyncio import AsyncSession
63

7-
from carrot.app.user.models import LocalAccount, SocialAccount, User
84
from carrot.app.image.models import Image
9-
from carrot.db.connection import get_db_session
105

116

127
class ImageRepository:
13-
def __init__(self, session: Annotated[AsyncSession, Depends(get_db_session)]) -> None:
8+
def __init__(self, session: AsyncSession) -> None:
149
self.session = session
1510

1611
async def upload_image(self, image: Image) -> Image:
1712
self.session.add(image)
18-
await self.session.commit()
13+
await self.session.flush()
1914
await self.session.refresh(image)
2015
return image
21-
22-
async def get_image_by_image_id(self, image_id: str) -> Image:
16+
17+
async def get_image_by_image_id(self, image_id: str) -> Image | None:
2318
query = select(Image).where(Image.id == image_id)
24-
posts = await self.session.execute(query)
25-
26-
return posts.scalars().one_or_none()
27-
19+
result = await self.session.execute(query)
20+
return result.scalars().one_or_none()
21+
2822
async def remove_image(self, image: Image) -> None:
2923
await self.session.delete(image)
30-
await self.session.commit()
24+
await self.session.flush()

carrot/app/image/router.py

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,52 @@
11
from typing import Annotated
2-
from fastapi import APIRouter, Depends
3-
from fastapi import FastAPI, File, UploadFile, HTTPException
4-
from fastapi.responses import StreamingResponse
5-
import boto3
6-
import io
7-
8-
from carrot.app.auth.utils import login_with_header, partial_login_with_header
9-
from carrot.app.user.models import User
10-
from carrot.app.image.schemas import (
11-
ImageResponse,
12-
)
2+
3+
from fastapi import APIRouter, Depends, File, UploadFile
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from carrot.db.connection import get_session_factory
7+
from carrot.app.image.schemas import ImageResponse
138
from carrot.app.image.services import ImageService
149

1510
image_router = APIRouter()
1611

12+
1713
@image_router.post("/product", status_code=201, response_model=ImageResponse)
1814
async def upload_product_image(
19-
service: Annotated[ImageService, Depends()],
15+
session: Annotated[AsyncSession, Depends(get_session_factory)],
2016
file: UploadFile = File(...),
2117
) -> ImageResponse:
22-
image = await service.upload_image(
23-
file,
24-
)
18+
service = ImageService(session)
19+
async with session.begin():
20+
image = await service.upload_image(file)
2521
return ImageResponse.model_validate(image)
2622

23+
2724
@image_router.post("/user", status_code=201, response_model=ImageResponse)
2825
async def upload_profile_image(
29-
service: Annotated[ImageService, Depends()],
26+
session: Annotated[AsyncSession, Depends(get_session_factory)],
3027
file: UploadFile = File(...),
3128
) -> ImageResponse:
32-
image = await service.upload_image(
33-
file,
34-
)
29+
service = ImageService(session)
30+
async with session.begin():
31+
image = await service.upload_image(file)
3532
return ImageResponse.model_validate(image)
3633

34+
3735
@image_router.get("/product/{image_id}", status_code=200, response_model=ImageResponse)
3836
async def view_product_image(
39-
service: Annotated[ImageService, Depends()],
40-
image_id: str
37+
session: Annotated[AsyncSession, Depends(get_session_factory)],
38+
image_id: str,
4139
) -> ImageResponse:
40+
service = ImageService(session)
4241
image = await service.view_image(image_id)
43-
4442
return ImageResponse.model_validate(image)
4543

44+
4645
@image_router.get("/user/{image_id}", status_code=200, response_model=ImageResponse)
4746
async def view_profile_image(
48-
service: Annotated[ImageService, Depends()],
49-
image_id: str
47+
session: Annotated[AsyncSession, Depends(get_session_factory)],
48+
image_id: str,
5049
) -> ImageResponse:
50+
service = ImageService(session)
5151
image = await service.view_image(image_id)
52-
53-
return ImageResponse.model_validate(image)
52+
return ImageResponse.model_validate(image)

0 commit comments

Comments
 (0)