284 lines
7.4 KiB
Vue
284 lines
7.4 KiB
Vue
<template>
|
|
<div>
|
|
<v-toolbar dark>
|
|
<v-spacer />
|
|
<v-toolbar-title>캔 충전</v-toolbar-title>
|
|
<v-spacer />
|
|
</v-toolbar>
|
|
|
|
<br>
|
|
<v-container>
|
|
<v-autocomplete
|
|
v-model="selectedMembers"
|
|
:items="searchItems"
|
|
:loading="searchLoading"
|
|
:search-input.sync="searchQuery"
|
|
label="닉네임으로 사용자 검색 (여러 명 선택 가능)"
|
|
item-text="nickname"
|
|
item-value="id"
|
|
return-object
|
|
multiple
|
|
small-chips
|
|
clearable
|
|
outlined
|
|
@update:search-input="onSearch"
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="manualInput"
|
|
label="회원번호 직접 입력 (여러 개 입력 가능, 콤마/공백 구분)"
|
|
outlined
|
|
clearable
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="method"
|
|
label="기록 내용"
|
|
outlined
|
|
required
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="can"
|
|
label="지급할 캔 수"
|
|
outlined
|
|
required
|
|
/>
|
|
|
|
<v-row>
|
|
<v-col cols="10" />
|
|
<v-col>
|
|
<v-btn
|
|
block
|
|
color="#3bb9f1"
|
|
dark
|
|
depressed
|
|
@click="confirm"
|
|
>
|
|
캔 지급
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row>
|
|
<v-dialog
|
|
v-model="show_confirm"
|
|
max-width="400px"
|
|
persistent
|
|
>
|
|
<v-card>
|
|
<v-card-title>캔 지급 확인</v-card-title>
|
|
<v-card-text>
|
|
지급 대상: {{ confirmTargets.join(', ') }}
|
|
</v-card-text>
|
|
<v-card-text>
|
|
기록내용: {{ method }}
|
|
</v-card-text>
|
|
<v-card-text>
|
|
지급할 캔 수: {{ can }} 캔
|
|
</v-card-text>
|
|
<v-card-actions v-show="!is_loading">
|
|
<v-spacer />
|
|
<v-btn
|
|
color="blue darken-1"
|
|
text
|
|
@click="submit"
|
|
>
|
|
캔 지급
|
|
</v-btn>
|
|
<v-spacer />
|
|
<v-btn
|
|
color="blue darken-1"
|
|
text
|
|
@click="cancel"
|
|
>
|
|
취소
|
|
</v-btn>
|
|
<v-spacer />
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-row>
|
|
</v-container>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import * as api from '@/api/can'
|
|
import { searchMembersByNickname } from '@/api/member'
|
|
|
|
export default {
|
|
name: "CanCharge",
|
|
|
|
data() {
|
|
return {
|
|
show_confirm: false,
|
|
is_loading: false,
|
|
// 기존 account_id -> member_id로 명칭 변경 및 다중 입력 구조로 변경
|
|
selectedMembers: [], // 검색으로 선택된 사용자 {id, nickname} 객체 배열
|
|
searchItems: [],
|
|
searchLoading: false,
|
|
searchQuery: '',
|
|
searchDebounceTimer: null,
|
|
lastSearchToken: 0,
|
|
manualInput: '', // 수동 입력: 회원번호 여러 개 (콤마/공백 구분)
|
|
method: '',
|
|
can: ''
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
// 확인 다이얼로그에 표시할 대상 이름들
|
|
confirmTargets() {
|
|
const names = []
|
|
// 검색으로 선택된 사용자 닉네임
|
|
if (this.selectedMembers && this.selectedMembers.length > 0) {
|
|
names.push(...this.selectedMembers.map(m => m.nickname))
|
|
}
|
|
// 수동 입력 회원번호는 번호 그대로 표기
|
|
const manualIds = this.parseManualIds()
|
|
if (manualIds.length > 0) {
|
|
names.push(...manualIds.map(String))
|
|
}
|
|
return names
|
|
}
|
|
},
|
|
|
|
beforeDestroy() {
|
|
if (this.searchDebounceTimer) {
|
|
clearTimeout(this.searchDebounceTimer)
|
|
this.searchDebounceTimer = null
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
notifyError(message) {
|
|
this.$dialog.notify.error(message)
|
|
},
|
|
|
|
notifySuccess(message) {
|
|
this.$dialog.notify.success(message)
|
|
},
|
|
|
|
onSearch(val) {
|
|
this.searchQuery = val
|
|
|
|
// 입력이 없으면 즉시 초기화하고 이전 타이머/로딩을 정리
|
|
if (!val || val.trim().length === 0) {
|
|
if (this.searchDebounceTimer) {
|
|
clearTimeout(this.searchDebounceTimer)
|
|
this.searchDebounceTimer = null
|
|
}
|
|
this.searchLoading = false
|
|
this.searchItems = []
|
|
return
|
|
}
|
|
|
|
// 디바운스: 입력 멈춘 뒤에만 호출
|
|
if (this.searchDebounceTimer) {
|
|
clearTimeout(this.searchDebounceTimer)
|
|
this.searchDebounceTimer = null
|
|
}
|
|
|
|
this.searchDebounceTimer = setTimeout(async () => {
|
|
if (val.trim().length >= 2) {
|
|
const token = ++this.lastSearchToken
|
|
this.searchLoading = true
|
|
try {
|
|
const items = await searchMembersByNickname(val)
|
|
// 가장 최근 쿼리에 대한 응답만 반영
|
|
if (token === this.lastSearchToken) {
|
|
this.searchItems = items
|
|
}
|
|
} catch (e) {
|
|
if (token === this.lastSearchToken) {
|
|
this.searchItems = []
|
|
}
|
|
} finally {
|
|
if (token === this.lastSearchToken) {
|
|
this.searchLoading = false
|
|
}
|
|
}
|
|
}
|
|
}, 300)
|
|
},
|
|
|
|
parseManualIds() {
|
|
if (!this.manualInput) return []
|
|
return this.manualInput
|
|
.split(/[\s,]+/)
|
|
.map(s => s.trim())
|
|
.filter(s => s.length > 0 && /^\d+$/.test(s))
|
|
.map(s => Number(s))
|
|
},
|
|
|
|
buildMemberIds() {
|
|
const idsFromSearch = (this.selectedMembers || []).map(m => Number(m.id)).filter(id => !isNaN(id))
|
|
const idsFromManual = this.parseManualIds()
|
|
// 중복 제거
|
|
const set = new Set([...idsFromSearch, ...idsFromManual])
|
|
return Array.from(set)
|
|
},
|
|
|
|
confirm() {
|
|
// 유효성 검증
|
|
if (this.method.trim() === '') {
|
|
return this.notifyError('기록할 내용을 입력하세요')
|
|
}
|
|
|
|
if (this.can === '' || isNaN(this.can)) {
|
|
return this.notifyError('캔은 숫자만 넣을 수 있습니다.')
|
|
}
|
|
|
|
const memberIds = this.buildMemberIds()
|
|
if (memberIds.length === 0) {
|
|
return this.notifyError('캔을 지급할 대상을 추가하세요. (닉네임 검색 선택 또는 회원번호 입력)')
|
|
}
|
|
|
|
if (!this.is_loading) {
|
|
this.show_confirm = true
|
|
}
|
|
},
|
|
|
|
cancel() {
|
|
this.show_confirm = false
|
|
},
|
|
|
|
async submit() {
|
|
if (!this.is_loading) {
|
|
this.is_loading = true
|
|
|
|
try {
|
|
this.show_confirm = false
|
|
|
|
const memberIds = this.buildMemberIds()
|
|
const res = await api.paymentCan(Number(this.can), this.method, memberIds)
|
|
if (res.status === 200 && res.data.success === true) {
|
|
this.notifySuccess('캔이 지급되었습니다.')
|
|
// 상태 초기화
|
|
this.selectedMembers = []
|
|
this.searchItems = []
|
|
this.searchQuery = ''
|
|
this.manualInput = ''
|
|
this.method = ''
|
|
this.can = ''
|
|
this.is_loading = false
|
|
} else {
|
|
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
|
this.is_loading = false
|
|
}
|
|
} catch (e) {
|
|
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
|
this.is_loading = false
|
|
} finally {
|
|
this.is_loading = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
|
|
</style>
|