카테고리 없음

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

비루블 2022. 7. 12. 12:58

요약정리

오전

백엔드 코드 작성

(exp_20220711>models>boardmodel.js)ㄴ

(exp_20220711>routes>board.js)

현재 시간 생성, 이미지, 파일 첨부 (모듈 설치 포함)

원하지 않는 데이터 제외하고 나머지 호출하는 방법

게시물 호출시 이미지도 같이 호출하기

return res.send({status:200, result:result, image:'/board/image?no='+no});

 

const 상수로 변경 불가능(하위는 가능)
var 어디서든지 사용가능(범위가 더 넓음)
let는 {}안에 만들면 안에서만 사용가능 (구역)

 

오후

집에서 할때 깃헙에서 내려 받고 node설치 방법(npm install)

 

꾸미지 않고 기본적인 요소만 입력

BoardSelect.vue 게시판(백엔드에서 작성한 text(검색어 포함된 글 찾기) 매우 중요, )

BoardInsert.vue 글쓰기

BoardContent.vue(실습)


오전일과

boardmodel.js

board.js(get 3, post 1)

 

exp > cmd > npm start

서버실행


exp_20220711 > models > boardmodel.js 보드 만들기

기본사항 적기(파일 포함)

// npm i mongoose --save
var mongoose = require('mongoose');

// 자동 번호 설정용, 시퀀스
// npm i mongoose-sequence --save
const AutoIncrement = require('mongoose-sequence')(mongoose);

var Schema = mongoose.Schema;
var boardSchema =  new Schema({
    _id : Number, // 기본키, 고유값, not null 비어있으면 안됨
    title   : {type:String, default:''},
    content : {type:String, default:''},
    writer  : {type:String, default:''},
    hit     : {type:Number, default:1}, //조횟수, 숫자 기본값1
    regdate : {type:String, default:''},
    filedata : {type:Buffer, default:null}, //파일데이터
    filesize : {type:Number, default:0}, //파일 크기
    filename : {type:String, default:''}, //파일 이름
    filetype : {type:String, default:''}, //파일의 종류 jpg,png등
});

// 스키마 항목중에서 시퀀스 필요한 것 설정
boardSchema.plugin(AutoIncrement, 
    {id:"SEQ_BOARD_NO", inc_field:'_id'});

//몽고db에 board컬렉션 생성됨.
module.exports = mongoose.model('boards', boardSchema);

bookmodel에서 양식을 끌어옴.

바뀐점은 중간은 싹 바꼇고 위는 그대로 하단부분은 조금 바뀜

 

 

exp_20220711 > routes > board.js 만들어만 놓기

 


-- 현재 시간 생성(cmd경로는 exp_20220711임)
CMD> npm i moment --save
CMD> npm i moment-timezone --save

-- 이미지 첨부
CMD> npm i multer --save

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
파일 업로드시 db에서 파일명이 깨져서 옛버전으로 설치
npm i multer@1.4.4

모듈 설치까지 해줌
-----------------------------------------

exp_20220711 > app.js에 board 추가

var boardRouter = require('./routes/board');

app.use('/board', boardRouter);


------------------------------------------
board.js 작성
multer
upload
moment

var express = require('express');
var router = express.Router();

// npm i multer --save
const multer = require('multer')

// 첨부되어 오는 파일을 저장하는 방식 ex)파일로 저장, 메모리로 저장
// 여기서는 데이터 베이스에 저장해야하기 때문에 메모리로 저장
const upload = multer({storage : multer.memoryStorage()});

// npm i moment
// npm i moment-timezone --save
require('moment-timezone');
var moment = require('moment');
moment.tz.setDefault('Asia/Seoul'); // 표준시 UTC 시간, 아시아.서울로 설정

//db연동을 위한 모델 ORM
var Board = require('../models/boardmodel');

기본 사항 + 이미지에 관련된 upload + 시간 moment 넣어줌



post 제목, 내용, 작성자, 파일 업로드

 포스트맨으로 post해보고 
 console.log(req.body); //db cmd보고 body로 전달 되는 것을 확인함
 console.log(req.file); //마찬가지 flile로 오는 것을 확인함

