본문 바로가기

프로젝트/미니 프로젝트

vue(CLI) + vue bootstrap + axios + movie api(tmdb)를 활용한 vue 영화 소개 사이트 제작하기 (vue movie, 영화 사이트, vue 영화 사이트)

반응형

loy124.github.io/vue-movie/

(완성된 사이트)

 

vue-movie

 

loy124.github.io

 

(코드주소)

github.com/loy124/vue-movie

vue + vue bootstrap + axios + movie api(tmdb) 를 활용해서 영화 소개 사이트를 만들었다. 

 

package.json

 

npm install -g @vue/cli

 vue cli 를 사용하기위해 vue-cli를 global 설치해준다.

 

그후 vue create vue-movie 를 통해 해당 프로젝트를 생성해준다.

 

 

해당과 같이 프로젝트를 세팅해준다.

 

npm i vue bootstrap-vue

를 install 한 후에 

 

 

main.js 를 수정해준다.

 

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.config.productionTip = false

// Install BootstrapVue
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

 

 

 

먼저 API를 활용하기 위해 

utils/axios.js 에 API 요청을 정의해두었다 (모듈화)

 

developers.themoviedb.org/3/getting-started/introduction   

 

해당 DB의 API를 활용해서 프로젝트를 구성하였다.

 

utils/axios.js

import axios from "axios";

const request = axios.create({
  baseURL: "https://api.themoviedb.org/3/",
  params: {
    api_key: "cb772a50acc4cd6917b12854484b9d91",
    language: "ko-KR",
  },
  
});
// https://image.tmdb.org/t/p/w300/ApiBzeaa95TNYliSbQ8pJv4Fje7.jpg
export const movieApi = {
  nowPlaying: () => request.get("movie/now_playing"),
  popular: () => request.get("movie/popular"),
  upComing: () => request.get("movie/upcoming"),
  // append to response에 대한 설명 https://developers.themoviedb.org/3/get1ting-started/append-to-response
  movieDetail: (id) =>
    request.get(`movie/${id}`, {
      params: { append_to_response: "videos" },
    }),

  search: (keyword) =>
    request.get("search/movie", {
      params: {
        query: keyword,
      },
    }),
};

 

axios.create를 활용해서 axios의 기본정의를 지정해주고  기본 parameter들도 정의해 주었다..

 

 

기본 라우터 구조

 

router/index.js

import Vue from "vue";
import VueRouter from "vue-router";
import Main from "../views/Main.vue";
import MovieDetail from "../views/MovieDetail.vue";
import Search from "../views/Search.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component: Main,
  },

  {
    path: "/detail/:id",
    name: "Detail",
    component:MovieDetail
  },
  {
    path:"/search",
    name:"Search",
    component:Search
  }
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

export default router;

 

기본 라우터들은 Main, Detail, Search 세가지로 구성이 되어있다.

 

이제 view에서 사용할 Component들을 두개 정의한다.

 

 

 

 

 

 

movieText.vue는

해당 부분이 반복되므로 component 화를 해준 부분이다.

 

components/MovieText.vue

 

<template>
  <div class="h4 ml-3 mt-5 mb-4 text-white">{{text}}</div>
</template>

<script>
export default {
    props:{
        text:String
    }
}
</script>

<style>

</style>

 

MovieList.vue는 해당과같이 영화리스트가 반복이되는데 이에 대한 중복을 최소화 하기 위해서 제작하였다.

movieList를 props로 받아온다.

 

 

 

 

components/MovieLists.vue

