vue.js/vue.js 2

66. <vue-news> 프로젝트 | 2차 완성

DEV-Front 2022. 8. 7. 23:17
반응형


<main.js>

import Vue from 'vue'
import App from './App.vue'
import { router }  from './routes/index.js';
import { store } from './store/index.js'

Vue.config.productionTip = false


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

routes 폴더 <index.js>

import Vue from 'vue'
import VueRouter from 'vue-router'
import NewsView from '../views/NewsView.vue'
import AskView from '../views/AskView.vue'
import JobsView from '../views/JobsView.vue'
import ItemView from '../views/ItemView.vue'
import UserView from '../views/UserView.vue'
import bus from '../utils/bus.js'
import {store} from '../store/index.js'
// import createListView from '../views/CreateListView'

Vue.use(VueRouter);

export const router = new VueRouter({
    mode: 'history', // url #값 제거
    routes: [ //routes도 커졌을때 모듈화 가능
        {
            path: '/',// url에 대한 정보가 담기는곳, url주소
            redirect: '/news',// url 주소로 갔을때 표시될 컴포넌트
            
        },
        {
            path: '/news', 
            component: NewsView, 
            name: 'news',
            // 하이 오더 컴포넌트
            // 기존에 있던 컴포넌트 위에 컴포넌트가 하나 더 생김
            // component: createListView('NewsView'),

            // 특정 URL로 접근할때 인증정보가 있는지 없는지 확인할때 가장 흔하게 쓰임
            beforeEnter:(to, from, next) => { 
                bus.$emit('start:spinner');

                store.dispatch('FETCH_LIST', to.name)
                    .then(() => {
                        // #5.                  
                        next();
                    })
                    .catch((error) => {
                        console.log(error);
                    });                
            }
        },
        {
            path: '/ask',
            component: AskView,
            name: 'ask',
            // component: createListView('AskView'),
            beforeEnter: (to, from, next) => {
                bus.$emit('start:spinner');

                store.dispatch('FETCH_LIST', to.name)
                    .then(() => {
                        // #5.                     
                        next();
                    })
                    .catch((error) => {
                        console.log(error);
                    });
            }
        },
        {
            path: '/jobs',
            component: JobsView,
            name: 'jobs',
            // component: createListView('JobsView'),
            beforeEnter: (to, from, next) => {
                bus.$emit('start:spinner');

                store.dispatch('FETCH_LIST', to.name)
                    .then(() => {
                        // #5.                 
                        next();
                    })
                    .catch((error) => {
                        console.log(error);
                    });
            }
        },
        {
            path: '/item/:id',
            component: ItemView,
        },
        {
            path: '/user/:id',
            component: UserView,
        },
    ]
});

api 폴더 <index.js>

import axios from 'axios';


// 1. HTTP Requset & Response 와 관련된 기본 설정
const config = {
    baseUrl: 'https://api.hnpwa.com/v0/'
};

// 2. 공통 API 함수들 정리
function fetchNewsList(){
    // return 바로 해준게 핵심
    // return axios.get(config.baseUrl+'/news/1.json');
    return axios.get(`${config.baseUrl}news/1.json`);
}

function fetchJobsList(){
    return axios.get(`${config.baseUrl}jobs/1.json`)
}

function fetchAskList() {
    return axios.get(`${config.baseUrl}ask/1.json`)
}

function fetchList(pageName) {
    return axios.get(`${config.baseUrl}${pageName}.json`)
}

function fetchUserInfo(username){
    // https://api.hnpwa.com/v0/user/32340433.json
    return axios.get(`${config.baseUrl}user/${username}.json`)
}

function fetchCommentItem(id){
    // https://api.hnpwa.com/v0/item/32340433.json
    return axios.get(`${config.baseUrl}item/${id}.json`)
}

// 3. 마지막 내보내기
export { fetchNewsList, fetchJobsList, fetchAskList, fetchUserInfo, fetchCommentItem, fetchList }

store 폴더 <index.js>

import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutation.js';
import actions from './action.js'



Vue.use(Vuex);

