NotificationCenter.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <template>
  2. <div class="notification-center">
  3. <v-btn
  4. icon
  5. variant="text"
  6. @click="toggleNotifications"
  7. class="notification-btn"
  8. >
  9. <v-icon>mdi-bell</v-icon>
  10. <v-badge
  11. v-if="unreadCount > 0"
  12. :content="unreadCount"
  13. color="error"
  14. offset-x="12"
  15. offset-y="12"
  16. >
  17. </v-badge>
  18. </v-btn>
  19. <v-menu
  20. v-model="showNotifications"
  21. :close-on-content-click="false"
  22. location="bottom end"
  23. offset="8"
  24. >
  25. <template v-slot:activator="{ props }">
  26. <div v-bind="props"></div>
  27. </template>
  28. <v-card class="notification-menu" min-width="320" max-width="400">
  29. <v-card-title class="notification-header">
  30. <span>알림</span>
  31. <v-btn
  32. v-if="notifications.length > 0"
  33. variant="text"
  34. size="small"
  35. @click="markAllAsRead"
  36. >
  37. 모두 읽음
  38. </v-btn>
  39. </v-card-title>
  40. <v-divider></v-divider>
  41. <v-card-text class="notification-list" v-if="notifications.length > 0">
  42. <div
  43. v-for="notification in notifications"
  44. :key="notification.id"
  45. class="notification-item"
  46. :class="{ 'unread': !notification.read }"
  47. @click="markAsRead(notification.id)"
  48. >
  49. <div class="notification-content">
  50. <div class="notification-title">{{ notification.title }}</div>
  51. <div class="notification-message">{{ notification.message }}</div>
  52. <div class="notification-time">{{ formatTime(notification.createdAt) }}</div>
  53. </div>
  54. <v-btn
  55. icon="mdi-close"
  56. variant="text"
  57. size="x-small"
  58. @click.stop="removeNotification(notification.id)"
  59. ></v-btn>
  60. </div>
  61. </v-card-text>
  62. <v-card-text v-else class="text-center text-grey">
  63. 새로운 알림이 없습니다.
  64. </v-card-text>
  65. </v-card>
  66. </v-menu>
  67. </div>
  68. </template>
  69. <script setup>
  70. import { ref, computed, onMounted, onUnmounted } from 'vue'
  71. const { $eventBus } = useNuxtApp()
  72. const showNotifications = ref(false)
  73. const notifications = ref([])
  74. const unreadCount = computed(() => {
  75. return notifications.value.filter(n => !n.read).length
  76. })
  77. const toggleNotifications = () => {
  78. showNotifications.value = !showNotifications.value
  79. }
  80. const addNotification = (title, message, type = 'info') => {
  81. const notification = {
  82. id: Date.now() + Math.random(),
  83. title,
  84. message,
  85. type,
  86. read: false,
  87. createdAt: new Date()
  88. }
  89. notifications.value.unshift(notification)
  90. // 최대 50개까지만 보관
  91. if (notifications.value.length > 50) {
  92. notifications.value = notifications.value.slice(0, 50)
  93. }
  94. }
  95. const markAsRead = (id) => {
  96. const notification = notifications.value.find(n => n.id === id)
  97. if (notification) {
  98. notification.read = true
  99. }
  100. }
  101. const markAllAsRead = () => {
  102. notifications.value.forEach(n => n.read = true)
  103. }
  104. const removeNotification = (id) => {
  105. const index = notifications.value.findIndex(n => n.id === id)
  106. if (index > -1) {
  107. notifications.value.splice(index, 1)
  108. }
  109. }
  110. const formatTime = (date) => {
  111. const now = new Date()
  112. const diff = now - date
  113. const minutes = Math.floor(diff / 60000)
  114. const hours = Math.floor(minutes / 60)
  115. const days = Math.floor(hours / 24)
  116. if (minutes < 1) return '방금 전'
  117. if (minutes < 60) return `${minutes}분 전`
  118. if (hours < 24) return `${hours}시간 전`
  119. if (days < 7) return `${days}일 전`
  120. return date.toLocaleDateString()
  121. }
  122. const handleDeliveryStatusChange = (data) => {
  123. const statusText = {
  124. 'NEW': '신규',
  125. 'PENDING': '대기',
  126. 'COMPLETE': '완료'
  127. }[data.status] || data.status
  128. addNotification(
  129. '배송 상태 변경',
  130. `${data.itemName}의 상태가 "${statusText}"로 변경되었습니다.`,
  131. 'success'
  132. )
  133. }
  134. const handleNewOrderReceived = (data) => {
  135. addNotification(
  136. '새 주문 접수',
  137. `${data.itemName}에 새로운 주문이 접수되었습니다.`,
  138. 'info'
  139. )
  140. }
  141. onMounted(() => {
  142. $eventBus.on('DELIVERY_STATUS_CHANGED', handleDeliveryStatusChange)
  143. $eventBus.on('NEW_ORDER_RECEIVED', handleNewOrderReceived)
  144. })
  145. onUnmounted(() => {
  146. $eventBus.off('DELIVERY_STATUS_CHANGED', handleDeliveryStatusChange)
  147. $eventBus.off('NEW_ORDER_RECEIVED', handleNewOrderReceived)
  148. })
  149. </script>
  150. <style scoped>
  151. .notification-center {
  152. position: relative;
  153. }
  154. .notification-btn {
  155. position: relative;
  156. }
  157. .notification-menu {
  158. max-height: 500px;
  159. overflow-y: auto;
  160. }
  161. .notification-header {
  162. display: flex;
  163. justify-content: space-between;
  164. align-items: center;
  165. padding: 12px 16px;
  166. }
  167. .notification-list {
  168. padding: 0;
  169. max-height: 400px;
  170. overflow-y: auto;
  171. }
  172. .notification-item {
  173. display: flex;
  174. justify-content: space-between;
  175. align-items: flex-start;
  176. padding: 12px 16px;
  177. border-bottom: 1px solid #e0e0e0;
  178. cursor: pointer;
  179. transition: background-color 0.2s;
  180. }
  181. .notification-item:hover {
  182. background-color: #f5f5f5;
  183. }
  184. .notification-item.unread {
  185. background-color: #e3f2fd;
  186. }
  187. .notification-item.unread::before {
  188. content: '';
  189. position: absolute;
  190. left: 8px;
  191. top: 50%;
  192. transform: translateY(-50%);
  193. width: 6px;
  194. height: 6px;
  195. border-radius: 50%;
  196. background-color: #2196f3;
  197. }
  198. .notification-content {
  199. flex: 1;
  200. min-width: 0;
  201. }
  202. .notification-title {
  203. font-weight: 600;
  204. font-size: 14px;
  205. margin-bottom: 4px;
  206. }
  207. .notification-message {
  208. font-size: 13px;
  209. color: #666;
  210. margin-bottom: 4px;
  211. line-height: 1.3;
  212. }
  213. .notification-time {
  214. font-size: 11px;
  215. color: #999;
  216. }
  217. </style>