563 lines
18 KiB
Vue
563 lines
18 KiB
Vue
<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>
|