
안녕하세요! 저는 AI기반 일정 추천 웹 프로젝트 개발 중 에러가 발생하여 트러블 슈팅 경험을 기록하러 왔습니다!
정말 사소한 에러였던 것 같은데 10시간 넘게 고생한 것 같습니다!!
우선 저는 단기 일정 추천 로직을 맡았고 Spring 서버에서 데이터를 AI 서버로 보내 단기 일정을 받아오는 구조였습니다.
AI 서버는 FastAPI를 사용하여 통신하였습니다!
그럼 트러블 슈팅 과정을 살펴보겠습니다!
DTO: 프론트에서 사용자에게 전달받은 선호도 데이터를 AI 서버에서 받아야하는 형식으로 가공하는 DTO 코드입니다!
@Data
public class AiShortTravelDTO {
private float LATITUDE; // 위도
private float LONGITUDE; // 경도
private String MVMN_NM; // "자가용", "대중교통 등"
private String GENDER; //"남성", "여성"
private int AGE_GRP; // 20대면 20, 30대면 30 등등
private int DAY;
private int TRAVEL_STYL_1;
private int TRAVEL_STYL_2;
private int TRAVEL_STYL_3;
private int TRAVEL_STYL_5;
private int TRAVEL_STYL_6;
private int TRAVEL_STYL_7;
private int TRAVEL_STYL_8;
private int TRAVEL_MOTIVE_1; // 여행 동기
private String REL_CD_Categorized; // 동행인 정보
public static AiShortTravelDTO toAiShortTravelDTO(ShortTravelDTO shortTravelDTO){
AiShortTravelDTO aiShortTravelDTO = new AiShortTravelDTO();
List<String> travelMotivations = List.of(
"일상적인 환경 및 역할에서의 탈출, 지루함 탈피",
"쉴 수 있는 기회, 육체 피로 해결 및 정신적인 휴식",
"여행 동반자와의 친밀감 및 유대감 증진",
"진정한 자아 찾기 또는 자신을 되돌아볼 기회 찾기",
"SNS 사진 등록 등 과시",
"운동, 건강 증진 및 충전",
"새로운 경험 추구",
"역사 탐방, 문화적 경험 등 교육적 동기",
"특별한 목적(칠순여행, 신혼여행, 수학여행, 인센티브여행)",
"기타"
);
aiShortTravelDTO.LATITUDE = shortTravelDTO.getLocation().getLatitude();
aiShortTravelDTO.LONGITUDE = shortTravelDTO.getLocation().getLongitude();
aiShortTravelDTO.DAY = shortTravelDTO.getDay_duration();
if (shortTravelDTO.getTransport().equals("차량 이용")){
aiShortTravelDTO.MVMN_NM = "자가용";
} else{
aiShortTravelDTO.MVMN_NM = "대중교통 등";
}
aiShortTravelDTO.GENDER = shortTravelDTO.getGender();
aiShortTravelDTO.AGE_GRP = Integer.parseInt(shortTravelDTO.getAgeGroup().replace("대", "").replace(" 이상", "").replace(" 미만", ""));
aiShortTravelDTO.TRAVEL_STYL_1 = Integer.parseInt(shortTravelDTO.getPreferences().getNature());
aiShortTravelDTO.TRAVEL_STYL_2 = Integer.parseInt(shortTravelDTO.getPreferences().getDuration());
aiShortTravelDTO.TRAVEL_STYL_3 = Integer.parseInt(shortTravelDTO.getPreferences().getNewPlaces());
aiShortTravelDTO.TRAVEL_STYL_5 = Integer.parseInt(shortTravelDTO.getPreferences().getRelaxation());
aiShortTravelDTO.TRAVEL_STYL_6 = Integer.parseInt(shortTravelDTO.getPreferences().getExploration());
aiShortTravelDTO.TRAVEL_STYL_7 = Integer.parseInt(shortTravelDTO.getPreferences().getPlanning());
aiShortTravelDTO.TRAVEL_STYL_8 = Integer.parseInt(shortTravelDTO.getPreferences().getPhotography());
for (int i = 0; i < travelMotivations.size(); i++){
if (travelMotivations.get(i).equals(shortTravelDTO.getTravelPurpose())){
aiShortTravelDTO.TRAVEL_MOTIVE_1 = i + 1;
break;
}
}
aiShortTravelDTO.REL_CD_Categorized = shortTravelDTO.getCompanion().replace("와", "").replace("과", "");
return aiShortTravelDTO;
}
}
@Data
public class RecommendationDTO {
// "1일차", "2일차"
private String day;
// 해당 날짜의 추천 장소 리스트
private List<PlaceDTO> places;
}
app.py: UserInput을 받아서 선호도를 기반으로 단기 일정 추천 결과를 받아오고 일 별로 5개씩 잘라서 반환해주는 코드입니다.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Dict
import pickle
import pandas as pd
from utils import process_user_input, filter_and_merge, predict_recommendations
from evaluate import main
app = FastAPI()
# 모델 및 데이터 파일 경로
model_path = '../models/catboost_model.pkl'
info_path = '../data/attraction_info.csv'
# 사용자 입력을 위한 Pydantic 모델 정의
class UserInput(BaseModel):
LATITUDE: float
LONGITUDE: float
MVMN_NM: str # 이동수단
GENDER: str # 성별
AGE_GRP: int # 나이
DAY: int
TRAVEL_STYL_1: int
TRAVEL_STYL_2: int
TRAVEL_STYL_3: int
TRAVEL_STYL_5: int
TRAVEL_STYL_6: int
TRAVEL_STYL_7: int
TRAVEL_STYL_8: int
TRAVEL_MOTIVE_1: int # 여행 동기
REL_CD_Categorized: str # 동행자 정보
=def split_recommendations(recommendations, day_count):
result = []
for i in range(0, len(recommendations), 5):
day_recommendations = recommendations[i:i+5]
day = {"day": f"{(i // 5) + 1}일차", "places": day_recommendations}
result.append(day)
return result
@app.post("/ai/trip/short", response_model=List[dict])
def get_recommendations(user_input: UserInput):
try:
# 입력 데이터를 처리하는 부분
user_df = user_input.dict()
# evaluate.py의 main 함수 호출
recommendations = main(user_input=user_df, model_path=model_path, info_path=info_path)
# 데이터 검증 및 NaN, Infinity 값 처리
for recommendation in recommendations:
for key, value in recommendation.items():
if isinstance(value, float) and (pd.isna(value) or value in [float('inf'), float('-inf')]):
recommendation[key] = 0.0 # NaN이나 Inf를 0으로 대체하거나 원하는 값으로 대체
split_result = split_recommendations(recommendations, user_input.DAY)
return split_result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
웹 페이지를 볼 수 있는 권한이 없습니다.처리할 수 없는 엔터티 응답 상태 코드는 서버가 요청 엔터티의 콘텐츠 유형을 이해하고
요청 엔터티의 구문이 정확하지만 포함된 명령을 처리할 수 없음을 나타냅니다 # 모든 경로 접속 권한 허가 코드
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"], # 모든 형태의 요청 허가
allow_headers=["*"],
)
class UserInput(BaseModel):
latitude: float
longitude: float
mvmn_nm: str # 이동수단
gender: str # 성별
age_grp: int # 나이
day: int
travel_styl_1: int
travel_styl_2: int
travel_styl_3: int
travel_styl_5: int
travel_styl_6: int
travel_styl_7: int
travel_styl_8: int
travel_motive_1: int # 여행 동기
rel_cd_categorized: str # 동행자 정보@app.post("/ai/trip/short", response_model=List[dict])
async def get_recommendations(user_input: UserInput):
try:
# 입력 데이터를 처리하는 부분
user_df = {
"LATITUDE": user_input.latitude,
"LONGITUDE": user_input.longitude,
"MVMN_NM": user_input.mvmn_nm,
"GENDER": user_input.gender,
"AGE_GRP": user_input.age_grp,
"DAY": user_input.day,
"TRAVEL_STYL_1": user_input.travel_styl_1,
"TRAVEL_STYL_2": user_input.travel_styl_2,
"TRAVEL_STYL_3": user_input.travel_styl_3,
"TRAVEL_STYL_5": user_input.travel_styl_5,
"TRAVEL_STYL_6": user_input.travel_styl_6,
"TRAVEL_STYL_7": user_input.travel_styl_7,
"TRAVEL_STYL_8": user_input.travel_styl_8,
"TRAVEL_MOTIVE_1": user_input.travel_motive_1,
"REL_CD_Categorized": user_input.rel_cd_categorized
}
# .. 이후 로직@Data
public class AiShortTravelDTO {
private float latitude; // 위도
private float longitude; // 경도
private String mvmn_nm; // "자가용", "대중교통 등"
private String gender; //"남성", "여성"
private int age_grp; // 20대면 20, 30대면 30 등등
private int day;
private int travel_styl_1;
private int travel_styl_2;
private int travel_styl_3;
private int travel_styl_5;
private int travel_styl_6;
private int travel_styl_7;
private int travel_styl_8;
private int travel_motive_1; // 여행 동기
private String rel_cd_categorized; // 동행인 정보
}