yarn 으로 시작하기
1. yarn 설치
https://pakss328.medium.com/yarn이란-b4e8edf1638b
2. yarn global add @vue/cli-init
3. vue init webpack bootstrap-bbs
$ vue init webpack bootstrap-bbs ? Project name vue-board ? Project description A Vue.js project ? Author junhochoi ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? No ? Set up unit tests Yes ? Pick a test runner jest ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recommended) yarn vue-cli ·· Generated "vue-board".
main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
// Make BootstrapVue available throughout your project
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
App.vue
<template>
<div id="app">
<Header />
<!-- <img src="./assets/logo.png"> -->
<router-view/>
</div>
</template>
<script>
import Header from '@/components/Header'
export default {
name: 'App',
components:{
Header,
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Board from '@/components/Board';
import ContentDetail from '@/components/ContentDetail';
import Create from '@/components/Create';
Vue.use(Router)
export default new Router({
mode:"history",
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/board/free',
name: 'Board',
component: Board
},
{
path: '/board/free/detail/:contentId',
name: 'ContentDetail',
component: ContentDetail
},
{
path: '/board/free/create/:contentId?',
name: 'Create',
component: Create
},
]
})
components/Header.vue
<template>
<div>
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand href="#">Vue.js로 게시판 만들기</b-navbar-brand>
<b-navbar-toggle target="nav_collapse"/>
<b-collapse is-nav id="nav_collapse">
<b-navbar-nav>
<b-nav-item href="#">공지사항</b-nav-item>
<b-nav-item to="/board/free">자유게시판</b-nav-item>
<b-nav-item href="#">구인구직</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {};
}
};
</script>
components/Board.vue
<template>
<div>
<b-table striped hover :items="items" :fields="fields" @row-clicked="rowClick"></b-table>
<b-button @click="writeContent">글쓰기</b-button>
</div>
</template>
<script>
import data from '@/data';
export default {
data() {
let items=data.Content.sort((a,b)=>{ return b.content_id-a.content_id})
items=items.map(contentItem=>{ return {...contentItem, user_name:data.User.filter(userItem=>userItem.user_id===contentItem.user_id)[0].name}})
return {
fields:[
{
key: 'content_id',
label: '글번호',
},
{
key: 'title',
label: '제목',
},
{
key: 'created_at',
label: '작성일',
},
{
key: 'user_name',
label: '글쓴',
}
],
items:items
}
},
methods:{
rowClick(item, index, e){
this.$router.push({
path: `/board/free/detail/${item.content_id}`
});
},
writeContent(){
this.$router.push({
path:'/board/free/create'
});
}
}
}
</script>
components/Create.vue
<template>
<b-container fluid class="mt-2 p-5">
<b-row class="mt-2">
<b-col sm="12" >
<b-input v-model="subject" placeholder="제목을 입력해 주세요" ></b-input>
</b-col>
<b-col sm="12" class="mt-2">
<b-form-textarea
v-model="context"
placeholder="내용을 입력해 주세요"
rows="3"
max-rows="10"
></b-form-textarea>
</b-col>
<b-col sm="12" class="mt-2">
<b-button @click="updateMode? updateContent():uploadContent() ">저장</b-button>
<b-button @click="cancle">취소</b-button>
</b-col>
</b-row>
</b-container>
</template>
<script>
import data from '@/data';
export default {
name:"Create",
data(){
return {
now: "00:00:00",
subject:"",
context:"",
userId:1,
createdAt:"",
updatedAt:null,
updateObject:null,
updateMode:this.$route.params.contentId>0?true:false
}
},
created(){
if(this.$route.params.contentId>0){
const contentId=Number(this.$route.params.contentId);
this.updateObject=data.Content.filter(item=>item.content_id===contentId)[0];
this.subject=this.updateObject.title;
this.context=this.updateObject.context;
this.createdAt=this.updateObject.created_at;
}
},
methods:{
uploadContent(){
let items=data.Content.sort((a,b)=>{return b.content_id-a.content_id});
const content_id=items[0].content_id+1;
data.Content.push({
content_id: content_id,
user_id: this.userId,
title: this.subject,
context: this.context,
created_at: this.currentTime(),
updated_at: null
});
this.$router.push({
path:'/board/free/'
});
},
updateContent(){
this.updateObject.title=this.subject;
this.updateObject.context=this.context;
this.updateObject.updated_at=this.currentTime();
this.$router.push({
path:'/board/free/'
});
},
cancle(){
this.$router.push({
path:'/board/free/'
});
},
currentTime(){
let today = new Date();
let year = today.getFullYear(); // 년도
let month = today.getMonth() + 1; // 월
let date = today.getDate(); // 날짜
let day = today.getDay(); // 요일
month=this.dateFormat(month);
date=this.dateFormat(date);
let currentDay=year + '-' + month + '-' + date;
this.now = " "+this.dateFormat(today.getHours()) + ":" + this.dateFormat(today.getMinutes()) + ":" +this.dateFormat(today.getSeconds());
console.log(currentDay+this.now);
return currentDay+this.now;
},
dateFormat(str){
str= "00"+str;
return str=str.slice(-2);
}
}
}
</script>
<style>
</style>
components/ContentDetail.vue
<template>
<b-container fluid class="mt-5 p-5">
<b-row class="mt-2">
<b-col sm="2">
<label for="textarea-small">글번호:</label>
</b-col>
<b-col sm="10">
<b-form-input v-model="contentId" readonly ></b-form-input>
</b-col>
</b-row>
<b-row class="mt-2">
<b-col sm="2">
<label for="textarea-small">글쓴이:</label>
</b-col>
<b-col sm="10">
<b-form-input v-model="user" readonly ></b-form-input>
</b-col>
</b-row>
<b-row class="mt-2">
<b-col sm="2">
<label for="textarea-default">등록일:</label>
</b-col>
<b-col sm="10">
<b-form-input v-model="created" readonly ></b-form-input>
</b-col>
</b-row>
<b-row class="mt-2">
<b-col sm="2">
<label for="textarea-default">수정일:</label>
</b-col>
<b-col sm="10">
<b-form-input v-model="updatedAt" readonly ></b-form-input>
</b-col>
</b-row>
<b-row class="mt-2">
<b-col sm="2">
<label for="textarea-large">내용:</label>
</b-col>
<b-col sm="10">
<b-form-textarea
size="lg"
rows="10"
v-model="context"
></b-form-textarea>
</b-col>
</b-row>
<b-row class="mt-2">
<b-col sm="12" class="text-center">
<b-button variant="primary" @click="updateData">수정</b-button>
<b-button variant="success" @click="deleteData">삭제</b-button>
</b-col>
</b-row>
<b-row class="mt-5">
<b-col sm="12 text-left"><span>댓글</span></b-col>
</b-row>
<b-row class="mb-5 comment">
<b-col sm="12">
<CommentList :contentId="contentId"/>
</b-col>
</b-row>
</b-container>
</template>
<script>
import data from '@/data';
import CommentList from '@/components/CommentList';
export default{
name:"ContentDetail",
components:{
CommentList
},
data(){
const contentId=Number(this.$route.params.contentId);
const contentData=data.Content.filter(item=>item.content_id===contentId)[0]
return{
contentId:contentId,
title:contentData.title,
context:contentData.context,
user:data.User.filter(item=>item.user_id===contentData.user_id)[0].name,
created:contentData.created_at,
updatedAt:contentData.updated_at
}
},
methods:{
updateData(){
this.$router.push({
path: `/board/free/create/${this.contentId}`
})
},
deleteData(){
const content_index = data.Content.findIndex(item => item.content_id === this.contentId);
data.Content.splice(content_index, 1)
this.$router.push({
path: '/board/free'
});
}
}
}
</script>
<style scoped>
.comment{
border: 1px solid black;
margin-top: 1rem;
padding: 2rem;
}
</style>
components/CommentList.vue
<template>
<div>
<div v-for="(item, index) in comments" :key="item.comment_id">
<div>
this index : {{index}}
<CommentListItem :commentObject="item" :reloadComment="reloadComment" :commentIndex="index" />
</div>
</div>
<div class="mb-5">
<CommentCreate :contentId="contentId" :reloadComment="reloadComment" />
</div>
</div>
</template>
<script>
import data from '@/data';
import CommentListItem from "@/components/CommentListItem";
import CommentCreate from "@/components/CommentCreate";
export default {
name:"CommentList",
props:{
contentId:Number,
},
components:{
CommentListItem,
CommentCreate,
},
data(){
return {
comments:data.Comment.filter(item => item.content_id === this.contentId)
}
},
methods:{
reloadComment(){
this.comments=data.Comment.filter(item => item.content_id === this.contentId)
}
}
}
</script>
<style>
</style>
components/CommentCreate.vue
<template>
<div>
<b-input-group :prepend="name" class="mt-3">
<b-form-textarea
v-model="context"
placeholder="코멘트를 달아 주세"
rows="3"
max-rows="6"
></b-form-textarea>
<b-input-group-append>
<b-button variant="info" @click="isSubComment ? createSubComment():createComment()">작성하기</b-button>
</b-input-group-append>
</b-input-group>
</div>
</template>
<script>
import data from "@/data";
export default {
name:"CommentCreate",
props:{
contentId:Number,
reloadComment:Function,
commentId:Number,
isSubComment:Boolean,
reloadSubComments:Function,
subCommentToggle:Function
},
data(){
return {
name:"르라나",
context:''
}
},
methods:{
createComment(){
data.Comment.push(
{
comment_id:data.Comment[data.Comment.length-1].comment_id+1,
user_id:1,
content_id:this.contentId,
context:this.context,
created_at:this.currentTime(),
updated_at:null
});
this.reloadComment();
this.context="";
},
createSubComment(){
let subCommentobj={
subcomment_id:data.SubComment[data.SubComment.length-1].subcomment_id+1,
user_id:1,
comment_id:this.commentId,
context:this.context,
created_at:this.currentTime(),
updated_at:null
}
data.SubComment.push(
subCommentobj
);
this.reloadSubComments();
this.context="";
this.subCommentToggle();
},
currentTime(){
let today = new Date();
let year = today.getFullYear(); // 년도
let month = today.getMonth() + 1; // 월
let date = today.getDate(); // 날짜
let day = today.getDay(); // 요일
month=this.dateFormat(month);
date=this.dateFormat(date);
let currentDay=year + '-' + month + '-' + date;
this.now = " "+this.dateFormat(today.getHours()) + ":" + this.dateFormat(today.getMinutes()) + ":" +this.dateFormat(today.getSeconds());
console.log(currentDay+this.now);
return currentDay+this.now;
},
dateFormat(str){
str= "00"+str;
return str=str.slice(-2);
}
}
}
</script>
<style>
</style>
components/CommentListItem.vue
<template>
<div>
<div class="comment-list-item">
<div class="comment-list-item-name">
<div>{{name}}</div>
<div>댓글 번호: {{commentId}}</div>
<div>{{commentObject.created_at}}</div>
</div>
<div class="comment-list-item-context">{{commentObject.context}}</div>
<div class="comment-list-item-button">
<b-button variant="info" @click="modalCommentBtn(commentObject)">수정</b-button>
<b-button variant="info" @click="commentDelete">삭제</b-button>
<b-button variant="info" @click="subCommentToggle">덧글 달기</b-button>
</div>
</div>
<template v-if="subCommentCreateToggle" >
<SubCommentCreate class="mb-3" :commentId="commentId" :isSubComment="true" :reloadSubComments="reloadSubComments" :subCommentToggle="subCommentToggle" />
</template>
<template v-if="subCommentList.length >0">
<div v-for="item in subCommentList" :key="item.subcomment_id" class="comment-list-item-subcomment-list ">
<div class="comment-list-item">
<div class="comment-list-item-name">
<div>{{item.user_name}}</div>
<div>댓글 번호: {{item.comment_id}}</div>
<div>서브 댓글 번호: {{item.subcomment_id}}</div>
<div>{{item.created_at}}</div>
</div>
<div class="comment-list-item-context">{{item.context}}</div>
<div class="comment-list-item-button">
<b-button variant="info" @click="modalSubCommentBtn(item)" >수정</b-button>
<b-button variant="info" @click="commentSubDelete(item.subcomment_id)">삭제</b-button>
</div>
</div>
</div>
</template>
<div>
<CommentUpdateModal ref="commentUpdateModal" :reloadComment="reloadComment" :commentObject="commentObject" :commentIndex="commentIndex" />
</div>
</div>
</template>
<script>
import data from "@/data";
import SubCommentCreate from "@/components/CommentCreate";
import CommentUpdateModal from "@/Components/CommentUpdateModal";
export default {
name:"CommentListItem",
props:{
commentObject:Object,
reloadComment:Function,
commentIndex:Number,
},
components:{
SubCommentCreate,
CommentUpdateModal
},
data(){
return{
commentId:this.commentObject.comment_id,
modalCommentObject:this.commentObject,
// userId:this.commentObject.user_id,
// contentId:this.commentObject.content_id,
// updatedAt:this.commentObject.updated_at,
name:data.User.filter(item=> item.user_id===this.commentObject.user_id)[0].name,
subCommentList:data.SubComment.filter(
item=>item.comment_id===this.commentObject.comment_id
).map(subCommentItem =>{return {
...subCommentItem, user_name:data.User.filter(item=> item.user_id===subCommentItem.user_id)[0].name,
}}),
subCommentCreateToggle:false,
}
},
methods:{
subCommentToggle(){
this.subCommentCreateToggle=!this.subCommentCreateToggle;
},
reloadSubComments(){
this.subCommentList=data.SubComment.filter(
item=>item.comment_id===this.commentObject.comment_id
).map(subCommentItem =>{return {
...subCommentItem, user_name:data.User.filter(item=> item.user_id===subCommentItem.user_id)[0].name,
}})
},
commentDelete(){
if(confirm("정말 삭제 하시겠습니까?")){
let commentId=this.commentObject.comment_id;
let findItem=data.Comment.find(function(item){ return item.comment_id===commentId});
let idx=data.Comment.indexOf(findItem);
console.log("findItem:");
console.dir(findItem);
console.log("idx:"+idx);
data.Comment.splice(idx, 1);
this.reloadComment();
}
},
commentSubDelete(subcommentId){
if(confirm("정말 삭제 하시겠습니까?")){
let findItem=data.SubComment.find(function(item){ return item.subcomment_id==subcommentId});
let idx=data.SubComment.indexOf(findItem);
data.SubComment.splice(idx, 1);
this.reloadSubComments();
}
},
modalCommentBtn(commentObject){
this.$refs.commentUpdateModal.$refs['my-modal'].hide();
this.$refs.commentUpdateModal.$refs['my-modal'].show();
},
}
}
</script>
<style scoped>
.comment-list-item {
display: flex;
justify-content: space-between;
padding-bottom: 1em;
}
.comment-list-item-name {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 0.5px solid black;
padding: 1em;
}
.comment-list-item-context {
display: flex;
justify-content: center;
align-items: center;
width: 50em;
border: 0.5px solid black;
}
.comment-list-item-button {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 0.5px solid black;
padding: 1em;
}
.btn {
margin-bottom: 1em;
}
.comment-list-item-subcomment-list {
display: flex;
justify-content: space-between;
padding-bottom: 1em;
margin-left: 10em;
}
</style>
components/CommentUpdateModal.vue
<template>
<div>
<b-modal
id="commentUpdate-modal"
ref="my-modal"
title="수정 하기"
@show="resetModal"
@hidden="resetModal"
@ok="handleOk"
>
<form ref="form" @submit.stop.prevent="handleSubmit">
<b-form-input
ref="modalCommentId"
v-model="commentId"
:state="commentIdState"
required
hidden
></b-form-input>
<b-form-textarea
ref="modalContext"
v-model="context"
:state="contextState"
required
></b-form-textarea>
</form>
</b-modal>
<b-modal
id="commentSubUpdate-modal"
ref="my-sub-modal"
title="서브 댓글 수정 하기"
@show="resetModal"
@hidden="resetModal"
@ok="handleOk"
>
<form ref="form" @submit.stop.prevent="handleSubSubmit">
<b-form-textarea
ref="subContext"
id="sub_context"
v-model="context"
required
></b-form-textarea>
</form>
</b-modal>
</div>
</template>
<script>
import data from "@/data";
export default {
name:"CommentUpdateModal",
props:{
reloadComment:Function,
commentObject:Object,
commentIndex:Number ,
reloadSubComments:Function
},
data() {
return {
context: this.commentObject.context,
commentId:this.commentObject.comment_id,
contextState: null,
commentIdState:null,
}
},
methods: {
checkFormValidity() {
const valid = this.$refs.form.checkValidity()
this.contextState = valid
return valid
},
resetModal() {
},
handleOk(bvModalEvt) {
// Prevent modal from closing
bvModalEvt.preventDefault()
// Trigger submit handler
this.handleSubmit()
},
handleSubmit() {
// Exit when the form isn't valid
if (!this.checkFormValidity()) {
alert("댓글을 작성해 주세요.");
this.$refs.modalContext.focus;
return
}
//데이타 업데이트
let commentId=this.commentId;
data.Comment.filter(item=>item.comment_id===commentId)[0].context=this.context;
this.reloadComment();
//모달 닫
this.$nextTick(() => {
this.$bvModal.hide('commentUpdate-modal');
});
},
handleSubSubmit() {
// Exit when the form isn't valid
if (!this.checkFormValidity()) {
alert("댓글을 작성해 주세요.");
this.$refs.modalContext.focus;
return
}
},
}
}
</script>















댓글 ( 6)
댓글 남기기