View on GitHub

マイリスト

Home

マイリスト

バックエンド

ビュー

src/backend/api/online/views.py

......
# ↓追加
class MoviesRatedView(APIView):
    """評価済み映画リストビュー"""
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        """
        評価済み映画リストを取得する。

        Requests
        --------
        user : User
            ユーザ

        Responses
        ---------
        movies : json
            評価済み映画リスト
        """
        # ユーザ認証
        user = _get_user(request)
        if user is None:
            return Response(
                {'detail': 'Authentication required.'},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        # オブジェクトの取得
        movies = UserMovieRating.objects\
            .select_related('movie')\
            .prefetch_related('movie__movie_genres__genre')\
            .filter(user=user)

        rating_map = {}
        if user:
            rating_map = {
                rating.movie_id: rating for rating in UserMovieRating.objects.filter(user=user)
            }

        # レスポンス
        movies_dict = [
            MovieMapper(
                reclist.movie,
                rating=rating_map.get(reclist.movie_id)
            ).as_dict() for reclist in movies
        ]
        data = {
            'movies': movies_dict,
        }
        return Response(data, status.HTTP_200_OK)
# ↑追加

URL マッピング

src/backend/api/online/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('movies/', views.MoviesView.as_view()),
    path('movies/<int:movie_id>/', views.MovieView.as_view()),
    path('movies/popularity/', views.MoviesPopularityView.as_view()),
    path('movies/movie_similarity/', views.MoviesMovieSimilarityView.as_view()),
    path('movies/bpr/', views.MoviesBPRView.as_view()),
    path('movies/rated/', views.MoviesRatedView.as_view()),  # <- 追加
    path('ratings/', views.RatingView.as_view()),
]

実行確認

サインインビューの作成の手順にしたがってサインインし、アクセストークンを取得してください。取得したトークンを用いて、バックエンドサーバが起動している状態で、下記コマンドを実行してください。

$
 curl -X GET http://localhost:8000/api/online/movies/rated/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

サインインユーザの評価済みの映画リストが表示されます。

フロントエンド

定数の定義

src/frontend/src/constants/styles.ts

/**
 * スタイル関連の定数
 */
export const STYLES = {
  ......
  // BPRベース映画推薦リスト関連
  LIST_MOVIE_BPR_PHRASE: 'あなたにおすすめの映画',

  // ↓追加
  // 評価済み映画リスト関連
  LIST_MOVIE_RATED_PHRASE: 'あなたが評価した映画',
  // ↑追加
  ......
} as const;

API

src/frontend/src/api/movies/getMoviesRated.ts

import { ApiContext, Movie } from "@/types/data";
import { fetcher } from "@/utils";

const context: ApiContext = {
  apiRootUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
};

/**
 * 評価済み映画リスト取得API
 * @returns movies - 評価済み映画リスト
 */
const getMoviesRated = async (): Promise<{ movies: Movie[] }> => {
  const access =
    typeof window !== "undefined" ? localStorage.getItem("access") : null;

  const url = `${context.apiRootUrl?.replace(/\/$/g, "")}/online/movies/rated/`;
  const headers: Record<string, string> = {
    Accept: "application/json",
    "Content-Type": "application/json",
    ...(access ? { Authorization: `Bearer ${access}` } : {}),
  };

  return await fetcher(url, {
    method: "GET",
    headers: headers,
    cache: "no-store",
  });
};

export default getMoviesRated;

コンポーネント

src/frontend/src/app/components/ui/list/ListMovieRated.tsx

"use client";

import { useEffect, useState } from "react";

import { Movie, User } from "@/types/data";
import { STYLES, ERROR_MESSAGES } from "@/constants";
import getMoviesRated from "@/api/movies/getMoviesRated";
import getMyAccount from "@/api/auth/getMyAccount";

import ListMovie from "./ListMovie";
import Loading from "../Loading";

/**
 * 評価済み映画リストコンポーネント
 */
const ListMovieRated = () => {
  const [loading, setLoading] = useState(true);
  const [loadingUser, setLoadingUser] = useState(true);
  const [user, setUser] = useState<User | null>(null);
  const [movies, setMovies] = useState<Movie[]>([]);

  const phrase = STYLES.LIST_MOVIE_RATED_PHRASE;

  useEffect(() => {
    const load = async () => {
      try {
        const user_ = await getMyAccount();
        setUser(user_);
      } catch (e) {
        setUser(null);
      } finally {
        setLoadingUser(false);
      }

      try {
        const { movies: movies_ } = await getMoviesRated();
        setMovies(movies_);
      } catch (e) {
        console.error(ERROR_MESSAGES.MOVIE_GET_FAILED, e);
        setMovies([]);
      } finally {
        setLoading(false);
      }
    };
    load();
  }, []);

  if (loading) {
    return (
      <div className={`${STYLES.LOADING_INLINE}`}>
        <Loading />
      </div>
    );
  }

  return (
    <>
      <ListMovie
        phrase={phrase}
        user={loadingUser ? null : user}
        movies={movies}
      />
    </>
  );
};

export default ListMovieRated;

src/frontend/src/app/components/ui/header/AccountMenu.tsx

......
export default function AccountMenu() {
......
  return (
    <React.Fragment>
      ...(略)...
      <Menu
        ...(略)...
      >
        <MenuItem onClick={handleClose}>
          <AccountCircle fontSize="small" />
          <Link href="/myaccount/">My Account</Link>
        </MenuItem>
        {/* ↓追加 */}
        <MenuItem onClick={handleClose}>
          <Avatar /> <Link href="/mypage/">My page</Link>
        </MenuItem>
        {/* ↑追加 */}
        <Divider />
        ...(略)...
      </Menu>
    </React.Fragment>
  );
}

ページ

src/frontend/src/app/mypage/page.tsx

"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";

import useAuth from "@/hooks/useAuth";

import ListMovieRated from "../components/ui/list/ListMovieRated";
import ListMovieBPR from "../components/ui/list/ListMovieBPR";
import { STYLES } from "@/constants";

const MyPage = () => {
  const router = useRouter();
  const { isAuthenticated, isLoading } = useAuth();

  useEffect(() => {
    if (isLoading || isAuthenticated) return;
    router.replace("/signin");
  }, [isLoading, isAuthenticated, router]);

  if (isLoading || !isAuthenticated) {
    return (
      <div className={`${STYLES.LOADING_PAGE}`}>
        <div className={`${STYLES.LOADING_SPINNER}`}></div>
      </div>
    );
  }

  return (
    <>
      <ListMovieRated />
      <ListMovieBPR />
    </>
  );
};

export default MyPage;

実行確認

サインインした状態で、ブラウザで下記 URL にアクセスするか、アカウントメニューからMy pageをクリックしてください。

マイリストのページが表示されます。サインインしていない状態でアクセスすると、サインインページにリダイレクトされます。