feat(gift): 관리자 선물함 리스트 및 운영 액션 구현
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>선물함 리스트</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container fluid>
|
||||
<v-form @submit.prevent="search">
|
||||
<v-row align="center">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<v-select
|
||||
v-model="filters.status"
|
||||
:items="giftStatuses"
|
||||
:disabled="is_loading || is_operating"
|
||||
label="상태"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="filters.applicationNo"
|
||||
:disabled="is_loading || is_operating"
|
||||
label="신청번호"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="filters.nickname"
|
||||
:disabled="is_loading || is_operating"
|
||||
label="발송인 또는 수취인 닉네임"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="#3bb9f1"
|
||||
:disabled="is_loading || is_operating"
|
||||
dark
|
||||
>
|
||||
검색
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
:disabled="is_loading || is_operating"
|
||||
@click="reset"
|
||||
>
|
||||
초기화
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="mb-2">
|
||||
총 {{ totalCount }}건
|
||||
</div>
|
||||
<v-progress-linear
|
||||
v-if="is_loading"
|
||||
indeterminate
|
||||
/>
|
||||
<v-simple-table class="elevation-10">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="header in headers"
|
||||
:key="header"
|
||||
class="text-center"
|
||||
>
|
||||
{{ header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in gifts"
|
||||
:key="item.applicationNo"
|
||||
>
|
||||
<td class="text-center">
|
||||
{{ item.applicationNo }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.senderNickname }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.recipientNickname }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.sizeName }} / {{ item.sizeCode }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.categoryName }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.classificationNumber }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.courierCompanyName || '-' }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.trackingNumber || '-' }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.statusName }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-btn
|
||||
class="ma-1"
|
||||
small
|
||||
:disabled="is_operating || is_loading"
|
||||
@click="openDetail(item.applicationNo)"
|
||||
>
|
||||
상세
|
||||
</v-btn>
|
||||
<gift-actions
|
||||
:actions="item.availableActions"
|
||||
:disabled="is_operating || is_loading"
|
||||
@select="requestAction(item, $event)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!gifts.length">
|
||||
<td
|
||||
:colspan="headers.length"
|
||||
class="text-center"
|
||||
>
|
||||
{{ is_loading ? '조회 중입니다.' : list_error ? '목록을 불러오지 못했습니다. 다시 검색해 주세요.' : '조회된 선물 신청이 없습니다.' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
:value="page"
|
||||
:length="total_page"
|
||||
:disabled="is_loading || is_operating"
|
||||
:total-visible="10"
|
||||
circle
|
||||
@input="changePage"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_detail_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>선물 상세</v-card-title>
|
||||
<v-progress-linear
|
||||
v-if="is_detail_loading"
|
||||
indeterminate
|
||||
/>
|
||||
<v-card-text>
|
||||
<template v-if="detail">
|
||||
<div class="mb-4">
|
||||
신청번호: {{ detail.applicationNo }} / 현재 상태: {{ detail.statusName }}
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="member in memberSections"
|
||||
:key="member.key"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<h3 class="mb-2">
|
||||
{{ member.title }}
|
||||
</h3>
|
||||
<v-simple-table>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="field in memberFields"
|
||||
:key="field.key"
|
||||
>
|
||||
<th>{{ field.label }}</th>
|
||||
<td>{{ detail[member.key][field.key] || (member.key === 'recipientInfo' && field.key !== 'nickname' ? '미입력' : '-') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<h3 class="mb-2">
|
||||
상품 정보
|
||||
</h3>
|
||||
<v-simple-table>
|
||||
<tbody>
|
||||
<tr><th>사이즈</th><td>{{ detail.productInfo.sizeName }} / {{ detail.productInfo.sizeCode }}</td></tr>
|
||||
<tr><th>카테고리</th><td>{{ detail.productInfo.categoryName }}</td></tr>
|
||||
<tr><th>분류번호</th><td>{{ detail.productInfo.classificationNumber }}</td></tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<h3 class="mb-2">
|
||||
입고 배송 정보
|
||||
</h3>
|
||||
<v-simple-table>
|
||||
<tbody>
|
||||
<tr><th>택배사</th><td>{{ detail.inboundDeliveryInfo.courierCompanyName || '-' }}</td></tr>
|
||||
<tr><th>운송장번호</th><td>{{ detail.inboundDeliveryInfo.trackingNumber || '-' }}</td></tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="mt-4">
|
||||
<h3 class="mb-2">
|
||||
가능한 액션
|
||||
</h3>
|
||||
<gift-actions
|
||||
:actions="detail.availableActions"
|
||||
:disabled="is_operating || is_detail_loading || is_loading"
|
||||
@select="requestAction(detail, $event)"
|
||||
/>
|
||||
<span v-if="!detail.availableActions.length">현재 가능한 액션이 없습니다.</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!is_detail_loading">
|
||||
상세 정보를 불러오지 못했습니다.
|
||||
<v-btn
|
||||
text
|
||||
color="primary"
|
||||
@click="getDetail(detailApplicationNo)"
|
||||
>
|
||||
다시 시도
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
:disabled="is_operating"
|
||||
@click="closeDetail"
|
||||
>
|
||||
닫기
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_reason_dialog"
|
||||
max-width="500px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>전달 불가 처리</v-card-title>
|
||||
<v-card-text>
|
||||
<p>신청번호: {{ pendingAction ? pendingAction.applicationNo : '' }}</p>
|
||||
<v-form @submit.prevent="confirmReason">
|
||||
<v-textarea
|
||||
v-model="reason"
|
||||
label="전달 불가 사유"
|
||||
:rules="[validateUndeliverableReason]"
|
||||
:error-messages="reason_error"
|
||||
:disabled="is_operating"
|
||||
counter="255"
|
||||
maxlength="255"
|
||||
auto-grow
|
||||
rows="3"
|
||||
@input="reason_error = ''"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
:disabled="is_operating"
|
||||
@click="cancelAction"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
:disabled="is_operating"
|
||||
@click="confirmReason"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>상태 변경 확인</v-card-title>
|
||||
<v-card-text v-if="pendingAction">
|
||||
<p>신청번호: {{ pendingAction.applicationNo }}</p>
|
||||
<p>{{ pendingAction.label }}하시겠습니까?</p>
|
||||
<p
|
||||
v-if="pendingAction.value === 'MARK_UNDELIVERABLE'"
|
||||
class="gift-reason"
|
||||
>
|
||||
사유: {{ reason.trim() }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
:disabled="is_operating"
|
||||
@click="cancelConfirmation"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
:loading="is_operating"
|
||||
:disabled="is_operating"
|
||||
@click="executeAction"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/gift'
|
||||
import { giftActions, giftStatuses, validateUndeliverableReason } from '@/utils/gift'
|
||||
import GiftActions from './GiftActions.vue'
|
||||
|
||||
export default {
|
||||
name: 'GiftList',
|
||||
components: { GiftActions },
|
||||
data() {
|
||||
return {
|
||||
giftStatuses,
|
||||
headers: ['신청번호', '발송인 닉네임', '수취인 닉네임', '사이즈', '카테고리', '분류번호', '택배사', '운송장번호', '상태', '액션'],
|
||||
memberSections: [{ key: 'senderInfo', title: '발송인 정보' }, { key: 'recipientInfo', title: '수취인 정보' }],
|
||||
memberFields: [{ key: 'nickname', label: '닉네임' }, { key: 'name', label: '이름' }, { key: 'phoneNumber', label: '전화번호' }, { key: 'address', label: '주소' }],
|
||||
filters: { status: '', applicationNo: '', nickname: '' },
|
||||
appliedFilters: { status: '', applicationNo: '', nickname: '' },
|
||||
gifts: [],
|
||||
page: 1,
|
||||
size: 20,
|
||||
totalCount: 0,
|
||||
hasNext: false,
|
||||
is_loading: false,
|
||||
list_error: false,
|
||||
listRequestId: 0,
|
||||
detail: null,
|
||||
detailApplicationNo: '',
|
||||
detailRequestId: 0,
|
||||
is_detail_loading: false,
|
||||
show_detail_dialog: false,
|
||||
is_operating: false,
|
||||
pendingAction: null,
|
||||
show_reason_dialog: false,
|
||||
show_confirm_dialog: false,
|
||||
reason: '',
|
||||
reason_error: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
total_page() {
|
||||
const count = Math.max(1, Math.ceil(this.totalCount / this.size))
|
||||
return this.hasNext ? Math.max(count, this.page + 1) : Math.max(1, this.page)
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.getGifts()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.listRequestId++
|
||||
this.detailRequestId++
|
||||
},
|
||||
methods: {
|
||||
validateUndeliverableReason,
|
||||
notifyError(error) {
|
||||
const response = error.response && error.response.data
|
||||
this.$dialog.notify.error((response && response.message) || error.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
},
|
||||
checkResponse(response) {
|
||||
if (response.success !== true) throw new Error(response.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
},
|
||||
async getGifts(page = this.page) {
|
||||
const requestId = ++this.listRequestId
|
||||
this.is_loading = true
|
||||
this.list_error = false
|
||||
try {
|
||||
const response = await api.getGifts({ ...this.appliedFilters, page: page - 1, size: this.size })
|
||||
if (requestId !== this.listRequestId) return
|
||||
this.checkResponse(response)
|
||||
this.gifts = response.data.items
|
||||
this.page = response.data.page + 1
|
||||
this.size = response.data.size
|
||||
this.totalCount = response.data.totalCount
|
||||
this.hasNext = response.data.hasNext
|
||||
} catch (error) {
|
||||
if (requestId !== this.listRequestId) return
|
||||
this.gifts = []
|
||||
this.totalCount = 0
|
||||
this.hasNext = false
|
||||
this.list_error = true
|
||||
this.notifyError(error)
|
||||
} finally {
|
||||
if (requestId === this.listRequestId) this.is_loading = false
|
||||
}
|
||||
},
|
||||
async search() {
|
||||
if (this.is_loading || this.is_operating) return
|
||||
this.appliedFilters = {
|
||||
status: this.filters.status || '',
|
||||
applicationNo: (this.filters.applicationNo || '').trim(),
|
||||
nickname: (this.filters.nickname || '').trim()
|
||||
}
|
||||
await this.getGifts(1)
|
||||
},
|
||||
async reset() {
|
||||
if (this.is_loading || this.is_operating) return
|
||||
this.filters = { status: '', applicationNo: '', nickname: '' }
|
||||
await this.search()
|
||||
},
|
||||
async changePage(page) {
|
||||
if (this.is_loading || this.is_operating) return
|
||||
await this.getGifts(page)
|
||||
},
|
||||
async openDetail(applicationNo) {
|
||||
if (this.is_operating) return
|
||||
this.detail = null
|
||||
this.show_detail_dialog = true
|
||||
await this.getDetail(applicationNo)
|
||||
},
|
||||
async getDetail(applicationNo) {
|
||||
const requestId = ++this.detailRequestId
|
||||
this.detailApplicationNo = applicationNo
|
||||
this.is_detail_loading = true
|
||||
try {
|
||||
const response = await api.getGift(applicationNo)
|
||||
if (requestId !== this.detailRequestId) return
|
||||
this.checkResponse(response)
|
||||
this.detail = response.data
|
||||
} catch (error) {
|
||||
if (requestId !== this.detailRequestId) return
|
||||
this.detail = null
|
||||
this.notifyError(error)
|
||||
} finally {
|
||||
if (requestId === this.detailRequestId) this.is_detail_loading = false
|
||||
}
|
||||
},
|
||||
closeDetail() {
|
||||
if (this.is_operating) return
|
||||
this.detailRequestId++
|
||||
this.show_detail_dialog = false
|
||||
this.is_detail_loading = false
|
||||
this.detail = null
|
||||
this.detailApplicationNo = ''
|
||||
},
|
||||
requestAction(item, value) {
|
||||
if (this.is_operating || this.is_loading || this.is_detail_loading || this.pendingAction) return
|
||||
const action = giftActions.find(action => action.value === value)
|
||||
if (!action || !item.availableActions.includes(value)) return
|
||||
this.pendingAction = { ...action, applicationNo: item.applicationNo }
|
||||
this.reason = ''
|
||||
this.reason_error = ''
|
||||
if (value === 'MARK_UNDELIVERABLE') {
|
||||
this.show_reason_dialog = true
|
||||
} else {
|
||||
this.show_confirm_dialog = true
|
||||
}
|
||||
},
|
||||
confirmReason() {
|
||||
if (this.is_operating || !this.pendingAction || this.pendingAction.value !== 'MARK_UNDELIVERABLE') return
|
||||
const validation = validateUndeliverableReason(this.reason)
|
||||
this.reason_error = validation === true ? '' : validation
|
||||
if (validation === true) this.show_confirm_dialog = true
|
||||
},
|
||||
cancelConfirmation() {
|
||||
if (this.is_operating) return
|
||||
if (this.pendingAction && this.pendingAction.value === 'MARK_UNDELIVERABLE') {
|
||||
this.show_confirm_dialog = false
|
||||
} else {
|
||||
this.cancelAction()
|
||||
}
|
||||
},
|
||||
cancelAction() {
|
||||
if (this.is_operating) return
|
||||
this.show_confirm_dialog = false
|
||||
this.show_reason_dialog = false
|
||||
this.pendingAction = null
|
||||
this.reason = ''
|
||||
this.reason_error = ''
|
||||
},
|
||||
async executeAction() {
|
||||
if (this.is_operating || !this.pendingAction || !this.show_confirm_dialog) return
|
||||
const action = this.pendingAction
|
||||
if (action.value === 'MARK_UNDELIVERABLE' && validateUndeliverableReason(this.reason) !== true) {
|
||||
this.show_confirm_dialog = false
|
||||
this.confirmReason()
|
||||
return
|
||||
}
|
||||
this.is_operating = true
|
||||
try {
|
||||
const response = action.value === 'MARK_UNDELIVERABLE'
|
||||
? await api.markUndeliverable(action.applicationNo, this.reason.trim())
|
||||
: await api[action.method](action.applicationNo)
|
||||
this.checkResponse(response)
|
||||
this.show_confirm_dialog = false
|
||||
this.show_reason_dialog = false
|
||||
this.pendingAction = null
|
||||
this.reason = ''
|
||||
this.$dialog.notify.success(`${action.label}되었습니다.`)
|
||||
const refreshes = [this.getGifts()]
|
||||
if (this.show_detail_dialog) refreshes.push(this.getDetail(this.detail ? this.detail.applicationNo : this.detailApplicationNo))
|
||||
await Promise.all(refreshes)
|
||||
} catch (error) {
|
||||
this.notifyError(error)
|
||||
} finally {
|
||||
this.is_operating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gift-reason {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user