에이전트 기능 #97
@@ -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 {
|
export {
|
||||||
getAgentList,
|
getAgentList,
|
||||||
getAgentSettlementRatioList,
|
getAgentSettlementRatioList,
|
||||||
createAgentSettlementRatio,
|
createAgentSettlementRatio,
|
||||||
updateAgentSettlementRatio,
|
updateAgentSettlementRatio,
|
||||||
searchAgentByNickname,
|
searchAgentByNickname,
|
||||||
|
// 에이전트 상세 - 소속 크리에이터 관리
|
||||||
|
getAgentAssignedCreatorList,
|
||||||
|
searchAdminAgentAssignableCreators,
|
||||||
|
assignAgentCreator,
|
||||||
|
removeAgentCreator,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,262 @@
|
|||||||
>
|
>
|
||||||
<v-icon>mdi-arrow-left</v-icon>
|
<v-icon>mdi-arrow-left</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-toolbar-title>에이전트 상세</v-toolbar-title>
|
<v-toolbar-title>{{ (agentNickname || '-') + ' 소속 크리에이터' }}</v-toolbar-title>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="#3bb9f1"
|
||||||
|
dark
|
||||||
|
:loading="assignDialog.loading"
|
||||||
|
@click="openAssignDialog"
|
||||||
|
>
|
||||||
|
소속 추가
|
||||||
|
</v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
<v-container>
|
<v-container>
|
||||||
|
<!-- 소속 크리에이터 목록 -->
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<h3>에이전트 상세 페이지 (준비중)</h3>
|
<v-simple-table class="elevation-10">
|
||||||
<p>Agent ID: {{ agentId }}</p>
|
<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-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
getAgentAssignedCreatorList,
|
||||||
|
searchAdminAgentAssignableCreators,
|
||||||
|
assignAgentCreator,
|
||||||
|
removeAgentCreator
|
||||||
|
} from '@/api/agent'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentDetail',
|
name: 'AgentDetail',
|
||||||
props: {
|
props: {
|
||||||
@@ -29,9 +270,171 @@ export default {
|
|||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
required: true
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
goAgentDetail(item) {
|
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) {
|
goSettlement(item, type) {
|
||||||
const id = item.agentId
|
const id = item.agentId
|
||||||
|
|||||||
Reference in New Issue
Block a user