index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. <template>
  2. <div class="mypage-container">
  3. <!-- 헤더 섹션 -->
  4. <div class="mypage-header">
  5. <div class="header-content">
  6. <div class="profile-section">
  7. <div class="profile-avatar">
  8. <div class="avatar-placeholder">
  9. <i class="mdi mdi-account"></i>
  10. </div>
  11. </div>
  12. <div class="profile-info">
  13. <h1 class="user-name">{{ useAtStore.auth.nickName || useAtStore.auth.companyName || '사용자' }}</h1>
  14. <p class="user-type">{{ memberType === 'INFLUENCER' ? '인플루언서' : memberType === 'VENDOR' ? '벤더사' : '브랜드사' }}</p>
  15. </div>
  16. </div>
  17. </div>
  18. <!-- 통계 요약 카드 -->
  19. <div class="stats-overview">
  20. <div class="stat-card">
  21. <div class="stat-icon orders">
  22. <i class="mdi mdi-cart"></i>
  23. </div>
  24. <div class="stat-info">
  25. <h3>{{ recentOrdersCount }}</h3>
  26. <p>최근 주문 (7일)</p>
  27. </div>
  28. </div>
  29. <div class="stat-card">
  30. <div class="stat-icon items">
  31. <i class="mdi mdi-package-variant"></i>
  32. </div>
  33. <div class="stat-info">
  34. <h3>{{ activeItemsCount }}</h3>
  35. <p>진행중인 공동구매</p>
  36. </div>
  37. </div>
  38. <div class="stat-card">
  39. <div class="stat-icon sales">
  40. <i class="mdi mdi-trending-up"></i>
  41. </div>
  42. <div class="stat-info">
  43. <h3>{{ totalSalesCount }}</h3>
  44. <p>총 주문 건수</p>
  45. </div>
  46. </div>
  47. <div class="stat-card">
  48. <div class="stat-icon influencers">
  49. <i class="mdi mdi-account-group"></i>
  50. </div>
  51. <div class="stat-info">
  52. <h3>{{ activeInfluencersCount }}</h3>
  53. <p>활성 인플루언서</p>
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. <!-- 대시보드 섹션 -->
  59. <div class="dashboard-section">
  60. <div class="dashboard-cards">
  61. <!-- 최근 주문 현황 카드 -->
  62. <div class="dashboard-card recent-orders">
  63. <div class="card-header">
  64. <h3>최근 들어온 주문</h3>
  65. <i class="mdi mdi-cart-outline"></i>
  66. </div>
  67. <div class="card-content">
  68. <div class="order-list">
  69. <div v-if="recentOrders.length > 0">
  70. <div
  71. v-for="order in recentOrders"
  72. :key="order.SEQ"
  73. class="order-item"
  74. @click="goToOrderDetail(order.ITEM_SEQ)"
  75. >
  76. <div class="order-info">
  77. <h4>{{ order.ORDER_NUMB }}</h4>
  78. <p class="buyer-name">{{ order.BUYER_NAME }}</p>
  79. <p class="order-date">{{ formatDateTime(order.REGDATE) }}</p>
  80. </div>
  81. <div class="order-details">
  82. <span class="quantity">{{ order.QTY }}개</span>
  83. <span class="status-badge active">신규</span>
  84. </div>
  85. </div>
  86. </div>
  87. <div v-else class="no-orders">
  88. <i class="mdi mdi-cart-off"></i>
  89. <p>최근 주문이 없습니다</p>
  90. </div>
  91. </div>
  92. <div class="view-all">
  93. <v-btn variant="text" class="custom-btn btn-white" @click="goToOrders()">전체 보기</v-btn>
  94. </div>
  95. </div>
  96. </div>
  97. <!-- 진행중인 공동구매 카드 -->
  98. <div class="dashboard-card active-items">
  99. <div class="card-header">
  100. <h3>진행중인 공동구매</h3>
  101. <i class="mdi mdi-package-variant-closed"></i>
  102. </div>
  103. <div class="card-content">
  104. <div class="item-list">
  105. <div v-if="activeItems.length > 0">
  106. <div
  107. v-for="item in activeItems"
  108. :key="item.SEQ"
  109. class="item-card"
  110. @click="goToItemDetail(item.SEQ)"
  111. >
  112. <div class="item-info">
  113. <h4>{{ item.NAME }}</h4>
  114. <p class="company-name">{{ item.COMPANY_NAME }}</p>
  115. <div class="item-dates">
  116. <span class="end-date">기간: {{ formatDate(item.ORDER_START_DATE) }} ~ {{ formatDate(item.ORDER_END_DATE) }}</span>
  117. </div>
  118. </div>
  119. <div class="item-stats">
  120. <div class="order-count">주문: {{ item.ORDER_COUNT || 0 }}건</div>
  121. </div>
  122. </div>
  123. </div>
  124. <div v-else class="no-items">
  125. <i class="mdi mdi-package-variant-closed-remove"></i>
  126. <p>진행중인 공동구매가 없습니다</p>
  127. </div>
  128. </div>
  129. <div class="view-all">
  130. <v-btn variant="text" class="custom-btn btn-white" @click="goToItems()">전체 보기</v-btn>
  131. </div>
  132. </div>
  133. </div>
  134. <!-- 인플루언서별 판매 통계 카드 -->
  135. <div class="dashboard-card influencer-stats">
  136. <div class="card-header">
  137. <h3>인플루언서별 주문 건수</h3>
  138. <i class="mdi mdi-account-star"></i>
  139. </div>
  140. <div class="card-content">
  141. <div class="influencer-list">
  142. <div v-if="influencerStats.length > 0">
  143. <div
  144. v-for="(stat, index) in influencerStats"
  145. :key="stat.INF_SEQ"
  146. class="influencer-item"
  147. >
  148. <div class="rank">{{ index + 1 }}</div>
  149. <div class="influencer-info">
  150. <h4>{{ stat.INF_NAME || '인플루언서' + stat.INF_SEQ }}</h4>
  151. <p>{{ stat.ORDER_COUNT }}건의 주문</p>
  152. </div>
  153. <div class="order-count-badge">
  154. {{ stat.ORDER_COUNT }}
  155. </div>
  156. </div>
  157. </div>
  158. <div v-else class="no-stats">
  159. <i class="mdi mdi-chart-line"></i>
  160. <p>통계 데이터가 없습니다</p>
  161. </div>
  162. </div>
  163. </div>
  164. </div>
  165. </div>
  166. </div>
  167. <!-- 문의 등록 팝업 -->
  168. <v-dialog v-model="showInquiryModal" max-width="600px" persistent>
  169. <v-card class="inquiry-modal">
  170. <v-card-title class="modal-header">
  171. <h3>문의 등록</h3>
  172. <v-btn icon @click="closeModal" class="close-btn">
  173. <v-icon>mdi-close</v-icon>
  174. </v-btn>
  175. </v-card-title>
  176. <v-card-text class="modal-body">
  177. <form @submit.prevent="submitInquiry">
  178. <div class="form-group">
  179. <label>문의 유형 <span class="required">*</span></label>
  180. <v-select
  181. v-model="inquiryForm.category"
  182. :items="categoryOptions"
  183. variant="outlined"
  184. placeholder="문의 유형을 선택하세요"
  185. class="custom-select"
  186. :rules="[v => !!v || '문의 유형을 선택해주세요']"
  187. ></v-select>
  188. </div>
  189. <div class="form-group">
  190. <label>제목 <span class="required">*</span></label>
  191. <v-text-field
  192. v-model="inquiryForm.title"
  193. variant="outlined"
  194. placeholder="문의 제목을 입력하세요"
  195. class="custom-input"
  196. :rules="[v => !!v || '제목을 입력해주세요']"
  197. ></v-text-field>
  198. </div>
  199. <div class="form-group">
  200. <label>내용 <span class="required">*</span></label>
  201. <v-textarea
  202. v-model="inquiryForm.content"
  203. variant="outlined"
  204. placeholder="문의 내용을 상세히 입력해주세요"
  205. rows="6"
  206. class="custom-textarea"
  207. :rules="[v => !!v || '내용을 입력해주세요']"
  208. ></v-textarea>
  209. </div>
  210. <div class="form-group">
  211. <label>첨부파일</label>
  212. <v-file-input
  213. v-model="inquiryForm.attachments"
  214. variant="outlined"
  215. placeholder="파일을 선택하세요"
  216. hide-details=""
  217. prepend-icon=""
  218. append-inner-icon="mdi-paperclip"
  219. class="custom-file-input mb--30"
  220. accept="image/*,.pdf,.doc,.docx,.hwp"
  221. multiple
  222. show-size
  223. ></v-file-input>
  224. <p class="file-info">* 이미지, PDF, 문서 파일만 업로드 가능 (최대 10MB)</p>
  225. </div>
  226. </form>
  227. </v-card-text>
  228. <v-card-actions class="modal-footer">
  229. <v-btn @click="closeModal" class="cancel-btn">취소</v-btn>
  230. <v-btn @click="submitInquiry" class="submit-btn" :loading="isSubmitting">등록하기</v-btn>
  231. </v-card-actions>
  232. </v-card>
  233. </v-dialog>
  234. </div>
  235. </template>
  236. <script setup>
  237. import "@vuepic/vue-datepicker/dist/main.css";
  238. import dayjs from 'dayjs';
  239. /************************************************************************
  240. | 레이아웃
  241. ************************************************************************/
  242. definePageMeta({
  243. layout: "default",
  244. });
  245. /************************************************************************
  246. | PROPS
  247. ************************************************************************/
  248. const props = defineProps({
  249. propsData: {
  250. type: Object,
  251. default: () => {},
  252. },
  253. });
  254. /************************************************************************
  255. | 스토어
  256. ************************************************************************/
  257. const useDtStore = useDetailStore();
  258. const useAtStore = useAuthStore();
  259. /************************************************************************
  260. | 전역
  261. ************************************************************************/
  262. const memberType = useAtStore.auth.memberType;
  263. const searchModel = ref("");
  264. const selectedRange = ref('all');
  265. const searchStartDate = ref("");
  266. const searchEndDate = ref("");
  267. const dateOptions = [
  268. { label: '오늘', value: 'today' },
  269. { label: '7일', value: '7d' },
  270. { label: '1개월', value: '1m' },
  271. { label: '3개월', value: '3m' },
  272. { label: '전체', value: 'all' },
  273. ]
  274. const datePickerFormat = "yyyy-MM-dd";
  275. const filter = ref("");
  276. const filderArr = ref([
  277. { title: "전체", value: "" },
  278. { title: "제목", value: "title" },
  279. { title: "내용", value: "content" },
  280. { title: "작성자", value: "writer" }
  281. ]);
  282. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  283. const router = useRouter();
  284. const pageId = computed(() => {
  285. return '대시보드';
  286. });
  287. const csList = ref([]);
  288. const recentOrders = ref([]);
  289. const activeItems = ref([]);
  290. const influencerStats = ref([]);
  291. // 대시보드 관련 computed
  292. const completedInquiries = computed(() => {
  293. return csList.value.filter(item => item.STATUS === '1').length;
  294. });
  295. const recentInquiries = computed(() => {
  296. return csList.value.slice(0, 3);
  297. });
  298. // 새로운 대시보드 통계
  299. const recentOrdersCount = computed(() => {
  300. const weekAgo = new Date();
  301. weekAgo.setDate(weekAgo.getDate() - 7);
  302. return recentOrders.value.filter(order =>
  303. new Date(order.REGDATE) >= weekAgo
  304. ).length;
  305. });
  306. const activeItemsCount = computed(() => {
  307. return activeItems.value.length;
  308. });
  309. const totalSalesCount = computed(() => {
  310. return recentOrders.value.length;
  311. });
  312. const activeInfluencersCount = computed(() => {
  313. return influencerStats.value.filter(stat => stat.ORDER_COUNT > 0).length;
  314. });
  315. // 문의 등록 팝업 관련
  316. const showInquiryModal = ref(false);
  317. const isSubmitting = ref(false);
  318. const inquiryForm = ref({
  319. category: '',
  320. title: '',
  321. content: '',
  322. attachments: []
  323. });
  324. const categoryOptions = ref([
  325. { title: '기능문의', value: 'D' },
  326. { title: '기타문의', value: 'E' }
  327. ]);
  328. /* eslint-disable */
  329. /* prettier-ignore */
  330. /************************************************************************
  331. | 함수(METHODS)
  332. ************************************************************************/
  333. // 대시보드 데이터 로드 함수들
  334. const loadRecentOrders = async () => {
  335. try {
  336. const _req = {
  337. COMPANY_NUMBER: useAtStore.auth.companyNumber,
  338. MEMBER_TYPE: useAtStore.auth.memberType,
  339. MEMBER_SEQ: useAtStore.auth.seq,
  340. LIMIT: 5
  341. };
  342. const response = await useAxios().post('/dashboard/recentOrders', _req);
  343. recentOrders.value = response.data;
  344. } catch (error) {
  345. console.error('최근 주문 로드 실패:', error);
  346. }
  347. };
  348. const loadActiveItems = async () => {
  349. try {
  350. const _req = {
  351. SHOW_YN: "Y",
  352. TYPE: "G",
  353. //INF_SEQ: useAtStore.auth.seq,
  354. MEMBER_TYPE: memberType,
  355. MEMBER_SEQ: useAtStore.auth.seq,
  356. STATUS: 0,
  357. COMPANY_NUMBER: useAtStore.auth.companyNumber,
  358. COUNT: 5,
  359. };
  360. console.warn(_req)
  361. const response = await useAxios().post('/item/list', _req);
  362. console.error(response)
  363. activeItems.value = response.data;
  364. } catch (error) {
  365. console.error('진행중인 공동구매 로드 실패:', error);
  366. }
  367. };
  368. const loadInfluencerStats = async () => {
  369. try {
  370. const _req = {
  371. COMPANY_NUMBER: useAtStore.auth.companyNumber,
  372. MEMBER_TYPE: useAtStore.auth.memberType,
  373. MEMBER_SEQ: useAtStore.auth.seq,
  374. LIMIT: 5
  375. };
  376. const response = await useAxios().post('/dashboard/influencerStats', _req);
  377. influencerStats.value = response.data;
  378. } catch (error) {
  379. console.error('인플루언서 통계 로드 실패:', error);
  380. }
  381. };
  382. const loadDashboardData = async () => {
  383. await Promise.all([
  384. loadRecentOrders(),
  385. loadActiveItems(),
  386. loadInfluencerStats(),
  387. csListGet()
  388. ]);
  389. };
  390. const isRecentUpdate = (dateStr) => {
  391. const today = new Date();
  392. const updateDate = new Date(dateStr);
  393. const diffDays = (today - updateDate) / (1000 * 60 * 60 * 24);
  394. // 업데이트 날짜가 오늘 날짜 기준 최근 7일인지 확인
  395. return diffDays <= 7;
  396. }
  397. const paginatedItems = computed(() => {
  398. const start = (currentPage.value - 1) * itemsPerPage;
  399. return csList.value.slice(start, start + itemsPerPage);
  400. });
  401. const setDateRange = (range) => {
  402. const today = dayjs();
  403. switch(range) {
  404. case 'today' :
  405. searchStartDate.value = today.format('YYYY-MM-DD');
  406. searchEndDate.value = today.format('YYYY-MM-DD');
  407. selectedRange.value = 'today';
  408. break;
  409. case '7d':
  410. searchStartDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
  411. searchEndDate.value = today.format('YYYY-MM-DD');
  412. selectedRange.value = '7d';
  413. break;
  414. case '1m':
  415. searchStartDate.value = today.subtract(1, 'month').format('YYYY-MM-DD');
  416. searchEndDate.value = today.format('YYYY-MM-DD');
  417. selectedRange.value = '1m';
  418. break;
  419. case '3m':
  420. searchStartDate.value = today.subtract(3, 'month').format('YYYY-MM-DD');
  421. searchEndDate.value = today.format('YYYY-MM-DD');
  422. selectedRange.value = '3m';
  423. break;
  424. case 'all':
  425. searchStartDate.value = "";
  426. searchEndDate.value = today.format('YYYY-MM-DD');
  427. selectedRange.value = 'all';
  428. break
  429. }
  430. }
  431. const addLocated = () => {
  432. showInquiryModal.value = true;
  433. };
  434. const goToCS = () => {
  435. router.push('/view/common/cs');
  436. };
  437. const goToItems = () => {
  438. router.push('/view/common/item');
  439. };
  440. const goToOrders = () => {
  441. router.push('/view/common/item');
  442. };
  443. const goToOrderDetail = (itemSeq) => {
  444. useDtStore.boardInfo.seq = itemSeq;
  445. useDtStore.boardInfo.pageType = "D";
  446. router.push({
  447. path: "/view/common/item/detail",
  448. query: { itemId: itemSeq }
  449. });
  450. };
  451. const goToItemDetail = (itemSeq) => {
  452. useDtStore.boardInfo.seq = itemSeq;
  453. router.push({
  454. path: "/view/common/item/detail",
  455. query: { itemId: itemSeq }
  456. });
  457. };
  458. const goToProfile = () => {
  459. $toast.info('프로필 수정 기능은 준비중입니다.');
  460. };
  461. const toItemDetail = (__EVENT) => {
  462. router.push({
  463. path: "/view/common/cs/detail",
  464. });
  465. useDtStore.boardInfo.seq = __EVENT;
  466. };
  467. const csListGet = async () => {
  468. let _req = {
  469. USER_SEQ : useAtStore.auth.seq,
  470. keyword: '',
  471. filter: '',
  472. startDate: '',
  473. endDate: ''
  474. };
  475. if(useAtStore.auth.seq == 2){
  476. _req.USER_SEQ = 0;
  477. }
  478. await useAxios()
  479. .post("/cs/list", _req)
  480. .then((res) => {
  481. csList.value = res.data;
  482. });
  483. };
  484. const fnSearch = (__KEYWORD, __FILTER) => {
  485. let _req = {
  486. USER_SEQ: useAtStore.auth.seq,
  487. filter: __FILTER,
  488. keyword: __KEYWORD,
  489. startDate: searchStartDate.value ? dayjs(searchStartDate.value).format('YYYY-MM-DD') : '',
  490. endDate: searchEndDate.value ? dayjs(searchEndDate.value).format('YYYY-MM-DD') : ''
  491. };
  492. // 관리자인 경우 모든 문의 조회
  493. if(useAtStore.auth.seq == 2){
  494. _req.USER_SEQ = 0;
  495. }
  496. useAxios()
  497. .post("/cs/search", _req)
  498. .then((res) => {
  499. csList.value = res.data;
  500. currentPage.value = 1; // 검색 후 첫 페이지로 이동
  501. })
  502. .catch((error) => {
  503. console.error('검색 실패:', error);
  504. $toast.error('검색에 실패했습니다.');
  505. });
  506. };
  507. const goToDeliveryDetail = (item) => {
  508. // 제품 정보를 스토어에 저장
  509. useDtStore.boardInfo.seq = item.SEQ;
  510. useDtStore.boardInfo.pageType = "D";
  511. // 배송 관리 페이지로 이동
  512. router.push({
  513. path: "/view/common/deli/detail",
  514. query: {
  515. itemId: item.SEQ,
  516. itemName: item.NAME,
  517. price1: item.PRICE1,
  518. price2: item.PRICE2 || item.PRICE1,
  519. thumbFile: item.THUMB_FILE || ''
  520. }
  521. });
  522. };
  523. const getStatusClass = (status) => {
  524. switch(status) {
  525. case '0':
  526. return 'status-waiting';
  527. case '1':
  528. return 'status-completed';
  529. }
  530. };
  531. const formatDate = (dateStr) => {
  532. if (!dateStr) return '';
  533. const date = new Date(dateStr);
  534. const year = date.getFullYear();
  535. const month = String(date.getMonth() + 1).padStart(2, '0');
  536. const day = String(date.getDate()).padStart(2, '0');
  537. return `${year}.${month}.${day}`;
  538. };
  539. const formatDateTime = (dateStr) => {
  540. if (!dateStr) return '';
  541. const date = new Date(dateStr);
  542. const month = String(date.getMonth() + 1).padStart(2, '0');
  543. const day = String(date.getDate()).padStart(2, '0');
  544. const hours = String(date.getHours()).padStart(2, '0');
  545. const minutes = String(date.getMinutes()).padStart(2, '0');
  546. return `${month}.${day} ${hours}:${minutes}`;
  547. };
  548. const getDaysRemaining = (endDate) => {
  549. if (!endDate) return 0;
  550. const end = new Date(endDate);
  551. const now = new Date();
  552. const diffTime = end - now;
  553. const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  554. return Math.max(0, diffDays);
  555. };
  556. // 팝업 관련 함수들
  557. const closeModal = () => {
  558. showInquiryModal.value = false;
  559. resetForm();
  560. };
  561. const resetForm = () => {
  562. inquiryForm.value = {
  563. category: '',
  564. title: '',
  565. content: '',
  566. attachments: []
  567. };
  568. };
  569. const submitInquiry = async () => {
  570. // 폼 유효성 검사
  571. if (!inquiryForm.value.category) {
  572. $toast.error('문의 유형을 선택해주세요.');
  573. return;
  574. }
  575. if (!inquiryForm.value.title) {
  576. $toast.error('제목을 입력해주세요.');
  577. return;
  578. }
  579. if (!inquiryForm.value.content) {
  580. $toast.error('내용을 입력해주세요.');
  581. return;
  582. }
  583. isSubmitting.value = true;
  584. try {
  585. const formData = new FormData();
  586. formData.append('USER_SEQ', useAtStore.auth.seq);
  587. formData.append('CATEGORY', inquiryForm.value.category);
  588. formData.append('TITLE', inquiryForm.value.title);
  589. formData.append('CONTENT', inquiryForm.value.content);
  590. // 첨부파일 처리
  591. if (inquiryForm.value.attachments && inquiryForm.value.attachments.length > 0) {
  592. inquiryForm.value.attachments.forEach((file, index) => {
  593. formData.append(`files[${index}]`, file);
  594. });
  595. }
  596. await useAxios()
  597. .post("/cs/reg", formData, {
  598. headers: {
  599. 'Content-Type': 'multipart/form-data'
  600. }
  601. })
  602. .then((res) => {
  603. if (res.data.success) {
  604. $toast.success('문의가 성공적으로 등록되었습니다.');
  605. closeModal();
  606. csListGet(); // 목록 새로고침
  607. } else {
  608. $toast.error('문의 등록에 실패했습니다.');
  609. }
  610. });
  611. } catch (error) {
  612. $toast.error('문의 등록 중 오류가 발생했습니다.');
  613. console.error('Error submitting inquiry:', error);
  614. } finally {
  615. isSubmitting.value = false;
  616. }
  617. };
  618. /************************************************************************
  619. | WATCH
  620. ************************************************************************/
  621. onMounted(() => {
  622. loadDashboardData();
  623. // 날짜 초기화
  624. const today = dayjs();
  625. searchEndDate.value = today.format('YYYY-MM-DD');
  626. });
  627. </script>