<template>
  <div class="d-flex flex-wrap" v-if="movieList">
    <div
      class="movie-card"
      style="width:125px;"
      v-for="li in movieList"
      :key="li.id"
    >
      <div  @click="goDetail(li.id)" v-if="li">
        <b-img style="width:125px; height:180px;" v-if="li.poster_path" fluid :src="image(li.poster_path)" alt="Image 2"></b-img>
        <b-img style="width:125px; height:180px;" v-else fluid :src="noImage" alt="Image 2"></b-img>
        <div class="movie-information">
          <div class="movie-title">{{ li.title }}</div>
          <div class="movie-date" v-if="li.release_date">{{ li.release_date.split("-")[0] }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  props: ['movieList'],
  data(){
    return {
      noImage: require("../assets/error.jpg")
    }
  }
,
  methods: {
    image(img) {
      return `https://image.tmdb.org/t/p/w300/${img}`;
    },
     goDetail(id){
      // console.log(id);
      this.$router.push(`detail/${id}`);
    }
  },
};
</script>

<style></style>

 

 

 

이제 component를 정의해주었으니 views에 해당 값들을 뿌려서 보여주면 된다.

 

 

해당 movie 프로젝트에서는 데이터를 로딩하기전 로딩 화면을 활용하는데 

이를 Vuex를 활용해서 나타내 주었다.

 

로딩시에 loading을 trure로 만들어 해당 창이 보이게하고 아닐경우에는 사라지게 하는 방식으로 진행하였다.

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    loading: true,

  },
  mutations: {
    SET_LOADING(state, data){
      state.loading = data;
    },
   
    
  },
  actions: {
  },
  modules: {
  }
})

 

또한 해당 로딩 컴포넌트(vue-bootstrap)를 

 

App.vue에 정의해 주었다. + 라우터 들도 정의해 두었다.

 

App.vue

<template>
  <div id="app">
    <div id="nav" class="text-left">
      <router-link  to="/">Home</router-link> |
      <router-link to="/search">Search</router-link>
    </div>
    <b-spinner
      class="d-block ml-auto mr-auto"
      v-if="loading"
      label="Spinning"
    ></b-spinner>
    <router-view />
  </div>
</template>
<script>
import { mapState } from "vuex";
export default {
  computed: {
    ...mapState(["loading"]),
  },
};
</script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400&display=swap");
* {
  color: #ffffff;
}
body {
  font-family: "Noto Sans KR", sans-serif;
}
#app {
  background-color: rgb(20, 20, 20);
  /* font-family: Avenir, Helvetica, Arial, sans-serif; */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  color: #2c3e50;
  min-height: 100vh;
}

#nav {
  text-align: center;
  padding: 20px;
  position: relative;
  z-index: 99;
  background-color: black;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
.router-link-active {
  /* color: white !important; */
}
.router-link-exact-active {
  color: white !important;
}
</style>

 

 

 

 

main.vue는 메인화면을 나타내며 NowPlaying, Popluar,  Comming soon의 데이터를 movieList component를 할용해 props로 데이터를 바인딩해서 뿌려준다.

views/Main.vue

<template>
  <div>
    <div class="d-flex flex-wrap" v-if="nowPlaying">
      <div class="h4 ml-3 mt-5 mb-4 text-white">Now Playing</div>
      <MovieLists :movieList="nowPlaying"></MovieLists>
      <MovieText :text="'Popular'"></MovieText>
      <MovieLists :movieList="popular"></MovieLists>
      <!-- <div class="h4 ml-3 mt-5 mb-4 text-white">Comming Soon</div> -->
      <MovieText :text="'Comming Soon'"></MovieText>
      <MovieLists :movieList="upComming"></MovieLists>
      <!-- <MovieLists :movieList="movieList"></MovieLists> -->
      <!-- <div
        class="movie-card"
        style="width:125px;"
        v-for="li in movieList"
        :key="li.id"
      >
        <movie-card :li="li" :image="image"></movie-card>
      </div> -->
    </div>
  </div>
</template>

