Vue 게시판(1)

shinhyocheol·2021년 7월 15일
1

Vue

목록 보기
1/2

JPA 게시판 API

해당 API를 통해 데이터를 요청하여 Vue 게시판을 그려보고자 한다.

또한 이전에 말했던 로그인 기능까지 추가해서 로그인+게시판으로 구성을 짤 계획이다.

이후 해보고 싶은 기능을 추가하며 나만의 낙서장으로 만들꺼다 🤪

우선 프로젝트부터 생성을 해보자. Vue 설치와 실행

예전에 작성한 글인데 버전만 다를뿐 생성방식은 동일하다. 다만 이전에 설치할때

vue2였다면 이번에는 vue3으로 작성을 진행함. 문법에 차이가 살짝 있으니

반드시 버전에 유의해야함!! 그리고 그 외 생성과 동시에 npm을 통해 추가로 설치한 도구들이 있다.

  • vue router 3.x.x (router)
  • axios 0.2x.x (ajax)
  • v-pagination-3 (테이블 페이징 컴포넌트)

App.vue

<template>
  <router-view></router-view>
</template>

<script>

export default {
  name: 'App'
}
</script>

router/index.js

import { createWebHistory, createRouter } from 'vue-router';

const routes = [
    {
      path: '/hello',
      name: 'Hello',
      component: () => import('@/components/HelloWorld'), // 동적 import
    },
    {
      path: '/',
      redirect: '/posts',
      name: 'TheContainer',
      component: () => import('@/components/layout/TheContainer'),
      children: [
        {
          path: '/posts',
          name: 'Posts',
          component: () => import('@/components/posts/Posts'),
        },
        {
          path: '/posts/detail',
          name: 'PostsDetail',
          component: () => import('@/components/posts/PostsDetail'),
        },
        {
          path: '/posts/reg',
          name: 'PostsReg',
          component: () => import('@/components/posts/PostsReg')
        }
      ]
    }
  ];

export const router = createRouter({
    history: createWebHistory(),
    routes,
  });

store/index.js

import { createStore } from 'vuex'
import createPersistedState from "vuex-persistedstate"
import modules from './modules.js'

const persistedState = createPersistedState({
    paths: ['token', 'id', 'name', 'role', 'nickname']
})

export const store = createStore({
    state:      modules.state,
    getters:    modules.getters,
    mutations:  modules.mutations,
    actions:    modules.actions,
    plugins:    [persistedState]
})

store/module.js

import axios from 'axios'
import { router } from '../router/index.js'

const state = {
  token: null,
  id: null,
  name: null,
  role: null,
  email: null,
  nickname: null
}

const getters = {
  'token': state => state.token,
  'id': state => state.id,
  'email': state => state.email,
  'nickname': state => state.nickname,
  'role': state => state.role
}

const mutations = {
  login (state, item) {
      state.token = item.headers['accesstoken']
      state.id = item.data['id']
      state.role = item.data['role']
      state.email = item.data['email']
      state.nickname = item.data['nickname']
  },
  logout (state) {
      state.token = null
      state.id = null
      state.role = null
      state.email = null
      state.nickname = null
  }
}

const actions = {
  login ({commit}, {id, password}) {
      const params = {
          "email": id,
          "password": password
      }
      axios.post("http://localhost:8080/signin", JSON.stringify(params), {
        headers: { 'content-type': 'application/json' }
      }).then(res => {
        alert("정보가 확인되었습니다.\n환영합니다!")
        commit('login', res)
        router.push("/posts")
      }).catch(e => {
        console.log(e)
        alert("로그인 요청에 문제가 발생했습니다.")
      })

  },
  logout ({commit}) {
      commit('logout')
  }
}

export default {
  state,
  getters,
  mutations,
  actions
}

main.js

import { createApp } from 'vue'

import { store } from './store'
import { router } from './router/index.js'
import axios from 'axios'

import App from './App.vue'

// Create Vue Instance
const app = createApp(App)
app.use(store)
app.use(router)

app.config.globalProperties.axios=axios

app.mount('#app')

index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    
    <title>ViewPlatform</title>
  </head>
  <body style="background: #dee2e6;">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script>
	   <div id="app"></div>
  </body>
</html>

