반응형

디자인적인 부분에 대한 설명은 따로 하지 않음

프로젝트 구조

사용 모듈

구현 코드 설명

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>
반응형

↓ 클릭시 이동

복사했습니다!