// vuex는 상태관리 도구 
// 상태는 여러 컴포넌트 간에 공유되는 데이터 속성
export const store = new Vuex.Store({
    state : { // 3. state에 저장한다.
        // news : [],
        // asks : [],
        // jobs : [],
        user : {},
        items: {},
        list: [], // list 데이터를 3개가 공유하다보니까 오동작남. 페이지 달라도 전 페이지 데이터 보여짐 잠깐
    },
    getters:{
        fetchedNews(state) {
            return state.news
        },
        fetchedAsk(state){
            return state.asks
        },
        fetchedJobs(state){            
            return state.jobs
        },
        fetchedItem(state){
            return state.items
        }
    },
    mutations, // 2. mutations으로 데이터 받아
    actions // 1. 백엔드 API를 actions으로 받고 
        
})

<actions.js>

import { fetchNewsList, fetchAskList, fetchJobsList, fetchUserInfo, fetchCommentItem, fetchList } from '../api/index.js'

export default { // 1. 백엔드 API를 actions으로 받고
    // FETCH_NEWS(context) {
    //    return fetchNewsList()
    //         .then(res => {
    //             context.commit('SET_NEWS', res.data);
    //             return res;
    //         })
    //         .catch(error => console.log(error));
    // },
    // FETCH_ASKS({ commit }) {
    //     return etchAskList()
    //         .then(({ data }) => {
    //             commit('SET_ASKS', data);
    //         })
    //         .catch(error => console.log(error))
    // },
    // FETCH_JOBS({ commit }) {
    //     return fetchJobsList()
    //         .then(({ data }) => {
    //             commit('SET_JBOS', data);
    //         })
    //         .catch(error => console.log(error))
    // },
    FETCH_USER({ commit }, name){
        return fetchUserInfo(name)
            .then(({ data }) => {
                commit('SET_USER', data);
            })
            .catch(error => console.log(error))
    },
    FETCH_ITEM({ commit }, id) {
        return fetchCommentItem(id)
            .then(({ data }) => {
                commit('SET_ITEM', data);
            })
            .catch(error => console.log(error))
    },
    // #2.
    FETCH_LIST({ commit }, pageName){
        // #3.
        return fetchList(pageName)
            .then(res => {
                // #4.
                console.log(4);
                commit('SET_LIST', res.data);
                return res;
            })
            .catch(error => console.log(error))
    }
}

<mutation.js>

export default{ // 2. mutations으로 데이터 받아서
        // SET_NEWS(state, data) {
        //     state.news = data;
        // },
        // SET_ASKS(state, data) {
        //     state.asks = data;
        // },
        // SET_JBOS(state, data) {
        //     state.jobs = data;
        // },
        SET_USER(state, data){
            state.user = data;
        },
        SET_ITEM(state, data){
            state.items = data;
        },
        SET_LIST(state, data){
          state.list = data;
        }

    }

<ListMixin.js>

import bus from '../utils/bus.js'
// mixins
export default {
    // 재사용할 컴포넌트 옵션 & 로직
    mounted() {
        bus.$emit('end:spinner');
    }
    // created() {
    //     bus.$emit('start:spinner');
    //     // #1.
    //     this.$store.dispatch('FETCH_LIST', this.$route.name)
    //         .then(() => {
    //             // #5.
    //             console.log(5);
    //             console.log('fetched');
    //             bus.$emit('end:spinner');
    //         })
    //         .catch((error) => {
    //             console.log(error);
    //         });

    //     // setTimeout(() => {
            
    //     // }, 3000)
    // }
}

<bus.js>

import Vue from 'vue'


// export 차이점
// 1. const로 했을때는
// 보내는곳에선 export const bus = new Vue(); 
// 받는곳에서 import {bus} from './bus.js' 이렇게 받고 

// 2. default로 했을떄는
// 보내는곳에선 export default new Vue();
// 받는곳에선 import bus from './bus.js'
export default new Vue();

<ToolBar.vue>

<template>
    <div class="header">
        <router-link to="/news">News</router-link> |
        <router-link to="/ask">Ask</router-link> |
        <router-link to="/jobs">Jobs</router-link>
    </div>
</template>

<script>
export default {
    name: 'VueAdvancedToolbar',

    data() {
        return {
            
        };
    },

    mounted() {
        
    },

    methods: {
        
    },
};
</script>

<style scoped>
.header{
    color: #fff;
    background-color: #42b883;
    display: flex;
    padding: 8px;
}
.header .router-link-exact-active{
    color: #354952;
}
.header a {
    color: #fff;
}
</style>

<listItem.vue>

