vue

 

 

vuetifyjs 테이블 페이징  https://vuetifyjs.com/en/components/data-tables/#external-sorting

모던웹(NEMV) 혼자 제작 하기 3기 - 72 게시판 페이징과 정렬 처리하기

소스 : https://github.com/braverokmc79/vf/commit/31afe53a854d420a93f54ccf08d0a7378bf0c542

 

 

1. 백엔드  nodejs   처리

const app = require('express')();
const cors = require('cors');
const admin = require('firebase-admin');
const db = admin.firestore();
require('express-async-errors');

app.use(cors({ origin: true }));



//인증 미들웨어
app.use(require('../middlewares/verifyToken'));


app.use((req, res, next) => {
    if (req.claims.accessLevel < 9) return res.status(403).send({ message: "not authorized" })
    next();
});



app.get('/users', async (req, res) => { 
   
    let { offset, limit, order, sort , search} = req.query
    offset = Number(offset);
    limit=Number(limit)
  
    const r = {
        items: [],
        totalCount:0
    }
    let snapshot = null;
    
    if (search) {       
        snapshot = await db.collection('users').where('email', '==', search).get();
        r.totalCount = snapshot.size;
    } else {
        const t = await db.collection('infos').doc('users').get();
        r.totalCount = t.data().counter;
        snapshot = await db.collection("users").orderBy(order, sort).offset(offset).limit(limit).get();
    }

    snapshot.forEach(v => {
        r.items.push(v.data())
    })

   // console.log(r);
    res.send(r);
});




app.get('/search', async (req, res) => {
    console.log(" search parameter : " + req.query.search);
    const snapshot = await db.collection('users').where('email', ">=", req.query.search).limit(3).get();  
    
    console.log("snapshot   ==>");
    console.log(snapshot);
    const items = [];

    snapshot.forEach(v => {
        //데이터 중에 이메일만 가져온다.
        items.push(v.data().email)
    })
    console.log(items);
    res.send(items);
});




//에러 처리 미들웨어
app.use(require('../middlewares/error'))



module.exports = app;



 

 

2. Vue  처리 

vuetify 의  페이징 처리는 다음 코드내용을 이해하고 

다음 vuetifyjs 주소를 참조해서 개발

vuetifyjs 테이블 페이징  https://vuetifyjs.com/en/components/data-tables/#external-sorting

 

user.vue

<template>
    
    <v-container  grid-list-md fluid>
        <v-row>
             <v-col cols="12" sm="12">
<v-toolbar
    dark
    color="primary"
  >
    <v-toolbar-title>회원관리</v-toolbar-title>
           
                <!-- multiple
                  outlined
                 -->
                <!-- <v-combobox
                        v-model="search"
                        :items="emails"
                        label="이메일을 입력해 주세요."
                        @update:search-input="searchEmails"
                      
                        dense
                        :loading="loadingSearch"                       
                ></v-combobox>               -->

                    <v-autocomplete
                        v-model="email"
                        :loading="loadingSearch"                           
                        :items="emails"
                         @update:search-input="searchEmails"
                         cache-items                        
                        label="이메일을 입력해 주세요."
                        flat
                        hide-no-data
                        hide-details
                        solo-inverted  
                        clearable
                        class="mx-4"  
                                                                             
                ></v-autocomplete> 
                <v-btn icon class="mx-4"  @click="searchBtn" >
                <v-icon>mdi-dots-vertical</v-icon>
                </v-btn>
            </v-toolbar>

            </v-col>            

        </v-row>

        <v-row>
            <v-col cols="12" sm="12">

                <v-card elevation="5">
                    <v-card-title primary-title="primary-title">
                        사용자 관리   - 전체 갯수 {{totalCount}}
                        <v-spacer></v-spacer>
                        <v-btn small="small" color="primary" @click="list" class="mr-3">get List</v-btn>
                        <v-btn small="small" color="primary" @click="corsTest">corsTest</v-btn>
                    </v-card-title>
              
                    <v-divider class="mt-5 mb-5"></v-divider>

                    <v-card-text>
                        <v-data-table
                            :headers="headers"
                            :items="items"
                            :items-per-page="5"

                            :options.sync="options"
                            :server-items-length="totalCount"
                            :loading="loading"
                            class="elevation-1"                           
                           

                            :footer-props="{
                                showFirstLastPage: false,
                               'items-per-page-text':'페이지당 표시'
                            }"
                            ></v-data-table>




