feat(agent): 에이전트 정산 비율 페이지 작성
- 에이전트 정산 비율 API 추가 (목록/등록/수정/닉네임 검색) - AgentSettlementRatio.vue 구현 — 목록 테이블, "에이전트 비율 추가" 버튼, 수정 버튼 및 등록/수정 공용 팝업 추가 - UX: 닉네임 검색(v-autocomplete), 숫자/범위(0~100) 검증, datetime-local 입력값 LocalDateTime 문자열 변환 처리 - 에러/로딩 상태 기본 처리 및 목록 새로고침 흐름 반영
This commit is contained in:
@@ -7,6 +7,42 @@ async function getAgentList() {
|
|||||||
return Vue.axios.get('/admin/partner/agent/list')
|
return Vue.axios.get('/admin/partner/agent/list')
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
// 에이전트 정산 비율 목록 조회
|
||||||
getAgentList
|
async function getAgentSettlementRatioList() {
|
||||||
|
return Vue.axios.get('/admin/partner/agent/ratio')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 에이전트 정산 비율 등록
|
||||||
|
// payload: { memberId: number, settlementRatio: number, effectiveFrom: string(yyyy-MM-ddTHH:mm:ss) }
|
||||||
|
async function createAgentSettlementRatio(payload) {
|
||||||
|
return Vue.axios.post('/admin/partner/agent/ratio', payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 에이전트 정산 비율 수정
|
||||||
|
// payload: { memberId: number, settlementRatio: number, effectiveFrom: string(yyyy-MM-ddTHH:mm:ss) }
|
||||||
|
async function updateAgentSettlementRatio(payload) {
|
||||||
|
return Vue.axios.post('/admin/partner/agent/ratio/update', payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 에이전트 닉네임 검색
|
||||||
|
// 반환: [{ id, nickname }]
|
||||||
|
async function searchAgentByNickname(query) {
|
||||||
|
try {
|
||||||
|
const res = await Vue.axios.get('/admin/partner/agent/search-by-nickname', {
|
||||||
|
params: { nickname: query, 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 {
|
||||||
|
getAgentList,
|
||||||
|
getAgentSettlementRatioList,
|
||||||
|
createAgentSettlementRatio,
|
||||||
|
updateAgentSettlementRatio,
|
||||||
|
searchAgentByNickname,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,339 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid>
|
<div>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>에이전트 정산 비율</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
dark
|
||||||
|
@click="onClickCreate"
|
||||||
|
>
|
||||||
|
에이전트 비율 추가
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<v-container>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<v-col
|
||||||
<h2>에이전트 정산 비율</h2>
|
cols="12"
|
||||||
<p>에이전트 정산 비율을 설정/관리합니다. (구현 예정)</p>
|
class="text-right"
|
||||||
|
>
|
||||||
|
총 에이전트 수: <strong>{{ totalCount | numberFormat }}</strong>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-simple-table class="elevation-10">
|
||||||
|
<template>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">
|
||||||
|
에이전트 닉네임
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
정산 비율(%)
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
관리
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.memberId"
|
||||||
|
>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.nickname }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ (item.current && item.current.settlementRatio != null) ? item.current.settlementRatio : '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
outlined
|
||||||
|
color="primary"
|
||||||
|
@click="onClickEdit(item)"
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!loading && (!items || items.length===0)">
|
||||||
|
<td
|
||||||
|
colspan="3"
|
||||||
|
class="text-center grey--text"
|
||||||
|
>
|
||||||
|
데이터가 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
</v-simple-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
|
<!-- 등록/수정 팝업 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
max-width="560px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
{{ isEdit ? '에이전트 비율 수정' : '에이전트 비율 등록' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
v-model="formValid"
|
||||||
|
>
|
||||||
|
<!-- 에이전트 선택/표시 -->
|
||||||
|
<div v-if="!isEdit">
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="selectedAgent"
|
||||||
|
:items="agentSearchItems"
|
||||||
|
:loading="agentSearchLoading"
|
||||||
|
:search-input.sync="agentSearchQuery"
|
||||||
|
hide-selected
|
||||||
|
hide-no-data
|
||||||
|
label="에이전트 닉네임 검색"
|
||||||
|
item-text="nickname"
|
||||||
|
item-value="id"
|
||||||
|
return-object
|
||||||
|
clearable
|
||||||
|
:rules="[v=>!!v || '에이전트를 선택하세요.']"
|
||||||
|
@update:search-input="onSearchAgent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.nickname"
|
||||||
|
label="에이전트"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="form.settlementRatio"
|
||||||
|
label="정산 비율(%)"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
:rules="ratioRules"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.effectiveFrom"
|
||||||
|
label="적용 시작 날짜"
|
||||||
|
type="date"
|
||||||
|
:rules="[v=>!!v || '적용 시작 날짜를 선택하세요.']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="closeDialog"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
dark
|
||||||
|
:loading="submitting"
|
||||||
|
:disabled="!formValid || submitting"
|
||||||
|
@click="onSubmit"
|
||||||
|
>
|
||||||
|
등록
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
getAgentSettlementRatioList,
|
||||||
|
createAgentSettlementRatio,
|
||||||
|
updateAgentSettlementRatio,
|
||||||
|
searchAgentByNickname
|
||||||
|
} from '@/api/agent'
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentSettlementRatio',
|
name: 'AgentSettlementRatio',
|
||||||
|
filters: {
|
||||||
|
numberFormat(v) {
|
||||||
|
if (v === null || v === undefined) return '-'
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat('ko-KR').format(v)
|
||||||
|
} catch (e) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
items: [],
|
||||||
|
totalCount: 0,
|
||||||
|
|
||||||
|
dialog: false,
|
||||||
|
isEdit: false,
|
||||||
|
submitting: false,
|
||||||
|
formValid: false,
|
||||||
|
form: {
|
||||||
|
memberId: null,
|
||||||
|
nickname: '',
|
||||||
|
settlementRatio: null,
|
||||||
|
effectiveFrom: ''
|
||||||
|
},
|
||||||
|
ratioRules: [
|
||||||
|
v => (v !== null && v !== '' && !isNaN(v)) || '숫자를 입력하세요.',
|
||||||
|
v => (v >= 0 && v <= 100) || '0~100 사이 값이어야 합니다.'
|
||||||
|
],
|
||||||
|
|
||||||
|
// 에이전트 검색
|
||||||
|
selectedAgent: null,
|
||||||
|
agentSearchQuery: '',
|
||||||
|
agentSearchItems: [],
|
||||||
|
agentSearchLoading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchList() {
|
||||||
|
if (this.loading) return
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await getAgentSettlementRatioList()
|
||||||
|
let payload = (res && res.data) ? res.data : null
|
||||||
|
if (payload && payload.data && (!payload.items && !payload.totalCount)) {
|
||||||
|
payload = payload.data
|
||||||
|
}
|
||||||
|
const data = payload || { totalCount: 0, items: [] }
|
||||||
|
this.totalCount = data.totalCount || 0
|
||||||
|
this.items = Array.isArray(data.items) ? data.items : []
|
||||||
|
} catch (e) {
|
||||||
|
this.totalCount = 0
|
||||||
|
this.items = []
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickCreate() {
|
||||||
|
this.isEdit = false
|
||||||
|
this.resetForm()
|
||||||
|
this.dialog = true
|
||||||
|
},
|
||||||
|
onClickEdit(item) {
|
||||||
|
this.isEdit = true
|
||||||
|
this.resetForm()
|
||||||
|
this.form.memberId = item.memberId
|
||||||
|
this.form.nickname = item.nickname
|
||||||
|
this.form.settlementRatio = item && item.current ? item.current.settlementRatio : null
|
||||||
|
this.form.effectiveFrom = this.defaultDateTimeLocal()
|
||||||
|
this.dialog = true
|
||||||
|
},
|
||||||
|
closeDialog() {
|
||||||
|
if (this.submitting) return
|
||||||
|
this.dialog = false
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.submitting = false
|
||||||
|
this.formValid = false
|
||||||
|
this.form = {
|
||||||
|
memberId: null,
|
||||||
|
nickname: '',
|
||||||
|
settlementRatio: null,
|
||||||
|
effectiveFrom: this.defaultDateTimeLocal()
|
||||||
|
}
|
||||||
|
this.selectedAgent = null
|
||||||
|
this.agentSearchQuery = ''
|
||||||
|
this.agentSearchItems = []
|
||||||
|
if (this.$refs.form && this.$refs.form.resetValidation) this.$refs.form.resetValidation()
|
||||||
|
},
|
||||||
|
defaultDateTimeLocal() {
|
||||||
|
// 날짜만 선택하도록 기본값은 오늘 날짜 (YYYY-MM-DD)
|
||||||
|
const d = new Date()
|
||||||
|
const pad = n => n.toString().padStart(2, '0')
|
||||||
|
const yyyy = d.getFullYear()
|
||||||
|
const MM = pad(d.getMonth() + 1)
|
||||||
|
const dd = pad(d.getDate())
|
||||||
|
return `${yyyy}-${MM}-${dd}`
|
||||||
|
},
|
||||||
|
toLocalDateTimeString(v) {
|
||||||
|
// 입력 타입이 date이므로 "YYYY-MM-DD"를 받아 "YYYY-MM-DDT00:00:00"로 변환
|
||||||
|
// 과거 호환: datetime-local("YYYY-MM-DDTHH:mm")가 들어오면 초를 붙여 반환
|
||||||
|
if (!v) return null
|
||||||
|
if (v.length === 10) return `${v}T00:00:00`
|
||||||
|
if (v.length === 16) return `${v}:00`
|
||||||
|
// 이미 초까지 포함된 형태면 그대로 사용
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/.test(v)) {
|
||||||
|
return v.length === 16 ? `${v}:00` : v
|
||||||
|
}
|
||||||
|
return `${v}T00:00:00`
|
||||||
|
},
|
||||||
|
async onSubmit() {
|
||||||
|
if (this.submitting) return
|
||||||
|
const ok = await this.$refs.form.validate?.()
|
||||||
|
if (!ok) return
|
||||||
|
try {
|
||||||
|
this.submitting = true
|
||||||
|
const payload = {
|
||||||
|
memberId: this.isEdit ? this.form.memberId : (this.selectedAgent && this.selectedAgent.id),
|
||||||
|
settlementRatio: Number(this.form.settlementRatio),
|
||||||
|
effectiveFrom: this.toLocalDateTimeString(this.form.effectiveFrom)
|
||||||
|
}
|
||||||
|
if (!payload.memberId) {
|
||||||
|
this.submitting = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isEdit) {
|
||||||
|
await updateAgentSettlementRatio(payload)
|
||||||
|
} else {
|
||||||
|
await createAgentSettlementRatio(payload)
|
||||||
|
}
|
||||||
|
this.dialog = false
|
||||||
|
await this.fetchList()
|
||||||
|
} catch (e) {
|
||||||
|
// 실패 시에도 버튼 로딩 해제
|
||||||
|
} finally {
|
||||||
|
this.submitting = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onSearchAgent(q) {
|
||||||
|
if (!q) {
|
||||||
|
this.agentSearchItems = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.agentSearchLoading = true
|
||||||
|
try {
|
||||||
|
const list = await searchAgentByNickname(q)
|
||||||
|
this.agentSearchItems = Array.isArray(list) ? list : []
|
||||||
|
} catch (e) {
|
||||||
|
this.agentSearchItems = []
|
||||||
|
} finally {
|
||||||
|
this.agentSearchLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.clickable { cursor: pointer; }
|
||||||
|
.link { color: #1976d2; cursor: pointer; text-decoration: underline; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user