에이전트 기능 #97

Merged
klaus merged 10 commits from test into main 2026-04-14 10:00:04 +00:00
3 changed files with 448 additions and 7 deletions
Showing only changes of commit c7a02ea4cc - Show all commits

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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