<template>
  <v-card
    max-width="auto"
    class="mx-auto"
  >
    <v-container>
      <v-row dense>      
        <v-col  v-for="(item, i) in items" :key="i"              >
          <v-card color="#385f73" dark>
            <div class="d-flex flex-no-wrap justify-space-between">
              <div>
                <v-card-title class="text-h5" v-text="item.email" ></v-card-title>
                <v-card-subtitle v-text="item.name"></v-card-subtitle>
                <v-card-actions>
                  <v-btn              
                    class="ml-2 mt-5"
                    outlined
                    rounded
                    small
                  >
                    {{item.user_id}}
                  </v-btn>
                </v-card-actions>
              </div>
            </div>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </v-card>
</template>




                    </v-card-text>
                </v-card>

            </v-col>
        </v-row>
    </v-container>
</template>

<script>
import _ from 'lodash';

    export default {
        data() {
            return {  

                headers: [
                    {
                        text: '아이디',
                        align: 'start',                       
                        value: 'user_id',
                        // sortable: false,
                    }, {
                        text: '이름',
                        value: 'name',                       
                    }, {
                        text: '이메일',
                        value: 'email',                       
                    }
                ],
                items: [],
                totalCount:0,                
                loading: false,
                options: {
                    sortBy:['user_id'],
                    sortDesc:[false],
                    mustSort:true, //화살표 방향 반드시 표시
                },

                search:'',
                emails:[],
                email:null,
                loadingSearch:false

,   
            items2: [
                    {
                    color: '#1F7087',
                    src: 'https://cdn.vuetifyjs.com/images/cards/foster.jpg',
                    title: 'Supermodel',
                    artist: 'Foster the People',
                    },
                    {
                    color: '#952175',
                    src: 'https://cdn.vuetifyjs.com/images/cards/halcyon.png',
                    title: 'Halcyon Days',
                    artist: 'Ellie Goulding',
                    },
                ],


            }
            
        },

        watch: {
            options: {
                 handler() {                    
                    this.list();
                 },
                 deep:true
            },

            search(val){
                console.log(" var : " +val);
                val && val !== this.select  && this.searchEmails(val);
            },

            emails(n,o){
                if(n!==o) this.list();
            }
        },
            
        methods: {
            async list() {      
                console.log(" search : " +this.search)          ;
                this.loading=true;
                console.log("해당 header 컬럼 value 값이    this.options.sortBy[0]으로 변경처리 된다.  = >" +this.options.sortBy[0]);
                const {data} = await this.$axios.get("admin/users", {
                    params:{
                        offset: this.options.page>0 ? (this.options.page-1) * this.options.itemsPerPage:0 ,
                        limit:this.options.itemsPerPage,
                        order:this.options.sortBy[0],
                        sort:this.options.sortDesc[0] ? 'desc' : 'asc',
                        search:this.email
                    }
                });     
                 
                this.items=data.items;
                this.totalCount=data.totalCount;                           
             
                console.log("this.options=>");
                console.log(this.options);

                this.loading=false;
            },

            async corsTest(){
                //const data=await this.$axios.get("corsTest");
                //
            },


            // async searchEmails(){
            //     this.loadingSearch=true
            //     const {data} =await this.$axios.get("admin/search");
            //     this.emails =data;
            //     this.loadingSearch=false;
            // }
                
            searchEmails: _.debounce(
               function(val){
                   this.loadingSearch=true
             
                   this.search=val;
                   this.$axios.get('admin/search',{
                       params:{search:val}
                   })
                    .then(({data})=>{
                        this.emails=data;
    
                    })
                    .catch(e=>{
                        this.$toasted.global.error(e.message);
                    })
                    .finally(()=>{
                        this.loadingSearch=false;
                    })
                   
               }, 500
           )                
            ,

            searchBtn(){
                this.list();
            }

        
        }


    }
</script>

<style></style>

 

 

 

 

 

 

 

 

3. 기타 참고