그리고 부트스트랩을 사용하기 위해 CDN을 index.html 추가해주었다.

위에서 작업한 파일들과 앞으로 추가될 컴포넌트등등이 이 공간에 표현된다.

위 라우터를 보면 알 수 있겠지만 TheContainer라는 공간에 각 컴포넌트들이 하위 컴포넌트로 소속되어있다.

헤더나 푸터 그리고 메뉴바 등등을 생각해볼때 일일히 그려주는 것은 비효율적이니 따로 나눠서 작성했다.

TheContainer라는 공간에 고정으로 배치해놓고 영역별 컴포넌트만 교체되어 화면에 표시하면 되니까 😬

TheContainer.vue

<template>
  <div class="wrapper">
    <!-- 헤더 -->
    <Header />  
    <div class="content-view">
      <router-view></router-view>
    </div>
  </div>
</template>
<script>
import Header from '@/components/layout/Header' 
export default {
  components: {Header},
  name: "TheContainer"
}
</script>

Header.vue

<template>
  <div class="main-header">
    <div class="navbar navbar-dark bg-dark shadow-sm mb-3">
      <div class="container d-flex justify-content-between">
        <a href="#" class="navbar-brand d-flex align-items-right">
          <strong class="text-right">Platform</strong>
        </a>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'TopBar'
}
</script>

API를 작성했을때 글 목록 조회를 가장 먼저 작업했으니 여기서도 마찬가지 글 목록 테이블 화면부터 그렸다.

테이블도 마찬가지로 다른 곳에서 사용할 가능성이 높기에 공통 테이블 컴포넌트를 만들어놓고 필요한곳에서

import하여 데이터만 전달해주면 알아서 테이블을 그려주는 컴포넌트를 작성했다.

Table.vue

<!-- 공용 테이블 (table.vue) 컴포넌트 -->
<template>
  <div class="table-scrollable white-backgroud">
    <table class="table table-striped table-bordered table-hover">
      <thead>
        <tr class="heading">
          <slot name="header" /> 
        </tr>
      </thead>
      <tbody>
      <template v-for="(row, index) in list" :key="index">
        <tr>
          <slot v-bind:row="row" /> 
        </tr>
      </template>
      <tr v-if="list.length === 0">
        <td colspan="30" class="text-center">데이터가 존재하지 않습니다.</td>
      </tr>
      </tbody>
    </table>
  </div>
  <pagination 
    v-model="page" 
    :records="count" 
    :per-page="10" 
    @paginate="pagingHandle"
  />
</template>
<script>
import Pagination from 'v-pagination-3'
export default {
  components: {Pagination},
  name: "Table",
  props: ['list', 'cnt', 'getData'],
  data () {
    return {
      page: 1,
      count: 0
    }
  },
  watch: {
    list() {
      this.count = this.cnt
    }
  },
  methods: {
    pagingHandle (page) {
      const params = new URLSearchParams()
      params.append("page", page)
      this.getData(params)
    }
  }
}
</script>

Posts.vue(글 목록)

<template>
  <div class="container">

      <div class="card">
        <div class="card-body">
          <Table :list="list"
                 :cnt="totalCnt"
                 :getData="getPosts"
                  id="table" >
            <template v-slot:header>
              <th>#</th>
              <th>제목</th>
              <th>본문</th>
              <th>작성자</th>
              <th>작성일</th>
              <th class="text-center">비고</th>
            </template>
            <template v-slot:default="slotProps">
              <td>{{slotProps.row.id}}</td>
              <td>{{slotProps.row.title}}</td>
              <td>{{slotProps.row.content}}</td>
              <td>{{slotProps.row.author}}</td>
              <td>{{slotProps.row.createdDate}}</td>
              <td class="text-center">
                <router-link :to="{ 
                                path:'/posts/detail', 
                                query: { id: slotProps.row.id }
                              }"
                              class="btn btn-sm btn-primary">
                  <i class="fa fa-search" />
                </router-link>
              </td>
            </template> 
          </Table>
        </div>
      </div>

      <div class="row mt-3 float-right">
        <div class="col-auto">
          <router-link :to="{path:'/posts/reg'}"
                        class="btn btn-primary">
            <i class="fa fa-plus">등록</i>
          </router-link>
        </div>
      </div>

  </div>
