add.vue 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258
  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. } else {
  572. //console.error('❌ API 실패:', response.data.message);
  573. influencerList.value = [];
  574. }
  575. } catch (error) {
  576. //console.error('❌ 인플루언서 목록 조회 실패:', error);
  577. influencerList.value = [];
  578. }
  579. };
  580. // 인플루언서 검색 기능
  581. const searchInfluencers = () => {
  582. if (!influencerSearchQuery.value.trim()) {
  583. filteredInfluencerList.value = influencerList.value;
  584. return;
  585. }
  586. isSearching.value = true;
  587. setTimeout(() => {
  588. const query = influencerSearchQuery.value.toLowerCase().trim();
  589. filteredInfluencerList.value = influencerList.value.filter(influencer => {
  590. const nickname = (influencer.INFLUENCER_NICKNAME || '').toLowerCase();
  591. const name = (influencer.INFLUENCER_NAME || '').toLowerCase();
  592. return nickname.includes(query) || name.includes(query);
  593. });
  594. isSearching.value = false;
  595. }, 300);
  596. };
  597. // 소셜 아이콘 반환
  598. const getSocialIcon = (snsType) => {
  599. switch(snsType?.toLowerCase()) {
  600. case 'instagram': return 'mdi-instagram';
  601. case 'youtube': return 'mdi-youtube';
  602. case 'tiktok': return 'mdi-music-note';
  603. case 'twitter': return 'mdi-twitter';
  604. default: return 'mdi-web';
  605. }
  606. };
  607. // 인플루언서 선택 모달 열기
  608. const openInfluencerModal = async () => {
  609. await getInfluencerList();
  610. filteredInfluencerList.value = influencerList.value;
  611. selectedInfluencer.value = null;
  612. influencerSearchQuery.value = "";
  613. influencerModal.value = true;
  614. };
  615. // 인플루언서 선택 모달 닫기
  616. const closeInfluencerModal = () => {
  617. influencerModal.value = false;
  618. selectedInfluencer.value = null;
  619. influencerSearchQuery.value = "";
  620. };
  621. // 인플루언서 선택
  622. const selectInfluencer = (influencer) => {
  623. // 화면에 표시할 이름
  624. const displayName = influencer.NICK_NAME
  625. ? `${influencer.NICK_NAME} (${influencer.NAME})`
  626. : influencer.NAME;
  627. // 실제 전송할 SEQ 값과 표시용 이름 한번에 저장
  628. form.value.contact_inf = influencer.SEQ; // SEQ 값 저장
  629. form.value.contact_inf_display = displayName; // 유저이름 저장
  630. influencerModal.value = false;
  631. };
  632. // 브랜드사 목록 조회
  633. const getBrandList = async () => {
  634. try {
  635. const params = {
  636. page: 1,
  637. size: 100,
  638. };
  639. //console.log('🔍 getBrandList 호출됨:', params);
  640. const response = await useAxios().post('/user/brandlist', params);
  641. if (response.data && response.data.length > 0) {
  642. //console.log('📋 받아온 브랜드사 목록:', response.data.length, response.data);
  643. brandList.value = response.data;
  644. } else {
  645. //console.error('❌ 브랜드사 목록이 비어있음');
  646. brandList.value = [];
  647. }
  648. } catch (error) {
  649. //console.error('❌ 브랜드사 목록 조회 실패:', error);
  650. brandList.value = [];
  651. }
  652. };
  653. // 브랜드사 검색 기능
  654. const searchBrands = () => {
  655. if (!brandSearchQuery.value.trim()) {
  656. filteredBrandList.value = brandList.value;
  657. return;
  658. }
  659. isBrandSearching.value = true;
  660. setTimeout(() => {
  661. const query = brandSearchQuery.value.toLowerCase().trim();
  662. filteredBrandList.value = brandList.value.filter(brand => {
  663. const companyName = (brand.COMPANY_NAME || '').toLowerCase();
  664. const name = (brand.NAME || '').toLowerCase();
  665. return companyName.includes(query) || name.includes(query);
  666. });
  667. isBrandSearching.value = false;
  668. }, 300);
  669. };
  670. // 브랜드사 선택 모달 열기
  671. const openBrandModal = async () => {
  672. await getBrandList();
  673. filteredBrandList.value = brandList.value;
  674. selectedBrand.value = null;
  675. brandSearchQuery.value = "";
  676. brandModal.value = true;
  677. };
  678. // 브랜드사 선택 모달 닫기
  679. const closeBrandModal = () => {
  680. brandModal.value = false;
  681. selectedBrand.value = null;
  682. brandSearchQuery.value = "";
  683. };
  684. // 브랜드사 선택
  685. const selectBrand = (brand) => {
  686. // 화면에 표시할 이름
  687. const displayName = brand.COMPANY_NAME;
  688. // 실제 전송할 SEQ 값과 표시용 이름 한번에 저장
  689. form.value.contact_brd = brand.SEQ; // SEQ 값 저장
  690. form.value.contact_brd_display = displayName; // 회사명 저장
  691. brandModal.value = false;
  692. selectedBrand.value = brand;
  693. };
  694. // 인플루언서 삭제
  695. const clearInfluencer = () => {
  696. form.value.contact_inf = "";
  697. form.value.contact_inf_display = "";
  698. };
  699. // 브랜드사 삭제
  700. const clearBrand = () => {
  701. form.value.contact_brd = "";
  702. form.value.contact_brd_display = "";
  703. };
  704. // 브랜드사 이름 가져오기
  705. const getBrandNameBySeq = async (brandSeq) => {
  706. try {
  707. if (!brandSeq) return '';
  708. // 이미 로드된 목록에서 찾기
  709. if (brandList.value.length > 0) {
  710. const brand = brandList.value.find(b => b.SEQ == brandSeq);
  711. if (brand) {
  712. return brand.COMPANY_NAME;
  713. }
  714. }
  715. // 목록에 없으면 API 호출해서 전체 목록 가져오기
  716. await getBrandList();
  717. const brand = brandList.value.find(b => b.SEQ == brandSeq);
  718. if (brand) {
  719. return brand.COMPANY_NAME;
  720. }
  721. return `브랜드사 ID: ${brandSeq}`;
  722. } catch (error) {
  723. //console.error('브랜드사 이름 조회 실패:', error);
  724. return `브랜드사 ID: ${brandSeq}`;
  725. }
  726. };
  727. /*======================================================================
  728. | 작성 시퀀스
  729. | 1. 작성 컨펌
  730. | 2. 버튼 체크
  731. | 3. 등록시 -> 등록 API 호출
  732. ======================================================================*/
  733. const fnBtnEvt = () => {
  734. //await editorContent();
  735. nextTick(() => {
  736. if (addForm.value && typeof addForm.value.validate === "function") {
  737. addForm.value
  738. .validate()
  739. .then((isValid) => {
  740. if (
  741. isValid.valid
  742. ) {
  743. if (pageType.value == "I") fnRegEvt();
  744. else fnUpdEvt();
  745. } else {
  746. let param = {
  747. id: pageId,
  748. title: pageId,
  749. content: "필수항목을 입력해주세요.",
  750. yes: {
  751. text: "확인",
  752. isProc: false,
  753. }
  754. };
  755. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  756. }
  757. })
  758. .catch((err) => {
  759. });
  760. } else {
  761. }
  762. });
  763. };
  764. const fnOpenCommPop = (__TEXT) => {
  765. let param = {
  766. id: pageId,
  767. title: "알림",
  768. content: __TEXT,
  769. yes: {
  770. text: "확인",
  771. isProc: false,
  772. },
  773. };
  774. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  775. };
  776. const fnRegEvt = () => {
  777. let param = {
  778. id: pageId,
  779. title: pageId,
  780. content: "등록하시겠습니까?",
  781. yes: {
  782. text: "등록",
  783. isProc: true,
  784. event: "FN_INSERT",
  785. param: "",
  786. },
  787. no: {
  788. text: "취소",
  789. isProc: false,
  790. },
  791. };
  792. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  793. };
  794. const fnUpdEvt = () => {
  795. let param = {
  796. id: pageId,
  797. title: pageId,
  798. content: "수정하시겠습니까?",
  799. yes: {
  800. text: "확인",
  801. isProc: true,
  802. event: "FN_UPDATE",
  803. param: "",
  804. },
  805. no: {
  806. text: "취소",
  807. isProc: false,
  808. },
  809. };
  810. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  811. };
  812. const fnInsert = async () => {
  813. const formData = new FormData();
  814. formData.append('name', form.value.formValue1);
  815. formData.append('price1', form.value.formValue2);
  816. formData.append('price2', form.value.formValue3);
  817. formData.append('deli_fee', form.value.formValue4);
  818. formData.append('thumb_file', form.value.formValue5);
  819. formData.append('sub_title', form.value.formValue6);
  820. formData.append('detail', updatedContent.value);
  821. formData.append('zip_file', form.value.formValue7);
  822. formData.append('status', form.value.formValue8);
  823. formData.append('show_yn', form.value.formValue9);
  824. formData.append('add_info', form.value.formValue10);
  825. formData.append('order_link', form.value.order_link);
  826. formData.append('order_start_date', dayjs(form.value.order_start_date).format('YYYY-MM-DD'));
  827. formData.append('order_end_date', dayjs(form.value.order_end_date).format('YYYY-MM-DD'));
  828. formData.append('item_type', itemType);
  829. formData.append('contact_inf', form.value.contact_inf);
  830. // 벤더사의 COMPANY_NUMBER 사용
  831. const memberCompanyNumber = useAtStore.auth.companyNumber || "1";
  832. formData.append('company_number', memberCompanyNumber);
  833. if(memberType === "BRAND"){
  834. formData.append('contact_brd', useAtStore.auth.seq);
  835. } else {
  836. formData.append('contact_brd', form.value.contact_brd);
  837. }
  838. useAxios()
  839. .post('/item/reg', formData, {
  840. headers: {'Content-Type': 'multipart/form-data'},
  841. })
  842. .then((res) => {
  843. router.push("/view/common/item");
  844. })
  845. .catch((error) => {
  846. })
  847. .finally(() => {
  848. });
  849. };
  850. const fnUpdate = async () => {
  851. let req = {
  852. seq: useDtStore.boardInfo.seq,
  853. };
  854. const formData = new FormData();
  855. formData.append('name', form.value.formValue1);
  856. // formData.append('price1', form.value.formValue2);
  857. // formData.append('price2', form.value.formValue3);
  858. // formData.append('deli_fee', form.value.formValue4);
  859. // if (form.value.formValue5 instanceof File) {
  860. // formData.append('thumb_file', form.value.formValue5);
  861. // }
  862. // formData.append('sub_title', form.value.formValue6);
  863. // formData.append('detail', updatedContent.value);
  864. // if (form.value.formValue7 instanceof File) {
  865. // formData.append('zip_file', form.value.formValue7);
  866. // }
  867. // formData.append('status', form.value.formValue8);
  868. // formData.append('show_yn', form.value.formValue9);
  869. // formData.append('add_info', form.value.formValue10);
  870. formData.append('order_link', form.value.order_link);
  871. formData.append('order_start_date', dayjs(form.value.order_start_date).format('YYYY-MM-DD'));
  872. formData.append('order_end_date', dayjs(form.value.order_end_date).format('YYYY-MM-DD'));
  873. formData.append('contact_inf', form.value.contact_inf);
  874. // 벤더사의 COMPANY_NUMBER 사용
  875. const memberCompanyNumber = useAtStore.auth.companyNumber || "1";
  876. formData.append('company_number', memberCompanyNumber);
  877. if(memberType === "BRAND"){
  878. formData.append('contact_brd', useAtStore.auth.seq);
  879. } else {
  880. formData.append('contact_brd', form.value.contact_brd);
  881. }
  882. try {
  883. const res = await useAxios().post(`/item/update/${req.seq}`, formData, {
  884. headers: { 'Content-Type': 'multipart/form-data' },
  885. });
  886. router.push("/view/common/item/detail");
  887. } catch (error) {
  888. }
  889. };
  890. const fnDetail = () => {
  891. let req = {
  892. seq: useDtStore.boardInfo.seq,
  893. };
  894. useAxios()
  895. .get(`/item/detail/${req.seq}`)
  896. .then(async (res) => {
  897. form.value.formValue1 = res.data.NAME;
  898. // form.value.formValue2 = res.data.PRICE1;
  899. // form.value.formValue3 = res.data.PRICE2;
  900. // form.value.formValue4 = res.data.DELI_FEE;
  901. // form.value.formValue5 = res.data.THUMB_FILE;
  902. // form.value.formValue6 = res.data.SUB_TITLE;
  903. // zipInfo.value.file_path = res.data.ZIP_FILE;
  904. // zipInfo.value.original_name = res.data.ZIP_FILE_ORIGIN;
  905. //에디터에 컨텐츠 전달
  906. //editorContentReq.value = res.data.DETAIL;
  907. form.value.formValue8 = res.data.STATUS;
  908. form.value.formValue9 = res.data.SHOW_YN;
  909. form.value.formValue10 = res.data.ADD_INFO;
  910. form.value.order_link = res.data.ORDER_LINK;
  911. form.value.order_start_date = res.data.ORDER_START_DATE;
  912. form.value.order_end_date = res.data.ORDER_END_DATE;
  913. form.value.contact_inf = res.data.CONTACT_INF;
  914. form.value.contact_brd = res.data.CONTACT_BRD;
  915. // contact_inf 값이 있으면 인플루언서 이름 조회
  916. if (res.data.CONTACT_INF) {
  917. const infData = await getInfName(res.data.CONTACT_INF);
  918. if (infData) {
  919. form.value.contact_inf_display = infData.NICK_NAME
  920. ? `${infData.NICK_NAME} (${infData.NAME})`
  921. : infData.NAME;
  922. }
  923. }
  924. if (res.data.CONTACT_BRD) {
  925. const brdData = await getBrdName(res.data.CONTACT_BRD);
  926. if (brdData) {
  927. form.value.contact_brd_display = brdData.NAME;
  928. }
  929. }
  930. //썸네일 파일이 있으면 넣어줌
  931. if(form.value.formValue5){
  932. imgTemp.value = `https://shopdeli.mycafe24.com/writable/uploads/item/thumb/${form.value.formValue5}`;
  933. }
  934. })
  935. .catch((error) => {
  936. })
  937. .finally(() => {
  938. });
  939. };
  940. /*=======================================================================
  941. | 최종 에디터 이미지 url치환 : S
  942. /*=======================================================================*/
  943. // const editorContent = async () => {
  944. // const content = sunEditorWrapper.value.getEditorContent();
  945. // updatedContent.value = await processEditorContent(content);
  946. // console.log("Updated content:", updatedContent.value);
  947. // };
  948. // Base64 데이터를 Blob으로 변환
  949. const base64ToBlob = (base64, mimeType) => {
  950. const byteString = atob(base64.split(",")[1]);
  951. const arrayBuffer = new ArrayBuffer(byteString.length);
  952. const uint8Array = new Uint8Array(arrayBuffer);
  953. for (let i = 0; i < byteString.length; i++) {
  954. uint8Array[i] = byteString.charCodeAt(i);
  955. }
  956. return new Blob([uint8Array], { type: mimeType });
  957. };
  958. // Base64 데이터를 File 객체로 변환
  959. const base64ToFile = (base64, mimeType, fileName) => {
  960. const blob = base64ToBlob(base64, mimeType);
  961. return new File([blob], fileName, { type: mimeType });
  962. };
  963. // 이미지 업로드 처리 (useAxios)
  964. const uploadImage = async (file) => {
  965. const formDataEdt = new FormData();
  966. formDataEdt.append("picObj", file);
  967. return useAxios()
  968. .post("/pic/upload", formDataEdt, {
  969. headers: { "Content-Type": "multipart/form-data" },
  970. })
  971. .then((res) => {
  972. const filePath = res.data.ogn_name.path.replace(/.*\/files\//, "");
  973. const fileName = res.data.ogn_name.file_name;
  974. return `${apiUrl.value}/images/${filePath}/${fileName}`; // 최종 URL 반환
  975. })
  976. .catch((error) => {
  977. console.error("Image upload failed:", error);
  978. return null;
  979. });
  980. };
  981. // 에디터 내용 처리 및 이미지 업로드
  982. const processEditorContent = async (content) => {
  983. const parser = new DOMParser();
  984. const doc = parser.parseFromString(content, "text/html");
  985. const images = doc.querySelectorAll("img");
  986. for (let i = 0; i < images.length; i++) {
  987. const img = images[i];
  988. const src = img.src;
  989. if (src.startsWith("data:image")) {
  990. // MIME 타입과 파일 이름 추출
  991. const mimeType = src.split(";")[0].split(":")[1];
  992. const extension = mimeType.split("/")[1];
  993. const fileName = `image-${i + 1}.${extension}`;
  994. // Base64 데이터를 File 객체로 변환
  995. const file = base64ToFile(src, mimeType, fileName);
  996. // 이미지 업로드 및 URL 반환
  997. const finalUrl = await uploadImage(file);
  998. if (finalUrl) {
  999. img.src = finalUrl; // 이미지 src 업데이트
  1000. }
  1001. }
  1002. }
  1003. return doc.body.innerHTML; // 최종 수정된 HTML 반환
  1004. };
  1005. /*=======================================================================
  1006. | 최종 에디터 이미지 url치환 : E
  1007. /*=======================================================================*/
  1008. /************************************************************************
  1009. | 팝업 이벤트버스 정의
  1010. ************************************************************************/
  1011. $eventBus.off("FN_INSERT");
  1012. $eventBus.on("FN_INSERT", () => {
  1013. fnInsert();
  1014. });
  1015. $eventBus.off("FN_UPDATE");
  1016. $eventBus.on("FN_UPDATE", () => {
  1017. fnUpdate();
  1018. });
  1019. $eventBus.off("FN_DELETE");
  1020. $eventBus.on("FN_DELETE", () => {
  1021. fnDelete();
  1022. });
  1023. /************************************************************************
  1024. | 라이프사이클
  1025. ************************************************************************/
  1026. onMounted(() => {
  1027. pageType.value = useDtStore.boardInfo.pageType;
  1028. if(pageType.value == "I"){
  1029. if(itemType == "G"){
  1030. pageId.value = "공동구매 등록"
  1031. } else {
  1032. pageId.value = "제품 등록"
  1033. }
  1034. } else if(pageType.value == "U"){
  1035. if(itemType == "G"){
  1036. pageId.value = "공동구매 수정"
  1037. } else {
  1038. pageId.value = "제품 수정"
  1039. }
  1040. } else {
  1041. if(itemType == "G"){
  1042. pageId.value = "공동구매 상세"
  1043. } else {
  1044. pageId.value = "제품 상세"
  1045. }
  1046. }
  1047. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  1048. if (pageType.value !== "I") {
  1049. fnDetail();
  1050. }
  1051. });
  1052. /************************************************************************
  1053. | WATCH
  1054. ************************************************************************/
  1055. // 시작일이 변경될 때, 종료일이 시작일보다 이전이면 종료일을 시작일과 같게 설정
  1056. watch(() => form.value.order_start_date, (newStartDate) => {
  1057. if (newStartDate && form.value.order_end_date && form.value.order_end_date < newStartDate) {
  1058. form.value.order_end_date = newStartDate;
  1059. }
  1060. });
  1061. // 종료일이 변경될 때, 종료일이 시작일보다 이전이면 시작일과 같게 설정
  1062. watch(() => form.value.order_end_date, (newEndDate) => {
  1063. if (newEndDate && form.value.order_start_date && newEndDate < form.value.order_start_date) {
  1064. form.value.order_end_date = form.value.order_start_date;
  1065. }
  1066. });
  1067. </script>
  1068. <style scoped>
  1069. .cursor-pointer {
  1070. cursor: pointer;
  1071. }
  1072. .cursor-pointer:hover {
  1073. background-color: #f5f5f5;
  1074. }
  1075. .order-link {
  1076. color: #1976d2;
  1077. text-decoration: none;
  1078. display: inline-flex;
  1079. align-items: center;
  1080. transition: color 0.2s;
  1081. }
  1082. .order-link:hover {
  1083. color: #1565c0;
  1084. text-decoration: underline;
  1085. }
  1086. .no-link {
  1087. color: #999;
  1088. font-style: italic;
  1089. }
  1090. .delete-btn.v-btn--variant-outlined {
  1091. border-color: #f44336 !important;
  1092. }
  1093. /* 데이트피커 관련 스타일 */
  1094. .form-section {
  1095. overflow: visible !important;
  1096. }
  1097. .date-picker-wrapper {
  1098. position: relative;
  1099. z-index: 10;
  1100. }
  1101. .date-picker-group {
  1102. position: relative;
  1103. z-index: 10;
  1104. }
  1105. .date-picker-item {
  1106. position: relative;
  1107. z-index: 10;
  1108. }
  1109. /* VueDatePicker 팝업이 form-section을 벗어나도 보이도록 */
  1110. .modern-date-picker {
  1111. position: relative;
  1112. z-index: 1000;
  1113. }
  1114. /* VueDatePicker 오버레이 스타일 조정 */
  1115. :deep(.dp__overlay) {
  1116. z-index: 1001 !important;
  1117. }
  1118. :deep(.dp__menu) {
  1119. z-index: 1001 !important;
  1120. }
  1121. :deep(.dp__calendar_wrap) {
  1122. z-index: 1001 !important;
  1123. }
  1124. /* form-container와 main-content도 overflow visible로 설정 */
  1125. .form-container {
  1126. overflow: visible !important;
  1127. }
  1128. .main-content {
  1129. overflow: visible !important;
  1130. }
  1131. </style>