add.vue 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  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. formData.append('contact_brd', form.value.contact_brd);
  831. // 벤더사의 COMPANY_NUMBER 사용
  832. const memberCompanyNumber = useAtStore.auth.companyNumber || "1";
  833. formData.append('company_number', memberCompanyNumber);
  834. useAxios()
  835. .post('/item/reg', formData, {
  836. headers: {'Content-Type': 'multipart/form-data'},
  837. })
  838. .then((res) => {
  839. router.push("/view/common/item");
  840. })
  841. .catch((error) => {
  842. })
  843. .finally(() => {
  844. });
  845. };
  846. const fnUpdate = async () => {
  847. let req = {
  848. seq: useDtStore.boardInfo.seq,
  849. };
  850. const formData = new FormData();
  851. formData.append('name', form.value.formValue1);
  852. // formData.append('price1', form.value.formValue2);
  853. // formData.append('price2', form.value.formValue3);
  854. // formData.append('deli_fee', form.value.formValue4);
  855. // if (form.value.formValue5 instanceof File) {
  856. // formData.append('thumb_file', form.value.formValue5);
  857. // }
  858. // formData.append('sub_title', form.value.formValue6);
  859. // formData.append('detail', updatedContent.value);
  860. // if (form.value.formValue7 instanceof File) {
  861. // formData.append('zip_file', form.value.formValue7);
  862. // }
  863. // formData.append('status', form.value.formValue8);
  864. // formData.append('show_yn', form.value.formValue9);
  865. // formData.append('add_info', form.value.formValue10);
  866. formData.append('order_link', form.value.order_link);
  867. formData.append('order_start_date', dayjs(form.value.order_start_date).format('YYYY-MM-DD'));
  868. formData.append('order_end_date', dayjs(form.value.order_end_date).format('YYYY-MM-DD'));
  869. formData.append('contact_inf', form.value.contact_inf);
  870. formData.append('contact_brd', form.value.contact_brd);
  871. // 벤더사의 COMPANY_NUMBER 사용
  872. const memberCompanyNumber = useAtStore.auth.companyNumber || "1";
  873. formData.append('company_number', memberCompanyNumber);
  874. try {
  875. const res = await useAxios().post(`/item/update/${req.seq}`, formData, {
  876. headers: { 'Content-Type': 'multipart/form-data' },
  877. });
  878. router.push("/view/common/item/detail");
  879. } catch (error) {
  880. }
  881. };
  882. const fnDetail = () => {
  883. let req = {
  884. seq: useDtStore.boardInfo.seq,
  885. };
  886. useAxios()
  887. .get(`/item/detail/${req.seq}`)
  888. .then(async (res) => {
  889. form.value.formValue1 = res.data.NAME;
  890. // form.value.formValue2 = res.data.PRICE1;
  891. // form.value.formValue3 = res.data.PRICE2;
  892. // form.value.formValue4 = res.data.DELI_FEE;
  893. // form.value.formValue5 = res.data.THUMB_FILE;
  894. // form.value.formValue6 = res.data.SUB_TITLE;
  895. // zipInfo.value.file_path = res.data.ZIP_FILE;
  896. // zipInfo.value.original_name = res.data.ZIP_FILE_ORIGIN;
  897. //에디터에 컨텐츠 전달
  898. //editorContentReq.value = res.data.DETAIL;
  899. form.value.formValue8 = res.data.STATUS;
  900. form.value.formValue9 = res.data.SHOW_YN;
  901. form.value.formValue10 = res.data.ADD_INFO;
  902. form.value.order_link = res.data.ORDER_LINK;
  903. form.value.order_start_date = res.data.ORDER_START_DATE;
  904. form.value.order_end_date = res.data.ORDER_END_DATE;
  905. form.value.contact_inf = res.data.CONTACT_INF;
  906. form.value.contact_brd = res.data.CONTACT_BRD;
  907. // contact_inf 값이 있으면 인플루언서 이름 조회
  908. if (res.data.CONTACT_INF) {
  909. const infData = await getInfName(res.data.CONTACT_INF);
  910. if (infData) {
  911. form.value.contact_inf_display = infData.NICK_NAME
  912. ? `${infData.NICK_NAME} (${infData.NAME})`
  913. : infData.NAME;
  914. }
  915. }
  916. if (res.data.CONTACT_BRD) {
  917. const brdData = await getBrdName(res.data.CONTACT_BRD);
  918. if (brdData) {
  919. form.value.contact_brd_display = brdData.NAME;
  920. }
  921. }
  922. //썸네일 파일이 있으면 넣어줌
  923. if(form.value.formValue5){
  924. imgTemp.value = `https://shopdeli.mycafe24.com/writable/uploads/item/thumb/${form.value.formValue5}`;
  925. }
  926. })
  927. .catch((error) => {
  928. })
  929. .finally(() => {
  930. });
  931. };
  932. /*=======================================================================
  933. | 최종 에디터 이미지 url치환 : S
  934. /*=======================================================================*/
  935. // const editorContent = async () => {
  936. // const content = sunEditorWrapper.value.getEditorContent();
  937. // updatedContent.value = await processEditorContent(content);
  938. // console.log("Updated content:", updatedContent.value);
  939. // };
  940. // Base64 데이터를 Blob으로 변환
  941. const base64ToBlob = (base64, mimeType) => {
  942. const byteString = atob(base64.split(",")[1]);
  943. const arrayBuffer = new ArrayBuffer(byteString.length);
  944. const uint8Array = new Uint8Array(arrayBuffer);
  945. for (let i = 0; i < byteString.length; i++) {
  946. uint8Array[i] = byteString.charCodeAt(i);
  947. }
  948. return new Blob([uint8Array], { type: mimeType });
  949. };
  950. // Base64 데이터를 File 객체로 변환
  951. const base64ToFile = (base64, mimeType, fileName) => {
  952. const blob = base64ToBlob(base64, mimeType);
  953. return new File([blob], fileName, { type: mimeType });
  954. };
  955. // 이미지 업로드 처리 (useAxios)
  956. const uploadImage = async (file) => {
  957. const formDataEdt = new FormData();
  958. formDataEdt.append("picObj", file);
  959. return useAxios()
  960. .post("/pic/upload", formDataEdt, {
  961. headers: { "Content-Type": "multipart/form-data" },
  962. })
  963. .then((res) => {
  964. const filePath = res.data.ogn_name.path.replace(/.*\/files\//, "");
  965. const fileName = res.data.ogn_name.file_name;
  966. return `${apiUrl.value}/images/${filePath}/${fileName}`; // 최종 URL 반환
  967. })
  968. .catch((error) => {
  969. console.error("Image upload failed:", error);
  970. return null;
  971. });
  972. };
  973. // 에디터 내용 처리 및 이미지 업로드
  974. const processEditorContent = async (content) => {
  975. const parser = new DOMParser();
  976. const doc = parser.parseFromString(content, "text/html");
  977. const images = doc.querySelectorAll("img");
  978. for (let i = 0; i < images.length; i++) {
  979. const img = images[i];
  980. const src = img.src;
  981. if (src.startsWith("data:image")) {
  982. // MIME 타입과 파일 이름 추출
  983. const mimeType = src.split(";")[0].split(":")[1];
  984. const extension = mimeType.split("/")[1];
  985. const fileName = `image-${i + 1}.${extension}`;
  986. // Base64 데이터를 File 객체로 변환
  987. const file = base64ToFile(src, mimeType, fileName);
  988. // 이미지 업로드 및 URL 반환
  989. const finalUrl = await uploadImage(file);
  990. if (finalUrl) {
  991. img.src = finalUrl; // 이미지 src 업데이트
  992. }
  993. }
  994. }
  995. return doc.body.innerHTML; // 최종 수정된 HTML 반환
  996. };
  997. /*=======================================================================
  998. | 최종 에디터 이미지 url치환 : E
  999. /*=======================================================================*/
  1000. /************************************************************************
  1001. | 팝업 이벤트버스 정의
  1002. ************************************************************************/
  1003. $eventBus.off("FN_INSERT");
  1004. $eventBus.on("FN_INSERT", () => {
  1005. fnInsert();
  1006. });
  1007. $eventBus.off("FN_UPDATE");
  1008. $eventBus.on("FN_UPDATE", () => {
  1009. fnUpdate();
  1010. });
  1011. $eventBus.off("FN_DELETE");
  1012. $eventBus.on("FN_DELETE", () => {
  1013. fnDelete();
  1014. });
  1015. /************************************************************************
  1016. | 라이프사이클
  1017. ************************************************************************/
  1018. onMounted(() => {
  1019. pageType.value = useDtStore.boardInfo.pageType;
  1020. if(pageType.value == "I"){
  1021. if(itemType == "G"){
  1022. pageId.value = "공동구매 등록"
  1023. } else {
  1024. pageId.value = "제품 등록"
  1025. }
  1026. } else if(pageType.value == "U"){
  1027. if(itemType == "G"){
  1028. pageId.value = "공동구매 수정"
  1029. } else {
  1030. pageId.value = "제품 수정"
  1031. }
  1032. } else {
  1033. if(itemType == "G"){
  1034. pageId.value = "공동구매 상세"
  1035. } else {
  1036. pageId.value = "제품 상세"
  1037. }
  1038. }
  1039. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  1040. if (pageType.value !== "I") {
  1041. fnDetail();
  1042. }
  1043. });
  1044. /************************************************************************
  1045. | WATCH
  1046. ************************************************************************/
  1047. // 시작일이 변경될 때, 종료일이 시작일보다 이전이면 종료일을 시작일과 같게 설정
  1048. watch(() => form.value.order_start_date, (newStartDate) => {
  1049. if (newStartDate && form.value.order_end_date && form.value.order_end_date < newStartDate) {
  1050. form.value.order_end_date = newStartDate;
  1051. }
  1052. });
  1053. // 종료일이 변경될 때, 종료일이 시작일보다 이전이면 시작일과 같게 설정
  1054. watch(() => form.value.order_end_date, (newEndDate) => {
  1055. if (newEndDate && form.value.order_start_date && newEndDate < form.value.order_start_date) {
  1056. form.value.order_end_date = form.value.order_start_date;
  1057. }
  1058. });
  1059. </script>
  1060. <style scoped>
  1061. .cursor-pointer {
  1062. cursor: pointer;
  1063. }
  1064. .cursor-pointer:hover {
  1065. background-color: #f5f5f5;
  1066. }
  1067. .order-link {
  1068. color: #1976d2;
  1069. text-decoration: none;
  1070. display: inline-flex;
  1071. align-items: center;
  1072. transition: color 0.2s;
  1073. }
  1074. .order-link:hover {
  1075. color: #1565c0;
  1076. text-decoration: underline;
  1077. }
  1078. .no-link {
  1079. color: #999;
  1080. font-style: italic;
  1081. }
  1082. .delete-btn.v-btn--variant-outlined {
  1083. border-color: #f44336 !important;
  1084. }
  1085. </style>