국비 코딩 풀스택 수업 13일차
요약 정리
itemContentPage
버튼 개수 제한
물품상제정보 페이지
컨텐츠 아이템 사이 공백 및 간격 만들기
그리드 합치기
null 사용시 v-if
다중 이미지 업로드(이미지 인덱스 안섞이는 정석 방법)
이미지 업로드시 문법 ($event, tmp) 사진정보 + 순서
상세정보 가져오는 handleData, 이미지 파일 가져오는 handleDataImage
같이 들고오면 편하지만 데이터키 등등 기술적 한계로 같이 못들고온다라고 들음
다중 이미지 컨텐츠에 표기, 표시
삭제버튼으로 이미지 삭제
MarketPage
4배열의 컨텐츠 상품 칸 만들기
MarketContentPage
주문하기(로그인창 이동 -> 로그인 이후 -> 있었던 페이지에 정보 유지한채 돌아감) query 쿼리, path 이용
(컨텐츠 -> 주문하기 -> 로그인 -> 컨텐츠)
MarketContentPage
나머지 만들기 실습
오전일과
ItemPage.vue
잠깐 복습
itemContentPage(매우 중요)
오후일과
MarketPage.vue
MarketContentPage.vue
ItemContentPage.vue만들기


<template>
<div>
<h3>물품상세정보</h3>
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">
번호 : {{state.row._id}}<br/>
물품명 : {{state.row.name}}<br/>
가격 : {{state.row.price}}<br/>
수량 : {{state.row.quantity}}<br/>
서버이미지 표시
<div v-for="tmp of state.subimage" :key="tmp"
style="border:1px solid red">
{{tmp._id}}
<img :src="tmp.img" style="width:60px" />
<button @click="handleSubDelete(tmp._id)">삭제</button>
</div>
<hr/>
서버이미지등록
<p v-for="tmp in state.imagecount" :key="tmp">
<img :src="state.imagesurl[tmp-1]" style="width:50px" />
<input type="file" @change="handleChangeImage($event, tmp)"/>
<!-- handleChangeImage === handleChangeImage($event, tmp) -->
</p>
<button @click="handleAdd">항목추가</button>
<button @click="handleSub">항목삭제</button>
<button @click="handleSubImage">이미지등록</button>
{{state.images}}
</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
</div>
</div>
</template>
<script>
import { reactive } 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 state = reactive({
no : Number(route.query.no),
row : '', /* null로 했으면 위에서 if문 써야함 */
imagecount : 2,
images : [null, null, null, null, null, null],
imagesurl : [
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png')
],
subimage : [],
});
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 handleAdd = () => {
state.imagecount++;
if(state.imagecount > 6) {
state.imagecount = 6;
}
};
const handleSub = () => {
state.imagecount--;
if(state.imagecount < 2) {
state.imagecount = 2;
}
};
const handleSubImage = async() => {
const url = `/item101/insertimages.json`;
const headers = {"Content-Type" : "multipart/form-data"};
let body = new FormData();
body.append("code", state.no);
for(let i=0; i<state.images.length; i++){
if(state.images[i] !== null) {
body.append("image", state.images[i]);
}
}
const { data } = await axios.post(url, body, {headers});
if(data.status === 200){
alert('업로드 완료')
handleData(); // 물품의 상세정보
handleDataImage(); // 물품의 서버 이미지들
}
};
const handleChangeImage = (e, idx) => {
console.log('handleChangeImage', e, idx);
if(e.target.files.length > 0){ // 이미지 등록시 파일의 길이가 0보다 커짐
// 파일이 등록 되었다는 소리
console.log(e.target.files)
state.images[idx-1] = e.target.files[0];
// idx-1는 실제 번호차례와 인덱스때문 // e.target.files[0]은 첨부한 파일의 정보
// state.images 배열에 해당 이미지 정보를 넣어라
// state.images는 [null, x 6]
state.imagesurl[idx-1] = URL.createObjectURL(e.target.files[0]);
// 이미지 등록시 화면 표기를 위한 가상의 크롬url 생성
// state.imageurl 또한 배열(noimage 배열6)
}
else{
state.images[idx-1] = null;
// 이미지의 정보가 없다면(이미지 선택 취소)
state.imagesurl[idx-1] = require('../assets/imgs/noimage.png');
// imagesurl의 배열에 다시 noimage를 넣어라
}
console.log(state.images);
};
const handleDataImage = 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.subimage = data.result;
}
};
const handleSubDelete = async(no) => {
const url = `/item101/subimagedelete.json?no=${no}`;
const headers = {"Content-Type" : "application/json"}
const {data} = await axios.delete(url, {headers:headers, data:{}});
if(data.status === 200) {
handleDataImage();
}
};
onMounted(()=>{ // 화면이 로딩될떄 또는 f5를 눌러을떄
handleData(); // 물품의 상세정보
handleDataImage(); // 물품의 서버 이미지들
});
return {state, handleAdd, handleSub, handleSubImage, handleChangeImage, handleSubDelete}
}
}
</script>
<style lang="css" scoped>
.container {
width : 800px;
padding : 10px;
border : 1px solid #cccccc;
display : grid;
grid-template-columns: 2fr 8fr 1fr;
column-gap: 10px; /* 아이템 사이의 가로 간격 */
row-gap: 5px; /* 아이템 사이의 세로 간격 */
}
.item {
border : 1px solid #cccccc;
padding : 20px
}
.item:nth-child(5) { /* 5번은 6번칸이랑 합쳐짐 */
grid-column-end: span 2;
}
.item:nth-child(6) { /* 5번은 6번칸이랑 합쳐짐 */
grid-column-end: span 3;
}
</style>
컨텐츠 아이템 사이의 가로 세로 간격 및 공백 스타일
column-gap, row-gap
style 에서 칸 합치기(컨텐츠 합치기) 2개 3개
한줄 합치기 가능
.item:nth-child(5) { /* 5번은 6번칸이랑 합쳐짐 */
grid-column-end: span 2;
}
.item:nth-child(6) { /* 5번은 6번칸이랑 합쳐짐 */
grid-column-end: span 3;
}
row : '', /* null로 했으면 위에서 if문 써야함 */
v-if 사용 (프론트 엔드 개발자라면 알고 있어야함)
위 템플레이트 부분은 공백이고 아래서 데이터를 끌어온뒤 화면에 표시하기 때문에
v-if를 사용하여 null이라면 공백으로 표기
중요중요중요 다중 이미지 업로드
이미지 항목 추가
최대 6개까지만 첨부하도록 설정
최소 2개까지 유지
이미지 handleSubImage
서버로 올리기
images : [] 6배열만들기
images : [null, null, null, null, null, null],
//handleChangeImage 기입
서버이미지등록
<p v-for="tmp in state.imagecount" :key="tmp">
<img :src="state.imagesurl[tmp-1]" style="width:50px" />
<input type="file" @change="handleChangeImage($event, tmp)"/>
<!-- handleChangeImage === handleChangeImage($event, tmp) -->
</p>
handleChangeImage 괄호 붙이면 언디파인드 뜸
but
handleChangeImage === handleChangeImage($event)
이건 됨
handleChangeImage === handleChangeImage($event, tmp)
이건 반복문일때 여러개의 정보를 들고 올때 사용(다중 이벤트 정보)
$변경시길 정보, tmp 몇번쨰 정보
아래껄로 $event(해당 사진의 정보)와 tmp(몇번째 정보인지) 출력 해봄
const handleChangeImage = (e, idx) => {
console.log('handleChangeImage', e, idx);
};
몇번째 사진인지 잘 나옴
기존 배열로 [1, 3, 2]처럼
데이터가 섞여서
배열을 만들어 놓고 시작하는게 정석
images : [null, null, null, null, null, null],
handleChangeImage 꽤 어려움 사진 참고
배열 뒤섞임 방지 프론트 엔드에서 사용
=====================================================
컨텐츠 게시물 접속시
서버 이미지들 받아오기
handleDataImage()
같은 파일로 관리가 아니기 때문에
상세정보 가져오는 handleData
이미지 파일 가져오는 handleDataImage
같이 들고오면 편하지만
데이터키 등등 기술적 한계로 같이 못들고옴
=========================
다중이미지 화면 표기
subimage에 있는 만큼 표기
서버이미지 표시
<div v-for="tmp of state.subimage" :key="tmp"
style="border:1px solid red">
{{tmp._id}}
<img :src="tmp.img" style="width:60px" />
<button @click="handleSubDelete(tmp._id)">삭제</button>
</div>
const state = reactive({
no : Number(route.query.no),
row : '', /* null로 했으면 위에서 if문 써야함 */
imagecount : 2,
images : [null, null, null, null, null, null],
imagesurl : [
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png'),
require('../assets/imgs/noimage.png')
],
subimage : [],
});
=======================
이미지삭제버튼
handleSubDelete
const handleSubDelete = async(no) => {
const url = `/item101/subimagedelete.json?no=${no}`;
const headers = {"Content-Type" : "application/json"}
const {data} = await axios.delete(url, {headers:headers, data:{}});
if(data.status === 200) {
handleDataImage();
}
};
오후일과
MarketPage.vue