<template>
    <div>
        <ul class="news-list">
            <li v-for="(item, i) in listItems" v-bind:key="i" class="post">
                <!--포인트 영역-->
                <div class="points">
                    {{ item.points || 0 }}
                </div>
                <!-- 기타 정보 영역-->
                <div>
                    <!-- 타이블 -->
                    <p class="news-title">
                        <template v-if="item.domain">
                            <a v-bind:href="item.url">
                                {{ item.title }}
                            </a>
                        </template>
                        <template v-else>
                            <router-link v-bind:to="`item/${item.id}`">
                                {{ item.title }}
                            </router-link>
                        </template>
                    </p>

                    <small class="link-text">
                        {{ item.time_ago }}
                        by
                        <router-link v-if="item.user" v-bind:to="`/user/${item.user}`" class="link-text">
                                {{ item.user }}
                        </router-link>                        
                        
                        <a :href="item.url" v-else>
                                {{ item.domain }}
                        </a>
                    </small>
                </div>
            </li>
        </ul>
    </div>
</template>

<script>

export default {
    computed: {
        listItems() {
            return this.$store.state.list;
            // const routename = this.$route.name;

            // if (routename === 'news') {
            //     return this.$store.state.news;
            // } else if (routename === 'ask') {
            //     return this.$store.state.asks;
            // } else if (routename === 'jobs') {
            //     return this.$store.state.jobs;
            // } 
            // return this.$store.state.news;
        }
        
    },
};
</script>

<style scoped>
.news-list {
    margin: 0;
    padding: 0;
}

.post {
    list-style: none;
    display: flex;
    align-items: center;
    border-bottom: 1px solid #eee;
}

.points {
    width: 80px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #42b883;
}

.news-title {
    margin: 0;
}

.link-text {
    color: #828282;
}
</style>

<userProfile.vue>

<template>
    <div>
        <div class="user-container">
            <div>
                <i class="fa-solid fa-user"></i>
            </div>
            <div class="user-description">
                <slot name="username">
                    <!-- 상위 컴포넌트에서 정의할 영역 -->
                </slot>

                <div class="time">
                    <slot name="time">
                        <!-- 상위 컴포넌트에서 정의할 영역 -->
                    </slot>
                    <slot name="karma">
                        <!-- 상위 컴포넌트에서 정의할 영역 -->
                    </slot>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    props:{
        info: Object
    },
};
</script>

<style scoped>

.user-container {
    display: flex;
    align-items: center;
    padding: 0.5rem;
}

.fa-user {
    font-size: 2.5rem;
}

.user-description {
    padding-left: 8px;
}

.time {
    font-size: 0.7rem;
}
</style>

<Spinner.vue>

<template>
    <div class="lds-facebook" v-if="loading">
        <div>
        </div>
        <div>
        </div>
        <div>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        loading: {
            type: Boolean,
            required: true,
        },
    },
}
</script>

<style scoped>
.lds-facebook {
    display: inline-block;
    position: absolute;
    width: 64px;
    height: 64px;
    top: 47%;
    left: 47%;
}

.lds-facebook div {
    display: inline-block;
    position: absolute;
    left: 6px;
    width: 13px;
    background: #42b883;
    animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite;
}

.lds-facebook div:nth-child(1) {
    left: 6px;
    animation-delay: -0.24s;
}

.lds-facebook div:nth-child(2) {
    left: 26px;
    animation-delay: -0.12s;
}

.lds-facebook div:nth-child(3) {
    left: 45px;
    animation-delay: 0;
}

@keyframes lds-facebook {
    0% {
        top: 6px;
        height: 51px;
    }

    50%,
    100% {
        top: 19px;
        height: 26px;
    }
}
</style>

<App.vue>

<template>
  <div id="app">

    <tool-bar></tool-bar>

    <transition name="page">
      <router-view/>
    </transition>

    <spinner :loading="lodingStatus"></spinner>

  </div>
</template>

<script>
import ToolBar from './components/ToolBar.vue';
import Spinner from './components/Spinner.vue';
import bus from './utils/bus.js';

export default {
  components:{
    ToolBar,
    Spinner
  },
  data(){
    return{
      lodingStatus: false,
    };
  },
  methods: {
    startSpinner(){
      this.lodingStatus = true;
    },
    endSpinner(){
      this.lodingStatus = false;
    }
  },
  created(){
    bus.$on('start:spinner', this.startSpinner)
    bus.$on('end:spinner', this.endSpinner)
  },
  beforeDestroy(){
    // 이벤트버스는 이벤트객체가 계속 쌓이기전에 off 시켜야함
    bus.$off('start:spinner', this.startSpinner)
    bus.$off('end:spinner', this.endSpinner)
  }
}
</script>

