feat(calculate): 오리지널 시리즈 정산 기능 추가

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
Yu Sung
2026-04-22 10:20:19 +09:00
parent 2e499483dd
commit de18086699
4 changed files with 394 additions and 2 deletions

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

@@ -173,9 +173,9 @@ export default {
// ignore
}
// 정산 관리 메뉴에 '채널 후원 정산' 추가
// 정산현황 메뉴에 '채널 후원 정산' 추가
try {
const calculateMenu = this.items.find(m => m && m.title === '정산 관리')
const calculateMenu = this.items.find(m => m && m.title === '정산현황')
if (calculateMenu) {
if (!Array.isArray(calculateMenu.items)) {
calculateMenu.items = calculateMenu.items ? [].concat(calculateMenu.items) : []
@@ -188,6 +188,16 @@ export default {
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

View File

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

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: 10,
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>