Compare commits

...

12 Commits

Author SHA1 Message Date
Yu Sung
90377bdb3c feat(content-list): 검색 버튼 색상 수정
- 검색 버튼 색상: #3bb9f1로 변경
2026-05-07 15:29:34 +09:00
Yu Sung
a58a5cc0d1 feat(member): 계정 상세 팝업에 콘텐츠 관리자 권한 추가 및 라벨/검색 버튼 색상 수정
- 신규 권한: CONTENT_MANAGER 라디오 옵션 및 매핑 추가
- 라벨 변경: '사용 여부' → '권한'
- 검색 버튼 색상: #3bb9f1로 변경

왜: 콘텐츠 관리자 권한 지원 및 UI 용어/가시성 개선
무엇: MemberList.vue 수정으로 옵션/매핑/라벨/컬러 반영
2026-05-07 15:29:02 +09:00
Yu Sung
e7c95ab91b fix(content): ADMIN 권한에서만 테마 조회 API 호출하도록 수정
ContentList.vue의 created 훅에서 isAdmin 검사 후 getAudioContentThemeList 조건부 호출.

불필요한 API 호출 방지 및 권한 준수.
2026-05-07 15:23:49 +09:00
Yu Sung
ad4d498eeb feat(menu): 역할 기반 사이드 메뉴 추가/노출 로직 개선
- ADMIN 권한에만 추가 메뉴(시리즈 배너, 캐릭터 챗봇, 에이전트 관리, 정산 확장) 노출

- API 메뉴가 비어있고 CONTENT_MANAGER이면 '콘텐츠 리스트(/content/list)' 기본 메뉴 추가

- 기존 예외 처리 유지
2026-05-07 15:21:39 +09:00
Yu Sung
c72e1c18df fix(content): 관리 컬럼과 버튼을 ADMIN 권한에만 표시
배경: 비ADMIN 계정에서도 관리 열과 수정/삭제/공유 버튼이 노출되어 접근 혼란을 유발.

변경: computed isAdmin(Vuex accountStore.role 우선, localStorage 폴백) 추가 후, 테이블 헤더와 각 행의 관리 영역에 v-if="isAdmin" 적용.

영향: ADMIN 외 권한에서는 UI 요소가 렌더링되지 않음. 기능 동작 변경 없음.
2026-05-07 14:28:47 +09:00
Yu Sung
9435334734 fix(api): 관리자 로그인 API 엔드포인트 변경
- /member/login -> /admin/member/login
- 프론트엔드 관리자 로그인 경로와 백엔드 변경사항 동기화
2026-05-07 14:21:59 +09:00
Yu Sung
f01f002614 refactor(account): 로그인 상태 필드 정리 및 role 저장
왜: userId/nickname/profileImage는 사용처가 없어 유지보수 단순화. 대신 권한 판별을 위해 role 필요.\n무엇: accountStore에서 세 필드 삭제, role 추가. isAuthenticated 동기화 수정. LOGIN/LOGOUT 로직 role 반영. Axios Authorization 유지.
2026-05-07 14:19:21 +09:00
Yu Sung
a833f0b6b8 feat(can): 캔 등록 화면에 일본 엔(JPY) 화폐 단위를 추가 2026-05-01 14:27:41 +09:00
Yu Sung
9b756cbaf1 fix(calculate): 오리지널 시리즈 정산 - pageSize 20으로 수정 2026-04-22 11:12:53 +09:00
Yu Sung
de18086699 feat(calculate): 오리지널 시리즈 정산 기능 추가
Co-authored-by: Junie <junie@jetbrains.com>
2026-04-22 10:20:19 +09:00
Yu Sung
2e499483dd feat(agent): 소속 추가 다이얼로그 크리에이터 검색 디바운스 적용
- onSearchCreators에 300ms 디바운스 로직 추가
- assignDialog.searchDebounceTimer 상태 추가 및 다이얼로그/소멸 시 정리
- 최신 검색어와 응답 불일치 시 결과 반영 방지 가드
- 검색어 비었을 때 로딩/결과 초기화 처리 강화
2026-04-13 14:05:56 +09:00
Yu Sung
ceee1681c9 feat(agent): 에이전트-크리에이터 오류 메시지 표시 다이얼로그 추가 2026-04-13 13:59:26 +09:00
10 changed files with 639 additions and 140 deletions