// 크롬에서 127.0.0.1:3000/board/insert.json
//{"title":"aaa", "content":"bbb", "wirter":"ccc", "flile":첨부파일}
//파일 넣을꺼면 upload.sing("") 넣어줘야함
router.post('/insert.json', upload.single("file"), async function(req, res, next) {
    try{
        console.log(req.body); //db cmd보고 body로 전달 되는 것을 확인함
        console.log(req.file); //마찬가지 flile로 오는 것을 확인함

        var obj = new Board(); // 빈 객체를 생성
        obj.title = req.body.title;
        obj.content = req.body.content;
        obj.writer = req.body.writer;
        obj.regdate = moment().format('YYYY-MM-DD HH:mm:ss');

        obj.filedata = req.file.buffer;
        obj.filename = req.file.originalname;
        obj.filetype = req.file.mimetype;
        obj.filesize = req.file.size;

        const result = await obj.save();
        if(result !== null){
            return res.send({status:200})
        }
        return res.send({status:0})

    }
    catch(e){
        console.error(e);
        return res.send({status:-1, result:e});
    }
});

받아 온 것들을 각각 만든 obj에 넣어줌.

 

 


get select 토탈(total), 페이지 개수(page)

// 127.0.0.1:3000/board/select.json?page=5&text=a
router.get('/select.json', async function(req, res, next) {
    try{
        const page =Number(req.query.page); //5
        const text = req.query.text; //a

        //1페이지이면 점프 10개
        //2페이지이면 점프 20개

        const query = {title: new RegExp(text,'i')};
        const projection = {_id:1, title:1, writer:1, hit:1, regdate:1}
        const sort = {_id:-1}; //오름차수는 1 내림은 -1
        const result = await Board.find(query,projection)
        .sort(sort).skip((page-1)*10).limit(10); // 조건이 들어와야함, 가져올 항목 중요

        const total = await Board.countDocuments(query);
        if(result !== null){
            return res.send({status:200, result:result, total:total});
        }
        return res.send({status:0})

    }
    catch(e){
        console.error(e);
        return res.send({status:-1, result:e});
    }
});

위는 글 찾기 하면서 수정된 코드임.

원래는 const query = {};

 

        const query = {title: new RegExp(text,'i')};
        const projection = {_id:1, title:1, writer:1, hit:1, regdate:1}
        const sort = {_id:-1}; //오름차수는 1 내림은 -1
        const result = await Board.find(query,projection)
        .sort(sort).skip((page-1)*10).limit(10); // 조건이 들어와야함, 가져올 항목 중요


//가져올 데이터 항목 따로 관리(길어지면 힘들기 때문)
const sort = {_id:-1}; //오름차수는 1 내림은 -1
const result = await Board.find(query,projection).sort(sort);
// find안에 조건이 들어와야함, 가져올 항목 여기서는 query 쿼리, projection 프로젝션 중요중요중요중요(projection)


토탈 내보내기

const total = await Board.countDocuments(query);
if(result !== null){
    return res.send({status:200, result:result, total:total});
}
return res.send({status:0})



페이지네이션(query 쿼리 이용)

