전체적으로 components를 적용시킨 홈페이지를 만들어보자!
-- 1. nodejs 다운로드 (v8엔진)
https://nodejs.org/ko/
-- 2. node, npm 설치 버전 확인
CMD> node -v
18.12.1
CMD> npm -v
8.19.2
-- 2. vue, @vue/cli 설치하기
CMD> npm i vue -g
CMD> npm i @vue/cli -g
-- 3. 설치 확인
CMD> vue --version
@vue/cli 5.0.8
CMD> vue create vue_20221226
CMD> cd vue_20221226
CMD> npm i vue-router@next --save
CMD> npm i axios --save
CMD> npm i vuex --save
CMD> npm i element-plus --save
환경설정파일 : vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 벡엔드 연동 proxy설정정
devServer :{
proxy : {
'/board101' : {
target : 'http://1.234.5.158:23000',
changeOrigin : true,
logLevel:'debug'
},
'/item101' : {
target : 'http://1.234.5.158:23000',
changeOrigin : true,
logLevel:'debug'
},
'/member101' : {
target : 'http://1.234.5.158:23000',
changeOrigin : true,
logLevel:'debug'
},
}
}
})
CMD> npm run serve
// 파일명 : routes/index.js
// 1.라이브러리 가져오기
import { createWebHashHistory, createRouter } from 'vue-router';
// 이번엔 # 들어간 주소로 해보자
// 2. 라우트 설정
import Home from '@/components/HomePage.vue'; // 앞엔 변수명이라 짧아도됨. 뒤에는 파일명
import Seller from '@/components/SellerPage.vue';
import ItemInsert from '@/components/ItemInsertPage.vue' ;
import ItemUpdate from '@/components/ItemUpdatePage.vue' ;
const router = createRouter({
history : createWebHashHistory(),
routes : [
{path:'/', component:Home },
{path:'/seller', component:Seller },
{path :'/iteminsert', component:ItemInsert},
{path :'/itemupdate', component:ItemUpdate},
]
});
// 이전페이지 이동페이지 정보 확인용
router.beforeEach((to, from, next)=>{
console.log(to, from);
next(); // next가 없으면 페이지 이동이 안됨.
});
// 3. 모듈 export
export default router;
// 파일명 : main.js
import { createApp } from 'vue';
import App from './App.vue';
// element-plus
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
// router
import routes from './routes/index';
// index는 안넣어도 되지만 헷갈릴까봐
// createApp(App).설정라이브러리.mount('#app');
// 위와 아래는 같은 표현이지만 위와 같이 표현하면 중간이 계속 길어져서 보기싫어!
const app = createApp(App);
app.use(routes);
app.use(ElementPlus);
app.mount('#app');
// 파일명 : SellerPage.vue
<template>
<div>
<h3>판매자</h3>
<el-button type="primary" @click="handleItemInsert()">물품등록</el-button>
// 라우터 링크로 하니 밑줄 생겨서 함수로 했으나 style에서 text-decoration:none으로 하면 밑줄없어진다구요
<el-table :data="rows" size="small">
// 리턴에서 toRef했기 때문에 state 생략가능
<el-table-column prop="_id" label="물품번호" width="70" />
<el-table-column label="이미지">
<template #default="scope">
<img :src="scope.row.img" style="width:50px;" />
</template>
</el-table-column>
// 이미지를 넣으려면 예제에서 버튼 들어가있는것 찾아보자. 기존의 것은 string만 가능
// 최초에 한번만 데이터를 주고 나머지는 내부에서 데이터를 꺼내서 사용해야한다.
// 데이터를 rows로 통으로 줬기 때문에 하나의 정보만 꺼내는게 필요->scope 이용!
// scope는 components 안의 데이터를 다시 꺼낼 때 사용하는것
// 예제를 보니 scope.row.tag에서 가져왔구나! 그래서 row에 데이터를 받았다는걸 추측해서 사용..! components의 내부를 못보기때문에 예제를 최대한 활용하는수밖에
<el-table-column prop="name" label="물품명" width="70" />
<el-table-column prop="price" label="가격" width="70" />
<el-table-column prop="quantity" label="수량" width="70" />
<el-table-column prop="content" label="내용" />
<el-table-column prop="regdate" label="날짜" width="170"/>
<el-table-column label="버튼">
<template #default="scope">
<el-button size="small" @click="handleUpdate(scope.row._id)">수정</el-button>
<el-button size="small" @click="handleDelete(scope.row._id)">삭제</el-button>
// 삭제/수정하고자 하는 글의 번호 정보를 줘야 한다
// 여기에 scope를 쓰는이유? 밑에서 준 data를 다운 받은 components에 넣어주고 싶어서? 다시 한번 찾아보자
</template>
</el-table-column>
</el-table>
<div>
<el-pagination small background layout="prev, pager, next" :page-size=12 :total="total" @current-change="handlePage" />
// 함수 뒤에 () 안붙이는 이유는 주는 파라미터가 몇갠지 모르기 때문에 비워두는것!
</div>
<el-dialog v-model="centerDialogVisible" title="Warning" width="30%" center>
<span>
삭제할까요?
</span>
<template #footer>
<el-button type="primary" @click="handleDeleteAction()">
확인
</el-button>
<el-button @click="centerDialogVisible = false">
취소
</el-button>
// 클릭했을때 값을 바로 바꿔주도록
</template>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
import { reactive, toRefs } from '@vue/reactivity';
import { onMounted } from '@vue/runtime-core';
import { useRouter } from 'vue-router';
export default {
setup () {
// 상태 변수
const state = reactive({
page : 1,
rows : null, //[{},{},{}]
total : 0,
centerDialogVisible : false,
no : 0,
});
const router = useRouter();
// 함수
const handleUpdate = (no) => {
router.push({path:'/itemupdate', query:{no:no}});
}
const handleItemInsert = () => {
router.push({path:'/iteminsert'});
};
const handleDeleteAction = async() => {
// 벡엔드를 호출해서 삭제하기
const url = `/item101/delete.json?no=${state.no}`;
const headers = {"Content-Type":"application/json"};
const body = {};
const { data } = await axios.delete(url, {headers:headers, data:body});
console.log(data);
if(data.status === 200){
// 다이얼로그 닫기
state.centerDialogVisible = false;
handleData();
}
}
const handleDelete = (no) => {
console.log(no);
state.no = no;
state.centerDialogVisible = true;
};
// dialog는 화면상에 존재하지만 숨겨져있다가 삭제버튼 눌리면 보이게 하는것
// 보이게 하는 타이밍이 centerDialogVisible 값이 참이 될때 인것!
// 취소 눌러서 창 사라지게 하려면 centerDialogVisible 값을 거짓으로 변경
const handlePage = ( no ) => {
console.log(no);
state.page = no;
handleData();
};
const handleData = async() =>{
const url = `/item101/selectlistpage.json?page=${state.page}`;
const headers = {"Content-Type":"application/json"};
const { data } = await axios.get(url, {headers});
// axios가 import 안되서 수동으로 함... 오류생기면 import 확인
console.log(data);
if(data.status === 200){
state.rows = data.result;
state.total = data.total;
for(let tmp of data.result) {
if(tmp.content.length >20) {
tmp.content = tmp.content.substring(0, 20) + "...";
// 길이 20자 이상이면 이후는 ...으로 보이도록 substring 사용!
}
}
}
};
// 생명주기
onMounted(()=>{
handleData();
});
// 리턴
return {
state,
...toRefs(state),
handlePage,
handleDelete,
handleDeleteAction,
handleItemInsert,
handleUpdate
}
}
}
</script>
<style lang="css" scoped>
</style>
// 파일명 : ItemUpdatePage.vue
<template>
<div>
{{ state }}
<div v-if="row !== null">
<div>
<label>물품명</label>
<el-input v-model="row.name" style="width: 400px;" />
</div>
<div>
<label>물품 설명</label>
<el-input type="textarea" v-model="row.content" :rows="6" style="width: 400px;" />
</div>
<div>
<label>수량</label>
<el-input-number v-model="row.quantity" :min="1" />
</div>
<div>
<label>가격</label>
<el-input-number v-model="row.price" :min="1" />
</div>
<div>
<label></label>
<el-button type="info" size="small" @click="handleUpdate()">수정하기</el-button>
</div>
<hr />
<div v-for="tmp of state.imageurl" :key="tmp" style="display:inline-block;">
<img :src="tmp.img" style="width: 100px;" />
</div>
<div v-for="tmp of state.cnt" :key="tmp">
<input type="file" @change="handleImage(tmp, $event)" />
// tmp는 위치정보, $event는 이미지의 정보(첨부 또는 취소 정보)
// input type="file"에는 v-model이 안걸린다..!
</div>
<button @click="FilePlus()">항목+</button>
<button @click="FileMinus()">항목-</button>
<button @click="handleSubImage()">서브이미지등록</button>
</div>
</div>
</template>
<script>
import { reactive, toRefs } from '@vue/reactivity';
import { useRoute } from 'vue-router'
import { onMounted } from '@vue/runtime-core';
import axios from 'axios';
export default {
setup () {
const route = useRoute();
const router = useRouter();
const state = reactive({
no : Number( route.query.no ), //주소창 ?no=557
row : null,
cnt : 2,
images : [null, null, null, null, null], // 최대 5개 가능, 그냥 []로 표현해도 됨
imageurl : [],
});
// 이미지가 변경될 시 정보를 state.images배열에 추가
// v-model을 사용할 수 없음, 수동으로 처리해야 함.
const handleImage = (tmp, img) => {
// tmp는 위치정보
// img는 이미지의 정보(첨부 또는 취소정보)
console.log(tmp-1, img); // 배열은 0부터 시작이므로 tmp-1
if(img.target.files.length>0){
state.images[tmp-1] = img.target.files[0];
}
else {
state.images[tmp-1] = null;
}
};
// 추가시 물품번호 필요
const handleSubImage = async() => {
console.log('handleSubImage');
const url = `/item101/insertimages.json`;
const headers = {"Content-Type":"multipart/form-data"};
const body = new FormData();
body.append("code", state.no); // code가 물품번호! 조회할때는 물품번호 필요
for(let i=0; i<state.cnt; i++){// 배열은 시작하는 값이 0이므로 등호 안들어감!
body.append("image", state.images[i]);
}
const { data } = await axios.post(url, body, {headers});
console.log(data);
};
const FilePlus = () => {
state.cnt++;
if(state.cnt > 5) {
state.cnt = 5; // 파일 첨부 창 최대 5개
}
};
const FileMinus = () => {
state.cnt--;
if(state.cnt < 2) {
state.cnt = 2; // 파일 첨부 창 최소 2개
}
};
const handleUpdate = async() => {
const url = `/item101/update.json?no=${state.no}`;
const headers = {"Content-Type":"application/json"};
const body = {
name : state.row.name,
price : state.row.price,
content : state.row.content,
quantity : state.row.quantity
}
const { data } = await axios.put(url, body, {headers});
console.log(data);
if(data.status === 200) {
router.push({path:'/seller'});
}
};
const handleData = async() => {
const url = `/item101/selectone.json?no=${state.no}`;
const headers = {"Content-Type":"application/json"};
const { data } = await axios.get(url, {headers});
console.log(data);
if(data.status === 200) {
state.row = data.result;
}
};
// 서브 이미지 읽기
const handleData1 = async() => {
const url = `/item101/subimagecode.json?code=${state.no}`;
const headers = {"Content-Type":"application/json"};
const { data } = await axios.get(url, {headers});
console.log(data);
if(data.status === 200) {
state.imageurl = data.result;
}
};
onMounted(() => {
handleData();
handleData1();
});
return {
state,
...toRefs(state),
FilePlus,
FileMinus,
handleSubImage,
handleImage,
};
}
}
</script>
<style lang="css" scoped>
label {
display: inline-block;
width: 100px;
}
</style>
// 파일명 : ItemInsertPage.vue
<template>
<div>
<div>
<label>물품명</label>
<el-input v-model="state.name" style="width: 400px;" placeholder="Please input" />
</div>
<div>
<label>물품 설명</label>
<el-input type="textarea" v-model="state.content" :rows="6" style="width: 400px;" placeholder="Please input" />
</div>
<div>
<label>수량</label>
<el-input-number v-model="state.quantity" :min="1" />
</div>
<div>
<label>가격</label>
<el-input-number v-model="state.price" :min="1" />
</div>
<div>
<label>이미지</label>
<img :src="state.imageurl" style="width: 100px;"/>
<input type="file" style="width: 200px;" @change="handleImage($event)" />
// 여기에 el-input으로 하니 e.target.files에 보관 되있지 않네...! 어디에 들어가있는지 예를 봐도 모르면 어떻게 하나?
// input에는 change 안되게 되어있었음... upload 이용해야함
</div>
<div>
<label></label>
<el-button type="info" size="small" @click="handleInsert()">등록하기</el-button>
</div>
</div>
</template>
<script>
import { reactive } from '@vue/reactivity'
import axios from 'axios';
import { useRouter } from 'vue-router';
export default {
setup () {
const state = reactive({
name : null,
content : null,
quantity : 0,
price : 0,
imagedata : null,
imageurl : require('../assets/imgs/noimage.png')
});
const router = useRouter();
const handleImage = (e) => {
console.log(e);
if(e.target.files.length > 0) {
state.imagedata = e.target.files[0];
state.imageurl = URL.createObjectURL(e.target.files[0]);
}
else {
state.imagedata = null;
state.imageurl = require('../assets/imgs/noimage.png')
}
}
const handleInsert = async() => {
const url = `/item101/insert.json`;
const headers = {"Content-Type":"multipart/form-data"};
let body = new FormData();
body.append('name', state.name);
body.append('price', Number(state.price) );
body.append('content', state.content);
body.append('quantity', Number(state.quantity) );
body.append('image', state.imagedata);
const { data } = await axios.post(url, body, {headers});
console.log(data);
if(data.status === 200) {
router.push({path:'/seller'});
}
}
return {
state,
handleImage,
handleInsert
}
}
}
</script>
<style lang="css" scoped>
label {
display: inline-block;
width: 100px;
}
</style>
참고) parameter에 대해서 -> https://axce.tistory.com/55