서비스 API 소개
- npm install, npm run start 를 통해서 실행 localhost:3000 으로 확인
- 연락처 서비스 목록
GET /contacts | 연락처 목록을 조회하며 특정 페이지 데이터를 조회 |
GET /contacts/no | 특정 일련번호 한 건의 연락처 정보를 조회 |
GET /contacts/search/:name | 이름의 일부를 이용해 연락처를 검색. 2글자 이상가능 |
POST /contacts | 새로운 연락처 추가 |
PUT /contacts/:no | 기전 연락처 정보 수정 |
DELETE /contacts/:no | 연락처 정보 삭제 |
POST /contacts/batchinsert | 한번에 여러 건의 연락처 정보 추가 |
POST /contacts/:no/photo | 연락처에 사람의 사진 정보 등록 |
GET /contacts_long | GET /contacts 와 동일한 기능이지만 1초의 의도적 지연 시간 후에 응답 |
GET /contacts_long/search/:name | GET /contacts/search/:name 과 동일한 기능이지만 1초의 의도적 지연 시간 후에 응답 |
http 프록시 설정
SOP(Same Origin Policy)
- 브라우저가 HTML 페이지를 가진 웹사이트(localhost:8080)에 접속해서 HTML문서를 다운로드 하고 보여주게됨
- 이때 브라우저의 Origin은 http://localhost:8080으로 자동설정
- 이 의미는 "현재 보여주고 있는 문서는 Origin에서 내려받은 것이다." 임
- 이 HTML문서에서 다른 외부 서버와 통신하려는 경우, 현재 브라우저의 Origin과 다른 Origin에 해당하는 서버와 통신하려고 할때 응답 전송까지는 정상적으로 수행되지만 브라우저로 로딩하는 단계에서 오류가 발생
- 보안 정책으로 인해 크로스 오리진으로부터 데이터를 로드할 수 없는 현상
- 오리진 정보가 한 글자라도 다르면 크로스 오리진 상태
SOP 해결방법
- Consumer Server 측에 프록시 생성
- Service Provider 측에서 CORS(Cross Origin Resource Sharing) 기능을 제공
- Service Provider 측에서 JSONP(JSON Padding) 기능을 제공
아래는 Consumer Server 측에 프록시 생성 방법의 예
Vue-CLI에서는 웹팩 개발서버를 설정파일을 이용하여 프록시 서버 기능을 이용할 수 있음
- vue.config.js - package.json 파일이 있는 디렉토리에 위치
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
//target: 'http://sample.bmaster.kro.kr',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
CORS는 server.js 파일을 열어서 app.use(cors()); 의 주석을 제거
CORS 참조
- http://developer.mozillz.org/ko/docs/Web/HTTP/Access_control_CORS
- http://homoeffcio.github.io/2015/07/21/Cross-Origin-Resource-Sharing
axios 사용
- cd contactapp | yarn add axios 또는 npm install --save sxios
- <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
C:\JetBrains\vscode_workspace>cd contactapp
C:\JetBrains\vscode_workspace\contactapp>yarn add axios
yarn add v1.22.10
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.3: The platform "win32" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "win32" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ axios@0.21.0
info All dependencies
└─ axios@0.21.0
Done in 11.38s.
저수준 API
- axios(config)
- axios(url, config)
각 메소드 별 명칭
- axios.get(url[, config])
- axios.delete(url[, config])
- axios.post(url[, data[, config]])
- axios.post(url[, data[, config]])
- axios.put(url[, data[, config]])
- axios.head(url[, config])
- axios.options(url[, config])
src/AppAxiosTest.vue
<template>
<div id="app">
<div class="container">
<div class="form-group">
<button @click="fetchContacts">폐이지 연락처 조회</button>
</div>
<div class="form-group">
<input type="text" v-model="name" placeholder="이름을 입력합니다.">
<input type="text" v-model="tel" placeholder="전화번호를 입력합니다.">
<input type="text" v-model="address" placeholder="주소를 입력합니다.">
<button @click="addContact">연락처 1건 추가</button>
</div>
<div class="form-group">
<input type="text" v-model="no">
<button @click="fetchContactOne">연락처 1건 조회</button>
</div>
<div class="form-group">
<input type="text" v-model="no">
<input type="text" v-model="name" placeholder="이름을 입력합니다.">
<input type="text" v-model="tel" placeholder="전화번호를 입력합니다.">
<input type="text" v-model="address" placeholder="주소를 입력합니다.">
<button @click="updateContact">수정</button>
</div>
<div class="form-group">
<input type="text" v-model="no">
<button @click="deleteContact">삭제</button>
</div>
<div class="form-group">
<input type="text" v-model="no">
<input type="file" ref="photofile" name="photo" >
<button @click="changePhoto">파일 변경</button>
</div>
</div>
<span>JSON 출력</span>
<div id="result" class="container">
<xmp>{{ result }}</xmp>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name : 'app',
data() {
return {
no : 0, name : '', tel: '', address: '',
result: null
}
},
methods : {
fetchContacts : function() {
axios.get('/api/contacts')
.then((response)=> {
console.log(response);
this.result = response.data;
});
},
addContact : function() {
axios.post('/api/contacts',{
name : this.name, tel: this.tel, address: this.address,
})
.then((response)=> {
console.log(response);
this.result = response.data;
this.no = response.data.no;
})
.catch((ex) => {
console.log('Error!!!: ', ex);
});
},
fetchContactOne : function() {
axios.get('/api/contacts/' + this.no)
.then((response)=> {
console.log(response);
this.result = response.data;
});
},
updateContact : function() {
axios.put('/api/contacts/' + this.no,{
name : this.name, tel: this.tel, address: this.address,
})
.then((response)=> {
console.log(response);
this.name = '';
this.tel = '';
this.address = '';
this.result = response.data;
})
.catch((ex) => {
console.log('Error!!!: ', ex);
});
},
deleteContact : function() {
axios.delete('/api/contacts/' + this.no)
.then((response)=> {
console.log(response);
this.no = 0;
this.result = response.data;
});
},
changePhoto : function() {
var data = new FormData();
var file = this.$refs.photofile.files[0];
data.append('file',file);
axios.post('/api/contacts/'+this.no+'/photo',data)
.then((response) => {
this.result = response.data;
})
.catch((ex) => {
console.log('updatePhotoFailed ', ex);
});
}
}
}
</script>
<style>
@import url("https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.css");
#app {
font-family : 'Avenir',Helvetica, Arial, sans-serif;
-webkit-font-smoothing : antialiased;-webkit-osx-font-smoothing : grayscale;
text-align: center;color: #2c3e50;margin-top: 60px;
}
.container {
border: solid 1px gray; padding: 10px;
margin-bottom: 10px; text-align: left;
}
#result { text-align: left; padding: 20px; border: solid 1px black;}
.form-group { border: dashed 1px gray; padding: 5px 5px 5px 20px;}
</style>
axios 요청과 config 옵션
- baseUrl : 이 옵션을 이용해 공통적인 URL 앞 부분을 미리 등록해두면 요청 시 나머지 부분만으로 URL로 전달 하면 됨. axios.defaults.baseURL 값을 미리 바꾸는 편이 좋음
- transformReqeust : 요청 데이터를 서버로 전송하기 전에 데이터를 변환하기 위한 함수를 등록
- transformResponse : 응답 데이터를 수신한 직후에 데이터를 변환하기 위한 함수를 등록
- headers : 요청 시에 서버로 전달하고자 하는 HTTP 헤더정보를 설정
Vue 인스턴스에서 axios 이용하기
// src/main.js
import Vue from 'vue';
import App from './AppAxiosTest.vue';
import axios from 'axios';
Vue.prototype.$axios = axios;
Vue.config.productionTip = false;
new Vue({
render: h => h(App)
}).$mount('#app');
// src/AppAxiosTest.vue
this.$axios.get('/api/contacts/' +this.no)
.then((response) => {
console.log(response);
this.result = response.data;
});
axios 사용 시 주의사항
- then() 처리할 때는 화살표 사용을 권장. this 가 Vue 인스턴스를 참조하기 때문
- 화살표 함수가 아닌 함수를 쓰면 클로저를 써야하는 불편함이 생김
var vm = this; //클로저를 사용해야함
this.$axios.get('/api/contacts/' +this.no)
.then(function(response) {
console.log(response);
vm.result = response.data;
});
연락처 애플리케이션 예제
- 컴포넌트의 모든 상태를 App.vue 에서 관리
- ContactList.vue : 연락처 목록을 보여줌 (초기화면)
- AddContact.vue, UpdateContact.vue : 연락처 추가, 편집 할때만 화면에 나타나므로 동적 컴포넌트 이용
- ContactForm.vue : AddContact.vue, UpdateContact.vue의 화면이 유사하므로 ContactForm.vue의 하위 컴포넌트로 공유
- ContactForm.vue 에서 이벤트 발생. 이벤트 버스를 통해 App.vue 로 데이터 전달
vuejs-pagenate, bootstrap@3.3.x, es6-promise
## 페이징 처리및 vuejs-paginate는 부트스트랩 CSS를 필요로 하므로 다음과 같이 설치
yarn add vuejs-paginate bootstrap@3.3.x
또는
npm install --save vuejs-paginate bootstrap@3.3.x
## IE는 Promise를 지원하지 않으므로 axios 를 원활히 사용하기 위해 polyfill 요소를 다운 참조 필요
yarn add es6-promise
또는
npm install --save es6-promise
src/main.js
// 다음 항목 추가
import axios from 'axios';
import 'bootstrap/dist/css/bootstrap.css';
import ES6Promise from 'es6-promise';
ES6Promise.polyfill();
Vue.prototype.$axios = axios;
src/Config.js
let BASE_URL = "/api";
export default {
// 한 페이지에 보여줄 페이지 사이즈
PAGESIZE: 5,
// 전체 연락처 데이터 요청
FETCH: BASE_URL + "/contacts",
// 연락처 추가
ADD: BASE_URL + "/contacts",
// 연락처 업데이트
UPDATE: BASE_URL + "/contacts/${no}",
// 연락처 한건 조회
FETCH_ONE: BASE_URL + "/contacts/${no}",
// 연락처 삭제
DELETE: BASE_URL + "/contacts/${no}",
// 연락처 사진 업로드 변경
UPDATE_PHOTO: BASE_URL + "/contacts/${no}/photo",
}
src/EventBus.js
import Vue from 'vue';
var vm = new Vue({ name: 'EventBus' });
export default vm;
src/App.vue
<template>
<div id="container">
<div class="page-header">
<h1 class="text-center">연락처 관리 애플리케이션</h1>
<p>(Dynamic Component + EventBus + Axios)</p>
</div>
<component :is="currentView" :contact="contact"></component>
<contactlist :contactlist="contactlist"></contactlist>
</div>
</template>
<script>
import ContactList from './components/ContactList';
import AddContact from './components/AddContact';
import UpdateContact from './components/UpdateContact';
import UpdatePhoto from './components/UpdatePhoto';
import CONF from './Config.js';
import eventBus from './EventBus.js';
export default {
name: 'App',
components: {
ContactList, AddContact, UpdateContact, UpdatePhoto
},
data() {
return {
currentView : null,
contact : {no: 0, name: '', tel: '', address: '', photo: ''},
contactlist : { pageno : 1, pagesize : CONF.PAGESIZE, totalcount : 0, contacts : []}
}
},
mounted : function() {
},
methods : {
}
}
</script>
<style scoped>
#container {
font-family: 'Avenir', Helvetica, sans-serif;
-webkit-font-smoothing : antialised;
-webkit-osx-font-smoothing : grayscale;
text-align : center;
color : #2c3e50;
margin-top : 60px;
}
</style>
src/App.vue - methods
methods : {
/*
보여줄 페이지를 변경함. data 속성의 conactlist 정보를 변경후
fetchContacts를 호출하도록 작성. Paginate 컴포넌트에서 이 함수를 바인딩함.
*/
pageChanged : function(page) {
this.contactlist.pageno = page;
this.fetchContacts();
},
/*
전체 연락처 데이터를 페이징하여 조회함
pageno, pagesize는 data 속성의 contactlist 정보를 활용함
*/
fetchContacts : function() {
this.$axios.get(CONF.FETCH, {
params : {
pageno : this.contactlist.pageno,
pagesize : this.contactlist.pagesize,
}
}).then((reponse) => {
this.contactlist = response.data;
}).catch((ex) => {
console.log('fetchContacts failed',ex);
this.contactlist.contacts = [];
});
},
/* 연락처 한건을 추가함 */
addContact : function(contact) {
this.$axios.post(CONF.ADD, contact).then((response) => {
if( response.data.status === 'success') {
this.contactlist.pageno = 1;
this.fetchContacts();
} else {
console.log('연락처 추가 실패 : ' + response.data.message);
}
}).catch((ex) => {
console.log('addContact failed',ex);
});
},
/* 연락처 한건을 수정함 */
updateContact : function(contact) {
this.$axios.put(CONF.UPDATE,replace('${no}',contact.no),contact).then((response) => {
if( response.data.status === 'success') {
this.fetchContacts();
} else {
console.log('연락처 변경 실패 : ' + response.data.message);
}
}).catch((ex) => {
console.log('updateContact failed',ex);
});
},
/* 일련번호를 이용해 특정 연락처 한건을 조회함 */
fetchContactOne : function(no) {
this.$axios.get(CONF.FETCH_ONE.replace('${no}',no)).then((response) => {
this.contact = response.data;
}).catch((ex) => {
console.log('fetchContactOne failed',ex);
});
},
/* 일련번호를 이용해 연락처 한건을 삭제함 */
deleteContact : function(no) {
this.$axios.delete(CONF.DELETE.replace('${no}',no)).then((response) => {
if( response.data.status === 'success') {
this.fetchContacts();
} else {
console.log('연락처 삭제 실패 : ' + response.data.message);
}
}).catch((ex) => {
console.log('delte failed',ex);
});
},
/* 일련번호와 파일 요소 정보를 이용해 사진 파일을 변경함 */
updatePhoto : function(no, file) {
var data = new FormData();
data.append('photo',file);
this.$axios.post(CONF.UPDATE_PHOTO.replace('${no}',no), data).then((reponse)=> {
if( response.data.status === 'success') {
this.fetchContacts();
} else {
console.log('연락처 사진 변경 실패 : ' + response.data.message);
}
}).catch((ex) => {
console.log('updatePhoto failed',ex);
});
}
}
src/App.vue - mounted 이벤트 훅에 이벤트 수신기능 작성
mounted : function() {
this.fetchContacts();
/* 모든 입력폼에서 취소 버튼을 클랙했을 때 발생되는 이벤트, currentView를 null로 변경함 */
eventBus.$on('cancel', () => {
this.currentView = null;
});
/*
연락처가 추가되는 이벤트. contact 객체를 받아서 addContact 메서드를 호출함.
연락처가 추가되면 입력폼은 사라져야 하므로 currentView를 null로 변경함
*/
eventBus.$on('addSubmit', (contact) => {
this.currentView = null;
this.addContact(contact);
});
/*
연락처가 수정되는 이벤트. updateContact 메서드를 호출함.
수정 폼은 사라지도록 currentView를 null로 설정함
*/
eventBus.$on('updateSubmit', (contact) => {
this.currentView = null;
this.updateContact(contact);
});
/* 연락처 추가폼이 나타날 수 있도록 currentView를 addContact로 변경함 */
eventBus.$on('addContactForm', () => {
this.currentView = 'addContact';
});
/*
변경폼에 기존 연락처 데이터가 나타날 수 있도록 no 인자를 이용해 fetchContactOne 메서드를 호출하고,
연락처 변경 폼이 나타날 수 있도록 currentView를 updateContact로 변경함
*/
eventBus.$on('editContactForm', (no) => {
this.fetchContactOne(no);
this.currentView = 'updateContact';
});
/* no를 이용해 deleteContact 메서드를 호출함 */
eventBus.$on('deleteContact', (no) => {
this.deleteContact(no);
});
/*
editContactForm 이벤트와 유사하게 no 인자를 이용해 fetchContactOne 메서드를 호출하고
currentView를 updateContact로 변경함
*/
eventBus.$on('editPhoto', (no) => {
this.fetchContactOne(no);
this.currentView = 'updatePhoto';
});
/*
파일 정보가 존재할 때 updatePhoto 메서드를 호출하고
사진 변경 폼이 사라질 수 있도록 currentView 메서드를 호출함
*/
eventBus.$on('updatePhoto', (no, file) => {
if(typeof file !== undefined){
this.UpdatePhoto(no, file);
}
});
/* page 번호를 이요해 페이지를 이동시키도록 pageChanged 메서드를 호출함 */
eventBus.$on('pageChanged', (page) => {
this.pageChanged(page);
});
},
src/components/ContactList.vue
<template>
<div>
<p class="addnew">
<button class="btn btn-primary" @click="addContact">새로운 연락처 추가하기</button>
</p>
<div id="example">
<table id="list" class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>이름</th>
<th>전화번호</th>
<th>주소</th>
<th>편집</th>
<th>삭제</th>
</tr>
</thead>
<tbody id="contacts">
<tr v-for="contact in contactlist.contacts" :key="contact.no">
<td>{{ contact.name }}</td>
<td>{{ contact.tel }}</td>
<td>{{ contact.address }}</td>
<td>
<img class="thumbnail" :src="contact.photo" @click="editPhoto(contact.no)">
</td>
<td>
<button class="btn btn-primary" @click="editContact(contact.no)">편집</button>
<button class="btn btn-primary" @click="deleteContact(contact.no)">삭제</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- paginate 사용 -->
<paginate ref="pagebuttons"
:page-count="totalpage"
:page-range="7"
:margin-pages="3"
:click-handler="pageChanged"
:prev-text="'이전'"
:next-text="'다음'"
:container-class="'pagination'"
:page-class="'page-item'">
</paginate>
</div>
</template>
<script>
import eventBus from '../EventBus.js';
import Paginate from 'vuejs-paginate';
export default {
name : 'contactList',
components : { Paginate },
/*
자신의 데이터를 가지지 못하고 상위 컴포넌트로 부터
props를 통해 전달 받은 데이터(contactlist)를 화면에 나타내기만 함
이러한 컴포넌트를 상태가 없는 컴포넌트(Stateless Component)라고 함
자신의 상태(데이터)가 없기 때문에 반드시 props를 통해 전달받아야 함
*/
props : ['contactlist'],
computed : {
totalpage : function() {
return Math.floor((this.contactlist.totalcount-1) / this.contactlist.pagesize) + 1;
}
},
/*
다른 페이지를 조회하던 중 새로운 연락처를 추가하면
방금 추가한 연락처를 확인할 수 있도록 첫번째 페이지로 이동하여야 함
vuejs-paginate 컴포넌트는 pageno를 바인딩하도록 만들어져 있지 않기 때문에
관찰 속성을 이용하여 직접 선택된 페이지 번호를 변경해 주어야함
*/
watch : {
['contactlist.pageno'] : function() {
this.$refs.pagebuttons.selected = this.contactlist.pageno;
}
},
methods : {
pageChanged : function(page) {
eventBus.$emit('pageChanged',page);
},
addContact : function() {
eventBus.$emit('addContactForm');
},
editContact : function(no) {
eventBus.$emit('editContactForm', no);
},
deleteContact : function(no) {
if(confirm("정말로 삭제하시겠습니까?")){
eventBus.$emit('deleteContact', no);
}
},
editPhoto : function(no) {
eventBus.$emit('editPhoto', no);
}
}
}
</script>
<style scoped>
.addnew {
margin: 10px auto; max-width:820px; min-width: 820px;
padding: 40px 0 0 0; text-align: left;
}
#example {
margin: 10px auto; max-width:820px; min-width:820px;
padding: 0px; position: relative; font: 13px "verdana";
}
#example .long { width: 100%;}
#example .short { width: 50%;}
#example input, textarea, select {
box-sizing: border-box; border: 12px solid #bebebe;
padding: 7px; margin: 0; outline: none;
}
#list { width: 800px; font: 13px "verdana" }
#list thead th { color: yellow; background-color: purple;}
#list th:nth-child(5n+1), #list td:nth-child(5n+1) { width: 200px; }
#list th:nth-child(5n+2), #list td:nth-child(5n+2) { width: 150px; }
#list th:nth-child(5n+3), #list td:nth-child(5n+3) { width: 250px; }
#list th:nth-child(5n+4), #list td:nth-child(5n+4) { width: 60px; }
#list th:nth-child(5n), #list td:nth-child(5n) { width: 150px; }
#list th { padding: 10px 5px;}
#list tr { border-bottom : solid 1px black;}
#list td, #list th { text-align: center; vertical-align: middle; }
img.thumbnail {
width: 48px; height: 48px; margin-top: auto;
margin-bottom: auto; display: block; cursor: pointer;
}
</style>
연락처 리스트 데이터의 전달과 삭제 이벤트 처리과정
Vue-CLI에서 콘솔 log를 사용할 수 있도록 변경
- Vue-CLI 로 생성한 프로젝트는 기본적으로 ESLint 기능에 의해 console 출력을 제한함
//package.json
....
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
// 여기서 console 을 출력하도록 설정
"rules": {
"no-console": "off"
}
....
입력폼, 수정폼 작성
src/components/AddContact.vue
<template>
<contactForm mode="add" />
</template>
<script>
import ContactForm from './ContactForm.vue';
export default {
name : 'addContact',
components : {ContactForm}
}
</script>
src/compoents/UpdateContact.vue
<template>
<contactForm mode="update" :contact="contact" />
</template>
<script>
import ContactForm from './ContactForm.vue';
export default {
name : 'updateContact',
components : {ContactForm},
props : ['contact']
}
</script>
src/components/ContactForm.vue
<template>
<div class="modal">
<div class="form" @keyup.esc="cancelEvent">
<h3 class="heading">:: {{headingText}}</h3>
<div v-if="mode=='update'" class="form-group">
<label>일련번호</label>
<input type="text" name="no" class="long" disabled v-model="contact.no">
</div>
<div class="form-group">
<label>이 름</label>
<input type="text" name="name" class="long" placeholder="이름을 입력하세요"
v-model="contact.name"
ref="name">
</div>
<div class="form-group">
<label>전화번호</label>
<input type="text" name="tel" class="long" placeholder="전화번호를 입력하세요"
v-model="contact.tel"
>
</div>
<div class="form-group">
<label>주 소</label>
<input type="text" name="address" class="long" placeholder="주소를 입력하세요"
v-model="contact.address"
>
</div>
<div class="form-group">
<label> </label>
<input type="button" class="btn btn-primary" :value="btnText" @click="submitEvent">
<input type="button" class="btn btn-primary" value="취 소" @click="cancelEvent">
</div>
</div>
</div>
</template>
<script>
import eventBus from '../EventBus.js';
export default {
name : 'contactForm',
props : {
mode : { type: String, default: 'add'},
contact : {
type : Object,
default: function() {
return {no : '', name: '', tel: '', address: '', photo : ''}
}
}
},
mounted : function() {
this.$refs.name.focus();
},
computed : {
btnText : function() {
if(this.mode != 'update') return '추 가';
else return '수 정';
},
headingText : function() {
if(this.mode != 'update') return '새로운 연락처 추가';
else return '연락처 변경';
}
},
methods : {
submitEvent : function() {
if(this.mode == 'update'){
eventBus.$emit('updateSubmit', this.contact);
}else {
eventBus.$emit('addSubmit', this.contact);
}
},
cancelEvent : function() {
eventBus.$emit('cancel');
}
}
}
</script>
<style scoped>
.modal {
display: block; position: fixed; z-index: 1;
left: 0; top: 0; width: 100%; height: 100%;
overflow: auto; background-color: rgb(0,0,0);
background-color: rgba(0,0,0, 0.4);
}
.form {
background-color: white; margin: 100px auto;
max-width: 400px; min-width: 200px; font: 13px "verdana";
padding: 10px;
}
.form div { padding: 0; display:block; margin: 10px 0 0 0;}
.form label {
text-align: left; margin: 0 0 3px 0; padding: 0;
display: block; font-weight: bold;
}
.form input, textarea, select {
box-sizing: border-box; border: 1px solid #bfbfbf;
padding: 7px; margin: 0; outline: none;
}
.form .long {width : 100%;}
.form .button {
background: #2b798d; padding: 8px 15px; border: none; color: #fff;
}
.form .button:hover { background: #4691A4;}
.form .heading {
background: #33a17f; font-weight: 300;
text-align: left; padding: 20px; color: #fff;
margin: 5px 0 30px 0; padding: 10px;
min-width: 200px; max-width: 400px;
}
</style>
사진 변경폼 작성
src/component/UpdatePhoto.vue
<template>
<div class="modal">
<div class="form">
<form method="post" enctype="multipart/form-data">
<h3 class="heading">:: 사진 변경</h3>
<input type="hidden" name="no" class="long" disabled="disabled" v-model="contact.no">
<div>
<label>현재 사진</label>
<img :src="contact.photo" class="thumb">
</div>
<div>
<label>사진 파일 선택</label>
<label>
<input type="file" ref="photofile" name="photo" class="long btn btn-default">
</label>
</div>
<div>
<div> </div>
<input type="button" value="변 경" class="btn btn-primary"
@click="photoSubmit">
<input type="button" value="취 소" class="btn btn-primary"
@click="cancelEvent">
</div>
</form>
</div>
</div>
</template>
<script>
import eventBus from '../EventBus.js';
export default {
name : 'updatePhoto',
props: ['contact'],
methods: {
cancelEvent : function() {
eventBus.$emit('cancel');
},
photoSubmit : function() {
var file = this.$refs.photofile.files[0];
eventBus.$emit('updatePhoto',this.contact.no, file);
eventBus.$emit('cancel');
}
}
}
</script>
<style scoped>
.modal {
display: block; position: fixed; z-index: 1;
left: 0; top: 0; width: 100%; height: 100%;
overflow: auto; background-color: rgb(0,0,0);
background-color: rgba(0,0,0, 0.4);
}
.form {
z-index: 10; background-color: white; margin: 100px auto;
max-width: 400px; min-width: 200px; font: 13px "verdana";
padding: 10px;
}
.form div { padding: 0; display:block; margin: 10px 0 0 0;}
.form label {
text-align: left; margin: 0 0 3px 0; padding: 0;
display: block; font-weight: bold;
}
.form input, textarea, select {
box-sizing: border-box; border: 1px solid #bebebe;
padding: 7px; margin: 0; outline: none;
}
.form .long {width : 100%;}
.form .heading {
background: #33a17f; font-weight: 300;
text-align: left; padding: 20px; color: #fff;
margin: 5px 0 30px 0; padding: 10px;
min-width: 200px; max-width: 400px;
}
img.thumb { width: 160px;}
</style>
'책 > Vue.js 퀵스타트' 카테고리의 다른 글
vue-router를 이용한 라우팅 (0) | 2020.11.28 |
---|---|
Vuex를 이용한 상태관리 (0) | 2020.11.27 |
컴포넌트 심화 (0) | 2020.11.23 |
Vue-CLI 도구 (0) | 2020.11.22 |
ECMAScript 2015 (0) | 2020.11.22 |