캐릭터 챗봇 #74
| @@ -8,9 +8,9 @@ async function getCharacterList(page = 1, size = 20) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // 캐릭터 검색 | // 캐릭터 검색 | ||||||
| async function searchCharacters(keyword, page = 1, size = 20) { | async function searchCharacters(searchTerm, page = 1, size = 20) { | ||||||
|   return Vue.axios.get('/api/admin/chat/character/search', { |   return Vue.axios.get('/admin/chat/banner/search-character', { | ||||||
|     params: { keyword, page, size } |     params: { searchTerm, page: page - 1, size } | ||||||
|   }) |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -74,10 +74,75 @@ async function updateCharacter(characterData, image = null) { | |||||||
|   }) |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 캐릭터 배너 리스트 조회 | ||||||
|  | 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}) | ||||||
|  | } | ||||||
|  |  | ||||||
| export { | export { | ||||||
|   getCharacterList, |   getCharacterList, | ||||||
|   searchCharacters, |   searchCharacters, | ||||||
|   getCharacter, |   getCharacter, | ||||||
|   createCharacter, |   createCharacter, | ||||||
|   updateCharacter |   updateCharacter, | ||||||
|  |   getCharacterBannerList, | ||||||
|  |   createCharacterBanner, | ||||||
|  |   updateCharacterBanner, | ||||||
|  |   deleteCharacterBanner, | ||||||
|  |   updateCharacterBannerOrder | ||||||
| } | } | ||||||
|   | |||||||
| @@ -265,12 +265,11 @@ const routes = [ | |||||||
|                 name: 'CharacterForm', |                 name: 'CharacterForm', | ||||||
|                 component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterForm.vue') |                 component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterForm.vue') | ||||||
|             }, |             }, | ||||||
|             // TODO: CharacterBanner 컴포넌트가 구현되면 아래 라우트 주석 해제 |             { | ||||||
|             // { |                 path: '/character/banner', | ||||||
|             //     path: '/character/banner', |                 name: 'CharacterBanner', | ||||||
|             //     name: 'CharacterBanner', |                 component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterBanner.vue') | ||||||
|             //     component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterBanner.vue') |             }, | ||||||
|             // }, |  | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|   | |||||||
							
								
								
									
										548
									
								
								src/views/Chat/CharacterBanner.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										548
									
								
								src/views/Chat/CharacterBanner.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,548 @@ | |||||||
|  | <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.imageUrl" | ||||||
|  |                 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="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: [], | ||||||
|  |       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.data) { | ||||||
|  |           const newBanners = response.data.content || []; | ||||||
|  |           this.banners = [...this.banners, ...newBanners]; | ||||||
|  |  | ||||||
|  |           // 더 불러올 데이터가 있는지 확인 | ||||||
|  |           this.hasMoreItems = newBanners.length > 0; | ||||||
|  |           this.page++; | ||||||
|  |         } | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('배너 목록 로드 오류:', 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.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.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 = []; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     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.data) { | ||||||
|  |           this.searchResults = response.data.content || []; | ||||||
|  |         } | ||||||
|  |       } 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) { | ||||||
|  |           // 배너 수정 | ||||||
|  |           await updateCharacterBanner({ | ||||||
|  |             image: this.bannerForm.image, | ||||||
|  |             characterId: this.selectedCharacter.id, | ||||||
|  |             bannerId: this.bannerForm.bannerId | ||||||
|  |           }); | ||||||
|  |           this.notifySuccess('배너가 수정되었습니다.'); | ||||||
|  |         } else { | ||||||
|  |           // 배너 추가 | ||||||
|  |           await createCharacterBanner({ | ||||||
|  |             image: this.bannerForm.image, | ||||||
|  |             characterId: this.selectedCharacter.id | ||||||
|  |           }); | ||||||
|  |           this.notifySuccess('배너가 추가되었습니다.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 다이얼로그 닫고 배너 목록 새로고침 | ||||||
|  |         this.closeDialog(); | ||||||
|  |         this.refreshBanners(); | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('배너 저장 오류:', error); | ||||||
|  |         this.notifyError('배너 저장에 실패했습니다.'); | ||||||
|  |       } finally { | ||||||
|  |         this.isSubmitting = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     async deleteBanner() { | ||||||
|  |       if (!this.selectedBanner || this.isSubmitting) return; | ||||||
|  |  | ||||||
|  |       this.isSubmitting = true; | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         await deleteCharacterBanner(this.selectedBanner.id); | ||||||
|  |         this.notifySuccess('배너가 삭제되었습니다.'); | ||||||
|  |         this.showDeleteDialog = false; | ||||||
|  |         this.refreshBanners(); | ||||||
|  |       } 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); | ||||||
|  |         await updateCharacterBannerOrder(bannerIds); | ||||||
|  |         this.notifySuccess('배너 순서가 변경되었습니다.'); | ||||||
|  |       } 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> | ||||||
		Reference in New Issue
	
	Block a user