<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="#9970ff" dark depressed @click="showWriteDialog" > 콘텐츠 등록 </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"> 테마 </th> <th class="text-center"> 태그 </th> <th class="text-center"> 가격 </th> <th class="text-center"> 19금 </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 audio_contents" :key="item.audioContentId" > <td align="center"> <v-img max-width="100" max-height="100" :src="item.coverImageUrl" /> </td> <td>{{ item.title }}</td> <td style="max-width: 350px !important; word-break:break-all; height: auto;"> {{ item.detail }} </td> <td>{{ item.creatorNickname }}</td> <td>{{ item.theme }}</td> <td style="max-width: 200px !important; word-break:break-all; height: auto;"> {{ item.tags }} </td> <td v-if="item.price > 0"> {{ item.price }} 캔 </td> <td v-else> 무료 </td> <td> <div v-if="item.isAdult"> O </div> <div v-else> X </div> </td> <td>{{ item.remainingTime }}</td> <td> <vuetify-audio :file="item.contentUrl" :downloadable="true" :auto-play="false" /> </td> <td>{{ item.date }}</td> <td> <v-row> <v-col> <v-btn :disabled="is_loading" @click="showModifyDialog(item)" > 수정 </v-btn> </v-col> <v-spacer /> <v-col> <v-btn :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-row> <v-dialog v-model="show_modify_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="audio_content.cover_image" accept="image/*" @change="imageAdd" /> </div> <img v-if="audio_content.cover_image_url" :src="audio_content.cover_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="audio_content.title" 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-text-field v-model="audio_content.detail" label="내용" required /> </v-col> </v-row> </v-card-text> <v-card-text> <v-row> <v-col cols="4"> 연령제한 </v-col> <v-col cols="8" align="left" > <input v-model="audio_content.is_adult" type="checkbox" > </v-col> </v-row> </v-card-text> <v-card-text> <v-row> <v-col cols="4"> 댓글 가능 </v-col> <v-col cols="8" align="left" > <input v-model="audio_content.is_comment_available" 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 color="blue darken-1" text @click="modify" > 수정 </v-btn> </v-card-actions> </v-card> </v-dialog> </v-row> <v-row> <v-dialog v-model="show_create_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="audio_content.cover_image" accept="image/*" @change="imageAdd" /> </div> <img v-if="audio_content.cover_image_url" :src="audio_content.cover_image_url" alt="" class="image-preview" > <div class="content-select"> <label for="content"> 콘텐츠 등록 </label> <v-file-input id="content" v-model="audio_content.content_file" accept="audio/*" @change="contentAdd" /> <v-row v-if="audio_content.content_file_name !== undefined" align="center" > <v-col cols="4" /> <v-col cols="8"> <v-text-field v-model="audio_content.content_file_name" label="" disabled /> </v-col> </v-row> </div> <v-card-text> <v-row align="center"> <v-col cols="4"> 제목 </v-col> <v-col cols="8"> <v-text-field v-model="audio_content.title" 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-text-field v-model="audio_content.detail" 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-text-field v-model="audio_content.tags" 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-select v-model="audio_content.theme_id" :items="themeList" item-text="name" item-value="value" label="테마 선택" /> </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-text-field v-model="audio_content.price" label="가격" required /> </v-col> </v-row> </v-card-text> <v-card-text> <v-row> <v-col cols="4"> 연령제한 </v-col> <v-col cols="8" align="left" > <input v-model="audio_content.is_adult" type="checkbox" > </v-col> </v-row> </v-card-text> <v-card-text> <v-row> <v-col cols="4"> 댓글 가능 </v-col> <v-col cols="8" align="left" > <input v-model="audio_content.is_comment_available" 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 color="blue darken-1" text @click="save" > 등록 </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_audio_content.title }}"을 삭제하시겠습니까? </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="deleteAudioContent" > 확인 </v-btn> </v-card-actions> </v-card> </v-dialog> </div> </template> <script> import * as api from '@/api/audio_content' import VuetifyAudio from 'vuetify-audio' export default { name: "AudioContentList", components: {VuetifyAudio}, data() { return { is_loading: false, show_create_dialog: false, show_modify_dialog: false, show_delete_confirm_dialog: false, page: 1, total_page: 0, search_word: '', audio_content: { price: 0, is_adult: false, is_comment_available: true, }, audio_contents: [], themeList: [], selected_audio_content: {}, utm_source: '', utm_medium: '', utm_campaign: '', } }, async created() { await this.getAudioContentThemeList() await this.getAudioContent() }, methods: { notifyError(message) { this.$dialog.notify.error(message) }, notifySuccess(message) { this.$dialog.notify.success(message) }, deleteConfirm(item) { this.selected_audio_content = item this.show_delete_confirm_dialog = true }, deleteCancel() { this.selected_audio_content = {} this.show_delete_confirm_dialog = false }, showWriteDialog() { this.show_create_dialog = true this.audio_content.price = '0' }, showModifyDialog(item) { this.selected_audio_content = item this.audio_content.id = item.audioContentId this.audio_content.title = item.title this.audio_content.detail = item.detail this.audio_content.is_adult = item.isAdult this.audio_content.is_comment_available = item.isCommentAvailable this.audio_content.cover_image_url = item.coverImageUrl console.log(this.audio_content) this.show_modify_dialog = true }, cancel() { this.selected_audio_content = {} this.audio_content = { price: 0, is_adult: false, is_comment_available: true, } this.show_create_dialog = false this.show_modify_dialog = false this.show_delete_confirm_dialog = false }, contentAdd(payload) { const file = payload; if (file) { this.audio_content.content_file_name = file.name URL.revokeObjectURL(file) } else { this.audio_content.content_file_name = null } }, imageAdd(payload) { const file = payload; if (file) { this.audio_content.cover_image_url = URL.createObjectURL(file) URL.revokeObjectURL(file) } else { this.audio_content.cover_image_url = null } }, async getAudioContentThemeList() { this.is_loading = true try { const res = await api.getAudioContentThemeList() if (res.status === 200 && res.data.success === true) { this.themeList = res.data.data.map((item) => { return {name: item.theme, value: item.id} }) } else { this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } this.is_loading = false } catch (e) { this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') this.is_loading = false } }, async save() { if ( this.audio_content.title === null || this.audio_content.title === undefined || this.audio_content.title.trim().length <= 0 ) { this.notifyError("제목을 입력하세요") return } if ( this.audio_content.detail === null || this.audio_content.detail === undefined || this.audio_content.detail.trim().length <= 0 ) { this.notifyError("내용을 입력하세요") return } if ( this.audio_content.theme_id === null || this.audio_content.theme_id === undefined || this.audio_content.theme_id <= 0 ) { this.notifyError("테마를 선택하세요") return } if (this.is_loading) return; this.isLoading = true try { const formData = new FormData() const request = { title: this.audio_content.title, detail: this.audio_content.detail, tags: this.audio_content.tags, price: this.audio_content.price, themeId: this.audio_content.theme_id, isAdult: this.audio_content.is_adult, isCommentAvailable: this.audio_content.is_comment_available, } formData.append("coverImage", this.audio_content.cover_image) formData.append("contentFile", this.audio_content.content_file) formData.append("request", JSON.stringify(request)) const res = await api.createAudioContent(formData) if (res.status === 200 && res.data.success === true) { this.cancel() this.notifySuccess("등록한 콘텐츠가 업로드 중입니다.\n" + "콘텐츠 등록이 완료되면 알림을 보내드립니다.\n" + "이 페이지를 나가도 콘텐츠는 자동으로 등록됩니다.") this.audio_contents = [] await this.getAudioContent() } else { this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } } catch (e) { this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } finally { this.is_loading = false } }, async modify() { if ( this.audio_content.title === null || this.audio_content.title === undefined || this.audio_content.title.trim().length <= 0 ) { this.notifyError("제목을 입력하세요") return } if ( this.audio_content.detail === null || this.audio_content.detail === undefined || this.audio_content.detail.trim().length <= 0 ) { this.notifyError("내용을 입력하세요") return } if (this.is_loading) return; this.isLoading = true try { const formData = new FormData() const request = {id: this.audio_content.id} if ( this.selected_audio_content.title !== this.audio_content.title && this.audio_content.title.trim().length > 0 ) { request.title = this.audio_content.title } if ( this.selected_audio_content.detail !== this.audio_content.detail && this.audio_content.detail.trim().length > 0 ) { 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.isAdult !== this.audio_content.is_adult) { request.isAdult = this.audio_content.is_adult } if (this.selected_audio_content.isCommentAvailable !== this.audio_content.is_comment_available) { request.isCommentAvailable = this.audio_content.is_comment_available } if (this.audio_content.cover_image !== null) { formData.append("coverImage", this.audio_content.cover_image) } formData.append("request", JSON.stringify(request)) const res = await api.modifyAudioContent(formData) if (res.status === 200 && res.data.success === true) { this.cancel() this.notifySuccess('수정되었습니다.') this.audio_contents = [] await this.getAudioContent() } else { this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } } catch (e) { this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } finally { this.is_loading = false } }, async deleteAudioContent() { if (this.is_loading) return; this.is_loading = true try { let request = {id: this.selected_audio_content.audioContentId, isActive: false} const res = await api.modifyAudioContent(request) if (res.status === 200 && res.data.success === true) { this.cancel() this.notifySuccess('삭제되었습니다.') this.audio_contents = [] await this.getAudioContent() } else { this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } } catch (e) { this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } finally { this.is_loading = false } }, async next() { if (this.search_word.length < 2) { this.search_word = '' await this.getAudioContent() } else { await this.searchAudioContent() } }, async getAudioContent() { this.is_loading = true try { const res = await api.getAudioContentList(this.page) if (res.status === 200 && res.data.success === true) { const data = res.data.data const total_page = Math.ceil(data.totalCount / 10) this.audio_contents = data.items if (total_page <= 0) this.total_page = 1 else this.total_page = total_page } else { this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } this.is_loading = false } catch (e) { this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') this.is_loading = false } }, async searchAudioContent() { if (this.search_word.length === 0) { await this.getAudioContent() } else if (this.search_word.length < 2) { this.notifyError('검색어를 2글자 이상 입력하세요.') } else { this.is_loading = true try { const res = await api.searchAudioContent(this.search_word, this.page) if (res.status === 200 && res.data.success === true) { const data = res.data.data const total_page = Math.ceil(data.totalCount / 10) this.audio_contents = data.items if (total_page <= 0) this.total_page = 1 else this.total_page = total_page } else { this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') } this.is_loading = false } catch (e) { this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') this.is_loading = false } } }, } } </script> <style scoped> .content-select label { display: inline-block; padding: 10px 20px; background-color: #232d4a; color: #fff; vertical-align: middle; font-size: 15px; cursor: pointer; border-radius: 5px; margin-top: 30px; } .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%; max-height: 250px; width: 250px; object-fit: cover; margin-top: 10px; } </style>