detail.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>고객센터</span>
  8. <span>{{ pageId }}</span>
  9. </div>
  10. </div>
  11. <div class="data--list--wrap">
  12. <div class="item--list--wrap">
  13. <!-- 문의 상세 카드 -->
  14. <div class="cs--detail--wrap">
  15. <div class="cs--detail--header">
  16. <div class="status-badge" :class="getStatusClass(csDetail.STATUS)">
  17. {{ csDetail.STATUS == '0' ? '답변대기' : '답변완료' }}
  18. </div>
  19. <div class="inquiry-meta">
  20. <span class="category-badge">{{ getCategoryName(csDetail.CATEGORY) }}</span>
  21. <span class="date">{{ formatDate(csDetail.REGDATE) }}</span>
  22. </div>
  23. </div>
  24. <div class="cs--detail--content">
  25. <h3 class="inquiry-title">{{ csDetail.TITLE }}</h3>
  26. <div class="inquiry-info">
  27. <div class="info-item">
  28. <span class="label">작성자</span>
  29. <span class="value">{{ csDetail.NICK_NAME || csDetail.COMPANY_NAME || '알 수 없음' }}</span>
  30. </div>
  31. <div class="info-item">
  32. <span class="label">등록일</span>
  33. <span class="value">{{ formatDetailDate(csDetail.REGDATE) }}</span>
  34. </div>
  35. </div>
  36. <div class="inquiry-content">
  37. <h4>문의 내용</h4>
  38. <div class="content-text" v-html="formatContent(csDetail.CONTENT)"></div>
  39. </div>
  40. <!-- 첨부파일 -->
  41. <div v-if="csDetail.FILE_NAME" class="attachment-section">
  42. <h4>첨부파일</h4>
  43. <div class="attachment-item">
  44. <i class="mdi mdi-paperclip"></i>
  45. <span class="file-name">{{ csDetail.FILE_NAME_ORIGIN }}</span>
  46. <v-btn class="download-btn" size="small" @click="downloadFile">
  47. <i class="mdi mdi-download"></i>
  48. 다운로드
  49. </v-btn>
  50. </div>
  51. </div>
  52. <!-- 답변 섹션 -->
  53. <div v-if="csDetail.STATUS == '1'" class="answer-section">
  54. <h4>답변</h4>
  55. <div class="answer-content">
  56. <div class="answer-meta">
  57. <span class="admin-badge">관리자</span>
  58. <span class="answer-date">{{ formatDetailDate(csDetail.ANSWER_REGDATE) }}</span>
  59. </div>
  60. <div class="answer-text" v-html="formatContent(csDetail.ADMIN_ANSWER)"></div>
  61. </div>
  62. </div>
  63. <!-- 답변 작성 섹션 (관리자만) -->
  64. <div class="answer-write-section" v-if="useAtStore.auth.seq === '2' && csDetail.STATUS == '0'">
  65. <h4>답변 작성</h4>
  66. <div class="answer-write-form">
  67. <v-textarea
  68. v-model="answerContent"
  69. placeholder="답변 내용을 입력하세요."
  70. rows="4"
  71. variant="outlined"
  72. hide-details
  73. ></v-textarea>
  74. <div class="answer-actions">
  75. <v-btn
  76. class="submit-answer-btn"
  77. color="primary"
  78. @click="submitAnswer"
  79. :loading="isSubmitting"
  80. >
  81. 답변 등록
  82. </v-btn>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. <!-- 하단 버튼 -->
  90. <div class="view-btm-btn">
  91. <div class="btn-l">
  92. <v-btn class="custom-btn btn-list" @click="listLocated">
  93. <i class="mdi mdi-format-list-bulleted"></i>목록
  94. </v-btn>
  95. </div>
  96. <div class="btn-r">
  97. <!-- 필요시 수정/삭제 버튼 추가 -->
  98. </div>
  99. </div>
  100. </div>
  101. </div>
  102. </template>
  103. <script setup>
  104. import useAxios from "@/composables/useAxios";
  105. import dayjs from 'dayjs';
  106. /************************************************************************
  107. | 레이아웃
  108. ************************************************************************/
  109. definePageMeta({
  110. layout: "default",
  111. });
  112. /************************************************************************
  113. | 스토어
  114. ************************************************************************/
  115. const useDtStore = useDetailStore();
  116. const useAtStore = useAuthStore();
  117. /************************************************************************
  118. | 전역
  119. ************************************************************************/
  120. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  121. const router = useRouter();
  122. const pageId = ref("문의 상세");
  123. // CS 상세 정보
  124. const csDetail = ref({
  125. SEQ: '',
  126. USER_SEQ: '',
  127. CATEGORY: '',
  128. TITLE: '',
  129. CONTENT: '',
  130. STATUS: '',
  131. REGDATE: '',
  132. FILE_NAME: '',
  133. FILE_NAME_ORIGIN: '',
  134. ANSWER: '',
  135. ANSWER_DATE: '',
  136. NICK_NAME: '',
  137. COMPANY_NAME: ''
  138. });
  139. // 답변 작성 관련
  140. const answerContent = ref('');
  141. const isSubmitting = ref(false);
  142. const isAdmin = computed(() => {
  143. // 관리자 권한 체크 (실제 권한 체크 로직에 맞게 수정 필요)
  144. return useAtStore.auth.seq === '2';
  145. });
  146. /************************************************************************
  147. | 함수(METHODS)
  148. ************************************************************************/
  149. const listLocated = () => {
  150. router.push({
  151. path: "/view/common/cs",
  152. });
  153. };
  154. const getCategoryName = (category) => {
  155. switch(category) {
  156. case 'D': return '기능문의';
  157. case 'E': return '기타문의';
  158. }
  159. };
  160. const getStatusClass = (status) => {
  161. switch(status) {
  162. case '0': return 'status-waiting';
  163. case '1': return 'status-completed';
  164. default: return '';
  165. }
  166. };
  167. const formatDate = (dateStr) => {
  168. if (!dateStr) return '';
  169. const date = new Date(dateStr);
  170. const year = date.getFullYear();
  171. const month = String(date.getMonth() + 1).padStart(2, '0');
  172. const day = String(date.getDate()).padStart(2, '0');
  173. return `${year}.${month}.${day}`;
  174. };
  175. const formatDetailDate = (dateStr) => {
  176. if (!dateStr) return '';
  177. return dayjs(dateStr).format('YYYY년 MM월 DD일 HH:mm');
  178. };
  179. const formatContent = (content) => {
  180. if (!content) return '';
  181. return content.replace(/\n/g, '<br>');
  182. };
  183. const downloadFile = () => {
  184. if (csDetail.value.FILE_NAME) {
  185. window.open(`https://shopdeli.mycafe24.com/cs/download/${csDetail.value.FILE_NAME}`, '_blank');
  186. }
  187. };
  188. const fnDetail = () => {
  189. let req = {
  190. seq: useDtStore.boardInfo.seq,
  191. };
  192. useAxios()
  193. .get(`/cs/detail/${req.seq}`)
  194. .then((res) => {
  195. csDetail.value = res.data;
  196. })
  197. .catch((error) => {
  198. console.error('CS 상세 조회 실패:', error);
  199. $toast.error('문의 상세 정보를 불러올 수 없습니다.');
  200. });
  201. };
  202. const submitAnswer = async () => {
  203. if (!answerContent.value.trim()) {
  204. $toast.error('답변 내용을 입력해주세요.');
  205. return;
  206. }
  207. isSubmitting.value = true;
  208. try {
  209. const response = await useAxios().post('/cs/answer', {
  210. CS_SEQ: csDetail.value.SEQ,
  211. ANSWER: answerContent.value,
  212. ADMIN_SEQ: useAtStore.auth.seq,
  213. });
  214. if (response.data.status === 'success') {
  215. $toast.success('답변이 등록되었습니다.');
  216. answerContent.value = '';
  217. // 상세 정보 다시 불러오기
  218. fnDetail();
  219. }
  220. } catch (error) {
  221. console.error('답변 등록 실패:', error);
  222. $toast.error('답변 등록에 실패했습니다.');
  223. } finally {
  224. isSubmitting.value = false;
  225. }
  226. };
  227. /************************************************************************
  228. | 라이프사이클
  229. ************************************************************************/
  230. onMounted(() => {
  231. fnDetail();
  232. });
  233. </script>