vue

국비 코딩 풀스택 수업 13일차

비루블 2022. 7. 8. 12:45

요약 정리

 

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로 이동 시켜줌
라우터를 이렇게 씀.