feat(agent): AgentList.vue 구현 — 총 에이전트 수, 정산 항목별 금액, 합계 표시 및 라우팅 추가

- api: /admin/partner/agent/list 연동을 위한 api/agent.js 추가(getAgentList)
- router: 에이전트 상세 및 5종 정산 상세 라우트 추가(파라미터 agentId 사용)
- AgentDetail.vue와 정산 상세 5개 뷰(플레이스홀더) 추가
- 숫자/통화 포맷 적용 및 클릭 가능한 스타일 클래스 추가
This commit is contained in:
Yu Sung
2026-04-11 20:51:28 +09:00
parent 2277f9eca6
commit 2adb0d5daa
9 changed files with 483 additions and 8 deletions

12
src/api/agent.js Normal file
View File

@@ -0,0 +1,12 @@
import Vue from 'vue'
// 에이전트 리스트 조회
// 서버 스펙에 페이지네이션이 없다면 단순 GET으로 사용
// 추후 필요 시 params(page,size) 확장 가능
async function getAgentList() {
return Vue.axios.get('/admin/partner/agent/list')
}
export {
getAgentList
}

View File

@@ -56,6 +56,42 @@ const routes = [
name: 'AgentList',
component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/AgentList.vue')
},
{
path: '/agent/:agentId',
name: 'AgentDetail',
props: true,
component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/AgentDetail.vue')
},
{
path: '/agent/:agentId/settlement/live',
name: 'AgentSettlementLive',
props: true,
component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/AgentLiveSettlement.vue')
},
{
path: '/agent/:agentId/settlement/content',
name: 'AgentSettlementContent',
props: true,
component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/AgentContentSettlement.vue')
},
{
path: '/agent/:agentId/settlement/community',
name: 'AgentSettlementCommunity',
props: true,
component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/AgentCommunitySettlement.vue')
},
{
path: '/agent/:agentId/settlement/content-donation',
name: 'AgentSettlementContentDonation',
props: true,
component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/AgentContentDonationSettlement.vue')
},
{
path: '/agent/:agentId/settlement/channel-donation',
name: 'AgentSettlementChannelDonation',
props: true,
component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/AgentChannelDonationSettlement.vue')
},
{
path: '/agent/settlement-ratio',
name: 'AgentSettlementRatio',

View File

@@ -0,0 +1,37 @@
<template>
<div>
<v-toolbar dark>
<v-btn
icon
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>에이전트 정산 상세 - 채널 후원</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-container>
<v-row>
<v-col>
<h3>채널 후원 정산 상세 페이지 (준비중)</h3>
<p>Agent ID: {{ agentId }}</p>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'AgentChannelDonationSettlement',
props: {
agentId: {
type: [String, Number],
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div>
<v-toolbar dark>
<v-btn
icon
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>에이전트 정산 상세 - 커뮤니티</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-container>
<v-row>
<v-col>
<h3>커뮤니티 정산 상세 페이지 (준비중)</h3>
<p>Agent ID: {{ agentId }}</p>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'AgentCommunitySettlement',
props: {
agentId: {
type: [String, Number],
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div>
<v-toolbar dark>
<v-btn
icon
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>에이전트 정산 상세 - 콘텐츠 후원</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-container>
<v-row>
<v-col>
<h3>콘텐츠 후원 정산 상세 페이지 (준비중)</h3>
<p>Agent ID: {{ agentId }}</p>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'AgentContentDonationSettlement',
props: {
agentId: {
type: [String, Number],
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div>
<v-toolbar dark>
<v-btn
icon
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>에이전트 정산 상세 - 콘텐츠</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-container>
<v-row>
<v-col>
<h3>콘텐츠 정산 상세 페이지 (준비중)</h3>
<p>Agent ID: {{ agentId }}</p>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'AgentContentSettlement',
props: {
agentId: {
type: [String, Number],
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div>
<v-toolbar dark>
<v-btn
icon
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>에이전트 상세</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-container>
<v-row>
<v-col>
<h3>에이전트 상세 페이지 (준비중)</h3>
<p>Agent ID: {{ agentId }}</p>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'AgentDetail',
props: {
agentId: {
type: [String, Number],
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,19 +1,224 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h2>에이전트 리스트</h2>
<p>에이전트 목록을 관리합니다. (구현 예정)</p>
</v-col>
</v-row>
</v-container>
<div>
<v-toolbar dark>
<v-spacer />
<v-toolbar-title>에이전트 리스트</v-toolbar-title>
<v-spacer />
</v-toolbar>
<br>
<v-container>
<v-row>
<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>
<th class="text-center">
콘텐츠
</th>
<th class="text-center">
커뮤니티
</th>
<th class="text-center">
콘텐츠 후원
</th>
<th class="text-center">
채널 후원
</th>
<th class="text-center">
합계
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in agentList"
:key="item.agentId"
>
<td
class="link"
@click="goAgentDetail(item)"
>
{{ item.agentNickname }}
</td>
<td class="text-center">
{{ item.assignedCreatorCount | numberFormat }}
</td>
<td
class="text-right clickable"
@click="goSettlement(item, 'live')"
>
{{ formatCurrency(item.liveAgentSettlementAmount) }}
</td>
<td
class="text-right clickable"
@click="goSettlement(item, 'content')"
>
{{ formatCurrency(item.contentAgentSettlementAmount) }}
</td>
<td
class="text-right clickable"
@click="goSettlement(item, 'community')"
>
{{ formatCurrency(item.communityAgentSettlementAmount) }}
</td>
<td
class="text-right clickable"
@click="goSettlement(item, 'content-donation')"
>
{{ formatCurrency(item.contentDonationAgentSettlementAmount) }}
</td>
<td
class="text-right clickable"
@click="goSettlement(item, 'channel-donation')"
>
{{ formatCurrency(item.channelDonationAgentSettlementAmount) }}
</td>
<td class="text-right">
{{ formatCurrency(totalAmount(item)) }}
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-col>
</v-row>
<!-- 페이지네이션은 서버 지원 활성화
<v-row class="text-center">
<v-col>
<v-pagination
v-model="page"
:length="total_page"
circle
@input="fetchList"
/>
</v-col>
</v-row>
-->
</v-container>
</div>
</template>
<script>
import { getAgentList } from '@/api/agent'
export default {
name: 'AgentList',
filters: {
numberFormat(v) {
if (v === null || v === undefined) return '-'
try {
return new Intl.NumberFormat('ko-KR').format(v)
} catch (e) {
return v
}
}
},
data() {
return {
agentList: [],
totalCount: 0,
// 페이지네이션(필요 시)
page: 1,
total_page: 1,
page_size: 20,
is_loading: false,
}
},
created() {
this.fetchList()
},
methods: {
async fetchList() {
if (this.is_loading) return
this.is_loading = true
try {
const res = await getAgentList(/* this.page, this.page_size */)
// 일부 API는 { data: { totalCount, items } } 형태로 한 번 더 래핑됨을 대비
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.agentList = Array.isArray(data.items) ? data.items : []
// 서버가 페이지네이션 정보를 주면 설정하도록 남김
this.total_page = Math.max(1, Math.ceil(this.totalCount / this.page_size))
} catch (e) {
this.totalCount = 0
this.agentList = []
} finally {
this.is_loading = false
}
},
formatCurrency(n) {
const num = Number(n || 0)
return new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW', maximumFractionDigits: 0 }).format(num)
},
totalAmount(item) {
const {
liveAgentSettlementAmount = 0,
contentAgentSettlementAmount = 0,
communityAgentSettlementAmount = 0,
contentDonationAgentSettlementAmount = 0,
channelDonationAgentSettlementAmount = 0,
} = item || {}
return (
(liveAgentSettlementAmount || 0) +
(contentAgentSettlementAmount || 0) +
(communityAgentSettlementAmount || 0) +
(contentDonationAgentSettlementAmount || 0) +
(channelDonationAgentSettlementAmount || 0)
)
},
goAgentDetail(item) {
this.$router.push({ name: 'AgentDetail', params: { agentId: item.agentId } })
},
goSettlement(item, type) {
const id = item.agentId
const nameMap = {
'live': 'AgentSettlementLive',
'content': 'AgentSettlementContent',
'community': 'AgentSettlementCommunity',
'content-donation': 'AgentSettlementContentDonation',
'channel-donation': 'AgentSettlementChannelDonation',
}
const name = nameMap[type]
if (!name) return
this.$router.push({ name, params: { agentId: id } })
}
}
}
</script>
<style scoped>
.link {
color: #3f51b5;
cursor: pointer;
text-decoration: underline;
}
.clickable {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div>
<v-toolbar dark>
<v-btn
icon
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>에이전트 정산 상세 - 라이브</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-container>
<v-row>
<v-col>
<h3>라이브 정산 상세 페이지 (준비중)</h3>
<p>Agent ID: {{ agentId }}</p>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'AgentLiveSettlement',
props: {
agentId: {
type: [String, Number],
required: true
}
}
}
</script>
<style scoped>
</style>