add.vue 36 KB

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