개발은 재밌어야 한다
article thumbnail
반응형

Vue로 만든 다이어리입니다. 요구사항은 다음과 같습니다.

요구사항
첫번째 화면(로그인)
두번째 화면 다이어리 목록
다이어리 목록화면
다이어리에서 목록보기 형식(타임라인/그리드)
카드방식 목록
일기 작성 화면
일기 상세 내용화면
일기 수정 화면

 

일단 Vue webpack을 생성합니다.

CMD 에서 빈 디렉토리로 이동 후 vue init webpack-simple를 통해 webpack을 생성합니다.

이후 폴더의 구조는 아래와 같습니다.

처음에 package.json에는 위와 같이 생성되어 있는데 해당 프로젝트에서는 vuex와 vue-router, element-ui가 필요하므로 install하여 설치 해 준다.

npm을 기준으로

npm install vuex --save 

npm install vue-router --save

npm install element-ui -S

다음과 같이 설치해준다.

그럼 이후에 "dependencies" 부분에 보면 다음과 같이 vuex,vue-router,element-ui가 설치된걸 알 수 있다.

이후 vue파일들과 js파일들을 작성해주면 된다.

<javascript />
import Vue from 'vue' import App from './App.vue' import VueRouter from 'vue-router'; import { store } from './store'; import LoginForm from './components/LoginForm'; import DiaryNav from './components/DiaryNav' import DiaryWrite from './components/DiaryWrite'; import DiaryList from './components/DiaryList'; import ListByNo from './components/ListByNo'; import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' //npm install url-loader --save-dev을 통해서 url-loader 설치 import locale from 'element-ui/lib/locale/lang/ko' Vue.use(ElementUI, { locale }) Vue.use(VueRouter); const router = new VueRouter({ routes: [ { path: '/', redirect: '/login' }, { path: '/login', component: LoginForm }, { path: '/diary', component: DiaryNav, children: [ { path: '/', component: DiaryList }, { path: 'write/:no?', component: DiaryWrite}, //no파라미터로 수정 게시글 번호 받음 { path: ':no', component: ListByNo, props:true } ] } ] }) new Vue({ el: '#app', store: store, render: h => h(App), router })

main.js

컴포넌트들을 선언 후 각 routes path에 맞게 사용해준다.

<html />
import Vue from "vue"; import Vuex from "vuex"; import Constant from "./constant"; Vue.use(Vuex); //vuex export const store = new Vuex.Store({ state: { //저장하는 값들 name: '윤동주', //사용자이름 no: 0, //인덱스 list: [] //목록리스트 }, mutations: { [Constant.CLEAR_LIST]: (state) => { state.list = []; }, [Constant.DELETE_DIARY]: (state, payload) => { //다이어리글 삭제 state.list.splice(payload.index, 1); //payload.index -> 지울려고하는 인덱스 값 }, [Constant.REVISE_DIARY]: (state, payload) => { //다이어리 글 수정 state.list[payload.list.no] = payload.list; //payload.no 값으로 인덱스 지정 후 payload의 리스트로 저장 } } });

store.js

다이어리 목록들의 내용과 사용자의 이름을 담을 수 있는 vuex를 사용할 store.js를 만들어준다.

<html />
export default{ CLEAR_LIST : "clearList", DELETE_DIARY : "deleteDiary", REVISE_DIARY : "reviseDiary" }

constant.js

store에 있는 상태를 변경하는 작업(mutation)과 action을 constant.js에 등록한다.

<html />
<template> <div id="app"> <router-view></router-view> </div> </template> <script> export default { name: 'app', data () { return { } } } </script> <style> html, body {padding: 0; margin: 0; height: 100%; width: 100%; overflow: hidden;} #app{height: 100%;width: 100%;} </style>

App.vue

첫 화면이 렌더링 되는 템플릿이 저장되어 있는 App.vue이다. 

/ path를 리다이렉트하여 요구사항에 맞는 첫화면이 /login이 되게 한다.

