feat(admin-can): 캔 지급 화면에 닉네임 검색·다중 회원번호 입력 및 다중 지급 지원
This commit is contained in:
@@ -13,8 +13,8 @@ async function insertCan(can, rewardCan, price, currency) {
|
||||
return Vue.axios.post('/admin/can', request);
|
||||
}
|
||||
|
||||
async function paymentCan(can, method, member_id) {
|
||||
const request = {memberId: member_id, method: method, can: can}
|
||||
async function paymentCan(can, method, memberIds) {
|
||||
const request = {memberIds: memberIds, method: method, can: can}
|
||||
return Vue.axios.post('/admin/can/charge', request)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,32 @@ async function resetPassword(id) {
|
||||
return Vue.axios.post("/admin/member/password/reset", request)
|
||||
}
|
||||
|
||||
/**
|
||||
* 닉네임으로 회원 검색 API
|
||||
* - 서버 구현 차이를 흡수하기 위해 nickname, search_word 두 파라미터 모두 전송
|
||||
* - 응답은 다음 두 형태를 모두 허용하고 배열로 정규화하여 반환
|
||||
* 1) [{ id, nickname }, ...]
|
||||
* 2) { data: [{ id, nickname }, ...] }
|
||||
* @param {string} query
|
||||
* @returns {Promise<Array<{id:number,nickname:string}>>}
|
||||
*/
|
||||
async function searchMembersByNickname(query) {
|
||||
try {
|
||||
const res = await Vue.axios.get('/admin/member/search-by-nickname', {
|
||||
params: { search_word: query }
|
||||
})
|
||||
if (res && Array.isArray(res.data)) {
|
||||
return res.data
|
||||
}
|
||||
if (res && res.data && Array.isArray(res.data.data)) {
|
||||
return res.data.data
|
||||
}
|
||||
return []
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
login,
|
||||
getMemberList,
|
||||
@@ -60,5 +86,6 @@ export {
|
||||
searchCreator,
|
||||
updateMember,
|
||||
getCreatorAllList,
|
||||
resetPassword
|
||||
resetPassword,
|
||||
searchMembersByNickname
|
||||
}
|
||||
|
||||
@@ -8,11 +8,27 @@
|
||||
|
||||
<br>
|
||||
<v-container>
|
||||
<v-text-field
|
||||
v-model="account_id"
|
||||
label="회원번호"
|
||||
<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
|
||||
required
|
||||
@update:search-input="onSearch"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="manualInput"
|
||||
label="회원번호 직접 입력 (여러 개 입력 가능, 콤마/공백 구분)"
|
||||
outlined
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
@@ -34,7 +50,7 @@
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#9970ff"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="confirm"
|
||||
@@ -52,7 +68,7 @@
|
||||
<v-card>
|
||||
<v-card-title>캔 지급 확인</v-card-title>
|
||||
<v-card-text>
|
||||
회원번호: {{ account_id }}
|
||||
지급 대상: {{ confirmTargets.join(', ') }}
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
기록내용: {{ method }}
|
||||
@@ -88,6 +104,7 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/can'
|
||||
import { searchMembersByNickname } from '@/api/member'
|
||||
|
||||
export default {
|
||||
name: "CanCharge",
|
||||
@@ -96,12 +113,43 @@ export default {
|
||||
return {
|
||||
show_confirm: false,
|
||||
is_loading: false,
|
||||
account_id: '',
|
||||
// 기존 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)
|
||||
@@ -111,19 +159,81 @@ export default {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
confirm() {
|
||||
if (this.account_id.trim() === '' || isNaN(this.account_id)) {
|
||||
return this.notifyError('캔을 지급할 회원의 회원번호를 입력하세요.')
|
||||
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 (isNaN(this.can)) {
|
||||
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
|
||||
}
|
||||
@@ -140,10 +250,15 @@ export default {
|
||||
try {
|
||||
this.show_confirm = false
|
||||
|
||||
const res = await api.paymentCan(Number(this.can), this.method, this.account_id)
|
||||
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.account_id = ''
|
||||
// 상태 초기화
|
||||
this.selectedMembers = []
|
||||
this.searchItems = []
|
||||
this.searchQuery = ''
|
||||
this.manualInput = ''
|
||||
this.method = ''
|
||||
this.can = ''
|
||||
this.is_loading = false
|
||||
|
||||
Reference in New Issue
Block a user