<script>
// import MovieCard from "../components/MovieCard";
import MovieLists from "../components/MovieLists";
import MovieText from "../components/MovieText";
import { movieApi } from "../utils/axios";
import { mapMutations } from "vuex";
export default {
  data() {
    return {
      nowPlaying: {},
      popular: {},
      upComming: {},
    };
  },
  components: {
    // MovieCard,
    MovieText,
    MovieLists,
  },
  methods: {
    ...mapMutations(["SET_LOADING"]),
    // image(img) {
    //   return `https://image.tmdb.org/t/p/w300/${img}`;
    // },
  },
  
  created() {
    this.SET_LOADING(true);
  },
  async mounted() {
    try {
      // vuex를 통해서 로딩을 없애준다.
      const { data } = await movieApi.nowPlaying();
      console.log(data.results);
      this.movieList = data.results;
      const { nowPlaying, popular, upComing } = movieApi;
      const requestArr = [nowPlaying, popular, upComing];
      const [now, pop, up] = await Promise.all(
        requestArr.map((li) => li().then((res) => res.data.results))
      );
      console.log("pop");
      console.log(pop);
      this.SET_LOADING(false);
      this.nowPlaying = now;
      this.popular = pop;
      this.upComming = up;
    } catch (error) {
      this.movieList = "해당 자료가 존재하지 않습니다.";
    }
  },
};
</script>

<style>
.movie-card {
  margin: 12px;
  width: 125px;
  font-size: 12px;
  font-weight: 400;
}
.movie-card:hover {
  opacity: 0.5;
  cursor: pointer;
}
.movie-card > img {
  height: 180px;
  border-radius: 8px;
}
.movie-information {
  margin-top: 7px;
}

.movie-date {
  font-size: 10px;
  margin-top: 5px;
  color: #cccccc;
}
</style>

 

 

movieDetail은 말그대로 영화의 상세 정보들이 기록되어있따.

년도, 상영시간, 장르, 홈페이지 링크, 

그리고 Youtube 를 통해 예고 영상을 출력해준다.

Views/MovieDetail.vue

 

<template>
  <div class="movie-detail" v-if="movieDetail && movieDetail.backdrop_path">
    <div
      class="movie-detail-image"
      :style="{ backgroundImage: `url(${image(movieDetail.backdrop_path)})` }"
    ></div>
    <div class="movie-content d-flex">
      <div style="">
        <img
          class="mt-2 "
          style="height:80vh;"
          :src="image(movieDetail.poster_path)"
        />
      </div>
      <div class="ml-4 w-75">
        <h1 class="movie-title">{{ movieDetail.title }}</h1>
        <div class="movie-information-wrapper mt-4 d-flex align-items-center">
          <div>{{ movieDetail.release_date.split("-")[0] }}</div>
          <span class="ml-1">ㆍ</span>
          <div>{{ movieDetail.runtime }} 분</div>
          <span class="ml-1">ㆍ</span>
          <div class="ml-2 d-flex">
            <div
              class="genres"
              v-for="genre in movieDetail.genres"
              :key="genre.id"
            >
              {{ genre.name }}
            </div>
          </div>
          <span v-if="movieDetail.homepage" class="ml-1">ㆍ</span>
          <a
            v-if="movieDetail.homepage"
            class="ml-1 h4 homepage-link"
            target="_blank"
            :href="movieDetail.homepage"
            ><svg
              width="1em"
              height="1em"
              viewBox="0 0 16 16"
              class="bi bi-link-45deg"
              fill="currentColor"
              xmlns="https://www.w3.org/2000/svg"
            >
              <path
                d="M4.715 6.542L3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.001 1.001 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"
              />
              <path
                d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 0 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 0 0-4.243-4.243L6.586 4.672z"
              /></svg
          ></a>
        </div>
        <div class="movie-overview mt-3">{{ movieDetail.overview }}</div>
        <!-- <div class="mt-3"> -->
        <!-- <b-embed
          v-if="movieDetail.videos && movieDetail.videos.results"
            type="iframe"
             :key="movieDetail.videos.results[0].key"
             
            aspect="16by9 "
             :src="youtube(movieDetail.videos.results[0].key)"
            allowfullscreen
          ></b-embed> -->
        <div v-if="movieDetail.videos && movieDetail.videos.results">
          <iframe
          v-if="movieDetail.videos.results[0]"
            class="mt-5"
            :key="movieDetail.videos.results[0].key"
            width="640"
            height="360"
            :src="youtube(movieDetail.videos.results[0].key)"
            frameborder="0"
            allow=" fullscreen "
          >
          </iframe>
        </div>
        <!-- </div> -->
      </div>
    </div>
  </div>