View File

@@ -1,7 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
async function login(email, password) { async function login(email, password) {
return Vue.axios.post('/member/login', { return Vue.axios.post('/admin/member/login', {
email, email,
password, password,
isAdmin: true, isAdmin: true,

View File

@@ -0,0 +1,37 @@
import Vue from 'vue';
// 소지 유저 조회
async function getOwners() {
return Vue.axios.get('/admin/calculate/original-series/owners');
}
// 정산 내역 조회 (page는 1부터 시작하는 UI 기준, 서버에는 0부터 전달)
async function getSettlementDetails({ startDate, endDate, creatorId, page = 1, size = 10 }) {
const params = new URLSearchParams();
// 서버 파라미터 스펙 변경: start_date, end_date, creator_id
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (creatorId != null) params.append("creator_id", creatorId);
params.append('page', Math.max(0, (page || 1) - 1));
params.append('size', size || 10);
return Vue.axios.get(`/admin/calculate/original-series/settlement-details?${params.toString()}`);
}
// 엑셀 다운로드 (xlsx 바이너리)
async function downloadSettlementExcel({ startDate, endDate }) {
const params = new URLSearchParams();
// 서버 파라미터 스펙 변경: start_date, end_date
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
return Vue.axios.get(`/admin/calculate/original-series/settlement-details/excel?${params.toString()}` , {
responseType: 'blob'
});
}
export {
getOwners,
getSettlementDetails,
downloadSettlementExcel,
};

View File

@@ -94,103 +94,135 @@ export default {
this.isLoading = true this.isLoading = true
try { try {
let res = await api.getMenus(); let res = await api.getMenus();
if (res.status === 200 && res.data.success === true && res.data.data.length > 0) { if (res.status === 200 && res.data.success === true) {
this.items = res.data.data // 기본 메뉴 설정 (API 결과가 비어있을 수 있음)
this.items = Array.isArray(res.data.data) ? res.data.data : []
// '시리즈 관리' 메뉴에 '배너 등록' 하위 메뉴 추가 // 현재 사용자 역할 확인
try { const role = (this.$store && this.$store.state && this.$store.state.accountStore && this.$store.state.accountStore.role)
const seriesMenu = this.items.find(m => m && m.title === '시리즈 관리') || localStorage.role
if (seriesMenu) {
if (!Array.isArray(seriesMenu.items)) { // ADMIN 권한 전용 추가 메뉴들
seriesMenu.items = seriesMenu.items ? [].concat(seriesMenu.items) : [] if (role === 'ADMIN') {
} // '시리즈 관리' 메뉴에 '배너 등록' 하위 메뉴 추가
const exists = seriesMenu.items.some(ci => ci && ci.route === '/content/series/banner') try {
if (!exists) { const seriesMenu = this.items.find(m => m && m.title === '시리즈 관리')
seriesMenu.items.push({ if (seriesMenu) {
title: '배너 등록', if (!Array.isArray(seriesMenu.items)) {
route: '/content/series/banner', seriesMenu.items = seriesMenu.items ? [].concat(seriesMenu.items) : []
items: null }
}) const exists = seriesMenu.items.some(ci => ci && ci.route === '/content/series/banner')
if (!exists) {
seriesMenu.items.push({
title: '배너 등록',
route: '/content/series/banner',
items: null
})
}
} }
} catch (e) {
// ignore
} }
} catch (e) {
// ignore
}
// 캐릭터 챗봇 메뉴 추가 // 캐릭터 챗봇 메뉴 추가
this.items.push({ this.items.push({
title: '캐릭터 챗봇', title: '캐릭터 챗봇',
route: null,
items: [
{
title: '배너 등록',
route: '/character/banner',
items: null
},
{
title: '캐릭터 리스트',
route: '/character',
items: null
},
{
title: '큐레이션',
route: '/character/curation',
items: null
},
{
title: '정산',
route: '/character/calculate',
items: null
},
{
title: '원작',
route: '/original-work',
items: null
},
]
})
// 에이전트 관리 메뉴를 '크리에이터 관리' 바로 아래에 추가
try {
const insertAfterTitle = '크리에이터 관리'
const agentMenu = {
title: '에이전트 관리',
route: null, route: null,
items: [ items: [
{ title: '에이전트 리스트', route: '/agent/list', items: null }, {
{ title: '에이전트 정산 비율', route: '/agent/settlement-ratio', items: null }, title: '배너 등록',
route: '/character/banner',
items: null
},
{
title: '캐릭터 리스트',
route: '/character',
items: null
},
{
title: '큐레이션',
route: '/character/curation',
items: null
},
{
title: '정산',
route: '/character/calculate',
items: null
},
{
title: '원작',
route: '/original-work',
items: null
},
] ]
})
// 에이전트 관리 메뉴를 '크리에이터 관리' 바로 아래에 추가
try {
const insertAfterTitle = '크리에이터 관리'
const agentMenu = {
title: '에이전트 관리',
route: null,
items: [
{ title: '에이전트 리스트', route: '/agent/list', items: null },
{ title: '에이전트 정산 비율', route: '/agent/settlement-ratio', items: null },
]
}
const idx = this.items.findIndex(m => m && m.title === insertAfterTitle)
if (idx >= 0) {
this.items.splice(idx + 1, 0, agentMenu)
} else {
// 기준 메뉴가 없으면 하단에 추가
this.items.push(agentMenu)
}
} catch (e) {
// ignore
} }
const idx = this.items.findIndex(m => m && m.title === insertAfterTitle) // 정산현황 메뉴에 '채널 후원 정산' 및 '오리지널 시리즈 정산' 추가
if (idx >= 0) { try {
this.items.splice(idx + 1, 0, agentMenu) const calculateMenu = this.items.find(m => m && m.title === '정산현황')
} else { if (calculateMenu) {
// 기준 메뉴가 없으면 하단에 추가 if (!Array.isArray(calculateMenu.items)) {
this.items.push(agentMenu) calculateMenu.items = calculateMenu.items ? [].concat(calculateMenu.items) : []
}
const exists = calculateMenu.items.some(ci => ci && ci.route === '/calculate/channel-donation')
if (!exists) {
calculateMenu.items.push({
title: '채널 후원 정산',
route: '/calculate/channel-donation',
items: null
})
}
const existsOriginal = calculateMenu.items.some(ci => ci && ci.route === '/calculate/original-series')
if (!existsOriginal) {
calculateMenu.items.push({
title: '오리지널 시리즈 정산',
route: '/calculate/original-series',
items: null
})
}
}
} catch (e) {
// ignore
} }
} catch (e) {
// ignore
} }
// 정산 관리 메뉴에 '채널 후원 정산' 추가 // 조회한 메뉴가 비어 있고, 콘텐츠 매니저라면 기본 메뉴 추가
try { if (this.items.length === 0 && role === 'CONTENT_MANAGER') {
const calculateMenu = this.items.find(m => m && m.title === '정산 관리') this.items.push({
if (calculateMenu) { title: '콘텐츠 리스트',
if (!Array.isArray(calculateMenu.items)) { route: '/content/list',
calculateMenu.items = calculateMenu.items ? [].concat(calculateMenu.items) : [] items: null
} })
const exists = calculateMenu.items.some(ci => ci && ci.route === '/calculate/channel-donation') }
if (!exists) {
calculateMenu.items.push({ // 그래도 비어있다면 이전 동작과 동일하게 처리
title: '채널 후원 정산', if (this.items.length === 0) {
route: '/calculate/channel-donation', this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!")
items: null this.logout();
})
}
}
} catch (e) {
// ignore
} }
} else { } else {
this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!") this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!")

View File

@@ -51,6 +51,11 @@ const routes = [
component: () => import(/* webpackChunkName: "counselor" */ '../views/Creator/CreatorSettlementRatio.vue') component: () => import(/* webpackChunkName: "counselor" */ '../views/Creator/CreatorSettlementRatio.vue')
}, },
// Agent Management // Agent Management
{
path: '/calculate/original-series',
name: 'OriginalSeriesSettlement',
component: () => import(/* webpackChunkName: "calculate" */ '../views/Calculate/OriginalSeriesSettlement.vue')
},
{ {
path: '/agent/list', path: '/agent/list',
name: 'AgentList', name: 'AgentList',

View File

@@ -12,17 +12,13 @@ enhanceAccessToken();
const accountStore = { const accountStore = {
namespaced: true, namespaced: true,
state: { state: {
userId: '',
nickname: '',
accessToken: '', accessToken: '',
profileImage: '', role: '',
}, },
getters: { getters: {
isAuthenticated(state) { isAuthenticated(state) {
state.userId = state.userId || localStorage.userId
state.nickname = state.nickname || localStorage.nickname
state.profileImage = state.profileImage || localStorage.profileImage
state.accessToken = state.accessToken || localStorage.accessToken state.accessToken = state.accessToken || localStorage.accessToken
state.role = state.role || localStorage.role
return state.accessToken !== undefined && return state.accessToken !== undefined &&
state.accessToken !== null && state.accessToken !== null &&
@@ -31,27 +27,19 @@ const accountStore = {
} }
}, },
mutations: { mutations: {
LOGIN(state, {userId, nickname, token, profileImage}) { LOGIN(state, {token, role}) {
state.userId = userId
localStorage.userId = userId
state.nickname = nickname
localStorage.nickname = nickname
state.profileImage = profileImage
localStorage.profileImage = profileImage
state.accessToken = token state.accessToken = token
localStorage.accessToken = token localStorage.accessToken = token
state.role = role
localStorage.role = role
Vue.axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; Vue.axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}, },
LOGOUT(state) { LOGOUT(state) {
state.userId = ''
state.nickname = ''
state.profileImage = ''
state.accessToken = '' state.accessToken = ''
state.role = ''
localStorage.clear() localStorage.clear()
if (location.pathname === '/') { if (location.pathname === '/') {

View File

@@ -289,6 +289,8 @@ export default {
searchQuery: '', searchQuery: '',
searchItems: [], searchItems: [],
searchLoading: false, searchLoading: false,
// 디바운스 타이머 보관
searchDebounceTimer: null,
}, },
unassignDialog: { unassignDialog: {
@@ -316,22 +318,50 @@ export default {
created() { created() {
this.fetchList(1) this.fetchList(1)
}, },
beforeDestroy() {
// 검색 디바운스 타이머 정리
if (this.assignDialog && this.assignDialog.searchDebounceTimer) {
clearTimeout(this.assignDialog.searchDebounceTimer)
this.assignDialog.searchDebounceTimer = null
}
},
methods: { methods: {
notifyError(message) {
// vuetify-dialog 플러그인을 통해 오류 노출
if (this.$dialog && this.$dialog.notify && this.$dialog.notify.error) {
this.$dialog.notify.error(message)
}
},
getErrorMessage(e) {
const fallback = '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'
try {
if (!e) return fallback
// axios 형태의 에러 응답을 우선 사용
const msg = (e.response && e.response.data && (e.response.data.message || e.response.data.error || e.response.data.msg))
|| e.message
return msg || fallback
} catch (_) {
return fallback
}
},
async fetchList(page = this.page) { async fetchList(page = this.page) {
if (this.is_loading) return if (this.is_loading) return
this.is_loading = true this.is_loading = true
try { try {
this.page = page this.page = page
const res = await getAgentAssignedCreatorList(this.agentId, Math.max(1, this.page), this.page_size) const res = await getAgentAssignedCreatorList(this.agentId, Math.max(1, this.page), this.page_size)
// ApiResponse<GetAdminAgentAssignedCreatorResponse> if (res && res.status === 200 && res.data && res.data.success === true) {
let payload = res && res.data ? res.data : null const data = (res.data && res.data.data) || { totalCount: 0, items: [] }
if (payload && payload.data) payload = payload.data this.totalCount = data.totalCount || 0
const data = payload || { totalCount: 0, items: [] } this.items = Array.isArray(data.items) ? data.items : []
this.totalCount = data.totalCount || 0 } else {
this.items = Array.isArray(data.items) ? data.items : [] const msg = res && res.data && (res.data.message || res.data.error || res.data.msg)
this.notifyError(msg || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
}
} catch (e) { } catch (e) {
this.totalCount = 0 this.totalCount = 0
this.items = [] this.items = []
this.notifyError(this.getErrorMessage(e))
} finally { } finally {
this.is_loading = false this.is_loading = false
} }
@@ -345,41 +375,81 @@ export default {
}, },
closeAssignDialog() { closeAssignDialog() {
this.assignDialog.visible = false this.assignDialog.visible = false
// 다이얼로그 닫힐 때 디바운스 및 로딩 상태 초기화
if (this.assignDialog.searchDebounceTimer) {
clearTimeout(this.assignDialog.searchDebounceTimer)
this.assignDialog.searchDebounceTimer = null
}
this.assignDialog.searchLoading = false
}, },
async onSearchCreators(q) { onSearchCreators(q) {
const query = (q || '').trim() const query = (q || '').trim()
this.assignDialog.searchQuery = query this.assignDialog.searchQuery = query
// 기존 타이머가 있으면 취소
if (this.assignDialog.searchDebounceTimer) {
clearTimeout(this.assignDialog.searchDebounceTimer)
this.assignDialog.searchDebounceTimer = null
}
if (!query) { if (!query) {
// 검색어 없으면 결과/로딩 초기화
this.assignDialog.searchItems = [] this.assignDialog.searchItems = []
this.assignDialog.searchLoading = false
return return
} }
this.assignDialog.searchLoading = true
try { // 디바운스: 300ms 이후 최신 검색어로 요청
const res = await searchAdminAgentAssignableCreators(query) this.assignDialog.searchDebounceTimer = setTimeout(async () => {
let payload = res && res.data ? res.data : null // 요청 직전 로딩 시작
if (payload && payload.data) payload = payload.data this.assignDialog.searchLoading = true
const data = payload || { totalCount: 0, items: [] } const currentQuery = this.assignDialog.searchQuery
this.assignDialog.searchItems = Array.isArray(data.items) ? data.items : [] try {
} catch (e) { const res = await searchAdminAgentAssignableCreators(currentQuery)
this.assignDialog.searchItems = [] // 사용자가 그 사이에 검색어를 바꿨다면 이 응답은 무시
} finally { if (this.assignDialog.searchQuery !== currentQuery) return
this.assignDialog.searchLoading = false
} if (res && res.status === 200 && res.data && res.data.success === true) {
const data = (res.data && res.data.data) || { totalCount: 0, items: [] }
this.assignDialog.searchItems = Array.isArray(data.items) ? data.items : []
} else {
this.assignDialog.searchItems = []
const msg = res && res.data && (res.data.message || res.data.error || res.data.msg)
this.notifyError(msg || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
}
} catch (e) {
// 에러 시에도 최신 검색어와 일치할 때만 처리
if (this.assignDialog.searchQuery === currentQuery) {
this.assignDialog.searchItems = []
this.notifyError(this.getErrorMessage(e))
}
} finally {
// 최신 검색어와 일치할 때만 로딩 해제
if (this.assignDialog.searchQuery === currentQuery) {
this.assignDialog.searchLoading = false
}
}
}, 300)
}, },
async onAssign() { async onAssign() {
if (!this.canAssign) return if (!this.canAssign) return
this.assignDialog.loading = true this.assignDialog.loading = true
try { try {
const assignedAt = `${this.assignDialog.assignedDate}T00:00:00` const assignedAt = `${this.assignDialog.assignedDate}T00:00:00`
await assignAgentCreator({ const res = await assignAgentCreator({
agentId: Number(this.agentId), agentId: Number(this.agentId),
creatorId: Number(this.assignDialog.selectedCreatorId), creatorId: Number(this.assignDialog.selectedCreatorId),
assignedAt, assignedAt,
}) })
this.closeAssignDialog() if (res && res.status === 200 && res.data && res.data.success === true) {
this.fetchList(1) this.closeAssignDialog()
this.fetchList(1)
} else {
const msg = res && res.data && (res.data.message || res.data.error || res.data.msg)
this.notifyError(msg || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
}
} catch (e) { } catch (e) {
// noop: 에러 토스트 자리는 프로젝트 전역 플러그인 유무에 따라 추가 가능 this.notifyError(this.getErrorMessage(e))
} finally { } finally {
this.assignDialog.loading = false this.assignDialog.loading = false
} }
@@ -400,14 +470,19 @@ export default {
try { try {
const time = this.unassignDialog.time || '00:00' const time = this.unassignDialog.time || '00:00'
const unassignedAt = `${this.unassignDialog.date}T${time}:00` const unassignedAt = `${this.unassignDialog.date}T${time}:00`
await removeAgentCreator({ const res = await removeAgentCreator({
creatorId: Number(this.unassignDialog.target.creatorId), creatorId: Number(this.unassignDialog.target.creatorId),
unassignedAt, unassignedAt,
}) })
this.closeUnassignDialog() if (res && res.status === 200 && res.data && res.data.success === true) {
this.fetchList(this.page) this.closeUnassignDialog()
this.fetchList(this.page)
} else {
const msg = res && res.data && (res.data.message || res.data.error || res.data.msg)
this.notifyError(msg || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
}
} catch (e) { } catch (e) {
// noop this.notifyError(this.getErrorMessage(e))
} finally { } finally {
this.unassignDialog.loading = false this.unassignDialog.loading = false
} }

View File

@@ -0,0 +1,340 @@
<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-btn
color="primary"
:loading="excelLoading"
@click="onDownloadExcel"
>
엑셀 다운로드
</v-btn>
</v-toolbar>
<v-container>
<!-- 필터 영역 -->
<v-row
class="mt-2 mb-2"
align="center"
justify="end"
>
<v-col
cols="12"
md="3"
>
<v-autocomplete
v-model="selectedCreatorId"
:items="ownerOptions"
:loading="ownersLoading"
item-text="nickname"
item-value="creatorId"
label="멤버 선택"
dense
clearable
hide-details
/>
</v-col>
<v-col
cols="12"
md="3"
>
<v-menu
v-model="menuStart"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-bind="attrs"
label="시작일"
readonly
dense
:value="startDateStr"
v-on="on"
/>
</template>
<v-date-picker
v-model="startDateStr"
scrollable
@input="menuStart = false"
/>
</v-menu>
</v-col>
<v-col
cols="12"
md="3"
>
<v-menu
v-model="menuEnd"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-bind="attrs"
label="종료일"
readonly
dense
:value="endDateStr"
v-on="on"
/>
</template>
<v-date-picker
v-model="endDateStr"
scrollable
@input="menuEnd = false"
/>
</v-menu>
</v-col>
<v-col
cols="12"
md="2"
>
<v-btn
color="primary"
:loading="isLoading"
@click="onSearch"
>
조회
</v-btn>
</v-col>
</v-row>
<!-- 테이블 영역 -->
<v-data-table
:headers="headers"
:items="items"
:loading="isLoading"
:items-per-page="pageSize"
class="elevation-1"
disable-pagination
hide-default-footer
>
<template v-slot:no-data>
<div class="text-center grey--text pa-6">
데이터가 없습니다.
</div>
</template>
<template v-slot:item.price="{ item }">
<span>{{ numberFormat(item.price) }}</span>
</template>
<template v-slot:item.totalCan="{ item }">
<span>{{ numberFormat(item.totalCan) }}</span>
</template>
<template v-slot:item.totalPoint="{ item }">
<span>{{ numberFormat(item.totalPoint) }}</span>
</template>
</v-data-table>
<v-row
class="mt-4"
justify="center"
>
<v-pagination
v-model="page"
:length="totalPages"
:total-visible="7"
@input="fetchList"
/>
</v-row>
</v-container>
</div>
</template>
<script>
import {
getOwners,
getSettlementDetails,
downloadSettlementExcel,
} from "@/api/original_series_settlement";
export default {
name: "OriginalSeriesSettlement",
data() {
return {
// 필터 상태
ownersLoading: false,
ownerOptions: [],
selectedCreatorId: null,
startDateStr: "",
endDateStr: "",
menuStart: false,
menuEnd: false,
// 목록 상태
isLoading: false,
items: [],
totalCount: 0,
page: 1,
pageSize: 20,
excelLoading: false,
headers: [
{ text: "시리즈 제목", value: "seriesTitle", align: "center" },
{ text: "콘텐츠 제목", value: "contentTitle", align: "center" },
{ text: "가격", value: "price", align: "center" },
{ text: "구분", value: "orderType", align: "center" },
{ text: "판매 수", value: "salesCount", align: "center" },
{ text: "합계(캔)", value: "totalCan", align: "center" },
{ text: "합계(포인트)", value: "totalPoint", align: "center" },
],
};
},
computed: {
totalPages() {
return Math.max(1, Math.ceil(this.totalCount / this.pageSize));
},
},
created() {
this.initDefaultDates();
this.loadOwners();
},
methods: {
numberFormat(v) {
if (v === null || v === undefined) return "-";
try {
return Number(v).toLocaleString();
} catch (e) {
return String(v);
}
},
notifyError(message) {
this.$dialog.notify.error(message);
},
notifySuccess(message) {
this.$dialog.notify.success(message);
},
// 이번 달 1일 ~ 오늘
initDefaultDates() {
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const first = new Date(y, m, 1);
const pad = (n) => (n < 10 ? "0" + n : "" + n);
this.startDateStr = `${first.getFullYear()}-${pad(
first.getMonth() + 1
)}-${pad(first.getDate())}`;
this.endDateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(
now.getDate()
)}`;
},
async loadOwners() {
this.ownersLoading = true;
try {
const res = await getOwners();
if (res.status === 200 && res.data && res.data.success) {
const list = Array.isArray(res.data.data) ? res.data.data : [];
this.ownerOptions = list;
// 가장 조회된 값 순서로 온다고 가정, 첫 번째를 기본 선택
if (list.length > 0) {
this.selectedCreatorId = list[0].creatorId;
}
// 초기 조회 (이번 달 범위, 기본 멤버)
this.page = 1;
await this.fetchList();
} else {
this.notifyError(
res.data && res.data.message
? res.data.message
: "소지 유저 조회 실패"
);
}
} catch (e) {
this.notifyError("소지 유저 조회 중 오류가 발생했습니다.");
} finally {
this.ownersLoading = false;
}
},
async onSearch() {
this.page = 1;
await this.fetchList();
},
async fetchList() {
if (!this.selectedCreatorId) {
this.items = [];
this.totalCount = 0;
return;
}
this.isLoading = true;
try {
const res = await getSettlementDetails({
startDate: this.startDateStr,
endDate: this.endDateStr,
creatorId: this.selectedCreatorId,
page: this.page,
size: this.pageSize,
});
if (res.status === 200 && res.data) {
if (res.data.success) {
const data = res.data.data || { totalCount: 0, items: [] };
this.totalCount = data.totalCount || 0;
this.items = Array.isArray(data.items) ? data.items : [];
} else {
this.notifyError(res.data.message || "정산 내역 조회 실패");
}
} else {
this.notifyError("정산 내역 조회 실패");
}
} catch (e) {
this.notifyError("정산 내역 조회 중 오류가 발생했습니다.");
} finally {
this.isLoading = false;
}
},
async onDownloadExcel() {
this.excelLoading = true;
try {
const res = await downloadSettlementExcel({
startDate: this.startDateStr,
endDate: this.endDateStr,
});
const blob = new Blob([res.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "오리지널_시리즈_정산.xlsx");
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
window.URL.revokeObjectURL(url);
this.notifySuccess("엑셀 다운로드가 시작되었습니다.");
} catch (e) {
this.notifyError("엑셀 다운로드 중 오류가 발생했습니다.");
} finally {
this.excelLoading = false;
}
},
},
};
</script>
<style scoped>
/* 테이블 헤더/바디 모두 가운데 정렬 */
.v-data-table table thead th,
.v-data-table table tbody td {
text-align: center !important;
}
</style>

View File

@@ -135,7 +135,8 @@ export default {
currency: 'KRW', currency: 'KRW',
currencies: [ currencies: [
{ text: 'KRW (한국 원)', value: 'KRW' }, { text: 'KRW (한국 원)', value: 'KRW' },
{ text: 'USD (미국 달러)', value: 'USD' } { text: 'USD (미국 달러)', value: 'USD' },
{ text: 'JPY (일본 엔)', value: 'JPY' }
], ],
headers: [ headers: [
{ {

View File

@@ -36,7 +36,7 @@
> >
<v-btn <v-btn
slot="append" slot="append"
color="#9970ff" color="#3bb9f1"
dark dark
@click="search" @click="search"
> >
@@ -96,7 +96,10 @@
<th class="text-center"> <th class="text-center">
오픈 예정일 오픈 예정일
</th> </th>
<th class="text-center"> <th
v-if="isAdmin"
class="text-center"
>
관리 관리
</th> </th>
</tr> </tr>
@@ -214,7 +217,7 @@
</td> </td>
<td>{{ item.date }}</td> <td>{{ item.date }}</td>
<td>{{ item.releaseDate }}</td> <td>{{ item.releaseDate }}</td>
<td> <td v-if="isAdmin">
<v-row> <v-row>
<v-col> <v-col>
<v-btn <v-btn
@@ -527,6 +530,14 @@ export default {
}; };
}, },
computed: {
isAdmin() {
const role = (this.$store && this.$store.state && this.$store.state.accountStore && this.$store.state.accountStore.role)
|| (typeof localStorage !== 'undefined' ? localStorage.role : '');
return role === 'ADMIN';
},
},
async created() { async created() {
this.audio_content = { this.audio_content = {
id: null, id: null,
@@ -539,7 +550,10 @@ export default {
is_settlement_ratio_deleted: false, is_settlement_ratio_deleted: false,
settlement_ratio: "", settlement_ratio: "",
}; };
await this.getAudioContentThemeList(); // ADMIN 권한일 때만 테마 리스트 조회
if (this.isAdmin) {
await this.getAudioContentThemeList();
}
await this.getAudioContent(); await this.getAudioContent();
}, },

View File

@@ -19,7 +19,7 @@
> >
<v-btn <v-btn
slot="append" slot="append"
color="#9970ff" color="#3bb9f1"
dark dark
@click="search" @click="search"
> >
@@ -171,7 +171,7 @@
<v-card-text> <v-card-text>
<v-row align="center"> <v-row align="center">
<v-col cols="4"> <v-col cols="4">
사용 여부 권한
</v-col> </v-col>
<v-col cols="8"> <v-col cols="8">
<v-radio-group <v-radio-group
@@ -191,6 +191,10 @@
value="USER" value="USER"
label="일반회원" label="일반회원"
/> />
<v-radio
value="CONTENT_MANAGER"
label="콘텐츠 관리자"
/>
<v-spacer /> <v-spacer />
</v-radio-group> </v-radio-group>
</v-col> </v-col>
@@ -451,6 +455,8 @@ export default {
this.user_type = 'CREATOR' this.user_type = 'CREATOR'
} else if (member.userType === '에이전트') { } else if (member.userType === '에이전트') {
this.user_type = 'AGENT' this.user_type = 'AGENT'
} else if (member.userType === '콘텐츠 관리자') {
this.user_type = 'CONTENT_MANAGER'
} }
this.email = member.email this.email = member.email
@@ -519,7 +525,8 @@ export default {
if ( if (
(this.user_type === 'CREATOR' && this.member.userType === '크리에이터') || (this.user_type === 'CREATOR' && this.member.userType === '크리에이터') ||
(this.user_type === 'USER' && this.member.userType === '일반회원') || (this.user_type === 'USER' && this.member.userType === '일반회원') ||
(this.user_type === 'AGENT' && this.member.userType === '에이전트') (this.user_type === 'AGENT' && this.member.userType === '에이전트') ||
(this.user_type === 'CONTENT_MANAGER' && this.member.userType === '콘텐츠 관리자')
) { ) {
this.notifyError("변경사항이 없습니다.") this.notifyError("변경사항이 없습니다.")
} else { } else {