VUE 에서  등로 및 삭제시 카운터 처리 방법

firebase.vue

//아이디가 존재할 경우
async function fbWriteSetDoc(collectionName,  id, objData) {

    //firebase 데이터 저장 - setDoc 일경우 아이디가 자동 생성
    await setDoc(doc(db, collectionName, id),  objData);

    // 전체등록 갯수 가져오기
    const docRef = doc(db, collectionName + "_totalCount", "tot_id");
    const totNum = await getDoc(docRef);

    //전체등록 갯수 업데이트   // totNum.exists() 존재할 경우 업데이트 처리 - 카운트 증가처리  :   없으면 1 로 최초등록
    await setDoc(doc(db, collectionName + "_totalCount", "tot_id"), {
        totalCount: totNum.exists() ? totNum.data().totalCount + 1 : 1,
        upDate: serverTimestamp()
    });

}


//firebase 삭제하기
async function fbDelete(collectionName, id) {
    await deleteDoc(doc(db, collectionName, id));

    // 전체등록 갯수 가져오기
    const docRef = doc(db, collectionName + "_totalCount", "tot_id");
    const totNum = await getDoc(docRef);

    //전체 갯수 업데이트   // totNum.docs[0] 값이 없을 경우 최초 등록으로 1 , 존재할 경우 업데이트 처리 - 카운트 감소
    await setDoc(doc(db, collectionName + "_totalCount", "tot_id"), {
        totalCount: totNum.data().totalCount > 0 ? totNum.data().totalCount - 1 : 0,
        upDate: serverTimestamp()
    });
}
import "firebase/auth";
import "firebase/firestore";
import Vue from 'vue'
import * as firebase from 'firebase/app';
import { initializeApp } from 'firebase/app';
import { query, orderBy, startAfter, limit, endBefore, limitToLast } from "firebase/firestore";
import { getFirestore, collection, getDocs, serverTimestamp, addDoc, doc, getDoc, updateDoc, deleteDoc, setDoc } from 'firebase/firestore';
import firebaseConfig from '../../firebaseConfig.js';
import { head } from 'lodash'
import store from "../store/index.js";



//구글 로그인 인증
import {
    signInWithPopup, GoogleAuthProvider, initializeAuth,
    browserSessionPersistence, browserPopupRedirectResolver, createUserWithEmailAndPassword,
    signInWithEmailAndPassword, onAuthStateChanged, signOut, updateProfile
    //getAuth
} from "firebase/auth";



const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

const auth = initializeAuth(app, {
    persistence: browserSessionPersistence,
    popupRedirectResolver: browserPopupRedirectResolver,
});
const provider = new GoogleAuthProvider();



//collectionName => table 이름
//1. 아이디 자동 생성 firebase 저장하기
async function fbWrite(collectionName, objData) {

    //firebase 데이터 저장 - addDoc 일경우 아이디가 자동 생성
    await addDoc(collection(getFirestore(), collectionName), objData);
    
    // 전체등록 갯수 가져오기
    const docRef = doc(db, collectionName + "_totalCount", "tot_id");
    const totNum = await getDoc(docRef);

    //전체등록 갯수 업데이트   // totNum.exists() 존재할 경우 업데이트 처리 - 카운트 증가처리  :   없으면 1 로 최초등록
    await setDoc(doc(db, collectionName + "_totalCount", "tot_id"), {
        totalCount: totNum.exists() ? totNum.data().totalCount + 1 :  1,
        upDate: serverTimestamp()
    });

}


//아이디가 존재할 경우
async function fbWriteSetDoc(collectionName,  id, objData) {

    //firebase 데이터 저장 - setDoc 일경우 아이디가 자동 생성
    await setDoc(doc(db, collectionName, id),  objData);

    // 전체등록 갯수 가져오기
    const docRef = doc(db, collectionName + "_totalCount", "tot_id");
    const totNum = await getDoc(docRef);

    //전체등록 갯수 업데이트   // totNum.exists() 존재할 경우 업데이트 처리 - 카운트 증가처리  :   없으면 1 로 최초등록
    await setDoc(doc(db, collectionName + "_totalCount", "tot_id"), {
        totalCount: totNum.exists() ? totNum.data().totalCount + 1 : 1,
        upDate: serverTimestamp()
    });

}