<html />
<template> <div class="form"> <el-row class="center"> <el-row> <el-col> <span>vue-diary</span> </el-col> </el-row> <el-row> <el-input label="사용자" placeholder="사용자 이름" v-model="name" v-on:keyup.enter.native="login()"></el-input> </el-row> <el-row> <!--로그인 버튼--> <el-button type="info" size="medium" @click="login" class="button-style">Sign In</el-button> </el-row> </el-row> </div> </template> <script> export default { name: "LoginForm", data() { return { name: '' }; }, methods: { login() { //로그인버튼 let router = this.$router; if (this.name != '' && this.name == this.$store.state.name) { //입력한 값이 있고, store의 name의 값과 일치하면 /diary path로 router.push('/diary'); this.$store.state.name = this.name; this.$message({ type:'info', message: `${this.name}님 반갑습니다.` }); } } } } </script> <style scoped> .form{ width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #9C97F0; background-color:#9C97F0; } .center{ justify-content: center; align-items: center; background-color:white; padding: 10px 60px 10px 60px; text-align:center; } .el-row{ margin: 10px; } .button-style{ width: 300px; background-color:#9C97F0; } </style>

LoginForm.vue

위와 같이 / -> /login으로 path를 지정하였기 때문에 /login에 지정한 LoginForm.vue가

router-view를 통해서 화면에 나타나게 된다.

store.js에서 미리 지정해 둔 사용자의 name과 같게 된다면 다음 화면인 다이어리의 목록으로 route.push하게 되어 /diary의 path로 이동하게 된다.

/diary의 path는 DiaryNav.vue로 컴포넌트를 지정하였다.

상단의 메뉴를 고정으로 목록, 작성, 상세보기, 수정들이 네스티드 라우터(Nested Router)구조를 가지고 있기 때문이다.

네스티드 라우터란 네스티드라는 말 자체 그대로 둥지를 짓다,중첩이라는 의미를 가지고 있기에

하나의 상위 컴포넌트 1개에 하위 컴포넌트 1개를 포함하는 구조를 가진 구조를 뜻한다.

<html />
<template> <el-row type="flex" justify="center" class="list-main"> <el-col :span="16"> <!--리스트형,카드형,작성 검색 영역 --> <el-row> <!-- 작성(Write)로 이동 --> <el-col :span="19"> <!-- (타임라인,카드) 토글 --> <el-radio-group v-model="radio" class="radio-group"> <el-radio-button size="small" label="list"><i class="el-icon-tickets"></i></el-radio-button> <el-radio-button size="small" label="card"><i class="el-icon-menu"></i></el-radio-button> </el-radio-group> <!-- 작성 버튼 --> <el-button size="small" class="el-icon-edit" @click="write" circle></el-button> </el-col> <!-- 검색 부분 --> <el-col :span="5"> <!-- 검색(input) --> <el-input v-model="search" placeholder="검색" prefix-icon="el-icon-search " size="small" v-on:keyup.enter.native="submit(search)"> <!-- 클리어 아이콘 --> <i slot="suffix" class="el-input__icon el-icon-circle-close el-input__clear" @click="reset" v-if="search !== ''"></i> </el-input> </el-col> <!-- /검색 부분 끝 --> </el-row> <!--/라디오 버튼(토글), 작성, 검색 영역 끝 --> <el-divider></el-divider><!--구분선--> <!-- 타임라인 형태 시작 --> <el-row v-show="radio==='list'"> <!-- 다이어리 목록 출력 부분 --> <div class="scroll-box"> <el-timeline class="scroll"> <el-timeline-item :timestamp="getDateTime(index)" placement="top" v-for="(list,index) in lists" :key="index"> <el-card @click.native="detail(index)"> <div> <!-- 타임라인 제목 --> <h4>{{list.title}}</h4> <!-- 타임라인 카드 날짜 년 월 일 시 분 --> <p class="card-date"> {{getCardDate(index)}} </p> </div> </el-card> </el-timeline-item> </el-timeline> </div> <!-- /다이어리 목록 출력 부분 --> </el-row> <!-- 타임라인 형태 끝 --> <!-- 카드 형태 시작 --> <el-row v-show="radio==='card'" class="scroll-box"> <!-- 다이어리 목록 출력 부분 --> <el-row class="scroll" :gutter="12"> <el-col :xs="8" :sm="8" :md="8" :lg="8" :xl="8" :span="8" v-for="(list,index) in lists" :key="index"> <!-- 카드영역 --> <div @click="detail(index)" class="card"> <el-card shadow="hover"> <div class="card-date"> {{getDateTime(index)}} <span><i @click.stop="remove(index)" class="el-icon-error"></i></span> </div> <div class="card-title">{{list.title}}</div> <div class="card-content">{{list.content}}</div> </el-card> </div> <!-- 카드영역 끝 --> </el-col> </el-row> </el-row> <!-- 카드 형태 끝 --> </el-col> </el-row> </template> <script> import Constant from '../constant' export default { name: "DiaryList", data() { return { lists: this.$store.state.list, no: 0, search: '', radio: 'list', //radio버튼 }; }, created: function() { this.no = this.$store.state.list.no; }, methods: { write() { //작성 버튼 let router = this.$router; router.push({ path: '/diary/write' }) }, reset() { //검색 초기화 버튼(claerable) this.search = ''; this.lists = this.$store.state.list; }, submit(search) { //검색 enter 메소드 let newArr = this.$store.state.list.filter(it => it.title.includes(search)); //새 리스트를 만들어서 원래의 리스트의 제목과 입력받은(search)이 포함된것 리턴 this.lists = newArr; //연결된 리스트에 찾은 리스트를 넣는다. }, remove(index) { //다이어리 목록의 리스트 항목을 제거 this.$store.commit(Constant.DELETE_DIARY, {index: index}); this.$message({ type:'info', message: '삭제하였습니다.' }); }, detail(no) { //목록의 자세히 보기 버튼 let router = this.$router; router.push(`/diary/${no}`); }, getDateTime(index){ //날짜 타임스탬프 사용 메소드 let date = `${this.lists[index].date.getFullYear()}${this.lists[index].date.getMonth()+1}${this.lists[index].date.getDate()}일`; return date; }, getCardDate(index){ //타임라인 년월일 시분초 let date; let hours = this.lists[index].date.getHours(); let minutes = this.lists[index].date.getMinutes(); if(minutes < 10) {minutes = `0${minites}`;} if(hours > 12){ hours = hours-12; if(hours < 10) {hours = `0${hours}`;} date = `${this.getDateTime(index)} 오후 ${hours}${minutes}분 작성`; } else{ if(hours < 10) {hours = "0"+hours;} date = `${this.getDateTime(index)} 오전 ${hours}${minutes}분 작성`; } return date; } } } </script> <style scoped> .list-main{ margin-top: 2%; } .el-divider--horizontal{ margin:10px 0px 40px 0px; } .el-icon-edit{ background-color: #9C97F0; color:white; } .el-icon-edit:hover{ color:black; } .radio-group{ margin-right: 20px; } .card{ margin-top: 5%; } .card-date{ font-size: 13px; color:#9C97F0; } .card-title,.card-content{ margin-top: 5%; font-size: 13px; } .card-content{ color: gray; } .scroll-box{ height:500px; overflow:overlay; overflow-x: hidden; } .scroll{ overflow:auto; padding: 0 4% 0 3%; } .el-icon-error{ float:right; color: gray; } .el-icon-error{ visibility: hidden; } .el-card__body:hover .el-icon-error{ visibility: visible; } .el-card:hover{ background-color: #F6F7FD; } </style>

DiaryList.vue

DiaryNav아래에 다이어리 리스트를 나타내는 자식 컴포넌트로 DiaryList.vue를 통해 다이어리의 리스트를 보여준다.

<html />
<template> <el-row> <el-row class="content-top" type="flex" justify="center"> <el-col :span="14"> <!--날짜--> <el-row class="date"> {{getDate}} </el-row> <!-- 제목, 날씨, 기분 --> <el-row> <span>{{lists[no].title}}</span> <span class="weather-feel-icon">(<i :class="weather"></i> {{lists[no].feel}})</span> </el-row> </el-col> <!-- 아이콘 모음 --> <el-col :span="4" class="icon-area"> <el-button icon="el-icon-back" circle @click="back()"></el-button> <!-- 뒤로가기 버튼 --> <el-button class="el-icon-edit" circle @click="revise()"></el-button> <!-- 수정하기 버튼 --> <el-button type="info" icon="el-icon-delete" circle @click="remove()"></el-button> <!-- 삭제 버튼 --> </el-col> </el-row> <!-- 하단 내용 부분 --> <el-row> <el-row justify="center" type="flex"> <el-col :span="18"> <el-divider></el-divider> <!--경계선--> </el-col> </el-row> <el-row justify="center" type="flex" class="content"> <el-col :span="17"> {{lists[no].content}} </el-col> </el-row> </el-row> <!-- 하단 입력부 끝 --> </el-row> </template> <script> import constant from '../constant'; export default { name: "ListByNo", data() { return { lists: this.$store.state.list, //이 리스트의 값을 store의 리스트값으로 넣는다. }; }, computed:{ getDate(){ //날짜는 계산된 채로 불러오기 때문에 계산된 값으로 computed에 넣어서 캐싱처리 let dateDay; let yearMonthDay = `${this.lists[this.no].date.getFullYear()}${this.lists[this.no].date.getMonth()+1}${this.lists[this.no].date.getDate()}일`; let hours = this.lists[this.no].date.getHours(); let minutes = this.lists[this.no].date.getMinutes(); if(minutes < 10) {minutes = `0${minites}`;} if(hours > 12){ hours = hours-12; if(hours < 10) {hours = `0${hours}`;} dateDay = `${yearMonthDay} 오후 ${hours}${minutes}분`; } else{ if(hours < 10) {hours = `0${hours}`;} dateDay = `${yearMonthDay} 오전 ${hours}${minutes}분`; } return dateDay; }, weather(){ //날씨 아이콘 클래스 바인딩하기 위해 사용 return ('el-icon-'+this.lists[this.no].weather); } }, props:['no'], methods: { back(){ //뒤로가기 버튼: 목록으로 전환 let router = this.$router; router.push('/diary'); }, revise(){ //수정 버튼 let router = this.$router; router.push(`/diary/write/${this.no}`); }, remove() { //삭제 버튼 this.$store.commit(constant.DELETE_DIARY, {index: this.no}); let router = this.$router; router.push('/diary'); this.$message({ type:'info', message: '일기를 삭제하였습니다.' }); } } } </script> <style scoped> .content-top{ margin-top:2%; padding:0 40px 0 40px; } .date, .weather-feel-icon{ color: gray; } .icon-area{ text-align: right; } .el-icon-edit{ background-color: #9C97F0; color: white; } .content{ height: 500px; overflow-y: auto; overflow-x: auto; width: 100%; } </style>

 

ListByNo.vue

 

다이어리의 상세 내용보기는 각 게시글의 no 번호를 props로 받아서 리스트에서 해당 글의 no 번호로 게시글의 상세 보기를 할 수 있게 하였다.

<html />
<template> <el-row type="flex" justify="center"> <el-col> <!-- 상단 타이틀 --> <el-row class="content-top-margin" type="flex" justify="center"> <el-col :span="1"> <i class="el-icon-back" @click="cancel"></i> <!--뒤로가기 아이콘--> </el-col> <el-col :span="16"> <span>일기 작성</span> </el-col> </el-row> <!-- /상단 타이틀 끝--> <!-- 하단 입력부 --> <el-row type="flex" justify="center"> <el-col :span="18"> <el-divider></el-divider> <!--경계선--> <el-row> <el-col :span="24" class="content-area"> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="120px" class="demo-ruleForm"> <el-form-item label="제목" prop="title"> <!-- 제목 입력 --> <el-input v-model="ruleForm.title"></el-input> </el-form-item> <el-form-item label="날씨" prop="weather"> <!-- 날씨 선택 --> <el-radio-group v-model="ruleForm.weather"> <el-radio-button label="sunny"><i class="el-icon-sunny"></i></el-radio-button> <el-radio-button label="cloudy"><i class="el-icon-cloudy"></i></el-radio-button> <el-radio-button label="heavy-rain"><i class="el-icon-heavy-rain"></i></el-radio-button> <el-radio-button label="light-rain"><i class="el-icon-light-rain"></i></el-radio-button> </el-radio-group> </el-form-item> <el-form-item label="기분" prop="feel"> <!-- 기분 선택 --> <el-select v-model="ruleForm.feel" placeholder="기분을 선택하세요"> <el-option label="기쁨" value="기쁨"></el-option> <el-option label="우울" value="우울"></el-option> <el-option label="화남" value="화남"></el-option> </el-select> </el-form-item> <el-form-item label="내용" prop="content"> <!-- 내용 입력 --> <el-input type="textarea" v-model="ruleForm.content" :rows="10"></el-input> </el-form-item> <el-form-item class="submit"> <!--버튼 가운데 정렬--> <el-button type="primary" @click="save('ruleForm')">저장</el-button> <!-- 저장 버튼 --> <el-button @click="cancel">취소</el-button> <!-- 취소 버튼(목록가기) --> </el-form-item> </el-form> </el-col> </el-row> </el-col> </el-row> <!-- 하단 입력부 끝 --> </el-col> </el-row> </template> <script> import constant from '../constant'; export default { name: "DiaryWrite", data() { return { no: this.$route.params.no, //라우터 파라미터 값을 받아온다. ruleForm:{ title: '', content: '', weather: '', feel: '' }, rules:{ //유효성 검사 title:[ {required: true, message: '제목을 입력해 주세요.', trigger: 'blur'} ], content:[ {required: true, message: '내용을 입력해 주세요.', trigger: 'blur'} ], weather:[ {required: true, message: '날씨를 선택해 주세요.', trigger: 'chagne'} //변경시 trigger 감지 ], feel:[ {required: true, message: '기분을 선택해 주세요.', trigger: 'change'} //변경시 trigger 감지 ] } }; }, methods: { save(formName){ //폼 저장 버튼 클릭 this.$refs[formName].validate((valid) => { if(valid){ let date = new Date(); let router = this.$router; if(this.no!=null){ //라우터 값이 있어서 no값이 null이 아니면 let listItem ={ 'no':this.no, 'title':this.ruleForm.title, 'content':this.ruleForm.content, date, 'weather':this.ruleForm.weather, 'feel':this.ruleForm.feel }; this.$store.commit(constant.REVISE_DIARY, {list:listItem}); this.$message({ //수정 메세지 출력 type:'info', message: '일기를 수정하였습니다.' }); } else{ //현재글이 수정이 아니라 처음 작성이라면 this.$store.state.list.push({ //store의 리스트에 no,title,content,date,weather,feel를 넣는다 'no': this.$store.state.no++, //no값을 처음에 0을 통해 넣고 ++을 통한 증감 연산자를 통해서 다음 값을 증가해서 다음 등록때에는 no값이 증가 'title': this.ruleForm.title, 'content': this.ruleForm.content, date, 'weather':this.ruleForm.weather, 'feel':this.ruleForm.feel }); this.$message({ //작성 메세지 출력 type:'info', message: '일기를 작성하였습니다.' }); } router.push('/diary'); } else{ return false; } }); }, fetchData(){ //이미 작성한 글의 데이터를 data에 바인딩 if(this.no != null){ this.ruleForm.title = this.$store.state.list[this.no].title; this.ruleForm.content = this.$store.state.list[this.no].content; this.ruleForm.weather = this.$store.state.list[this.no].weather; this.ruleForm.feel = this.$store.state.list[this.no].feel; } }, cancel() { //취소버튼 let router = this.$router; if(this.no != null){ router.back(); //취소시에 리스트로 이동 } else{ router.push('/diary'); //취소시에 리스트로 이동 } } }, created(){ //뷰가 생성되고 데이터가 이미 감시 되고 있을 때 데이터를 가져온다. this.fetchData() } } </script> <style scoped> .content-top-margin{ margin-top:2%; } .content-area{ padding: 0 5% 0 0; } .submit { text-align: center; } </style>

DiaryWrite.vue

다이어리 작성부분은 해당 글의 수정의 내용까지 함께 하여 재사용할 수 있게끔 하기 위해서 라우터의 params값을 통해 no값이 있다면 created 훅에서 fetchData라는 메소드를 실행 시켜 기존에 있는 리스트의 데이터 내용을 바인딩 할 수 있게 하였다.

최종 완성 소스코드 github 주소

https://github.com/skarbgud/vue-diary

 

GitHub - skarbgud/vue-diary: 📂Vue Diary

📂Vue Diary. Contribute to skarbgud/vue-diary development by creating an account on GitHub.

github.com

 

반응형

'javascript > Vue' 카테고리의 다른 글

Vue devtools 설치 및 사용  (0) 2021.07.25
Vue.js 컴포넌트 (심화)  (0) 2021.04.13
Vue.js 컴포넌트 (기본)  (0) 2021.03.20
profile

개발은 재밌어야 한다

@ghyeong

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!