index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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. <div class="data--list--wrap">
  11. <div class="item--list--wrap">
  12. <div class="cs--list--wrap">
  13. <div class="cs--header">
  14. <h3>문의 등록</h3>
  15. <v-btn class="custom-btn" @click="addLocated()">문의 등록</v-btn>
  16. </div>
  17. <div class="cs--header">
  18. <h3>나의 문의 내역</h3>
  19. </div>
  20. <!-- 검색 영역 -->
  21. <div class="cs--search-area">
  22. <div class="search-controls">
  23. <div class="search-filter">
  24. <v-select
  25. v-model="filter"
  26. :items="filderArr"
  27. variant="outlined"
  28. density="compact"
  29. hide-details
  30. ></v-select>
  31. </div>
  32. <div class="search-input">
  33. <v-text-field
  34. v-model="searchModel"
  35. placeholder="검색어를 입력하세요"
  36. variant="outlined"
  37. density="compact"
  38. hide-details
  39. prepend-inner-icon="mdi-magnify"
  40. @keyup.enter="fnSearch(searchModel, filter)"
  41. ></v-text-field>
  42. </div>
  43. <div class="search-actions">
  44. <v-btn
  45. class="custom-btn mini btn--pp"
  46. color="primary"
  47. @click="fnSearch(searchModel, filter)"
  48. >
  49. 검색
  50. </v-btn>
  51. <v-btn
  52. class="custom-btn mini btn-white"
  53. color="secondary"
  54. variant="outlined"
  55. @click="resetSearch"
  56. >
  57. 초기화
  58. </v-btn>
  59. </div>
  60. </div>
  61. </div>
  62. <div class="cs--list" v-if="csList.length > 0">
  63. <div v-for="(items, index) in paginatedItems" :key="index">
  64. <div class="list" @click="toItemDetail(items.SEQ)">
  65. <span class="list--seq">{{ items.SEQ }}</span>
  66. <span class="list--circle">{{ items.CATEGORY == 'D' ? '기능문의' : '기타문의' }}</span>
  67. <span class="list--writer">{{ items.NICK_NAME ? items.NICK_NAME : items.COMPANY_NAME }}</span>
  68. <span class="list--title">{{ items.TITLE }}</span>
  69. <span class="list--ml" :class="getStatusClass(items.STATUS)">{{ items.STATUS == '0' ? "답변대기" : "답변완료" }}</span>
  70. <span class="list--date">{{ formatDate(items.REGDATE) }}</span>
  71. </div>
  72. </div>
  73. </div>
  74. <div class="cs--list" v-else>
  75. <div class="no-data">
  76. <h4>등록된 문의가 없습니다</h4>
  77. <p>새로운 문의를 등록해보세요</p>
  78. </div>
  79. </div>
  80. </div>
  81. <div class="item--pagination" v-if="csList.length > 0">
  82. <v-pagination
  83. v-model="currentPage"
  84. :length="Math.ceil(csList.length / itemsPerPage)"
  85. ></v-pagination>
  86. </div>
  87. </div>
  88. </div>
  89. <!-- 문의 등록 팝업 -->
  90. <v-dialog v-model="showInquiryModal" max-width="600px" persistent>
  91. <v-card class="inquiry-modal">
  92. <v-card-title class="modal-header">
  93. <h3>문의 등록</h3>
  94. <v-btn icon @click="closeModal" class="close-btn">
  95. <v-icon>mdi-close</v-icon>
  96. </v-btn>
  97. </v-card-title>
  98. <v-card-text class="modal-body">
  99. <form @submit.prevent="submitInquiry">
  100. <div class="form-group">
  101. <label>문의 유형 <span class="required">*</span></label>
  102. <v-select
  103. v-model="inquiryForm.category"
  104. :items="categoryOptions"
  105. variant="outlined"
  106. placeholder="문의 유형을 선택하세요"
  107. class="custom-select"
  108. :rules="[v => !!v || '문의 유형을 선택해주세요']"
  109. ></v-select>
  110. </div>
  111. <div class="form-group">
  112. <label>제목 <span class="required">*</span></label>
  113. <v-text-field
  114. v-model="inquiryForm.title"
  115. variant="outlined"
  116. placeholder="문의 제목을 입력하세요"
  117. class="custom-input"
  118. :rules="[v => !!v || '제목을 입력해주세요']"
  119. ></v-text-field>
  120. </div>
  121. <div class="form-group">
  122. <label>내용 <span class="required">*</span></label>
  123. <v-textarea
  124. v-model="inquiryForm.content"
  125. variant="outlined"
  126. placeholder="문의 내용을 상세히 입력해주세요"
  127. rows="6"
  128. class="custom-textarea"
  129. :rules="[v => !!v || '내용을 입력해주세요']"
  130. ></v-textarea>
  131. </div>
  132. <div class="form-group">
  133. <label>첨부파일</label>
  134. <v-file-input
  135. v-model="inquiryForm.attachments"
  136. variant="outlined"
  137. placeholder="파일을 선택하세요"
  138. hide-details=""
  139. prepend-icon=""
  140. append-inner-icon="mdi-paperclip"
  141. class="custom-file-input mb--30"
  142. accept="image/*,.pdf,.doc,.docx,.hwp"
  143. multiple
  144. show-size
  145. ></v-file-input>
  146. <p class="file-info">* 이미지, PDF, 문서 파일만 업로드 가능 (최대 10MB)</p>
  147. </div>
  148. </form>
  149. </v-card-text>
  150. <v-card-actions class="modal-footer">
  151. <v-btn @click="closeModal" class="cancel-btn">취소</v-btn>
  152. <v-btn @click="submitInquiry" class="submit-btn" :loading="isSubmitting">등록하기</v-btn>
  153. </v-card-actions>
  154. </v-card>
  155. </v-dialog>
  156. </div>
  157. </template>
  158. <script setup>
  159. import "@vuepic/vue-datepicker/dist/main.css";
  160. import dayjs from 'dayjs';
  161. /************************************************************************
  162. | 레이아웃
  163. ************************************************************************/
  164. definePageMeta({
  165. layout: "default",
  166. });
  167. /************************************************************************
  168. | PROPS
  169. ************************************************************************/
  170. const props = defineProps({
  171. propsData: {
  172. type: Object,
  173. default: () => {},
  174. },
  175. });
  176. /************************************************************************
  177. | 스토어
  178. ************************************************************************/
  179. const useDtStore = useDetailStore();
  180. const useAtStore = useAuthStore();
  181. /************************************************************************
  182. | 전역
  183. ************************************************************************/
  184. const memberType = useAtStore.auth.memberType;
  185. const searchModel = ref("");
  186. const selectedRange = ref('all');
  187. const searchStartDate = ref("");
  188. const searchEndDate = ref("");
  189. const dateOptions = [
  190. { label: '오늘', value: 'today' },
  191. { label: '7일', value: '7d' },
  192. { label: '1개월', value: '1m' },
  193. { label: '3개월', value: '3m' },
  194. { label: '전체', value: 'all' },
  195. ]
  196. const datePickerFormat = "yyyy-MM-dd";
  197. const filter = ref("");
  198. const filderArr = ref([
  199. { title: "전체", value: "" },
  200. { title: "제목", value: "title" },
  201. { title: "내용", value: "content" },
  202. { title: "작성자", value: "writer" }
  203. ]);
  204. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  205. const router = useRouter();
  206. const pageId = computed(() => {
  207. return '고객센터';
  208. });
  209. const csList = ref([]);
  210. const allCsList = ref([]); // 전체 데이터 저장용
  211. const itemsPerPage = 7;
  212. const currentPage = ref(1);
  213. // 문의 등록 팝업 관련
  214. const showInquiryModal = ref(false);
  215. const isSubmitting = ref(false);
  216. const inquiryForm = ref({
  217. category: '',
  218. title: '',
  219. content: '',
  220. attachments: []
  221. });
  222. const categoryOptions = ref([
  223. { title: '기능문의', value: 'D' },
  224. { title: '기타문의', value: 'E' }
  225. ]);
  226. /* eslint-disable */
  227. /* prettier-ignore */
  228. /************************************************************************
  229. | 함수(METHODS)
  230. ************************************************************************/
  231. const isRecentUpdate = (dateStr) => {
  232. const today = new Date();
  233. const updateDate = new Date(dateStr);
  234. const diffDays = (today - updateDate) / (1000 * 60 * 60 * 24);
  235. // 업데이트 날짜가 오늘 날짜 기준 최근 7일인지 확인
  236. return diffDays <= 7;
  237. }
  238. const paginatedItems = computed(() => {
  239. const start = (currentPage.value - 1) * itemsPerPage;
  240. return csList.value.slice(start, start + itemsPerPage);
  241. });
  242. const setDateRange = (range) => {
  243. const today = dayjs();
  244. switch(range) {
  245. case 'today' :
  246. searchStartDate.value = today.format('YYYY-MM-DD');
  247. searchEndDate.value = today.format('YYYY-MM-DD');
  248. selectedRange.value = 'today';
  249. break;
  250. case '7d':
  251. searchStartDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
  252. searchEndDate.value = today.format('YYYY-MM-DD');
  253. selectedRange.value = '7d';
  254. break;
  255. case '1m':
  256. searchStartDate.value = today.subtract(1, 'month').format('YYYY-MM-DD');
  257. searchEndDate.value = today.format('YYYY-MM-DD');
  258. selectedRange.value = '1m';
  259. break;
  260. case '3m':
  261. searchStartDate.value = today.subtract(3, 'month').format('YYYY-MM-DD');
  262. searchEndDate.value = today.format('YYYY-MM-DD');
  263. selectedRange.value = '3m';
  264. break;
  265. case 'all':
  266. searchStartDate.value = "";
  267. searchEndDate.value = today.format('YYYY-MM-DD');
  268. selectedRange.value = 'all';
  269. break
  270. }
  271. }
  272. const addLocated = () => {
  273. showInquiryModal.value = true;
  274. };
  275. const toItemDetail = (__EVENT) => {
  276. router.push({
  277. path: "/view/common/cs/detail",
  278. });
  279. useDtStore.boardInfo.seq = __EVENT;
  280. };
  281. const csListGet = async () => {
  282. let _req = {
  283. USER_SEQ : useAtStore.auth.seq,
  284. keyword: '',
  285. filter: '',
  286. startDate: '',
  287. endDate: ''
  288. };
  289. if(useAtStore.auth.seq == 2){
  290. _req.USER_SEQ = 0;
  291. }
  292. await useAxios()
  293. .post("/cs/list", _req)
  294. .then((res) => {
  295. allCsList.value = res.data; // 전체 데이터 저장
  296. csList.value = res.data; // 초기 표시용
  297. });
  298. };
  299. const fnSearch = (__KEYWORD, __FILTER) => {
  300. let filteredItems = [...allCsList.value];
  301. // 키워드 검색
  302. if (__KEYWORD && __KEYWORD.trim() !== '') {
  303. const keyword = __KEYWORD.toLowerCase();
  304. filteredItems = filteredItems.filter(item => {
  305. if (__FILTER === 'title') {
  306. return item.TITLE && item.TITLE.toLowerCase().includes(keyword);
  307. } else if (__FILTER === 'content') {
  308. return item.CONTENT && item.CONTENT.toLowerCase().includes(keyword);
  309. } else if (__FILTER === 'writer') {
  310. // 작성자 검색: 닉네임, 회사명, 이름 모두 검색
  311. return (item.NICK_NAME && item.NICK_NAME.toLowerCase().includes(keyword)) ||
  312. (item.COMPANY_NAME && item.COMPANY_NAME.toLowerCase().includes(keyword)) ||
  313. (item.NAME && item.NAME.toLowerCase().includes(keyword));
  314. } else {
  315. // 전체 검색 (제목, 내용, 작성자 모두)
  316. return (item.TITLE && item.TITLE.toLowerCase().includes(keyword)) ||
  317. (item.CONTENT && item.CONTENT.toLowerCase().includes(keyword)) ||
  318. (item.NICK_NAME && item.NICK_NAME.toLowerCase().includes(keyword)) ||
  319. (item.COMPANY_NAME && item.COMPANY_NAME.toLowerCase().includes(keyword)) ||
  320. (item.NAME && item.NAME.toLowerCase().includes(keyword));
  321. }
  322. });
  323. }
  324. csList.value = filteredItems;
  325. currentPage.value = 1; // 검색 시 첫 페이지로 이동
  326. };
  327. // 검색 초기화
  328. const resetSearch = () => {
  329. searchModel.value = '';
  330. filter.value = '';
  331. csList.value = [...allCsList.value];
  332. currentPage.value = 1;
  333. };
  334. const goToDeliveryDetail = (item) => {
  335. // 제품 정보를 스토어에 저장
  336. useDtStore.boardInfo.seq = item.SEQ;
  337. useDtStore.boardInfo.pageType = "D";
  338. // 배송 관리 페이지로 이동
  339. router.push({
  340. path: "/view/common/deli/detail",
  341. query: {
  342. itemId: item.SEQ,
  343. itemName: item.NAME,
  344. price1: item.PRICE1,
  345. price2: item.PRICE2 || item.PRICE1,
  346. thumbFile: item.THUMB_FILE || ''
  347. }
  348. });
  349. };
  350. const getStatusClass = (status) => {
  351. switch(status) {
  352. case '0':
  353. return 'status-waiting';
  354. case '1':
  355. return 'status-completed';
  356. }
  357. };
  358. const formatDate = (dateStr) => {
  359. if (!dateStr) return '';
  360. const date = new Date(dateStr);
  361. const year = date.getFullYear();
  362. const month = String(date.getMonth() + 1).padStart(2, '0');
  363. const day = String(date.getDate()).padStart(2, '0');
  364. return `${year}.${month}.${day}`;
  365. };
  366. // 팝업 관련 함수들
  367. const closeModal = () => {
  368. showInquiryModal.value = false;
  369. resetForm();
  370. };
  371. const resetForm = () => {
  372. inquiryForm.value = {
  373. category: '',
  374. title: '',
  375. content: '',
  376. attachments: []
  377. };
  378. };
  379. const submitInquiry = async () => {
  380. // 폼 유효성 검사
  381. if (!inquiryForm.value.category) {
  382. $toast.error('문의 유형을 선택해주세요.');
  383. return;
  384. }
  385. if (!inquiryForm.value.title) {
  386. $toast.error('제목을 입력해주세요.');
  387. return;
  388. }
  389. if (!inquiryForm.value.content) {
  390. $toast.error('내용을 입력해주세요.');
  391. return;
  392. }
  393. isSubmitting.value = true;
  394. try {
  395. const formData = new FormData();
  396. formData.append('USER_SEQ', useAtStore.auth.seq);
  397. formData.append('CATEGORY', inquiryForm.value.category);
  398. formData.append('TITLE', inquiryForm.value.title);
  399. formData.append('CONTENT', inquiryForm.value.content);
  400. // 첨부파일 처리
  401. if (inquiryForm.value.attachments && inquiryForm.value.attachments.length > 0) {
  402. inquiryForm.value.attachments.forEach((file, index) => {
  403. formData.append(`files[${index}]`, file);
  404. });
  405. }
  406. await useAxios()
  407. .post("/cs/reg", formData, {
  408. headers: {
  409. 'Content-Type': 'multipart/form-data'
  410. }
  411. })
  412. .then((res) => {
  413. if (res.data.success) {
  414. $toast.success('문의가 성공적으로 등록되었습니다.');
  415. closeModal();
  416. csListGet(); // 목록 새로고침
  417. } else {
  418. $toast.error('문의 등록에 실패했습니다.');
  419. }
  420. });
  421. } catch (error) {
  422. $toast.error('문의 등록 중 오류가 발생했습니다.');
  423. console.error('Error submitting inquiry:', error);
  424. } finally {
  425. isSubmitting.value = false;
  426. }
  427. };
  428. /************************************************************************
  429. | WATCH
  430. ************************************************************************/
  431. onMounted(() => {
  432. csListGet();
  433. // 날짜 초기화
  434. const today = dayjs();
  435. searchEndDate.value = today.format('YYYY-MM-DD');
  436. });
  437. </script>