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)
댓글 남기기