캐릭터 챗봇 #74
| @@ -8,9 +8,9 @@ async function getCharacterList(page = 1, size = 20) { | ||||
| } | ||||
|  | ||||
| // 캐릭터 검색 | ||||
| async function searchCharacters(keyword, page = 1, size = 20) { | ||||
|   return Vue.axios.get('/api/admin/chat/character/search', { | ||||
|     params: { keyword, page, size } | ||||
| async function searchCharacters(searchTerm, page = 1, size = 20) { | ||||
|   return Vue.axios.get('/admin/chat/banner/search-character', { | ||||
|     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 { | ||||
|   getCharacterList, | ||||
|   searchCharacters, | ||||
|   getCharacter, | ||||
|   createCharacter, | ||||
|   updateCharacter | ||||
|   updateCharacter, | ||||
|   getCharacterBannerList, | ||||
|   createCharacterBanner, | ||||
|   updateCharacterBanner, | ||||
|   deleteCharacterBanner, | ||||
|   updateCharacterBannerOrder | ||||
| } | ||||
|   | ||||
| @@ -265,12 +265,11 @@ const routes = [ | ||||
|                 name: 'CharacterForm', | ||||
|                 component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterForm.vue') | ||||
|             }, | ||||
|             // TODO: CharacterBanner 컴포넌트가 구현되면 아래 라우트 주석 해제 | ||||
|             // { | ||||
|             //     path: '/character/banner', | ||||
|             //     name: 'CharacterBanner', | ||||
|             //     component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterBanner.vue') | ||||
|             // }, | ||||
|             { | ||||
|                 path: '/character/banner', | ||||
|                 name: 'CharacterBanner', | ||||
|                 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