feat(agent): 에이전트 정산 비율 페이지 작성

- 에이전트 정산 비율 API 추가 (목록/등록/수정/닉네임 검색)
- AgentSettlementRatio.vue 구현 — 목록 테이블, "에이전트 비율 추가" 버튼, 수정 버튼 및 등록/수정 공용 팝업 추가
- UX: 닉네임 검색(v-autocomplete), 숫자/범위(0~100) 검증, datetime-local 입력값 LocalDateTime 문자열 변환 처리
- 에러/로딩 상태 기본 처리 및 목록 새로고침 흐름 반영
This commit is contained in:
Yu Sung
2026-04-11 22:01:53 +09:00
parent 49de523552
commit 7608cefba1
2 changed files with 366 additions and 10 deletions

View File

@@ -7,6 +7,42 @@ async function getAgentList() {
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,
}

View File

@@ -1,19 +1,339 @@
<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-col cols="12">
<h2>에이전트 정산 비율</h2>
<p>에이전트 정산 비율을 설정/관리합니다. (구현 예정)</p>
<v-col
cols="12"
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-row>
</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>
<script>
import {
getAgentSettlementRatioList,
createAgentSettlementRatio,
updateAgentSettlementRatio,
searchAgentByNickname
} from '@/api/agent'
export default {
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>
<style scoped>
.clickable { cursor: pointer; }
.link { color: #1976d2; cursor: pointer; text-decoration: underline; }
</style>