feat(agent): 에이전트 상세 페이지 작성
- api/agent.js에 소속 관리 API 추가 (조회/검색/소속/해제) 및 Pageable 0-based 처리 - AgentDetail.vue 구현 — 목록/페이지네이션/소속 추가(자정 00:00:00)/소속 해제(날짜+시간) - AgentList.vue에서 상세 진입 시 닉네임을 쿼리로 전달하여 상세에서 표시 - AgentDetail.vue에 간단한 스타일 클래스 추가
This commit is contained in:
@@ -39,10 +39,48 @@ async function searchAgentByNickname(query) {
|
||||
}
|
||||
}
|
||||
|
||||
// 에이전트 소속 크리에이터 목록 조회
|
||||
// GET /admin/partner/agent/{agentId}/creator/list
|
||||
// params: { page, size }
|
||||
async function getAgentAssignedCreatorList(agentId, page = 1, size = 20) {
|
||||
// Spring Pageable은 일반적으로 0-based page index를 사용
|
||||
const zeroBasedPage = Math.max(0, Number(page || 1) - 1)
|
||||
return Vue.axios.get(`/admin/partner/agent/${agentId}/creator/list`, {
|
||||
params: { page: zeroBasedPage, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 추가 가능한 크리에이터 검색
|
||||
// GET /admin/partner/agent/creator/search?search_word=...
|
||||
async function searchAdminAgentAssignableCreators(search_word) {
|
||||
return Vue.axios.get('/admin/partner/agent/creator/search', {
|
||||
params: { search_word }
|
||||
})
|
||||
}
|
||||
|
||||
// 에이전트에 크리에이터 소속 시키기
|
||||
// POST /admin/partner/agent/assignment
|
||||
// payload: { agentId, creatorId, assignedAt } // assignedAt: LocalDateTime string (yyyy-MM-ddTHH:mm:ss)
|
||||
async function assignAgentCreator(payload) {
|
||||
return Vue.axios.post('/admin/partner/agent/assignment', payload)
|
||||
}
|
||||
|
||||
// 크리에이터 소속 해제
|
||||
// POST /admin/partner/agent/assignment/remove
|
||||
// payload: { creatorId, unassignedAt } // unassignedAt: LocalDateTime string
|
||||
async function removeAgentCreator(payload) {
|
||||
return Vue.axios.post('/admin/partner/agent/assignment/remove', payload)
|
||||
}
|
||||
|
||||
export {
|
||||
getAgentList,
|
||||
getAgentSettlementRatioList,
|
||||
createAgentSettlementRatio,
|
||||
updateAgentSettlementRatio,
|
||||
searchAgentByNickname,
|
||||
// 에이전트 상세 - 소속 크리에이터 관리
|
||||
getAgentAssignedCreatorList,
|
||||
searchAdminAgentAssignableCreators,
|
||||
assignAgentCreator,
|
||||
removeAgentCreator,
|
||||
}
|
||||
|
||||
@@ -7,21 +7,262 @@
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>에이전트 상세</v-toolbar-title>
|
||||
<v-toolbar-title>{{ (agentNickname || '-') + ' 소속 크리에이터' }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
:loading="assignDialog.loading"
|
||||
@click="openAssignDialog"
|
||||
>
|
||||
소속 추가
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<!-- 소속 크리에이터 목록 -->
|
||||
<v-row>
|
||||
<v-col>
|
||||
<h3>에이전트 상세 페이지 (준비중)</h3>
|
||||
<p>Agent ID: {{ agentId }}</p>
|
||||
<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-if="is_loading">
|
||||
<td
|
||||
colspan="3"
|
||||
class="text-center"
|
||||
>
|
||||
불러오는 중...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="items.length === 0">
|
||||
<td
|
||||
colspan="3"
|
||||
class="text-center"
|
||||
>
|
||||
소속된 크리에이터가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="row in items"
|
||||
:key="row.creatorId"
|
||||
>
|
||||
<td class="text-center">
|
||||
{{ row.creatorNickname }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ formatDateTime(row.assignedAt) }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
color="error"
|
||||
@click="openUnassignDialog(row)"
|
||||
>
|
||||
소속 해제
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row
|
||||
v-if="total_page > 1"
|
||||
class="text-center"
|
||||
>
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="fetchList"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- 소속 추가 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="assignDialog.visible"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>크리에이터 소속 추가</v-card-title>
|
||||
<v-card-text>
|
||||
<v-autocomplete
|
||||
v-model="assignDialog.selectedCreatorId"
|
||||
:items="assignDialog.searchItems"
|
||||
:loading="assignDialog.searchLoading"
|
||||
:search-input.sync="assignDialog.searchQuery"
|
||||
hide-no-data
|
||||
hide-selected
|
||||
clearable
|
||||
label="크리에이터 검색"
|
||||
item-text="creatorNickname"
|
||||
item-value="creatorId"
|
||||
@update:search-input="onSearchCreators"
|
||||
/>
|
||||
|
||||
<v-menu
|
||||
ref="menuAssignedDate"
|
||||
v-model="assignDialog.menu"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
min-width="auto"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="assignDialog.assignedDate"
|
||||
label="소속 날짜(자정으로 처리)"
|
||||
readonly
|
||||
dense
|
||||
v-bind="attrs"
|
||||
clearable
|
||||
v-on="on"
|
||||
/>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="assignDialog.assignedDate"
|
||||
locale="ko-kr"
|
||||
@input="$refs.menuAssignedDate.save(assignDialog.assignedDate)"
|
||||
/>
|
||||
</v-menu>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="closeAssignDialog"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!canAssign"
|
||||
:loading="assignDialog.loading"
|
||||
@click="onAssign"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 소속 해제 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="unassignDialog.visible"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>소속 해제</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="mb-3">
|
||||
크리에이터: <strong>{{ unassignDialog.target && unassignDialog.target.creatorNickname || '-' }}</strong>
|
||||
</div>
|
||||
|
||||
<v-menu
|
||||
ref="menuUnassignDate"
|
||||
v-model="unassignDialog.menuDate"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
min-width="auto"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="unassignDialog.date"
|
||||
label="해제 날짜"
|
||||
readonly
|
||||
dense
|
||||
v-bind="attrs"
|
||||
clearable
|
||||
v-on="on"
|
||||
/>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="unassignDialog.date"
|
||||
locale="ko-kr"
|
||||
@input="$refs.menuUnassignDate.save(unassignDialog.date)"
|
||||
/>
|
||||
</v-menu>
|
||||
|
||||
<v-menu
|
||||
ref="menuUnassignTime"
|
||||
v-model="unassignDialog.menuTime"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
min-width="290px"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="unassignDialog.time"
|
||||
label="해제 시간(시:분)"
|
||||
readonly
|
||||
dense
|
||||
v-bind="attrs"
|
||||
clearable
|
||||
v-on="on"
|
||||
/>
|
||||
</template>
|
||||
<v-time-picker
|
||||
v-if="unassignDialog.menuTime"
|
||||
v-model="unassignDialog.time"
|
||||
format="24hr"
|
||||
full-width
|
||||
@click:minute="$refs.menuUnassignTime.save(unassignDialog.time)"
|
||||
/>
|
||||
</v-menu>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="closeUnassignDialog"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="!canUnassign"
|
||||
:loading="unassignDialog.loading"
|
||||
@click="onUnassign"
|
||||
>
|
||||
해제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getAgentAssignedCreatorList,
|
||||
searchAdminAgentAssignableCreators,
|
||||
assignAgentCreator,
|
||||
removeAgentCreator
|
||||
} from '@/api/agent'
|
||||
|
||||
export default {
|
||||
name: 'AgentDetail',
|
||||
props: {
|
||||
@@ -29,9 +270,171 @@ export default {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
agentNickname: this.$route && this.$route.query ? this.$route.query.nickname : '',
|
||||
is_loading: false,
|
||||
items: [],
|
||||
totalCount: 0,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
|
||||
assignDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
selectedCreatorId: null,
|
||||
assignedDate: this.todayStr(),
|
||||
menu: false,
|
||||
searchQuery: '',
|
||||
searchItems: [],
|
||||
searchLoading: false,
|
||||
},
|
||||
|
||||
unassignDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
target: null,
|
||||
date: this.todayStr(),
|
||||
time: this.nowTimeHHmm(),
|
||||
menuDate: false,
|
||||
menuTime: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
total_page() {
|
||||
return Math.max(1, Math.ceil((this.totalCount || 0) / this.page_size))
|
||||
},
|
||||
canAssign() {
|
||||
return !!(this.assignDialog.selectedCreatorId && this.assignDialog.assignedDate)
|
||||
},
|
||||
canUnassign() {
|
||||
return !!(this.unassignDialog.target && this.unassignDialog.date && this.unassignDialog.time)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchList(1)
|
||||
},
|
||||
methods: {
|
||||
async fetchList(page = this.page) {
|
||||
if (this.is_loading) return
|
||||
this.is_loading = true
|
||||
try {
|
||||
this.page = page
|
||||
const res = await getAgentAssignedCreatorList(this.agentId, Math.max(1, this.page), this.page_size)
|
||||
// ApiResponse<GetAdminAgentAssignedCreatorResponse>
|
||||
let payload = res && res.data ? res.data : null
|
||||
if (payload && payload.data) 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.is_loading = false
|
||||
}
|
||||
},
|
||||
openAssignDialog() {
|
||||
this.assignDialog.visible = true
|
||||
this.assignDialog.selectedCreatorId = null
|
||||
this.assignDialog.assignedDate = this.todayStr()
|
||||
this.assignDialog.searchQuery = ''
|
||||
this.assignDialog.searchItems = []
|
||||
},
|
||||
closeAssignDialog() {
|
||||
this.assignDialog.visible = false
|
||||
},
|
||||
async onSearchCreators(q) {
|
||||
const query = (q || '').trim()
|
||||
this.assignDialog.searchQuery = query
|
||||
if (!query) {
|
||||
this.assignDialog.searchItems = []
|
||||
return
|
||||
}
|
||||
this.assignDialog.searchLoading = true
|
||||
try {
|
||||
const res = await searchAdminAgentAssignableCreators(query)
|
||||
let payload = res && res.data ? res.data : null
|
||||
if (payload && payload.data) payload = payload.data
|
||||
const data = payload || { totalCount: 0, items: [] }
|
||||
this.assignDialog.searchItems = Array.isArray(data.items) ? data.items : []
|
||||
} catch (e) {
|
||||
this.assignDialog.searchItems = []
|
||||
} finally {
|
||||
this.assignDialog.searchLoading = false
|
||||
}
|
||||
},
|
||||
async onAssign() {
|
||||
if (!this.canAssign) return
|
||||
this.assignDialog.loading = true
|
||||
try {
|
||||
const assignedAt = `${this.assignDialog.assignedDate}T00:00:00`
|
||||
await assignAgentCreator({
|
||||
agentId: Number(this.agentId),
|
||||
creatorId: Number(this.assignDialog.selectedCreatorId),
|
||||
assignedAt,
|
||||
})
|
||||
this.closeAssignDialog()
|
||||
this.fetchList(1)
|
||||
} catch (e) {
|
||||
// noop: 에러 토스트 자리는 프로젝트 전역 플러그인 유무에 따라 추가 가능
|
||||
} finally {
|
||||
this.assignDialog.loading = false
|
||||
}
|
||||
},
|
||||
openUnassignDialog(row) {
|
||||
this.unassignDialog.visible = true
|
||||
this.unassignDialog.loading = false
|
||||
this.unassignDialog.target = row
|
||||
this.unassignDialog.date = this.todayStr()
|
||||
this.unassignDialog.time = this.nowTimeHHmm()
|
||||
},
|
||||
closeUnassignDialog() {
|
||||
this.unassignDialog.visible = false
|
||||
},
|
||||
async onUnassign() {
|
||||
if (!this.canUnassign) return
|
||||
this.unassignDialog.loading = true
|
||||
try {
|
||||
const time = this.unassignDialog.time || '00:00'
|
||||
const unassignedAt = `${this.unassignDialog.date}T${time}:00`
|
||||
await removeAgentCreator({
|
||||
creatorId: Number(this.unassignDialog.target.creatorId),
|
||||
unassignedAt,
|
||||
})
|
||||
this.closeUnassignDialog()
|
||||
this.fetchList(this.page)
|
||||
} catch (e) {
|
||||
// noop
|
||||
} finally {
|
||||
this.unassignDialog.loading = false
|
||||
}
|
||||
},
|
||||
formatDateTime(s) {
|
||||
if (!s) return '-'
|
||||
try {
|
||||
// s는 LocalDateTime 문자열(예: 2026-01-01T00:00:00)
|
||||
const [d, t] = String(s).split('T')
|
||||
const hhmm = (t || '').slice(0,5)
|
||||
return `${d} ${hhmm}`
|
||||
} catch (e) {
|
||||
return s
|
||||
}
|
||||
},
|
||||
todayStr() {
|
||||
const d = new Date()
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${mm}-${dd}`
|
||||
},
|
||||
nowTimeHHmm() {
|
||||
const d = new Date()
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${hh}:${mm}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@@ -193,7 +193,7 @@ export default {
|
||||
)
|
||||
},
|
||||
goAgentDetail(item) {
|
||||
this.$router.push({ name: 'AgentDetail', params: { agentId: item.agentId } })
|
||||
this.$router.push({ name: 'AgentDetail', params: { agentId: item.agentId }, query: { nickname: item.agentNickname } })
|
||||
},
|
||||
goSettlement(item, type) {
|
||||
const id = item.agentId
|
||||
|
||||
Reference in New Issue
Block a user