<template>
<div class="container">
<div class="box">
<div class="item" v-for="tmp in state.rows" :key="tmp">
<el-card class="box-card"
@click="handleContent(tmp._id)"
style="cursor:pointer">
<img :src="tmp.img" style="width:100%; height:100px" />
<p v-text="tmp.name"></p>
<p>{{tmp.price}}원</p>
<p style="width: 200px">{{tmp.content}}</p>
</el-card>
</div>
</div>
<el-pagination style="width: 340px; margin: 0 auto;"
background layout="prev, pager, next"
:page-size="12"
@current-change="handlePage" :total="state.total" />
</div>
</template>
<script>
import { reactive } from '@vue/reactivity'
import { onMounted } from '@vue/runtime-core';
import axios from 'axios';
import { useRouter } from 'vue-router';
export default {
setup () {
const router = useRouter();
const state = reactive({
page : 1,
rows : [],
total : 0,
pages : 0,
});
const handleData = async() => {
const url = `/item101/selectlistpage.json?page=${state.page}`;
const headers = {"Content-Type":"application/json"};
const response = await axios.get(url, {headers});
console.log(response.data);
if(response.data.status === 200) {
state.rows = response.data.result;
state.total = response.data.total;
state.pages
= Math.floor( (response.data.total-1)/12 )+1;
}
};
onMounted(()=>{
handleData();
})
const handlePage = (page) => {
console.log(page);
state.page = page;
handleData();
};
const handleContent = ( code ) => {
router.push({path:'/marketc', query:{code:code}});
}
return {state, handleContent, handlePage}
}
}
</script>
<style lang="css" scoped>
.box {
width : 1000px;
padding : 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
column-gap: 5px;
row-gap: 5px;
margin : 0 auto;
}
.item {
border:0px solid #cccccc;
padding: 0px;
}
</style>
특이사항 x
MarketContentPage.vue
<template>
<div>
<h1>마켓 컨텐츠</h1>
<router-link to ="/market">
<button>목록</button>
</router-link>
<hr/>
번호 : {{state.no}} <br/>
물품명 : {{state.row.name}} <br/>
내용 : <label v-html="state.row.content"></label> <br/>
가격 : {{state.row.price}} <br/>
수량 : {{state.row.quantity}} <br/>
날짜 : {{state.row.regdate}} <hr/>
이미지 <br/>
<img :src="state.row.img" style="width:500px"/>
<br/>
<el-select v-model="state.count" placeholder="Select">
<el-option v-for="item in 100" :key="item" :label="item" :value="item"/>
</el-select>
<button @click="handleOrder">주문하기</button>
</div>
</template>
<script>
import { reactive } from "@vue/reactivity";
import { useRoute, useRouter } 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.code),
count : 1,
token : sessionStorage.getItem("TOKEN"),
row : '',
prev : '',
next : '',
subimage : [],
});
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;
console.log(data);
}
};
const handleDataImage = 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.subimage = data.result;
}
};
onMounted(() => {
handleData();
handleDataImage();
});
const handleBoardList = () =>{
router.push({path:'/market'});
}
const handleOrder = async() => {
//로그인 사용자의 토큰
//주문수량과 물품번호
if(state.token !==null) {
alert('주문이 완료되었습니다.')
}
else {
router.push({path:'/login1'})
}
};
return { state, handleBoardList, handleOrder, handleDataImage };
},
};
</script>
<style lang="css" scoped>
.container {
width: 800px;
padding: 10px;
border: 1px solid #cccccc;
display: grid;
column-gap: 5px; /* 아이템 사이에 간격 */
}
.item {
border: 1px solid #cccccc;
padding: 10px;
}
.item:nth-child(2) {
background: #efefef;
}
</style>
주문할때 토큰이 없다면 로그인이 안된 상태로 간주하여 로그인 페이지로 이동시킨다.
로그인 페이지 이동후 로그인시 이전 페이지로 다시 이동 (query + path로 이전 게시물로 이동)
routes > index.js
//라우터를 이동경로를 추적할 수 있음.
router.beforeEach((to, from, next)=>{
console.log('router =>', to, from);
if(to.path !== '/login1'&& to.path !=='/logout1'
&& to.path !== '/login' && to.path !=='/logout'){
sessionStorage.setItem("CURRENT_URL", to.path);
sessionStorage.setItem("CURRENT_QUERY", JSON.stringify(to.query));
}
next();
});
페이지를 이동할때마다 이전 페이지의 path + query를 저장한다.
로그인 및 로그아웃시 저장 제외
to from next 중에서 to만 사용하고 있는 것임.
컨텐츠 -> 주문하기 -> 로그인 -> 컨텐츠
const backurl = sessionStorage.getItem("CURRENT_URL");
const backquery = sessionStorage.getItem("CURRENT_QUERY");
router.push({ path: backurl, query:JSON.parse(backquery) });
로그인에서 로그인 성공시 이전 url과 이전 query로 이동 시켜줌
라우터를 이렇게 씀.