Skip to content

Commit f585d18

Browse files
committed
fix: main, recommender files
1 parent a407cca commit f585d18

2 files changed

Lines changed: 175 additions & 148 deletions

File tree

main.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ async def lifespan(app: FastAPI):
5353
"http://localhost:8080", # Spring Boot 로컬
5454
"http://localhost:3000", # React 로컬
5555
# 프로덕션 도메인 추가
56+
"*"
5657
]
5758

5859
app.add_middleware(
@@ -109,26 +110,39 @@ def get_auction_recommendations(
109110
print(f"[/recommend-auctions] 요청 시작: user_id={request.user_id}")
110111
start_time = time.time()
111112

112-
# 1. 사용자 존재 확인
113-
user_exists = db.query(UserDB.id).filter(UserDB.id == request.user_id).first()
114-
if not user_exists:
115-
raise HTTPException(
116-
status_code=status.HTTP_404_NOT_FOUND,
117-
detail=f"User with id {request.user_id} not found"
113+
try:
114+
# 1. 사용자 존재 확인
115+
user_exists = db.query(UserDB.id).filter(UserDB.id == request.user_id).first()
116+
if not user_exists:
117+
raise HTTPException(
118+
status_code=status.HTTP_404_NOT_FOUND,
119+
detail=f"User with id {request.user_id} not found"
120+
)
121+
122+
# ⭐ 2. 추천 실행 (새 세션 전달)
123+
recommended_items = recommender.recommend_items(
124+
target_user_id=request.user_id,
125+
n_recommendations=10,
126+
db_session=db # ⭐ 새 세션 전달
118127
)
128+
129+
elapsed = time.time() - start_time
130+
print(f"[/recommend-auctions] 요청 완료. 추천 수: {len(recommended_items)}, 소요 시간: {elapsed:.4f}초")
131+
print(f"{'='*60}\n")
132+
133+
# 3. 응답 반환
134+
return RecommendationResponse(recommended_items=recommended_items)
119135

120-
# 2. 추천 실행
121-
recommended_items = recommender.recommend_items(
122-
target_user_id=request.user_id,
123-
n_recommendations=10
124-
)
125-
126-
elapsed = time.time() - start_time
127-
print(f"[/recommend-auctions] 요청 완료. 추천 수: {len(recommended_items)}, 소요 시간: {elapsed:.4f}초")
128-
print(f"{'='*60}\n")
129-
130-
# 3. 응답 반환
131-
return RecommendationResponse(recommended_items=recommended_items)
136+
except HTTPException:
137+
raise
138+
except Exception as e:
139+
# ⭐ 에러 발생 시 롤백
140+
print(f"[ERROR] 추천 API 오류: {str(e)}")
141+
db.rollback()
142+
raise HTTPException(
143+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
144+
detail=f"추천 생성 중 오류 발생: {str(e)}"
145+
)
132146

133147
if __name__ == "__main__":
134148
import uvicorn

utils/recommender.py

Lines changed: 143 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -185,163 +185,176 @@ def get_similar_users(self, target_user_id: int, n_users: int = 5) -> List[int]:
185185
def recommend_items(
186186
self,
187187
target_user_id: int,
188-
n_recommendations: int = 10
188+
n_recommendations: int = 10,
189+
db_session: Session = None # ⭐ 새 파라미터 추가
189190
) -> List[ItemRecommendation]:
190191
"""
191192
대상 사용자에게 경매 상품 추천
192193
193194
Args:
194195
target_user_id: 추천 대상 사용자 ID
195196
n_recommendations: 추천할 상품 개수 (기본값: 10)
197+
db_session: DB 세션 (매 요청마다 새로 전달)
196198
197199
Returns:
198200
추천 상품 리스트 (ItemRecommendation 객체)
199201
"""
200202
from datetime import datetime
201203

202-
print(f"사용자 {target_user_id}에 대한 추천 생성 시작...")
204+
# ⭐ 세션 선택: 전달받은 세션이 있으면 사용, 없으면 초기화 시 세션 사용
205+
db = db_session if db_session is not None else self.db
203206

204-
# 1. 유사 사용자 찾기
205-
similar_users = self.get_similar_users(target_user_id, n_users=5)
206-
207-
# Cold Start 대응: 유사 사용자가 없으면 인기 상품 추천
208-
if not similar_users:
209-
print(f"사용자 {target_user_id}의 유사 사용자를 찾을 수 없습니다. 인기 상품으로 대체합니다.")
210-
return self._get_popular_items(target_user_id, n_recommendations)
211-
212-
print(f"유사 사용자 {len(similar_users)}명 발견: {similar_users}")
213-
214-
# 2. 유사 사용자들이 입찰/찜한 상품 수집
215-
candidate_items = []
216-
for uid in similar_users:
217-
# 입찰한 상품
218-
candidate_items.extend(self.user_bid_items.get(uid, []))
219-
# 찜한 상품
220-
candidate_items.extend(self.user_liked_items.get(uid, []))
221-
222-
# 3. 대상 사용자가 이미 접한 상품 제외
223-
user_interacted = self.user_bid_items.get(target_user_id, set()) | \
224-
self.user_liked_items.get(target_user_id, set())
225-
226-
# 4. 빈도수 계산 (많이 추천될수록 높은 점수)
227-
candidate_counts = Counter(candidate_items)
228-
229-
# 이미 접한 상품 제거
230-
for item_id in user_interacted:
231-
candidate_counts.pop(item_id, None)
232-
233-
# 5. 상위 N개 추출
234-
recommended_item_ids = [item_id for item_id, _ in candidate_counts.most_common(n_recommendations * 2)]
235-
236-
# Cold Start 대응: 후보 아이템이 없으면 인기 상품 추천
237-
if not recommended_item_ids:
238-
print("협업 필터링 후보가 없습니다. 인기 상품으로 대체합니다.")
239-
return self._get_popular_items(target_user_id, n_recommendations)
240-
241-
# 6. DB에서 상품 상세 정보 조회
242-
# 수정: 현재 시간 기준으로 실제 입찰 가능한 상품만 필터링
243-
now = datetime.utcnow()
244-
items = self.db.query(ItemDB).filter(
245-
ItemDB.item_id.in_(recommended_item_ids),
246-
ItemDB.item_status == ItemStatusEnum.BIDDING,
247-
ItemDB.end_time > now # 추가: 경매 종료 시간 체크
248-
).all()
249-
250-
# 7. ItemRecommendation 객체로 변환
251-
recommended_items = []
252-
for item in items[:n_recommendations]:
253-
recommendation = ItemRecommendation(
254-
item_id=item.item_id,
255-
name=item.name,
256-
title=item.title,
257-
category=item.category,
258-
current_price=item.current_price,
259-
end_time=item.end_time,
260-
item_status=item.item_status,
261-
region_name=item.region.sigungu,
262-
view_count=item.view_count,
263-
bid_count=item.bid_count,
264-
recommendation_score=candidate_counts.get(item.item_id, 0)
265-
)
266-
recommended_items.append(recommendation)
207+
print(f"사용자 {target_user_id}에 대한 추천 생성 시작...")
267208

268-
# Cold Start 대응: 결과가 부족하면 인기 상품으로 채우기
269-
if len(recommended_items) < n_recommendations:
270-
print(f"추천 결과가 부족합니다 ({len(recommended_items)}/{n_recommendations}). 인기 상품으로 보완합니다.")
271-
popular_items = self._get_popular_items(target_user_id, n_recommendations - len(recommended_items))
209+
try:
210+
# 1. 유사 사용자 찾기
211+
similar_users = self.get_similar_users(target_user_id, n_users=5)
212+
213+
# Cold Start 대응: 유사 사용자가 없으면 인기 상품 추천
214+
if not similar_users:
215+
print(f"사용자 {target_user_id}의 유사 사용자를 찾을 수 없습니다. 인기 상품으로 대체합니다.")
216+
# ⭐ 롤백 후 인기 상품 조회
217+
db.rollback()
218+
return self._get_popular_items(target_user_id, n_recommendations, db)
219+
220+
print(f"유사 사용자 {len(similar_users)}명 발견: {similar_users}")
221+
222+
# 2. 유사 사용자들이 입찰/찜한 상품 수집
223+
candidate_items = []
224+
for uid in similar_users:
225+
candidate_items.extend(self.user_bid_items.get(uid, []))
226+
candidate_items.extend(self.user_liked_items.get(uid, []))
227+
228+
# 3. 대상 사용자가 이미 접한 상품 제외
229+
user_interacted = self.user_bid_items.get(target_user_id, set()) | \
230+
self.user_liked_items.get(target_user_id, set())
231+
232+
# 4. 빈도수 계산
233+
candidate_counts = Counter(candidate_items)
234+
235+
for item_id in user_interacted:
236+
candidate_counts.pop(item_id, None)
237+
238+
# 5. 상위 N개 추출
239+
recommended_item_ids = [item_id for item_id, _ in candidate_counts.most_common(n_recommendations * 2)]
272240

273-
# 중복 제거
274-
existing_ids = {item.item_id for item in recommended_items}
275-
for item in popular_items:
276-
if item.item_id not in existing_ids:
277-
recommended_items.append(item)
278-
if len(recommended_items) >= n_recommendations:
279-
break
280-
281-
print(f"추천 생성 완료. 추천 상품 수: {len(recommended_items)}")
282-
return recommended_items
241+
# Cold Start 대응: 후보 아이템이 없으면 인기 상품 추천
242+
if not recommended_item_ids:
243+
print("협업 필터링 후보가 없습니다. 인기 상품으로 대체합니다.")
244+
# ⭐ 롤백 후 인기 상품 조회
245+
db.rollback()
246+
return self._get_popular_items(target_user_id, n_recommendations, db)
247+
248+
# ⭐ 6. DB에서 상품 상세 정보 조회 (try-except 추가)
249+
now = datetime.utcnow()
250+
items = db.query(ItemDB).filter(
251+
ItemDB.item_id.in_(recommended_item_ids),
252+
ItemDB.item_status == ItemStatusEnum.BIDDING,
253+
ItemDB.end_time > now
254+
).all()
255+
256+
# 7. ItemRecommendation 객체로 변환
257+
recommended_items = []
258+
for item in items[:n_recommendations]:
259+
recommendation = ItemRecommendation(
260+
item_id=item.item_id,
261+
name=item.name,
262+
title=item.title,
263+
category=item.category,
264+
current_price=item.current_price,
265+
end_time=item.end_time,
266+
item_status=item.item_status,
267+
region_name=item.region.sigungu,
268+
view_count=item.view_count,
269+
bid_count=item.bid_count,
270+
recommendation_score=candidate_counts.get(item.item_id, 0)
271+
)
272+
recommended_items.append(recommendation)
273+
274+
# Cold Start 대응: 결과가 부족하면 인기 상품으로 채우기
275+
if len(recommended_items) < n_recommendations:
276+
print(f"추천 결과가 부족합니다 ({len(recommended_items)}/{n_recommendations}). 인기 상품으로 보완합니다.")
277+
# ⭐ 롤백 후 인기 상품 조회
278+
db.rollback()
279+
popular_items = self._get_popular_items(target_user_id, n_recommendations - len(recommended_items), db)
280+
281+
existing_ids = {item.item_id for item in recommended_items}
282+
for item in popular_items:
283+
if item.item_id not in existing_ids:
284+
recommended_items.append(item)
285+
if len(recommended_items) >= n_recommendations:
286+
break
287+
288+
print(f"추천 생성 완료. 추천 상품 수: {len(recommended_items)}")
289+
return recommended_items
290+
291+
except Exception as e:
292+
# ⭐ 에러 발생 시 반드시 롤백
293+
print(f"[ERROR] 추천 생성 중 오류 발생: {str(e)}")
294+
db.rollback()
295+
# 인기 상품으로 폴백
296+
return self._get_popular_items(target_user_id, n_recommendations, db)
283297

284-
def _get_popular_items(self, target_user_id: int, n_items: int) -> List[ItemRecommendation]:
298+
def _get_popular_items(
299+
self,
300+
target_user_id: int,
301+
n_items: int,
302+
db_session: Session = None # ⭐ 새 파라미터 추가
303+
) -> List[ItemRecommendation]:
285304
"""
286305
인기 상품 추천 (Cold Start 대응)
287-
- 최근 3일 내 생성
288-
- 입찰 수 많은 순
289-
- 사용자가 이미 접한 상품 제외
290-
291-
Args:
292-
target_user_id: 대상 사용자 ID
293-
n_items: 추천할 상품 개수
294-
295-
Returns:
296-
인기 상품 리스트
297306
"""
298307
from datetime import datetime, timedelta
299308

300-
# 사용자가 이미 접한 상품 제외
301-
user_interacted = self.user_bid_items.get(target_user_id, set()) | \
302-
self.user_liked_items.get(target_user_id, set())
303-
304-
# 최근 3일 내 생성된 입찰 가능한 인기 상품 조회
305-
three_days_ago = datetime.utcnow() - timedelta(days=3)
306-
now = datetime.utcnow()
309+
# ⭐ 세션 선택
310+
db = db_session if db_session is not None else self.db
307311

308-
# 사용자가 접한 상품이 있을 때만 필터링
309-
if user_interacted:
310-
items = self.db.query(ItemDB).filter(
311-
ItemDB.item_status == ItemStatusEnum.BIDDING,
312-
ItemDB.end_time > now,
313-
ItemDB.created_at > three_days_ago,
314-
~ItemDB.item_id.in_(user_interacted)
315-
).order_by(
316-
ItemDB.bid_count.desc(),
317-
ItemDB.view_count.desc()
318-
).limit(n_items * 2).all()
319-
else:
320-
items = self.db.query(ItemDB).filter(
312+
try:
313+
# 사용자가 이미 접한 상품 제외
314+
user_interacted = self.user_bid_items.get(target_user_id, set()) | \
315+
self.user_liked_items.get(target_user_id, set())
316+
317+
# 최근 3일 내 생성된 입찰 가능한 인기 상품 조회
318+
three_days_ago = datetime.utcnow() - timedelta(days=3)
319+
now = datetime.utcnow()
320+
321+
# ⭐ 쿼리 간소화
322+
query = db.query(ItemDB).filter(
321323
ItemDB.item_status == ItemStatusEnum.BIDDING,
322324
ItemDB.end_time > now,
323325
ItemDB.created_at > three_days_ago
324-
).order_by(
326+
)
327+
328+
if user_interacted:
329+
query = query.filter(~ItemDB.item_id.in_(user_interacted))
330+
331+
items = query.order_by(
325332
ItemDB.bid_count.desc(),
326333
ItemDB.view_count.desc()
327334
).limit(n_items * 2).all()
335+
336+
# ItemRecommendation 객체로 변환
337+
popular_items = []
338+
for item in items[:n_items]:
339+
recommendation = ItemRecommendation(
340+
item_id=item.item_id,
341+
name=item.name,
342+
title=item.title,
343+
category=item.category,
344+
current_price=item.current_price,
345+
end_time=item.end_time,
346+
item_status=item.item_status,
347+
region_name=item.region.sigungu,
348+
view_count=item.view_count,
349+
bid_count=item.bid_count,
350+
recommendation_score=0
351+
)
352+
popular_items.append(recommendation)
353+
354+
return popular_items
328355

329-
# ItemRecommendation 객체로 변환
330-
popular_items = []
331-
for item in items[:n_items]:
332-
recommendation = ItemRecommendation(
333-
item_id=item.item_id,
334-
name=item.name,
335-
title=item.title,
336-
category=item.category,
337-
current_price=item.current_price,
338-
end_time=item.end_time,
339-
item_status=item.item_status,
340-
region_name=item.region.sigungu,
341-
view_count=item.view_count,
342-
bid_count=item.bid_count,
343-
recommendation_score=0 # 인기 상품은 점수 0
344-
)
345-
popular_items.append(recommendation)
346-
347-
return popular_items
356+
except Exception as e:
357+
# ⭐ 에러 발생 시 롤백 후 빈 리스트 반환
358+
print(f"[ERROR] 인기 상품 조회 중 오류 발생: {str(e)}")
359+
db.rollback()
360+
return []

0 commit comments

Comments
 (0)