Compare commits
175 Commits
fedecfed99
...
main
Author | SHA1 | Date | |
---|---|---|---|
6886c372aa | |||
![]() |
ee36120ed3 | ||
8dd3dcb770 | |||
![]() |
b5dbccf515 | ||
![]() |
379e5b67f3 | ||
![]() |
fd9ea2f5bb | ||
![]() |
3c28367be9 | ||
![]() |
8f0958848d | ||
1a435b6074 | |||
![]() |
a4cf43b88a | ||
![]() |
40c5a6593e | ||
492859dae3 | |||
![]() |
edab727c22 | ||
![]() |
00b12d0edb | ||
![]() |
6507b025de | ||
18b59b5598 | |||
![]() |
cd86973b60 | ||
![]() |
1e4dcffc17 | ||
5fcdd7f06d | |||
![]() |
5ee0fe6a60 | ||
![]() |
199049ab7c | ||
![]() |
bc8833483a | ||
![]() |
b94aa54365 | ||
![]() |
478ef2e7fe | ||
![]() |
63ebe9708f | ||
![]() |
071502d869 | ||
![]() |
806af4aba0 | ||
![]() |
e09f654aba | ||
![]() |
30e08c862a | ||
![]() |
231539fd27 | ||
![]() |
8f502f6d4d | ||
![]() |
38161af543 | ||
![]() |
ba248f7680 | ||
![]() |
a3e82a81f8 | ||
![]() |
efca5e445d | ||
![]() |
7ed23047e9 | ||
![]() |
bbacab88c5 | ||
![]() |
062bb4f7b2 | ||
![]() |
6bd3a62134 | ||
![]() |
d1f700842f | ||
![]() |
a9e832bc26 | ||
![]() |
80b298440b | ||
![]() |
7f56d0b423 | ||
![]() |
72b1627f3f | ||
![]() |
13c85bb2a8 | ||
![]() |
3783714c75 | ||
![]() |
49cd5a795b | ||
![]() |
94a989ea57 | ||
![]() |
439cc21e57 | ||
![]() |
dbc46482b1 | ||
![]() |
3aae253180 | ||
![]() |
89b2f1f740 | ||
1e149f7e41 | |||
![]() |
09c6605aed | ||
aca3767a24 | |||
![]() |
0aff527266 | ||
![]() |
cea0887d90 | ||
![]() |
d3f98ec9cb | ||
![]() |
d8e75f299b | ||
![]() |
256f65e370 | ||
d51655f15e | |||
![]() |
7821f766e6 | ||
47dd32939f | |||
![]() |
46f966f324 | ||
2e1891ab08 | |||
![]() |
35be9832e6 | ||
99d70cc8f7 | |||
![]() |
ba14bd1673 | ||
9f1675e82d | |||
![]() |
0a47b5d33f | ||
c2838be2ed | |||
![]() |
c81b31ddeb | ||
b5c2941c0d | |||
![]() |
2eb179a18e | ||
d5c01d8d23 | |||
![]() |
3ca2a36fa8 | ||
7118b0649a | |||
![]() |
f2f022531d | ||
8f5346581e | |||
![]() |
308a083f32 | ||
e43f2e30be | |||
![]() |
6e1a7dba06 | ||
![]() |
1e8f9f41c6 | ||
397fd267e0 | |||
![]() |
3575a4975b | ||
![]() |
421e0b2b5f | ||
fe4b88350b | |||
![]() |
42492a7d55 | ||
537474e162 | |||
![]() |
cc71a40f1b | ||
b5abdf3cf5 | |||
![]() |
81bd7a2e3f | ||
![]() |
1f8f2ff92e | ||
![]() |
4420b02f29 | ||
![]() |
1f5506dbc4 | ||
a2e457b5e8 | |||
![]() |
bcd0ea090c | ||
![]() |
efd50729f6 | ||
![]() |
3a6426e2e1 | ||
![]() |
73664768f9 | ||
![]() |
697de48d9c | ||
![]() |
de2f89bff1 | ||
![]() |
a6bcea3076 | ||
![]() |
16a314a8e9 | ||
![]() |
63fac77342 | ||
![]() |
21058bcb4f | ||
![]() |
e1feff39f8 | ||
![]() |
befe04c243 | ||
![]() |
af45c0093e | ||
![]() |
22b185c31a | ||
05ddd417cd | |||
![]() |
152fe817e8 | ||
e70426af68 | |||
![]() |
8f3a3ec8cc | ||
![]() |
f558c9260e | ||
![]() |
c2a4a64417 | ||
81b33e1322 | |||
![]() |
6edd6c1558 | ||
588fcfbe90 | |||
![]() |
13958733a7 | ||
ff2c126382 | |||
![]() |
67a17a44aa | ||
![]() |
a0a80bf626 | ||
![]() |
4b9259c525 | ||
![]() |
d5d365d0ad | ||
![]() |
521345b9c8 | ||
702daca29f | |||
8e9008a3c1 | |||
5c0c00aad4 | |||
e0949c6d73 | |||
0449bac8d5 | |||
d412c15c9d | |||
ed16a6ddad | |||
f06e2d41e0 | |||
7505269db3 | |||
15eeb6943d | |||
7e7ed46cea | |||
fd01786649 | |||
c48c1c2f09 | |||
9bcf3a3cdb | |||
4c5b987d98 | |||
f168403048 | |||
82ee1584e7 | |||
65cb918389 | |||
784baf9a2f | |||
7a85ac41cc | |||
9d4c9437cf | |||
68845aeae1 | |||
bbdca29337 | |||
c14c041daa | |||
a515a144eb | |||
54a6773905 | |||
d97087b4e9 | |||
ddb2449053 | |||
8aca07cdf7 | |||
0ba845d95a | |||
64b1fd5395 | |||
639bea70fa | |||
6a89ba059b | |||
ff83041585 | |||
e660be0bf4 | |||
62cdd57069 | |||
f8346ed5ef | |||
9656b9a9d1 | |||
97a58266bb | |||
8fc0cfa345 | |||
22f9c2287d | |||
9284f7d5c3 | |||
e6f27a4529 | |||
6a33d1c024 | |||
3b83789c15 | |||
55f0ab9af3 | |||
9b168a6112 | |||
c47937933e | |||
4744fe7d9a |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -218,4 +218,7 @@ $RECYCLE.BIN/
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
.kiro/
|
||||
.junie/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/webstorm,visualstudiocode,vuejs,macos,windows
|
||||
|
7
package-lock.json
generated
7
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"vue": "^2.6.11",
|
||||
"vue-excel-xlsx": "^1.2.2",
|
||||
"vue-router": "^3.2.0",
|
||||
@@ -9025,8 +9026,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -22908,8 +22908,7 @@
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
|
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"vue": "^2.6.11",
|
||||
"vue-excel-xlsx": "^1.2.2",
|
||||
"vue-router": "^3.2.0",
|
||||
|
@@ -7,7 +7,7 @@ async function getAudioContentList(status, page) {
|
||||
)
|
||||
}
|
||||
|
||||
async function searchAudioContent(searchWord, page){
|
||||
async function searchAudioContent(searchWord, page) {
|
||||
return Vue.axios.get(
|
||||
"/admin/audio-content/search?search_word=" + searchWord +
|
||||
"&page=" + (page - 1) +
|
||||
@@ -19,8 +19,8 @@ async function modifyAudioContent(request) {
|
||||
return Vue.axios.put("/admin/audio-content", request)
|
||||
}
|
||||
|
||||
async function getBannerList() {
|
||||
return Vue.axios.get("/admin/audio-content/banner")
|
||||
async function getBannerList(tabId) {
|
||||
return Vue.axios.get("/admin/audio-content/banner?tabId=" + tabId)
|
||||
}
|
||||
|
||||
async function saveBanner(formData) {
|
||||
@@ -43,8 +43,8 @@ async function updateBannerOrders(ids) {
|
||||
return Vue.axios.put('/admin/audio-content/banner/orders', {ids: ids})
|
||||
}
|
||||
|
||||
async function getCurations() {
|
||||
return Vue.axios.get("/admin/audio-content/curation")
|
||||
async function getCurations(tabId) {
|
||||
return Vue.axios.get("/admin/audio-content/curation?tabId=" + tabId)
|
||||
}
|
||||
|
||||
async function saveCuration(request) {
|
||||
@@ -63,6 +63,88 @@ async function getAudioContentThemeList() {
|
||||
return Vue.axios.get("/admin/audio-content/theme")
|
||||
}
|
||||
|
||||
async function getAudioContentMainTabList() {
|
||||
return Vue.axios.get("/admin/audio-content/main/tab")
|
||||
}
|
||||
|
||||
async function getCurationItems(curationId) {
|
||||
return Vue.axios.get("/admin/audio-content/curation/items?curationId=" + curationId)
|
||||
}
|
||||
|
||||
async function searchContentItem(curationId, searchWord) {
|
||||
return Vue.axios.get("/admin/audio-content/curation/search/content?curationId=" + curationId + "&searchWord=" + searchWord)
|
||||
}
|
||||
|
||||
async function searchSeriesItem(curationId, searchWord) {
|
||||
return Vue.axios.get("/admin/audio-content/curation/search/series?curationId=" + curationId + "&searchWord=" + searchWord)
|
||||
}
|
||||
|
||||
async function addItemToCuration(curationId, itemIdList) {
|
||||
return Vue.axios.post(
|
||||
"/admin/audio-content/curation/add/item",
|
||||
{curationId: curationId, itemIdList: itemIdList}
|
||||
)
|
||||
}
|
||||
|
||||
async function removeItemInCuration(curationId, itemId) {
|
||||
return Vue.axios.put(
|
||||
"/admin/audio-content/curation/remove/item",
|
||||
{curationId: curationId, itemId: itemId}
|
||||
)
|
||||
}
|
||||
|
||||
async function updateItemInCurationOrders(curationId, itemIds) {
|
||||
return Vue.axios.put(
|
||||
"/admin/audio-content/curation/orders/item",
|
||||
{curationId: curationId, itemIds: itemIds}
|
||||
)
|
||||
}
|
||||
|
||||
async function getHashTagCurations() {
|
||||
return Vue.axios.get("/admin/audio-content/tag/curation")
|
||||
}
|
||||
|
||||
async function saveHashTagCuration(request) {
|
||||
return Vue.axios.post("/admin/audio-content/tag/curation", request)
|
||||
}
|
||||
|
||||
async function modifyHashTagCuration(request) {
|
||||
return Vue.axios.put("/admin/audio-content/tag/curation", request)
|
||||
}
|
||||
|
||||
async function updateHashTagCurationOrders(ids) {
|
||||
return Vue.axios.put('/admin/audio-content/tag/curation/orders', {ids: ids})
|
||||
}
|
||||
|
||||
async function getHashTagCurationItems(curationId) {
|
||||
return Vue.axios.get('/admin/audio-content/tag/curation/items?curationId=' + curationId)
|
||||
}
|
||||
|
||||
async function addItemToHashTagCuration(curationId, itemIdList) {
|
||||
return Vue.axios.post(
|
||||
"/admin/audio-content/tag/curation/add/item",
|
||||
{curationId: curationId, itemIdList: itemIdList}
|
||||
)
|
||||
}
|
||||
|
||||
async function removeItemInHashTagCuration(curationId, itemId) {
|
||||
return Vue.axios.put(
|
||||
"/admin/audio-content/tag/curation/remove/item",
|
||||
{curationId: curationId, itemId: itemId}
|
||||
)
|
||||
}
|
||||
|
||||
async function searchHashTagContentItem(curationId, searchWord) {
|
||||
return Vue.axios.get("/admin/audio-content/tag/curation/search/content?curationId=" + curationId + "&searchWord=" + searchWord)
|
||||
}
|
||||
|
||||
async function updateItemInHashTagCurationOrders(curationId, itemIds) {
|
||||
return Vue.axios.put(
|
||||
"/admin/audio-content/tag/curation/orders/item",
|
||||
{curationId: curationId, itemIds: itemIds}
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
getAudioContentList,
|
||||
searchAudioContent,
|
||||
@@ -75,5 +157,21 @@ export {
|
||||
saveCuration,
|
||||
modifyCuration,
|
||||
updateCurationOrders,
|
||||
getAudioContentThemeList
|
||||
getAudioContentThemeList,
|
||||
getAudioContentMainTabList,
|
||||
getCurationItems,
|
||||
searchSeriesItem,
|
||||
searchContentItem,
|
||||
addItemToCuration,
|
||||
removeItemInCuration,
|
||||
updateItemInCurationOrders,
|
||||
getHashTagCurations,
|
||||
saveHashTagCuration,
|
||||
modifyHashTagCuration,
|
||||
updateHashTagCurationOrders,
|
||||
getHashTagCurationItems,
|
||||
addItemToHashTagCuration,
|
||||
removeItemInHashTagCuration,
|
||||
searchHashTagContentItem,
|
||||
updateItemInHashTagCurationOrders
|
||||
}
|
||||
|
@@ -20,10 +20,15 @@ async function updateAudioContentSeriesGenreOrders(ids) {
|
||||
return Vue.axios.put('/admin/audio-content/series/genre/orders', {ids: ids})
|
||||
}
|
||||
|
||||
async function searchSeriesList(searchWord) {
|
||||
return Vue.axios.get("/admin/audio-content/series/search?search_word=" + searchWord)
|
||||
}
|
||||
|
||||
export {
|
||||
getAudioContentSeriesList,
|
||||
getAudioContentSeriesGenreList,
|
||||
createAudioContentSeriesGenre,
|
||||
updateAudioContentSeriesGenre,
|
||||
updateAudioContentSeriesGenreOrders
|
||||
updateAudioContentSeriesGenreOrders,
|
||||
searchSeriesList
|
||||
}
|
||||
|
32
src/api/audio_content_series_recommend.js
Normal file
32
src/api/audio_content_series_recommend.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
async function getRecommendSeriesList(isFree) {
|
||||
return Vue.axios.get("/admin/audio-content/series/recommend?isFree=" + isFree);
|
||||
}
|
||||
|
||||
async function saveRecommendSeries(formData) {
|
||||
return Vue.axios.post('/admin/audio-content/series/recommend', formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function modifyRecommendSeries(formData) {
|
||||
return Vue.axios.put('/admin/audio-content/series/recommend', formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function updateRecommendSeriesOrders(ids) {
|
||||
return Vue.axios.put('/admin/audio-content/series/recommend/orders', {ids: ids})
|
||||
}
|
||||
|
||||
export {
|
||||
getRecommendSeriesList,
|
||||
saveRecommendSeries,
|
||||
modifyRecommendSeries,
|
||||
updateRecommendSeriesOrders
|
||||
}
|
@@ -24,6 +24,36 @@ async function getAuditionDetail(id) {
|
||||
return Vue.axios.get("/admin/audition/" + id);
|
||||
}
|
||||
|
||||
export {
|
||||
getAuditionList, createAudition, updateAudition, getAuditionDetail
|
||||
async function createAuditionRole(formData) {
|
||||
return Vue.axios.post("/admin/audition/role", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function updateAuditionRole(formData) {
|
||||
return Vue.axios.put("/admin/audition/role", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function getAuditionRoleDetail(id) {
|
||||
return Vue.axios.get("/admin/audition/role/" + id);
|
||||
}
|
||||
|
||||
async function getAuditionApplicantList(id, page) {
|
||||
return Vue.axios.get("/admin/audition/role/" + id + "/applicant?page=" + (page - 1) + "&size=20");
|
||||
}
|
||||
|
||||
async function deleteAuditionApplicant(id) {
|
||||
return Vue.axios.delete("/admin/audition/applicant/" + id);
|
||||
}
|
||||
|
||||
export {
|
||||
getAuditionList, createAudition, updateAudition, getAuditionDetail,
|
||||
createAuditionRole, updateAuditionRole, getAuditionRoleDetail, getAuditionApplicantList,
|
||||
deleteAuditionApplicant
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ async function getCalculateCommunityPost(startDate, endDate, page, size) {
|
||||
}
|
||||
|
||||
async function getSettlementRatio(page) {
|
||||
return Vue.axios.get('/admin/calculate/ratio?page=' + (page - 1) + "&size=20'");
|
||||
return Vue.axios.get('/admin/calculate/ratio?page=' + (page - 1) + "&size=20");
|
||||
}
|
||||
|
||||
async function createCreatorSettlementRatio(creatorSettlementRatio) {
|
||||
@@ -57,6 +57,21 @@ async function getCalculateCommunityByCreator(startDate, endDate, page, size) {
|
||||
)
|
||||
}
|
||||
|
||||
async function updateCreatorSettlementRatio(creatorSettlementRatio) {
|
||||
const request = {
|
||||
memberId: creatorSettlementRatio.creator_id,
|
||||
subsidy: creatorSettlementRatio.subsidy,
|
||||
liveSettlementRatio: creatorSettlementRatio.liveSettlementRatio,
|
||||
contentSettlementRatio: creatorSettlementRatio.contentSettlementRatio,
|
||||
communitySettlementRatio: creatorSettlementRatio.communitySettlementRatio
|
||||
};
|
||||
return Vue.axios.post('/admin/calculate/ratio/update', request);
|
||||
}
|
||||
|
||||
async function deleteCreatorSettlementRatio(memberId) {
|
||||
return Vue.axios.post('/admin/calculate/ratio/delete/' + memberId);
|
||||
}
|
||||
|
||||
export {
|
||||
getCalculateLive,
|
||||
getCalculateContent,
|
||||
@@ -65,6 +80,8 @@ export {
|
||||
getCalculateCommunityPost,
|
||||
getSettlementRatio,
|
||||
createCreatorSettlementRatio,
|
||||
updateCreatorSettlementRatio,
|
||||
deleteCreatorSettlementRatio,
|
||||
getCalculateLiveByCreator,
|
||||
getCalculateContentByCreator,
|
||||
getCalculateCommunityByCreator
|
||||
|
@@ -5,11 +5,11 @@ async function deleteCan(id) {
|
||||
}
|
||||
|
||||
async function getCans() {
|
||||
return Vue.axios.get('/can');
|
||||
return Vue.axios.get('/admin/can');
|
||||
}
|
||||
|
||||
async function insertCan(can, rewardCan, price) {
|
||||
const request = {can: can, rewardCan: rewardCan, price: price}
|
||||
async function insertCan(can, rewardCan, price, currency) {
|
||||
const request = {can: can, rewardCan: rewardCan, price: price, currency}
|
||||
return Vue.axios.post('/admin/can', request);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ async function getCouponList(page) {
|
||||
return Vue.axios.get('/can/coupon?page=' + (page - 1) + "&size=20");
|
||||
}
|
||||
|
||||
async function generateCoupon(couponName, can, validity, isMultipleUse, couponNumberCount) {
|
||||
const request = {couponName, can, validity: validity + ' 23:59:59', isMultipleUse, couponNumberCount};
|
||||
async function generateCoupon(couponName, couponType, can, validity, isMultipleUse, couponNumberCount) {
|
||||
const request = {couponName, couponType, can, validity: validity + ' 23:59:59', isMultipleUse, couponNumberCount};
|
||||
return Vue.axios.post('/can/coupon', request);
|
||||
}
|
||||
|
||||
|
293
src/api/character.js
Normal file
293
src/api/character.js
Normal file
@@ -0,0 +1,293 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
// 캐릭터 리스트
|
||||
async function getCharacterList(page = 1, size = 20) {
|
||||
return Vue.axios.get('/admin/chat/character/list', {
|
||||
params: { page: page - 1, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 검색 (배너용 기존 함수)
|
||||
async function searchCharacters(searchTerm, page = 1, size = 20) {
|
||||
return Vue.axios.get('/admin/chat/banner/search-character', {
|
||||
params: { searchTerm, page: page - 1, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 리스트 검색 (요구사항: /admin/chat/character/search)
|
||||
async function searchCharacterList(searchTerm, page = 1, size = 20) {
|
||||
return Vue.axios.get('/admin/chat/character/search', {
|
||||
params: { searchTerm, page: page - 1, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 상세 조회
|
||||
async function getCharacter(id) {
|
||||
return Vue.axios.get(`/admin/chat/character/${id}`)
|
||||
}
|
||||
|
||||
// 내부 헬퍼: 빈 문자열을 null로 변환
|
||||
function toNullIfBlank(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim() === '' ? null : value;
|
||||
}
|
||||
return value === '' ? null : value;
|
||||
}
|
||||
|
||||
// 캐릭터 등록
|
||||
async function createCharacter(characterData) {
|
||||
const formData = new FormData()
|
||||
|
||||
// 이미지만 FormData에 추가
|
||||
if (characterData.image) formData.append('image', characterData.image)
|
||||
|
||||
// 나머지 데이터는 JSON 문자열로 변환하여 request 필드에 추가
|
||||
const requestData = {
|
||||
name: toNullIfBlank(characterData.name),
|
||||
systemPrompt: toNullIfBlank(characterData.systemPrompt),
|
||||
description: toNullIfBlank(characterData.description),
|
||||
age: toNullIfBlank(characterData.age),
|
||||
gender: toNullIfBlank(characterData.gender),
|
||||
mbti: toNullIfBlank(characterData.mbti),
|
||||
characterType: toNullIfBlank(characterData.type),
|
||||
originalWorkId: characterData.originalWorkId || null,
|
||||
speechPattern: toNullIfBlank(characterData.speechPattern),
|
||||
speechStyle: toNullIfBlank(characterData.speechStyle),
|
||||
appearance: toNullIfBlank(characterData.appearance),
|
||||
tags: characterData.tags || [],
|
||||
hobbies: characterData.hobbies || [],
|
||||
values: characterData.values || [],
|
||||
goals: characterData.goals || [],
|
||||
relationships: characterData.relationships || [],
|
||||
personalities: characterData.personalities || [],
|
||||
backgrounds: characterData.backgrounds || [],
|
||||
memories: characterData.memories || []
|
||||
}
|
||||
|
||||
formData.append('request', JSON.stringify(requestData))
|
||||
|
||||
return Vue.axios.post('/admin/chat/character/register', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 수정
|
||||
async function updateCharacter(characterData, image = null) {
|
||||
const formData = new FormData()
|
||||
|
||||
// 이미지가 있는 경우에만 FormData에 추가
|
||||
if (image) formData.append('image', image)
|
||||
|
||||
// 변경된 데이터만 JSON 문자열로 변환하여 request 필드에 추가 ('' -> null 변환)
|
||||
// characterData는 이미 변경된 필드만 포함하고 있음
|
||||
const processed = {}
|
||||
Object.keys(characterData).forEach(key => {
|
||||
const value = characterData[key]
|
||||
if (typeof value === 'string' || value === '') {
|
||||
processed[key] = toNullIfBlank(value)
|
||||
} else {
|
||||
processed[key] = value
|
||||
}
|
||||
})
|
||||
formData.append('request', JSON.stringify(processed))
|
||||
|
||||
return Vue.axios.put(`/admin/chat/character/update`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 배너 리스트 조회
|
||||
async function getCharacterBannerList(page = 1, size = 20) {
|
||||
return Vue.axios.get('/admin/chat/banner/list', {
|
||||
params: { page: page - 1, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 배너 등록
|
||||
async function createCharacterBanner(bannerData) {
|
||||
const formData = new FormData()
|
||||
|
||||
// 이미지 FormData에 추가
|
||||
if (bannerData.image) formData.append('image', bannerData.image)
|
||||
|
||||
// 캐릭터 ID를 JSON 문자열로 변환하여 request 필드에 추가
|
||||
const requestData = {
|
||||
characterId: bannerData.characterId
|
||||
}
|
||||
|
||||
formData.append('request', JSON.stringify(requestData))
|
||||
|
||||
return Vue.axios.post('/admin/chat/banner/register', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 배너 수정
|
||||
async function updateCharacterBanner(bannerData) {
|
||||
const formData = new FormData()
|
||||
|
||||
// 이미지가 있는 경우에만 FormData에 추가
|
||||
if (bannerData.image) formData.append('image', bannerData.image)
|
||||
|
||||
// 캐릭터 ID와 배너 ID를 JSON 문자열로 변환하여 request 필드에 추가
|
||||
const requestData = {
|
||||
characterId: bannerData.characterId,
|
||||
bannerId: bannerData.bannerId
|
||||
}
|
||||
|
||||
formData.append('request', JSON.stringify(requestData))
|
||||
|
||||
return Vue.axios.put('/admin/chat/banner/update', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 배너 삭제
|
||||
async function deleteCharacterBanner(bannerId) {
|
||||
return Vue.axios.delete(`/admin/chat/banner/${bannerId}`)
|
||||
}
|
||||
|
||||
// 캐릭터 배너 순서 변경
|
||||
async function updateCharacterBannerOrder(bannerIds) {
|
||||
return Vue.axios.put('/admin/chat/banner/orders', {ids: bannerIds})
|
||||
}
|
||||
|
||||
// 캐릭터 이미지 리스트
|
||||
async function getCharacterImageList(characterId, page = 1, size = 20) {
|
||||
return Vue.axios.get('/admin/chat/character/image/list', {
|
||||
params: { characterId, page: page - 1, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 이미지 상세
|
||||
async function getCharacterImage(imageId) {
|
||||
return Vue.axios.get(`/admin/chat/character/image/${imageId}`)
|
||||
}
|
||||
|
||||
// 캐릭터 이미지 등록
|
||||
async function createCharacterImage(imageData) {
|
||||
const formData = new FormData()
|
||||
if (imageData.image) formData.append('image', imageData.image)
|
||||
const requestData = {
|
||||
characterId: imageData.characterId,
|
||||
imagePriceCan: imageData.imagePriceCan,
|
||||
messagePriceCan: imageData.messagePriceCan,
|
||||
isAdult: imageData.isAdult,
|
||||
triggers: imageData.triggers || []
|
||||
}
|
||||
formData.append('request', JSON.stringify(requestData))
|
||||
return Vue.axios.post('/admin/chat/character/image/register', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// 캐릭터 이미지 수정 (트리거만 수정)
|
||||
async function updateCharacterImage(imageData) {
|
||||
const imageId = imageData.imageId
|
||||
const payload = { triggers: imageData.triggers || [] }
|
||||
return Vue.axios.put(`/admin/chat/character/image/${imageId}/triggers`, payload)
|
||||
}
|
||||
|
||||
// 캐릭터 이미지 삭제
|
||||
async function deleteCharacterImage(imageId) {
|
||||
return Vue.axios.delete(`/admin/chat/character/image/${imageId}`)
|
||||
}
|
||||
|
||||
// 캐릭터 이미지 순서 변경
|
||||
async function updateCharacterImageOrder(characterId, imageIds) {
|
||||
return Vue.axios.put('/admin/chat/character/image/orders', { characterId, ids: imageIds })
|
||||
}
|
||||
|
||||
// 캐릭터 큐레이션 목록
|
||||
async function getCharacterCurationList() {
|
||||
return Vue.axios.get('/admin/chat/character/curation/list')
|
||||
}
|
||||
|
||||
// 캐릭터 큐레이션 등록
|
||||
async function createCharacterCuration({ title, isAdult, isActive }) {
|
||||
return Vue.axios.post('/admin/chat/character/curation/register', { title, isAdult, isActive })
|
||||
}
|
||||
|
||||
// 캐릭터 큐레이션 수정
|
||||
// payload: { id: Long, title?, isAdult?, isActive? }
|
||||
async function updateCharacterCuration(payload) {
|
||||
return Vue.axios.put('/admin/chat/character/curation/update', payload)
|
||||
}
|
||||
|
||||
// 캐릭터 큐레이션 삭제
|
||||
async function deleteCharacterCuration(curationId) {
|
||||
return Vue.axios.delete(`/admin/chat/character/curation/${curationId}`)
|
||||
}
|
||||
|
||||
// 캐릭터 큐레이션 정렬 순서 변경
|
||||
async function updateCharacterCurationOrder(ids) {
|
||||
return Vue.axios.put('/admin/chat/character/curation/reorder', { ids })
|
||||
}
|
||||
|
||||
// 큐레이션에 캐릭터 등록 (다중 등록)
|
||||
// characterIds: Array<Long>
|
||||
async function addCharacterToCuration(curationId, characterIds) {
|
||||
return Vue.axios.post(`/admin/chat/character/curation/${curationId}/characters`, { characterIds })
|
||||
}
|
||||
|
||||
// 큐레이션에서 캐릭터 삭제
|
||||
async function removeCharacterFromCuration(curationId, characterId) {
|
||||
return Vue.axios.delete(`/admin/chat/character/curation/${curationId}/characters/${characterId}`)
|
||||
}
|
||||
|
||||
// 큐레이션 내 캐릭터 정렬 순서 변경
|
||||
async function updateCurationCharactersOrder(curationId, characterIds) {
|
||||
return Vue.axios.put(`/admin/chat/character/curation/${curationId}/characters/reorder`, { characterIds })
|
||||
}
|
||||
|
||||
// 큐레이션 캐릭터 목록 조회 (가정된 엔드포인트)
|
||||
async function getCharactersInCuration(curationId) {
|
||||
return Vue.axios.get(`/admin/chat/character/curation/${curationId}/characters`)
|
||||
}
|
||||
|
||||
// 캐릭터별 정산 목록
|
||||
// params: { startDateStr, endDateStr, sort, page, size }
|
||||
async function getCharacterCalculateList({ startDateStr, endDateStr, sort = 'TOTAL_SALES_DESC', page = 0, size = 30 }) {
|
||||
return Vue.axios.get('/admin/chat/calculate/characters', {
|
||||
params: { startDateStr, endDateStr, sort, page, size }
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
getCharacterList,
|
||||
searchCharacters,
|
||||
searchCharacterList,
|
||||
getCharacter,
|
||||
createCharacter,
|
||||
updateCharacter,
|
||||
getCharacterBannerList,
|
||||
createCharacterBanner,
|
||||
updateCharacterBanner,
|
||||
deleteCharacterBanner,
|
||||
updateCharacterBannerOrder,
|
||||
getCharacterImageList,
|
||||
getCharacterImage,
|
||||
createCharacterImage,
|
||||
updateCharacterImage,
|
||||
deleteCharacterImage,
|
||||
updateCharacterImageOrder,
|
||||
// Character Curation
|
||||
getCharacterCurationList,
|
||||
createCharacterCuration,
|
||||
updateCharacterCuration,
|
||||
deleteCharacterCuration,
|
||||
updateCharacterCurationOrder,
|
||||
addCharacterToCuration,
|
||||
removeCharacterFromCuration,
|
||||
updateCurationCharactersOrder,
|
||||
getCharactersInCuration,
|
||||
getCharacterCalculateList
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
async function save(formData) {
|
||||
return Vue.axios.post('/event', formData, {
|
||||
return Vue.axios.post('/admin/event/banner', formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
@@ -9,7 +9,7 @@ async function save(formData) {
|
||||
}
|
||||
|
||||
async function modify(formData) {
|
||||
return Vue.axios.put('/event', formData, {
|
||||
return Vue.axios.put('/admin/event/banner', formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
@@ -17,11 +17,11 @@ async function modify(formData) {
|
||||
}
|
||||
|
||||
async function deleteEvent(id) {
|
||||
return Vue.axios.delete("/event/" + id)
|
||||
return Vue.axios.delete("/admin/event/banner/" + id)
|
||||
}
|
||||
|
||||
async function getEvents() {
|
||||
return Vue.axios.get("/event")
|
||||
return Vue.axios.get("/admin/event/banner")
|
||||
}
|
||||
|
||||
export {save, modify, deleteEvent, getEvents}
|
||||
|
24
src/api/marketing.js
Normal file
24
src/api/marketing.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
async function createMediaPartner(request) {
|
||||
return Vue.axios.post("/admin/marketing/media-partner", request)
|
||||
}
|
||||
|
||||
async function updateMediaPartner(request) {
|
||||
return Vue.axios.put("/admin/marketing/media-partner", request)
|
||||
}
|
||||
|
||||
async function getMediaPartnerList(page) {
|
||||
return Vue.axios.get("/admin/marketing/media-partner?page=" + (page - 1) + "&size=20")
|
||||
}
|
||||
|
||||
async function getStatistics(startDate, endDate, page) {
|
||||
return Vue.axios.get("/admin/marketing/statistics?startDateStr=" + startDate + "&endDateStr=" + endDate + "&page=" + (page - 1) + "&size=20")
|
||||
}
|
||||
|
||||
export {
|
||||
createMediaPartner,
|
||||
updateMediaPartner,
|
||||
getMediaPartnerList,
|
||||
getStatistics
|
||||
}
|
@@ -47,6 +47,11 @@ async function getCreatorAllList() {
|
||||
return Vue.axios.get("/admin/member/creator/all/list")
|
||||
}
|
||||
|
||||
async function resetPassword(id) {
|
||||
const request = {memberId: id}
|
||||
return Vue.axios.post("/admin/member/password/reset", request)
|
||||
}
|
||||
|
||||
export {
|
||||
login,
|
||||
getMemberList,
|
||||
@@ -54,5 +59,6 @@ export {
|
||||
getCreatorList,
|
||||
searchCreator,
|
||||
updateMember,
|
||||
getCreatorAllList
|
||||
getCreatorAllList,
|
||||
resetPassword
|
||||
}
|
||||
|
10
src/api/member_statistics.js
Normal file
10
src/api/member_statistics.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
async function getStatistics(startDate, endDate, page) {
|
||||
return Vue.axios.get(
|
||||
"/admin/member/statistics?startDateStr=" + startDate +
|
||||
"&endDateStr=" + endDate + "&page=" + (page - 1) + "&size=30"
|
||||
)
|
||||
}
|
||||
|
||||
export { getStatistics }
|
87
src/api/original.js
Normal file
87
src/api/original.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
// 공통: 값 그대로 전달 (빈 문자열 유지)
|
||||
function toNullIfBlank(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim() === '' ? null : value;
|
||||
}
|
||||
return value === '' ? null : value;
|
||||
}
|
||||
|
||||
// 원작 리스트
|
||||
export async function getOriginalList(page = 1, size = 20) {
|
||||
return Vue.axios.get('/admin/chat/original/list', {
|
||||
params: { page: page - 1, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 원작 등록
|
||||
export async function createOriginal(data) {
|
||||
const formData = new FormData();
|
||||
if (data.image) formData.append('image', data.image);
|
||||
const request = {
|
||||
title: toNullIfBlank(data.title),
|
||||
contentType: toNullIfBlank(data.contentType),
|
||||
category: toNullIfBlank(data.category),
|
||||
isAdult: !!data.isAdult,
|
||||
description: toNullIfBlank(data.description),
|
||||
originalLink: toNullIfBlank(data.originalLink), // 원천 원작 링크
|
||||
originalWork: toNullIfBlank(data.originalWork),
|
||||
writer: toNullIfBlank(data.writer),
|
||||
studio: toNullIfBlank(data.studio),
|
||||
originalLinks: Array.isArray(data.originalLinks) ? data.originalLinks : [],
|
||||
tags: Array.isArray(data.tags) ? data.tags : []
|
||||
};
|
||||
formData.append('request', JSON.stringify(request));
|
||||
return Vue.axios.post('/admin/chat/original/register', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// 원작 수정
|
||||
export async function updateOriginal(data, image = null) {
|
||||
const formData = new FormData();
|
||||
if (image) formData.append('image', image);
|
||||
const processed = {};
|
||||
Object.keys(data).forEach(key => {
|
||||
processed[key] = data[key];
|
||||
})
|
||||
formData.append('request', JSON.stringify(processed));
|
||||
return Vue.axios.put('/admin/chat/original/update', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// 원작 삭제
|
||||
export async function deleteOriginal(id) {
|
||||
return Vue.axios.delete(`/admin/chat/original/${id}`)
|
||||
}
|
||||
|
||||
// 원작 상세
|
||||
export async function getOriginal(id) {
|
||||
return Vue.axios.get(`/admin/chat/original/${id}`)
|
||||
}
|
||||
|
||||
// 원작 속 캐릭터 조회
|
||||
export async function getOriginalCharacters(id, page = 1, size = 20) {
|
||||
return Vue.axios.get(`/admin/chat/original/${id}/characters`, {
|
||||
params: { page: page - 1, size }
|
||||
})
|
||||
}
|
||||
|
||||
// 원작 검색
|
||||
export async function searchOriginals(searchTerm) {
|
||||
return Vue.axios.get('/admin/chat/original/search', {
|
||||
params: { searchTerm }
|
||||
})
|
||||
}
|
||||
|
||||
// 원작에 캐릭터 연결
|
||||
export async function assignCharactersToOriginal(id, characterIds = []) {
|
||||
return Vue.axios.post(`/admin/chat/original/${id}/assign-characters`, { characterIds })
|
||||
}
|
||||
|
||||
// 원작에서 캐릭터 연결 해제
|
||||
export async function unassignCharactersFromOriginal(id, characterIds = []) {
|
||||
return Vue.axios.post(`/admin/chat/original/${id}/unassign-characters`, { characterIds })
|
||||
}
|
19
src/api/point_policy.js
Normal file
19
src/api/point_policy.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
async function getPointPolicyList(page) {
|
||||
return Vue.axios.get("/admin/point-policies?page=" + (page - 1) + "&page_size=20")
|
||||
}
|
||||
|
||||
async function createPointPolicyList(request) {
|
||||
return Vue.axios.post("/admin/point-policies", request)
|
||||
}
|
||||
|
||||
async function updatePointPolicyList(id, request) {
|
||||
return Vue.axios.put("/admin/point-policies/" + id, request)
|
||||
}
|
||||
|
||||
export {
|
||||
getPointPolicyList,
|
||||
createPointPolicyList,
|
||||
updatePointPolicyList
|
||||
}
|
@@ -43,6 +43,7 @@
|
||||
>
|
||||
<v-list-item
|
||||
:to="childItem.route"
|
||||
:exact="childItem.route === '/character'"
|
||||
active-class="blue white--text"
|
||||
>
|
||||
<v-list-item-title>{{ childItem.title }}</v-list-item-title>
|
||||
@@ -95,6 +96,39 @@ export default {
|
||||
let res = await api.getMenus();
|
||||
if (res.status === 200 && res.data.success === true && res.data.data.length > 0) {
|
||||
this.items = res.data.data
|
||||
|
||||
// 캐릭터 챗봇 메뉴 추가
|
||||
this.items.push({
|
||||
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
|
||||
},
|
||||
]
|
||||
})
|
||||
} else {
|
||||
this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!")
|
||||
this.logout();
|
||||
|
@@ -30,6 +30,11 @@ const routes = [
|
||||
name: 'MemberList',
|
||||
component: () => import(/* webpackChunkName: "member" */ '../views/Member/MemberList')
|
||||
},
|
||||
{
|
||||
path: '/member/statistics',
|
||||
name: 'MemberStatistics',
|
||||
component: () => import(/* webpackChunkName: "member" */ '../views/Member/MemberStatisticsView.vue')
|
||||
},
|
||||
{
|
||||
path: '/creator/tags',
|
||||
name: 'CreatorTags',
|
||||
@@ -80,15 +85,40 @@ const routes = [
|
||||
name: 'ContentCuration',
|
||||
component: () => import(/* webpackChunkName: "content" */ '../views/Content/ContentCuration.vue')
|
||||
},
|
||||
{
|
||||
path: '/content/curation/detail',
|
||||
name: 'ContentCurationDetail',
|
||||
component: () => import(/* webpackChunkName: "content" */ '../views/Content/ContentCurationDetail.vue')
|
||||
},
|
||||
{
|
||||
path: '/content/tag/curation',
|
||||
name: 'ContentHashTagCuration',
|
||||
component: () => import(/* webpackChunkName: "content" */ '../views/Content/ContentHashTagCuration.vue')
|
||||
},
|
||||
{
|
||||
path: '/content/tag/curation/detail',
|
||||
name: 'ContentHashTagCurationDetail',
|
||||
component: () => import(/* webpackChunkName: "content" */ '../views/Content/ContentHashTagCurationDetail.vue')
|
||||
},
|
||||
{
|
||||
path: '/content/series/list',
|
||||
name: 'ContentSeriesList',
|
||||
component: () => import(/* webpackChunkName: "content" */ '../views/Content/ContentSeriesList.vue')
|
||||
component: () => import(/* webpackChunkName: "series" */ '../views/Series/ContentSeriesList.vue')
|
||||
},
|
||||
{
|
||||
path: '/content/series/genre',
|
||||
name: 'ContentSeriesGenre',
|
||||
component: () => import(/* webpackChunkName: "content" */ '../views/Content/ContentSeriesGenre.vue')
|
||||
component: () => import(/* webpackChunkName: "series" */ '../views/Series/ContentSeriesGenre.vue')
|
||||
},
|
||||
{
|
||||
path: '/content/series/new',
|
||||
name: 'ContentSeriesNew',
|
||||
component: () => import(/* webpackChunkName: "series" */ '../views/Series/ContentSeriesNew.vue')
|
||||
},
|
||||
{
|
||||
path: '/content/series/recommend-free',
|
||||
name: 'ContentSeriesRecommendFree',
|
||||
component: () => import(/* webpackChunkName: "series" */ '../views/Series/ContentSeriesRecommendFree.vue')
|
||||
},
|
||||
{
|
||||
path: '/promotion/event',
|
||||
@@ -105,6 +135,11 @@ const routes = [
|
||||
name: 'ChargeEvent',
|
||||
component: () => import(/* webpackChunkName: "promotion" */ '../views/Promotion/ChargeEvent.vue')
|
||||
},
|
||||
{
|
||||
path: '/promotion/point-policy',
|
||||
name: 'PointPolicyView',
|
||||
component: () => import(/* webpackChunkName: "promotion" */ '../views/Promotion/PointPolicyView.vue')
|
||||
},
|
||||
{
|
||||
path: '/can/management',
|
||||
name: 'CoinView',
|
||||
@@ -197,9 +232,84 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/audition',
|
||||
name: 'AuditionListView',
|
||||
name: 'AuditionView',
|
||||
component: () => import(/* webpackChunkName: "audition" */ '../views/Audition/AuditionView.vue')
|
||||
},
|
||||
{
|
||||
path: '/audition/detail',
|
||||
name: 'AuditionDetailView',
|
||||
component: () => import(/* webpackChunkName: "audition" */ '../views/Audition/AuditionDetailView.vue')
|
||||
},
|
||||
{
|
||||
path: '/audition/role/detail',
|
||||
name: 'AuditionRoleDetailView',
|
||||
component: () => import(/* webpackChunkName: "audition" */ '../views/Audition/AuditionRoleDetailView.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing/media-partner-code',
|
||||
name: 'MarketingMediaPartnerCodeView',
|
||||
component: () => import(/* webpackChunkName: "marketing" */ '../views/Marketing/MarketingMediaPartnerCodeView.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing/ad-statistics',
|
||||
name: 'MarketingAdStatisticsView',
|
||||
component: () => import(/* webpackChunkName: "marketing" */ '../views/Marketing/MarketingAdStatisticsView.vue')
|
||||
},
|
||||
{
|
||||
path: '/character',
|
||||
name: 'CharacterList',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterList.vue')
|
||||
},
|
||||
{
|
||||
path: '/character/form',
|
||||
name: 'CharacterForm',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterForm.vue')
|
||||
},
|
||||
{
|
||||
path: '/character/banner',
|
||||
name: 'CharacterBanner',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterBanner.vue')
|
||||
},
|
||||
{
|
||||
path: '/character/images',
|
||||
name: 'CharacterImageList',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageList.vue')
|
||||
},
|
||||
{
|
||||
path: '/character/images/form',
|
||||
name: 'CharacterImageForm',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageForm.vue')
|
||||
},
|
||||
{
|
||||
path: '/character/curation',
|
||||
name: 'CharacterCuration',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCuration.vue')
|
||||
},
|
||||
{
|
||||
path: '/character/curation/detail',
|
||||
name: 'CharacterCurationDetail',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCurationDetail.vue')
|
||||
},
|
||||
{
|
||||
path: '/character/calculate',
|
||||
name: 'CharacterCalculate',
|
||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue')
|
||||
},
|
||||
{
|
||||
path: '/original-work',
|
||||
name: 'OriginalList',
|
||||
component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalList.vue')
|
||||
},
|
||||
{
|
||||
path: '/original-work/form',
|
||||
name: 'OriginalForm',
|
||||
component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalForm.vue')
|
||||
},
|
||||
{
|
||||
path: '/original-work/detail',
|
||||
name: 'OriginalDetail',
|
||||
component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalDetail.vue')
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
606
src/views/Audition/AuditionDetailView.vue
Normal file
606
src/views/Audition/AuditionDetailView.vue
Normal file
@@ -0,0 +1,606 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>{{ audition_title }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-card>
|
||||
<v-img
|
||||
:src="audition_detail.imageUrl"
|
||||
class="audition-image"
|
||||
/>
|
||||
|
||||
<v-card-text
|
||||
v-if="
|
||||
audition_detail.originalWorkUrl !== undefined &&
|
||||
audition_detail.originalWorkUrl.length > 0
|
||||
"
|
||||
>
|
||||
<a
|
||||
:href="audition_detail.originalWorkUrl"
|
||||
class="original-work-link"
|
||||
>
|
||||
원작링크
|
||||
</a>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<b>오디션 정보</b>
|
||||
<vue-show-more-text
|
||||
:text="audition_detail.information"
|
||||
:lines="3"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="8">
|
||||
<v-row>
|
||||
<v-col cols="8" />
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showWriteDialog"
|
||||
>
|
||||
배역 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="audition_role_list.length > 0">
|
||||
<v-col
|
||||
v-for="(item, i) in audition_role_list"
|
||||
:key="i"
|
||||
cols="4"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-spacer />
|
||||
<v-img
|
||||
:src="item.imageUrl"
|
||||
class="audition-image"
|
||||
@click="selectAuditionRole(item)"
|
||||
/>
|
||||
<v-spacer />
|
||||
</v-card-title>
|
||||
<v-card-text class="audition-title-container">
|
||||
{{ item.name }}
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
상태 : {{ getStatusStr(item.status) }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="showModifyDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col>
|
||||
등록된 배역이 없습니다.
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_write_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
오디션 배역 등록
|
||||
</v-card-title>
|
||||
|
||||
<div class="image-select">
|
||||
<label for="image">
|
||||
배역 이미지 등록
|
||||
</label>
|
||||
<v-file-input
|
||||
id="image"
|
||||
v-model="audition_role.image"
|
||||
accept="image/*"
|
||||
@change="imageAdd"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
v-if="audition_role.image_url"
|
||||
:src="audition_role.image_url"
|
||||
alt=""
|
||||
class="image-preview"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
배역 이름*
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="audition_role.name"
|
||||
label="배역 이름"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
오디션 배역 정보*
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-textarea
|
||||
v-model="audition_role.information"
|
||||
label="오디션 배역 정보"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
오디션 대본 URL*
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="audition_role.audition_script_url"
|
||||
label="오디션 대본 URL"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="selected_role !== null">
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
모집상태
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
align="left"
|
||||
>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<input
|
||||
id="radio_in_progress"
|
||||
v-model="audition_role.status"
|
||||
type="radio"
|
||||
value="IN_PROGRESS"
|
||||
>
|
||||
<label for="radio_in_progress"> 모집중</label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<input
|
||||
id="radio_complete"
|
||||
v-model="audition_role.status"
|
||||
type="radio"
|
||||
value="COMPLETED"
|
||||
>
|
||||
<label for="radio_complete"> 모집완료</label>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selected_role !== null"
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="modify"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="validate"
|
||||
>
|
||||
등록
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text>
|
||||
삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="deleteAuditionRole"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/audition'
|
||||
|
||||
import VueShowMoreText from 'vue-show-more-text'
|
||||
|
||||
export default {
|
||||
name: "AuditionDetailView",
|
||||
|
||||
components: {VueShowMoreText},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
audition_id: 0,
|
||||
audition_title: '',
|
||||
audition_detail: {},
|
||||
audition_role_list: [],
|
||||
|
||||
show_write_dialog: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
|
||||
audition_role: {},
|
||||
selected_role: null,
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.audition_id = this.$route.params.audition_id
|
||||
this.audition_title = this.$route.params.audition_title
|
||||
|
||||
if (this.audition_id !== undefined && this.audition_id > 0) {
|
||||
await this.getAuditionDetail()
|
||||
} else {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
getStatusStr(status) {
|
||||
if (status === 'NOT_STARTED') {
|
||||
return "모집전"
|
||||
} else if (status === 'COMPLETED') {
|
||||
return "모집완료"
|
||||
} else {
|
||||
return "모집중"
|
||||
}
|
||||
},
|
||||
|
||||
isValidUrl(string) {
|
||||
try {
|
||||
new URL(string); // URL 생성 시 예외가 발생하면 유효하지 않은 URL
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async getAuditionDetail() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getAuditionDetail(this.audition_id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const data = res.data.data
|
||||
|
||||
this.audition_title = data.title
|
||||
this.audition_detail.imageUrl = data.imageUrl
|
||||
this.audition_detail.information = data.information
|
||||
this.audition_role_list = data.roleList
|
||||
|
||||
if (this.isValidUrl(data.originalWorkUrl)) {
|
||||
this.audition_detail.originalWorkUrl = data.originalWorkUrl
|
||||
}
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
imageAdd(payload) {
|
||||
const file = payload;
|
||||
if (file) {
|
||||
this.audition_role.image_url = URL.createObjectURL(file)
|
||||
URL.revokeObjectURL(file)
|
||||
} else {
|
||||
this.audition_role.image_url = null
|
||||
}
|
||||
},
|
||||
|
||||
showWriteDialog() {
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
showModifyDialog(auditionRole) {
|
||||
this.audition_role = {
|
||||
name: auditionRole.name,
|
||||
image_url: auditionRole.imageUrl,
|
||||
information: auditionRole.information,
|
||||
audition_script_url: auditionRole.auditionScriptUrl,
|
||||
status: auditionRole.status
|
||||
}
|
||||
|
||||
this.selected_role = auditionRole
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.audition_role = {}
|
||||
this.selected_role = null
|
||||
this.show_write_dialog = false
|
||||
this.show_delete_confirm_dialog = false
|
||||
},
|
||||
|
||||
deleteConfirm(auditionRole) {
|
||||
this.selected_role = auditionRole
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (this.audition_role.image === undefined || this.audition_role.image === null) {
|
||||
this.notifyError('배역 이미지를 선택하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.audition_role.name.trim().length <= 0) {
|
||||
this.notifyError('배역 이름을 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
this.audition_role.information === undefined ||
|
||||
this.audition_role.information === null ||
|
||||
this.audition_role.information.trim().length <= 10
|
||||
) {
|
||||
this.notifyError('오디션 배역 정보를 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
this.audition_role.audition_script_url === undefined ||
|
||||
this.audition_role.audition_script_url === null ||
|
||||
this.audition_role.audition_script_url.trim().length <= 10
|
||||
) {
|
||||
this.notifyError('오디션 대본 URL을 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
this.submit()
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const request = {
|
||||
auditionId: this.audition_id,
|
||||
name: this.audition_role.name,
|
||||
information: this.audition_role.information,
|
||||
auditionScriptUrl: this.audition_role.audition_script_url
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("image", this.audition_role.image);
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
const res = await api.createAuditionRole(formData);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel();
|
||||
this.notifySuccess(res.data.message || '등록되었습니다.')
|
||||
this.is_loading = false
|
||||
|
||||
await this.getAuditionDetail()
|
||||
} else {
|
||||
this.is_loading = false
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async modify() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const request = {id: this.selected_role.id}
|
||||
if (this.audition_role.name !== this.selected_role.name) {
|
||||
request.name = this.audition_role.name
|
||||
}
|
||||
|
||||
if (this.audition_role.information !== this.selected_role.information) {
|
||||
request.information = this.audition_role.information
|
||||
}
|
||||
|
||||
if (this.audition_role.audition_script_url !== this.selected_role.audition_script_url) {
|
||||
request.auditionScriptUrl = this.audition_role.audition_script_url
|
||||
}
|
||||
|
||||
if (this.audition_role.status !== this.selected_role.status) {
|
||||
request.status = this.audition_role.status
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
if (this.audition_role.image !== undefined && this.audition_role.image !== null) {
|
||||
formData.append("image", this.audition_role.image)
|
||||
}
|
||||
|
||||
const res = await api.updateAuditionRole(formData);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel();
|
||||
this.notifySuccess(res.data.message || '등록되었습니다.')
|
||||
this.is_loading = false
|
||||
|
||||
await this.getAuditionDetail()
|
||||
} else {
|
||||
this.is_loading = false
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async deleteAuditionRole() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const request = {id: this.selected_role.id, isActive: false}
|
||||
const formData = new FormData()
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
const res = await api.updateAuditionRole(formData)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel();
|
||||
this.notifySuccess('오디션 배역이 삭제되었습니다.')
|
||||
this.is_loading = false
|
||||
|
||||
await this.getAuditionDetail()
|
||||
} else {
|
||||
this.is_loading = false
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
selectAuditionRole(auditionRole) {
|
||||
this.$router.push(
|
||||
{
|
||||
name: 'AuditionRoleDetailView',
|
||||
params: {
|
||||
audition_role_id: auditionRole.id,
|
||||
audition_title: this.audition_title
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-select label {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #232d4a;
|
||||
color: #fff;
|
||||
vertical-align: middle;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.v-file-input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
max-width: 100%;
|
||||
width: 300px;
|
||||
object-fit: cover;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.original-work-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.audition-image {
|
||||
aspect-ratio: 1000 / 530;
|
||||
}
|
||||
|
||||
.audition-title-container {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
max-height: 2em;
|
||||
}
|
||||
|
||||
.no-audition_role {
|
||||
height: 50vh;
|
||||
margin-top: 100px;
|
||||
}
|
||||
</style>
|
321
src/views/Audition/AuditionRoleDetailView.vue
Normal file
321
src/views/Audition/AuditionRoleDetailView.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>{{ audition_title }} - {{ audition_role_name }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-card>
|
||||
<v-img
|
||||
:src="audition_role_detail.imageUrl"
|
||||
class="audition-image"
|
||||
/>
|
||||
|
||||
<v-card-text
|
||||
v-if="
|
||||
audition_role_detail.auditionScriptUrl !== undefined &&
|
||||
audition_role_detail.auditionScriptUrl.length > 0
|
||||
"
|
||||
>
|
||||
<a
|
||||
:href="audition_role_detail.auditionScriptUrl"
|
||||
class="audition-script-link"
|
||||
>
|
||||
오디션 대본 링크
|
||||
</a>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<b>오디션 배역 정보</b>
|
||||
<vue-show-more-text
|
||||
:text="audition_role_detail.information"
|
||||
:lines="3"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="8">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-simple-table class="elevation-10">
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
지원 번호
|
||||
</th>
|
||||
<th class="text-center">
|
||||
프로필
|
||||
</th>
|
||||
<th class="text-center">
|
||||
닉네임
|
||||
</th>
|
||||
<th class="text-center">
|
||||
듣기
|
||||
</th>
|
||||
<th class="text-center">
|
||||
추천수
|
||||
</th>
|
||||
<th class="text-center">
|
||||
관리
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in audition_role_applicant_list"
|
||||
:key="item.applicantId"
|
||||
>
|
||||
<td>{{ item.applicantId }}</td>
|
||||
<td align="center">
|
||||
<v-img
|
||||
max-width="70"
|
||||
max-height="70"
|
||||
:src="item.profileImageUrl"
|
||||
class="rounded-circle"
|
||||
/>
|
||||
<br>
|
||||
<a
|
||||
:href="item.profileImageUrl"
|
||||
class="v-btn v-btn--outlined"
|
||||
>
|
||||
다운로드
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.nickname }}<br>{{ formatPhoneNumber(item.phoneNumber) }}</td>
|
||||
<td>
|
||||
<vuetify-audio
|
||||
:file="item.voiceUrl"
|
||||
:downloadable="true"
|
||||
:auto-play="false"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ item.voteCount }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
:disabled="is_loading"
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="next"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text>
|
||||
"{{ selected_audition_role_applicant.nickname }}"님의 오디션지원을 취소하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="deleteCancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="deleteAuditionApplicant"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/audition'
|
||||
|
||||
import VueShowMoreText from 'vue-show-more-text'
|
||||
import VuetifyAudio from "vuetify-audio";
|
||||
|
||||
export default {
|
||||
name: "AuditionDetailView",
|
||||
|
||||
components: {VuetifyAudio, VueShowMoreText},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
audition_role_id: 0,
|
||||
audition_title: '',
|
||||
audition_role_name: '',
|
||||
|
||||
audition_role_detail: {},
|
||||
audition_role_applicant_list: [],
|
||||
|
||||
selected_audition_role_applicant: {},
|
||||
show_delete_confirm_dialog: false,
|
||||
|
||||
page: 1,
|
||||
total_page: 0,
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.audition_role_id = this.$route.params.audition_role_id
|
||||
this.audition_title = this.$route.params.audition_title
|
||||
|
||||
if (this.audition_role_id !== undefined && this.audition_role_id > 0) {
|
||||
await this.getAuditionRoleDetail()
|
||||
await this.getAuditionRoleApplicant()
|
||||
} else {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
deleteConfirm(item) {
|
||||
this.selected_audition_role_applicant = item
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
deleteCancel() {
|
||||
this.selected_audition_role_applicant = {}
|
||||
this.show_delete_confirm_dialog = false
|
||||
},
|
||||
|
||||
isValidUrl(string) {
|
||||
try {
|
||||
new URL(string); // URL 생성 시 예외가 발생하면 유효하지 않은 URL
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
formatPhoneNumber(phoneNumber) {
|
||||
// 전화번호가 올바른 길이인지 확인
|
||||
if (phoneNumber.length === 11 && /^\d+$/.test(phoneNumber)) {
|
||||
// 형식을 변경하여 반환
|
||||
return `${phoneNumber.slice(0, 3)}-${phoneNumber.slice(3, 7)}-${phoneNumber.slice(7)}`;
|
||||
} else {
|
||||
return phoneNumber;
|
||||
}
|
||||
},
|
||||
|
||||
async getAuditionRoleDetail() {
|
||||
try {
|
||||
const res = await api.getAuditionRoleDetail(this.audition_role_id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const data = res.data.data
|
||||
|
||||
this.audition_role_name = data.name
|
||||
this.audition_role_detail.imageUrl = data.imageUrl
|
||||
this.audition_role_detail.information = data.information
|
||||
|
||||
if (this.isValidUrl(data.auditionScriptUrl)) {
|
||||
this.audition_role_detail.auditionScriptUrl = data.auditionScriptUrl
|
||||
}
|
||||
} else {
|
||||
this.is_loading = false
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async getAuditionRoleApplicant() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getAuditionApplicantList(this.audition_role_id, this.page)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const data = res.data.data
|
||||
const total_page = Math.ceil(data.totalCount / 20)
|
||||
this.audition_role_applicant_list = data.items
|
||||
|
||||
if (total_page <= 0) {
|
||||
this.total_page = 1
|
||||
} else {
|
||||
this.total_page = total_page
|
||||
}
|
||||
} else {
|
||||
this.is_loading = false
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async next() {
|
||||
await this.getAuditionRoleApplicant()
|
||||
},
|
||||
|
||||
async deleteAuditionApplicant() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.deleteAuditionApplicant(this.selected_audition_role_applicant.applicantId)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.notifySuccess(res.data.message || '오디션 지원이 취소 되었습니다.')
|
||||
|
||||
this.audition_role_applicant_list = []
|
||||
this.page = 1
|
||||
await this.getAuditionRoleApplicant()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
@@ -34,6 +34,7 @@
|
||||
<v-img
|
||||
:src="item.imageUrl"
|
||||
class="cover-image"
|
||||
@click="selectAudition(item)"
|
||||
/>
|
||||
<v-spacer />
|
||||
</v-card-title>
|
||||
@@ -147,29 +148,6 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
마감날짜
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
align="left"
|
||||
>
|
||||
<DatePicker
|
||||
v-model="audition.end_date"
|
||||
value-type="format"
|
||||
/>
|
||||
|
||||
<vue-timepicker
|
||||
v-model="audition.end_time"
|
||||
format="HH:mm"
|
||||
input-class="skip-error-style"
|
||||
:minute-interval="15"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
@@ -290,17 +268,10 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/audition'
|
||||
import VueTimepicker from 'vue2-timepicker'
|
||||
import DatePicker from 'vue2-datepicker';
|
||||
|
||||
import 'vue2-datepicker/index.css';
|
||||
import 'vue2-timepicker/dist/VueTimepicker.css'
|
||||
|
||||
export default {
|
||||
name: "AuditionView",
|
||||
|
||||
components: {VueTimepicker, DatePicker},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
@@ -311,8 +282,6 @@ export default {
|
||||
title: '',
|
||||
information: '',
|
||||
image_url: '',
|
||||
end_date: '',
|
||||
end_time: '',
|
||||
is_adult: false,
|
||||
original_work_url: ''
|
||||
},
|
||||
@@ -372,12 +341,6 @@ export default {
|
||||
status: audition.status,
|
||||
}
|
||||
|
||||
if (audition.endDate !== undefined && audition.endDate !== null) {
|
||||
const splitEndDate = audition.endDate.split(" ")
|
||||
this.audition.end_date = splitEndDate[0]
|
||||
this.audition.end_time = splitEndDate[1]
|
||||
}
|
||||
|
||||
this.selected_audition = audition
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
@@ -387,8 +350,6 @@ export default {
|
||||
title: '',
|
||||
information: '',
|
||||
image_url: '',
|
||||
end_date: '',
|
||||
end_time: '',
|
||||
is_adult: false,
|
||||
original_work_url: ''
|
||||
}
|
||||
@@ -436,10 +397,6 @@ export default {
|
||||
isAdult: this.audition.is_adult
|
||||
}
|
||||
|
||||
if (this.audition.end_date !== '' && this.audition.end_time !== '') {
|
||||
request.endDateString = this.audition.end_date + ' ' + this.audition.end_time
|
||||
}
|
||||
|
||||
if (this.audition.original_work_url !== '') {
|
||||
request.originalWorkUrl = this.audition.original_work_url
|
||||
}
|
||||
@@ -467,6 +424,17 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
selectAudition(audition) {
|
||||
this.$router.push(
|
||||
{
|
||||
name: 'AuditionDetailView',
|
||||
params: {
|
||||
audition_id: audition.id, audition_title: audition.title
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
async next() {
|
||||
await this.getAuditionList()
|
||||
},
|
||||
@@ -524,11 +492,6 @@ export default {
|
||||
request.status = this.audition.status
|
||||
}
|
||||
|
||||
const splitEndDate = this.selected_audition.endDate.split(" ")
|
||||
if (this.audition.end_date !== splitEndDate[0] || this.audition.end_time !== splitEndDate[1]) {
|
||||
request.endDateString = this.audition.end_date + ' ' + this.audition.end_time
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
|
@@ -60,7 +60,7 @@
|
||||
<v-card-text>
|
||||
지급할 캔 수: {{ can }} 캔
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!isLoading">
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
show_confirm: false,
|
||||
isLoading: false,
|
||||
is_loading: false,
|
||||
account_id: '',
|
||||
method: '',
|
||||
can: ''
|
||||
@@ -124,7 +124,7 @@ export default {
|
||||
return this.notifyError('캔은 숫자만 넣을 수 있습니다.')
|
||||
}
|
||||
|
||||
if (!this.isLoading) {
|
||||
if (!this.is_loading) {
|
||||
this.show_confirm = true
|
||||
}
|
||||
},
|
||||
@@ -134,8 +134,8 @@ export default {
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (!this.isLoading) {
|
||||
this.isLoading = true
|
||||
if (!this.is_loading) {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
this.show_confirm = false
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#9970ff"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showWriteDialog"
|
||||
@@ -42,6 +42,10 @@
|
||||
<span @click="getCouponNumberList(item)">{{ item.couponName }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.couponType="{ item }">
|
||||
{{ item.couponType }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.can="{ item }">
|
||||
{{ item.can.toLocaleString('en-US') }}
|
||||
</template>
|
||||
@@ -132,6 +136,38 @@
|
||||
label="발행수량"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
쿠폰종류
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="datepicker-wrapper"
|
||||
>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<input
|
||||
id="can_coupon"
|
||||
v-model="coupon_type"
|
||||
type="radio"
|
||||
value="CAN"
|
||||
>
|
||||
<label for="can_coupon"> 캔 쿠폰</label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<input
|
||||
id="point_coupon"
|
||||
v-model="coupon_type"
|
||||
type="radio"
|
||||
value="POINT"
|
||||
>
|
||||
<label for="point_coupon"> 포인트 쿠폰</label>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
@@ -373,6 +409,7 @@ export default {
|
||||
is_active: null,
|
||||
is_multiple_use: false,
|
||||
coupon_number_count: null,
|
||||
coupon_type: 'CAN',
|
||||
|
||||
page: 1,
|
||||
total_page: 0,
|
||||
@@ -414,6 +451,12 @@ export default {
|
||||
sortable: false,
|
||||
value: 'couponName',
|
||||
},
|
||||
{
|
||||
text: '쿠폰종류',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'couponType',
|
||||
},
|
||||
{
|
||||
text: '쿠폰금액',
|
||||
align: 'center',
|
||||
@@ -542,6 +585,7 @@ export default {
|
||||
this.is_active = null
|
||||
this.is_multiple_use = false
|
||||
this.coupon_number_count = null
|
||||
this.coupon_type = 'CAN'
|
||||
},
|
||||
|
||||
showModifyDialog(value) {
|
||||
@@ -628,6 +672,7 @@ export default {
|
||||
try {
|
||||
const res = await api.generateCoupon(
|
||||
this.coupon_name,
|
||||
this.coupon_type,
|
||||
this.can,
|
||||
this.validity,
|
||||
this.is_multiple_use,
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#9970ff"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
v-bind="attrs"
|
||||
@@ -39,16 +39,16 @@
|
||||
class="elevation-1"
|
||||
hide-default-footer
|
||||
>
|
||||
<template v-slot:item.price="{ item }">
|
||||
{{ item.price.toLocaleString('en-US') }} 원
|
||||
<template v-slot:item.priceStr="{ item }">
|
||||
{{ formatMoney(item.priceStr, item.currency) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.can="{ item }">
|
||||
{{ item.can.toLocaleString('en-US') }} 캔
|
||||
{{ formatNumber(item.can) }} 캔
|
||||
</template>
|
||||
|
||||
<template v-slot:item.rewardCan="{ item }">
|
||||
{{ item.rewardCan.toLocaleString('en-US') }} 캔
|
||||
{{ formatNumber(item.rewardCan) }} 캔
|
||||
</template>
|
||||
|
||||
<template v-slot:item.management="{ item }">
|
||||
@@ -70,7 +70,13 @@
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="price"
|
||||
label="원화"
|
||||
label="가격"
|
||||
required
|
||||
/>
|
||||
<v-select
|
||||
v-model="currency"
|
||||
:items="currencies"
|
||||
label="화폐 단위"
|
||||
required
|
||||
/>
|
||||
</v-card-text>
|
||||
@@ -125,12 +131,17 @@ export default {
|
||||
price: null,
|
||||
can: null,
|
||||
reward_can: null,
|
||||
currency: 'KRW',
|
||||
currencies: [
|
||||
{ text: 'KRW (한국 원)', value: 'KRW' },
|
||||
{ text: 'USD (미국 달러)', value: 'USD' }
|
||||
],
|
||||
headers: [
|
||||
{
|
||||
text: '원화(VAT포함)',
|
||||
text: '가격(VAT포함)',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'price',
|
||||
value: 'priceStr',
|
||||
},
|
||||
{
|
||||
text: '충전캔',
|
||||
@@ -173,9 +184,26 @@ export default {
|
||||
this.can = null
|
||||
this.price = null
|
||||
this.reward_can = null
|
||||
this.currency = 'KRW'
|
||||
this.selected_can = null
|
||||
},
|
||||
|
||||
formatMoney(priceStr, currencyCode, locale = navigator.language) {
|
||||
const price = Number(priceStr);
|
||||
const formatted = new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currencyCode
|
||||
}).format(price);
|
||||
|
||||
return formatted.replace(/([^\d\s])(\d)/, '$1 $2');
|
||||
},
|
||||
|
||||
formatNumber(num) {
|
||||
return new Intl.NumberFormat(navigator.language, {
|
||||
style: 'decimal'
|
||||
}).format(num);
|
||||
},
|
||||
|
||||
async getCans() {
|
||||
this.isLoading = true
|
||||
try {
|
||||
@@ -204,13 +232,14 @@ export default {
|
||||
async submit() {
|
||||
this.isLoading = true
|
||||
|
||||
const res = await api.insertCan(this.can, this.reward_can, this.price)
|
||||
const res = await api.insertCan(this.can, this.reward_can, this.price, this.currency)
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.show_dialog = false
|
||||
this.can = null
|
||||
this.price = null
|
||||
this.reward_can = null
|
||||
this.currency = 'KRW'
|
||||
this.selected_can = null
|
||||
this.notifySuccess(res.data.message || '등록되었습니다.')
|
||||
|
||||
|
@@ -36,7 +36,7 @@
|
||||
<v-col cols="2">
|
||||
<v-btn
|
||||
block
|
||||
color="#9970ff"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="getChargeStatus"
|
||||
|
583
src/views/Chat/CharacterBanner.vue
Normal file
583
src/views/Chat/CharacterBanner.vue
Normal file
@@ -0,0 +1,583 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-btn
|
||||
icon
|
||||
@click="goBack"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>캐릭터 배너 관리</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
dark
|
||||
@click="showAddDialog"
|
||||
>
|
||||
배너 추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
</v-row>
|
||||
|
||||
<!-- 로딩 표시 -->
|
||||
<v-row v-if="isLoading && banners.length === 0">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 배너 그리드 -->
|
||||
<v-row>
|
||||
<draggable
|
||||
v-model="banners"
|
||||
class="row"
|
||||
style="width: 100%"
|
||||
:options="{ animation: 150 }"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<v-col
|
||||
v-for="banner in banners"
|
||||
:key="banner.id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
class="banner-item"
|
||||
>
|
||||
<v-card
|
||||
class="mx-auto"
|
||||
max-width="300"
|
||||
>
|
||||
<v-img
|
||||
:src="banner.imagePath"
|
||||
height="200"
|
||||
contain
|
||||
/>
|
||||
<v-card-text class="text-center">
|
||||
<div>{{ banner.characterName }}</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
@click="showEditDialog(banner)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="confirmDelete(banner)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</draggable>
|
||||
</v-row>
|
||||
|
||||
<!-- 데이터가 없을 때 표시 -->
|
||||
<v-row v-if="!isLoading && banners.length === 0">
|
||||
<v-col class="text-center">
|
||||
<p>등록된 배너가 없습니다.</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 무한 스크롤 로딩 -->
|
||||
<v-row v-if="isLoading && banners.length > 0">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- 배너 추가/수정 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
max-width="600px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ isEdit ? '배너 수정' : '배너 추가' }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-file-input
|
||||
v-model="bannerForm.image"
|
||||
label="배너 이미지"
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-camera"
|
||||
show-size
|
||||
truncate-length="15"
|
||||
:rules="imageRules"
|
||||
outlined
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="previewImage || (isEdit && bannerForm.imageUrl)">
|
||||
<v-col
|
||||
cols="12"
|
||||
class="text-center"
|
||||
>
|
||||
<v-img
|
||||
:src="previewImage || bannerForm.imageUrl"
|
||||
max-height="200"
|
||||
contain
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="searchKeyword"
|
||||
label="캐릭터 검색"
|
||||
outlined
|
||||
@keyup.enter="searchCharacter"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="searchResults.length > 0">
|
||||
<v-col cols="12">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="character in searchResults"
|
||||
:key="character.id"
|
||||
@click="selectCharacter(character)"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="character.imageUrl" />
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ character.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="searchPerformed && searchResults.length === 0">
|
||||
<v-col cols="12">
|
||||
<v-alert
|
||||
type="info"
|
||||
outlined
|
||||
>
|
||||
검색결과가 없습니다.
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="selectedCharacter">
|
||||
<v-col cols="12">
|
||||
<v-alert
|
||||
type="info"
|
||||
outlined
|
||||
>
|
||||
<v-row align="center">
|
||||
<v-col cols="auto">
|
||||
<v-avatar size="50">
|
||||
<v-img :src="selectedCharacter.imageUrl" />
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<div class="font-weight-medium">
|
||||
선택된 캐릭터: {{ selectedCharacter.name }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="closeDialog"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
:disabled="!isFormValid || isSubmitting"
|
||||
:loading="isSubmitting"
|
||||
@click="saveBanner"
|
||||
>
|
||||
저장
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showDeleteDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
배너 삭제
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
삭제 할까요?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="showDeleteDialog = false"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red darken-1"
|
||||
text
|
||||
:loading="isSubmitting"
|
||||
@click="deleteBanner"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getCharacterBannerList,
|
||||
createCharacterBanner,
|
||||
updateCharacterBanner,
|
||||
deleteCharacterBanner,
|
||||
updateCharacterBannerOrder,
|
||||
searchCharacters
|
||||
} from '@/api/character';
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
export default {
|
||||
name: 'CharacterBanner',
|
||||
|
||||
components: {
|
||||
draggable
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
banners: [],
|
||||
page: 1,
|
||||
hasMoreItems: true,
|
||||
showDialog: false,
|
||||
showDeleteDialog: false,
|
||||
isEdit: false,
|
||||
selectedBanner: null,
|
||||
selectedCharacter: null,
|
||||
searchKeyword: '',
|
||||
searchResults: [],
|
||||
searchPerformed: false,
|
||||
previewImage: null,
|
||||
bannerForm: {
|
||||
image: null,
|
||||
imageUrl: '',
|
||||
characterId: null,
|
||||
bannerId: null
|
||||
},
|
||||
imageRules: [
|
||||
v => !!v || this.isEdit || '이미지를 선택하세요'
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return (this.bannerForm.image || (this.isEdit && this.bannerForm.imageUrl)) && this.selectedCharacter;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'bannerForm.image': {
|
||||
handler(newImage) {
|
||||
if (newImage) {
|
||||
this.createImagePreview(newImage);
|
||||
} else {
|
||||
this.previewImage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadBanners();
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message);
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message);
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$router.push('/character');
|
||||
},
|
||||
|
||||
async loadBanners() {
|
||||
if (this.isLoading || !this.hasMoreItems) return;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await getCharacterBannerList(this.page);
|
||||
|
||||
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||
const data = response.data.data;
|
||||
const newBanners = data.content || [];
|
||||
this.banners = [...this.banners, ...newBanners];
|
||||
|
||||
// 더 불러올 데이터가 있는지 확인
|
||||
this.hasMoreItems = newBanners.length > 0;
|
||||
this.page++;
|
||||
} else {
|
||||
this.notifyError('배너 목록을 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
this.notifyError('배너 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
const scrollPosition = window.innerHeight + window.scrollY;
|
||||
const documentHeight = document.documentElement.offsetHeight;
|
||||
|
||||
// 스크롤이 페이지 하단에 도달하면 추가 데이터 로드
|
||||
if (scrollPosition >= documentHeight - 200 && !this.isLoading && this.hasMoreItems) {
|
||||
this.loadBanners();
|
||||
}
|
||||
},
|
||||
|
||||
showAddDialog() {
|
||||
this.isEdit = false;
|
||||
this.selectedCharacter = null;
|
||||
this.bannerForm = {
|
||||
image: null,
|
||||
imageUrl: '',
|
||||
characterId: null,
|
||||
bannerId: null
|
||||
};
|
||||
this.previewImage = null;
|
||||
this.searchKeyword = '';
|
||||
this.searchResults = [];
|
||||
this.searchPerformed = false;
|
||||
this.showDialog = true;
|
||||
},
|
||||
|
||||
showEditDialog(banner) {
|
||||
this.isEdit = true;
|
||||
this.selectedBanner = banner;
|
||||
this.selectedCharacter = {
|
||||
id: banner.characterId,
|
||||
name: banner.characterName,
|
||||
imageUrl: banner.characterImageUrl
|
||||
};
|
||||
this.bannerForm = {
|
||||
image: null,
|
||||
imageUrl: banner.imageUrl,
|
||||
characterId: banner.characterId,
|
||||
bannerId: banner.id
|
||||
};
|
||||
this.previewImage = null;
|
||||
this.searchKeyword = '';
|
||||
this.searchResults = [];
|
||||
this.searchPerformed = false;
|
||||
this.showDialog = true;
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
this.selectedCharacter = null;
|
||||
this.bannerForm = {
|
||||
image: null,
|
||||
imageUrl: '',
|
||||
characterId: null,
|
||||
bannerId: null
|
||||
};
|
||||
this.previewImage = null;
|
||||
this.searchKeyword = '';
|
||||
this.searchResults = [];
|
||||
this.searchPerformed = false;
|
||||
},
|
||||
|
||||
confirmDelete(banner) {
|
||||
this.selectedBanner = banner;
|
||||
this.showDeleteDialog = true;
|
||||
},
|
||||
|
||||
createImagePreview(file) {
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.previewImage = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
|
||||
async searchCharacter() {
|
||||
if (!this.searchKeyword || this.searchKeyword.length < 2) {
|
||||
this.notifyError('검색어를 2글자 이상 입력하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await searchCharacters(this.searchKeyword);
|
||||
|
||||
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||
const data = response.data.data;
|
||||
this.searchResults = data.content || [];
|
||||
this.searchPerformed = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('캐릭터 검색 오류:', error);
|
||||
this.notifyError('캐릭터 검색에 실패했습니다.');
|
||||
}
|
||||
},
|
||||
|
||||
selectCharacter(character) {
|
||||
this.selectedCharacter = character;
|
||||
this.bannerForm.characterId = character.id;
|
||||
this.searchResults = [];
|
||||
},
|
||||
|
||||
async saveBanner() {
|
||||
if (!this.isFormValid || this.isSubmitting) return;
|
||||
|
||||
this.isSubmitting = true;
|
||||
|
||||
try {
|
||||
if (this.isEdit) {
|
||||
// 배너 수정
|
||||
const response = await updateCharacterBanner({
|
||||
image: this.bannerForm.image,
|
||||
characterId: this.selectedCharacter.id,
|
||||
bannerId: this.bannerForm.bannerId
|
||||
});
|
||||
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||
this.notifySuccess('배너가 수정되었습니다.');
|
||||
} else {
|
||||
this.notifyError('배너 수정을 실패했습니다.');
|
||||
}
|
||||
} else {
|
||||
// 배너 추가
|
||||
const response = await createCharacterBanner({
|
||||
image: this.bannerForm.image,
|
||||
characterId: this.selectedCharacter.id
|
||||
});
|
||||
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||
this.notifySuccess('배너가 추가되었습니다.');
|
||||
// 다이얼로그 닫고 배너 목록 새로고침
|
||||
this.closeDialog();
|
||||
this.refreshBanners();
|
||||
} else {
|
||||
this.notifyError('배너 추가를 실패했습니다.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('배너 저장 오류:', error);
|
||||
this.notifyError('배너 저장에 실패했습니다.');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteBanner() {
|
||||
if (!this.selectedBanner || this.isSubmitting) return;
|
||||
|
||||
this.isSubmitting = true;
|
||||
|
||||
try {
|
||||
const response = await deleteCharacterBanner(this.selectedBanner.id);
|
||||
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||
this.notifySuccess('배너가 삭제되었습니다.');
|
||||
this.showDeleteDialog = false;
|
||||
this.refreshBanners();
|
||||
} else {
|
||||
this.notifyError('배너 삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('배너 삭제 오류:', error);
|
||||
this.notifyError('배너 삭제에 실패했습니다.');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
|
||||
refreshBanners() {
|
||||
// 배너 목록 초기화 후 다시 로드
|
||||
this.banners = [];
|
||||
this.page = 1;
|
||||
this.hasMoreItems = true;
|
||||
this.loadBanners();
|
||||
},
|
||||
|
||||
async onDragEnd() {
|
||||
// 드래그 앤 드롭으로 순서 변경 후 API 호출
|
||||
try {
|
||||
const bannerIds = this.banners.map(banner => banner.id);
|
||||
const response = await updateCharacterBannerOrder(bannerIds);
|
||||
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||
this.notifySuccess('배너 순서가 변경되었습니다.');
|
||||
} else {
|
||||
this.notifyError('배너 순서 변경에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('배너 순서 변경 오류:', error);
|
||||
this.notifyError('배너 순서 변경에 실패했습니다.');
|
||||
// 실패 시 목록 새로고침
|
||||
this.refreshBanners();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.banner-item {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.banner-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
315
src/views/Chat/CharacterCalculateList.vue
Normal file
315
src/views/Chat/CharacterCalculateList.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>캐릭터 정산</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row class="justify-center align-center text-center">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<v-menu
|
||||
ref="menuStart"
|
||||
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-model="filters.startDateStr"
|
||||
label="시작일"
|
||||
readonly
|
||||
dense
|
||||
v-bind="attrs"
|
||||
clearable
|
||||
v-on="on"
|
||||
/>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="filters.startDateStr"
|
||||
:max="filters.endDateStr && filters.endDateStr < todayStr ? filters.endDateStr : todayStr"
|
||||
locale="ko-kr"
|
||||
@input="$refs.menuStart.save(filters.startDateStr)"
|
||||
/>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<v-menu
|
||||
ref="menuEnd"
|
||||
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-model="filters.endDateStr"
|
||||
label="종료일"
|
||||
readonly
|
||||
dense
|
||||
v-bind="attrs"
|
||||
clearable
|
||||
v-on="on"
|
||||
/>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="filters.endDateStr"
|
||||
:min="filters.startDateStr"
|
||||
:max="todayStr"
|
||||
locale="ko-kr"
|
||||
@input="$refs.menuEnd.save(filters.endDateStr)"
|
||||
/>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<v-select
|
||||
v-model="filters.sort"
|
||||
:items="sortItems"
|
||||
label="정렬"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
class="d-flex justify-center align-center"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
small
|
||||
:loading="is_loading"
|
||||
@click="fetchList(1)"
|
||||
>
|
||||
조회
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
text
|
||||
small
|
||||
@click="resetFilters"
|
||||
>
|
||||
초기화
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-simple-table class="elevation-10 text-center">
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
이미지
|
||||
</th>
|
||||
<th class="text-center">
|
||||
캐릭터명
|
||||
</th>
|
||||
<th class="text-center">
|
||||
이미지 단독 구매(캔)
|
||||
</th>
|
||||
<th class="text-center">
|
||||
메시지 구매(캔)
|
||||
</th>
|
||||
<th class="text-center">
|
||||
채팅 횟수 구매(캔)
|
||||
</th>
|
||||
<th class="text-center">
|
||||
합계(캔)
|
||||
</th>
|
||||
<th class="text-center">
|
||||
합계(원화)
|
||||
</th>
|
||||
<th class="text-center">
|
||||
정산금액(원)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in items"
|
||||
:key="item.characterId"
|
||||
>
|
||||
<td align="center">
|
||||
<v-img
|
||||
:src="item.characterImage"
|
||||
max-width="64"
|
||||
max-height="64"
|
||||
class="rounded-circle"
|
||||
contain
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.name }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ formatNumber(item.imagePurchaseCan) }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ formatNumber(item.messagePurchaseCan) }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ formatNumber(item.quotaPurchaseCan) }}
|
||||
</td>
|
||||
<td class="text-center font-weight-bold">
|
||||
{{ formatNumber(item.totalCan) }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ formatCurrency(item.totalKrw) }}
|
||||
</td>
|
||||
<td class="text-center font-weight-bold">
|
||||
{{ formatCurrency(item.settlementKrw) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!is_loading && items.length === 0">
|
||||
<td
|
||||
colspan="7"
|
||||
class="text-center grey--text"
|
||||
>
|
||||
데이터가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="onPageChange"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCharacterCalculateList } from "@/api/character";
|
||||
|
||||
function formatDate(date) {
|
||||
const pad = (n) => n.toString().padStart(2, "0");
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "CharacterCalculateList",
|
||||
data() {
|
||||
const today = new Date();
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(today.getDate() - 7);
|
||||
|
||||
return {
|
||||
is_loading: false,
|
||||
menuStart: false,
|
||||
menuEnd: false,
|
||||
todayStr: formatDate(today),
|
||||
page: 1,
|
||||
size: 30,
|
||||
total_page: 1,
|
||||
total_count: 0,
|
||||
items: [],
|
||||
sortItems: [
|
||||
{ text: "매출순", value: "TOTAL_SALES_DESC" },
|
||||
{ text: "최신캐릭터순", value: "LATEST_DESC" }
|
||||
],
|
||||
filters: {
|
||||
startDateStr: formatDate(new Date(today.getFullYear(), today.getMonth(), 1)),
|
||||
endDateStr: formatDate(today),
|
||||
sort: "TOTAL_SALES_DESC"
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchList(1);
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog && this.$dialog.notify ? this.$dialog.notify.error(message) : alert(message);
|
||||
},
|
||||
onPageChange() {
|
||||
this.fetchList(this.page);
|
||||
},
|
||||
resetFilters() {
|
||||
const today = new Date();
|
||||
// 이번 달 1일로 시작일 설정
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
this.filters.startDateStr = formatDate(firstDay);
|
||||
// 종료일은 오늘
|
||||
this.filters.endDateStr = formatDate(today);
|
||||
this.filters.sort = "TOTAL_SALES_DESC";
|
||||
// 페이지를 1로 리셋하고 목록 조회
|
||||
this.fetchList(1);
|
||||
},
|
||||
async fetchList(page = 1) {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true;
|
||||
try {
|
||||
const params = {
|
||||
startDateStr: this.filters.startDateStr || null,
|
||||
endDateStr: this.filters.endDateStr || null,
|
||||
sort: this.filters.sort,
|
||||
page: (page - 1),
|
||||
size: this.size
|
||||
};
|
||||
const res = await getCharacterCalculateList(params);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data && res.data.data ? res.data.data : res.data;
|
||||
if (data) {
|
||||
this.total_count = data.totalCount || 0;
|
||||
this.items = data.items || [];
|
||||
const totalPage = Math.ceil(this.total_count / this.size);
|
||||
this.total_page = totalPage > 0 ? totalPage : 1;
|
||||
this.page = page;
|
||||
} else {
|
||||
this.items = [];
|
||||
this.total_count = 0;
|
||||
this.total_page = 1;
|
||||
}
|
||||
} else {
|
||||
this.notifyError("목록 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("정산 목록 조회 오류:", e);
|
||||
this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.");
|
||||
} finally {
|
||||
this.is_loading = false;
|
||||
}
|
||||
},
|
||||
formatNumber(n) {
|
||||
const num = Number(n || 0);
|
||||
return num.toLocaleString("ko-KR");
|
||||
},
|
||||
formatCurrency(n) {
|
||||
const num = Number(n || 0);
|
||||
return num.toLocaleString("ko-KR");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-simple-table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
341
src/views/Chat/CharacterCuration.vue
Normal file
341
src/views/Chat/CharacterCuration.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>캐릭터 큐레이션</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row class="mb-4">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
dark
|
||||
@click="showWriteDialog"
|
||||
>
|
||||
큐레이션 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="curations"
|
||||
:loading="isLoading"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
hide-default-footer
|
||||
disable-pagination
|
||||
>
|
||||
<template v-slot:body="props">
|
||||
<draggable
|
||||
v-model="props.items"
|
||||
tag="tbody"
|
||||
@end="onDragEnd(props.items)"
|
||||
>
|
||||
<tr
|
||||
v-for="item in props.items"
|
||||
:key="item.id"
|
||||
>
|
||||
<td @click="goDetail(item)">
|
||||
{{ item.title }}
|
||||
</td>
|
||||
<td @click="goDetail(item)">
|
||||
<h3 v-if="item.isAdult">
|
||||
O
|
||||
</h3>
|
||||
<h3 v-else>
|
||||
X
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
:disabled="isLoading"
|
||||
@click="showModifyDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col class="text-center">
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
:disabled="isLoading"
|
||||
@click="confirmDelete(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- 등록/수정 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
max-width="600px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ isModify ? '큐레이션 수정' : '큐레이션 등록' }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.title"
|
||||
label="제목"
|
||||
outlined
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-checkbox
|
||||
v-model="form.isAdult"
|
||||
label="19금"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="closeDialog"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
:loading="isSubmitting"
|
||||
:disabled="!isFormValid || isSubmitting"
|
||||
@click="saveCuration"
|
||||
>
|
||||
저장
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showDeleteDialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
큐레이션 삭제
|
||||
</v-card-title>
|
||||
<v-card-text>"{{ selectedCuration && selectedCuration.title }}"을(를) 삭제하시겠습니까?</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="showDeleteDialog = false"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="red darken-1"
|
||||
:loading="isSubmitting"
|
||||
@click="deleteCuration"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable';
|
||||
import {
|
||||
getCharacterCurationList,
|
||||
createCharacterCuration,
|
||||
updateCharacterCuration,
|
||||
deleteCharacterCuration,
|
||||
updateCharacterCurationOrder
|
||||
} from '@/api/character';
|
||||
|
||||
export default {
|
||||
name: 'CharacterCuration',
|
||||
components: { draggable },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
curations: [],
|
||||
headers: [
|
||||
{ text: '제목', align: 'center', sortable: false, value: 'title' },
|
||||
{ text: '19금', align: 'center', sortable: false, value: 'isAdult' },
|
||||
{ text: '관리', align: 'center', sortable: false, value: 'management' }
|
||||
],
|
||||
showDialog: false,
|
||||
isModify: false,
|
||||
form: { id: null, title: '', isAdult: false },
|
||||
selectedCuration: null,
|
||||
showDeleteDialog: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return this.form.title && this.form.title.trim().length > 0;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadCurations();
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) { this.$dialog.notify.error(message); },
|
||||
notifySuccess(message) { this.$dialog.notify.success(message); },
|
||||
|
||||
async loadCurations() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await getCharacterCurationList();
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.curations = res.data.data || [];
|
||||
} else {
|
||||
this.notifyError(res.data.message || '목록을 불러오지 못했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('목록을 불러오지 못했습니다.');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
onDragEnd(items) {
|
||||
const ids = items.map(i => i.id);
|
||||
this.updateOrders(ids);
|
||||
},
|
||||
|
||||
async updateOrders(ids) {
|
||||
try {
|
||||
const res = await updateCharacterCurationOrder(ids);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.notifySuccess('순서가 변경되었습니다.');
|
||||
} else {
|
||||
this.notifyError(res.data.message || '순서 변경에 실패했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('순서 변경에 실패했습니다.');
|
||||
}
|
||||
},
|
||||
|
||||
goDetail(item) {
|
||||
this.$router.push({
|
||||
name: 'CharacterCurationDetail',
|
||||
params: { curationId: item.id, title: item.title, isAdult: item.isAdult }
|
||||
});
|
||||
},
|
||||
|
||||
showWriteDialog() {
|
||||
this.isModify = false;
|
||||
this.form = { id: null, title: '', isAdult: false };
|
||||
this.showDialog = true;
|
||||
},
|
||||
|
||||
showModifyDialog(item) {
|
||||
this.isModify = true;
|
||||
this.form = { id: item.id, title: item.title, isAdult: item.isAdult };
|
||||
this.showDialog = true;
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
this.form = { id: null, title: '', isAdult: false };
|
||||
},
|
||||
|
||||
async saveCuration() {
|
||||
if (this.isSubmitting || !this.isFormValid) return;
|
||||
this.isSubmitting = true;
|
||||
try {
|
||||
if (this.isModify) {
|
||||
const payload = { id: this.form.id };
|
||||
if (this.form.title) payload.title = this.form.title;
|
||||
payload.isAdult = this.form.isAdult;
|
||||
const res = await updateCharacterCuration(payload);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.notifySuccess('수정되었습니다.');
|
||||
this.closeDialog();
|
||||
await this.loadCurations();
|
||||
} else {
|
||||
this.notifyError(res.data.message || '수정에 실패했습니다.');
|
||||
}
|
||||
} else {
|
||||
const res = await createCharacterCuration({
|
||||
title: this.form.title,
|
||||
isAdult: this.form.isAdult,
|
||||
isActive: true
|
||||
});
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.notifySuccess('등록되었습니다.');
|
||||
this.closeDialog();
|
||||
await this.loadCurations();
|
||||
} else {
|
||||
this.notifyError(res.data.message || '등록에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete(item) {
|
||||
this.selectedCuration = item;
|
||||
this.showDeleteDialog = true;
|
||||
},
|
||||
|
||||
async deleteCuration() {
|
||||
if (!this.selectedCuration) return;
|
||||
this.isSubmitting = true;
|
||||
try {
|
||||
const res = await deleteCharacterCuration(this.selectedCuration.id);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.notifySuccess('삭제되었습니다.');
|
||||
this.showDeleteDialog = false;
|
||||
await this.loadCurations();
|
||||
} else {
|
||||
this.notifyError(res.data.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('삭제에 실패했습니다.');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
429
src/views/Chat/CharacterCurationDetail.vue
Normal file
429
src/views/Chat/CharacterCurationDetail.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-btn
|
||||
icon
|
||||
@click="goBack"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row class="mb-2">
|
||||
<v-col
|
||||
cols="4"
|
||||
class="text-right"
|
||||
>
|
||||
19금 :
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
{{ isAdult ? 'O' : 'X' }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="mb-4">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
dark
|
||||
@click="openAddDialog"
|
||||
>
|
||||
캐릭터 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<draggable
|
||||
v-model="characters"
|
||||
class="row"
|
||||
style="width: 100%"
|
||||
:options="{ animation: 150 }"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<v-col
|
||||
v-for="ch in characters"
|
||||
:key="ch.id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-card>
|
||||
<v-img
|
||||
:src="ch.imageUrl"
|
||||
height="200"
|
||||
contain
|
||||
/>
|
||||
<v-card-text class="text-center">
|
||||
{{ ch.name }}
|
||||
</v-card-text>
|
||||
<v-card-text class="text-center">
|
||||
{{ ch.description }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="confirmRemove(ch)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</draggable>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="isLoading && characters.length === 0">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="48"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="!isLoading && characters.length === 0">
|
||||
<v-col class="text-center">
|
||||
등록된 캐릭터가 없습니다.
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- 등록 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showAddDialog"
|
||||
max-width="700px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>캐릭터 등록</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="searchWord"
|
||||
label="캐릭터 검색"
|
||||
outlined
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
small
|
||||
class="mb-2"
|
||||
@click="search"
|
||||
>
|
||||
검색
|
||||
</v-btn>
|
||||
|
||||
<v-row v-if="searchResults.length > 0 || addList.length > 0">
|
||||
<v-col>
|
||||
검색결과
|
||||
<v-simple-table>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
이름
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in searchResults"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
@click="addItem(item)"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col v-if="addList.length > 0">
|
||||
추가할 캐릭터
|
||||
<v-simple-table>
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
이름
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in addList"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="removeItem(item)"
|
||||
>
|
||||
제거
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-alert
|
||||
v-else-if="searchPerformed"
|
||||
type="info"
|
||||
outlined
|
||||
>
|
||||
검색결과가 없습니다.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="closeAddDialog"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
:disabled="addList.length === 0 || isSubmitting"
|
||||
:loading="isSubmitting"
|
||||
@click="addItemInCuration"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showDeleteDialog"
|
||||
max-width="420px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
캐릭터 삭제
|
||||
</v-card-title>
|
||||
<v-card-text>"{{ targetCharacter && targetCharacter.name }}"을(를) 큐레이션에서 삭제할까요?</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="showDeleteDialog = false"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="red darken-1"
|
||||
:loading="isSubmitting"
|
||||
@click="removeTarget"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable';
|
||||
import {
|
||||
getCharactersInCuration,
|
||||
addCharacterToCuration,
|
||||
removeCharacterFromCuration,
|
||||
updateCurationCharactersOrder,
|
||||
searchCharacters
|
||||
} from '@/api/character';
|
||||
|
||||
export default {
|
||||
name: 'CharacterCurationDetail',
|
||||
components: { draggable },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
curationId: null,
|
||||
title: '',
|
||||
isAdult: false,
|
||||
characters: [],
|
||||
showAddDialog: false,
|
||||
showDeleteDialog: false,
|
||||
targetCharacter: null,
|
||||
searchWord: '',
|
||||
searchResults: [],
|
||||
searchPerformed: false,
|
||||
addList: []
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.curationId = this.$route.params.curationId;
|
||||
this.title = this.$route.params.title;
|
||||
this.isAdult = this.$route.params.isAdult;
|
||||
await this.loadCharacters();
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) { this.$dialog.notify.error(message); },
|
||||
notifySuccess(message) { this.$dialog.notify.success(message); },
|
||||
|
||||
goBack() { this.$router.push({ name: 'CharacterCuration' }); },
|
||||
|
||||
async loadCharacters() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await getCharactersInCuration(this.curationId);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.characters = res.data.data || [];
|
||||
} else {
|
||||
this.notifyError(res.data.message || '캐릭터 목록을 불러오지 못했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('캐릭터 목록을 불러오지 못했습니다.');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
openAddDialog() {
|
||||
this.showAddDialog = true;
|
||||
this.searchWord = '';
|
||||
this.searchResults = [];
|
||||
this.addList = [];
|
||||
this.searchPerformed = false;
|
||||
},
|
||||
closeAddDialog() {
|
||||
this.showAddDialog = false;
|
||||
this.searchWord = '';
|
||||
this.searchResults = [];
|
||||
this.addList = [];
|
||||
this.searchPerformed = false;
|
||||
},
|
||||
|
||||
async search() {
|
||||
if (!this.searchWord || this.searchWord.length < 2) {
|
||||
this.notifyError('검색어를 2글자 이상 입력하세요.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await searchCharacters(this.searchWord);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
const data = res.data.data;
|
||||
const list = data.content || [];
|
||||
const existingIds = new Set(this.characters.map(c => c.id));
|
||||
const pendingIds = new Set(this.addList.map(c => c.id));
|
||||
this.searchResults = list.filter(item => !existingIds.has(item.id) && !pendingIds.has(item.id));
|
||||
this.searchPerformed = true;
|
||||
} else {
|
||||
this.notifyError(res.data.message || '검색에 실패했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('검색에 실패했습니다.');
|
||||
}
|
||||
},
|
||||
|
||||
addItem(item) {
|
||||
// 검색결과에서 제거하고 추가 목록에 삽입 (중복 방지)
|
||||
if (!this.addList.find(t => t.id === item.id)) {
|
||||
this.addList.push(item);
|
||||
}
|
||||
this.searchResults = this.searchResults.filter(t => t.id !== item.id);
|
||||
},
|
||||
|
||||
removeItem(item) {
|
||||
this.addList = this.addList.filter(t => t.id !== item.id);
|
||||
// 제거 시 검색결과에 다시 추가
|
||||
if (!this.searchResults.find(t => t.id === item.id)) {
|
||||
this.searchResults.push(item);
|
||||
}
|
||||
},
|
||||
|
||||
async addItemInCuration() {
|
||||
if (!this.addList || this.addList.length === 0) return;
|
||||
this.isSubmitting = true;
|
||||
try {
|
||||
const ids = this.addList.map(i => i.id);
|
||||
const res = await addCharacterToCuration(this.curationId, ids);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.notifySuccess(`${this.addList.length}명 추가되었습니다.`);
|
||||
this.closeAddDialog();
|
||||
await this.loadCharacters();
|
||||
} else {
|
||||
this.notifyError((res.data && res.data.message) || '추가에 실패했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('추가에 실패했습니다.');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmRemove(item) { this.targetCharacter = item; this.showDeleteDialog = true; },
|
||||
|
||||
async removeTarget() {
|
||||
if (!this.targetCharacter) return;
|
||||
this.isSubmitting = true;
|
||||
try {
|
||||
const res = await removeCharacterFromCuration(this.curationId, this.targetCharacter.id);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.notifySuccess('삭제되었습니다.');
|
||||
this.showDeleteDialog = false;
|
||||
await this.loadCharacters();
|
||||
} else {
|
||||
this.notifyError(res.data.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('삭제에 실패했습니다.');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
|
||||
async onDragEnd() {
|
||||
try {
|
||||
const ids = this.characters.map(c => c.id);
|
||||
const res = await updateCurationCharactersOrder(this.curationId, ids);
|
||||
if (res.status === 200 && res.data && res.data.success === true) {
|
||||
this.notifySuccess('순서가 변경되었습니다.');
|
||||
} else {
|
||||
this.notifyError(res.data.message || '순서 변경에 실패했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('순서 변경에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
1968
src/views/Chat/CharacterForm.vue
Normal file
1968
src/views/Chat/CharacterForm.vue
Normal file
File diff suppressed because it is too large
Load Diff
306
src/views/Chat/CharacterImageForm.vue
Normal file
306
src/views/Chat/CharacterImageForm.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-btn
|
||||
icon
|
||||
@click="goBack"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>{{ isEdit ? '이미지 수정' : '이미지 등록' }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-card class="pa-4">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="isFormValid"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-show="!isEdit"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-file-input
|
||||
v-if="!isEdit"
|
||||
v-model="form.image"
|
||||
label="이미지 (800x1000 비율 권장)"
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-camera"
|
||||
show-size
|
||||
truncate-length="15"
|
||||
outlined
|
||||
dense
|
||||
:rules="imageRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="previewImage || form.imageUrl"
|
||||
cols="12"
|
||||
:md="isEdit ? 12 : 6"
|
||||
>
|
||||
<div class="text-center">
|
||||
<v-img
|
||||
:src="previewImage || form.imageUrl"
|
||||
max-height="240"
|
||||
:aspect-ratio="0.8"
|
||||
contain
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model.number="form.soloPurchasePriceCan"
|
||||
label="이미지 단독 구매 가격(캔)"
|
||||
type="number"
|
||||
min="0"
|
||||
outlined
|
||||
dense
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model.number="form.messagePurchasePriceCan"
|
||||
label="메시지에서 구매 가격(캔)"
|
||||
type="number"
|
||||
min="0"
|
||||
outlined
|
||||
dense
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-switch
|
||||
v-model="form.adult"
|
||||
label="성인 이미지 여부"
|
||||
inset
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-combobox
|
||||
v-model="triggers"
|
||||
label="트리거 단어 입력"
|
||||
multiple
|
||||
chips
|
||||
small-chips
|
||||
deletable-chips
|
||||
outlined
|
||||
dense
|
||||
:rules="triggerRules"
|
||||
@keydown.space.prevent="addTrigger"
|
||||
>
|
||||
<template v-slot:selection="{ attrs, item, select, selected }">
|
||||
<v-chip
|
||||
v-bind="attrs"
|
||||
:input-value="selected"
|
||||
close
|
||||
@click="select"
|
||||
@click:close="removeTrigger(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<div class="caption grey--text text--darken-1">
|
||||
트리거를 입력하고 엔터를 누르면 추가됩니다. (20자 이내, 최소 3개, 최대 10개)
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="goBack"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
:disabled="!canSubmit || isSubmitting"
|
||||
:loading="isSubmitting"
|
||||
@click="save"
|
||||
>
|
||||
저장
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createCharacterImage, updateCharacterImage, getCharacterImage } from '@/api/character'
|
||||
|
||||
export default {
|
||||
name: 'CharacterImageForm',
|
||||
data() {
|
||||
return {
|
||||
isEdit: !!this.$route.query.imageId,
|
||||
isSubmitting: false,
|
||||
isFormValid: false,
|
||||
characterId: Number(this.$route.query.characterId),
|
||||
imageId: this.$route.query.imageId ? Number(this.$route.query.imageId) : null,
|
||||
form: {
|
||||
image: null,
|
||||
imageUrl: '',
|
||||
soloPurchasePriceCan: null,
|
||||
messagePurchasePriceCan: null,
|
||||
adult: false
|
||||
},
|
||||
previewImage: null,
|
||||
triggers: [],
|
||||
triggerRules: [
|
||||
v => (v && v.length >= 3 && v.length <= 10) || '트리거는 최소 3개, 최대 10개까지 등록 가능합니다'
|
||||
],
|
||||
imageRules: [
|
||||
v => !!v || '이미지를 선택하세요'
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canSubmit() {
|
||||
const triggersValid = this.triggers && this.triggers.length >= 3 && this.triggers.length <= 10
|
||||
if (this.isEdit) return triggersValid
|
||||
return !!this.form.image && triggersValid
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'form.image'(newVal) {
|
||||
if (!this.isEdit) {
|
||||
if (newVal) this.createImagePreview(newVal)
|
||||
else this.previewImage = null
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.characterId) {
|
||||
this.notifyError('캐릭터 ID가 없습니다.')
|
||||
this.goBack();
|
||||
return
|
||||
}
|
||||
if (this.isEdit && this.imageId) {
|
||||
this.loadDetail()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
notifyError(m) { this.$dialog.notify.error(m) },
|
||||
notifySuccess(m) { this.$dialog.notify.success(m) },
|
||||
goBack() {
|
||||
this.$router.push({ path: '/character/images', query: { characterId: this.characterId, name: this.$route.query.name || '' } })
|
||||
},
|
||||
createImagePreview(file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => { this.previewImage = e.target.result }
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
addTrigger(e) {
|
||||
const value = (e.target.value || '').trim()
|
||||
if (!value) return
|
||||
if (value.length > 20) {
|
||||
this.notifyError('트리거는 20자 이내여야 합니다.')
|
||||
return
|
||||
}
|
||||
if (this.triggers.length >= 10) {
|
||||
this.notifyError('트리거는 최대 10개까지 등록 가능합니다.')
|
||||
return
|
||||
}
|
||||
if (!this.triggers.includes(value)) this.triggers.push(value)
|
||||
e.target.value = ''
|
||||
},
|
||||
removeTrigger(item) {
|
||||
this.triggers = this.triggers.filter(t => t !== item)
|
||||
},
|
||||
async loadDetail() {
|
||||
try {
|
||||
const resp = await getCharacterImage(this.imageId)
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
const d = resp.data.data
|
||||
// 수정 시 트리거만 노출하며 나머지는 비활성화
|
||||
this.form.imageUrl = d.imageUrl
|
||||
this.form.soloPurchasePriceCan = d.imagePriceCan
|
||||
this.form.messagePurchasePriceCan = d.messagePriceCan
|
||||
this.form.adult = d.isAdult
|
||||
this.triggers = d.triggers || []
|
||||
} else {
|
||||
this.notifyError('이미지 정보를 불러오지 못했습니다.')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 상세 오류:', e)
|
||||
this.notifyError('이미지 정보를 불러오지 못했습니다.')
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
if (this.isSubmitting) return
|
||||
// 트리거 개수 검증: 최소 3개, 최대 10개
|
||||
if (!this.triggers || this.triggers.length < 3 || this.triggers.length > 10) {
|
||||
this.notifyError('트리거는 최소 3개, 최대 10개여야 합니다.')
|
||||
return
|
||||
}
|
||||
this.isSubmitting = true
|
||||
try {
|
||||
if (this.isEdit) {
|
||||
const resp = await updateCharacterImage({ imageId: this.imageId, triggers: this.triggers })
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
this.notifySuccess('수정되었습니다.')
|
||||
this.goBack()
|
||||
} else {
|
||||
this.notifyError('수정에 실패했습니다.')
|
||||
}
|
||||
} else {
|
||||
const resp = await createCharacterImage({
|
||||
characterId: this.characterId,
|
||||
image: this.form.image,
|
||||
imagePriceCan: this.form.soloPurchasePriceCan,
|
||||
messagePriceCan: this.form.messagePurchasePriceCan,
|
||||
isAdult: this.form.adult,
|
||||
triggers: this.triggers
|
||||
})
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
this.notifySuccess('등록되었습니다.')
|
||||
this.goBack()
|
||||
} else {
|
||||
this.notifyError('등록에 실패했습니다.')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 저장 오류:', e)
|
||||
this.notifyError('작업 중 오류가 발생했습니다.')
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
325
src/views/Chat/CharacterImageList.vue
Normal file
325
src/views/Chat/CharacterImageList.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-btn
|
||||
icon
|
||||
@click="goBack"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>캐릭터 이미지 관리</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row class="align-center mb-4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<div class="subtitle-1">
|
||||
캐릭터: {{ characterName || characterId }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
class="text-right"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
dark
|
||||
@click="goToAdd"
|
||||
>
|
||||
이미지 추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 로딩 -->
|
||||
<v-row v-if="isLoading && images.length === 0">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="48"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 목록 -->
|
||||
<draggable
|
||||
v-if="images.length > 0"
|
||||
v-model="images"
|
||||
class="image-grid"
|
||||
:options="{ animation: 150 }"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<div
|
||||
v-for="img in images"
|
||||
:key="img.id"
|
||||
class="image-card"
|
||||
>
|
||||
<v-card>
|
||||
<div class="image-wrapper">
|
||||
<v-img
|
||||
:src="img.imageUrl"
|
||||
:aspect-ratio="0.8"
|
||||
contain
|
||||
/>
|
||||
<div
|
||||
v-if="img.isAdult"
|
||||
class="ribbon"
|
||||
>
|
||||
성인
|
||||
</div>
|
||||
</div>
|
||||
<v-card-text class="pt-2">
|
||||
<div class="price-row d-flex align-center">
|
||||
<div class="price-label">
|
||||
단독 :
|
||||
</div>
|
||||
<div class="price-value">
|
||||
{{ img.imagePriceCan }} 캔
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-row d-flex align-center">
|
||||
<div class="price-label">
|
||||
메시지 :
|
||||
</div>
|
||||
<div class="price-value">
|
||||
{{ img.messagePriceCan }} 캔
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<v-chip
|
||||
v-for="(t, i) in (img.triggers || [])"
|
||||
:key="i"
|
||||
small
|
||||
class="ma-1"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ t }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
@click="goToEdit(img)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="confirmDelete(img)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<!-- 데이터 없음 -->
|
||||
<v-row v-if="!isLoading && images.length === 0">
|
||||
<v-col class="text-center grey--text">
|
||||
등록된 이미지가 없습니다.
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showDeleteDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
이미지 삭제
|
||||
</v-card-title>
|
||||
<v-card-text>삭제하시겠습니까?</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="showDeleteDialog = false"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="red darken-1"
|
||||
:loading="isSubmitting"
|
||||
@click="deleteImage"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCharacterImageList, deleteCharacterImage, updateCharacterImageOrder } from '@/api/character'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
name: 'CharacterImageList',
|
||||
components: { draggable },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
images: [],
|
||||
characterId: null,
|
||||
characterName: this.$route.query.name || '',
|
||||
showDeleteDialog: false,
|
||||
selectedImage: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.characterId = Number(this.$route.query.characterId)
|
||||
if (!this.characterId) {
|
||||
this.notifyError('캐릭터 ID가 없습니다.');
|
||||
this.goBack()
|
||||
return
|
||||
}
|
||||
this.loadImages()
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) { this.$dialog.notify.error(message) },
|
||||
notifySuccess(message) { this.$dialog.notify.success(message) },
|
||||
goBack() { this.$router.push('/character') },
|
||||
async loadImages() {
|
||||
this.isLoading = true
|
||||
try {
|
||||
const resp = await getCharacterImageList(this.characterId, 1, 20)
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
const data = resp.data.data
|
||||
this.images = (data.content || data || [])
|
||||
} else {
|
||||
this.notifyError('이미지 목록을 불러오지 못했습니다.')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 목록 오류:', e)
|
||||
this.notifyError('이미지 목록 조회 중 오류가 발생했습니다.')
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
goToAdd() {
|
||||
this.$router.push({ path: '/character/images/form', query: { characterId: this.characterId, name: this.characterName } })
|
||||
},
|
||||
goToEdit(img) {
|
||||
this.$router.push({ path: '/character/images/form', query: { characterId: this.characterId, imageId: img.id, name: this.characterName } })
|
||||
},
|
||||
confirmDelete(img) {
|
||||
this.selectedImage = img
|
||||
this.showDeleteDialog = true
|
||||
},
|
||||
async deleteImage() {
|
||||
if (!this.selectedImage || this.isSubmitting) return
|
||||
this.isSubmitting = true
|
||||
try {
|
||||
const resp = await deleteCharacterImage(this.selectedImage.id)
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
this.notifySuccess('삭제되었습니다.')
|
||||
this.showDeleteDialog = false
|
||||
await this.loadImages()
|
||||
} else {
|
||||
this.notifyError('삭제에 실패했습니다.')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 삭제 오류:', e)
|
||||
this.notifyError('삭제 중 오류가 발생했습니다.')
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
}
|
||||
},
|
||||
async onDragEnd() {
|
||||
try {
|
||||
const ids = this.images.map(img => img.id)
|
||||
const resp = await updateCharacterImageOrder(this.characterId, ids)
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
this.notifySuccess('이미지 순서가 변경되었습니다.')
|
||||
} else {
|
||||
this.notifyError('이미지 순서 변경에 실패했습니다.')
|
||||
await this.loadImages()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 순서 변경 오류:', e)
|
||||
this.notifyError('이미지 순서 변경에 실패했습니다.')
|
||||
await this.loadImages()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.image-card {
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 1264px) {
|
||||
.image-grid { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
.image-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.image-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
/* Image wrapper for overlays */
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ribbon style for adult indicator */
|
||||
.ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: #e53935; /* red darken-1 */
|
||||
color: #fff;
|
||||
padding: 6px 20px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
text-transform: none;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Price rows styling */
|
||||
.price-row {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.price-label {
|
||||
width: 72px; /* 긴 쪽 기준으로 라벨 고정폭 */
|
||||
text-align: left;
|
||||
color: rgba(0,0,0,0.6);
|
||||
font-weight: 700;
|
||||
}
|
||||
.price-value {
|
||||
flex: 1;
|
||||
font-weight: 700;
|
||||
color: rgba(0,0,0,0.87);
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
438
src/views/Chat/CharacterList.vue
Normal file
438
src/views/Chat/CharacterList.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>캐릭터 리스트</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
dark
|
||||
@click="showAddDialog"
|
||||
>
|
||||
캐릭터 추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="d-flex justify-end align-center"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="searchTerm"
|
||||
label="검색어"
|
||||
placeholder="캐릭터명, 태그, mbti 검색"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
style="max-width: 320px;"
|
||||
class="mr-2"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
<v-btn
|
||||
:style="{ backgroundColor: '#3bb9f1', color: 'white' }"
|
||||
:disabled="is_loading"
|
||||
@click="onSearch"
|
||||
>
|
||||
검색
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-simple-table class="elevation-10">
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
ID
|
||||
</th>
|
||||
<th class="text-center">
|
||||
이미지
|
||||
</th>
|
||||
<th class="text-center">
|
||||
캐릭터명
|
||||
</th>
|
||||
<th class="text-center">
|
||||
성별
|
||||
</th>
|
||||
<th class="text-center">
|
||||
나이
|
||||
</th>
|
||||
<th class="text-center">
|
||||
캐릭터 설명
|
||||
</th>
|
||||
<th class="text-center">
|
||||
MBTI
|
||||
</th>
|
||||
<th class="text-center">
|
||||
말투
|
||||
</th>
|
||||
<th class="text-center">
|
||||
대화 스타일
|
||||
</th>
|
||||
<th class="text-center">
|
||||
태그
|
||||
</th>
|
||||
<th class="text-center">
|
||||
등록일
|
||||
</th>
|
||||
<th class="text-center">
|
||||
수정일
|
||||
</th>
|
||||
<th class="text-center">
|
||||
관리
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in characters"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.id }}</td>
|
||||
<td align="center">
|
||||
<v-img
|
||||
max-width="100"
|
||||
max-height="100"
|
||||
:src="item.imageUrl"
|
||||
class="rounded-circle"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.gender || '-' }}</td>
|
||||
<td>{{ item.age || '-' }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
small
|
||||
color="info"
|
||||
@click="showDetailDialog(item, 'description')"
|
||||
>
|
||||
보기
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>{{ item.mbti || '-' }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
small
|
||||
color="info"
|
||||
@click="showDetailDialog(item, 'speechPattern')"
|
||||
>
|
||||
보기
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn
|
||||
small
|
||||
color="info"
|
||||
@click="showDetailDialog(item, 'speechStyle')"
|
||||
>
|
||||
보기
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="item.tags && item.tags.length > 0">
|
||||
<v-chip
|
||||
v-for="(tag, index) in item.tags"
|
||||
:key="index"
|
||||
small
|
||||
class="ma-1"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>{{ item.createdAt }}</td>
|
||||
<td>{{ item.updatedAt || '-' }}</td>
|
||||
<td>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
:disabled="is_loading"
|
||||
@click="showEditDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="info"
|
||||
:disabled="is_loading"
|
||||
@click="goToImageList(item)"
|
||||
>
|
||||
이미지
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
:disabled="is_loading"
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="next"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text>
|
||||
"{{ selected_character.name }}"을(를) 삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="closeDeleteDialog"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red darken-1"
|
||||
text
|
||||
@click="deleteCharacter"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 상세 내용 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="show_detail_dialog"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ detail_title }}
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pt-4">
|
||||
<div style="white-space: pre-wrap;">
|
||||
{{ detail_content }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
text
|
||||
@click="closeDetailDialog"
|
||||
>
|
||||
닫기
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCharacterList, updateCharacter, searchCharacterList } from '@/api/character'
|
||||
|
||||
export default {
|
||||
name: "CharacterList",
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
show_detail_dialog: false,
|
||||
detail_type: '',
|
||||
detail_content: '',
|
||||
detail_title: '',
|
||||
page: 1,
|
||||
total_page: 0,
|
||||
characters: [],
|
||||
selected_character: {},
|
||||
searchTerm: ''
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getCharacters()
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
|
||||
showDetailDialog(item, type) {
|
||||
this.selected_character = item;
|
||||
this.detail_type = type;
|
||||
|
||||
// 타입에 따라 제목과 내용 설정
|
||||
switch(type) {
|
||||
case 'description':
|
||||
this.detail_title = '캐릭터 설명';
|
||||
this.detail_content = item.description || '내용이 없습니다.';
|
||||
break;
|
||||
case 'speechPattern':
|
||||
this.detail_title = '말투';
|
||||
this.detail_content = item.speechPattern || '내용이 없습니다.';
|
||||
break;
|
||||
case 'speechStyle':
|
||||
this.detail_title = '대화 스타일';
|
||||
this.detail_content = item.speechStyle || '내용이 없습니다.';
|
||||
break;
|
||||
default:
|
||||
this.detail_title = '';
|
||||
this.detail_content = '';
|
||||
}
|
||||
|
||||
this.show_detail_dialog = true;
|
||||
},
|
||||
|
||||
closeDetailDialog() {
|
||||
this.show_detail_dialog = false;
|
||||
this.detail_type = '';
|
||||
this.detail_content = '';
|
||||
this.detail_title = '';
|
||||
},
|
||||
|
||||
showAddDialog() {
|
||||
// 페이지로 이동
|
||||
this.$router.push('/character/form');
|
||||
},
|
||||
|
||||
goToImageList(item) {
|
||||
this.$router.push({
|
||||
path: '/character/images',
|
||||
query: { characterId: item.id, name: item.name }
|
||||
})
|
||||
},
|
||||
|
||||
showEditDialog(item) {
|
||||
// 페이지로 이동하면서 id 전달
|
||||
this.$router.push({
|
||||
path: '/character/form',
|
||||
query: { id: item.id }
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
deleteConfirm(item) {
|
||||
this.selected_character = item
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
closeDeleteDialog() {
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.selected_character = {}
|
||||
},
|
||||
|
||||
|
||||
async deleteCharacter() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
// 삭제 대신 isActive를 false로 설정하여 비활성화
|
||||
const updateData = {
|
||||
id: this.selected_character.id,
|
||||
isActive: false
|
||||
};
|
||||
await updateCharacter(updateData);
|
||||
this.closeDeleteDialog();
|
||||
this.notifySuccess('삭제되었습니다.');
|
||||
await this.getCharacters();
|
||||
} catch (e) {
|
||||
console.error('캐릭터 삭제 오류:', e);
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
|
||||
} finally {
|
||||
this.is_loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async next() {
|
||||
await this.getCharacters()
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.page = 1;
|
||||
this.getCharacters();
|
||||
},
|
||||
|
||||
async getCharacters() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
const hasSearch = this.searchTerm && this.searchTerm.trim() !== '';
|
||||
const response = hasSearch
|
||||
? await searchCharacterList(this.searchTerm.trim(), this.page, 20)
|
||||
: await getCharacterList(this.page);
|
||||
|
||||
if (response && response.status === 200) {
|
||||
if (response.data.success === true) {
|
||||
const data = response.data.data;
|
||||
this.characters = data.content || [];
|
||||
|
||||
const total_page = Math.ceil((data.totalCount || 0) / 20);
|
||||
this.total_page = total_page <= 0 ? 1 : total_page;
|
||||
} else {
|
||||
this.notifyError('응답 데이터가 없습니다.');
|
||||
}
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('캐릭터 목록 조회 오류:', e);
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
|
||||
} finally {
|
||||
this.is_loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-data-table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
356
src/views/Chat/OriginalDetail.vue
Normal file
356
src/views/Chat/OriginalDetail.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-btn
|
||||
icon
|
||||
@click="goBack"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>원작 상세</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="openAssignDialog"
|
||||
>
|
||||
캐릭터 연결
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-card
|
||||
v-if="detail"
|
||||
class="pa-4"
|
||||
>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-img
|
||||
:src="detail.imageUrl"
|
||||
contain
|
||||
height="240"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<h2>{{ detail.title }}</h2>
|
||||
<div class="mt-2">
|
||||
콘텐츠 타입: {{ detail.contentType || '-' }}
|
||||
</div>
|
||||
<div>카테고리(장르): {{ detail.category || '-' }}</div>
|
||||
<div>19금 여부: {{ detail.isAdult ? '예' : '아니오' }}</div>
|
||||
<div>원천 원작: {{ detail.originalWork || '-' }}</div>
|
||||
<div class="mt-1">
|
||||
원천 원작 링크:
|
||||
<a
|
||||
v-if="detail.originalLink"
|
||||
:href="detail.originalLink"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ detail.originalLink }}</a>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
<div>글/그림: {{ detail.writer || '-' }}</div>
|
||||
<div>제작사: {{ detail.studio || '-' }}</div>
|
||||
<div class="mt-1">
|
||||
원작 링크:
|
||||
<template v-if="detail.originalLinks && detail.originalLinks.length">
|
||||
<div>
|
||||
<div
|
||||
v-for="(link, idx) in detail.originalLinks"
|
||||
:key="idx"
|
||||
>
|
||||
<a
|
||||
:href="link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ link }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
태그:
|
||||
<template v-if="detail.tags && detail.tags.length">
|
||||
<v-chip
|
||||
v-for="(t, i) in detail.tags"
|
||||
:key="i"
|
||||
small
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ t }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
작품 소개:
|
||||
</div>
|
||||
<div style="white-space:pre-wrap;">
|
||||
{{ detail.description || '-' }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
<v-card class="pa-4 mt-6">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<h3>연결된 캐릭터</h3>
|
||||
<v-spacer />
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="c in characters"
|
||||
:key="c.id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
<v-card>
|
||||
<v-img
|
||||
:src="c.imagePath"
|
||||
height="180"
|
||||
contain
|
||||
/>
|
||||
<v-card-title class="text-no-wrap">
|
||||
{{ c.name }}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="unassign([c.id])"
|
||||
>
|
||||
해제
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isLoadingCharacters">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="assignDialog"
|
||||
max-width="800"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>캐릭터 연결</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="searchKeyword"
|
||||
label="캐릭터 검색"
|
||||
outlined
|
||||
dense
|
||||
@input="onSearchInput"
|
||||
/>
|
||||
<v-data-table
|
||||
v-model="selectedToAssign"
|
||||
:headers="headers"
|
||||
:items="searchResults"
|
||||
:loading="searchLoading"
|
||||
item-key="id"
|
||||
show-select
|
||||
:items-per-page="5"
|
||||
>
|
||||
<template v-slot:item.imageUrl="{ item }">
|
||||
<v-img
|
||||
:src="item.imagePath"
|
||||
max-width="60"
|
||||
max-height="60"
|
||||
class="rounded-circle"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="assignDialog = false"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="selectedToAssign.length===0"
|
||||
@click="assign"
|
||||
>
|
||||
연결
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOriginal, getOriginalCharacters, assignCharactersToOriginal, unassignCharactersFromOriginal } from '@/api/original'
|
||||
import { searchCharacters } from '@/api/character'
|
||||
|
||||
export default {
|
||||
name: 'OriginalDetail',
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
detail: null,
|
||||
characters: [],
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
isLoadingCharacters: false,
|
||||
assignDialog: false,
|
||||
searchKeyword: '',
|
||||
searchLoading: false,
|
||||
searchResults: [],
|
||||
selectedToAssign: [],
|
||||
headers: [
|
||||
{ text: '이미지', value: 'imageUrl', sortable: false },
|
||||
{ text: '이름', value: 'name' },
|
||||
{ text: 'ID', value: 'id' }
|
||||
],
|
||||
debounceTimer: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.id = this.$route.query.id
|
||||
if (!this.id) {
|
||||
this.$dialog.notify.error('잘못된 접근입니다.');
|
||||
this.$router.push('/original-work');
|
||||
return;
|
||||
}
|
||||
this.loadDetail();
|
||||
this.loadCharacters();
|
||||
window.addEventListener('scroll', this.onScroll);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) { this.$dialog.notify.error(message) },
|
||||
notifySuccess(message) { this.$dialog.notify.success(message) },
|
||||
goBack() { this.$router.push('/original-work') },
|
||||
async loadDetail() {
|
||||
try {
|
||||
const res = await getOriginal(this.id);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.detail = res.data.data;
|
||||
} else {
|
||||
this.notifyError('상세 조회 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('상세 조회 실패');
|
||||
}
|
||||
},
|
||||
async loadCharacters() {
|
||||
if (this.isLoadingCharacters || !this.hasMore) return;
|
||||
this.isLoadingCharacters = true;
|
||||
try {
|
||||
const res = await getOriginalCharacters(this.id, this.page);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const content = res.data.data?.content || [];
|
||||
this.characters = this.characters.concat(content);
|
||||
this.hasMore = content.length > 0;
|
||||
this.page++;
|
||||
} else {
|
||||
this.notifyError('캐릭터 목록 조회 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('캐릭터 목록 조회 실패');
|
||||
} finally {
|
||||
this.isLoadingCharacters = false;
|
||||
}
|
||||
},
|
||||
onScroll() {
|
||||
const scrollPosition = window.innerHeight + window.scrollY;
|
||||
const documentHeight = document.documentElement.offsetHeight;
|
||||
if (scrollPosition >= documentHeight - 200 && !this.isLoadingCharacters && this.hasMore) {
|
||||
this.loadCharacters();
|
||||
}
|
||||
},
|
||||
openAssignDialog() {
|
||||
this.assignDialog = true;
|
||||
this.searchKeyword = '';
|
||||
this.searchResults = [];
|
||||
this.selectedToAssign = [];
|
||||
},
|
||||
onSearchInput() {
|
||||
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = setTimeout(this.search, 300);
|
||||
},
|
||||
async search() {
|
||||
if (!this.searchKeyword || !this.searchKeyword.trim()) {
|
||||
this.searchResults = [];
|
||||
return;
|
||||
}
|
||||
this.searchLoading = true;
|
||||
try {
|
||||
const res = await searchCharacters(this.searchKeyword.trim(), 1, 20);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.searchResults = res.data.data?.content || [];
|
||||
} else {
|
||||
this.notifyError('검색 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('검색 실패');
|
||||
} finally {
|
||||
this.searchLoading = false;
|
||||
}
|
||||
},
|
||||
async assign() {
|
||||
if (this.selectedToAssign.length === 0) return;
|
||||
try {
|
||||
const ids = this.selectedToAssign.map(x => x.id);
|
||||
const res = await assignCharactersToOriginal(this.id, ids);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess('연결되었습니다.');
|
||||
this.assignDialog = false;
|
||||
// 목록 초기화 후 재조회
|
||||
this.characters = [];
|
||||
this.page = 1;
|
||||
this.hasMore = true;
|
||||
this.loadCharacters();
|
||||
} else {
|
||||
this.notifyError('연결 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('연결 실패');
|
||||
}
|
||||
},
|
||||
async unassign(ids) {
|
||||
if (!ids || ids.length === 0) return;
|
||||
try {
|
||||
const res = await unassignCharactersFromOriginal(this.id, ids);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess('해제되었습니다.');
|
||||
this.characters = this.characters.filter(c => !ids.includes(c.id));
|
||||
} else {
|
||||
this.notifyError('해제 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('해제 실패');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-no-wrap { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
</style>
|
505
src/views/Chat/OriginalForm.vue
Normal file
505
src/views/Chat/OriginalForm.vue
Normal file
@@ -0,0 +1,505 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-btn
|
||||
icon
|
||||
@click="goBack"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>{{ isEdit ? '원작 수정' : '원작 등록' }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-card class="pa-4">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="isFormValid"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-file-input
|
||||
v-model="form.image"
|
||||
label="이미지"
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-camera"
|
||||
outlined
|
||||
dense
|
||||
:class="{ 'required-asterisk': !isEdit }"
|
||||
:rules="imageRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="previewImage || form.imageUrl"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<div class="text-center">
|
||||
<v-avatar size="150">
|
||||
<v-img
|
||||
:src="previewImage || form.imageUrl"
|
||||
contain
|
||||
/>
|
||||
</v-avatar>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.title"
|
||||
label="제목"
|
||||
outlined
|
||||
dense
|
||||
:rules="[v=>!!v||'제목은 필수입니다']"
|
||||
class="required-asterisk"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="form.contentType"
|
||||
label="콘텐츠 타입"
|
||||
outlined
|
||||
dense
|
||||
:rules="contentTypeRules"
|
||||
:class="{ 'required-asterisk': !isEdit }"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="form.category"
|
||||
label="카테고리(장르)"
|
||||
outlined
|
||||
dense
|
||||
:rules="categoryRules"
|
||||
:class="{ 'required-asterisk': !isEdit }"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 추가 메타 정보 (요구 순서: 글/그림, 제작사, 원천원작, 원천 원작 링크) -->
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="form.writer"
|
||||
label="글/그림"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="form.studio"
|
||||
label="제작사"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="form.originalWork"
|
||||
label="원천 원작"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="form.originalLink"
|
||||
label="원천 원작 링크"
|
||||
outlined
|
||||
dense
|
||||
:rules="originalLinkRules"
|
||||
:class="{ 'required-asterisk': !isEdit }"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="12"
|
||||
>
|
||||
<v-switch
|
||||
v-model="form.isAdult"
|
||||
label="19금 여부"
|
||||
inset
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 원작 링크(여러 개) 추가 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-divider class="my-4" />
|
||||
<h3 class="mb-2">
|
||||
원작 링크
|
||||
</h3>
|
||||
<v-row>
|
||||
<v-col cols="11">
|
||||
<v-text-field
|
||||
v-model="newOriginalLink"
|
||||
label="원작 링크 추가"
|
||||
outlined
|
||||
dense
|
||||
@keyup.enter="addOriginalLink"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mt-1"
|
||||
block
|
||||
:disabled="!newOriginalLink || !newOriginalLink.trim()"
|
||||
@click="addOriginalLink"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card
|
||||
outlined
|
||||
class="mt-2"
|
||||
>
|
||||
<v-list v-if="form.originalLinks && form.originalLinks.length > 0">
|
||||
<v-list-item
|
||||
v-for="(link, idx) in form.originalLinks"
|
||||
:key="idx"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="text-truncate">
|
||||
{{ link }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="removeOriginalLink(idx)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-card-text
|
||||
v-else
|
||||
class="grey--text"
|
||||
>
|
||||
추가된 원작 링크가 없습니다.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 태그 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-divider class="my-4" />
|
||||
<h3 class="mb-2">
|
||||
태그
|
||||
</h3>
|
||||
<v-combobox
|
||||
v-model="form.tags"
|
||||
multiple
|
||||
chips
|
||||
small-chips
|
||||
deletable-chips
|
||||
outlined
|
||||
dense
|
||||
label="태그를 입력 후 엔터로 추가"
|
||||
@keydown.space.prevent="onTagSpace"
|
||||
>
|
||||
<template v-slot:selection="{ attrs, item, select, selected }">
|
||||
<v-chip
|
||||
v-bind="attrs"
|
||||
:input-value="selected"
|
||||
close
|
||||
@click="select"
|
||||
@click:close="removeTag(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="form.description"
|
||||
label="작품 소개"
|
||||
outlined
|
||||
rows="4"
|
||||
:rules="descriptionRules"
|
||||
:class="{ 'required-asterisk': !isEdit }"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!canSubmit"
|
||||
@click="onSubmit"
|
||||
>
|
||||
{{ isEdit ? '수정' : '등록' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createOriginal, updateOriginal, getOriginal } from '@/api/original'
|
||||
|
||||
export default {
|
||||
name: 'OriginalForm',
|
||||
data() {
|
||||
return {
|
||||
isEdit: false,
|
||||
isFormValid: false,
|
||||
previewImage: null,
|
||||
newOriginalLink: '',
|
||||
form: {
|
||||
id: null,
|
||||
image: null,
|
||||
imageUrl: null,
|
||||
title: '',
|
||||
contentType: '',
|
||||
category: '',
|
||||
isAdult: false,
|
||||
description: '',
|
||||
originalLink: '', // 원천 원작 링크(파라미터명 유지)
|
||||
originalWork: '',
|
||||
writer: '',
|
||||
studio: '',
|
||||
originalLinks: [], // 추가 원작 링크들
|
||||
tags: []
|
||||
},
|
||||
originalInitial: null,
|
||||
imageRules: [v => (this.isEdit ? true : (!!v || '이미지를 선택하세요'))],
|
||||
contentTypeRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '콘텐츠 타입은 필수입니다'))],
|
||||
categoryRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '카테고리는 필수입니다'))],
|
||||
originalLinkRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '원천 원작 링크는 필수입니다'))],
|
||||
descriptionRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '작품 소개는 필수입니다'))]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageChanged() {
|
||||
return !!this.form.image;
|
||||
},
|
||||
hasNonImageChanges() {
|
||||
if (!this.isEdit || !this.originalInitial) return false;
|
||||
const fields = ['title', 'contentType', 'category', 'isAdult', 'description', 'originalLink', 'originalWork', 'writer', 'studio'];
|
||||
const basicChanged = fields.some(f => this.form[f] !== this.originalInitial[f]);
|
||||
const arraysChanged = !this.arraysEqual(this.form.originalLinks, this.originalInitial.originalLinks)
|
||||
|| !this.arraysEqual(this.form.tags, this.originalInitial.tags);
|
||||
return basicChanged || arraysChanged;
|
||||
},
|
||||
hasEditChanges() {
|
||||
return this.imageChanged || this.hasNonImageChanges;
|
||||
},
|
||||
canSubmit() {
|
||||
if (this.isEdit) return this.hasEditChanges && !!(this.form.title && this.form.title.toString().trim());
|
||||
const required = [this.form.image, this.form.title, this.form.contentType, this.form.category, this.form.originalLink, this.form.description];
|
||||
return required.every(v => !!(v && (v.toString ? v.toString().trim() : v)));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'form.image': {
|
||||
handler(newImage) {
|
||||
if (newImage) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => { this.previewImage = e.target.result }
|
||||
reader.readAsDataURL(newImage)
|
||||
} else {
|
||||
this.previewImage = null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.$route.query.id) {
|
||||
this.isEdit = true;
|
||||
this.load(this.$route.query.id);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) { this.$dialog.notify.error(message) },
|
||||
notifySuccess(message) { this.$dialog.notify.success(message) },
|
||||
goBack() { this.$router.push('/original-work') },
|
||||
arraysEqual(a, b) {
|
||||
const arrA = Array.isArray(a) ? a : [];
|
||||
const arrB = Array.isArray(b) ? b : [];
|
||||
if (arrA.length !== arrB.length) return false;
|
||||
for (let i = 0; i < arrA.length; i++) {
|
||||
if (arrA[i] !== arrB[i]) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
addOriginalLink() {
|
||||
if (!this.newOriginalLink || !this.newOriginalLink.trim()) return;
|
||||
const val = this.newOriginalLink.trim();
|
||||
if (!this.form.originalLinks) this.form.originalLinks = [];
|
||||
if (!this.form.originalLinks.includes(val)) {
|
||||
this.form.originalLinks.push(val);
|
||||
}
|
||||
this.newOriginalLink = '';
|
||||
},
|
||||
removeOriginalLink(index) {
|
||||
if (!this.form.originalLinks) return;
|
||||
this.form.originalLinks.splice(index, 1);
|
||||
},
|
||||
onTagSpace() {
|
||||
// CharacterForm의 태그 방식과 유사: 마지막 항목을 공백 기준으로 확정
|
||||
if (!Array.isArray(this.form.tags)) this.form.tags = [];
|
||||
const last = this.form.tags[this.form.tags.length - 1];
|
||||
if (typeof last === 'string' && last.trim()) {
|
||||
let processed = last.trim().replace(/\s+/g, '');
|
||||
if (processed.length > 50) processed = processed.substring(0, 50);
|
||||
this.form.tags.splice(this.form.tags.length - 1, 1, processed);
|
||||
this.$nextTick(() => this.form.tags.push(''));
|
||||
}
|
||||
},
|
||||
removeTag(item) {
|
||||
if (!Array.isArray(this.form.tags)) return;
|
||||
const idx = this.form.tags.indexOf(item);
|
||||
if (idx >= 0) this.form.tags.splice(idx, 1);
|
||||
},
|
||||
async load(id) {
|
||||
try {
|
||||
const res = await getOriginal(id);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const d = res.data.data;
|
||||
this.form = {
|
||||
id: d.id,
|
||||
image: null,
|
||||
imageUrl: d.imageUrl,
|
||||
title: d.title || '',
|
||||
contentType: d.contentType || '',
|
||||
category: d.category || '',
|
||||
isAdult: !!d.isAdult,
|
||||
description: d.description || '',
|
||||
originalLink: d.originalLink || '',
|
||||
originalWork: d.originalWork || '',
|
||||
writer: d.writer || '',
|
||||
studio: d.studio || '',
|
||||
originalLinks: Array.isArray(d.originalLinks) ? d.originalLinks.slice() : [],
|
||||
tags: Array.isArray(d.tags) ? d.tags.slice() : []
|
||||
}
|
||||
this.originalInitial = {
|
||||
id: d.id,
|
||||
imageUrl: d.imageUrl,
|
||||
title: d.title || '',
|
||||
contentType: d.contentType || '',
|
||||
category: d.category || '',
|
||||
isAdult: !!d.isAdult,
|
||||
description: d.description || '',
|
||||
originalLink: d.originalLink || '',
|
||||
originalWork: d.originalWork || '',
|
||||
writer: d.writer || '',
|
||||
studio: d.studio || '',
|
||||
originalLinks: Array.isArray(d.originalLinks) ? d.originalLinks.slice() : [],
|
||||
tags: Array.isArray(d.tags) ? d.tags.slice() : []
|
||||
}
|
||||
} else {
|
||||
this.notifyError('상세 조회 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('상세 조회 실패');
|
||||
}
|
||||
},
|
||||
async onSubmit() {
|
||||
try {
|
||||
const isValid = this.$refs.form ? this.$refs.form.validate() : true;
|
||||
if (!isValid) {
|
||||
this.notifyError(this.isEdit ? '입력을 확인해주세요.' : '필수 항목을 모두 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEdit) {
|
||||
const fields = ['title', 'contentType', 'category', 'isAdult', 'description', 'originalLink', 'originalWork', 'writer', 'studio'];
|
||||
const patch = { id: this.form.id };
|
||||
if (this.originalInitial) {
|
||||
fields.forEach(f => {
|
||||
if (this.form[f] !== this.originalInitial[f]) {
|
||||
patch[f] = this.form[f];
|
||||
}
|
||||
});
|
||||
if (!this.arraysEqual(this.form.originalLinks, this.originalInitial.originalLinks)) {
|
||||
patch.originalLinks = this.form.originalLinks;
|
||||
}
|
||||
if (!this.arraysEqual(this.form.tags, this.originalInitial.tags)) {
|
||||
patch.tags = this.form.tags;
|
||||
}
|
||||
}
|
||||
const image = this.form.image || null;
|
||||
if (Object.keys(patch).length === 1 && !image) {
|
||||
this.notifyError('변경된 내용이 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await updateOriginal(patch, image);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess('수정되었습니다.');
|
||||
this.$router.push('/original-work');
|
||||
} else {
|
||||
this.notifyError('수정 실패');
|
||||
}
|
||||
} else {
|
||||
const res = await createOriginal(this.form);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess('등록되었습니다.');
|
||||
this.$router.push('/original-work');
|
||||
} else {
|
||||
this.notifyError('등록 실패');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError(this.isEdit ? '수정 실패' : '등록 실패');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.required-asterisk >>> .v-label::after { content: ' *'; color: #ff5252; }
|
||||
</style>
|
205
src/views/Chat/OriginalList.vue
Normal file
205
src/views/Chat/OriginalList.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>원작 리스트</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
dark
|
||||
@click="goToCreate"
|
||||
>
|
||||
원작 등록
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row v-if="isLoading && originals.length === 0">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="item in originals"
|
||||
:key="item.id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
<v-card
|
||||
class="mx-auto"
|
||||
max-width="344"
|
||||
style="cursor:pointer;"
|
||||
@click="openDetail(item)"
|
||||
>
|
||||
<v-img
|
||||
:src="item.imageUrl"
|
||||
height="200"
|
||||
contain
|
||||
/>
|
||||
<v-card-title class="text-no-wrap">
|
||||
{{ item.title }}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
@click.stop="editOriginal(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click.stop="confirmDelete(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="!isLoading && originals.length === 0">
|
||||
<v-col class="text-center">
|
||||
데이터가 없습니다.
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="isLoading && originals.length > 0">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="deleteDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
삭제 확인
|
||||
</v-card-title>
|
||||
<v-card-text>정말 삭제하시겠습니까?</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="deleteDialog = false"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
text
|
||||
@click="deleteItem"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOriginalList, deleteOriginal } from '@/api/original'
|
||||
|
||||
export default {
|
||||
name: 'OriginalList',
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
originals: [],
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deleteDialog: false,
|
||||
selected: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadMore();
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) { this.$dialog.notify.error(message) },
|
||||
notifySuccess(message) { this.$dialog.notify.success(message) },
|
||||
async loadMore() {
|
||||
if (this.isLoading || !this.hasMore) return;
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await getOriginalList(this.page);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const content = res.data.data?.content || [];
|
||||
this.originals = this.originals.concat(content);
|
||||
this.hasMore = content.length > 0;
|
||||
this.page++;
|
||||
} else {
|
||||
this.notifyError('원작 목록 조회 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('원작 목록 조회 실패');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
handleScroll() {
|
||||
const scrollPosition = window.innerHeight + window.scrollY;
|
||||
const documentHeight = document.documentElement.offsetHeight;
|
||||
if (scrollPosition >= documentHeight - 200 && !this.isLoading && this.hasMore) {
|
||||
this.loadMore();
|
||||
}
|
||||
},
|
||||
goToCreate() {
|
||||
this.$router.push('/original-work/form');
|
||||
},
|
||||
editOriginal(item) {
|
||||
this.$router.push({ path: '/original-work/form', query: { id: item.id } });
|
||||
},
|
||||
openDetail(item) {
|
||||
this.$router.push({ path: '/original-work/detail', query: { id: item.id } });
|
||||
},
|
||||
confirmDelete(item) {
|
||||
this.selected = item;
|
||||
this.deleteDialog = true;
|
||||
},
|
||||
async deleteItem() {
|
||||
if (!this.selected) return;
|
||||
try {
|
||||
const res = await deleteOriginal(this.selected.id);
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess('삭제되었습니다.');
|
||||
this.originals = this.originals.filter(x => x.id !== this.selected.id);
|
||||
} else {
|
||||
this.notifyError('삭제 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('삭제 실패');
|
||||
} finally {
|
||||
this.deleteDialog = false;
|
||||
this.selected = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-no-wrap { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
</style>
|
@@ -10,11 +10,25 @@
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="10" />
|
||||
<v-col cols="9">
|
||||
<v-radio-group
|
||||
v-model="selected_tab_id"
|
||||
row
|
||||
@change="getCurations"
|
||||
>
|
||||
<v-radio
|
||||
v-for="tab in tabs"
|
||||
:key="tab.tabId"
|
||||
:label="tab.title"
|
||||
:value="tab.tabId"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#9970ff"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showWriteDialog"
|
||||
@@ -45,12 +59,24 @@
|
||||
v-for="(item, index) in props.items"
|
||||
:key="index"
|
||||
>
|
||||
<td>
|
||||
<td
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</td>
|
||||
<td>
|
||||
<td
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
{{ item.description }}
|
||||
</td>
|
||||
<td>
|
||||
<h3 v-if="item.isSeries">
|
||||
O
|
||||
</h3>
|
||||
<h3 v-else>
|
||||
X
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<h3 v-if="item.isAdult">
|
||||
O
|
||||
@@ -103,6 +129,26 @@
|
||||
<v-card-title v-else>
|
||||
콘텐츠 큐레이션 등록
|
||||
</v-card-title>
|
||||
<v-card-text v-if="is_modify === false">
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
메인 탭
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-radio-group
|
||||
v-model="curation.tab_id"
|
||||
row
|
||||
>
|
||||
<v-radio
|
||||
v-for="tab in tabs"
|
||||
:key="tab.tabId"
|
||||
:label="tab.title"
|
||||
:value="tab.tabId"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
@@ -131,6 +177,19 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="is_modify === false">
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
시리즈 큐레이션
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<input
|
||||
v-model="curation.is_series"
|
||||
type="checkbox"
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
@@ -220,8 +279,10 @@ export default {
|
||||
show_delete_confirm_dialog: false,
|
||||
show_write_dialog: false,
|
||||
selected_curation: {},
|
||||
curation: {is_adult: false},
|
||||
curation: {is_adult: false, is_series: false},
|
||||
curations: [],
|
||||
tabs: [],
|
||||
selected_tab_id: 1,
|
||||
headers: [
|
||||
{
|
||||
text: '제목',
|
||||
@@ -235,6 +296,12 @@ export default {
|
||||
sortable: false,
|
||||
value: 'description',
|
||||
},
|
||||
{
|
||||
text: '시리즈 큐레이션',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'isSeries',
|
||||
},
|
||||
{
|
||||
text: '19금',
|
||||
align: 'center',
|
||||
@@ -252,7 +319,7 @@ export default {
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getCurations()
|
||||
await this.getAudioContentMainTabList()
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -273,22 +340,48 @@ export default {
|
||||
this.selected_curation = item
|
||||
|
||||
this.curation.id = item.id
|
||||
this.curation.tab_id = item.tabId
|
||||
this.curation.title = item.title
|
||||
this.curation.description = item.description
|
||||
this.curation.is_series = item.isSeries
|
||||
this.curation.is_adult = item.isAdult
|
||||
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.curation = {is_adult: false}
|
||||
this.curation = {is_adult: false, is_series: false}
|
||||
this.selected_curation = {}
|
||||
|
||||
this.is_modify = false
|
||||
this.show_write_dialog = false
|
||||
},
|
||||
|
||||
handleItemClick(item) {
|
||||
this.$router.push(
|
||||
{
|
||||
name: 'ContentCurationDetail',
|
||||
params: {
|
||||
curation_id: item.id,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
is_series: item.isSeries,
|
||||
is_adult: item.isAdult
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (
|
||||
this.curation.tab_id === null ||
|
||||
this.curation.tab_id === undefined ||
|
||||
this.curation.tab_id <= 0
|
||||
) {
|
||||
this.notifyError("메인 탭을 선택하세요")
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
this.curation.title === null ||
|
||||
this.curation.title === undefined ||
|
||||
@@ -320,6 +413,27 @@ export default {
|
||||
this.show_delete_confirm_dialog = false
|
||||
},
|
||||
|
||||
async getAudioContentMainTabList() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getAudioContentMainTabList()
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const data = res.data.data
|
||||
|
||||
this.tabs = data.filter(item => item.title !== '홈')
|
||||
this.selected_tab_id = this.tabs[0].tabId
|
||||
await this.getCurations()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) return;
|
||||
if (this.is_loading) return;
|
||||
@@ -328,8 +442,10 @@ export default {
|
||||
|
||||
try {
|
||||
const request = {
|
||||
tabId: this.curation.tab_id,
|
||||
title: this.curation.title,
|
||||
description: this.curation.description,
|
||||
isSeries: this.curation.is_series,
|
||||
isAdult: this.curation.is_adult
|
||||
}
|
||||
|
||||
@@ -357,6 +473,10 @@ export default {
|
||||
|
||||
try {
|
||||
let request = {id: this.curation.id}
|
||||
if (this.selected_curation.tab_id !== this.curation.tab_id) {
|
||||
request.tabId = this.curation.tab_id
|
||||
}
|
||||
|
||||
if (this.selected_curation.title !== this.curation.title && this.curation.title.trim().length > 0) {
|
||||
request.title = this.curation.title
|
||||
}
|
||||
@@ -368,6 +488,10 @@ export default {
|
||||
request.description = this.curation.description
|
||||
}
|
||||
|
||||
if (this.selected_curation.isSeries !== this.curation.is_series) {
|
||||
request.isSeries = this.curation.is_series
|
||||
}
|
||||
|
||||
if (this.selected_curation.isAdult !== this.curation.is_adult) {
|
||||
request.isAdult = this.curation.is_adult
|
||||
}
|
||||
@@ -439,7 +563,7 @@ export default {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getCurations()
|
||||
const res = await api.getCurations(this.selected_tab_id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.curations = res.data.data
|
||||
} else {
|
||||
|
630
src/views/Content/ContentCurationDetail.vue
Normal file
630
src/views/Content/ContentCurationDetail.vue
Normal file
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>{{ curation_title }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="4"
|
||||
align="right"
|
||||
>
|
||||
19금 :
|
||||
</v-col>
|
||||
<v-col
|
||||
align="left"
|
||||
>
|
||||
<div v-if="is_adult">
|
||||
O
|
||||
</div>
|
||||
<div v-else>
|
||||
X
|
||||
</div>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="is_series"
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showAddSeries"
|
||||
>
|
||||
시리즈 등록
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showAddContent"
|
||||
>
|
||||
콘텐츠 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="4"
|
||||
align="right"
|
||||
>
|
||||
내용 :
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
align="left"
|
||||
>
|
||||
<vue-show-more-text
|
||||
:style="{ padding: '0' }"
|
||||
:text="curation_description"
|
||||
:lines="2"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-simple-table class="elevation-10">
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
썸네일
|
||||
</th>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th class="text-center">
|
||||
내용
|
||||
</th>
|
||||
<th class="text-center">
|
||||
크리에이터
|
||||
</th>
|
||||
<th class="text-center">
|
||||
19금
|
||||
</th>
|
||||
<th class="text-center">
|
||||
관리
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
v-model="items"
|
||||
tag="tbody"
|
||||
@end="onDropCallback(items)"
|
||||
>
|
||||
<tr
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
>
|
||||
<td align="center">
|
||||
<v-img
|
||||
max-width="70"
|
||||
max-height="70"
|
||||
:src="item.coverImageUrl"
|
||||
class="rounded-circle"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<vue-show-more-text
|
||||
:text="item.title"
|
||||
:lines="3"
|
||||
/>
|
||||
</td>
|
||||
<td style="max-width: 200px !important; word-break:break-all; height: auto;">
|
||||
<vue-show-more-text
|
||||
:text="item.desc"
|
||||
:lines="3"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ item.creatorNickname }}</td>
|
||||
<td>
|
||||
<div v-if="item.isAdult">
|
||||
O
|
||||
</div>
|
||||
<div v-else>
|
||||
X
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn
|
||||
:disabled="is_loading"
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_add_content_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
콘텐츠 추가
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="search_word"
|
||||
label="콘텐츠 제목"
|
||||
@keyup.enter="searchContentItem"
|
||||
>
|
||||
<v-btn
|
||||
slot="append"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
@click="searchContentItem"
|
||||
>
|
||||
검색
|
||||
</v-btn>
|
||||
</v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="search_item_list.length > 0 || add_item_list.length > 0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
검색결과
|
||||
<v-simple-table>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in search_item_list"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
color="#3bb9f1"
|
||||
@click="addItem(item)"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col v-if="add_item_list.length > 0">
|
||||
추가할 콘텐츠
|
||||
<v-simple-table>
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in add_item_list"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
color="#3bb9f1"
|
||||
@click="removeItem(item)"
|
||||
>
|
||||
제거
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="addItemInCuration"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_add_series_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
시리즈 추가
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="search_word"
|
||||
label="시리즈 제목"
|
||||
@keyup.enter="searchSeriesItem"
|
||||
>
|
||||
<v-btn
|
||||
slot="append"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
@click="searchSeriesItem"
|
||||
>
|
||||
검색
|
||||
</v-btn>
|
||||
</v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="search_item_list.length > 0 || add_item_list.length > 0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
검색결과
|
||||
<v-simple-table>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in search_item_list"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
color="#3bb9f1"
|
||||
@click="addItem(item)"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col v-if="add_item_list.length > 0">
|
||||
추가할 시리즈
|
||||
<v-simple-table>
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in add_item_list"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
color="#3bb9f1"
|
||||
@click="removeItem(item)"
|
||||
>
|
||||
제거
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="addItemInCuration"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text v-if="selected_item !== null">
|
||||
{{ selected_item.title }} 삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-text v-else>
|
||||
삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="removeItemInCuration"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from 'vuedraggable';
|
||||
import * as api from "@/api/audio_content"
|
||||
import VueShowMoreText from 'vue-show-more-text'
|
||||
|
||||
export default {
|
||||
name: 'ContentCurationDetail',
|
||||
|
||||
components: {VueShowMoreText, Draggable},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
curation_id: 0,
|
||||
curation_title: '',
|
||||
curation_description: '',
|
||||
is_series: false,
|
||||
is_adult: false,
|
||||
|
||||
items: [],
|
||||
|
||||
show_add_series_dialog: false,
|
||||
show_add_content_dialog: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
|
||||
search_word: '',
|
||||
selected_item: null,
|
||||
add_item_list: [],
|
||||
search_item_list: [],
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.curation_id = this.$route.params.curation_id
|
||||
this.curation_title = this.$route.params.title
|
||||
this.curation_description = this.$route.params.description
|
||||
this.is_series = this.$route.params.is_series
|
||||
this.is_adult = this.$route.params.is_adult
|
||||
await this.getCurationItems()
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.search_word = ''
|
||||
this.add_item_list = []
|
||||
this.search_item_list = []
|
||||
this.selected_item = null
|
||||
this.show_add_series_dialog = false
|
||||
this.show_add_content_dialog = false
|
||||
this.show_delete_confirm_dialog = false
|
||||
},
|
||||
|
||||
deleteConfirm(item) {
|
||||
this.selected_item = item
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
showAddContent() {
|
||||
this.show_add_content_dialog = true
|
||||
},
|
||||
|
||||
showAddSeries() {
|
||||
this.show_add_series_dialog = true
|
||||
},
|
||||
|
||||
addItem(item) {
|
||||
this.search_item_list = this.search_item_list.filter((t) => {
|
||||
return t.id !== item.id
|
||||
});
|
||||
this.add_item_list.push(item)
|
||||
},
|
||||
|
||||
removeItem(item) {
|
||||
this.add_item_list = this.add_item_list.filter((t) => {
|
||||
return t.id !== item.id
|
||||
});
|
||||
this.search_item_list.push(item)
|
||||
},
|
||||
|
||||
async onDropCallback(items) {
|
||||
const ids = items.map((item) => {
|
||||
return item.id
|
||||
})
|
||||
|
||||
try {
|
||||
this.is_loading = true
|
||||
const res = await api.updateItemInCurationOrders(this.curation_id, ids)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess(res.data.message)
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async searchContentItem() {
|
||||
if (this.search_word.length < 2) {
|
||||
this.notifyError('검색어를 2글자 이상 입력하세요.')
|
||||
return
|
||||
}
|
||||
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.searchContentItem(this.curation_id, this.search_word)
|
||||
|
||||
if (res.data.success === true) {
|
||||
this.search_item_list = res.data.data
|
||||
if (res.data.data.length <= 0) {
|
||||
this.notifyError('검색결과가 없습니다.')
|
||||
}
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async searchSeriesItem() {
|
||||
if (this.search_word.length < 2) {
|
||||
this.notifyError('검색어를 2글자 이상 입력하세요.')
|
||||
return
|
||||
}
|
||||
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.searchSeriesItem(this.curation_id, this.search_word)
|
||||
|
||||
if (res.data.success === true) {
|
||||
this.search_item_list = res.data.data
|
||||
if (res.data.data.length <= 0) {
|
||||
this.notifyError('검색결과가 없습니다.')
|
||||
}
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async addItemInCuration() {
|
||||
this.is_loading = true
|
||||
const itemIdList = this.add_item_list.map((item) => {
|
||||
return item.id
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await api.addItemToCuration(this.curation_id, itemIdList)
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
await this.getCurationItems()
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async removeItemInCuration() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.removeItemInCuration(this.curation_id, this.selected_item.id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
await this.getCurationItems()
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async getCurationItems() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getCurationItems(this.curation_id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.items = res.data.data
|
||||
console.log(this.items)
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
429
src/views/Content/ContentHashTagCuration.vue
Normal file
429
src/views/Content/ContentHashTagCuration.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>태그 큐레이션</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-spacer />
|
||||
<v-col cols="3">
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showWriteDialog"
|
||||
>
|
||||
태그 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="curations"
|
||||
:loading="is_loading"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
hide-default-footer
|
||||
disable-pagination
|
||||
>
|
||||
<template v-slot:body="props">
|
||||
<draggable
|
||||
v-model="props.items"
|
||||
tag="tbody"
|
||||
@end="onDropCallback(props.items)"
|
||||
>
|
||||
<tr
|
||||
v-for="(item, index) in props.items"
|
||||
:key="index"
|
||||
>
|
||||
<td
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
{{ item.tag }}
|
||||
</td>
|
||||
<td>
|
||||
<h3 v-if="item.isAdult">
|
||||
O
|
||||
</h3>
|
||||
<h3 v-else>
|
||||
X
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<v-row>
|
||||
<v-col />
|
||||
<v-col>
|
||||
<v-btn
|
||||
:disabled="is_loading"
|
||||
@click="showModifyDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-btn
|
||||
:disabled="is_loading"
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col />
|
||||
</v-row>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-row>
|
||||
<v-dialog
|
||||
v-model="show_write_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title v-if="is_modify === true">
|
||||
태그 큐레이션 수정
|
||||
</v-card-title>
|
||||
<v-card-title v-else>
|
||||
태그 큐레이션 등록
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
태그
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="curation.tag"
|
||||
label="태그"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
19금
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<input
|
||||
v-model="curation.is_adult"
|
||||
type="checkbox"
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="is_modify === true"
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="modify"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="submit"
|
||||
>
|
||||
등록
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text>
|
||||
"{{ selected_curation.tag }}"을 삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="deleteCancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="deleteCuration"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from 'vuedraggable';
|
||||
import * as api from "@/api/audio_content"
|
||||
|
||||
export default {
|
||||
name: "ContentHashTagCuration",
|
||||
components: {Draggable},
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
is_modify: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
show_write_dialog: false,
|
||||
selected_curation: {},
|
||||
curation: {is_adult: false},
|
||||
curations: [],
|
||||
headers: [
|
||||
{
|
||||
text: '태그',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'tag',
|
||||
},
|
||||
{
|
||||
text: '19금',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'isAdult',
|
||||
},
|
||||
{
|
||||
text: '관리',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'management'
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getHashTagCurations();
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
showWriteDialog() {
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
showModifyDialog(item) {
|
||||
this.is_modify = true
|
||||
this.selected_curation = item
|
||||
|
||||
this.curation.id = item.id
|
||||
this.curation.tag = item.tag
|
||||
this.curation.is_adult = item.isAdult
|
||||
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.curation = {is_adult: false}
|
||||
this.selected_curation = {}
|
||||
|
||||
this.is_modify = false
|
||||
this.show_write_dialog = false
|
||||
},
|
||||
|
||||
handleItemClick(item) {
|
||||
this.$router.push(
|
||||
{
|
||||
name: 'ContentHashTagCurationDetail',
|
||||
params: {
|
||||
curation_id: item.id,
|
||||
tag: item.tag,
|
||||
is_adult: item.isAdult
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (
|
||||
this.curation.tag === null ||
|
||||
this.curation.tag === undefined ||
|
||||
this.curation.tag.trim().length <= 0
|
||||
) {
|
||||
this.notifyError("태그를 입력하세요")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
deleteConfirm(curation) {
|
||||
this.selected_curation = curation
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
deleteCancel() {
|
||||
this.selected_curation = {}
|
||||
this.show_delete_confirm_dialog = false
|
||||
},
|
||||
|
||||
async getHashTagCurations() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getHashTagCurations()
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.curations = res.data.data
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) return;
|
||||
if (this.is_loading) return;
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
const request = {
|
||||
tag: this.curation.tag,
|
||||
isAdult: this.curation.is_adult
|
||||
}
|
||||
|
||||
const res = await api.saveHashTagCuration(request)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess('등록되었습니다.')
|
||||
|
||||
this.curations = []
|
||||
await this.getHashTagCurations()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async modify() {
|
||||
if (!this.validate()) return;
|
||||
if (this.is_loading) return;
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
let request = {id: this.curation.id}
|
||||
if (this.selected_curation.tag !== this.curation.tag && this.curation.tag.trim().length > 0) {
|
||||
request.tag = this.curation.tag
|
||||
}
|
||||
|
||||
if (this.selected_curation.isAdult !== this.curation.is_adult) {
|
||||
request.isAdult = this.curation.is_adult
|
||||
}
|
||||
|
||||
const res = await api.modifyHashTagCuration(request)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess('수정되었습니다.')
|
||||
|
||||
this.curations = []
|
||||
await this.getHashTagCurations()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async deleteCuration() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
let request = {id: this.selected_curation.id, isActive: false}
|
||||
|
||||
const res = await api.modifyHashTagCuration(request)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.cancel()
|
||||
this.notifySuccess('삭제되었습니다.')
|
||||
|
||||
this.curations = []
|
||||
await this.getHashTagCurations()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async onDropCallback(items) {
|
||||
this.curations = items
|
||||
const ids = items.map((item) => {
|
||||
return item.id
|
||||
})
|
||||
|
||||
try {
|
||||
this.is_loading = true
|
||||
const res = await api.updateHashTagCurationOrders(ids)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess(res.data.message)
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
450
src/views/Content/ContentHashTagCurationDetail.vue
Normal file
450
src/views/Content/ContentHashTagCurationDetail.vue
Normal file
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>{{ tag }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="4"
|
||||
align="right"
|
||||
>
|
||||
19금 :
|
||||
</v-col>
|
||||
<v-col
|
||||
align="left"
|
||||
>
|
||||
<div v-if="is_adult">
|
||||
O
|
||||
</div>
|
||||
<div v-else>
|
||||
X
|
||||
</div>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showAddContent"
|
||||
>
|
||||
콘텐츠 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-simple-table class="elevation-10">
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
썸네일
|
||||
</th>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th class="text-center">
|
||||
내용
|
||||
</th>
|
||||
<th class="text-center">
|
||||
크리에이터
|
||||
</th>
|
||||
<th class="text-center">
|
||||
19금
|
||||
</th>
|
||||
<th class="text-center">
|
||||
관리
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
v-model="items"
|
||||
tag="tbody"
|
||||
@end="onDropCallback(items)"
|
||||
>
|
||||
<tr
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
>
|
||||
<td align="center">
|
||||
<v-img
|
||||
max-width="70"
|
||||
max-height="70"
|
||||
:src="item.coverImageUrl"
|
||||
class="rounded-circle"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<vue-show-more-text
|
||||
:text="item.title"
|
||||
:lines="3"
|
||||
/>
|
||||
</td>
|
||||
<td style="max-width: 200px !important; word-break:break-all; height: auto;">
|
||||
<vue-show-more-text
|
||||
:text="item.desc"
|
||||
:lines="3"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ item.creatorNickname }}</td>
|
||||
<td>
|
||||
<div v-if="item.isAdult">
|
||||
O
|
||||
</div>
|
||||
<div v-else>
|
||||
X
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn
|
||||
:disabled="is_loading"
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_add_content_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
콘텐츠 추가
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="search_word"
|
||||
label="콘텐츠 제목"
|
||||
@keyup.enter="searchContentItem"
|
||||
>
|
||||
<v-btn
|
||||
slot="append"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
@click="searchContentItem"
|
||||
>
|
||||
검색
|
||||
</v-btn>
|
||||
</v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="search_item_list.length > 0 || add_item_list.length > 0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
검색결과
|
||||
<v-simple-table>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in search_item_list"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
color="#3bb9f1"
|
||||
@click="addItem(item)"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col v-if="add_item_list.length > 0">
|
||||
추가할 콘텐츠
|
||||
<v-simple-table>
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
제목
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in add_item_list"
|
||||
:key="item.id"
|
||||
>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
color="#3bb9f1"
|
||||
@click="removeItem(item)"
|
||||
>
|
||||
제거
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="addItemInHashTagCuration"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text v-if="selected_item !== null">
|
||||
{{ selected_item.title }} 삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-text v-else>
|
||||
삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="removeItemInHashTagCuration"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from 'vuedraggable';
|
||||
import * as api from "@/api/audio_content"
|
||||
import VueShowMoreText from 'vue-show-more-text'
|
||||
|
||||
export default {
|
||||
name: "ContentHashTagCurationDetail",
|
||||
|
||||
components: {VueShowMoreText, Draggable},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
curation_id: 0,
|
||||
tag: '',
|
||||
is_adult: false,
|
||||
|
||||
items: [],
|
||||
|
||||
show_add_content_dialog: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
|
||||
search_word: '',
|
||||
selected_item: null,
|
||||
add_item_list: [],
|
||||
search_item_list: [],
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.curation_id = this.$route.params.curation_id
|
||||
this.tag = this.$route.params.tag
|
||||
this.is_adult = this.$route.params.is_adult
|
||||
await this.getHashTagCurationItems()
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.search_word = ''
|
||||
this.add_item_list = []
|
||||
this.search_item_list = []
|
||||
this.selected_item = null
|
||||
this.show_add_content_dialog = false
|
||||
this.show_delete_confirm_dialog = false
|
||||
},
|
||||
|
||||
deleteConfirm(item) {
|
||||
this.selected_item = item
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
showAddContent() {
|
||||
this.show_add_content_dialog = true
|
||||
},
|
||||
|
||||
addItem(item) {
|
||||
this.search_item_list = this.search_item_list.filter((t) => {
|
||||
return t.id !== item.id
|
||||
});
|
||||
this.add_item_list.push(item)
|
||||
},
|
||||
|
||||
removeItem(item) {
|
||||
this.add_item_list = this.add_item_list.filter((t) => {
|
||||
return t.id !== item.id
|
||||
});
|
||||
this.search_item_list.push(item)
|
||||
},
|
||||
|
||||
async onDropCallback(items) {
|
||||
const ids = items.map((item) => {
|
||||
return item.id
|
||||
})
|
||||
|
||||
try {
|
||||
this.is_loading = true
|
||||
const res = await api.updateItemInHashTagCurationOrders(this.curation_id, ids)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess(res.data.message)
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async searchContentItem() {
|
||||
if (this.search_word.length < 2) {
|
||||
this.notifyError('검색어를 2글자 이상 입력하세요.')
|
||||
return
|
||||
}
|
||||
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.searchHashTagContentItem(this.curation_id, this.search_word)
|
||||
|
||||
if (res.data.success === true) {
|
||||
this.search_item_list = res.data.data
|
||||
if (res.data.data.length <= 0) {
|
||||
this.notifyError('검색결과가 없습니다.')
|
||||
}
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async addItemInHashTagCuration() {
|
||||
this.is_loading = true
|
||||
const itemIdList = this.add_item_list.map((item) => {
|
||||
return item.id
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await api.addItemToHashTagCuration(this.curation_id, itemIdList)
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
await this.getHashTagCurationItems()
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async removeItemInHashTagCuration() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.removeItemInHashTagCuration(this.curation_id, this.selected_item.id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
await this.getHashTagCurationItems()
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async getHashTagCurationItems() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getHashTagCurationItems(this.curation_id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.items = res.data.data
|
||||
console.log(this.items)
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@@ -63,9 +63,6 @@
|
||||
<th class="text-center">
|
||||
내용
|
||||
</th>
|
||||
<th class="text-center">
|
||||
큐레이션
|
||||
</th>
|
||||
<th class="text-center">
|
||||
크리에이터
|
||||
</th>
|
||||
@@ -134,7 +131,6 @@
|
||||
:lines="3"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ item.curationTitle || '없음' }}</td>
|
||||
<td>{{ item.creatorNickname }}</td>
|
||||
<td>{{ item.theme }}</td>
|
||||
<td style="max-width: 100px !important; word-break:break-all; height: auto;">
|
||||
@@ -307,22 +303,6 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
큐레이션
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-select
|
||||
v-model="audio_content.curation_id"
|
||||
:items="curations"
|
||||
item-text="title"
|
||||
item-value="value"
|
||||
label="큐레이션 선택"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
@@ -415,7 +395,6 @@ export default {
|
||||
search_word: '',
|
||||
audio_content: {},
|
||||
audio_contents: [],
|
||||
curations: [],
|
||||
themeList: [],
|
||||
selected_audio_content: {},
|
||||
utm_source: '',
|
||||
@@ -426,7 +405,6 @@ export default {
|
||||
|
||||
async created() {
|
||||
await this.getAudioContentThemeList();
|
||||
await this.getCurations()
|
||||
await this.getAudioContent()
|
||||
},
|
||||
|
||||
@@ -455,7 +433,6 @@ export default {
|
||||
this.audio_content.id = item.audioContentId
|
||||
this.audio_content.title = item.title
|
||||
this.audio_content.detail = item.detail
|
||||
this.audio_content.curation_id = item.curationId
|
||||
this.audio_content.theme_id = item.themeId
|
||||
this.audio_content.is_adult = item.isAdult
|
||||
this.audio_content.is_comment_available = item.isCommentAvailable
|
||||
@@ -513,10 +490,6 @@ export default {
|
||||
request.detail = this.audio_content.detail
|
||||
}
|
||||
|
||||
if (this.selected_audio_content.curationId !== this.audio_content.curation_id) {
|
||||
request.curationId = this.audio_content.curation_id
|
||||
}
|
||||
|
||||
if (this.selected_audio_content.themeId !== this.audio_content.theme_id) {
|
||||
request.themeId = this.audio_content.theme_id
|
||||
}
|
||||
@@ -598,26 +571,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async getCurations() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
const res = await api.getCurations()
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.curations = res.data.data.map((curation) => {
|
||||
return {title: curation.title, value: curation.id}
|
||||
})
|
||||
|
||||
this.curations.unshift({title: '없음', value: 0})
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async getAudioContent() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
|
@@ -16,11 +16,25 @@
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="10" />
|
||||
<v-col cols="9">
|
||||
<v-radio-group
|
||||
v-model="selected_tab_id"
|
||||
row
|
||||
@change="getBanners"
|
||||
>
|
||||
<v-radio
|
||||
v-for="tab in tabs"
|
||||
:key="tab.tabId"
|
||||
:label="tab.title"
|
||||
:value="tab.tabId"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#9970ff"
|
||||
color="#3BB9F1"
|
||||
dark
|
||||
depressed
|
||||
v-bind="attrs"
|
||||
@@ -69,7 +83,32 @@
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>배너 등록</v-card-title>
|
||||
<v-card-title v-if="is_modify === true">
|
||||
배너 수정
|
||||
</v-card-title>
|
||||
<v-card-title v-else>
|
||||
배너 등록
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
메인 탭
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-radio-group
|
||||
v-model="banner.tab_id"
|
||||
row
|
||||
>
|
||||
<v-radio
|
||||
v-for="tab in tabs"
|
||||
:key="tab.tabId"
|
||||
:label="tab.title"
|
||||
:value="tab.tabId"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
@@ -92,6 +131,10 @@
|
||||
value="EVENT"
|
||||
label="이벤트"
|
||||
/>
|
||||
<v-radio
|
||||
value="SERIES"
|
||||
label="시리즈"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -102,12 +145,19 @@
|
||||
크리에이터
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-select
|
||||
v-model="banner.creator_id"
|
||||
<v-combobox
|
||||
v-model="banner.creator_nickname"
|
||||
:items="creators"
|
||||
:loading="is_loading"
|
||||
:search-input.sync="search_query_creator"
|
||||
label="크리에이터를 검색하세요"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
label="크리에이터 선택"
|
||||
no-data-text="No results found"
|
||||
hide-selected
|
||||
clearable
|
||||
@change="onSelect"
|
||||
@update:search-input="onSearchUpdate"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -126,6 +176,29 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="banner.type === 'SERIES'">
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
시리즈
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-combobox
|
||||
v-model="banner.series_title"
|
||||
:items="series"
|
||||
:loading="is_loading"
|
||||
:search-input.sync="search_query_series"
|
||||
label="시리즈를 검색하세요"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
no-data-text="No results found"
|
||||
hide-selected
|
||||
clearable
|
||||
@change="onSelectSeries"
|
||||
@update:search-input="onSearchSeriesUpdate"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text v-else>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
@@ -234,7 +307,9 @@
|
||||
|
||||
<script>
|
||||
import Draggable from "vuedraggable";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
import * as seriesApi from "@/api/audio_content_series"
|
||||
import * as memberApi from "@/api/member";
|
||||
import * as eventApi from "@/api/event";
|
||||
import * as api from "@/api/audio_content"
|
||||
@@ -246,22 +321,46 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_selecting: false,
|
||||
is_loading: false,
|
||||
is_modify: false,
|
||||
show_write_dialog: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
selected_banner: {},
|
||||
banner: {type: 'CREATOR'},
|
||||
banner: {type: 'CREATOR', tab_id: 1},
|
||||
banners: [],
|
||||
events: [],
|
||||
creators: [],
|
||||
series: [],
|
||||
search_query_creator: '',
|
||||
search_query_series: '',
|
||||
tabs: [],
|
||||
selected_tab_id: 1
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
search_query_creator() {
|
||||
if (!this.is_selecting) {
|
||||
this.debouncedSearch();
|
||||
}
|
||||
},
|
||||
|
||||
search_query_series() {
|
||||
if (!this.is_selecting) {
|
||||
this.debouncedSearchSeries();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getCreatorAllList()
|
||||
await this.getEvents()
|
||||
await this.getBanners()
|
||||
await this.getAudioContentMainTabList()
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.debouncedSearch = debounce(this.searchCreator, 500);
|
||||
this.debouncedSearchSeries = debounce(this.searchSeries, 500);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -277,9 +376,13 @@ export default {
|
||||
|
||||
cancel() {
|
||||
this.is_modify = false
|
||||
this.is_selecting = false
|
||||
this.show_write_dialog = false
|
||||
this.banner = {type: 'CREATOR'}
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.banner = {type: 'CREATOR', tab_id: 1}
|
||||
this.selected_banner = {}
|
||||
this.search_query_creator = ''
|
||||
this.search_query_series = ''
|
||||
},
|
||||
|
||||
notifyError(message) {
|
||||
@@ -290,10 +393,33 @@ export default {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
async getAudioContentMainTabList() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getAudioContentMainTabList()
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const data = res.data.data
|
||||
|
||||
this.tabs = data
|
||||
this.selected_tab_id = data[0].tabId
|
||||
await this.getBanners()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
showModifyBannerDialog(banner) {
|
||||
this.is_modify = true
|
||||
this.selected_banner = banner
|
||||
|
||||
this.is_selecting = true; // 선택 중 플래그 활성화
|
||||
|
||||
this.banner.id = banner.id
|
||||
this.banner.type = banner.type
|
||||
this.banner.thumbnail_image_url = banner.thumbnailImageUrl
|
||||
@@ -301,8 +427,15 @@ export default {
|
||||
this.banner.event_thumbnail_image = banner.eventThumbnailImage
|
||||
this.banner.creator_id = banner.creatorId
|
||||
this.banner.creator_nickname = banner.creatorNickname
|
||||
this.banner.series_id = banner.seriesId
|
||||
this.banner.series_title = banner.seriesTitle
|
||||
this.banner.link = banner.link
|
||||
this.banner.is_adult = banner.isAdult
|
||||
this.banner.tab_id = banner.tabId
|
||||
|
||||
setTimeout(() => {
|
||||
this.is_selecting = false; // 선택 상태 해제
|
||||
}, 1000);
|
||||
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
@@ -320,6 +453,13 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.banner.type === 'SERIES' &&
|
||||
(this.banner.series_id === null || this.banner.series_id === undefined)) {
|
||||
this.notifyError("시리즈를 선택하세요")
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.banner.type === 'LINK' &&
|
||||
(this.banner.link === null || this.banner.link === undefined || this.banner.link.trim().length <= 0)
|
||||
@@ -366,6 +506,12 @@ export default {
|
||||
request.eventId = this.banner.event_id
|
||||
} else if (this.banner.type === 'LINK') {
|
||||
request.link = this.banner.link
|
||||
} else if (this.banner.type === 'SERIES') {
|
||||
request.seriesId = this.banner.series_id
|
||||
}
|
||||
|
||||
if (this.banner.tab_id !== 1) {
|
||||
request.tabId = this.banner.tab_id
|
||||
}
|
||||
|
||||
formData.append("request", JSON.stringify(request))
|
||||
@@ -417,6 +563,15 @@ export default {
|
||||
request.creatorId = this.banner.creator_id
|
||||
}
|
||||
|
||||
if (
|
||||
this.selected_banner.series_id !== this.banner.series_id &&
|
||||
this.banner.series_id !== null &&
|
||||
this.banner.series_id !== undefined
|
||||
) {
|
||||
request.type = this.banner.type
|
||||
request.seriesId = this.banner.series_id
|
||||
}
|
||||
|
||||
if (
|
||||
this.selected_banner.link !== this.banner.link &&
|
||||
this.banner.link !== null &&
|
||||
@@ -430,6 +585,10 @@ export default {
|
||||
request.isAdult = this.banner.is_adult
|
||||
}
|
||||
|
||||
if (this.selected_banner.tabId !== this.banner.tab_id) {
|
||||
request.tabId = this.banner.tab_id
|
||||
}
|
||||
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
const res = await api.modifyBanner(formData)
|
||||
@@ -485,34 +644,92 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async getCreatorAllList() {
|
||||
this.is_loading = true
|
||||
async searchCreator() {
|
||||
if (this.search_query_creator === null || this.search_query_creator.length < 2) {
|
||||
this.creators = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.is_loading = true;
|
||||
|
||||
try {
|
||||
const res = await memberApi.getCreatorAllList()
|
||||
const res = await memberApi.searchCreator(this.search_query_creator, 1);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.creators = res.data.data.map((item) => {
|
||||
return {name: item.nickname, value: item.id}
|
||||
this.creators = res.data.data.items.map((item) => {
|
||||
return {name: item.nickname, value: item.id}
|
||||
})
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
this.creators = []
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
onSelect(value) {
|
||||
this.banner.creator_id = value.value
|
||||
this.is_selecting = true; // 선택 중 플래그 활성화
|
||||
setTimeout(() => {
|
||||
this.is_selecting = false; // 선택 상태 해제
|
||||
}, 0);
|
||||
},
|
||||
|
||||
onSearchUpdate(value) {
|
||||
if (!this.is_selecting) {
|
||||
this.search_query_creator = value
|
||||
}
|
||||
},
|
||||
|
||||
async searchSeries() {
|
||||
if (this.search_query_series === null || this.search_query_series.length < 2) {
|
||||
this.series = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.is_loading = true;
|
||||
|
||||
try {
|
||||
const res = await seriesApi.searchSeriesList(this.search_query_series);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.series = res.data.data.map((item) => {
|
||||
return {name: item.title, value: item.id}
|
||||
})
|
||||
} else {
|
||||
this.series = []
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
onSelectSeries(value) {
|
||||
this.banner.series_id = value.value
|
||||
this.is_selecting = true; // 선택 중 플래그 활성화
|
||||
setTimeout(() => {
|
||||
this.is_selecting = false; // 선택 상태 해제
|
||||
}, 0);
|
||||
},
|
||||
|
||||
onSearchSeriesUpdate(value) {
|
||||
if (!this.is_selecting) {
|
||||
this.search_query_series = value
|
||||
}
|
||||
},
|
||||
|
||||
async getEvents() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
const res = await eventApi.getEvents(1)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.events = res.data.data.eventList.map((item) => {
|
||||
this.events = res.data.data.map((item) => {
|
||||
return {title: item.title, value: item.id}
|
||||
})
|
||||
} else {
|
||||
@@ -529,7 +746,7 @@ export default {
|
||||
async getBanners() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
const res = await api.getBannerList()
|
||||
const res = await api.getBannerList(this.selected_tab_id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.banners = res.data.data
|
||||
} else {
|
||||
|
@@ -53,6 +53,24 @@
|
||||
<template v-slot:item.communitySettlementRatio="{ item }">
|
||||
{{ item.communitySettlementRatio }}%
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
text
|
||||
@click="openEdit(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
small
|
||||
color="red"
|
||||
text
|
||||
@click="confirmDelete(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -73,13 +91,20 @@
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>크리에이터 정산비율</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card-title>{{ is_edit ? '크리에이터 정산비율 수정' : '크리에이터 정산비율' }}</v-card-title>
|
||||
<v-card-text v-show="!is_edit">
|
||||
<v-text-field
|
||||
v-model="creator_settlement_ratio.creator_id"
|
||||
label="크리에이터 번호"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text v-show="is_edit">
|
||||
<v-text-field
|
||||
v-model="creator_settlement_ratio.nickname"
|
||||
disabled
|
||||
label="크리에이터 닉네임"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="creator_settlement_ratio.subsidy"
|
||||
@@ -118,7 +143,7 @@
|
||||
text
|
||||
@click="validate"
|
||||
>
|
||||
등록하기
|
||||
{{ is_edit ? '수정하기' : '등록하기' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -142,6 +167,8 @@ export default {
|
||||
items: [],
|
||||
creator_settlement_ratio: {},
|
||||
show_write_dialog: false,
|
||||
is_edit: false,
|
||||
editing_item_id: null,
|
||||
headers: [
|
||||
{
|
||||
text: '닉네임',
|
||||
@@ -173,6 +200,12 @@ export default {
|
||||
sortable: false,
|
||||
value: 'communitySettlementRatio',
|
||||
},
|
||||
{
|
||||
text: '관리',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'actions',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
@@ -191,11 +224,16 @@ export default {
|
||||
},
|
||||
|
||||
showWriteDialog() {
|
||||
this.is_edit = false
|
||||
this.editing_item_id = null
|
||||
this.creator_settlement_ratio = {}
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.creator_settlement_ratio = {}
|
||||
this.is_edit = false
|
||||
this.editing_item_id = null
|
||||
this.show_write_dialog = false
|
||||
},
|
||||
|
||||
@@ -225,7 +263,11 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
this.createCreatorSettlementRatio();
|
||||
if (this.is_edit) {
|
||||
this.updateCreatorSettlementRatio();
|
||||
} else {
|
||||
this.createCreatorSettlementRatio();
|
||||
}
|
||||
},
|
||||
|
||||
async createCreatorSettlementRatio() {
|
||||
@@ -253,6 +295,71 @@ export default {
|
||||
this.is_loading = false
|
||||
},
|
||||
|
||||
async updateCreatorSettlementRatio() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
try {
|
||||
// 수정은 생성과 동일한 파라미터를 전송 (memberId 기준)
|
||||
const payload = { ...this.creator_settlement_ratio }
|
||||
const res = await api.updateCreatorSettlementRatio(payload)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess(res.data.message || '수정되었습니다.')
|
||||
this.items = []
|
||||
await this.getSettlementRatio()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
openEdit(item) {
|
||||
this.is_edit = true
|
||||
this.editing_item_id = null
|
||||
this.creator_settlement_ratio = {
|
||||
creator_id: item.memberId,
|
||||
nickname: item.nickname,
|
||||
subsidy: item.subsidy,
|
||||
liveSettlementRatio: item.liveSettlementRatio,
|
||||
contentSettlementRatio: item.contentSettlementRatio,
|
||||
communitySettlementRatio: item.communitySettlementRatio,
|
||||
}
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
async confirmDelete(item) {
|
||||
try {
|
||||
const ok = await this.$dialog.confirm({ text: '삭제하시겠습니까?', title: '확인', actions: { false: '취소', true: '삭제' } })
|
||||
if (!ok) return
|
||||
} catch (e) {
|
||||
// 일부 구현체는 confirm이 boolean이 아닌 경우가 있음
|
||||
}
|
||||
this.deleteCreatorSettlementRatio(item)
|
||||
},
|
||||
|
||||
async deleteCreatorSettlementRatio(item) {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
try {
|
||||
const memberId = item.memberId
|
||||
const res = await api.deleteCreatorSettlementRatio(memberId)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess(res.data.message || '삭제되었습니다.')
|
||||
this.items = this.items.filter(x => (x.memberId) !== memberId)
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async getSettlementRatio() {
|
||||
this.is_loading = true
|
||||
|
||||
@@ -279,10 +386,6 @@ export default {
|
||||
},
|
||||
|
||||
async next() {
|
||||
if (this.search_word.length < 2) {
|
||||
this.search_word = ''
|
||||
}
|
||||
|
||||
await this.getSettlementRatio()
|
||||
},
|
||||
},
|
||||
|
361
src/views/Marketing/MarketingAdStatisticsView.vue
Normal file
361
src/views/Marketing/MarketingAdStatisticsView.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>광고 통계</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="getTodayStatistics"
|
||||
>
|
||||
오늘
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="getYesterdayStatistics"
|
||||
>
|
||||
어제
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="getWeekStatistics"
|
||||
>
|
||||
7일 전
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<datetime
|
||||
v-model="start_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
~
|
||||
</v-col>
|
||||
<v-col>
|
||||
<datetime
|
||||
v-model="end_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="getStatistics"
|
||||
>
|
||||
조회
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:loading="is_loading"
|
||||
:items-per-page="-1"
|
||||
class="elevation-1"
|
||||
hide-default-footer
|
||||
>
|
||||
<template slot="body.prepend">
|
||||
<tr class="summary">
|
||||
<td colspan="4">
|
||||
총합계
|
||||
</td>
|
||||
<td>{{ sumField('launchCount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('signUpCount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('firstPaymentCount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('firstPaymentTotalAmount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('repeatPaymentCount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('repeatPaymentTotalAmount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('allPaymentCount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('allPaymentTotalAmount').toLocaleString() }}</td>
|
||||
<td>{{ sumField('loginCount').toLocaleString() }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.date="{ item }">
|
||||
{{ item.date }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.mediaGroup="{ item }">
|
||||
{{ item.mediaGroup }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.pid="{ item }">
|
||||
{{ item.pid }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.pidName="{ item }">
|
||||
{{ item.pidName }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.launchCount="{ item }">
|
||||
{{ item.launchCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.signUpCount="{ item }">
|
||||
{{ item.signUpCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.firstPaymentCount="{ item }">
|
||||
{{ item.firstPaymentCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.firstPaymentTotalAmount="{ item }">
|
||||
{{ item.firstPaymentTotalAmount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.repeatPaymentCount="{ item }">
|
||||
{{ item.repeatPaymentCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.repeatPaymentTotalAmount="{ item }">
|
||||
{{ item.repeatPaymentTotalAmount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.allPaymentCount="{ item }">
|
||||
{{ item.allPaymentCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.allPaymentTotalAmount="{ item }">
|
||||
{{ item.allPaymentTotalAmount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.loginCount="{ item }">
|
||||
{{ item.loginCount.toLocaleString() }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="next"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from "@/api/marketing";
|
||||
import datetime from "vuejs-datetimepicker";
|
||||
|
||||
export default {
|
||||
name: 'MarketingAdStatisticsView',
|
||||
components: {datetime},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
page: 1,
|
||||
total_page: 0,
|
||||
|
||||
headers: [
|
||||
{
|
||||
text: '일자',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'date',
|
||||
},
|
||||
{
|
||||
text: '매체',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'mediaGroup',
|
||||
},
|
||||
{
|
||||
text: 'pid',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'pid',
|
||||
},
|
||||
{
|
||||
text: 'pid명',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'pidName',
|
||||
},
|
||||
{
|
||||
text: '앱 실행',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'launchCount',
|
||||
},
|
||||
{
|
||||
text: '가입수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'signUpCount',
|
||||
},
|
||||
{
|
||||
text: '첫결제건수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'firstPaymentCount',
|
||||
},
|
||||
{
|
||||
text: '첫결제금액',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'firstPaymentTotalAmount',
|
||||
},
|
||||
{
|
||||
text: '재결제건수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'repeatPaymentCount',
|
||||
},
|
||||
{
|
||||
text: '재결제금액',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'repeatPaymentTotalAmount',
|
||||
},
|
||||
{
|
||||
text: '총 결제건수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'allPaymentCount',
|
||||
},
|
||||
{
|
||||
text: '총 결제금액',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'allPaymentTotalAmount',
|
||||
},
|
||||
{
|
||||
text: '로그인 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'loginCount',
|
||||
}
|
||||
],
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getTodayStatistics();
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
sumField(key) {
|
||||
return this.items.reduce((a, b) => a + (b[key] || 0), 0)
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
async next() {
|
||||
await this.getStatistics()
|
||||
},
|
||||
|
||||
async getStatistics() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getStatistics(this.start_date, this.end_date, this.page);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
let data = res.data.data
|
||||
this.items = data.items
|
||||
|
||||
const totalPage = Math.ceil(data.totalCount / 20)
|
||||
if (totalPage <= 0)
|
||||
this.total_page = 1
|
||||
else
|
||||
this.total_page = totalPage
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
|
||||
this.is_loading = false
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async getTodayStatistics() {
|
||||
const today = new Date();
|
||||
|
||||
this.start_date = this.formatDate(today);
|
||||
this.end_date = this.formatDate(today);
|
||||
|
||||
await this.getStatistics()
|
||||
},
|
||||
|
||||
async getYesterdayStatistics() {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
this.start_date = this.formatDate(yesterday);
|
||||
this.end_date = this.formatDate(yesterday);
|
||||
await this.getStatistics()
|
||||
},
|
||||
|
||||
async getWeekStatistics() {
|
||||
const week = new Date();
|
||||
week.setDate(week.getDate() - 8);
|
||||
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
this.start_date = this.formatDate(week);
|
||||
this.end_date = this.formatDate(yesterday);
|
||||
await this.getStatistics()
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summary {
|
||||
background-color: #c4dbf1;
|
||||
}
|
||||
</style>
|
596
src/views/Marketing/MarketingMediaPartnerCodeView.vue
Normal file
596
src/views/Marketing/MarketingMediaPartnerCodeView.vue
Normal file
@@ -0,0 +1,596 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>매체 파트너 코드</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="8" />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showWriteDialog"
|
||||
>
|
||||
매체 파트너 코드 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:loading="is_loading"
|
||||
:items-per-page="-1"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
hide-default-footer
|
||||
>
|
||||
<template v-slot:item.id="{ item }">
|
||||
{{ item.id }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.mediaGroup="{ item }">
|
||||
{{ item.mediaGroup }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.pid="{ item }">
|
||||
{{ item.pid }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.pidName="{ item }">
|
||||
{{ item.pidName }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.type="{ item }">
|
||||
<span v-if="item.type === 'SERIES'">시리즈(오리지널 콘텐츠)</span>
|
||||
<span v-else-if="item.type === 'CONTENT'">개별 콘텐츠</span>
|
||||
<span v-else-if="item.type === 'LIVE'">라이브</span>
|
||||
<span v-else-if="item.type === 'CHANNEL'">채널</span>
|
||||
<span v-else>메인</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.createdAt="{ item }">
|
||||
{{ item.createdAt }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.utmSource="{ item }">
|
||||
{{ item.utmSource }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.utmMedium="{ item }">
|
||||
{{ item.utmMedium }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.link="{ item }">
|
||||
<v-btn @click="copyLink(item.link)">
|
||||
링크복사
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.isActive="{ item }">
|
||||
<div v-if="item.isActive">
|
||||
사용
|
||||
</div>
|
||||
<div v-else>
|
||||
미사용
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.management="{ item }">
|
||||
<v-btn
|
||||
:disabled="is_loading"
|
||||
@click="showModifyDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="next"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-dialog
|
||||
v-model="show_write_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>매체 파트너 코드 등록</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
매체 그룹
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="media_group"
|
||||
label="매체 그룹"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
PID
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="pid"
|
||||
label="PID"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
PID 명
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="pid_name"
|
||||
label="PID 명"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
구분
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-radio-group
|
||||
v-model="type"
|
||||
row
|
||||
>
|
||||
<v-radio
|
||||
v-for="item in type_list"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
utm_source
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="utm_source"
|
||||
label="UTM_SOURCE"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
utm_medium
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="utm_medium"
|
||||
label="UTM_MEDIUM"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-show="selected_media_partner_code !== null">
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
사용여부
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-radio-group
|
||||
v-model="is_active"
|
||||
row
|
||||
>
|
||||
<v-radio
|
||||
key="1"
|
||||
label="사용"
|
||||
:value="true"
|
||||
/>
|
||||
|
||||
<v-radio
|
||||
key="2"
|
||||
label="미사용"
|
||||
:value="false"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selected_media_partner_code !== null"
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="modify"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="validate"
|
||||
>
|
||||
등록
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from "@/api/marketing";
|
||||
|
||||
export default {
|
||||
name: "MarketingMediaPartnerCodeView",
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
page: 1,
|
||||
total_page: 0,
|
||||
show_write_dialog: false,
|
||||
|
||||
selected_media_partner_code: null,
|
||||
media_group: null,
|
||||
pid: null,
|
||||
pid_name: null,
|
||||
type: 'MAIN',
|
||||
is_active: true,
|
||||
utm_source: null,
|
||||
utm_medium: null,
|
||||
|
||||
type_list: [
|
||||
{
|
||||
label: '메인',
|
||||
value: 'MAIN'
|
||||
},
|
||||
{
|
||||
label: '시리즈(오리지널 콘텐츠 포함)',
|
||||
value: 'SERIES'
|
||||
},
|
||||
{
|
||||
label: '개별 콘텐츠',
|
||||
value: 'CONTENT'
|
||||
},
|
||||
{
|
||||
label: '라이브',
|
||||
value: 'LIVE'
|
||||
},
|
||||
{
|
||||
label: '채널',
|
||||
value: 'CHANNEL'
|
||||
},
|
||||
],
|
||||
|
||||
items: [],
|
||||
headers: [
|
||||
{
|
||||
text: '번호',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'id',
|
||||
},
|
||||
{
|
||||
text: '매체그룹',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'mediaGroup',
|
||||
},
|
||||
{
|
||||
text: 'Pid',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'pid',
|
||||
},
|
||||
{
|
||||
text: 'Pid명',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'pidName',
|
||||
},
|
||||
{
|
||||
text: '구분',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'type',
|
||||
},
|
||||
{
|
||||
text: '링크',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'link',
|
||||
},
|
||||
{
|
||||
text: '등록시간',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'createdAt',
|
||||
},
|
||||
{
|
||||
text: '기록추적(사용여부)',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'isActive',
|
||||
},
|
||||
{
|
||||
text: '관리',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'management',
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getMediaPartnerCodeList()
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
showWriteDialog() {
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
showModifyDialog(item) {
|
||||
this.selected_media_partner_code = item;
|
||||
this.media_group = item.mediaGroup;
|
||||
this.pid = item.pid
|
||||
this.pid_name = item.pidName
|
||||
this.type = item.type
|
||||
this.utm_source = item.utmSource
|
||||
this.utm_medium = item.utmMedium
|
||||
this.is_active = item.isActive;
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.selected_media_partner_code = null
|
||||
this.media_group = null
|
||||
this.pid = null
|
||||
this.pid_name = null
|
||||
this.type = 'MAIN'
|
||||
this.is_active = true
|
||||
this.utm_source = null
|
||||
this.utm_medium = null
|
||||
this.show_write_dialog = false
|
||||
},
|
||||
|
||||
async copyLink(link) {
|
||||
this.is_loading = true
|
||||
try {
|
||||
await navigator.clipboard.writeText(link)
|
||||
this.notifySuccess("링크가 복사되었습니다.")
|
||||
} catch (e) {
|
||||
this.notifyError("링크를 복사하지 못했습니다.")
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (this.media_group === null) {
|
||||
this.notifyError('매체 그룹을 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.pid === null) {
|
||||
this.notifyError('PID를 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.pid_name === null) {
|
||||
this.notifyError('PID명을 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
this.type !== 'MAIN' &&
|
||||
this.type !== 'SERIES' &&
|
||||
this.type !== 'CONTENT' &&
|
||||
this.type !== 'LIVE' &&
|
||||
this.type !== 'CHANNEL'
|
||||
) {
|
||||
this.notifyError('잘못된 광고타입입니다.')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.utm_source === null) {
|
||||
this.notifyError('utm_source를 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.utm_medium === null) {
|
||||
this.notifyError('utm_medium을 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
this.submit()
|
||||
},
|
||||
|
||||
async getMediaPartnerCodeList() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getMediaPartnerList(this.page);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
let data = res.data.data
|
||||
this.items = data.items
|
||||
|
||||
const totalPage = Math.ceil(data.totalCount / 20)
|
||||
if (totalPage <= 0)
|
||||
this.total_page = 1
|
||||
else
|
||||
this.total_page = totalPage
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
|
||||
this.is_loading = false
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const request = {
|
||||
mediaGroup: this.media_group,
|
||||
pid: this.pid,
|
||||
pidName: this.pid_name,
|
||||
type: this.type,
|
||||
utmSource: this.utm_source,
|
||||
utmMedium: this.utm_medium
|
||||
};
|
||||
const res = await api.createMediaPartner(request)
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess(res.data.message || '등록되었습니다.')
|
||||
|
||||
this.items = [];
|
||||
this.page = 1;
|
||||
this.total_page = 0;
|
||||
await this.getMediaPartnerCodeList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
|
||||
this.is_loading = false
|
||||
},
|
||||
|
||||
async modify() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const request = {
|
||||
id: this.selected_media_partner_code.id
|
||||
};
|
||||
|
||||
if (this.media_group !== this.selected_media_partner_code.mediaGroup) {
|
||||
request.mediaGroup = this.media_group
|
||||
}
|
||||
|
||||
if (this.pid !== this.selected_media_partner_code.pid) {
|
||||
request.pid = this.pid;
|
||||
}
|
||||
|
||||
if (this.pid_name !== this.selected_media_partner_code.pidName) {
|
||||
request.pidName = this.pid_name;
|
||||
}
|
||||
|
||||
if (this.type !== this.selected_media_partner_code.type) {
|
||||
request.type = this.type;
|
||||
}
|
||||
|
||||
if (this.utm_source !== this.selected_media_partner_code.utmSource) {
|
||||
request.utmSource = this.utm_source;
|
||||
}
|
||||
|
||||
if (this.utm_medium !== this.selected_media_partner_code.utmMedium) {
|
||||
request.utmMedium = this.utm_medium;
|
||||
}
|
||||
|
||||
if (this.is_active !== this.selected_media_partner_code.isActive) {
|
||||
request.isActive = this.is_active
|
||||
}
|
||||
|
||||
const res = await api.updateMediaPartner(request)
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess(res.data.message || '수정되었습니다.')
|
||||
|
||||
this.items = [];
|
||||
this.page = 1;
|
||||
this.total_page = 0;
|
||||
await this.getMediaPartnerCodeList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
|
||||
this.is_loading = false
|
||||
},
|
||||
|
||||
async next() {
|
||||
await this.getMediaPartnerCodeList()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -49,6 +49,9 @@
|
||||
<th class="text-center">
|
||||
회원타입
|
||||
</th>
|
||||
<th class="text-center">
|
||||
로그인 타입
|
||||
</th>
|
||||
<th class="text-center">
|
||||
OS
|
||||
</th>
|
||||
@@ -92,6 +95,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.userType }}</td>
|
||||
<td>{{ item.loginType }}</td>
|
||||
<td>
|
||||
<div v-if="item.container === 'aos'">
|
||||
Android
|
||||
@@ -189,6 +193,13 @@
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="confirmResetPassword"
|
||||
>
|
||||
비밀번호 재설정
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
@@ -208,6 +219,39 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-dialog
|
||||
v-model="show_confirm_reset_password_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ nickname }}님의 비밀번호를 재설정 하시겠습니까?
|
||||
</v-card-title>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="resetPassword"
|
||||
>
|
||||
비밀번호 재설정
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -228,7 +272,8 @@ export default {
|
||||
email: null,
|
||||
nickname: null,
|
||||
user_type: null,
|
||||
show_popup_dialog: false
|
||||
show_popup_dialog: false,
|
||||
show_confirm_reset_password_dialog: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -336,6 +381,7 @@ export default {
|
||||
this.nickname = null
|
||||
this.user_type = null
|
||||
this.show_popup_dialog = false
|
||||
this.show_confirm_reset_password_dialog = false
|
||||
},
|
||||
|
||||
async modify() {
|
||||
@@ -366,6 +412,32 @@ export default {
|
||||
|
||||
this.is_loading = false
|
||||
this.cancel()
|
||||
},
|
||||
|
||||
confirmResetPassword() {
|
||||
this.show_popup_dialog = false
|
||||
this.show_confirm_reset_password_dialog = true
|
||||
},
|
||||
|
||||
async resetPassword() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
const res = await api.resetPassword(this.member.id)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess(res.data.message)
|
||||
this.cancel()
|
||||
|
||||
this.page = 1
|
||||
await this.getMemberList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
|
||||
this.is_loading = false
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
283
src/views/Member/MemberStatisticsView.vue
Normal file
283
src/views/Member/MemberStatisticsView.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>일별 전체 회원 수</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-spacer />
|
||||
<v-col cols="2">
|
||||
<datetime
|
||||
v-model="start_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1">
|
||||
~
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<datetime
|
||||
v-model="end_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1" />
|
||||
|
||||
<v-col cols="2">
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="getStatistics"
|
||||
>
|
||||
조회
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:loading="is_loading"
|
||||
:items-per-page="-1"
|
||||
class="elevation-1"
|
||||
hide-default-footer
|
||||
>
|
||||
<template slot="body.prepend">
|
||||
<tr class="summary">
|
||||
<td>
|
||||
합계
|
||||
</td>
|
||||
<td>
|
||||
{{ total_sign_up_count }}
|
||||
</td>
|
||||
<td>
|
||||
{{ total_sign_up_email_count }}
|
||||
</td>
|
||||
<td>
|
||||
{{ total_sign_up_kakao_count }}
|
||||
</td>
|
||||
<td>
|
||||
{{ total_sign_up_google_count }}
|
||||
</td>
|
||||
<td>
|
||||
{{ total_auth_count }}
|
||||
</td>
|
||||
<td>
|
||||
{{ total_sign_out_count }}
|
||||
</td>
|
||||
<td>
|
||||
{{ total_payment_member_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.date="{ item }">
|
||||
{{ item.date }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.signUpCount="{ item }">
|
||||
{{ item.signUpCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.signUpEmailCount="{ item }">
|
||||
{{ item.signUpEmailCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.signUpKakaoCount="{ item }">
|
||||
{{ item.signUpKakaoCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.signUpGoogleCount="{ item }">
|
||||
{{ item.signUpGoogleCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.authCount="{ item }">
|
||||
{{ item.authCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.signOutCount="{ item }">
|
||||
{{ item.signOutCount.toLocaleString() }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.paymentMemberCount="{ item }">
|
||||
{{ item.paymentMemberCount.toLocaleString() }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="getStatistics"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from "@/api/member_statistics"
|
||||
import datetime from 'vuejs-datetimepicker';
|
||||
|
||||
export default {
|
||||
name: "MemberStatisticsView",
|
||||
components: {datetime},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
total_auth_count: 0,
|
||||
total_sign_up_count: 0,
|
||||
total_sign_up_email_count: 0,
|
||||
total_sign_up_kakao_count: 0,
|
||||
total_sign_up_google_count: 0,
|
||||
total_sign_out_count: 0,
|
||||
total_payment_member_count: 0,
|
||||
page: 1,
|
||||
total_page: 0,
|
||||
items: [],
|
||||
headers: [
|
||||
{
|
||||
text: '날짜',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'date',
|
||||
},
|
||||
{
|
||||
text: '회원가입 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'signUpCount',
|
||||
},
|
||||
{
|
||||
text: '이메일 가입 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'signUpEmailCount',
|
||||
},
|
||||
{
|
||||
text: '카카오 가입 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'signUpKakaoCount',
|
||||
},
|
||||
{
|
||||
text: '구글 가입 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'signUpGoogleCount',
|
||||
},
|
||||
{
|
||||
text: '본인인증 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'authCount',
|
||||
},
|
||||
{
|
||||
text: '회원탈퇴 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'signOutCount',
|
||||
},
|
||||
{
|
||||
text: '결제자 수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'paymentMemberCount',
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
const date = new Date();
|
||||
const firstDate = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
const lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
||||
|
||||
let firstDateMonth = (firstDate.getMonth() + 1).toString()
|
||||
if (firstDateMonth.length < 2) {
|
||||
firstDateMonth = '0' + firstDateMonth
|
||||
}
|
||||
|
||||
|
||||
let lastDateMonth = (lastDate.getMonth() + 1).toString()
|
||||
if (lastDateMonth.length < 2) {
|
||||
lastDateMonth = '0' + lastDateMonth
|
||||
}
|
||||
|
||||
this.start_date = firstDate.getFullYear() + '-' + firstDateMonth + '-0' + firstDate.getDate()
|
||||
this.end_date = lastDate.getFullYear() + '-' + lastDateMonth + '-' + lastDate.getDate()
|
||||
|
||||
await this.getStatistics()
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
async getStatistics() {
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getStatistics(this.start_date, this.end_date, this.page);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const data = res.data.data
|
||||
this.total_auth_count = data.totalAuthCount
|
||||
this.total_sign_up_count = data.totalSignUpCount
|
||||
this.total_sign_up_email_count = data.totalSignUpEmailCount
|
||||
this.total_sign_up_kakao_count = data.totalSignUpKakaoCount
|
||||
this.total_sign_up_google_count = data.totalSignUpGoogleCount
|
||||
this.total_sign_out_count = data.totalSignOutCount
|
||||
this.total_payment_member_count = data.totalPaymentMemberCount
|
||||
this.items = data.items
|
||||
|
||||
const totalPage = Math.ceil(data.totalCount / 30)
|
||||
if (totalPage <= 0)
|
||||
this.total_page = 1
|
||||
else
|
||||
this.total_page = totalPage
|
||||
} else {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
|
||||
this.is_loading = false
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summary {
|
||||
background-color: #c4dbf1;
|
||||
}
|
||||
</style>
|
@@ -14,7 +14,7 @@
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#9970ff"
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showWriteDialog"
|
||||
@@ -113,13 +113,13 @@
|
||||
<datetime
|
||||
v-model="start_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD H:i"
|
||||
/>
|
||||
<div> ~ </div>
|
||||
<datetime
|
||||
v-model="end_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD H:i"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@@ -75,13 +75,13 @@
|
||||
<datetime
|
||||
v-model="event.startDate"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD H:i"
|
||||
/>
|
||||
<div> ~ </div>
|
||||
<div> ~</div>
|
||||
<datetime
|
||||
v-model="event.endDate"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD H:i"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -281,16 +281,17 @@ import datetime from 'vuejs-datetimepicker';
|
||||
|
||||
export default {
|
||||
name: "EventView",
|
||||
components: { datetime },
|
||||
components: {datetime},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
is_modify: false,
|
||||
events: [],
|
||||
event: { isAdult: '' },
|
||||
event: {isAdult: ''},
|
||||
show_write_dialog: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
selected_event: {},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -343,7 +344,7 @@ export default {
|
||||
try {
|
||||
const res = await api.getEvents(this.page)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.events = res.data.data.eventList
|
||||
this.events = res.data.data
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
@@ -361,6 +362,7 @@ export default {
|
||||
|
||||
clickEvent(item) {
|
||||
this.is_modify = true
|
||||
this.selected_event = item
|
||||
this.event.id = item.id
|
||||
this.event.thumbnailImageUrl = item.thumbnailImageUrl
|
||||
this.event.detailImageUrl = item.detailImageUrl
|
||||
@@ -376,7 +378,8 @@ export default {
|
||||
|
||||
cancel() {
|
||||
this.is_modify = false
|
||||
this.event = { isAdult: '' }
|
||||
this.event = {isAdult: ''}
|
||||
this.selected_event = {}
|
||||
this.show_write_dialog = false
|
||||
},
|
||||
|
||||
@@ -440,7 +443,7 @@ export default {
|
||||
this.notifySuccess('등록되었습니다.')
|
||||
this.page = 1
|
||||
await this.getEvents()
|
||||
this.event = { isAdult: '' }
|
||||
this.event = {isAdult: ''}
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
@@ -457,7 +460,11 @@ export default {
|
||||
const formData = new FormData()
|
||||
formData.append("id", this.event.id)
|
||||
|
||||
if (this.event.title != null && this.event.title.trim().length > 0) {
|
||||
if (
|
||||
this.event.title != null &&
|
||||
this.event.title.trim().length > 0 &&
|
||||
this.selected_event.title !== this.event.title
|
||||
) {
|
||||
formData.append("title", this.event.title)
|
||||
}
|
||||
|
||||
@@ -477,7 +484,7 @@ export default {
|
||||
formData.append("isPopup", this.event.isPopup)
|
||||
}
|
||||
|
||||
if (this.event.link != null && this.event.link.trim().length > 0) {
|
||||
if (this.selected_event.link !== this.event.link) {
|
||||
formData.append("link", this.event.link)
|
||||
}
|
||||
|
||||
@@ -485,11 +492,11 @@ export default {
|
||||
formData.append("isAdult", JSON.parse(this.event.isAdult))
|
||||
}
|
||||
|
||||
if (this.event.startDate != null) {
|
||||
if (this.event.startDate != null && this.event.startDate !== this.selected_event.startDate) {
|
||||
formData.append("startDate", this.event.startDate)
|
||||
}
|
||||
|
||||
if (this.event.endDate != null) {
|
||||
if (this.event.endDate != null && this.event.endDate !== this.selected_event.endDate) {
|
||||
formData.append("endDate", this.event.endDate)
|
||||
}
|
||||
|
||||
@@ -499,7 +506,7 @@ export default {
|
||||
this.notifySuccess('수정되었습니다.')
|
||||
this.page = 1
|
||||
await this.getEvents()
|
||||
this.event = { isAdult: '' }
|
||||
this.event = {isAdult: ''}
|
||||
this.is_modify = false
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
|
585
src/views/Promotion/PointPolicyView.vue
Normal file
585
src/views/Promotion/PointPolicyView.vue
Normal file
@@ -0,0 +1,585 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>포인트 정책</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="10" />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3bb9f1"
|
||||
dark
|
||||
depressed
|
||||
@click="showWriteDialog"
|
||||
>
|
||||
포인트 정책 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="point_policy_list"
|
||||
:loading="is_loading"
|
||||
:items-per-page="-1"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
hide-default-footer
|
||||
>
|
||||
<template v-slot:item.title="{ item }">
|
||||
{{ item.title }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.policyType="{ item }">
|
||||
{{ policy_type_map[item.policyType] }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actionType="{ item }">
|
||||
{{ action_type_map[item.actionType] }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.threshold="{ item }">
|
||||
{{ item.threshold }} 번
|
||||
</template>
|
||||
|
||||
<template v-slot:item.availableCount="{ item }">
|
||||
{{ item.availableCount }} 번
|
||||
</template>
|
||||
|
||||
<template v-slot:item.period="{ item }">
|
||||
{{ item.startDate }} ~ {{ item.endDate }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.point="{ item }">
|
||||
{{ item.pointAmount }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.isActive="{ item }">
|
||||
<div v-if="item.isActive">
|
||||
O
|
||||
</div>
|
||||
<div v-else>
|
||||
X
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.management="{ item }">
|
||||
<v-btn
|
||||
:disabled="is_loading"
|
||||
@click="showModifyDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-center">
|
||||
<v-col>
|
||||
<v-pagination
|
||||
v-model="page"
|
||||
:length="total_page"
|
||||
circle
|
||||
@input="next"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-row>
|
||||
<v-dialog
|
||||
v-model="show_write_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>포인트 정책 등록</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="point_policy.title"
|
||||
label="제목"
|
||||
required
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="selected_point_policy === null">
|
||||
<v-radio-group
|
||||
v-model="point_policy.policy_type"
|
||||
label="지급 유형 선택"
|
||||
>
|
||||
<v-radio
|
||||
v-for="item in policy_type_list"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="selected_point_policy === null">
|
||||
<v-radio-group
|
||||
v-model="point_policy.action_type"
|
||||
label="액션 선택"
|
||||
>
|
||||
<v-radio
|
||||
v-for="item in action_type_list"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="selected_point_policy === null">
|
||||
<v-text-field
|
||||
v-model="point_policy.threshold"
|
||||
label="참여해야 하는 횟수"
|
||||
required
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="selected_point_policy === null">
|
||||
<v-text-field
|
||||
v-model="point_policy.point"
|
||||
label="포인트"
|
||||
required
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="selected_point_policy === null">
|
||||
<v-text-field
|
||||
v-model="point_policy.available_count"
|
||||
label="참여 가능 횟수"
|
||||
required
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
기간
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="datepicker-wrapper"
|
||||
>
|
||||
<datetime
|
||||
v-model="point_policy.start_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD H:i"
|
||||
/>
|
||||
<div> ~</div>
|
||||
<datetime
|
||||
v-model="point_policy.end_date"
|
||||
class="datepicker"
|
||||
format="YYYY-MM-DD H:i"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text v-show="selected_point_policy !== null">
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
활성화
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<input
|
||||
v-model="point_policy.is_active"
|
||||
type="checkbox"
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selected_point_policy !== null"
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="modify"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="validate"
|
||||
>
|
||||
등록
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/point_policy'
|
||||
import datetime from "vuejs-datetimepicker";
|
||||
|
||||
export default {
|
||||
name: "PointPolicyView",
|
||||
components: {datetime},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_loading: false,
|
||||
show_write_dialog: false,
|
||||
|
||||
action_type_list: [
|
||||
{
|
||||
name: '본인인증',
|
||||
value: 'USER_AUTHENTICATION'
|
||||
},
|
||||
{
|
||||
name: '콘텐츠 댓글',
|
||||
value: 'CONTENT_COMMENT'
|
||||
},
|
||||
{
|
||||
name: '구매한 콘텐츠 댓글',
|
||||
value: 'ORDER_CONTENT_COMMENT'
|
||||
},
|
||||
{
|
||||
name: '라이브 연속 청취 30분',
|
||||
value: 'LIVE_CONTINUOUS_LISTEN_30'
|
||||
},
|
||||
],
|
||||
action_type_map: {
|
||||
'USER_AUTHENTICATION': '본인인증',
|
||||
'CONTENT_COMMENT': '콘텐츠 댓글',
|
||||
'ORDER_CONTENT_COMMENT': '구매한 콘텐츠 댓글',
|
||||
'LIVE_CONTINUOUS_LISTEN_30': '라이브 연속 청취 30분',
|
||||
},
|
||||
|
||||
policy_type_list: [
|
||||
{
|
||||
name: '매일',
|
||||
value: 'DAILY'
|
||||
},
|
||||
{
|
||||
name: '전체',
|
||||
value: 'TOTAL'
|
||||
},
|
||||
],
|
||||
policy_type_map: {
|
||||
'DAILY': '매일',
|
||||
'TOTAL': '전체',
|
||||
},
|
||||
|
||||
point_policy: {
|
||||
title: '',
|
||||
policy_type: '',
|
||||
action_type: '',
|
||||
threshold: 0,
|
||||
available_count: 0,
|
||||
point: 0,
|
||||
start_date: '',
|
||||
end_date: ''
|
||||
},
|
||||
selected_point_policy: null,
|
||||
point_policy_list: [],
|
||||
|
||||
page: 1,
|
||||
total_page: 0,
|
||||
|
||||
headers: [
|
||||
{
|
||||
text: '제목',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'title',
|
||||
},
|
||||
{
|
||||
text: '지급유형',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'policyType',
|
||||
},
|
||||
{
|
||||
text: '액션',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'actionType',
|
||||
},
|
||||
{
|
||||
text: '참여해야 하는 횟수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'threshold',
|
||||
},
|
||||
{
|
||||
text: '참여 가능 횟수',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'availableCount',
|
||||
},
|
||||
{
|
||||
text: '기간',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'period',
|
||||
},
|
||||
{
|
||||
text: '포인트',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'pointAmount',
|
||||
},
|
||||
{
|
||||
text: '활성화',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'isActive',
|
||||
},
|
||||
{
|
||||
text: '관리',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
value: 'management'
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getPointPolicyList()
|
||||
},
|
||||
|
||||
methods: {
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
showWriteDialog() {
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
showModifyDialog(item) {
|
||||
this.selected_point_policy = item;
|
||||
this.point_policy = {
|
||||
title: item.title,
|
||||
policy_type: item.policyType,
|
||||
action_type: item.actionType,
|
||||
threshold: item.threshold,
|
||||
available_count: item.availableCount,
|
||||
point: item.pointAmount,
|
||||
start_date: item.startDate,
|
||||
end_date: item.endDate,
|
||||
is_active: item.isActive
|
||||
};
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (this.point_policy.title.trim() === '') {
|
||||
this.notifyError('제목을 입력하세요.')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.point_policy.policy_type.trim() === '') {
|
||||
this.notifyError('지급유형을 선택하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.point_policy.action_type.trim() === '') {
|
||||
this.notifyError('액션을 선택하세요')
|
||||
return
|
||||
}
|
||||
|
||||
if (isNaN(this.point_policy.threshold)) {
|
||||
this.notifyError('참여 해야하는 횟수는 숫자만 입력 가능합니다.')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.point_policy.threshold <= 0) {
|
||||
this.notifyError('참여 해야하는 횟수는 1이상 입력 가능합니다.')
|
||||
return
|
||||
}
|
||||
|
||||
if (isNaN(this.point_policy.point)) {
|
||||
this.notifyError('지급 포인트는 숫자만 입력 가능합니다.')
|
||||
return
|
||||
}
|
||||
|
||||
if (isNaN(this.point_policy.available_count)) {
|
||||
this.notifyError('참여 가능 횟수는 숫자만 입력 가능합니다.')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.point_policy.available_count <= 0) {
|
||||
this.notifyError('참여 가능 횟수는 1이상 입력 가능합니다.')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.point_policy.start_date.trim() === '') {
|
||||
this.notifyError('정책 시작 날짜를 입력하세요')
|
||||
return
|
||||
}
|
||||
|
||||
this.submit()
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.point_policy = {
|
||||
title: '',
|
||||
policy_type: '',
|
||||
action_type: '',
|
||||
threshold: 0,
|
||||
available_count: 0,
|
||||
point: 0,
|
||||
start_date: '',
|
||||
end_date: ''
|
||||
}
|
||||
this.show_write_dialog = false
|
||||
this.selected_point_policy = null;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const request = {
|
||||
'title': this.point_policy.title,
|
||||
'policyType': this.point_policy.policy_type,
|
||||
'actionType': this.point_policy.action_type,
|
||||
'threshold': this.point_policy.threshold,
|
||||
'availableCount': this.point_policy.available_count,
|
||||
'pointAmount': this.point_policy.point,
|
||||
'startDate': this.point_policy.start_date,
|
||||
'endDate': this.point_policy.end_date
|
||||
}
|
||||
const res = await api.createPointPolicyList(request)
|
||||
this.is_loading = false
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess(res.data.message || '등록되었습니다.')
|
||||
|
||||
this.page = 1
|
||||
this.point_policy_list = []
|
||||
await this.getPointPolicyList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
},
|
||||
|
||||
async modify() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const request = {}
|
||||
|
||||
if (this.point_policy.title !== this.selected_point_policy.title) {
|
||||
request.title = this.point_policy.title
|
||||
}
|
||||
|
||||
if (this.point_policy.start_date !== this.selected_point_policy.startDate) {
|
||||
request.startDate = this.point_policy.start_date
|
||||
}
|
||||
|
||||
if (this.point_policy.end_date !== this.selected_point_policy.endDate) {
|
||||
request.endDate = this.point_policy.end_date
|
||||
}
|
||||
|
||||
if (this.point_policy.is_active !== this.selected_point_policy.isActive) {
|
||||
request.isActive = this.point_policy.is_active
|
||||
}
|
||||
|
||||
const res = await api.updatePointPolicyList(this.selected_point_policy.id, request)
|
||||
this.is_loading = false
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess(res.data.message || '수정되었습니다.')
|
||||
|
||||
this.point_policy_list = []
|
||||
await this.getPointPolicyList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
},
|
||||
|
||||
async getPointPolicyList() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const res = await api.getPointPolicyList(this.page);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
const data = res.data.data
|
||||
|
||||
const total_page = Math.ceil(data.totalCount / 20)
|
||||
this.point_policy_list = data.items
|
||||
|
||||
if (total_page <= 0)
|
||||
this.total_page = 1
|
||||
else
|
||||
this.total_page = total_page
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
|
||||
this.is_loading = false
|
||||
},
|
||||
|
||||
async next() {
|
||||
await this.getPointPolicyList()
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.datepicker {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.datepicker-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.datepicker-wrapper > div {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.v-card__text {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.v-card__actions {
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.v-card__actions > .v-btn {
|
||||
font-size: 20px;
|
||||
|
||||
}
|
||||
</style>
|
481
src/views/Series/ContentSeriesNew.vue
Normal file
481
src/views/Series/ContentSeriesNew.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>새로운 시리즈</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_write_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="9" />
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3BB9F1"
|
||||
dark
|
||||
depressed
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
새로운 시리즈 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<draggable
|
||||
v-model="recommend_series_list"
|
||||
class="row"
|
||||
@end="onDropCallback(recommend_series_list)"
|
||||
>
|
||||
<v-col
|
||||
v-for="(item, i) in recommend_series_list"
|
||||
:key="i"
|
||||
cols="3"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-spacer />
|
||||
<v-img :src="item.imageUrl" />
|
||||
<v-spacer />
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="showModifyRecommendSeriesDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</draggable>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title v-if="is_modify === true">
|
||||
새로운 시리즈 수정
|
||||
</v-card-title>
|
||||
<v-card-title v-else>
|
||||
새로운 시리즈 등록
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
시리즈
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-combobox
|
||||
v-model="recommend_series.series_title"
|
||||
:items="series"
|
||||
:loading="is_loading"
|
||||
:search-input.sync="search_query_series"
|
||||
label="시리즈를 검색하세요"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
no-data-text="No results found"
|
||||
hide-selected
|
||||
clearable
|
||||
@change="onSelectSeries"
|
||||
@update:search-input="onSearchSeriesUpdate"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<div class="image-select">
|
||||
<label for="image">
|
||||
새로운 시리즈 이미지 등록
|
||||
</label>
|
||||
<v-file-input
|
||||
id="image"
|
||||
v-model="recommend_series.image"
|
||||
@change="imageAdd"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
v-if="recommend_series.image_url"
|
||||
:src="recommend_series.image_url"
|
||||
alt=""
|
||||
class="image-preview"
|
||||
>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="is_modify === true"
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="modify"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="submit"
|
||||
>
|
||||
등록
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text>
|
||||
삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="deleteRecommendSeries"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from "vuedraggable";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
import * as api from "@/api/audio_content_series_recommend"
|
||||
import * as seriesApi from "@/api/audio_content_series";
|
||||
|
||||
export default {
|
||||
name: "ContentSeriesNew",
|
||||
components: {Draggable},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_selecting: false,
|
||||
is_loading: false,
|
||||
is_modify: false,
|
||||
show_write_dialog: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
selected_recommend_series: {},
|
||||
recommend_series: {},
|
||||
recommend_series_list: [],
|
||||
series: [],
|
||||
search_query_series: '',
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
search_query_series() {
|
||||
if (!this.is_selecting) {
|
||||
this.debouncedSearchSeries();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getRecommendSeriesList();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.debouncedSearchSeries = debounce(this.searchSeries, 500);
|
||||
},
|
||||
|
||||
methods: {
|
||||
imageAdd(payload) {
|
||||
const file = payload;
|
||||
if (file) {
|
||||
this.recommend_series.image_url = URL.createObjectURL(file)
|
||||
URL.revokeObjectURL(file)
|
||||
} else {
|
||||
this.recommend_series.image_url = null
|
||||
}
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.is_modify = false
|
||||
this.is_selecting = false
|
||||
this.show_write_dialog = false
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.recommend_series = {}
|
||||
this.selected_recommend_series = {}
|
||||
this.search_query_series = ''
|
||||
this.series = []
|
||||
},
|
||||
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
showModifyRecommendSeriesDialog(recommendSeries) {
|
||||
this.is_modify = true
|
||||
this.selected_recommend_series = recommendSeries
|
||||
|
||||
this.is_selecting = true; // 선택 중 플래그 활성화
|
||||
|
||||
this.recommend_series.id = recommendSeries.id
|
||||
this.recommend_series.image_url = recommendSeries.imageUrl
|
||||
this.recommend_series.series_id = recommendSeries.seriesId
|
||||
this.recommend_series.series_title = recommendSeries.seriesTitle
|
||||
|
||||
setTimeout(() => {
|
||||
this.is_selecting = false; // 선택 상태 해제
|
||||
}, 1000);
|
||||
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
deleteConfirm(recommendSeries) {
|
||||
this.selected_recommend_series = recommendSeries
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
onSelectSeries(value) {
|
||||
this.recommend_series.series_id = value.value
|
||||
this.is_selecting = true; // 선택 중 플래그 활성화
|
||||
setTimeout(() => {
|
||||
this.is_selecting = false; // 선택 상태 해제
|
||||
}, 0);
|
||||
},
|
||||
|
||||
onSearchSeriesUpdate(value) {
|
||||
if (!this.is_selecting) {
|
||||
this.search_query_series = value
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (this.recommend_series.series_id === null || this.recommend_series.series_id === undefined) {
|
||||
this.notifyError("시리즈를 선택하세요")
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
async getRecommendSeriesList() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
const res = await api.getRecommendSeriesList(false)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.recommend_series_list = res.data.data
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) return;
|
||||
if (this.is_loading === true) return;
|
||||
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("image", this.recommend_series.image)
|
||||
|
||||
let request = {
|
||||
seriesId: this.recommend_series.series_id,
|
||||
isFree: false
|
||||
}
|
||||
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
const res = await api.saveRecommendSeries(formData)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess('등록되었습니다.')
|
||||
|
||||
this.recommend_series_list = []
|
||||
await this.getRecommendSeriesList()
|
||||
} else {
|
||||
this.notifyError(res.data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async modify() {
|
||||
if (this.is_loading) return;
|
||||
|
||||
this.is_loading = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
let request = {id: this.recommend_series.id}
|
||||
|
||||
if (this.recommend_series.image !== null) {
|
||||
formData.append("image", this.recommend_series.image)
|
||||
}
|
||||
|
||||
if (
|
||||
this.selected_recommend_series.series_id !== this.recommend_series.series_id &&
|
||||
this.recommend_series.series_id !== null &&
|
||||
this.recommend_series.series_id !== undefined
|
||||
) {
|
||||
request.seriesId = this.recommend_series.series_id
|
||||
}
|
||||
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
const res = await api.modifyRecommendSeries(formData)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess('수정되었습니다.')
|
||||
|
||||
this.recommend_series_list = []
|
||||
await this.getRecommendSeriesList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRecommendSeries() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("request", JSON.stringify({id: this.selected_recommend_series.id, isActive: false}))
|
||||
const res = await api.modifyRecommendSeries(formData)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.cancel()
|
||||
this.notifySuccess('삭제되었습니다.')
|
||||
|
||||
this.recommend_series_list = []
|
||||
await this.getRecommendSeriesList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async onDropCallback(items) {
|
||||
const ids = items.map((item) => {
|
||||
return item.id
|
||||
})
|
||||
|
||||
const res = await api.updateRecommendSeriesOrders(ids)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess(res.data.message || '수정되었습니다.')
|
||||
}
|
||||
},
|
||||
|
||||
async searchSeries() {
|
||||
if (this.search_query_series === null || this.search_query_series.length < 2) {
|
||||
this.series = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.is_loading = true;
|
||||
|
||||
try {
|
||||
const res = await seriesApi.searchSeriesList(this.search_query_series);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.series = res.data.data.map((item) => {
|
||||
return {name: item.title, value: item.id}
|
||||
})
|
||||
} else {
|
||||
this.series = []
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-select label {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #232d4a;
|
||||
color: #fff;
|
||||
vertical-align: middle;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.v-file-input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
max-width: 100%;
|
||||
width: 250px;
|
||||
object-fit: cover;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
481
src/views/Series/ContentSeriesRecommendFree.vue
Normal file
481
src/views/Series/ContentSeriesRecommendFree.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>무료 추천 시리즈</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<br>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_write_dialog"
|
||||
max-width="1000px"
|
||||
persistent
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="9" />
|
||||
<v-spacer />
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
color="#3BB9F1"
|
||||
dark
|
||||
depressed
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
무료 추천 시리즈 등록
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<draggable
|
||||
v-model="recommend_series_list"
|
||||
class="row"
|
||||
@end="onDropCallback(recommend_series_list)"
|
||||
>
|
||||
<v-col
|
||||
v-for="(item, i) in recommend_series_list"
|
||||
:key="i"
|
||||
cols="3"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-spacer />
|
||||
<v-img :src="item.imageUrl" />
|
||||
<v-spacer />
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="showModifyRecommendSeriesDialog(item)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
@click="deleteConfirm(item)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</draggable>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title v-if="is_modify === true">
|
||||
추천 시리즈 수정
|
||||
</v-card-title>
|
||||
<v-card-title v-else>
|
||||
추천 시리즈 등록
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="4">
|
||||
시리즈
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-combobox
|
||||
v-model="recommend_series.series_title"
|
||||
:items="series"
|
||||
:loading="is_loading"
|
||||
:search-input.sync="search_query_series"
|
||||
label="시리즈를 검색하세요"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
no-data-text="No results found"
|
||||
hide-selected
|
||||
clearable
|
||||
@change="onSelectSeries"
|
||||
@update:search-input="onSearchSeriesUpdate"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<div class="image-select">
|
||||
<label for="image">
|
||||
추천 시리즈 이미지 등록
|
||||
</label>
|
||||
<v-file-input
|
||||
id="image"
|
||||
v-model="recommend_series.image"
|
||||
@change="imageAdd"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
v-if="recommend_series.image_url"
|
||||
:src="recommend_series.image_url"
|
||||
alt=""
|
||||
class="image-preview"
|
||||
>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="is_modify === true"
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="modify"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="submit"
|
||||
>
|
||||
등록
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text />
|
||||
<v-card-text>
|
||||
삭제하시겠습니까?
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="deleteRecommendSeries"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from "vuedraggable";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
import * as api from "@/api/audio_content_series_recommend"
|
||||
import * as seriesApi from "@/api/audio_content_series";
|
||||
|
||||
export default {
|
||||
name: "ContentSeriesRecommendFree",
|
||||
components: {Draggable},
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_selecting: false,
|
||||
is_loading: false,
|
||||
is_modify: false,
|
||||
show_write_dialog: false,
|
||||
show_delete_confirm_dialog: false,
|
||||
selected_recommend_series: {},
|
||||
recommend_series: {},
|
||||
recommend_series_list: [],
|
||||
series: [],
|
||||
search_query_series: '',
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
search_query_series() {
|
||||
if (!this.is_selecting) {
|
||||
this.debouncedSearchSeries();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.getRecommendSeriesList();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.debouncedSearchSeries = debounce(this.searchSeries, 500);
|
||||
},
|
||||
|
||||
methods: {
|
||||
imageAdd(payload) {
|
||||
const file = payload;
|
||||
if (file) {
|
||||
this.recommend_series.image_url = URL.createObjectURL(file)
|
||||
URL.revokeObjectURL(file)
|
||||
} else {
|
||||
this.recommend_series.image_url = null
|
||||
}
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.is_modify = false
|
||||
this.is_selecting = false
|
||||
this.show_write_dialog = false
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.recommend_series = {}
|
||||
this.selected_recommend_series = {}
|
||||
this.search_query_series = ''
|
||||
this.series = []
|
||||
},
|
||||
|
||||
notifyError(message) {
|
||||
this.$dialog.notify.error(message)
|
||||
},
|
||||
|
||||
notifySuccess(message) {
|
||||
this.$dialog.notify.success(message)
|
||||
},
|
||||
|
||||
showModifyRecommendSeriesDialog(recommendSeries) {
|
||||
this.is_modify = true
|
||||
this.selected_recommend_series = recommendSeries
|
||||
|
||||
this.is_selecting = true; // 선택 중 플래그 활성화
|
||||
|
||||
this.recommend_series.id = recommendSeries.id
|
||||
this.recommend_series.image_url = recommendSeries.imageUrl
|
||||
this.recommend_series.series_id = recommendSeries.seriesId
|
||||
this.recommend_series.series_title = recommendSeries.seriesTitle
|
||||
|
||||
setTimeout(() => {
|
||||
this.is_selecting = false; // 선택 상태 해제
|
||||
}, 1000);
|
||||
|
||||
this.show_write_dialog = true
|
||||
},
|
||||
|
||||
deleteConfirm(recommendSeries) {
|
||||
this.selected_recommend_series = recommendSeries
|
||||
this.show_delete_confirm_dialog = true
|
||||
},
|
||||
|
||||
onSelectSeries(value) {
|
||||
this.recommend_series.series_id = value.value
|
||||
this.is_selecting = true; // 선택 중 플래그 활성화
|
||||
setTimeout(() => {
|
||||
this.is_selecting = false; // 선택 상태 해제
|
||||
}, 0);
|
||||
},
|
||||
|
||||
onSearchSeriesUpdate(value) {
|
||||
if (!this.is_selecting) {
|
||||
this.search_query_series = value
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (this.recommend_series.series_id === null || this.recommend_series.series_id === undefined) {
|
||||
this.notifyError("시리즈를 선택하세요")
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
async getRecommendSeriesList() {
|
||||
this.is_loading = true
|
||||
try {
|
||||
const res = await api.getRecommendSeriesList(true)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.recommend_series_list = res.data.data
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) return;
|
||||
if (this.is_loading === true) return;
|
||||
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("image", this.recommend_series.image)
|
||||
|
||||
let request = {
|
||||
seriesId: this.recommend_series.series_id,
|
||||
isFree: true
|
||||
}
|
||||
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
const res = await api.saveRecommendSeries(formData)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess('등록되었습니다.')
|
||||
|
||||
this.recommend_series_list = []
|
||||
await this.getRecommendSeriesList()
|
||||
} else {
|
||||
this.notifyError(res.data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async modify() {
|
||||
if (this.is_loading) return;
|
||||
|
||||
this.is_loading = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
let request = {id: this.recommend_series.id}
|
||||
|
||||
if (this.recommend_series.image !== null) {
|
||||
formData.append("image", this.recommend_series.image)
|
||||
}
|
||||
|
||||
if (
|
||||
this.selected_recommend_series.series_id !== this.recommend_series.series_id &&
|
||||
this.recommend_series.series_id !== null &&
|
||||
this.recommend_series.series_id !== undefined
|
||||
) {
|
||||
request.seriesId = this.recommend_series.series_id
|
||||
}
|
||||
|
||||
formData.append("request", JSON.stringify(request))
|
||||
|
||||
const res = await api.modifyRecommendSeries(formData)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.cancel()
|
||||
this.notifySuccess('수정되었습니다.')
|
||||
|
||||
this.recommend_series_list = []
|
||||
await this.getRecommendSeriesList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRecommendSeries() {
|
||||
if (this.is_loading) return;
|
||||
this.is_loading = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("request", JSON.stringify({id: this.selected_recommend_series.id, isActive: false}))
|
||||
const res = await api.modifyRecommendSeries(formData)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.show_delete_confirm_dialog = false
|
||||
this.cancel()
|
||||
this.notifySuccess('삭제되었습니다.')
|
||||
|
||||
this.recommend_series_list = []
|
||||
await this.getRecommendSeriesList()
|
||||
} else {
|
||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async onDropCallback(items) {
|
||||
const ids = items.map((item) => {
|
||||
return item.id
|
||||
})
|
||||
|
||||
const res = await api.updateRecommendSeriesOrders(ids)
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.notifySuccess(res.data.message || '수정되었습니다.')
|
||||
}
|
||||
},
|
||||
|
||||
async searchSeries() {
|
||||
if (this.search_query_series === null || this.search_query_series.length < 2) {
|
||||
this.series = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.is_loading = true;
|
||||
|
||||
try {
|
||||
const res = await seriesApi.searchSeriesList(this.search_query_series);
|
||||
|
||||
if (res.status === 200 && res.data.success === true) {
|
||||
this.series = res.data.data.map((item) => {
|
||||
return {name: item.title, value: item.id}
|
||||
})
|
||||
} else {
|
||||
this.series = []
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||
this.is_loading = false
|
||||
} finally {
|
||||
this.is_loading = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-select label {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #232d4a;
|
||||
color: #fff;
|
||||
vertical-align: middle;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.v-file-input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
max-width: 100%;
|
||||
width: 250px;
|
||||
object-fit: cover;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user