본문 바로가기

책/Vue.js 퀵스타트

axios를 이용한 서버통신

서비스 API 소개

 

stepanowon/contactsvc

node.js + express + sqlite 기반의 간단한 RESTful Service 예제 - stepanowon/contactsvc

github.com

  • 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)

  1. 브라우저가 HTML 페이지를 가진 웹사이트(localhost:8080)에 접속해서 HTML문서를 다운로드 하고 보여주게됨
  2. 이때 브라우저의 Origin은 http://localhost:8080으로 자동설정
  3. 이 의미는 "현재 보여주고 있는 문서는 Origin에서 내려받은 것이다." 임
  4. 이 HTML문서에서 다른 외부 서버와 통신하려는 경우, 현재 브라우저의 Origin과 다른 Origin에 해당하는 서버와 통신하려고 할때 응답 전송까지는 정상적으로 수행되지만 브라우저로 로딩하는 단계에서 오류가 발생
  5. 보안 정책으로 인해 크로스 오리진으로부터 데이터를 로드할 수 없는 현상
    • 오리진 정보가 한 글자라도 다르면 크로스 오리진 상태

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>&nbsp;</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>&nbsp;</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