250101 TIL #578 AI Tech #111 Movie Rec++ 진행 - 1

김춘복·2025년 1월 1일
0

TIL : Today I Learned

목록 보기
580/604

Today I Learned

새해 첫날부터 사이드 프로젝트 진행!


Movie Recommendation ++ 진행

사이드 프로젝트 진행 中

  • 프로젝트 구조
.
├── /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                # 앱 실행 파일
  • main.py
    일단 jinja2를 이용해서 SSR 방식으로 렌더링해서 모델, 서비스, 프론트 서버 일단 통합해서 제공중

@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)
  • select_movie.html
    jinja2로 html 구현
{% 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 %}
profile
Backend Dev / Data Engineer

0개의 댓글