</template>

<script>
import { movieApi } from "../utils/axios";
import { mapMutations } from "vuex";
export default {
  data() {
    return {
      movieDetail: {},
    };
  },
  async mounted() {
    this.SET_LOADING(true);
    console.log(this.$route);
    console.log(this.$route.params.id);
    const { id } = this.$route.params;
    const { data } = await movieApi.movieDetail(id);
    // axios 요청 보내기
    console.log(data);
    this.movieDetail = data;
    this.SET_LOADING(false);
    // backdro
  },
  methods: {
    ...mapMutations(["SET_LOADING"]),
    image(img) {
      console.log();
      return `https://image.tmdb.org/t/p/original/${img}`;
    },
    youtube(src) {
      return `https://www.youtube.com/embed/${src}`;
    },
  },
};
</script>
}

<style>
.movie-detail {
  /* z-index: 99; */
  position: relative;
  padding: 40px 40px;
}
.movie-detail-image {
  background-size: cover;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 0;

  /* filter: grayscale(px); */
}
.movie-detail-image::after {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  min-height: 100vh;
  background-color: rgb(40, 40, 40);
  opacity: 0.8;
  content: "";
  display: block;
}
.movie-content {
  position: relative;
  z-index: 999;
}
.movie-title {
  margin-left: 5px;
}
.movie-information-wrapper {
  font-size: 13px;
}
.genres {
  display: flex;
  align-items: center;
}
.genres:not(:first-of-type)::before {
  content: "/";

  /* background-color: white; */
  /* display: inline-block; */
  margin-bottom: 4px;
  margin-left: 6px;
  margin-right: 1px;
  font-size: 20px;
}
.movie-overview {
  max-width: 60%;
  font-size: 14px;
  color: #dddddddd;
}
.homepage-link:hover {
  opacity: 0.5;
}
/* .aa {
  min-height: 100vh;
  background-color: rgb(40, 40, 40);
  opacity: 0.8;
} */
</style>

 

 

 

 

마지막으로 검색 파트이다

검색어를 입력해서 데이터를 가져온후 해당 데이터들을 movieLists를 활용해서 데이터를 바인딩해준다. 

 

 

 

Search.vue

 

<template>
  <div>
    <b-form @submit.prevent="onSearch">
        <b-form-input class="border-black" v-model="keyword" placeholder="영화 제목을 입력하세요."></b-form-input>
    </b-form>
    <MovieText v-if="movieList" :text="'Search Result'"></MovieText>
    <MovieLists :movieList="movieList"></MovieLists>
  </div>
</template>

<script>
import MovieLists from "../components/MovieLists";
import MovieText from "../components/MovieText";
import { movieApi } from '../utils/axios';
import { mapMutations } from "vuex";
export default {
    data(){
        return {
            keyword:"",
            movieList:""
        }
    },
components:{
    MovieText,
    MovieLists
},
created(){
  this.SET_LOADING(false);
},
methods:{
    ...mapMutations(["SET_LOADING"]),
    async onSearch(){
        this.SET_LOADING(true);
        console.log(this.keyword);
        if(!this.keyword){
            alert("영화 제목을 입력하세요!");
            this.keyword = ""
            return;
        }
        const {data} = await movieApi.search(this.keyword);
        console.log(data);
        this.movieList=data.results;
        this.SET_LOADING(false);
        this.keyword = ""
    }    
}

}
</script>

<style>
.search-input{
    color:black;
}
</style>

 

 

 

반응형