새해 첫날부터 사이드 프로젝트 진행!
사이드 프로젝트 진행 中
.
├── /data # Data 관련 파일 및 코드
│ └── /src # input 파일 저장 경로
│ ├── /images # image input 파일 저장 경로
│ ├── /model_versions # 모델 weight 저장
│ ├── /submit # 제출 파일(submission.csv) 저장 경로
│ └── /text_vector # 벡터 파일
├── /logs # 로그 파일 저장 경로 .gitignore
├── /docker # Docker 관련 디렉토리
│ ├── /server # Docker server 관련 파일
│ └── /db # Docker DB 관련 Directory
├── /src # application 관련 코드
│ ├── /ai_models # Model 구현 코드
│ ├── /config # config 관련 설정 dataclass
│ ├── /db # db 설정 관련 코드
│ ├── /domain # API별 도메인 코드
│ ├── /schemas # DB Table 스키마 코드
│ └── dependency.py # Dependencies 관련 코드
├── .dockerignore # 도커 이미지 빌드 시 제외할 파일 목록
├── .gitignore # git에서 제외할 파일 목록
├── README.md # 프로젝트 설명 파일
└── main.py # 앱 실행 파일
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
# Create Database - 추후 sql 변경
logger.info("Creating database tables")
SQLModel.metadata.create_all(engine)
# 모델 로드
# logger.info("Loading model")
# load_model(config.model_path)
yield
except Exception as e:
logger.error(f"Startup error: {e}")
raise
finally:
logger.info("Shutting down application")
app = FastAPI(lifespan=lifespan)
app.include_router(user_router)
app.include_router(service_router)
app.include_router(model_router)
# 정적 파일 서빙 설정
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
# 시작 페이지
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
# 장르 선택 페이지
@app.get("/genre", response_class=HTMLResponse)
async def genre_page(request: Request):
return templates.TemplateResponse("select_genre.html", {"request": request})
# 영화 선택 페이지
@app.get("/movie", response_class=HTMLResponse)
async def movie_page(request: Request):
return templates.TemplateResponse("select_movie.html", {"request": request})
# 결과 페이지
@app.get("/inference", response_class=HTMLResponse)
async def result_page(request: Request):
return templates.TemplateResponse("result.html", {"request": request})
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)
{% extends "base.html" %}
{% block title %}영화 선택 - fillna Movie Recommendation{% endblock %}
{% block content %}
<div class="container">
<h1>아래의 영화 중 당신이 시청 했던 좋아하는 영화를 선택해주세요</h1>
<form id="movieForm" onsubmit="handleSubmit(event)">
<div class="movie-grid" id="movieContainer">
{% for movie in movies %}
<div class="movie-card">
<div class="movie-header">
<input type="checkbox" name="movie" value="{{ movie.id }}" id="movie{{ movie.id }}">
<label for="movie{{ movie.id }}">{{ movie.title }}</label>
</div>
<div class="movie-poster">
<img src="/static/posters/{{ movie.id }}.jpg" alt="{{ movie.title }} 포스터">
</div>
</div>
{% endfor %}
</div>
<button type="submit" class="select-btn">선택 완료</button>
</form>
</div>
<script>
window.onload = function() {
// sessionStorage에서 영화 목록 가져오기
const movieIds = JSON.parse(sessionStorage.getItem('selectedMovies') || '[]');
const container = document.getElementById('movieContainer');
movieIds.forEach(movieId => {
const movieCard = `
<div class="movie-card">
<div class="movie-header">
<input type="checkbox" name="movie" value="${movieId}" id="movie${movieId}">
<label for="movie${movieId}">Movie ${movieId}</label>
</div>
<div class="movie-poster">
<img src="/static/posters/${movieId}.jpg" alt="Movie ${movieId} 포스터">
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', movieCard);
});
};
function handleSubmit(event) {
event.preventDefault();
const selectedMovies = [];
const checkboxes = document.querySelectorAll('input[name="movie"]:checked');
const userId = parseInt(sessionStorage.getItem('userId'));
// 영화 선택 검증 추가
if (checkboxes.length === 0) {
alert('1개 이상의 영화를 선택해주세요.');
return false; // 폼 제출 중단
}
checkboxes.forEach(checkbox => {
selectedMovies.push(parseInt(checkbox.value));
});
fetch('/api/model/predict', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
movie_list: selectedMovies
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
sessionStorage.setItem('recommendedMovies', JSON.stringify(data.movie_list));
window.location.href = '/inference';
})
.catch(error => {
console.error('Error:', error);
alert('오류가 발생했습니다.');
});
}
</script>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.movie-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 15px;
margin: 30px 0;
}
.movie-card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s;
}
.movie-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.movie-header {
padding: 10px;
text-align: center;
background-color: #f8f9fa;
}
.movie-header label {
font-size: 14px;
margin-left: 5px;
cursor: pointer;
}
.movie-poster {
width: 100%;
aspect-ratio: 2/3;
overflow: hidden;
}
.movie-poster img {
width: 100%;
height: 100%;
object-fit: cover;
}
input[type="checkbox"] {
cursor: pointer;
}
input[type="checkbox"]:checked + label {
color: #4CAF50;
font-weight: bold;
}
input[type="checkbox"]:checked ~ .movie-poster {
border: 2px solid #4CAF50;
}
.select-btn {
display: block;
padding: 15px 30px;
font-size: 18px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 30px auto;
}
.select-btn:hover {
background-color: #45a049;
}
@media (max-width: 1200px) {
.movie-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 768px) {
.movie-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
{% endblock %}