// 127.0.0.1:3000/board/select.json?page=5&text=a
router.get('/select.json', async function(req, res, next) {
    try{
        const page =Number(req.query.page); //5
        const text = req.query.text; //a



글찾기 (검색 글자 포함 글들 찾기, 포함 문구)

const query = {title:text};
이렇게하면 딱 그것만 찾아짐

const query = {title: new RegExp(text,'i')};
이렇게하면 찾을려는 텍스트를 포함한 게시글이 찾아짐



11개 게시글일떄 10개의 게시물 표시
  1페이지이면 점프 0개
  2페이지이면 점프 10개

const result = await Board.find(query,projection).sort(sort).skip((page-1)*10).limit(10);

 

페이지 1개 뺴고 10 곱하면 됨.



selectone만들기 상세화면

// 127.0.0.1:3000/board/selectone.json?no=5
router.get('/selectone.json', async function(req, res, next) {
    try{
        const no =Number(req.query.no); //5

        const query = {_id:no};
        const projection = {filedata:0, filesize:0, filetype:0, filename:0};
    

        const result = await Board.find(query).select(projection)
        
        if(result !== null){
            return res.send({status:200, result:result, image:'/board/image?no='+no});
        }
        return res.send({status:0})

    }
    catch(e){
        console.error(e);
        return res.send({status:-1, result:e});
    }
});

 

select에서 뺄거 다 뺌
const projection = {filedata:0, filesize:0, filetype:0, filename:0};
위 4개 항목을 제외하고 데이터를 호출
원하지 않는 데이터 제외하고 나머지 호출하는 방법

 


get image 만듬(이미지 따로 불러내기) selectone과 다르게 findOne사용!!!!!!!!

//127.0.0.1:3000/board/image?no=4
//<img src="/board/image?no=4" style"width:50px" /> 뷰에서는 이렇게 쓸 예정
router.get('/image', async function(req, res, next) {
    try{
        const no = Number(req.query.no);
        const query = {_id:no};
        const projection = { filedata:1, filesize:1, filetype:1, filename:1};
        const result = await Board.findOne(query).select(projection);
        
        console.log(result);
        res.contentType(result.filetype); // contentType변경
        return res.send(result.filedata); // 파일데이터 
    }
    catch(e){
        console.error(e);
        return res.send({status:-1, result:e});
    }
});


const projection = {filedata:1, filesize:1, filetype:1, filename:1};
const result = await Board.findOne(query).select(projection);


get selectone 부분에 result에 image:'/board/image?no='+no 를 포함시킴

게시물을 가져올때, 이미지도 해당 넘버의 이미지도 같이 호출하려고

return res.send({status:200, result:result, image:'/board/image?no='+no});

오후 일과

vue_20220711>BoardSelect.vue

<template>
    <div>
        <h3>게시판 목록</h3>
        <router-link :to="{path:'/boardinsert'}">글쓰기</router-link>

        <input type="text" placeholder="검색어" v-model="state.text" @keyup.enter="handleText()"/>
        <table border="1">
            <tr>
                <th>번호</th>
                <th>제목</th>
                <th>작성자</th>
                <th>조회수</th>
                <th>날짜</th>
            </tr>
            <tr v-for="tmp of state.rows" :key="tmp._id">
                <td>{{tmp._id}}</td>
                <td @click="handleContent(tmp._id)" style="cursor:pointer">{{tmp.title}}</td>
                <td>{{tmp.writer}}</td>
                <td>{{tmp.hit}}</td>
                <td>{{tmp.regdate}}</td>
            </tr>
        </table>
        <div v-for="tmp of state.pages" :key="tmp" 
            style="display:inline-block">
            <button v-text="tmp" @click="handlePage(tmp)"></button>
        </div>
    </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,
            text : '',
            rows : [],
            total : 0,
            pages : 0,
        });
        
        const handleData = async() =>{
            const url = `/board/select.json?page=${state.page}&text=${state.text}`;
            const headers = {"Content-Type" : "application/json"};
            const {data} = await axios.get(url, {headers});
            console.log(data);
            if(data.status === 200){
                state.rows = data.result; //게시물 내용
                state.total = data.total; //게시물 개수
                //21 => (21-1)/10 + 1
                state.pages = Math.floor((data.total-1)/10)+1;
            }
        };

        const handlePage = (tmp) =>{
            state.page = tmp;
            handleData();
        }

        const handleText = () =>{
            state.page = 1;
            handleData();
        };

        const handleContent = async(no) => {
            console.log('handleContent', no);
            const url = `/board/selectone.json?no=${no}`;
            const headers = {"Content-Type":"application/json"};
            
            const {data} = await axios.get(url, {headers});

            console.log(data);
            if(data.status ===200){
                router.push({path:'/boardcontent', query:{sendno : no}});
            }
        };
        onMounted(()=> {
            handleData();
        });

        return {state, handlePage, handleText, handleContent}
    }
}
</script>

<style lang="scss" scoped>

</style>

 

라우트 등록


vue.config.js작성

      '/board': {
        target : 'http://127.0.0.1:3000',
        changeOrigin : true,
        logLevel : 'debug'
      }


코드 작성

기초 테이블

<table border="1">
    <tr>
        <th>번호</th>
        <th>제목</th>
        <th>작성자</th>
        <th>조회수</th>
        <th>날짜</th>
    </tr>
    <tr v-for="tmp of state.rows" :key="tmp._id">
        <td>{{tmp._id}}</td>
        <td @click="handleContent(tmp._id)" style="cursor:pointer">{{tmp.title}}</td>
        <td>{{tmp.writer}}</td>
        <td>{{tmp.hit}}</td>
        <td>{{tmp.regdate}}</td>
    </tr>
</table>

 

기초 페이지네이션

<div v-for="tmp of state.pages" :key="tmp" 
    style="display:inline-block">
    <button v-text="tmp" @click="handlePage(tmp)"></button>
</div>

 


키업엔터(검색어 텍스트 글 찾기) 아주 중요.

백엔드에서 코드 만든 것을 통해 원하는 글자가 포함된 글자를 가져옴exp_20220712>routes>board.js

