curationAdd.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  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="view-wrap mt--45">
  12. <div class="view-box">
  13. <div class="view-box-top">
  14. <h3 v-if="pageType == 'I'">CURATION 등록</h3>
  15. <h3 v-else>CURATION 수정</h3>
  16. </div>
  17. <div class="view-box-btm">
  18. <div class="form-style1">
  19. <v-form ref="addForm">
  20. <table>
  21. <colgroup>
  22. <col style="width: 12.5rem" />
  23. <col />
  24. </colgroup>
  25. <tbody>
  26. <tr>
  27. <th>노출여부<span class="bul">*</span></th>
  28. <td>
  29. <v-radio-group
  30. class="radio--group"
  31. v-model="form.formValue2"
  32. inline
  33. hide-details
  34. >
  35. <v-radio label="노출" value="Y"></v-radio>
  36. <v-radio label="비노출" value="N"></v-radio>
  37. </v-radio-group>
  38. </td>
  39. </tr>
  40. <tr>
  41. <th>구분<span class="bul">*</span></th>
  42. <td>
  43. <v-radio-group
  44. class="radio--group"
  45. v-model="form.formValue9"
  46. inline
  47. hide-details
  48. :rules="[useValid.required('구분')]"
  49. >
  50. <v-radio label="유튜브" value="Y"></v-radio>
  51. <v-radio label="링크드인" value="L"></v-radio>
  52. <v-radio label="네이버" value="N"></v-radio>
  53. </v-radio-group>
  54. </td>
  55. </tr>
  56. <tr>
  57. <th>제목<span class="bul">*</span></th>
  58. <td>
  59. <v-text-field
  60. v-model="form.formValue1"
  61. class="custom-input mini"
  62. placeholder="제목을 입력해주세요."
  63. :rules="[useValid.required('제목')]"
  64. ></v-text-field>
  65. </td>
  66. </tr>
  67. <tr>
  68. <th>내용<span class="bul">*</span></th>
  69. <td class="">
  70. <SunEditorWrapper
  71. ref="sunEditorWrapper"
  72. :initialContent="editorContentReq"
  73. />
  74. </td>
  75. </tr>
  76. <tr>
  77. <th>썸네일 이미지<span class="bul">*</span></th>
  78. <td>
  79. <div class="equip--image--wrap">
  80. <!--이미지가 없을 때-->
  81. <div class="equip--image" v-show="!form.formValue7">
  82. <img src="/assets/img/ic_no_img.svg" />
  83. </div>
  84. <!--이미지 첨부했을 때-->
  85. <div class="equip--image" v-show="form.formValue7">
  86. <CoolLightBox
  87. v-if="items.length > 0"
  88. :items="items"
  89. :index="index"
  90. @close="index = null"
  91. />
  92. <div class="images-wrapper">
  93. <div
  94. class="image"
  95. :key="imageIndex"
  96. @click="index = imageIndex"
  97. >
  98. <img id="preview_image" :src="imgTemp" />
  99. </div>
  100. </div>
  101. </div>
  102. <div class="equip--image--select">
  103. <div class="form--group">
  104. <label
  105. for="fileUpload_pic"
  106. class="file--btn"
  107. @click="fnPicFileUploadOpen()"
  108. >파일 선택</label
  109. >
  110. <v-file-input
  111. v-model="form.formValue7"
  112. id="fileUpload_pic"
  113. ref="fileupload_pic"
  114. accept=".jpg, .jpeg, .png, .gif"
  115. variant="plain"
  116. hide-details
  117. placeholder="선택된 파일 없음"
  118. prepend-icon=""
  119. class="custom-input"
  120. style="max-width: 400px"
  121. height="33px"
  122. :rules="[useValid.required('썸네일 이미지')]"
  123. :clearable="false"
  124. @change="fnUploadPicFileCheck()"
  125. >
  126. <template #append>
  127. <div class="v-input__icon v-input__icon--clear">
  128. <button
  129. @click="clearFile"
  130. type="button"
  131. aria-label="clear icon"
  132. tabindex="-1"
  133. class="v-icon notranslate v-icon--link mdi mdi-close"
  134. ></button>
  135. </div>
  136. </template>
  137. </v-file-input>
  138. </div>
  139. <p class="equip--image--desc">
  140. (권장 이미지 : 1024 x 768 / gif, jpg, jpeg, png)
  141. </p>
  142. </div>
  143. <div class="div_error_text">{{ objProc.validErrorMessage }}</div>
  144. </div>
  145. </td>
  146. </tr>
  147. <tr>
  148. <th>URL<span class="bul">*</span></th>
  149. <td>
  150. <v-text-field
  151. v-model="form.formValue5"
  152. class="custom-input mini"
  153. placeholder="URL 링크를 입력해주세요"
  154. :rules="[useValid.required('URL')]"
  155. ></v-text-field>
  156. </td>
  157. </tr>
  158. <tr>
  159. <th>팔로워<span class="bul">*</span></th>
  160. <td>
  161. <v-text-field
  162. v-model="form.formValue8"
  163. class="custom-input mini"
  164. placeholder="팔로워 및 구독자수를 입력해주세요"
  165. :rules="[useValid.required('팔로워')]"
  166. ></v-text-field>
  167. </td>
  168. </tr>
  169. </tbody>
  170. </table>
  171. </v-form>
  172. </div>
  173. </div>
  174. </div>
  175. <div class="view-btm-btn">
  176. <div class="btn-l">
  177. <v-btn class="custom-btn btn-list" @click="listLocated"
  178. ><i class="ico"></i>목록</v-btn
  179. >
  180. <v-btn v-if="pageType == 'U'" class="custom-btn btn-del" @click="fnDelEvt"
  181. ><i class="ico"></i>삭제</v-btn
  182. >
  183. </div>
  184. <div class="btn-r">
  185. <v-btn v-if="pageType == 'I'" class="custom-btn btn-blue2" @click="fnRegCheck"
  186. ><i class="ico"></i>저장</v-btn
  187. >
  188. <v-btn v-else class="custom-btn btn-blue2" @click="fnRegCheck"
  189. ><i class="ico"></i>수정</v-btn
  190. >
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. </template>
  196. <script setup>
  197. import useAxios from "@/composables/useAxios";
  198. import useUtil from "@/composables/useUtil";
  199. import useErrorHandler from "@/composables/useErrorHandler";
  200. import SunEditorWrapper from "@/components/sunEdt.vue";
  201. /************************************************************************
  202. | 레이아웃
  203. ************************************************************************/
  204. definePageMeta({
  205. layout: "default",
  206. });
  207. /************************************************************************
  208. | 스토어
  209. ************************************************************************/
  210. const useDtStore = useDetailStore();
  211. /************************************************************************
  212. | 전역
  213. ************************************************************************/
  214. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  215. const router = useRouter();
  216. const pageId = ref("CURATION");
  217. const sunEditorWrapper = ref(null); //에디터용 전역
  218. const updatedContent = ref(null); //에디터용 전역
  219. const editorContentReq = ref(""); //에디터용 전역
  220. const addForm = ref(null);
  221. const index = ref(null);
  222. const imageIndex = ref(0);
  223. const items = ref([]);
  224. const quillEditor = ref(null);
  225. const customToolbar = ref([["image"]]); // 에디터에서 이미지 첨부만 가능하게끔
  226. const imgTemp = ref(null);
  227. const rowId = ref();
  228. const form = ref({
  229. formValue0: "KR",
  230. formValue1: "",
  231. formValue2: "Y",
  232. // formValue3: "",
  233. formValue4: "",
  234. formValue5: "",
  235. formValue6: "",
  236. formValue7: null,
  237. formValue8: "",
  238. formValue9: "",
  239. // fileResponse: null,
  240. });
  241. const fileUpload = ref(null);
  242. const uploadFiles = ref([
  243. {
  244. file_name: "",
  245. ogn_name: "",
  246. },
  247. ]);
  248. const uploadPicFiles = ref([
  249. {
  250. file_name: "",
  251. ogn_name: "-",
  252. },
  253. ]);
  254. const pageType = ref("");
  255. const apiUrl = ref("");
  256. apiUrl.value = import.meta.env.VITE_APP_API_URL;
  257. const objProc = ref({
  258. validErrorMessage: "",
  259. });
  260. /************************************************************************
  261. | 함수(METHODS)
  262. ************************************************************************/
  263. const listLocated = () => {
  264. router.push({
  265. path: "/view/media/curationList",
  266. });
  267. };
  268. /*======================================================================
  269. | 작성 시퀀스
  270. | 1. 작성 컨펌
  271. | 2. 버튼 체크
  272. | 3. 등록시 -> 등록 API 호출
  273. ======================================================================*/
  274. const fnRegCheck = async () => {
  275. //BASE64에서 실제 파일서버에 파일 전성후 내용 컨텐츠 REAL주소로 변경
  276. await editorContent();
  277. nextTick(() => {
  278. if (addForm.value && typeof addForm.value.validate === "function") {
  279. addForm.value
  280. .validate()
  281. .then((isValid) => {
  282. if (
  283. isValid.valid &&
  284. form.value.formValue7 != null &&
  285. updatedContent.value != undefined &&
  286. updatedContent.value != "<p><br></p>" &&
  287. updatedContent.value != null
  288. ) {
  289. if (pageType.value == "I") fnRegEvt();
  290. else fnUpdEvt();
  291. } else {
  292. let param = {
  293. id: pageId,
  294. title: "공고사항",
  295. content: "필수항목을 입력해주세요.",
  296. yes: {
  297. text: "확인",
  298. isProc: false,
  299. },
  300. no: {
  301. text: "취소",
  302. isProc: false,
  303. },
  304. };
  305. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  306. }
  307. })
  308. .catch((err) => {
  309. console.error("벨리데이션 에러", err);
  310. });
  311. } else {
  312. console.error("항목 누락체크[fnRegCheck]]");
  313. }
  314. });
  315. };
  316. const fnRegEvt = () => {
  317. let param = {
  318. id: pageId,
  319. title: "큐레이션",
  320. content: "등록하시겠습니까?",
  321. yes: {
  322. text: "등록",
  323. isProc: true,
  324. event: "FN_INSERT",
  325. param: "",
  326. },
  327. no: {
  328. text: "취소",
  329. isProc: false,
  330. },
  331. };
  332. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  333. };
  334. const fnInsert = () => {
  335. let frm = new FormData();
  336. let wterGet = localStorage.getItem("tempAccess");
  337. let params = JSON.stringify({
  338. seq: "",
  339. url_link: form.value.formValue5,
  340. fol_cnt: Number(form.value.formValue8),
  341. wter: wterGet,
  342. brd_cd: "BR03",
  343. brd_lang: "KR",
  344. title: form.value.formValue1,
  345. show_yn: form.value.formValue2,
  346. content: updatedContent.value,
  347. ul_type: form.value.formValue9,
  348. fix_yn: "N",
  349. });
  350. frm.append("params", params);
  351. frm.append("picObj", form.value.formValue7);
  352. useAxios()
  353. .post("/brd/ins", frm, { headers: { "Content-Type": "multipart/form-data" } })
  354. .then((res) => {
  355. router.push("/view/media/curationList");
  356. })
  357. .catch((error) => {
  358. //$log.debug("[equipMgmtReg][fnGetTenantList][error]");
  359. //useErrorHandler().fnSetCommErrorHandle(error, fnGetTenantList);
  360. })
  361. .finally(() => {
  362. //$log.debug("[equipMgmtReg][fnGetTenantList][finished]");
  363. //objSlt.value.tenantNameList = _cloneDeep(temp);
  364. });
  365. };
  366. const fnUpdEvt = () => {
  367. let param = {
  368. id: pageId,
  369. title: "큐레이션",
  370. content: "수정하시겠습니까?",
  371. yes: {
  372. text: "확인",
  373. isProc: true,
  374. event: "FN_UPDATE",
  375. param: "",
  376. },
  377. no: {
  378. text: "취소",
  379. isProc: false,
  380. },
  381. };
  382. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  383. };
  384. const fnUpdate = () => {
  385. let frm = new FormData();
  386. let wterGet = localStorage.getItem("tempAccess");
  387. let params = JSON.stringify({
  388. seq: useDtStore.boardInfo.seq,
  389. url_link: form.value.formValue5,
  390. fol_cnt: Number(form.value.formValue8),
  391. wter: wterGet,
  392. brd_cd: "BR03",
  393. brd_lang: "KR",
  394. title: form.value.formValue1,
  395. show_yn: form.value.formValue2,
  396. content: updatedContent.value,
  397. ul_type: form.value.formValue9,
  398. fix_yn: "N",
  399. });
  400. frm.append("params", params);
  401. frm.append("picObj", form.value.formValue7);
  402. useAxios()
  403. .post("/brd/upd", frm, { headers: { "Content-Type": "multipart/form-data" } })
  404. .then((res) => {
  405. router.push("/view/media/curationList");
  406. })
  407. .catch((error) => {
  408. $log.debug("[equipMgmtReg][fnGetTenantList][error]");
  409. //useErrorHandler().fnSetCommErrorHandle(error, fnGetTenantList);
  410. })
  411. .finally(() => {
  412. //$log.debug("[equipMgmtReg][fnGetTenantList][finished]");
  413. //objSlt.value.tenantNameList = _cloneDeep(temp);
  414. });
  415. };
  416. const fnDelEvt = () => {
  417. let param = {
  418. id: pageId,
  419. title: "큐레이션",
  420. content: "삭제하시겠습니까?",
  421. yes: {
  422. text: "확인",
  423. isProc: true,
  424. event: "FN_DELETE",
  425. param: "",
  426. },
  427. no: {
  428. text: "취소",
  429. isProc: false,
  430. },
  431. };
  432. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  433. };
  434. const fnDelete = () => {
  435. let wterGet = localStorage.getItem("tempAccess");
  436. let req = {
  437. brd_cd: "BR03",
  438. seq: useDtStore.boardInfo.seq,
  439. wter: wterGet,
  440. };
  441. useAxios()
  442. .post("/brd/del", req)
  443. .then((res) => {
  444. router.push("/view/media/curationList");
  445. })
  446. .catch((error) => {
  447. //$log.debug("[equipMgmtReg][fnGetTenantList][error]");
  448. //useErrorHandler().fnSetCommErrorHandle(error, fnGetTenantList);
  449. })
  450. .finally(() => {
  451. //$log.debug("[equipMgmtReg][fnGetTenantList][finished]");
  452. //objSlt.value.tenantNameList = _cloneDeep(temp);
  453. });
  454. };
  455. const fnDetail = () => {
  456. let req = {
  457. seq: useDtStore.boardInfo.seq,
  458. };
  459. useAxios()
  460. .post("/brd/detail", req)
  461. .then((res) => {
  462. form.value.formValue0 = res.data.brd_lang;
  463. form.value.formValue1 = res.data.title;
  464. form.value.formValue2 = res.data.show_yn;
  465. //에디터에 컨텐츠 전달
  466. editorContentReq.value = res.data.content;
  467. form.value.formValue5 = res.data.url_link;
  468. form.value.formValue8 = res.data.fol_cnt;
  469. form.value.formValue9 = res.data.ul_type;
  470. form.value.formValue7 = res.data.file_title_pic;
  471. uploadPicFiles.value[0].file_name = res.data.file_title_pic;
  472. uploadPicFiles.value[0].ogn_name = res.data.ogn_f_title_pic;
  473. imgTemp.value =
  474. apiUrl.value +
  475. "/images/" +
  476. res.data.path.replace(/.*\/files\//, "") +
  477. "/" +
  478. res.data.file_title_pic;
  479. })
  480. .catch((error) => {
  481. //$log.debug("[equipMgmtReg][fnGetTenantList][error]");
  482. //useErrorHandler().fnSetCommErrorHandle(error, fnGetTenantList);
  483. })
  484. .finally(() => {
  485. //$log.debug("[equipMgmtReg][fnGetTenantList][finished]");
  486. //objSlt.value.tenantNameList = _cloneDeep(temp);
  487. });
  488. };
  489. const fnPicFileUploadOpen = () => {
  490. let fileUpload = document.getElementById("fileupload_pic");
  491. if (fileUpload != null) {
  492. fileUpload.click();
  493. }
  494. };
  495. const fnUploadPicFileCheck = () => {
  496. if (form.value.formValue7) {
  497. // 10Mb 이상은 업로드 불가
  498. if (form.value.formValue7.size > 10 * 1024 * 1024) {
  499. fnOpenCommPop("10mb 이상은 업로드가 불가합니다.");
  500. form.value.formValue7 = null;
  501. return;
  502. }
  503. // 이미지 파일 형식 체크
  504. let extension = form.value.formValue7.name.split(".").pop().toLowerCase();
  505. if (
  506. extension != "jpg" &&
  507. extension != "jpeg" &&
  508. extension != "png" &&
  509. extension != "gif"
  510. ) {
  511. fnOpenCommPop("파일 형식 또는 확장자가 올바르지 않습니다.");
  512. form.value.formValue7 = null;
  513. return;
  514. }
  515. objProc.validErrorMessage = "";
  516. // 이미지 미리보기
  517. let previewImage = new Image();
  518. let tempImageUrl = window.URL.createObjectURL(form.value.formValue7);
  519. previewImage.src = tempImageUrl;
  520. items.value[0] = tempImageUrl;
  521. imgTemp.value = tempImageUrl;
  522. }
  523. };
  524. const clearFile = () => {
  525. form.value.formValue7 = null;
  526. };
  527. const fnOpenCommPop = (__TEXT) => {
  528. let param = {
  529. id: pageId,
  530. title: "알림",
  531. content: __TEXT,
  532. yes: {
  533. text: "확인",
  534. isProc: false,
  535. },
  536. no: {
  537. text: "취소",
  538. isProc: false,
  539. },
  540. };
  541. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  542. };
  543. /*=======================================================================
  544. | 최종 에디터 이미지 url치환 : S
  545. /*=======================================================================*/
  546. const editorContent = async () => {
  547. const content = sunEditorWrapper.value.getEditorContent();
  548. updatedContent.value = await processEditorContent(content);
  549. console.log("Updated content:", updatedContent.value);
  550. };
  551. // Base64 데이터를 Blob으로 변환
  552. const base64ToBlob = (base64, mimeType) => {
  553. const byteString = atob(base64.split(",")[1]);
  554. const arrayBuffer = new ArrayBuffer(byteString.length);
  555. const uint8Array = new Uint8Array(arrayBuffer);
  556. for (let i = 0; i < byteString.length; i++) {
  557. uint8Array[i] = byteString.charCodeAt(i);
  558. }
  559. return new Blob([uint8Array], { type: mimeType });
  560. };
  561. // Base64 데이터를 File 객체로 변환
  562. const base64ToFile = (base64, mimeType, fileName) => {
  563. const blob = base64ToBlob(base64, mimeType);
  564. return new File([blob], fileName, { type: mimeType });
  565. };
  566. // 이미지 업로드 처리 (useAxios)
  567. const uploadImage = async (file) => {
  568. const formDataEdt = new FormData();
  569. formDataEdt.append("picObj", file);
  570. return useAxios()
  571. .post("/pic/upload", formDataEdt, {
  572. headers: { "Content-Type": "multipart/form-data" },
  573. })
  574. .then((res) => {
  575. const filePath = res.data.ogn_name.path.replace(/.*\/files\//, "");
  576. const fileName = res.data.ogn_name.file_name;
  577. return `${apiUrl.value}/images/${filePath}/${fileName}`; // 최종 URL 반환
  578. })
  579. .catch((error) => {
  580. console.error("Image upload failed:", error);
  581. return null;
  582. });
  583. };
  584. // 에디터 내용 처리 및 이미지 업로드
  585. const processEditorContent = async (content) => {
  586. const parser = new DOMParser();
  587. const doc = parser.parseFromString(content, "text/html");
  588. const images = doc.querySelectorAll("img");
  589. for (let i = 0; i < images.length; i++) {
  590. const img = images[i];
  591. const src = img.src;
  592. if (src.startsWith("data:image")) {
  593. // MIME 타입과 파일 이름 추출
  594. const mimeType = src.split(";")[0].split(":")[1];
  595. const extension = mimeType.split("/")[1];
  596. const fileName = `image-${i + 1}.${extension}`;
  597. // Base64 데이터를 File 객체로 변환
  598. const file = base64ToFile(src, mimeType, fileName);
  599. // 이미지 업로드 및 URL 반환
  600. const finalUrl = await uploadImage(file);
  601. if (finalUrl) {
  602. img.src = finalUrl; // 이미지 src 업데이트
  603. }
  604. }
  605. }
  606. return doc.body.innerHTML; // 최종 수정된 HTML 반환
  607. };
  608. /*=======================================================================
  609. | 최종 에디터 이미지 url치환 : E
  610. /*=======================================================================*/
  611. /************************************************************************
  612. | 팝업 이벤트버스 정의
  613. ************************************************************************/
  614. $eventBus.off("FN_INSERT");
  615. $eventBus.on("FN_INSERT", () => {
  616. fnInsert();
  617. });
  618. $eventBus.off("FN_DELETE");
  619. $eventBus.on("FN_DELETE", () => {
  620. fnDelete();
  621. });
  622. $eventBus.off("FN_UPDATE");
  623. $eventBus.on("FN_UPDATE", () => {
  624. fnUpdate();
  625. });
  626. $eventBus.off("FN_CONFIRM");
  627. $eventBus.on("FN_CONFIRM");
  628. /************************************************************************
  629. | 라이프사이클
  630. ************************************************************************/
  631. onMounted(() => {
  632. pageType.value = useDtStore.boardInfo.pageType;
  633. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  634. if (pageType.value == "U") {
  635. fnDetail();
  636. }
  637. });
  638. /************************************************************************
  639. | WATCH
  640. ************************************************************************/
  641. </script>