add.vue 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262
  1. <template>
  2. <div class="modern-item-add">
  3. <!-- 헤더 섹션 -->
  4. <!-- <div class="page-header">
  5. <div class="header-content">
  6. <div class="title-section">
  7. <h1 class="page-title">{{ pageId }}</h1>
  8. </div>
  9. <div class="breadcrumb-section">
  10. <div class="breadcrumb">
  11. <span class="breadcrumb-item">홈</span>
  12. <i class="mdi mdi-chevron-right breadcrumb-divider"></i>
  13. <span class="breadcrumb-item">공동구매</span>
  14. <i class="mdi mdi-chevron-right breadcrumb-divider"></i>
  15. <span class="breadcrumb-item active">{{ pageId }}</span>
  16. </div>
  17. </div>
  18. </div>
  19. </div> -->
  20. <!-- 메인 컨텐츠 -->
  21. <div class="main-content">
  22. <div class="form-container">
  23. <v-form ref="addForm" class="modern-form">
  24. <div class="form-section">
  25. <h3 class="section-title">
  26. <i class="mdi mdi-package-variant"></i>
  27. {{ pageId }}
  28. </h3>
  29. <!-- 제품명 -->
  30. <div class="form-field">
  31. <label class="field-label">
  32. 제품명
  33. <span v-if="pageType !== 'D'" class="required-mark">*</span>
  34. </label>
  35. <div class="field-content">
  36. <div v-if="pageType == 'D'" class="display-value">
  37. {{ form.formValue1 }}
  38. </div>
  39. <v-text-field
  40. v-else
  41. v-model="form.formValue1"
  42. :rules="[useValid.required('제품명')]"
  43. class="modern-input"
  44. variant="outlined"
  45. placeholder="제품명을 입력하세요"
  46. maxlength="50"
  47. hide-details="auto"
  48. ></v-text-field>
  49. </div>
  50. </div>
  51. <!-- 기간 (공동구매인 경우) -->
  52. <div v-if="itemType == 'G'" class="form-field">
  53. <label class="field-label">
  54. 판매 기간
  55. <span v-if="pageType !== 'D'" class="required-mark">*</span>
  56. </label>
  57. <div class="field-content">
  58. <div v-if="pageType == 'D'" class="display-value date-range">
  59. <i class="mdi mdi-calendar-range"></i>
  60. {{ form.order_start_date?.slice(0, 10) }} ~ {{ form.order_end_date?.slice(0, 10) }}
  61. </div>
  62. <div v-else class="date-picker-wrapper">
  63. <div class="date-picker-group">
  64. <div class="date-picker-item">
  65. <VueDatePicker
  66. v-model="form.order_start_date"
  67. :format="datePickerFormat"
  68. placeholder="시작 날짜"
  69. :auto-apply="true"
  70. week-start="0"
  71. class="modern-date-picker"
  72. />
  73. </div>
  74. <span class="date-separator">~</span>
  75. <div class="date-picker-item">
  76. <VueDatePicker
  77. v-model="form.order_end_date"
  78. :format="datePickerFormat"
  79. placeholder="종료 날짜"
  80. :auto-apply="true"
  81. week-start="0"
  82. :min-date="form.order_start_date"
  83. class="modern-date-picker"
  84. />
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. <!-- 인플루언서 (공동구매인 경우) -->
  91. <div class="form-field-group group--2">
  92. <div v-if="itemType == 'G' && pageType !== 'D'" class="form-field">
  93. <label class="field-label">인플루언서</label>
  94. <div class="field-content">
  95. <div class="selector-wrapper">
  96. <v-text-field
  97. v-model="form.contact_inf_display"
  98. class="modern-input selector-input"
  99. variant="outlined"
  100. placeholder="인플루언서를 선택하세요"
  101. readonly
  102. hide-details="auto"
  103. ></v-text-field>
  104. <v-btn
  105. class="selector-btn"
  106. color="primary"
  107. variant="outlined"
  108. @click="openInfluencerModal"
  109. >
  110. <i class="mdi mdi-account-search"></i>
  111. 선택
  112. </v-btn>
  113. <v-btn
  114. v-if="form.contact_inf_display"
  115. class="selector-btn delete-btn"
  116. color="error"
  117. variant="outlined"
  118. @click="clearInfluencer"
  119. >
  120. <i class="mdi mdi-close"></i>
  121. 삭제
  122. </v-btn>
  123. </div>
  124. </div>
  125. </div>
  126. <!-- 브랜드사 (공동구매인 경우) -->
  127. <div v-if="itemType == 'G' && pageType !== 'D' && memberType !== 'BRAND'" class="form-field">
  128. <label class="field-label">브랜드사</label>
  129. <div class="field-content">
  130. <div class="selector-wrapper">
  131. <v-text-field
  132. v-model="form.contact_brd_display"
  133. class="modern-input selector-input"
  134. variant="outlined"
  135. placeholder="브랜드사를 선택하세요"
  136. readonly
  137. hide-details="auto"
  138. ></v-text-field>
  139. <v-btn
  140. class="selector-btn"
  141. color="primary"
  142. variant="outlined"
  143. @click="openBrandModal"
  144. >
  145. <i class="mdi mdi-domain"></i>
  146. 선택
  147. </v-btn>
  148. <v-btn
  149. v-if="form.contact_brd_display"
  150. class="selector-btn delete-btn"
  151. color="error"
  152. variant="outlined"
  153. @click="clearBrand"
  154. >
  155. <i class="mdi mdi-close"></i>
  156. 삭제
  157. </v-btn>
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. <!-- 구매 링크 (공동구매인 경우) -->
  163. <div v-if="itemType == 'G'" class="form-field">
  164. <label class="field-label">구매 링크</label>
  165. <div class="field-content">
  166. <div v-if="pageType == 'D'" class="display-value link-display">
  167. <a
  168. v-if="form.order_link"
  169. :href="form.order_link"
  170. target="_blank"
  171. rel="noopener noreferrer"
  172. class="external-link"
  173. >
  174. <i class="mdi mdi-link"></i>
  175. {{ form.order_link }}
  176. <i class="mdi mdi-open-in-new"></i>
  177. </a>
  178. <span v-else class="no-data">링크가 설정되지 않았습니다</span>
  179. </div>
  180. <v-text-field
  181. v-else
  182. v-model="form.order_link"
  183. class="modern-input"
  184. variant="outlined"
  185. placeholder="공동구매 링크를 입력하세요"
  186. maxlength="200"
  187. hide-details="auto"
  188. ></v-text-field>
  189. </div>
  190. </div>
  191. </div>
  192. </v-form>
  193. </div>
  194. <!-- 액션 버튼 -->
  195. <div class="action-buttons">
  196. <div class="button-group left">
  197. <v-btn
  198. v-if="pageType =='I'"
  199. class="action-btn secondary"
  200. variant="outlined"
  201. @click="listLocated"
  202. >
  203. <i class="mdi mdi-format-list-bulleted"></i>
  204. 목록으로
  205. </v-btn>
  206. <v-btn
  207. v-if="pageType =='U'"
  208. class="action-btn secondary"
  209. variant="outlined"
  210. @click="detailLocated"
  211. >
  212. <i class="mdi mdi-refresh"></i>
  213. 돌아가기
  214. </v-btn>
  215. <!-- <v-btn
  216. v-show="pageType == 'U'"
  217. class="action-btn danger"
  218. variant="outlined"
  219. color="error"
  220. @click="fnDelEvt"
  221. >
  222. <i class="mdi mdi-delete"></i>
  223. 삭제
  224. </v-btn> -->
  225. </div>
  226. <div class="button-group right">
  227. <v-btn
  228. v-if="pageType !== 'D'"
  229. class="action-btn primary"
  230. color="primary"
  231. @click="fnBtnEvt"
  232. >
  233. <i class="mdi mdi-content-save"></i>
  234. 저장하기
  235. </v-btn>
  236. </div>
  237. </div>
  238. </div>
  239. <!-- 인플루언서 선택 모달 -->
  240. <v-dialog v-model="influencerModal" max-width="600px" persistent class="modern-modal">
  241. <v-card class="modal-card influencer-modal-card">
  242. <v-card-title class="modal-header">
  243. <div class="modal-title">
  244. <i class="mdi mdi-account-star"></i>
  245. 인플루언서 선택
  246. </div>
  247. <v-btn
  248. icon
  249. variant="text"
  250. @click="closeInfluencerModal"
  251. class="close-btn"
  252. >
  253. <i class="mdi mdi-close"></i>
  254. </v-btn>
  255. </v-card-title>
  256. <v-card-text class="modal-content">
  257. <!-- 검색 영역 -->
  258. <div class="search-section">
  259. <v-text-field
  260. v-model="influencerSearchQuery"
  261. class="search-input"
  262. variant="outlined"
  263. placeholder="이름 또는 닉네임으로 검색"
  264. prepend-inner-icon="mdi-magnify"
  265. clearable
  266. hide-details
  267. @input="searchInfluencers"
  268. >
  269. <template v-slot:append-inner>
  270. <v-progress-circular
  271. v-if="isSearching"
  272. indeterminate
  273. size="20"
  274. width="2"
  275. color="primary"
  276. ></v-progress-circular>
  277. </template>
  278. </v-text-field>
  279. </div>
  280. <!-- 결과 영역 -->
  281. <div v-if="filteredInfluencerList.length === 0 && !isSearching" class="empty-state">
  282. <i class="mdi mdi-account-search-outline"></i>
  283. <h4>{{ influencerSearchQuery ? '검색 결과가 없습니다' : '등록된 인플루언서가 없습니다' }}</h4>
  284. <p>{{ influencerSearchQuery ? '다른 검색어로 시도해보세요' : '먼저 인플루언서 등록을 진행해 주세요' }}</p>
  285. </div>
  286. <!-- 인플루언서 목록 -->
  287. <div v-else class="influencer-list-container">
  288. <div
  289. v-for="influencer in filteredInfluencerList"
  290. :key="influencer.SEQ"
  291. @click="selectInfluencer(influencer)"
  292. class="influencer-list-item"
  293. :class="{ selected: selectedInfluencer?.SEQ === influencer.SEQ }"
  294. >
  295. <div class="item-avatar">
  296. <i class="mdi mdi-account-circle"></i>
  297. </div>
  298. <div class="item-info">
  299. <h4 class="nickname">{{ influencer.NICK_NAME }} <span class="name">({{ influencer.NAME }})</span></h4>
  300. <div class="sns-info" v-if="influencer.SNS_TYPE">
  301. <i class="mdi" :class="getSocialIcon(influencer.SNS_TYPE)"></i>
  302. <span>{{ influencer.SNS_LINK_ID }}</span>
  303. </div>
  304. </div>
  305. </div>
  306. </div>
  307. </v-card-text>
  308. <v-card-actions class="modal-actions">
  309. </v-card-actions>
  310. </v-card>
  311. </v-dialog>
  312. <!-- 브랜드사 선택 모달 -->
  313. <v-dialog v-model="brandModal" max-width="600px" persistent class="modern-modal">
  314. <v-card class="modal-card brand-modal-card">
  315. <v-card-title class="modal-header">
  316. <div class="modal-title">
  317. <i class="mdi mdi-domain"></i>
  318. 브랜드사 선택
  319. </div>
  320. <v-btn
  321. icon
  322. variant="text"
  323. @click="closeBrandModal"
  324. class="close-btn"
  325. >
  326. <i class="mdi mdi-close"></i>
  327. </v-btn>
  328. </v-card-title>
  329. <v-card-text class="modal-content">
  330. <!-- 검색 영역 -->
  331. <div class="search-section">
  332. <v-text-field
  333. v-model="brandSearchQuery"
  334. class="search-input"
  335. variant="outlined"
  336. placeholder="회사명으로 검색"
  337. prepend-inner-icon="mdi-magnify"
  338. clearable
  339. hide-details
  340. @input="searchBrands"
  341. >
  342. <template v-slot:append-inner>
  343. <v-progress-circular
  344. v-if="isBrandSearching"
  345. indeterminate
  346. size="20"
  347. width="2"
  348. color="primary"
  349. ></v-progress-circular>
  350. </template>
  351. </v-text-field>
  352. </div>
  353. <!-- 결과 영역 -->
  354. <div v-if="filteredBrandList.length === 0 && !isBrandSearching" class="empty-state">
  355. <i class="mdi mdi-domain-off"></i>
  356. <h4>{{ brandSearchQuery ? '검색 결과가 없습니다' : '등록된 브랜드사가 없습니다' }}</h4>
  357. <p>{{ brandSearchQuery ? '다른 검색어로 시도해보세요' : '먼저 브랜드사 등록을 진행해 주세요' }}</p>
  358. </div>
  359. <!-- 브랜드사 목록 -->
  360. <div v-else class="influencer-list-container">
  361. <div
  362. v-for="brand in filteredBrandList"
  363. :key="brand.SEQ"
  364. @click="selectBrand(brand)"
  365. class="influencer-list-item"
  366. :class="{ selected: selectedBrand?.SEQ === brand.SEQ }"
  367. >
  368. <div class="item-avatar">
  369. <i class="mdi mdi-domain"></i>
  370. </div>
  371. <div class="item-info">
  372. <h4 class="nickname">{{ brand.COMPANY_NAME }}</h4>
  373. <!-- <p class="name">{{ brand.NAME }}</p> -->
  374. <!-- <div class="sns-info" v-if="brand.EMAIL">
  375. <i class="mdi mdi-email"></i>
  376. <span>{{ brand.EMAIL }}</span>
  377. </div> -->
  378. </div>
  379. </div>
  380. </div>
  381. </v-card-text>
  382. <v-card-actions class="modal-actions">
  383. </v-card-actions>
  384. </v-card>
  385. </v-dialog>
  386. </div>
  387. </template>
  388. <script setup>
  389. import useAxios from "@/composables/useAxios";
  390. import VueDatePicker from "@vuepic/vue-datepicker";
  391. import "@vuepic/vue-datepicker/dist/main.css";
  392. import dayjs from 'dayjs';
  393. /************************************************************************
  394. | 레이아웃
  395. ************************************************************************/
  396. definePageMeta({
  397. layout: "default",
  398. });
  399. /************************************************************************
  400. | 스토어
  401. ************************************************************************/
  402. const useDtStore = useDetailStore();
  403. const useAtStore = useAuthStore();
  404. /************************************************************************
  405. | 전역
  406. ************************************************************************/
  407. const memberType = useAtStore.auth.memberType;
  408. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  409. const router = useRouter();
  410. const pageId = ref("");
  411. const itemType = useDtStore.boardInfo.itemType;
  412. const datePickerFormat = "yyyy-MM-dd";
  413. const sunEditorWrapper = ref(null); //에디터용 전역
  414. const updatedContent = ref(null); //에디터용 전역
  415. const editorContentReq = ref(); //에디터용 전역
  416. const addForm = ref(null);
  417. const index = ref(null);
  418. const imageIndex = ref(0);
  419. const items = ref([]);
  420. const quillEditor = ref(null);
  421. const imgTemp = ref("");
  422. const zipInfo = ref({
  423. file_path: "",
  424. original_name: ""
  425. })
  426. const rowId = ref();
  427. const form = ref({
  428. formValue1: "",
  429. formValue2: "",
  430. formValue3: "",
  431. formValue4: "",
  432. formValue5: null,
  433. formValue6: "",
  434. formValue7: null,
  435. formValue8: "0",
  436. formValue8Arr: [
  437. { title: "판매중", value: "0" },
  438. { title: "품절", value: "1" },
  439. ],
  440. formValue9: "Y",
  441. formValue9Arr: [
  442. { title: "노출", value: "Y" },
  443. { title: "비노출", value: "N" },
  444. ],
  445. formValue10: "",
  446. contact_inf: "", // 실제 전송될 INFLUENCER_SEQ
  447. contact_inf_display: "", // 화면에 표시될 이름
  448. contact_brd: "", // 실제 전송될 contact_brd
  449. contact_brd_display: "", // 화면에 표시될 브랜드명
  450. order_link: "",
  451. order_start_date: "",
  452. order_end_date: "",
  453. });
  454. // 인플루언서 관련 변수
  455. const influencerModal = ref(false);
  456. const influencerList = ref([]);
  457. const filteredInfluencerList = ref([]);
  458. const selectedInfluencer = ref(null);
  459. const influencerSearchQuery = ref("");
  460. const isSearching = ref(false);
  461. // 브랜드사 관련 변수
  462. const brandModal = ref(false);
  463. const brandList = ref([]);
  464. const filteredBrandList = ref([]);
  465. const selectedBrand = ref(null);
  466. const brandSearchQuery = ref("");
  467. const isBrandSearching = ref(false);
  468. const apiUrl = ref("");
  469. apiUrl.value = import.meta.env.VITE_APP_API_URL;
  470. const objProc = ref({
  471. validErrorMessage: "",
  472. });
  473. const pageType = ref("");
  474. /************************************************************************
  475. | 함수(METHODS)
  476. ************************************************************************/
  477. const listLocated = () => {
  478. router.push({
  479. path: "/view/common/item",
  480. });
  481. useDtStore.boardInfo.itemType = itemType;
  482. };
  483. const detailLocated = () => {
  484. router.push({
  485. path: "/view/common/item/detail",
  486. });
  487. useDtStore.boardInfo.itemType = itemType;
  488. };
  489. // 인플루언서 이름 조회
  490. const getInfName = async (contact_inf) => {
  491. try {
  492. // contact_inf 값이 있을 때만 API 호출
  493. if (!contact_inf) {
  494. return null;
  495. }
  496. const response = await useAxios().get(`/user/getInfName/${contact_inf}`);
  497. if (response.data?.status === 'success') {
  498. return response.data.data;
  499. } else {
  500. return null;
  501. }
  502. } catch (error) {
  503. return null;
  504. }
  505. };
  506. // 브랜드사 조회
  507. const getBrdName = async (contact_brd) => {
  508. try {
  509. if (!contact_brd) {
  510. return null;
  511. }
  512. const response = await useAxios().get(`/user/getBrdName/${contact_brd}`);
  513. if (response.data?.status === 'success') {
  514. return response.data.data;
  515. } else {
  516. return null;
  517. }
  518. } catch (error) {
  519. return null;
  520. }
  521. };
  522. const fnPicFileUploadOpen = () => {
  523. let fileUpload = document.getElementById("fileupload_pic");
  524. if (fileUpload != null) {
  525. fileUpload.click();
  526. }
  527. };
  528. const fnUploadPicFileCheck = () => {
  529. if (form.value.formValue5) {
  530. // 10Mb 이상은 업로드 불가
  531. if (form.value.formValue5.size > 10 * 1024 * 1024) {
  532. fnOpenCommPop("10mb 이상은 업로드가 불가합니다.");
  533. form.value.formValue5 = null;
  534. return;
  535. }
  536. // 이미지 파일 형식 체크
  537. let extension = form.value.formValue5.name.split(".").pop().toLowerCase();
  538. if (
  539. extension != "jpg" &&
  540. extension != "jpeg" &&
  541. extension != "png" &&
  542. extension != "gif"
  543. ) {
  544. fnOpenCommPop("파일 형식 또는 확장자가 올바르지 않습니다.");
  545. form.value.formValue5 = null;
  546. return;
  547. }
  548. objProc.validErrorMessage = "";
  549. // 이미지 미리보기
  550. let previewImage = new Image();
  551. let tempImageUrl = window.URL.createObjectURL(form.value.formValue5);
  552. //console.log(tempImageUrl);
  553. previewImage.src = tempImageUrl;
  554. items.value[0] = tempImageUrl;
  555. imgTemp.value = tempImageUrl;
  556. }
  557. };
  558. const fnDownloadFile = () => {
  559. window.location.href = `https://shopdeli.mycafe24.com/item/download/${zipInfo.value.file_path}`;
  560. }
  561. // 인플루언서 목록 조회
  562. const getInfluencerList = async () => {
  563. try {
  564. const params = {
  565. page: 1,
  566. size: 100, // 충분한 수량
  567. };
  568. const response = await useAxios().post('/user/list', params);
  569. if (response.data) {
  570. influencerList.value = response.data;
  571. filteredInfluencerList.value = response.data;
  572. } else {
  573. //console.error('❌ API 실패:', response.data.message);
  574. influencerList.value = [];
  575. filteredInfluencerList.value = [];
  576. }
  577. } catch (error) {
  578. //console.error('❌ 인플루언서 목록 조회 실패:', error);
  579. influencerList.value = [];
  580. filteredInfluencerList.value = [];
  581. }
  582. };
  583. // 인플루언서 검색 기능
  584. const searchInfluencers = () => {
  585. if (!influencerSearchQuery.value.trim()) {
  586. filteredInfluencerList.value = influencerList.value;
  587. return;
  588. }
  589. isSearching.value = true;
  590. setTimeout(() => {
  591. const query = influencerSearchQuery.value.toLowerCase().trim();
  592. filteredInfluencerList.value = influencerList.value.filter(influencer => {
  593. const nickname = (influencer.NICK_NAME || '').toLowerCase();
  594. const name = (influencer.NAME || '').toLowerCase();
  595. const userName = (influencer.USER_NAME || '').toLowerCase();
  596. return nickname.includes(query) || name.includes(query) || userName.includes(query);
  597. });
  598. isSearching.value = false;
  599. }, 300);
  600. };
  601. // 소셜 아이콘 반환
  602. const getSocialIcon = (snsType) => {
  603. switch(snsType?.toLowerCase()) {
  604. case 'instagram': return 'mdi-instagram';
  605. case 'youtube': return 'mdi-youtube';
  606. case 'tiktok': return 'mdi-music-note';
  607. case 'twitter': return 'mdi-twitter';
  608. default: return 'mdi-web';
  609. }
  610. };
  611. // 인플루언서 선택 모달 열기
  612. const openInfluencerModal = async () => {
  613. await getInfluencerList();
  614. filteredInfluencerList.value = influencerList.value;
  615. selectedInfluencer.value = null;
  616. influencerSearchQuery.value = "";
  617. influencerModal.value = true;
  618. };
  619. // 인플루언서 선택 모달 닫기
  620. const closeInfluencerModal = () => {
  621. influencerModal.value = false;
  622. selectedInfluencer.value = null;
  623. influencerSearchQuery.value = "";
  624. };
  625. // 인플루언서 선택
  626. const selectInfluencer = (influencer) => {
  627. // 화면에 표시할 이름
  628. const displayName = influencer.NICK_NAME
  629. ? `${influencer.NICK_NAME} (${influencer.NAME})`
  630. : influencer.NAME;
  631. // 실제 전송할 SEQ 값과 표시용 이름 한번에 저장
  632. form.value.contact_inf = influencer.SEQ; // SEQ 값 저장
  633. form.value.contact_inf_display = displayName; // 유저이름 저장
  634. influencerModal.value = false;
  635. };
  636. // 브랜드사 목록 조회
  637. const getBrandList = async () => {
  638. try {
  639. const params = {
  640. page: 1,
  641. size: 100,
  642. };
  643. //console.log('🔍 getBrandList 호출됨:', params);
  644. const response = await useAxios().post('/user/brandlist', params);
  645. if (response.data && response.data.length > 0) {
  646. //console.log('📋 받아온 브랜드사 목록:', response.data.length, response.data);
  647. brandList.value = response.data;
  648. } else {
  649. //console.error('❌ 브랜드사 목록이 비어있음');
  650. brandList.value = [];
  651. }
  652. } catch (error) {
  653. //console.error('❌ 브랜드사 목록 조회 실패:', error);
  654. brandList.value = [];
  655. }
  656. };
  657. // 브랜드사 검색 기능
  658. const searchBrands = () => {
  659. if (!brandSearchQuery.value.trim()) {
  660. filteredBrandList.value = brandList.value;
  661. return;
  662. }
  663. isBrandSearching.value = true;
  664. setTimeout(() => {
  665. const query = brandSearchQuery.value.toLowerCase().trim();
  666. filteredBrandList.value = brandList.value.filter(brand => {
  667. const companyName = (brand.COMPANY_NAME || '').toLowerCase();
  668. const name = (brand.NAME || '').toLowerCase();
  669. return companyName.includes(query) || name.includes(query);
  670. });
  671. isBrandSearching.value = false;
  672. }, 300);
  673. };
  674. // 브랜드사 선택 모달 열기
  675. const openBrandModal = async () => {
  676. await getBrandList();
  677. filteredBrandList.value = brandList.value;
  678. selectedBrand.value = null;
  679. brandSearchQuery.value = "";
  680. brandModal.value = true;
  681. };
  682. // 브랜드사 선택 모달 닫기
  683. const closeBrandModal = () => {
  684. brandModal.value = false;
  685. selectedBrand.value = null;
  686. brandSearchQuery.value = "";
  687. };
  688. // 브랜드사 선택
  689. const selectBrand = (brand) => {
  690. // 화면에 표시할 이름
  691. const displayName = brand.COMPANY_NAME;
  692. // 실제 전송할 SEQ 값과 표시용 이름 한번에 저장
  693. form.value.contact_brd = brand.SEQ; // SEQ 값 저장
  694. form.value.contact_brd_display = displayName; // 회사명 저장
  695. brandModal.value = false;
  696. selectedBrand.value = brand;
  697. };
  698. // 인플루언서 삭제
  699. const clearInfluencer = () => {
  700. form.value.contact_inf = "";
  701. form.value.contact_inf_display = "";
  702. };
  703. // 브랜드사 삭제
  704. const clearBrand = () => {
  705. form.value.contact_brd = "";
  706. form.value.contact_brd_display = "";
  707. };
  708. // 브랜드사 이름 가져오기
  709. const getBrandNameBySeq = async (brandSeq) => {
  710. try {
  711. if (!brandSeq) return '';
  712. // 이미 로드된 목록에서 찾기
  713. if (brandList.value.length > 0) {
  714. const brand = brandList.value.find(b => b.SEQ == brandSeq);
  715. if (brand) {
  716. return brand.COMPANY_NAME;
  717. }
  718. }
  719. // 목록에 없으면 API 호출해서 전체 목록 가져오기
  720. await getBrandList();
  721. const brand = brandList.value.find(b => b.SEQ == brandSeq);
  722. if (brand) {
  723. return brand.COMPANY_NAME;
  724. }
  725. return `브랜드사 ID: ${brandSeq}`;
  726. } catch (error) {
  727. //console.error('브랜드사 이름 조회 실패:', error);
  728. return `브랜드사 ID: ${brandSeq}`;
  729. }
  730. };
  731. /*======================================================================
  732. | 작성 시퀀스
  733. | 1. 작성 컨펌
  734. | 2. 버튼 체크
  735. | 3. 등록시 -> 등록 API 호출
  736. ======================================================================*/
  737. const fnBtnEvt = () => {
  738. //await editorContent();
  739. nextTick(() => {
  740. if (addForm.value && typeof addForm.value.validate === "function") {
  741. addForm.value
  742. .validate()
  743. .then((isValid) => {
  744. if (
  745. isValid.valid
  746. ) {
  747. if (pageType.value == "I") fnRegEvt();
  748. else fnUpdEvt();
  749. } else {
  750. let param = {
  751. id: pageId,
  752. title: pageId,
  753. content: "필수항목을 입력해주세요.",
  754. yes: {
  755. text: "확인",
  756. isProc: false,
  757. }
  758. };
  759. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  760. }
  761. })
  762. .catch((err) => {
  763. });
  764. } else {
  765. }
  766. });
  767. };
  768. const fnOpenCommPop = (__TEXT) => {
  769. let param = {
  770. id: pageId,
  771. title: "알림",
  772. content: __TEXT,
  773. yes: {
  774. text: "확인",
  775. isProc: false,
  776. },
  777. };
  778. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  779. };
  780. const fnRegEvt = () => {
  781. let param = {
  782. id: pageId,
  783. title: pageId,
  784. content: "등록하시겠습니까?",
  785. yes: {
  786. text: "등록",
  787. isProc: true,
  788. event: "FN_INSERT",
  789. param: "",
  790. },
  791. no: {
  792. text: "취소",
  793. isProc: false,
  794. },
  795. };
  796. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  797. };
  798. const fnUpdEvt = () => {
  799. let param = {
  800. id: pageId,
  801. title: pageId,
  802. content: "수정하시겠습니까?",
  803. yes: {
  804. text: "확인",
  805. isProc: true,
  806. event: "FN_UPDATE",
  807. param: "",
  808. },
  809. no: {
  810. text: "취소",
  811. isProc: false,
  812. },
  813. };
  814. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  815. };
  816. const fnInsert = async () => {
  817. const formData = new FormData();
  818. formData.append('name', form.value.formValue1);
  819. formData.append('price1', form.value.formValue2);
  820. formData.append('price2', form.value.formValue3);
  821. formData.append('deli_fee', form.value.formValue4);
  822. formData.append('thumb_file', form.value.formValue5);
  823. formData.append('sub_title', form.value.formValue6);
  824. formData.append('detail', updatedContent.value);
  825. formData.append('zip_file', form.value.formValue7);
  826. formData.append('status', form.value.formValue8);
  827. formData.append('show_yn', form.value.formValue9);
  828. formData.append('add_info', form.value.formValue10);
  829. formData.append('order_link', form.value.order_link);
  830. formData.append('order_start_date', dayjs(form.value.order_start_date).format('YYYY-MM-DD'));
  831. formData.append('order_end_date', dayjs(form.value.order_end_date).format('YYYY-MM-DD'));
  832. formData.append('item_type', itemType);
  833. formData.append('contact_inf', form.value.contact_inf);
  834. // 벤더사의 COMPANY_NUMBER 사용
  835. const memberCompanyNumber = useAtStore.auth.companyNumber || "1";
  836. formData.append('company_number', memberCompanyNumber);
  837. if(memberType === "BRAND"){
  838. formData.append('contact_brd', useAtStore.auth.seq);
  839. } else {
  840. formData.append('contact_brd', form.value.contact_brd);
  841. }
  842. useAxios()
  843. .post('/item/reg', formData, {
  844. headers: {'Content-Type': 'multipart/form-data'},
  845. })
  846. .then((res) => {
  847. router.push("/view/common/item");
  848. })
  849. .catch((error) => {
  850. })
  851. .finally(() => {
  852. });
  853. };
  854. const fnUpdate = async () => {
  855. let req = {
  856. seq: useDtStore.boardInfo.seq,
  857. };
  858. const formData = new FormData();
  859. formData.append('name', form.value.formValue1);
  860. // formData.append('price1', form.value.formValue2);
  861. // formData.append('price2', form.value.formValue3);
  862. // formData.append('deli_fee', form.value.formValue4);
  863. // if (form.value.formValue5 instanceof File) {
  864. // formData.append('thumb_file', form.value.formValue5);
  865. // }
  866. // formData.append('sub_title', form.value.formValue6);
  867. // formData.append('detail', updatedContent.value);
  868. // if (form.value.formValue7 instanceof File) {
  869. // formData.append('zip_file', form.value.formValue7);
  870. // }
  871. // formData.append('status', form.value.formValue8);
  872. // formData.append('show_yn', form.value.formValue9);
  873. // formData.append('add_info', form.value.formValue10);
  874. formData.append('order_link', form.value.order_link);
  875. formData.append('order_start_date', dayjs(form.value.order_start_date).format('YYYY-MM-DD'));
  876. formData.append('order_end_date', dayjs(form.value.order_end_date).format('YYYY-MM-DD'));
  877. formData.append('contact_inf', form.value.contact_inf);
  878. // 벤더사의 COMPANY_NUMBER 사용
  879. const memberCompanyNumber = useAtStore.auth.companyNumber || "1";
  880. formData.append('company_number', memberCompanyNumber);
  881. if(memberType === "BRAND"){
  882. formData.append('contact_brd', useAtStore.auth.seq);
  883. } else {
  884. formData.append('contact_brd', form.value.contact_brd);
  885. }
  886. try {
  887. const res = await useAxios().post(`/item/update/${req.seq}`, formData, {
  888. headers: { 'Content-Type': 'multipart/form-data' },
  889. });
  890. router.push("/view/common/item/detail");
  891. } catch (error) {
  892. }
  893. };
  894. const fnDetail = () => {
  895. let req = {
  896. seq: useDtStore.boardInfo.seq,
  897. };
  898. useAxios()
  899. .get(`/item/detail/${req.seq}`)
  900. .then(async (res) => {
  901. form.value.formValue1 = res.data.NAME;
  902. // form.value.formValue2 = res.data.PRICE1;
  903. // form.value.formValue3 = res.data.PRICE2;
  904. // form.value.formValue4 = res.data.DELI_FEE;
  905. // form.value.formValue5 = res.data.THUMB_FILE;
  906. // form.value.formValue6 = res.data.SUB_TITLE;
  907. // zipInfo.value.file_path = res.data.ZIP_FILE;
  908. // zipInfo.value.original_name = res.data.ZIP_FILE_ORIGIN;
  909. //에디터에 컨텐츠 전달
  910. //editorContentReq.value = res.data.DETAIL;
  911. form.value.formValue8 = res.data.STATUS;
  912. form.value.formValue9 = res.data.SHOW_YN;
  913. form.value.formValue10 = res.data.ADD_INFO;
  914. form.value.order_link = res.data.ORDER_LINK;
  915. form.value.order_start_date = res.data.ORDER_START_DATE;
  916. form.value.order_end_date = res.data.ORDER_END_DATE;
  917. form.value.contact_inf = res.data.CONTACT_INF;
  918. form.value.contact_brd = res.data.CONTACT_BRD;
  919. // contact_inf 값이 있으면 인플루언서 이름 조회
  920. if (res.data.CONTACT_INF) {
  921. const infData = await getInfName(res.data.CONTACT_INF);
  922. if (infData) {
  923. form.value.contact_inf_display = infData.NICK_NAME
  924. ? `${infData.NICK_NAME} (${infData.NAME})`
  925. : infData.NAME;
  926. }
  927. }
  928. if (res.data.CONTACT_BRD) {
  929. const brdData = await getBrdName(res.data.CONTACT_BRD);
  930. if (brdData) {
  931. form.value.contact_brd_display = brdData.NAME;
  932. }
  933. }
  934. //썸네일 파일이 있으면 넣어줌
  935. if(form.value.formValue5){
  936. imgTemp.value = `https://shopdeli.mycafe24.com/writable/uploads/item/thumb/${form.value.formValue5}`;
  937. }
  938. })
  939. .catch((error) => {
  940. })
  941. .finally(() => {
  942. });
  943. };
  944. /*=======================================================================
  945. | 최종 에디터 이미지 url치환 : S
  946. /*=======================================================================*/
  947. // const editorContent = async () => {
  948. // const content = sunEditorWrapper.value.getEditorContent();
  949. // updatedContent.value = await processEditorContent(content);
  950. // console.log("Updated content:", updatedContent.value);
  951. // };
  952. // Base64 데이터를 Blob으로 변환
  953. const base64ToBlob = (base64, mimeType) => {
  954. const byteString = atob(base64.split(",")[1]);
  955. const arrayBuffer = new ArrayBuffer(byteString.length);
  956. const uint8Array = new Uint8Array(arrayBuffer);
  957. for (let i = 0; i < byteString.length; i++) {
  958. uint8Array[i] = byteString.charCodeAt(i);
  959. }
  960. return new Blob([uint8Array], { type: mimeType });
  961. };
  962. // Base64 데이터를 File 객체로 변환
  963. const base64ToFile = (base64, mimeType, fileName) => {
  964. const blob = base64ToBlob(base64, mimeType);
  965. return new File([blob], fileName, { type: mimeType });
  966. };
  967. // 이미지 업로드 처리 (useAxios)
  968. const uploadImage = async (file) => {
  969. const formDataEdt = new FormData();
  970. formDataEdt.append("picObj", file);
  971. return useAxios()
  972. .post("/pic/upload", formDataEdt, {
  973. headers: { "Content-Type": "multipart/form-data" },
  974. })
  975. .then((res) => {
  976. const filePath = res.data.ogn_name.path.replace(/.*\/files\//, "");
  977. const fileName = res.data.ogn_name.file_name;
  978. return `${apiUrl.value}/images/${filePath}/${fileName}`; // 최종 URL 반환
  979. })
  980. .catch((error) => {
  981. console.error("Image upload failed:", error);
  982. return null;
  983. });
  984. };
  985. // 에디터 내용 처리 및 이미지 업로드
  986. const processEditorContent = async (content) => {
  987. const parser = new DOMParser();
  988. const doc = parser.parseFromString(content, "text/html");
  989. const images = doc.querySelectorAll("img");
  990. for (let i = 0; i < images.length; i++) {
  991. const img = images[i];
  992. const src = img.src;
  993. if (src.startsWith("data:image")) {
  994. // MIME 타입과 파일 이름 추출
  995. const mimeType = src.split(";")[0].split(":")[1];
  996. const extension = mimeType.split("/")[1];
  997. const fileName = `image-${i + 1}.${extension}`;
  998. // Base64 데이터를 File 객체로 변환
  999. const file = base64ToFile(src, mimeType, fileName);
  1000. // 이미지 업로드 및 URL 반환
  1001. const finalUrl = await uploadImage(file);
  1002. if (finalUrl) {
  1003. img.src = finalUrl; // 이미지 src 업데이트
  1004. }
  1005. }
  1006. }
  1007. return doc.body.innerHTML; // 최종 수정된 HTML 반환
  1008. };
  1009. /*=======================================================================
  1010. | 최종 에디터 이미지 url치환 : E
  1011. /*=======================================================================*/
  1012. /************************************************************************
  1013. | 팝업 이벤트버스 정의
  1014. ************************************************************************/
  1015. $eventBus.off("FN_INSERT");
  1016. $eventBus.on("FN_INSERT", () => {
  1017. fnInsert();
  1018. });
  1019. $eventBus.off("FN_UPDATE");
  1020. $eventBus.on("FN_UPDATE", () => {
  1021. fnUpdate();
  1022. });
  1023. $eventBus.off("FN_DELETE");
  1024. $eventBus.on("FN_DELETE", () => {
  1025. fnDelete();
  1026. });
  1027. /************************************************************************
  1028. | 라이프사이클
  1029. ************************************************************************/
  1030. onMounted(() => {
  1031. pageType.value = useDtStore.boardInfo.pageType;
  1032. if(pageType.value == "I"){
  1033. if(itemType == "G"){
  1034. pageId.value = "공동구매 등록"
  1035. } else {
  1036. pageId.value = "제품 등록"
  1037. }
  1038. } else if(pageType.value == "U"){
  1039. if(itemType == "G"){
  1040. pageId.value = "공동구매 수정"
  1041. } else {
  1042. pageId.value = "제품 수정"
  1043. }
  1044. } else {
  1045. if(itemType == "G"){
  1046. pageId.value = "공동구매 상세"
  1047. } else {
  1048. pageId.value = "제품 상세"
  1049. }
  1050. }
  1051. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  1052. if (pageType.value !== "I") {
  1053. fnDetail();
  1054. }
  1055. });
  1056. /************************************************************************
  1057. | WATCH
  1058. ************************************************************************/
  1059. // 시작일이 변경될 때, 종료일이 시작일보다 이전이면 종료일을 시작일과 같게 설정
  1060. watch(() => form.value.order_start_date, (newStartDate) => {
  1061. if (newStartDate && form.value.order_end_date && form.value.order_end_date < newStartDate) {
  1062. form.value.order_end_date = newStartDate;
  1063. }
  1064. });
  1065. // 종료일이 변경될 때, 종료일이 시작일보다 이전이면 시작일과 같게 설정
  1066. watch(() => form.value.order_end_date, (newEndDate) => {
  1067. if (newEndDate && form.value.order_start_date && newEndDate < form.value.order_start_date) {
  1068. form.value.order_end_date = form.value.order_start_date;
  1069. }
  1070. });
  1071. </script>
  1072. <style scoped>
  1073. .cursor-pointer {
  1074. cursor: pointer;
  1075. }
  1076. .cursor-pointer:hover {
  1077. background-color: #f5f5f5;
  1078. }
  1079. .order-link {
  1080. color: #1976d2;
  1081. text-decoration: none;
  1082. display: inline-flex;
  1083. align-items: center;
  1084. transition: color 0.2s;
  1085. }
  1086. .order-link:hover {
  1087. color: #1565c0;
  1088. text-decoration: underline;
  1089. }
  1090. .no-link {
  1091. color: #999;
  1092. font-style: italic;
  1093. }
  1094. .delete-btn.v-btn--variant-outlined {
  1095. border-color: #f44336 !important;
  1096. }
  1097. /* 데이트피커 관련 스타일 */
  1098. .form-section {
  1099. overflow: visible !important;
  1100. }
  1101. .date-picker-wrapper {
  1102. position: relative;
  1103. z-index: 10;
  1104. }
  1105. .date-picker-group {
  1106. position: relative;
  1107. z-index: 10;
  1108. }
  1109. .date-picker-item {
  1110. position: relative;
  1111. z-index: 10;
  1112. }
  1113. /* VueDatePicker 팝업이 form-section을 벗어나도 보이도록 */
  1114. .modern-date-picker {
  1115. position: relative;
  1116. z-index: 1000;
  1117. }
  1118. /* VueDatePicker 오버레이 스타일 조정 */
  1119. :deep(.dp__overlay) {
  1120. z-index: 1001 !important;
  1121. }
  1122. :deep(.dp__menu) {
  1123. z-index: 1001 !important;
  1124. }
  1125. :deep(.dp__calendar_wrap) {
  1126. z-index: 1001 !important;
  1127. }
  1128. /* form-container와 main-content도 overflow visible로 설정 */
  1129. .form-container {
  1130. overflow: visible !important;
  1131. }
  1132. .main-content {
  1133. overflow: visible !important;
  1134. }
  1135. </style>