//firebase 삭제하기
async function fbDelete(collectionName, id) {
    await deleteDoc(doc(db, collectionName, id));

    // 전체등록 갯수 가져오기
    const docRef = doc(db, collectionName + "_totalCount", "tot_id");
    const totNum = await getDoc(docRef);

    //전체 갯수 업데이트   // totNum.docs[0] 값이 없을 경우 최초 등록으로 1 , 존재할 경우 업데이트 처리 - 카운트 감소
    await setDoc(doc(db, collectionName + "_totalCount", "tot_id"), {
        totalCount: totNum.data().totalCount > 0 ? totNum.data().totalCount - 1 : 0,
        upDate: serverTimestamp()
    });
}





//firebase 수정하기
async function fbUpdate(collectionName, objData) {
    const updateData = doc(db, collectionName, objData.id);
    await updateDoc(updateData, objData);
}



//firebase 한개의 데이터 가져오기
async function fbGetData(collectionName, id) {
    const docRef = doc(db, collectionName, id);
    return await getDoc(docRef);
}


// firebase 데이터 목록 가져오기
async function fbGetList(collectionName) {
    const querySnapshot = await getDocs(collection(db, collectionName));
    return querySnapshot;
}


// firebase query 데이터 목록 가져오기
async function fbGetQueryList(collectionName, culum, order, pageSize, documentSnapshots, currentPage, beforPage) {
    //1. 첫 페이지일 경우
    if (currentPage == undefined || currentPage == 1) {
        //1.첫페이지일 경우
        const first = query(collection(db, collectionName),
            orderBy(culum, order),
            limit(pageSize));

        const documentSnapshots2 = await getDocs(first);
        return documentSnapshots2;
    }


    if (beforPage <= currentPage) {

        //2.next 다음 페이지로 이동
        const lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1];

        if (documentSnapshots.size == 0) {
            return;
        }

        const next = query(collection(db, collectionName),
            orderBy(culum, order),
            startAfter(lastVisible),
            limit(pageSize));
        const documentSnapshots2 = await getDocs(next);

        return documentSnapshots2;


    } else {
        //3.이전 페이지로 이동

        const prevVisible = documentSnapshots.docs;

        const before = query(collection(db, collectionName),
            orderBy(culum, order),
            endBefore(head(prevVisible)),
            limitToLast(pageSize + 1)
        );
        const documentSnapshots2 = await getDocs(before);

        return documentSnapshots2;
    }
}




Vue.prototype.$isFirebaseAuth = false;

// onAuthStateChanged(auth, (user) => {
//     if(user){
//         Vue.prototype.$isFirebaseAuth = true;
//         router.push("/");
//     } else {
//         Vue.prototype.$isFirebaseAuth = false;
//         router.push("/sign");
//     }
//     store.dispatch('getUser', user)    
// });


onAuthStateChanged(auth, (user) => {     
    console.log("onAuthStateChanged : =>");
    store.dispatch('getUser', user); 
});




//전역변수로 사용 설정
Vue.prototype.$firebase = firebase


Vue.prototype.$fbWrite = fbWrite
Vue.prototype.$fbWriteSetDoc = fbWriteSetDoc

Vue.prototype.$fbUpdate = fbUpdate
Vue.prototype.$fbGetList = fbGetList
Vue.prototype.$fbGetQueryList = fbGetQueryList

Vue.prototype.$fbDelete = fbDelete
Vue.prototype.$fbGetData = fbGetData

Vue.prototype.$db = db
Vue.prototype.$getFirestore = getFirestore
Vue.prototype.$collection = collection
Vue.prototype.$getDocs = getDocs
Vue.prototype.$serverTimestamp = serverTimestamp
Vue.prototype.$addDoc = addDoc
Vue.prototype.$doc = doc
Vue.prototype.$getDoc = getDoc
Vue.prototype.$updateDoc = updateDoc
Vue.prototype.$deleteDoc = deleteDoc
Vue.prototype.$signOut = signOut
Vue.prototype.$updateProfile = updateProfile





Vue.prototype.$onAuthStateChanged = onAuthStateChanged