<style>
body{
  padding: 0;
  margin: 0;
}
a{
  text-decoration: none;
  color: #34495e;
}
a.router-link-exact-active{
  text-decoration: underline;
}
a:hover{
  cursor: pointer;
  color: #42b884;
  text-decoration: underline;
}
/* Router 트렌지션 */
.page-enter-active,
.page-leave-active {
  transition: opacity .5s;
}

.page-enter,
.page-leave-to

/* .fade-leave-active below version 2.1.8 */
  {
  opacity: 0;
}
</style>

<NewsView.vue>

<template>
    <div>
        <list-item></list-item>
    </div>
</template>

<script>
import ListItem  from '../components/listItem.vue';
import ListMixin from '../mixins/ListMixin.js';

export default {   
  components:{
     ListItem
  },
  mixins: [ListMixin]
};
</script>

<style scoped>

</style>

<AskView.vue>

<template>
    <div>
        <list-item></list-item>
    </div>
</template>

<script>
import ListItem from '../components/listItem.vue';
import ListMixin from '../mixins/ListMixin.js';


export default {
    components:{
        ListItem
    },
    mixins: [ListMixin],

};
</script>

<style scoped>

</style>

<JobsView.vue>

<template>
    <div>
        <list-item></list-item>
    </div>
</template>

<script>
import ListItem from '../components/listItem.vue';
import ListMixin from '../mixins/ListMixin.js';


export default {   
    components:{
        ListItem
    },
    mixins: [ListMixin],
};
</script>

<style scoped>

</style>

 


<UserView.vue>

<template>
    <div>
        <user-profile :info="userInfo">
            <div slot="username">{{ userInfo.id }}</div>
            <span slot="time">{{ 'Joined ' + userInfo.created }}, </span>
            <span slot="karma">{{ userInfo.karma }} </span>
        </user-profile>
    </div>
</template>

<script>
import UserProfile from '../components/userProfile.vue'

export default {
    components:{
        UserProfile
    },
    computed: {
        userInfo() {
            return this.$store.state.user;
        }
    },
   created(){        
        const userName = this.$route.params.id;       
        this.$store.dispatch('FETCH_USER', userName);       
   },
   
};
</script>

<style lang="scss" scoped>

</style>

<ItemView.vue>

<template>
    <div>
        <section>
            <user-profile :info="fetchedItem">
                <!-- <div slot="username"> {{ fetchedItem.user }} </div> -->
                <router-link slot="username" :to="`/user/${fetchedItem.user}`">
                    {{ fetchedItem.user }}
                </router-link>

                <template slot="time"> {{ 'Posted ' + fetchedItem.time_ago }} </template>
            </user-profile>
        </section>
        <section>
            <h2>{{ fetchedItem.title }}</h2>
        </section>
        <section>
            <!--질문 댓글-->
            <div v-html="fetchedItem.content"></div>
        </section>
    </div>
</template>

<script>
import UserProfile from '../components/userProfile.vue'
import { mapGetters } from 'vuex';

export default {
    components:{
        UserProfile
    },
    computed:{
        ...mapGetters(['fetchedItem'])
    },
   created(){
      const itemId = this.$route.params.id; // router-link로 넘긴 데이터 받을때
       this.$store.dispatch('FETCH_ITEM', itemId); // actions에 보내는 dispatch
   }
};
</script>

<style scoped>

.user-container{
    display: flex;
    align-items: center;
    padding: 0.5rem;
}
.fa-user{
    font-size: 2.5rem;
}
.user-description{
    padding-left: 8px;
}
.time{
    font-size: 0.7rem;
}
</style>

<CreateListView.js>

import ListView from './ListView.vue';
import bus from '../utils/bus.js'

export default function createListView(name){
    return{        
        // 재사용할 인스턴스(컴포넌트) 옵션들이 들어갈 자리
        name: name,
        created(){
            bus.$emit('start:spinner');
            this.$store.dispatch('FETCH_LIST', this.$route.name)
                .then(() => {
                    console.log('fetched')
                    bus.$emit('end:spinner');
                })
                .catch((error) => {
                    console.log(error);
                });          
        },
        render(createElement){
            return createElement(ListView);
        }
    }
}

 

반응형