</template>

<script>
import Table from '@/components/layout/Table'
export default {
  name: 'Posts',
  components: {Table},
  data () {
    return {
      posts: [],
      cnt : 0
    }
  },
  computed: {
    list () {
      return this.posts
    },
    totalCnt () {
      return this.cnt
    }
  },
  mounted () {
    this.handleService()
  },
  methods: {
    handleService() {
      var params = new URLSearchParams()
      params.append("page", 1)
      this.getPosts(params)
    },
    getPosts(params) {
      this.axios.get("http://127.0.0.1:8080/posts?" + params)
      .then(res => {
        this.posts = res.data.content
        this.cnt = res.data.totalElements
      }).catch(e => {
        alert(e)
      })
    },
  }
}
</script>

이렇게 게시글 조회 화면을 그리는 컴포넌트까지 작성했다. 근데 getPosts()를 보면 API로 데이터를 요청할 때

파라미터로 page라는 변수를 같이 넘기는 부분이 확인된다. 그렇다 페이징처리를 하기위해 페이징 컴포넌트를

같이 설치했다고 위에서 서술했고 공통 테이블 컴포넌트에서는 페이지 번호 버튼을 통해 데이터 요청 시 몇번째

페이지인지 번호를 넘겨주는 것이다. 그렇다면 API도 페이징 처리를 위해 수정이 되야 한다.

글 목록 조회 Controller

Controller(글 목록 조회)

**/*************************** 수정 전 ***************************/**
@GetMapping(value = {""})
public ResponseEntity<List<PostsResDto>> getPosts() {
    return ResponseEntity.ok()
			.body(postsService.getPostsService(pageble));
}

**/*************************** 수정 후 ***************************/**
@GetMapping(value = {""})
public ResponseEntity<PageImpl<PostsResDto>> getPosts(
	@RequestParam Integer page) {
	
    PageRequest pageble = PageRequest.of(page - 1, 10);

    return ResponseEntity.ok()
			.body(postsService.getPostsService(pageble));
}

Service(글 목록 조회)

**/*************************** 수정 전 ***************************/**
public List<PostsResDto> getPostsService() {

    List<Posts> entityList = postsRepository.findAll();

    return entityList.stream()
              .map(entity -> customModelMapper.toDto(entity, PostsResDto.class))
              .collect(Collectors.toList());
}

**/*************************** 수정 후 ***************************/**
public PageImpl<PostsResDto> getPostsService(PageRequest pageble) {

     Page<Posts> entityList = postsRepository.findAll(pageble);

     List<PostsResDto> result = entityList.stream()
              .map(entity -> customModelMapper.toDto(entity, PostsResDto.class))
              .collect(Collectors.toList());

    return new PageImpl<PostsResDto>(result, pageble, entityList.getTotalElements());
}

PageRequest : Pageable 인터페이스의 구현체이며 페이지 번호(필수), 페이지 사이즈(필수), 정렬방법(선택)를 매개변수로 받는다.

수정사항은 크게 없다. 특별하게 리턴타입과 Pageable을 매개변수로 넘겨줘야하는정도??

그리고 JpaRepository는 Pageable객체를 매개변수로 받아 리스트를 조회하는 메서드가
이미 준비되어있다.

이렇게 오버로드 방식으로 같은 메서드명을 가지는 기능이 준비되어있다. 리턴 타입이 Page인데

그리고 프론트에서 페이징 버튼처리를 위해 총 레코드의 수도 필요한데 Page의 getTotalElements를

통해 해당 값도 구할 수있다. 어쨌든 API의 수정은 여기까지이며 바로 화면을 확인해보자.

이렇게 글 목록을 조회할 수 있는 테이블 화면을 만들어봤다. 다음 글에서는 상세조회, 등록, 수정, 삭제기능을 추가하고 가능하다면 로그인 화면과 기능까지 추가하려고 한다.

profile
놀고싶다

2개의 댓글

comment-user-thumbnail
2021년 11월 19일

안녕하세요!
이 글 덕분에 Vue.js 라우터와 게시판 부분을 잘 이해하고 공부하고 있습니다.
혹시 main.js에서 ./store부분은 어떻게 작성되는지 알 수 있을까요?

1개의 답글