Vue.prototype.$signInWithPopup = signInWithPopup
Vue.prototype.$GoogleAuthProvider = GoogleAuthProvider
Vue.prototype.$auth = auth
Vue.prototype.$provider = provider
Vue.prototype.$createUserWithEmailAndPassword = createUserWithEmailAndPassword
Vue.prototype.$signInWithEmailAndPassword = signInWithEmailAndPassword

 

 

 

 

백엔드  nodejs 에서 함수로 전체 갯수 처리방법

 

const functions = require("firebase-functions");
const admin = require('firebase-admin');
const { getAuth } = require('firebase-admin/auth');
const serviceAccount = require("./serviceAccountKey.json");
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://vue-test-841ad.firebaseapp.com'
});
const db = admin.firestore();



//등록 처리시
exports.createUser = functions.auth.user().onCreate(async (user) => {
  let customClaims = {
    admin: false,
    accessLevel: 1
  };

  if (user.email && user.email.endsWith('braverokmc79@gmail.com') && user.emailVerified) {   
     //관리자일경우
      customClaims = {
        admin: true,
        accessLevel: 9
      };      
  }
  await getAuth().setCustomUserClaims(user.uid, customClaims);  
});

// 유저 삭제 함수
//const r = await db.collection('users').doc(user.uid).delete();
exports.deleteUser = functions.auth.user().onDelete( (user) => {  
  return db.collection('users').doc(user.email).delete();  
});


//유저정보 불러오기
exports.admin = functions.https.onRequest(require("./admin"));



//users 등록시 전체 갯수 증가처리 함수
exports.incrementUserCount = functions.firestore
  .document('users/{userId}')
  .onCreate((snap, context) => {
   return db.collection('infos').doc('users').update(
      'counter', admin.firestore.FieldValue.increment(1)
    )
});

//users 삭제시 전체 갯수 감소처리 함수
exports.decrementUserCount = functions.firestore
  .document('users/{userID}')
  .onDelete((snap, context) => {
  return db.collection('infos').doc('users').update(
      'counter', admin.firestore.FieldValue.increment(-1)
    )
});


// 백엔드 서버 실행 될때 infos collection (테이블) 이 생성 된다.
db.collection("infos").doc('users').get()
  .then(s => {
    if (!s.exists) db.collection('infos').doc('users').set({ counter: 0 })
});

 

 

 

백엔드 nodejs  미들웨어 참고

1)middlewares/verifyToken.js

const admin = require('firebase-admin');
const db = admin.firestore();
//const { getAuth } = require('firebase-admin/auth');
var jwt = require('jsonwebtoken');


module.exports = async (req, res, next) => {  

    try {       
        //const decodedToken = await getAuth().verifyIdToken(req.headers.authorization);
        // firebase verifyIdToken 오류로  jwt.decod 사용
        var decoded = jwt.decode(req.headers.authorization);        
        // 토큰 파싱 이메일 값 - decoded.email
        const users = await db.collection('users').doc(decoded.email).get();
        
        console.log("DB 이메일 존재 확인: " + decoded.email);
        if (users.data() === undefined) return res.status(403).send({ message: "not authorized" });
      
        console.log("DB 유저레벨 확인: " + users.data().accessLevel );
        if (decoded.accessLevel !== users.data().accessLevel) return res.status(405).send({ message: "not authorized" });

        req.claims = decoded;        

    } catch (error) {
        req.claims = error;
        console.error(error);
    }

    next();     
}

 

 

2)middlewares/ error.js

//에러 
module.exports =(err, req, res, next) => {
    if (err.message === 'access denied') {
        //res.status(403);
        //res.json({ error: err.message });
        console.log("에러2 ::");
        return res.send(" 에러 내용 :  " + err.message);
    }
    // res.json({ error: err.message });
    res.send(err.message);
    next(err);
};


 

 

 

 

 

 

 동영상강좌 참고

1) 페이징 처리

 

 

 

 

2) 정렬처리

 

 

 

3) 검색처리

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

무는 개를 돌아본다 , 무엇이든 나서서 보채야만 관심을 끌 수 있다는 말.

댓글 ( 4)

댓글 남기기

작성