vendors.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>{{ pageId }}</span>
  8. </div>
  9. </div>
  10. <!-- 검색 및 필터 영역 -->
  11. <div class="search--modules type2">
  12. <div class="search--inner">
  13. <div class="form--cont--filter">
  14. <v-select
  15. v-model="selectedCategory"
  16. :items="categoryOptions"
  17. variant="outlined"
  18. class="custom-select"
  19. label="카테고리"
  20. clearable
  21. >
  22. </v-select>
  23. </div>
  24. <div class="form--cont--text">
  25. <v-text-field
  26. v-model="searchName"
  27. class="custom-input mini"
  28. style="width: 100%"
  29. placeholder="벤더사명을 입력하세요"
  30. @keyup.enter="handleSearch"
  31. ></v-text-field>
  32. </div>
  33. </div>
  34. <v-btn
  35. class="custom-btn btn-blue mini sch--btn"
  36. @click="handleSearch"
  37. :loading="vendorsStore.getLoading"
  38. >
  39. 검색
  40. </v-btn>
  41. </div>
  42. <!-- 벤더사 리스트 -->
  43. <div class="data--list--wrap">
  44. <div class="btn--actions--wrap">
  45. <div class="left--sections">
  46. <span class="result-count">
  47. 총 {{ vendorsStore.getPagination?.totalCount || 0 }}개의 벤더사
  48. </span>
  49. </div>
  50. </div>
  51. <!-- 로딩 상태 -->
  52. <div v-if="vendorsStore.getLoading" class="loading-wrap">
  53. <v-progress-circular indeterminate color="primary"></v-progress-circular>
  54. <p>벤더사를 검색하고 있습니다...</p>
  55. </div>
  56. <!-- 에러 상태 -->
  57. <div v-else-if="vendorsStore.getError" class="error-wrap">
  58. <v-alert type="error" dismissible @click:close="vendorsStore.clearError()">
  59. {{ vendorsStore.getError }}
  60. </v-alert>
  61. </div>
  62. <!-- 벤더사 리스트 -->
  63. <div v-else-if="vendorsStore.getVendors?.length > 0" class="vendor--list--wrap">
  64. <v-data-table
  65. :headers="headers"
  66. :items="vendorsStore.getVendors"
  67. :loading="vendorsStore.getLoading"
  68. class="custom-data-table"
  69. @click:row="handleRowClick"
  70. >
  71. <template #item.logo="{ item }">
  72. <v-avatar size="40" class="vendor-logo">
  73. <v-img
  74. v-if="item.logo"
  75. :src="item.logo"
  76. :alt="item.name + ' 로고'"
  77. ></v-img>
  78. <div v-else class="no-logo">{{ item.name.charAt(0) }}</div>
  79. </v-avatar>
  80. </template>
  81. <template #item.category="{ item }">
  82. <v-chip
  83. :color="getCategoryColor(item.category)"
  84. size="small"
  85. variant="outlined"
  86. >
  87. {{ item.category }}
  88. </v-chip>
  89. </template>
  90. <template #item.status="{ item }">
  91. <v-chip
  92. :color="item.status === 'ACTIVE' ? 'success' : 'error'"
  93. size="small"
  94. >
  95. {{ item.status === 'ACTIVE' ? '활성' : '비활성' }}
  96. </v-chip>
  97. </template>
  98. <template #item.actions="{ item }">
  99. <v-btn
  100. icon="mdi-eye"
  101. size="small"
  102. variant="text"
  103. @click.stop="viewVendorDetail(item.id)"
  104. >
  105. </v-btn>
  106. </template>
  107. </v-data-table>
  108. <!-- 페이지네이션 -->
  109. <div class="pagination-wrap" v-if="(vendorsStore.getPagination?.totalPages || 0) > 1">
  110. <v-pagination
  111. v-model="currentPage"
  112. :length="vendorsStore.getPagination?.totalPages || 1"
  113. :total-visible="7"
  114. @update:model-value="handlePageChange"
  115. ></v-pagination>
  116. </div>
  117. </div>
  118. <!-- 검색 결과 없음 -->
  119. <div v-else class="no-data-wrap">
  120. <div class="no-data">
  121. <v-icon size="64" color="grey-lighten-1">mdi-store-search</v-icon>
  122. <h3>검색된 벤더사가 없습니다</h3>
  123. <p>다른 검색 조건을 시도해보세요</p>
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. </template>
  129. <script setup>
  130. import { ref, onMounted, computed } from 'vue'
  131. import { useRouter } from 'vue-router'
  132. import { useVendorsStore } from '@/stores/vendors'
  133. /************************************************************************
  134. | 레이아웃
  135. ************************************************************************/
  136. definePageMeta({
  137. layout: "default",
  138. })
  139. /************************************************************************
  140. | 스토어 & 라우터
  141. ************************************************************************/
  142. const vendorsStore = useVendorsStore()
  143. const router = useRouter()
  144. /************************************************************************
  145. | 반응형 데이터
  146. ************************************************************************/
  147. const pageId = ref("벤더사 관리")
  148. const searchName = ref("")
  149. const selectedCategory = ref("")
  150. const currentPage = ref(1)
  151. const categoryOptions = ref([
  152. { title: "전체", value: "" },
  153. { title: "패션·뷰티", value: "FASHION_BEAUTY" },
  154. { title: "식품·건강", value: "FOOD_HEALTH" },
  155. { title: "라이프스타일", value: "LIFESTYLE" },
  156. { title: "테크·가전", value: "TECH_ELECTRONICS" },
  157. { title: "스포츠·레저", value: "SPORTS_LEISURE" },
  158. { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" }
  159. ])
  160. const headers = [
  161. { title: "로고", key: "logo", sortable: false, width: "80px" },
  162. { title: "벤더사명", key: "name", sortable: true },
  163. { title: "카테고리", key: "category", sortable: true },
  164. { title: "담당자", key: "contactName", sortable: false },
  165. { title: "연락처", key: "contactPhone", sortable: false },
  166. { title: "이메일", key: "contactEmail", sortable: false },
  167. { title: "상태", key: "status", sortable: true },
  168. { title: "등록일", key: "createdAt", sortable: true },
  169. { title: "액션", key: "actions", sortable: false, width: "100px" }
  170. ]
  171. /************************************************************************
  172. | computed
  173. ************************************************************************/
  174. const currentSearchConditions = computed(() => vendorsStore.getSearchConditions)
  175. /************************************************************************
  176. | 메서드
  177. ************************************************************************/
  178. const handleSearch = async () => {
  179. const conditions = {
  180. name: searchName.value,
  181. category: selectedCategory.value,
  182. page: 1,
  183. size: 10
  184. }
  185. currentPage.value = 1
  186. await vendorsStore.searchVendors(conditions)
  187. }
  188. const handlePageChange = async (page) => {
  189. currentPage.value = page
  190. const conditions = {
  191. ...currentSearchConditions.value,
  192. page: page
  193. }
  194. await vendorsStore.searchVendors(conditions)
  195. }
  196. const handleRowClick = (event, { item }) => {
  197. if (item?.id) {
  198. viewVendorDetail(item.id)
  199. }
  200. }
  201. const viewVendorDetail = (vendorId) => {
  202. router.push(`/view/vendor/${vendorId}`)
  203. }
  204. const getCategoryColor = (category) => {
  205. const colors = {
  206. 'FASHION_BEAUTY': 'pink',
  207. 'FOOD_HEALTH': 'green',
  208. 'LIFESTYLE': 'blue',
  209. 'TECH_ELECTRONICS': 'purple',
  210. 'SPORTS_LEISURE': 'orange',
  211. 'CULTURE_ENTERTAINMENT': 'red'
  212. }
  213. return colors[category] || 'grey'
  214. }
  215. /************************************************************************
  216. | 라이프사이클
  217. ************************************************************************/
  218. onMounted(async () => {
  219. // 초기 검색 실행 (전체 벤더사 로드)
  220. await vendorsStore.searchVendors({
  221. name: '',
  222. category: '',
  223. page: 1,
  224. size: 10
  225. })
  226. })
  227. </script>
  228. <style scoped>
  229. .vendor--list--wrap {
  230. margin-top: 20px;
  231. }
  232. .loading-wrap, .error-wrap, .no-data-wrap {
  233. display: flex;
  234. flex-direction: column;
  235. align-items: center;
  236. justify-content: center;
  237. padding: 60px 20px;
  238. }
  239. .no-data {
  240. text-align: center;
  241. }
  242. .no-data h3 {
  243. margin: 16px 0 8px;
  244. color: #666;
  245. }
  246. .no-data p {
  247. color: #999;
  248. }
  249. .vendor-logo {
  250. border: 1px solid #e0e0e0;
  251. }
  252. .no-logo {
  253. background: #f5f5f5;
  254. color: #666;
  255. font-weight: bold;
  256. display: flex;
  257. align-items: center;
  258. justify-content: center;
  259. width: 100%;
  260. height: 100%;
  261. }
  262. .result-count {
  263. font-size: 14px;
  264. color: #666;
  265. font-weight: 500;
  266. }
  267. .pagination-wrap {
  268. display: flex;
  269. justify-content: center;
  270. margin-top: 20px;
  271. }
  272. .custom-data-table {
  273. background: white;
  274. border-radius: 8px;
  275. }
  276. .custom-data-table :deep(.v-data-table__tr) {
  277. cursor: pointer;
  278. }
  279. .custom-data-table :deep(.v-data-table__tr:hover) {
  280. background-color: #f5f5f5;
  281. }
  282. </style>