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) 검색처리
댓글 ( 4)
댓글 남기기