const text = req.query.text; //a
const query = {title: new RegExp(text,'i')};

vue_20220712>src>components>BoardSelect.vue

<input type="text" placeholder="검색어" v-model="state.text" @keyup.enter="handleText()"/



BoardInsert만들기 (간단한 글쓰기 양식 만들기)

<template>
    <div class="container">
        <h3>글쓰기</h3>
        
        <div>
            <label class="lbl">제목</label>
            <input type="text" placeholder="제목" v-model="state.title" />
        </div>

        <div>
            <label class="lbl">내용</label>
            <textarea rows="6" placeholder="내용" v-model="state.content"></textarea>
        </div>

        <div>
            <label class="lbl">작성자</label>
            <input type="text" placeholder="작성자" v-model="state.writer"/>
        </div>

        <div>
            <label class="lbl">이미지</label>
            <img :src="state.img" style="width:150px"/>
            <input type="file" @change="handleImage($event)"/>
        </div>
        
        <div>
            <label class="lbl"></label>
            <button @click="handleInsert">글쓰기</button>
        </div>
    </div>    
</template>

<script>
import { reactive } from '@vue/reactivity';
import axios from 'axios';
import { useRouter } from 'vue-router';
export default {
    setup () {
        const router = useRouter();

        const state = reactive({
            img      : require('../assets/imgs/noimage.png'),
            title     : '',
            content  : '',
            writer    : '',
            file     : null,        
        });
        
        const handleImage = (e) => {

            console.log(e.target.files[0]);
            console.log(e.target.files.length);
            if(e.target.files.length > 0) {
                state.file = e.target.files[0];
                state.img  = URL.createObjectURL( e.target.files[0] );	
            }
            
            else{
                state.file = '';
                state.img = require('../assets/imgs/noimage.png');
            }
        };


        const handleInsert = async() => {
            //유효성 검사

            const url = `/board/insert.json`;
            const headers = {"Content-Type" : "multipart/form-data"};
            
            //이미지가 있는 경우 body 정보 담기
            let body = new FormData();
            body.append("title", state.title);
            body.append("content", state.content);
            body.append("writer", state.writer);
            body.append("file", state.file);

            const {data} = await axios.post(url, body, {headers});
            console.log({data});
            if(data.status === 200){
                alert('게시물 등록')
                router.push({path:'/boardselect'});
            }
        }

        return {state, handleInsert, handleImage}
    }
}
</script>


<style lang="css" scoped>
    .container {
        width: 600px;
        border: 1px solid #cccccc;
        padding:20px;
    }
    .lbl {
        display: inline-block;
        width: 90px;
    }
</style>


특이사항:

가장 기본적인 모양의 글쓰기 양식

라벨, 인풋에 몰아 넣기(type, placeholder, v-model)

handleImage로 이미지 입력시 입력한 이미지로 출력

취소시 폴더의 noimage로 출력

글쓰기 handleInsert (사진이라 multipart/form-data 사용, body 사용, append 할때 양식)

 


BoardContent만들기 실습(내가 직접 만듬 다음시간에 해주실지 모르겠음.)

<template>
    <div>
        <h3>게시판상세</h3>
        {{ state }} <!-- 처음에 해놓는게 좋음 -->
        <div v-if="state.row">
            글번호 : {{state.row._id}} <br/>
            제목 : {{state.row.title}} <br/>
            제목 : {{state.row.content}} <br/>
            작성자 : {{state.row.writer}} <br/>
            조회수 : {{state.row.hit}} <br/>
            등록일 : {{state.row.regdate}} <br/>
            <img :src="state.img" style="width:150px"/> <br/>
            <router-link to ="/boardselect">
                <button>목록</button>
            </router-link>
        </div>
    </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(); //query값 받기
        const router = useRouter(); //페이지 이동 import 필수
        console.log(route);

        const state = reactive({ // 초기선언
            no : Number(route.query.sendno), // 목록에서 전달되는 글번호
            row : null, // 게시글 내용 1개
            img : '',
        });
        
        const handleData = async() => {
            const url = `board/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[0];
                state.img = data.image;
                console.log(data.image);
            }
        }
        onMounted( () => {
            handleData();
        });

        const handleUpadte = async() =>{
            // 컴포넌트 없음
            // BoardUpdatepage.vue 생성
            // 라우트 연결
            router.push({path:'/boardu', query :{sendno: state.no} });
        }
        return {state, handleUpadte}
    }
}
</script>

<style lang="scss" scoped>

</style>

직접 한거라 특이사항 x