오늘의 목표
게시판의 페이징 처리가 되도록 게시글의 total count를 관리하고 페이징 개수별로 처리되도록 해보자.
#1. total count 관리를 위한 functions 만들기 (increment / decrement)
#2. 페이징 처리를 위한 v-data-table 옵션 셋팅
#3. head, last 이용해서 첫번째, 마지막 게시물 정보 가져오기
#4. startAfter, endBefore 이용해서 전/후 페이지 이동하기
#1. total count 관리를 위한 functions 만들기 (increment / decrement)
- 페이징 처리를 하기 위해선 게시글의 총 개수 (total count)가 별도로 관리돼야 한다.
이를 위해 게시글이 생성되거나 삭제될 때 총 개수를 제어하는 functions를 아래와 같이 만들고 배포하자.
const fdb = admin.firestore()
...
// 게시글이 생성될 때마다 이곳을 거치게 됨
exports.incrementBoardCount = functions.firestore.document('boards/{bid}').onCreate(async (snap, context) => {
try {
await fdb.collection('meta').doc('boards').update('count', admin.firestore.FieldValue.increment(1))
} catch (e) {
await fdb.collection('meta').doc('boards').set({ count: 1 })
}
})
// 게시글이 생성될 때마다 이곳을 거치게 됨
exports.decrementBoardCount = functions.firestore.document('boards/{bid}').onDelete(async (snap, context) => {
await fdb.collection('meta').doc('boards').update('count', admin.firestore.FieldValue.increment(-1))
})
#2. 페이징 처리를 위한 v-data-table 옵션 셋팅
- 아래의 2가지 정도만 있으면 된다. 실질적으론 server-items-length만 있으면 됨
- serverItemsLength 변수를 만들어주자. 해당 정보는 게시글의 총 개수를 가지고 있다. (this.serverItemsLength = doc.data().count)
- options : 처음에는 비어져 있다. table이 처음 불려질 때 값들이 알아서 채워지는데 이 값들의 변경을 담당하는 것이 watch이다.
아래처럼 쓰여지는데, options의 handler에서 감지를 해준다.
- 우리는 게시판의 변경을 감시해야 하므로 handler에 this.subscribe()를 넣어서 감시해주자.
watch: {
options: {
handler (n, o) {
감시할 것에 대해 적어주자.
},
deep: true
},
},
<template>
<v-card>
<v-card-title>board</v-card-title>
<v-data-table
:headers="headers"
:items="items"
:options.sync="options"
:server-items-length="serverItemsLength"
:items-per-page="5"
>
<template v-slot:item.id="{ item }">
<v-btn icon @click="openDialog(item)"><v-icon>mdi-pencil</v-icon></v-btn>
<v-btn icon @click="remove(item)"><v-icon>mdi-delete</v-icon></v-btn>
</template>
</v-data-table>
<v-card-actions>
<v-btn @click="openDialog(null)"><v-icon left>mdi-pencil</v-icon></v-btn>
</v-card-actions>
<v-dialog max-width="500" v-model="dialog">
<v-card>
<v-form>
<v-card-text>
<v-text-field v-model="form.title"></v-text-field>
<v-text-field v-model="form.content"></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn @click="update" v-if="selectedItem">save</v-btn>
<v-btn @click="add" v-else>save</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-card>
</template>
<script>
export default {
data () {
return {
// 게시판 headers 정의해주기
headers: [
{ value: 'title', text: '제목' },
{ value: 'content', text: '내용' },
{ value: 'id', text: 'id' }
],
items: [],
form: {
title: '',
content: ''
},
dialog: false,
selectedItem: null,
unsubscribe: null, // 실시간 stream을 받기 위한 변수 (listen for realtime update)
unsubscribeCount: null,
serverItemsLength: 0,
options: {}
}
},
watch: {
options: {
handler (n, o) { // n: new, o: old
this.subscribe()
},
deep: true
}
},
destroyed () {
if (this.unsubscribe) this.unsubscribe() // 다른 페이지 넘어갈 때 구독 해지
if (this.unsubscribeCount) this.unsubscribeCount()
},
methods: {
subscribe () {
this.unsubscribeCount = this.$firebase.firestore().collection('meta').doc('boards').onSnapshot((doc) => {
if (!doc.exists) return
this.serverItemsLength = doc.data().count
})
this.unsubscribe = this.$firebase.firestore().collection('boards').limit(this.options.itemsPerPage).onSnapshot((sn) => {
if (sn.empty) {
this.items = []
return
}
this.items = sn.docs.map(v => {
const item = v.data()
return {
id: v.id,
title: item.title,
content: item.content
}
})
})
},
openDialog (item) {
this.selectedItem = item
this.dialog = true
if (!item) { // 신규 글쓰기인 경우
this.form.title = ''
this.form.content = ''
} else { // 수정 버튼을 클릭한 경우
this.form.title = item.title
this.form.content = item.content
}
},
add () {
this.$firebase.firestore().collection('boards').add(this.form)
this.dialog = false
},
update () {
this.$firebase.firestore().collection('boards').doc(this.selectedItem.id).update(this.form)
this.dialog = false
},
remove (item) {
this.$firebase.firestore().collection('boards').doc(item.id).delete()
this.dialog = false
}
}
}
</script>
#3. head, last 이용해서 첫번째, 마지막 게시물 정보 가져오기
- 사용을 위해 import 하자 (import { head, last } from 'lodash')
- subscribe() 에서 sn.docs를 this.docs에 저장해보자. (docs = [] 로 data에 추가로 선언할 것)
- head, last를 이용해서 배열의 첫번째/마지막째 값을 가져다 쓸 수 있다. (다음 #4에서 사용할 일이 있을 것이다.)
#4. startAfter, endBefore 이용해서 전/후 페이지 이동하기
- arrow 라는 변수를 통해 전/후페이지 이동에 대한 파악을 하자. (n.page - o.page 했을 때 -1이면 전페이지, 1이면 후페이지 등)
watch: {
options: {
handler (n, o) { // n: new, o: old
const arrow = n.page - o.page // arrow를 통해 전페이지 이동인지 후페이지 이동인지 파악
this.subscribe(arrow)
},
deep: true
}
},
- subscribe()에 order, sort, limit, ref, query값 추가 (전/후페이지 이동에 따라 query값을 변동시켜주며 그때그때 값을 불러옴)
※ endBefore일 때는 limitToLast, startAfter일 때는 limit으로 사용해줄 것
subscribe (arrow) {
this.unsubscribeCount = this.$firebase.firestore().collection('meta').doc('boards').onSnapshot((doc) => {
if (!doc.exists) return
this.serverItemsLength = doc.data().count
})
const order = head(this.options.sortBy)
const sort = head(this.options.sortDesc) ? 'desc' : 'asc'
const limit = this.options.itemsPerPage
const ref = this.$firebase.firestore().collection('boards').orderBy(order, sort)
let query
switch (arrow) {
case -1: query = ref.endBefore(head(this.docs)).limit(limit) // 전페이지 이동
break
case 1: query = ref.startAfter(last(this.docs)).limit(limit) // 후페이지 이동
break
default: query = ref.limit(limit)
break
}
this.unsubscribe = query.onSnapshot((sn) => {
if (sn.empty) {
this.items = []
return
}
this.items = sn.docs.map(v => {
const item = v.data()
return {
id: v.id,
title: item.title,
content: item.content,
createdAt: item.createdAt.toDate()
}
})
})
},
전체 소스
<template>
<v-card>
<v-card-title>board</v-card-title>
<v-data-table
:headers="headers"
:items="items"
:options.sync="options"
:server-items-length="serverItemsLength"
:items-per-page="5"
must-sort
>
<template v-slot:item.id="{ item }">
<v-btn icon @click="openDialog(item)"><v-icon>mdi-pencil</v-icon></v-btn>
<v-btn icon @click="remove(item)"><v-icon>mdi-delete</v-icon></v-btn>
</template>
<template v-slot:item.createdAt="{ item }">
{{item.createdAt.toLocaleString()}}
</template>
</v-data-table>
<v-card-actions>
<v-btn @click="openDialog(null)"><v-icon left>mdi-pencil</v-icon></v-btn>
</v-card-actions>
<v-dialog max-width="500" v-model="dialog">
<v-card>
<v-form>
<v-card-text>
<v-text-field v-model="form.title"></v-text-field>
<v-text-field v-model="form.content"></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn @click="update" v-if="selectedItem">save</v-btn>
<v-btn @click="add" v-else>save</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-card>
</template>
<script>
import { head, last } from 'lodash'
export default {
data () {
return {
// 게시판 headers 정의해주기
headers: [
{ value: 'createdAt', text: '작성일' },
{ value: 'title', text: '제목' },
{ value: 'content', text: '내용' },
{ value: 'id', text: '', sortable: false } // sort 불가능 옵션 추가
],
items: [],
form: {
title: '',
content: ''
},
dialog: false,
selectedItem: null,
unsubscribe: null, // 실시간 stream을 받기 위한 변수 (listen for realtime update)
unsubscribeCount: null,
serverItemsLength: 0,
options: { // 작성일 기준 내림차순 정렬 옵션
sortBy: ['createdAt'],
sortDesc: [true]
},
docs: []
}
},
watch: {
options: {
handler (n, o) { // n: new, o: old
const arrow = n.page - o.page // arrow를 통해 전페이지 이동인지 후페이지 이동인지 파악
this.subscribe(arrow)
},
deep: true
}
},
destroyed () {
if (this.unsubscribe) this.unsubscribe() // 다른 페이지 넘어갈 때 구독 해지
if (this.unsubscribeCount) this.unsubscribeCount()
},
methods: {
subscribe (arrow) {
this.unsubscribeCount = this.$firebase.firestore().collection('meta').doc('boards').onSnapshot((doc) => {
if (!doc.exists) return
this.serverItemsLength = doc.data().count
})
const order = head(this.options.sortBy)
const sort = head(this.options.sortDesc) ? 'desc' : 'asc'
const limit = this.options.itemsPerPage
const ref = this.$firebase.firestore().collection('boards').orderBy(order, sort)
let query
switch (arrow) {
case -1: query = ref.endBefore(head(this.docs)).limitToLast(limit) // 전페이지 이동
break
case 1: query = ref.startAfter(last(this.docs)).limit(limit) // 후페이지 이동
break
default: query = ref.limit(limit)
break
}
this.unsubscribe = query.onSnapshot((sn) => {
if (sn.empty) {
this.items = []
return
}
this.docs = sn.docs
this.items = sn.docs.map(v => {
const item = v.data()
return {
id: v.id,
title: item.title,
content: item.content,
createdAt: item.createdAt.toDate()
}
})
})
},
openDialog (item) {
this.selectedItem = item
this.dialog = true
if (!item) { // 신규 글쓰기인 경우
this.form.title = ''
this.form.content = ''
} else { // 수정 버튼을 클릭한 경우
this.form.title = item.title
this.form.content = item.content
}
},
add () {
const item = {}
// item.content = this.form.content; item.title = this.form.title; 과 같은 의미
Object.assign(item, this.form)
item.createdAt = new Date()
this.$firebase.firestore().collection('boards').add(item)
this.dialog = false
},
update () {
this.$firebase.firestore().collection('boards').doc(this.selectedItem.id).update(this.form)
this.dialog = false
},
remove (item) {
this.$firebase.firestore().collection('boards').doc(item.id).delete()
this.dialog = false
}
}
}
</script>
※ 해당 내용은 아래의 사이트에서 강의를 보며 실습한 내용입니다.
댓글