🔷 이전에 만든 Spring Boot 게시판을 조금 바꿔서 사용한다. 수정된 게시판은 SSAFY에서 제공해준 코드이므로 이곳에는 올리지 않는다.
❗
@RequestBody
import시 swagger의@RequestBody
가 아닌 스프링 프레임워크의 것으로 import가 될 수 있도록 주의한다. 이름이 겹치기 때문에 잘못된 것이 import될 수 있다.
🔷 이전에 만들었던 Axios 실습 자료에서 추가한다. (youtube 관련 컴포넌트와 뷰는 무시한다.)
⚙ 프로젝트 구조
⚙ router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import YoutubeView from '../views/YoutubeView.vue'
import BoardView from '../views/BoardView.vue'
import BoardList from '@/components/board/BoardList.vue'
import BoardCreate from '@/components/board/BoardCreate.vue'
import BoardDetail from '@/components/board/BoardDetail.vue'
import BoardUpdate from '@/components/board/BoardUpdate.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/youtube',
name: 'youtube',
component: YoutubeView
},
{
path: '/board',
name: 'board',
component: BoardView,
children: [
{
path: '',
name: 'boardList',
component: BoardList
},
{
path: 'create',
name: 'boardCreate',
component: BoardCreate
},
{
path: ':id',
name: 'boardDetail',
component: BoardDetail
},
{
path: 'update',
name: 'boardUpdate',
component: BoardUpdate
},
]
},
]
})
export default router
BoardView를 추가하면서 list, create, detail, update 컴포넌트들을 children으로 넣었다.
⚙ stores/board.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
import router from '@/router'
import axios from 'axios'
const REST_BOARD_API = "http://localhost:8080/api/board";
export const useBoardStore = defineStore('board', () => {
const boardList = ref([]);
const getBoardList = () => {
axios.get(REST_BOARD_API)
.then((res) => {
boardList.value = res.data;
})
.catch((err) => {
console.log(err);
});
};
const board = ref({})
const getBoard = (id) => {
axios.get(`${REST_BOARD_API}/${id}`)
.then((res) => {
board.value = res.data;
})
};
const createBoard = (board) => {
axios({
url: REST_BOARD_API,
method: 'POST',
headers: {
"Content-Type": "application/json"
},
data: board
})
.then(() => {
router.push({name:'boardList'})
})
.catch((err) => {
console.log(err);
});
};
const updateBoard = () => {
axios.put(REST_BOARD_API, board.value)
.then(() => {
router.push({ name: 'boardList' });
});
}
const searchBoardList = (searchCondition) => {
axios.get(REST_BOARD_API, {
params: searchCondition
})
.then((res) => {
boardList.value = res.data;
});
}
return { boardList, getBoardList, board, getBoard, createBoard, updateBoard, searchBoardList };
})
REST_BOARD_API
를 호출해 DB에 저장되어 있는 데이터를 가져와 조회, 생성, 수정, 삭제가 가능하도록 한다. 이 때, 게시물의 호출이 반복되므로 prop drilling
을 막기 위해 pinia
의 store
기능을 사용한다.
⚙ components
⚙ 1. common/TheHeaderNav.vue
<template>
<div>
<header>
<nav>
<RouterLink to="/">Home</RouterLink> |
<RouterLink to="/youtube">Youtube</RouterLink> |
<RouterLink :to="{name: 'boardList'}">BoardList</RouterLink> |
<RouterLink :to="{name: 'boardCreate'}">BoardCreate</RouterLink>
</nav>
</header>
</div>
</template>
<script setup>
</script>
<style scoped>
nav {
padding: 30px;
}
nav a {
text-decoration: none;
font-weight: bold;
color: black;
}
nav a.router-link-exact-active {
color: rgb(26, 207, 26);
}
</style>
기존에 App.vue에 있던 nav를 Header로 분리하였다. 고유한 컴포넌트이므로 원칙에 따라 이름 앞에 The를 붙였다.
⚙ 2. board/BoardList.vue
<template>
<div>
<h4>게시글 목록</h4>
<hr>
<table>
<tr>
<th>번호</th>
<th>제목</th>
<th>글쓴이</th>
<th>조회수</th>
<th>등록</th>
</tr>
<tr v-for="board in store.boardList" :key="board.id">
<td>{{ board.id }}</td>
<td>
<RouterLink :to="`/board/${board.id}`">{{ board.title }}</RouterLink>
</td>
<td>{{ board.writer }}</td>
<td>{{ board.viewCnt }}</td>
<td>{{ board.regDate }}</td>
</tr>
</table>
<BoardSearchInput />
</div>
</template>
<script setup>
import { useBoardStore } from "@/stores/board";
import { onMounted } from "vue";
import BoardSearchInput from "./BoardSearchInput.vue";
const store = useBoardStore()
onMounted(() => {
store.getBoardList()
})
</script>
<style scoped></style>
DB에서 게시물 리스트를 꺼내오면 게시판 형식으로 출력한다.
⚙ 3. board/BoardDetail.vue
<template>
<div>
<h4>게시글 상세</h4>
<hr>
<div>{{ store.board.title }}</div>
<div>{{ store.board.writer }}</div>
<div>{{ store.board.regDate }}</div>
<div>{{ store.board.viewCnt }}</div>
<div>{{ store.board.content }}</div>
<button @click="deleteBoard">삭제</button>
<button @click="updateBoard">수정</button>
</div>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router';
import { useBoardStore } from '@/stores/board';
import { onMounted } from 'vue';
import axios from 'axios';
const store = useBoardStore();
const route = useRoute();
const router = useRouter();
onMounted(() => {
store.getBoard(route.params.id);
});
const deleteBoard = () => {
axios.delete(`http://localhost:8080/api/board/${route.params.id}`)
.then(() => {
router.push({ name: 'boardList' })
})
};
const updateBoard = () => {
router.push({ name: 'boardUpdate' });
};
</script>
<style scoped>
</style>
게시물을 누르면 상세 페이지로 이동하는데 게시물의 정보 확인과 해당 게시물을 삭제, 수정이 가능하다.
⚙ 3. board/BoardUpdate.vue
<template>
<div>
<h4>게시글 수정</h4>
<fieldset>
<legend>수정</legend>
<div>
<label for="title">제목 : </label>
<input type="text" id="title" v-model="store.board.title">
</div>
<div>
<label for="writer">글쓴이 : </label>
<input type="text" id="writer" readonly v-model="store.board.writer">
</div>
<div>
<label for="content">내용 : </label>
<textarea id="content" cols="30" rows="10" v-model="store.board.content"></textarea>
</div>
<div>
<button @click="updateBoard">수정</button>
</div>
</fieldset>
</div>
</template>
<script setup>
import { useBoardStore } from '@/stores/board';
const store = useBoardStore();
const updateBoard = () => {
store.updateBoard();
}
</script>
<style scoped>
</style>
상세 게시물의 id를 받아와 해당 id와 일치하는 게시글 수정이 가능하다.
⚙ 5. board/BoardSearchInput.vue
<template>
<div class="search">
<div>
<label>검색 기준 :</label>
<select v-model="searchInfo.key">
<option value='none'>없음</option>
<option value="writer">글쓴이</option>
<option value="title">제목</option>
<option value="content">내용</option>
</select>
</div>
<div>
<label>검색 내용 :</label>
<input type="text" v-model="searchInfo.word" />
</div>
<div>
<label>정렬 기준 :</label>
<select v-model="searchInfo.orderBy">
<option value='none'>없음</option>
<option value="writer">글쓴이</option>
<option value="title">제목</option>
<option value="view_cnt">조회수</option>
</select>
</div>
<div>
<label>정렬 방향 :</label>
<select v-model="searchInfo.orderByDir">
<option value="asc">오름차순</option>
<option value="desc">내림차순</option>
</select>
</div>
<div>
<button @click="searchBoardList">검색</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useBoardStore } from '@/stores/board'
const store = useBoardStore()
const searchInfo = ref({
key: 'none',
word: '',
orderBy: 'none',
orderByDir: 'asc'
})
const searchBoardList = function () {
store.searchBoardList(searchInfo.value)
}
</script>
<style scoped>
.search {
display: flex;
}
</style>
이전에 사용했던 검색 기능이다.
api가 SearchCondition을 기반으로 검색 결과를 도출하면 이곳에서 출력한다.
⚙ 6. board/BoardCreate.vue
<template>
<div>
<h4>게시글 작성</h4>
<fieldset>
<legend>등록</legend>
<div>
<label for="title">제목 : </label>
<input type="text" id="title" v-model="board.title">
</div>
<div>
<label for="writer">글쓴이 : </label>
<input type="text" id="writer" v-model="board.writer">
</div>
<div>
<label for="content">내용 : </label>
<textarea id="content" cols="30" rows="10" v-model="board.content"></textarea>
</div>
<div>
<button @click="createBoard">등록</button>
</div>
</fieldset>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useBoardStore } from "@/stores/board";
const store = useBoardStore()
const board = ref({
title: '',
writer: '',
content: ''
})
const createBoard = function () {
store.createBoard(board.value)
}
</script>
<style scoped></style>
새로운 게시물을 생성하여 API를 통해 DB에 저장한다.
🖨 결과
분량이 커서 그런지 velog의 문제인지 더 이상 이미지가 올라가지 않으므로 목록, 상세 페이지, 수정 페이지만 이곳에 올린다.
정신없이 지나갔지만 완성은 잘 되어서 다행이다.