반응형
디자인적인 부분에 대한 설명은 따로 하지 않음
프로젝트 구조
사용 모듈
- Vue(Vue-Cli)
- 설치 방법 : https://chb2005.tistory.com/20
- Vuetify
- 설치 방법 : https://chb2005.tistory.com/21
- axios
- npm i axios
- vue-router
- npm i vue-router@3.5.3
구현 코드 설명
App.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
- 별 내용은 없고 vue-router을 이용해 페이지 라우팅
main.js
import Vue from 'vue'
import App from './App.vue'
import vuetify from './plugins/vuetify'
import VueRouter from 'vue-router'
Vue.config.productionTip = false
Vue.use(VueRouter);
import MainPage from './pages/Main.vue'
import BoardPage from './pages/Board.vue'
import WritePage from './pages/Write.vue'
import ContentPage from './pages/Content.vue'
const router = new VueRouter({
mode: 'history',
routes: [
{
path:'/', // 메인 페이지
component: MainPage,
}, {
path: '/board/:id', // 게시판 1,2,3 구분
component: BoardPage,
}, {
path: '/board/:id/write', // 글 작성 페이지
component: WritePage,
}, {
path: '/board/:id/content', // 글 내용 보는 페이지
component: ContentPage,
}
]
});
new Vue({
vuetify,
render: h => h(App),
router,
}).$mount('#app')
pages/Main.vue (메인화면)
<template>
<v-app>
<v-app-bar app color="red" dark>
<v-spacer></v-spacer>
<v-app-bar-title>
<div align="center" :style="{fontSize:'xx-large'} ">메인화면</div>
</v-app-bar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-main>
<v-container>
<v-row>
<v-col cols="12" md="3"></v-col>
<v-col cols="12" md="2" align="center">
<v-btn color="light-green" @click="movetoboard1"
:style="{height:'50px', width:'170px', fontWeight:'bold', fontSize:'large'}">게시판1</v-btn>
</v-col>
<v-col cols="12" md="2" align="center">
<v-btn color="yellow" @click="movetoboard2"
:style="{height:'50px', width:'170px', fontWeight:'bold', fontSize:'large'}">게시판2</v-btn>
</v-col>
<v-col cols="12" md="2" align="center">
<v-btn color="orange" type="submit" @click="movetoboard3"
:style="{height:'50px', width:'170px', fontWeight:'bold', fontSize:'large'}">게시판3</v-btn>
</v-col>
<v-col cols="12" md="3"></v-col>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
methods: {
// 페이지 이동시 params로 게시판 구분, query로 페이지 구분
movetoboard1(){
window.location.href='/board/1/?page=1'
},
movetoboard2(){
window.location.href='/board/2/?page=1'
},
movetoboard3(){
window.location.href='/board/3/?page=1'
}
},
};
</script>
pages/Board.vue (게시판화면(글 리스트))
<template>
<v-app>
<v-app-bar app color="red" dark>
<v-spacer></v-spacer>
<v-app-bar-title>
<div align="center" :style="{fontSize:'xx-large'} ">게시판 {{$route.params.id}}</div>
<!-- params를 받아올 때는 $route.params.id,
query를 받아올 때는 $route.query.id 이런 형식으로 받아옴 -->
</v-app-bar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-main>
<v-container>
<v-row>
<v-col cols="12" md="4"></v-col>
<v-col cols="12" md="2">
<v-btn color="cyan" @click="movetomain"
:style="{height:'50px', width:'170px', fontWeight:'bold', fontSize:'large'}">홈으로</v-btn>
</v-col>
<v-col cols="12" md="2">
<v-btn color="pink accent-1" @click="movetowrite"
:style="{height:'50px', width:'170px', fontWeight:'bold', fontSize:'large'}">글작성</v-btn>
</v-col>
<v-col cols="12" md="2"></v-col>
<v-col cols="12" md="2">
<div :style="{fontSize:'x-large', marginTop: '7px'}">글갯수 : {{cnt}}개</div>
<!-- 현재 게시판 총 글갯수 = cnt -->
</v-col>
</v-row>
<v-row>
<v-simple-table style="width: 100%;">
<thead>
<tr style="font-weight: bolder;">
<td style="width:20%; font-size: x-large;">작성자</td>
<td style="width:50%; font-size: x-large;">제목</td>
<td style="width:30%; font-size: x-large;">작성일</td>
</tr>
</thead>
<tbody>
<tr v-for="item in contentlist" :key="item.id" @click="movetocontent(item.id)">
<td>{{item.writer}}</td>
<td>{{item.title}}</td>
<td>{{item.createdAt.split('T')[0]}}</td>
<!-- Sequelize의 createdAt, updatedAt의 날짜 형식이 '2021-12-10T12:38:52.000Z' 이런 식이여서
split('T')[0]을 통해 날짜만 표시 -->
</tr>
</tbody>
</v-simple-table>
</v-row>
<v-row style="padding-top: 20px;">
<v-spacer/>
<v-btn width="10px" @click="movetopreviouspage"> <!-- 이전페이지로 이동 -->
<v-icon color="red" large> mdi-arrow-left-bold-outline </v-icon>
</v-btn>
<div style="margin-top: 5px; margin-right: 10px; margin-left: 10px;">
{{$route.query.page}}/{{totalpage}} page</div>
<!-- 위와 같이 해줌으로서 '현재페이지/총페이지 page' 식으로 나타냄 -->
<v-btn width="10px" @click="movetonextpage"> <!-- 다음페이지로 이동 -->
<v-icon color="red" large> mdi-arrow-right-bold-outline </v-icon>
</v-btn>
<v-spacer/>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
import axios from 'axios' // backend와 axios 통신을 위해 필요
export default {
data(){
return {
contentlist: [], // 현재 게시판과 페이지에 맞는 글 리스트들
cnt: 0, // 현재 게시판의 총 글 개수
}
},
computed: { // computed는 계산 목적으로 사용된다고 보면 됨
totalpage(){
if(this.cnt == 0) { // 현재 게시판 글 갯수가 0개일때 총 페이지가 0이 되는거 방지
return 1;
}else {
return Math.ceil(this.cnt/10); // (글 갯수/10)한 후 올림 연산을 통해 총 페이지 계산
}
}
},
mounted() { // mounted는 페이지가 켜질때 실행됨, 페이지가 켜질때 글 리스트들을 db에서 불러옴
axios({ // 게시글 작성자, 제목, 작성일 가져오기
url: "http://127.0.0.1:52273/content/boardlist/",
method: "POST",
data: {
// back 서버에 현재 게시판번호와 페이지번호를 넘겨줘야 해당하는 글 리스트 불러올 수 있음
boardnum: this.$route.params.id,
page: this.$route.query.page,
},
}).then(res => {
this.contentlist = res.data;
}).catch(err => {
alert(err);
});
axios({ // 현재 게시판 글 개수 가져오기
url: "http://127.0.0.1:52273/content/boardlistcnt/",
method: "POST",
data: {
boardnum: this.$route.params.id,
},
}).then(res => {
this.cnt = res.data;
}).catch(err => {
alert(err);
});
},
methods: {
movetomain() {
window.location.href="/"
},
movetowrite() {
window.location.href = window.location.pathname + 'write'
// window.location.pathname이 현재 주소를 의미
// 여기다 write를 붙여주면 글 작성 페이지로 라우팅 되게 됨
},
movetocontent(id) { // 클릭된 글의 id를 받아와야 라우팅할때 보낼 수 있음
window.location.href = window.location.pathname + 'content?id=' + id
},
movetopreviouspage(){
if(this.$route.query.page == 1) {
alert('첫번째 페이지입니다!');
} else {
var pp = parseInt(this.$route.query.page) - 1;
window.location.href = window.location.pathname + '?page=' + pp
}
},
movetonextpage(){
if(this.$route.query.page == Math.ceil(this.cnt/10)) {
alert('마지막 페이지입니다!');
} else{
var pp = parseInt(this.$route.query.page) + 1;
window.location.href = window.location.pathname + '?page=' + pp
}
},
},
};
</script>
<style scoped>
.tr,td {
border: 1px solid;
text-align: center;
}
</style>
pages/Content.vue (글 내용화면(+수정,삭제))
<template>
<v-app>
<v-app-bar app color="red" dark>
<v-spacer></v-spacer>
<v-app-bar-title>
<div align="center" :style="{fontSize:'xx-large'} ">게시판 {{$route.params.id}}</div>
</v-app-bar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-main>
<v-container>
<v-row>
<v-col cols="12" md="1">
<v-btn color="cyan" @click="movetomain"
:style="{height:'50px', width:'90px', fontWeight:'bold', fontSize:'large'}">홈으로</v-btn>
</v-col>
<v-col cols="12" md="10">
<v-card>
<div style="width: 300px; margin-left: 100px; padding-top: 20px;">글쓴이 : {{writer}}</div>
<div style="width: 300px; margin-left: 100px; padding-top: 10px;">제목 : {{title}}</div>
<div style="width: 300px; margin-left: 100px; padding-top: 10px;">작성일 : {{createdAt}}</div>
<div style="width: 300px; margin-left: 100px; padding-top: 10px;">최근수정일 : {{updatedAt}}</div>
<div style="width: 300px; margin-left: 100px; padding-top: 10px;">내용</div>
<v-textarea v-model="text" outlined rows="13" style="width: 730px; margin-left: 100px; padding-top: 10px;"
:disabled="editable===false"></v-textarea>
<!-- textarea의 disabled 속성을 통해 원래는 수정을 할 수 없지만
수정 버튼을 누르면 수정할 수 있게끔 바뀜 -->
<v-btn width="100px" style="margin-left: 470px; margin-bottom:20px;" @click="moveback">뒤로가기</v-btn>
<v-btn width="100px" style="margin-left: 30px; margin-bottom:20px;" @click="editcontent"
v-if="editable===false">수정</v-btn>
<v-btn width="100px" style="margin-left: 30px; margin-bottom:20px;" @click="editcontentfinish"
v-if="editable===true">수정완료</v-btn>
<v-btn width="100px" style="margin-left: 30px; margin-bottom:20px;" @click="deletecontent">삭제</v-btn>
</v-card>
</v-col>
<v-col cols="12" md="1"/>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
import axios from 'axios'
export default {
data(){
return {
writer: '', // 작성자
title: '', // 글 제목
createdAt: '', // 작성일
updatedAt: '', // 최근 수정일
text: '', // 글 내용
editable: false, // 수정가능여부 (수정 버튼누르면 true로 바뀜)
}
},
mounted() {
axios({
url: "http://127.0.0.1:52273/content/content/",
method: "POST",
data: {
id: this.$route.query.id
},
}).then(res => {
this.writer = res.data.writer;
this.title = res.data.title;
this.createdAt = res.data.createdAt.split('T')[0];
this.updatedAt = res.data.updatedAt.split('T')[0];
this.text = res.data.text;
}).catch(err => {
alert(err);
});
},
methods: {
moveback() {
window.history.back(); // window.history.back()을 통해 뒤로가기
},
deletecontent() { // 글에 들어가서 삭제버튼 눌렀을 때
axios({
url: "http://127.0.0.1:52273/content/delete/",
method: "POST",
data: {
id: this.$route.query.id
},
}).then(res => {
alert(res.data.message);
window.location.href = window.location.pathname.slice(0,-8) + '/?page=1';
// 삭제 후 그 게시판의 1페이지로 이동
}).catch(err => {
alert(err);
});
},
editcontent() {
this.editable = true;
},
editcontentfinish() { // 수정완료 버튼을 눌렀을 때, 수정된 내용이 저장되야 되기 때문에 back서버와 통신 필요
axios({
url: "http://127.0.0.1:52273/content/edit/",
method: "POST",
data: {
id: this.$route.query.id,
text: this.text,
},
}).then(res => {
alert(res.data.message);
this.editable = false;
}).catch(err => {
alert(err);
});
},
movetomain() {
window.location.href='/';
},
},
};
</script>
pages/Write.vue (글 작성화면)
<template>
<v-app>
<v-app-bar app color="red" dark>
<v-spacer></v-spacer>
<v-app-bar-title>
<div align="center" :style="{fontSize:'xx-large'} ">게시판 {{$route.params.id}} 글쓰기</div>
</v-app-bar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-main>
<v-container>
<v-row>
<v-col cols="12" md="1">
<v-btn color="cyan" @click="movetomain"
:style="{height:'50px', width:'90px', fontWeight:'bold', fontSize:'large'}">홈으로</v-btn>
</v-col>
<v-col cols="12" md="10">
<v-card>
<v-form ref="form" @submit.prevent="onSubmitForm">
<v-text-field v-model="writer" dense outlined label="작성자" style="width: 300px; margin-left: 100px; padding-top: 20px;"
:rules="[v => !!v || '작성자는 필수입니다.']"></v-text-field>
<!-- rules를 통해 작성자와 제목을 안 쓰면 제출 못하게끔 함 -->
<v-text-field v-model="title" dense outlined label="제목" style="width: 500px; margin-left: 100px;"
:rules="[v => !!v || '제목은 필수입니다.']"></v-text-field>
<v-textarea v-model="text" label="내용" outlined rows="13" style="width: 730px; margin-left: 100px;"></v-textarea>
<v-btn width="100px" style="margin-left: 600px; margin-bottom:30px;" @click="moveback">취소</v-btn>
<v-btn width="100px" style="margin-left: 30px; margin-bottom:30px;" type="submit">제출</v-btn>
</v-form>
</v-card>
</v-col>
<v-col cols="12" md="1"/>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
import axios from 'axios'
export default {
data(){
return {
writer: '',
title: '',
text: '',
}
},
methods: {
onSubmitForm(){
if(this.$refs.form.validate()) { // 위에 써준 rules를 만족하면 실행
axios({
url: "http://127.0.0.1:52273/content/write/",
method: "POST",
data: {
boardnum: this.$route.params.id,
writer: this.writer,
title: this.title,
text: this.text,
},
}).then(res => {
alert(res.data.message);
window.history.back();
}).catch(err => {
alert(err);
});
}
},
moveback() {
window.history.back();
},
movetomain() {
window.location.href='/';
},
},
};
</script>
반응형
'Node.js' 카테고리의 다른 글
[Node.js + Vue.js] 게시판 만들기 4. 검색 기능 추가 (0) | 2021.12.11 |
---|---|
[Node.js + Vue.js] 게시판 만들기 3. Node.js를 사용한 Back Server 구현 (0) | 2021.12.11 |
[Node.js + Vue.js] 게시판 만들기 1. 설계 & 결과 (4) | 2021.12.10 |
[Node.js] Sequelize 사용하기 (0) | 2021.12.08 |
[Node.js] DB(MySQL)과 연결하기 (0) | 2021.12.03 |