detail.vue 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  1. <template>
  2. <div class="modern-item-add">
  3. <!-- 메인 컨텐츠 -->
  4. <div class="main-content">
  5. <div class="form-container">
  6. <v-form ref="addForm" class="modern-form">
  7. <div class="form-section">
  8. <h3 class="section-title">
  9. <i class="mdi mdi-package-variant"></i>
  10. {{ pageId }}
  11. </h3>
  12. <!-- 제품명 -->
  13. <div class="form-field">
  14. <label class="field-label">
  15. 제품명
  16. </label>
  17. <div class="field-content">
  18. <div class="display-value">
  19. {{ form.formValue1 }}
  20. </div>
  21. </div>
  22. </div>
  23. <div class="form-field">
  24. <label class="field-label">
  25. 판매 기간
  26. </label>
  27. <div class="field-content df--type w--50">
  28. <div class="display-value date-range">
  29. <i class="mdi mdi-calendar-range"></i>
  30. {{ form.order_start_date?.slice(0, 10) }} ~ {{ form.order_end_date?.slice(0, 10) }}
  31. </div>
  32. <v-btn
  33. v-if="form.formValue8 == 0 && memberType !== 'INFLUENCER'"
  34. class="closed-btn"
  35. color="primary"
  36. variant="outlined"
  37. @click="fnCloseEvt"
  38. >
  39. <i class="mdi mdi-power"></i>
  40. 마감
  41. </v-btn>
  42. </div>
  43. </div>
  44. <!-- 인플루언서 (공동구매인 경우) -->
  45. <div class="form-field-group group--2" v-if="memberType !== 'INFLUENCER'">
  46. <div class="form-field">
  47. <label class="field-label">인플루언서</label>
  48. <div class="field-content">
  49. <div v-if="form.contact_inf" class="display-value date-range">
  50. <i class="mdi mdi-account-circle"></i>
  51. {{ form.contact_inf_display }}
  52. </div>
  53. <div v-else class="display-value date-range">
  54. <i class="mdi mdi-account-circle"></i>
  55. 배정된 인플루언서가 없습니다.
  56. </div>
  57. </div>
  58. </div>
  59. <!-- 브랜드사 (공동구매인 경우) -->
  60. <div v-if="memberType !== 'BRAND'" class="form-field">
  61. <label class="field-label">브랜드사</label>
  62. <div class="field-content">
  63. <div v-if="form.contact_brd" class="display-value date-range">
  64. <i class="mdi mdi-domain"></i>
  65. {{ form.contact_brd_display }}
  66. </div>
  67. <div v-else class="display-value date-range">
  68. <i class="mdi mdi-domain"></i>
  69. 배정된 브랜드사가 없습니다.
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. <div class="form-field" v-if="form.order_link">
  75. <label class="field-label">구매 링크</label>
  76. <div class="field-content">
  77. <div v-if="pageType == 'D'" class="display-value link-display">
  78. <a
  79. :href="form.order_link"
  80. target="_blank"
  81. rel="noopener noreferrer"
  82. class="external-link"
  83. >
  84. <i class="mdi mdi-link"></i>
  85. {{ form.order_link }}
  86. <i class="mdi mdi-open-in-new"></i>
  87. </a>
  88. </div>
  89. <v-text-field
  90. v-else
  91. v-model="form.order_link"
  92. class="modern-input"
  93. variant="outlined"
  94. placeholder="공동구매 링크를 입력하세요"
  95. maxlength="200"
  96. hide-details="auto"
  97. ></v-text-field>
  98. </div>
  99. </div>
  100. <div class="form-field">
  101. <div class="field-content">
  102. <div class="btn--actions--wrap pb--rem">
  103. <div class="left--sections">
  104. <label class="mb--0 field-label">주문 내역</label>
  105. </div>
  106. <div class="right--sections">
  107. <div class="caption--wrap">
  108. <i class="ico">!</i>
  109. <div class="caption--box">
  110. - 주문 내역 입력 후 저장 버튼을 꼭 클릭해 주세요.<br />
  111. - 엑셀 파일은 최대 10MB까지 업로드 가능합니다.<br />
  112. - 엑셀 파일의 헤더명(주문번호, 구매자명)이 일치해야 정상적으로 업로드됩니다.
  113. </div>
  114. </div>
  115. <v-btn class="custom-btn btn-white mini" @click="addEmptyRow"
  116. ><i class="ico"></i>항목 추가</v-btn
  117. >
  118. <v-btn class="custom-btn btn-white mini" @click="deleteSelectedRows"
  119. ><i class="ico"></i>항목 삭제</v-btn
  120. >
  121. <input
  122. ref="excelFileInput"
  123. type="file"
  124. accept=".xlsx,.xls"
  125. @change="handleExcelUpload"
  126. style="display: none"
  127. />
  128. <v-btn class="custom-btn btn-excel" @click="$refs.excelFileInput.click()"
  129. ><i class="ico"></i>엑셀 업로드</v-btn
  130. >
  131. <v-btn class="custom-btn btn-excel" @click="downloadExcel"
  132. ><i class="ico"></i>엑셀 다운로드</v-btn
  133. >
  134. <v-btn class="custom-btn btn-purple mini" @click="fnRegEvt"
  135. ><i class="ico"></i>저장</v-btn
  136. >
  137. </div>
  138. </div>
  139. <div class="tbl-wrapper">
  140. <div class="tbl-wrap">
  141. <!-- ag grid -->
  142. <ag-grid-vue
  143. :style="{ width: '100%', height: gridHeight }"
  144. class="ag-theme-quartz order--table"
  145. :gridOptions="gridOptions"
  146. rowSelection="multiple"
  147. :rowData="tblItems"
  148. :paginationPageSize="pageObj.pageSize"
  149. :suppressPaginationPanel="true"
  150. @grid-ready="onGridReady"
  151. @cell-value-changed="onCellValueChanged"
  152. >
  153. </ag-grid-vue>
  154. <!-- 페이징 -->
  155. <!-- <div class="ag-grid-custom-pagenations">
  156. <pagination @chg_page="chgPage" :pageObj="pageObj"></pagination>
  157. </div> -->
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. </div>
  163. </v-form>
  164. </div>
  165. <!-- 액션 버튼 -->
  166. <div class="action-buttons">
  167. <div class="button-group left">
  168. <v-btn
  169. class="action-btn secondary"
  170. variant="outlined"
  171. @click="listLocated"
  172. >
  173. <i class="mdi mdi-format-list-bulleted"></i>
  174. 목록으로
  175. </v-btn>
  176. <v-btn
  177. v-if="memberType !== 'INFLUENCER'"
  178. class="action-btn danger"
  179. variant="outlined"
  180. color="error"
  181. @click="fnDelEvt"
  182. >
  183. <i class="mdi mdi-close-circle"></i>
  184. 삭제
  185. </v-btn>
  186. </div>
  187. <div class="button-group right">
  188. <v-btn
  189. v-if="memberType !== 'INFLUENCER'"
  190. class="action-btn primary"
  191. color="primary"
  192. @click="fnBtnEvt"
  193. >
  194. <i class="mdi mdi-pencil"></i>
  195. 수정하기
  196. </v-btn>
  197. </div>
  198. </div>
  199. </div>
  200. </div>
  201. </template>
  202. <script setup>
  203. import useAxios from "@/composables/useAxios";
  204. import "@vuepic/vue-datepicker/dist/main.css";
  205. import { AgGridVue } from "ag-grid-vue3";
  206. import * as XLSX from "xlsx";
  207. /************************************************************************
  208. | 레이아웃
  209. ************************************************************************/
  210. definePageMeta({
  211. layout: "default",
  212. });
  213. /************************************************************************
  214. | 스토어
  215. ************************************************************************/
  216. const useDtStore = useDetailStore();
  217. const useAtStore = useAuthStore();
  218. /************************************************************************
  219. | 전역
  220. ************************************************************************/
  221. const memberType = useAtStore.auth.memberType;
  222. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  223. const router = useRouter();
  224. const pageId = ref("");
  225. const itemType = useDtStore.boardInfo.itemType;
  226. let pageObj = ref({
  227. page: 1, // 현재 페이지
  228. pageMaxNumSize: 10, // 페이지 숫자 최대 표현 개수
  229. pageSize: 10, // 테이블 조회 데이터 개수
  230. totalCnt: 0, // 전체 페이지
  231. });
  232. const addForm = ref(null);
  233. const form = ref({
  234. formValue1: "",
  235. formValue2: "",
  236. formValue3: "",
  237. formValue4: "",
  238. formValue5: null,
  239. formValue6: "",
  240. formValue7: null,
  241. formValue8: "0",
  242. formValue8Arr: [
  243. { title: "판매중", value: "0" },
  244. { title: "품절", value: "1" },
  245. ],
  246. formValue9: "Y",
  247. formValue9Arr: [
  248. { title: "노출", value: "Y" },
  249. { title: "비노출", value: "N" },
  250. ],
  251. formValue10: "",
  252. contact_inf: "", // 실제 전송될 INFLUENCER_SEQ
  253. contact_inf_display: "", // 화면에 표시될 이름
  254. contact_brd: "", // 실제 전송될 contact_brd
  255. contact_brd_display: "", // 화면에 표시될 브랜드명
  256. order_link: "",
  257. order_start_date: "",
  258. order_end_date: "",
  259. });
  260. const apiUrl = ref("");
  261. apiUrl.value = import.meta.env.VITE_APP_API_URL;
  262. const objProc = ref({
  263. validErrorMessage: "",
  264. });
  265. const pageType = ref("");
  266. // ag-grid 관련 변수
  267. const tblItems = ref([]);
  268. const gridApi = ref(null);
  269. const gridOptions = ref({
  270. columnDefs: [
  271. { checkboxSelection: true, headerCheckboxSelection: true, width: 50, sortable: false, filter: false,},
  272. {
  273. headerName: "No",
  274. valueGetter: (params) => params.api.getDisplayedRowCount() - params.node.rowIndex,
  275. sortable: false,
  276. filter: false,
  277. width: 80,
  278. },
  279. {
  280. headerName: "주문번호",
  281. field: "ORDER_NUMB",
  282. cellStyle: { textAlign: 'center' },
  283. editable: true,
  284. },
  285. {
  286. headerName: "구매자명",
  287. field: "BUYER_NAME",
  288. editable: true,
  289. },
  290. {
  291. headerName: "연락처",
  292. field: "PHONE",
  293. editable: true,
  294. },
  295. // {
  296. // headerName: "배송주소",
  297. // field: "ADDRESS",
  298. // },
  299. {
  300. headerName: "수량",
  301. field: "QTY",
  302. editable: true,
  303. valueFormatter: (params) => {
  304. return params.value ? Number(params.value).toLocaleString() : '0';
  305. }
  306. },
  307. {
  308. headerName: "배송업체",
  309. field: "DELI_COMP",
  310. editable: true,
  311. },
  312. {
  313. headerName: "송장번호",
  314. field: "DELI_NUMB",
  315. editable: true,
  316. },
  317. ],
  318. autoSizeStrategy: {
  319. type: "fitGridWidth", // width맞춤
  320. },
  321. suppressHorizontalScroll: true, // 가로 스크롤 제거
  322. defaultColDef: {
  323. sortable: true,
  324. filter: true,
  325. resizable: false,
  326. },
  327. suppressMovableColumns: true,
  328. suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
  329. rowMultiSelectWithClick: true,
  330. rowSelection: {
  331. checkboxes: true,
  332. headerCheckbox: true,
  333. enableClickSelection: false,
  334. mode: "multiRow",
  335. },
  336. localeText: {
  337. noRowsToShow: '주문 내역이 없습니다.'
  338. }
  339. });
  340. /************************************************************************
  341. | 함수(METHODS)
  342. ************************************************************************/
  343. // 동적 높이 계산
  344. const gridHeight = computed(() => {
  345. const rowCount = tblItems.value.length;
  346. const minRows = 3; // 최소 5줄 높이
  347. const maxRows = 10; // 최대 15줄 높이 (스크롤 시작점)
  348. const rowHeight = 2.94; // rem 단위
  349. if (rowCount <= minRows) {
  350. return `calc(${minRows} * ${rowHeight}rem)`;
  351. } else if (rowCount > maxRows) {
  352. return `calc(${maxRows} * ${rowHeight}rem)`;
  353. } else {
  354. return `calc(${rowCount} * ${rowHeight}rem)`;
  355. }
  356. });
  357. const addEmptyRow = () => {
  358. const newRow = {
  359. BUYER_NAME: "",
  360. ADDRESS: "",
  361. PHONE: "",
  362. EMAIL: "",
  363. QTY: "",
  364. TOTAL: "",
  365. DELI_COMP: "",
  366. DELI_NUMB: "",
  367. ORDER_DATE: "",
  368. };
  369. // 맨 앞에 추가 (unshift 사용)
  370. tblItems.value.unshift(newRow);
  371. pageObj.value.totalCnt = tblItems.value.length;
  372. // ag-grid 데이터 갱신
  373. if (gridApi.value) {
  374. gridApi.value.setGridOption("rowData", tblItems.value);
  375. }
  376. $toast.success("새 항목이 추가되었습니다.");
  377. };
  378. const deleteSelectedRows = () => {
  379. if (!gridApi.value) return;
  380. const selectedRows = gridApi.value.getSelectedRows();
  381. if (selectedRows.length === 0) {
  382. $toast.warning("삭제할 항목을 선택해주세요.");
  383. return;
  384. }
  385. let param = {
  386. id: pageId,
  387. title: pageId,
  388. content: `선택된 ${selectedRows.length}개 항목을 삭제하시겠습니까?`,
  389. yes: {
  390. text: "삭제",
  391. isProc: true,
  392. event: "FN_DELETE_SELECTED",
  393. param: selectedRows,
  394. },
  395. no: {
  396. text: "취소",
  397. isProc: false,
  398. },
  399. };
  400. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  401. };
  402. const fnDeleteSelected = (selectedRows) => {
  403. // 선택된 행들을 tblItems에서 제거
  404. selectedRows.forEach((selectedRow) => {
  405. const index = tblItems.value.findIndex(
  406. (item) =>
  407. item.BUYER_NAME === selectedRow.BUYER_NAME &&
  408. item.ADDRESS === selectedRow.ADDRESS &&
  409. item.PHONE === selectedRow.PHONE &&
  410. item.EMAIL === selectedRow.EMAIL
  411. );
  412. if (index > -1) {
  413. tblItems.value.splice(index, 1);
  414. }
  415. });
  416. pageObj.value.totalCnt = tblItems.value.length;
  417. // ag-grid 데이터 갱신
  418. if (gridApi.value) {
  419. gridApi.value.setGridOption("rowData", tblItems.value);
  420. }
  421. $toast.success(`${selectedRows.length}개 항목이 삭제되었습니다.`);
  422. };
  423. const handleExcelUpload = async (event) => {
  424. const file = event.target.files[0];
  425. if (!file) return;
  426. const errorHandler = useErrorHandler();
  427. // 파일 크기 검증 (10MB)
  428. const maxSize = 10 * 1024 * 1024;
  429. if (file.size > maxSize) {
  430. const sizeError = new Error("파일 크기 초과");
  431. sizeError.name = "FileSizeError";
  432. errorHandler.handleFileError(sizeError, file.name);
  433. event.target.value = "";
  434. return;
  435. }
  436. // 파일 형식 검증
  437. const allowedTypes = [
  438. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  439. "application/vnd.ms-excel",
  440. ];
  441. if (!allowedTypes.includes(file.type)) {
  442. const typeError = new Error("지원하지 않는 파일 형식");
  443. typeError.name = "FileTypeError";
  444. errorHandler.handleFileError(typeError, file.name);
  445. event.target.value = "";
  446. return;
  447. }
  448. const reader = new FileReader();
  449. reader.onload = async (e) => {
  450. try {
  451. const data = new Uint8Array(e.target.result);
  452. const workbook = XLSX.read(data, { type: "array", cellDates: true });
  453. const sheetName = workbook.SheetNames[0];
  454. const worksheet = workbook.Sheets[sheetName];
  455. const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
  456. if (jsonData.length < 2) {
  457. $toast.error("엑셀 파일에 데이터가 없습니다. 헤더와 최소 1개 이상의 데이터 행이 필요합니다.");
  458. return;
  459. }
  460. const headers = jsonData[0];
  461. const rows = jsonData.slice(1);
  462. // 헤더 매핑 (다양한 형태의 헤더명 지원)
  463. const headerMapping = {
  464. "주문번호": "ORDER_NUMB",
  465. "구매자명": "BUYER_NAME",
  466. "구매자이름": "BUYER_NAME",
  467. "구매자성명": "BUYER_NAME",
  468. "연락처": "PHONE",
  469. "수량": "QTY",
  470. "배송업체": "DELI_COMP",
  471. "택배사": "DELI_COMP",
  472. "송장번호": "DELI_NUMB"
  473. };
  474. // 필수 헤더 검증 (like 검색으로 변경)
  475. const requiredHeaders = ["주문번호", "구매자명"];
  476. const foundHeaders = headers.filter(header =>
  477. requiredHeaders.some(required =>
  478. (required === "주문번호" && header.includes("주문번호")) ||
  479. (required === "구매자명" && (
  480. header === "구매자" || header === "수취인" ||
  481. (header.includes("구매자") && (header.includes("이름") || header.includes("성함") || header.includes("명"))) ||
  482. (header.includes("수취인") && (header.includes("이름") || header.includes("성함") || header.includes("명")))
  483. ))
  484. )
  485. );
  486. if (foundHeaders.length < requiredHeaders.length) {
  487. $toast.error(`필수 헤더가 누락되었습니다. 필요한 헤더: ${requiredHeaders.join(", ")}`);
  488. return;
  489. }
  490. // 데이터 변환
  491. const mappedData = rows
  492. .map((row, rowIndex) => {
  493. const mappedRow = {};
  494. let hasValidData = false;
  495. headers.forEach((header, index) => {
  496. let fieldName = null;
  497. // 헤더 매핑 로직
  498. if (header.includes("주문번호")) fieldName = "ORDER_NUMB";
  499. else if (header === "구매자" || header === "수취인" ||
  500. (header.includes("구매자") && (header.includes("이름") || header.includes("성함") || header.includes("명"))) ||
  501. (header.includes("수취인") && (header.includes("이름") || header.includes("성함") || header.includes("명")))) {
  502. fieldName = "BUYER_NAME";
  503. }
  504. else if (header.includes("연락처")) fieldName = "PHONE";
  505. else if (header.includes("수량")) fieldName = "QTY";
  506. else if (header.includes("배송업체") || header.includes("택배사")) fieldName = "DELI_COMP";
  507. else if (header.includes("송장")) fieldName = "DELI_NUMB";
  508. if (fieldName && row[index] !== undefined && row[index] !== "") {
  509. mappedRow[fieldName] = row[index].toString().trim();
  510. hasValidData = true;
  511. }
  512. });
  513. // 필수 필드 검증
  514. if (hasValidData) {
  515. const missingFields = [];
  516. if (!mappedRow.ORDER_NUMB) missingFields.push("주문번호");
  517. if (!mappedRow.BUYER_NAME) missingFields.push("구매자명");
  518. if (missingFields.length > 0) {
  519. console.warn(`${rowIndex + 2}행: 필수 필드 누락 - ${missingFields.join(", ")}`);
  520. return null;
  521. }
  522. }
  523. return hasValidData ? mappedRow : null;
  524. })
  525. .filter((row) => row !== null);
  526. if (mappedData.length === 0) {
  527. $toast.error("매핑 가능한 데이터가 없습니다. 엑셀 헤더명과 데이터를 확인해주세요.");
  528. return;
  529. }
  530. // 기존 데이터 업데이트 및 신규 데이터 추가 처리
  531. let updatedCount = 0;
  532. let newCount = 0;
  533. mappedData.forEach(newItem => {
  534. // 기존 데이터에서 주문번호 + 구매자명이 일치하는 항목 찾기
  535. const existingIndex = tblItems.value.findIndex(existingItem =>
  536. existingItem.ORDER_NUMB === newItem.ORDER_NUMB &&
  537. existingItem.BUYER_NAME === newItem.BUYER_NAME
  538. );
  539. if (existingIndex !== -1) {
  540. // 업데이트 메타데이터 추가
  541. newItem._metadata = {
  542. isUpdated: true,
  543. isNew: false,
  544. originalCreatedAt: tblItems.value[existingIndex].created_at || tblItems.value[existingIndex]._metadata?.originalCreatedAt,
  545. lastModifiedAt: new Date().toISOString()
  546. };
  547. // 기존 데이터 업데이트
  548. tblItems.value[existingIndex] = { ...tblItems.value[existingIndex], ...newItem };
  549. updatedCount++;
  550. } else {
  551. // 신규 메타데이터 추가
  552. newItem._metadata = {
  553. isUpdated: false,
  554. isNew: true,
  555. originalCreatedAt: new Date().toISOString(),
  556. lastModifiedAt: new Date().toISOString()
  557. };
  558. // 신규 데이터 추가
  559. tblItems.value.push(newItem);
  560. newCount++;
  561. }
  562. });
  563. pageObj.value.totalCnt = tblItems.value.length;
  564. // ag-grid 데이터 갱신
  565. if (gridApi.value) {
  566. gridApi.value.setGridOption("rowData", tblItems.value);
  567. }
  568. // 결과 메시지 표시
  569. if (updatedCount > 0 && newCount > 0) {
  570. $toast.success(`총 ${mappedData.length}건 처리완료 (업데이트: ${updatedCount}건, 신규추가: ${newCount}건)`);
  571. } else if (updatedCount > 0) {
  572. $toast.success(`${updatedCount}건의 기존 주문이 업데이트되었습니다.`);
  573. } else {
  574. $toast.success(`${newCount}건의 주문 내역이 추가되었습니다.`);
  575. }
  576. // 파일 입력 초기화
  577. event.target.value = "";
  578. } catch (error) {
  579. console.error("엑셀 파일 처리 중 오류:", error);
  580. $toast.error("엑셀 파일을 읽는 중 오류가 발생했습니다. 파일 형식을 확인해주세요.");
  581. }
  582. };
  583. reader.onerror = () => {
  584. $toast.error("파일을 읽는 중 오류가 발생했습니다.");
  585. };
  586. reader.readAsArrayBuffer(file);
  587. }
  588. const downloadExcel = () => {
  589. if (!tblItems.value || tblItems.value.length === 0) {
  590. $toast.warning("다운로드할 데이터가 없습니다.");
  591. return;
  592. }
  593. // 한글 헤더명 배열
  594. const headers = [
  595. "주문번호",
  596. "구매자명",
  597. "연락처",
  598. "구매수량",
  599. "배송업체",
  600. "송장번호",
  601. ];
  602. // 데이터를 엑셀 형식으로 변환
  603. const excelData = tblItems.value.map((item) => [
  604. item.ORDER_NUMB || "",
  605. item.BUYER_NAME || "",
  606. item.PHONE || "",
  607. item.QTY || "",
  608. item.DELI_COMP || "",
  609. item.DELI_NUMB || "",
  610. ]);
  611. // 헤더를 첫 번째 행에 추가
  612. const worksheetData = [headers, ...excelData];
  613. // 워크시트 생성
  614. const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
  615. // 워크북 생성
  616. const workbook = XLSX.utils.book_new();
  617. XLSX.utils.book_append_sheet(workbook, worksheet, "배송관리");
  618. // 파일명 생성 (현재 날짜 포함)
  619. const today = new Date();
  620. const dateString =
  621. today.getFullYear() +
  622. String(today.getMonth() + 1).padStart(2, "0") +
  623. String(today.getDate()).padStart(2, "0");
  624. const fileName = `배송관리_${dateString}.xlsx`;
  625. // 엑셀 파일 다운로드
  626. XLSX.writeFile(workbook, fileName);
  627. $toast.success("엑셀 파일이 다운로드되었습니다.");
  628. };
  629. const listLocated = () => {
  630. router.push({
  631. path: "/view/common/item",
  632. });
  633. useDtStore.boardInfo.itemType = itemType;
  634. };
  635. // 인플루언서 이름 조회
  636. const getInfName = async (contact_inf) => {
  637. try {
  638. // contact_inf 값이 있을 때만 API 호출
  639. if (!contact_inf) {
  640. return null;
  641. }
  642. const response = await useAxios().get(`/user/getInfName/${contact_inf}`);
  643. if (response.data?.status === 'success') {
  644. return response.data.data;
  645. } else {
  646. return null;
  647. }
  648. } catch (error) {
  649. return null;
  650. }
  651. };
  652. // 브랜드사 조회
  653. const getBrdName = async (contact_brd) => {
  654. try {
  655. if (!contact_brd) {
  656. return null;
  657. }
  658. const response = await useAxios().get(`/user/getBrdName/${contact_brd}`);
  659. if (response.data?.status === 'success') {
  660. return response.data.data;
  661. } else {
  662. return null;
  663. }
  664. } catch (error) {
  665. return null;
  666. }
  667. };
  668. /*======================================================================
  669. | 작성 시퀀스
  670. | 1. 작성 컨펌
  671. | 2. 버튼 체크
  672. | 3. 등록시 -> 등록 API 호출
  673. ======================================================================*/
  674. const fnBtnEvt = () => {
  675. //await editorContent();
  676. router.push({
  677. path: "/view/common/item/add",
  678. });
  679. useDtStore.boardInfo.itemType = itemType;
  680. useDtStore.boardInfo.pageType = "U";
  681. };
  682. const fnDelEvt = () => {
  683. let param = {
  684. id: pageId,
  685. title: pageId,
  686. content: "공동구매를 종료하시겠습니까?",
  687. yes: {
  688. text: "확인",
  689. isProc: true,
  690. event: "FN_DELETE",
  691. param: "",
  692. },
  693. no: {
  694. text: "취소",
  695. isProc: false,
  696. },
  697. };
  698. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  699. };
  700. const fnCloseEvt = () => {
  701. let param = {
  702. id: pageId,
  703. title: pageId,
  704. content: "마감하시겠습니까?",
  705. yes: {
  706. text: "확인",
  707. isProc: true,
  708. event: "FN_CLOSE",
  709. param: "",
  710. },
  711. no: {
  712. text: "취소",
  713. isProc: false,
  714. },
  715. };
  716. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  717. };
  718. const fnClose = () => {
  719. let req = {
  720. seq: useDtStore.boardInfo.seq,
  721. };
  722. useAxios()
  723. .post(`/item/close/${req.seq}`)
  724. .then((res) => {
  725. router.push("/view/common/item");
  726. })
  727. .catch((error) => {
  728. })
  729. .finally(() => {
  730. });
  731. };
  732. const fnDelete = () => {
  733. let req = {
  734. seq: useDtStore.boardInfo.seq,
  735. };
  736. useAxios()
  737. .post(`/item/delete/${req.seq}`)
  738. .then((res) => {
  739. router.push("/view/common/item");
  740. })
  741. .catch((error) => {
  742. })
  743. .finally(() => {
  744. });
  745. };
  746. const getOrderList = () => {
  747. let req = {
  748. MEMBER_TYPE: memberType,
  749. COMPANY_NUMBER: useAtStore.auth.companyNumber || "1",
  750. INF_SEQ: useAtStore.auth.seq,
  751. TYPE: itemType,
  752. ITEM_SEQ: useDtStore.boardInfo.seq // 특정 아이템의 주문만 조회하려면 추가
  753. };
  754. useAxios()
  755. .get(`/deli/orderList/${req.ITEM_SEQ}`, req)
  756. .then(async (res) => {
  757. // 특정 아이템의 주문만 필터링
  758. const filteredData = res.data.filter(item => item.ITEM_SEQ == useDtStore.boardInfo.seq);
  759. tblItems.value = filteredData;
  760. pageObj.value.totalCnt = filteredData.length;
  761. })
  762. .catch((error) => {
  763. })
  764. .finally(() => {
  765. });
  766. };
  767. // ag-grid 관련 함수들
  768. const onGridReady = (params) => {
  769. gridApi.value = params.api;
  770. };
  771. const onCellValueChanged = (params) => {
  772. console.log('셀 값 변경:', params);
  773. };
  774. const chgPage = (page) => {
  775. pageObj.value.page = page;
  776. // 페이징이 필요한 경우 여기에 추가 로직 구현
  777. };
  778. const fnRegEvt = () => {
  779. let param = {
  780. id: pageId,
  781. title: pageId,
  782. content: "주문 내역을 저장하시겠습니까?",
  783. yes: {
  784. text: "확인",
  785. isProc: true,
  786. event: "FN_INSERT",
  787. param: "",
  788. },
  789. no: {
  790. text: "취소",
  791. isProc: false,
  792. },
  793. };
  794. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  795. };
  796. const fnDetail = () => {
  797. let req = {
  798. seq: useDtStore.boardInfo.seq,
  799. };
  800. useAxios()
  801. .get(`/item/detail/${req.seq}`)
  802. .then(async (res) => {
  803. form.value.formValue1 = res.data.NAME;
  804. form.value.formValue8 = res.data.STATUS;
  805. form.value.formValue9 = res.data.SHOW_YN;
  806. form.value.formValue10 = res.data.ADD_INFO;
  807. form.value.order_link = res.data.ORDER_LINK;
  808. form.value.order_start_date = res.data.ORDER_START_DATE;
  809. form.value.order_end_date = res.data.ORDER_END_DATE;
  810. form.value.contact_inf = res.data.CONTACT_INF;
  811. form.value.contact_brd = res.data.CONTACT_BRD;
  812. // contact_inf 값이 있으면 인플루언서 이름 조회
  813. if (res.data.CONTACT_INF) {
  814. const infData = await getInfName(res.data.CONTACT_INF);
  815. if (infData) {
  816. form.value.contact_inf_display = infData.NICK_NAME
  817. ? `${infData.NICK_NAME} (${infData.NAME})`
  818. : infData.NAME;
  819. }
  820. }
  821. if (res.data.CONTACT_BRD) {
  822. const brdData = await getBrdName(res.data.CONTACT_BRD);
  823. if (brdData) {
  824. form.value.contact_brd_display = brdData.NAME;
  825. }
  826. }
  827. getOrderList();
  828. })
  829. .catch((error) => {
  830. })
  831. .finally(() => {
  832. });
  833. };
  834. const fnInsert = async () => {
  835. try {
  836. const req = {
  837. item_seq: useDtStore.boardInfo.seq,
  838. orderList: tblItems.value || []
  839. };
  840. const response = await useAxios().post('/deli/reg', req);
  841. console.log('응답 상태:', response.status);
  842. console.log('전체 응답:', response);
  843. // 응답 상태가 400대 에러면 에러로 처리
  844. if (response.status >= 400) {
  845. throw new Error(response.data?.message || '서버 에러가 발생했습니다.');
  846. }
  847. if (response.data) {
  848. const { message, updated_count, new_count, deleted_count, errors } = response.data;
  849. console.log('백엔드 응답:', response.data);
  850. // 백엔드에서 "저장할 데이터가 없습니다" 메시지가 온 경우
  851. if (message === '저장할 데이터가 없습니다.') {
  852. console.log('저장할 데이터 없음 감지');
  853. $toast.warning("저장할 데이터가 없습니다.");
  854. return; // 새로고침하지 않음
  855. }
  856. // 결과 메시지 표시
  857. const totalProcessed = updated_count + new_count + deleted_count;
  858. if (totalProcessed === 0) {
  859. $toast.success("주문 내역이 저장되었습니다.");
  860. } else {
  861. let message = `주문 내역이 저장되었습니다.`;
  862. $toast.success(message);
  863. }
  864. // 에러가 있으면 콘솔에 출력하고 토스트로 표시
  865. if (errors && errors.length > 0) {
  866. console.warn('저장 중 일부 오류 발생:', errors);
  867. errors.forEach(error => {
  868. $toast.error(error);
  869. });
  870. }
  871. // 저장 후 페이지 새로고침
  872. window.location.reload();
  873. }
  874. } catch (error) {
  875. console.error('주문 내역 저장 중 오류:', error);
  876. // 백엔드 에러 응답 확인
  877. if (error.response?.data) {
  878. const errorData = error.response.data;
  879. console.log('에러 응답:', errorData);
  880. // "처리할 수 있는 데이터가 없습니다" 에러를 저장할 데이터 없음으로 처리
  881. if (errorData.messages?.error === "처리할 수 있는 데이터가 없습니다." ||
  882. errorData.message === "처리할 수 있는 데이터가 없습니다.") {
  883. $toast.warning("저장할 데이터가 없습니다.");
  884. return; // 새로고침하지 않음
  885. }
  886. // 다른 에러 메시지
  887. if (errorData.message) {
  888. $toast.error(errorData.message);
  889. } else if (errorData.messages?.error) {
  890. $toast.error(errorData.messages.error);
  891. } else {
  892. $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
  893. }
  894. } else {
  895. $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
  896. }
  897. }
  898. };
  899. /************************************************************************
  900. | 팝업 이벤트버스 정의
  901. ************************************************************************/
  902. $eventBus.off("FN_DELETE");
  903. $eventBus.on("FN_DELETE", () => {
  904. fnDelete();
  905. });
  906. $eventBus.off("FN_CLOSE");
  907. $eventBus.on("FN_CLOSE", () => {
  908. fnClose();
  909. });
  910. $eventBus.off("FN_INSERT");
  911. $eventBus.on("FN_INSERT", () => {
  912. fnInsert();
  913. });
  914. $eventBus.off("FN_DELETE_SELECTED");
  915. $eventBus.on("FN_DELETE_SELECTED", (selectedRows) => {
  916. fnDeleteSelected(selectedRows);
  917. });
  918. /************************************************************************
  919. | 라이프사이클
  920. ************************************************************************/
  921. onMounted(() => {
  922. pageType.value = "D";
  923. if(pageType.value == "I"){
  924. if(itemType == "G"){
  925. pageId.value = "공동구매 등록"
  926. } else {
  927. pageId.value = "제품 등록"
  928. }
  929. } else if(pageType.value == "U"){
  930. if(itemType == "G"){
  931. pageId.value = "공동구매 수정"
  932. } else {
  933. pageId.value = "제품 수정"
  934. }
  935. } else {
  936. if(itemType == "G"){
  937. pageId.value = "공동구매 상세"
  938. } else {
  939. pageId.value = "제품 상세"
  940. }
  941. }
  942. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  943. if (pageType.value !== "I") {
  944. fnDetail();
  945. }
  946. });
  947. </script>
  948. <style scoped>
  949. .cursor-pointer {
  950. cursor: pointer;
  951. }
  952. .cursor-pointer:hover {
  953. background-color: #f5f5f5;
  954. }
  955. .order-link {
  956. color: #1976d2;
  957. text-decoration: none;
  958. display: inline-flex;
  959. align-items: center;
  960. transition: color 0.2s;
  961. }
  962. .order-link:hover {
  963. color: #1565c0;
  964. text-decoration: underline;
  965. }
  966. .no-link {
  967. color: #999;
  968. font-style: italic;
  969. }
  970. </style>