detail.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068
  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. inf_seq: form.value.contact_inf,
  839. orderList: tblItems.value || []
  840. };
  841. const response = await useAxios().post('/deli/reg', req);
  842. if (response.data) {
  843. const { message, updated_count, new_count, deleted_count, errors } = response.data;
  844. // 결과 메시지 표시
  845. const totalProcessed = updated_count + new_count + deleted_count;
  846. if (totalProcessed === 0) {
  847. $toast.success("주문 내역이 저장되었습니다.");
  848. } else {
  849. let message = `주문 내역이 저장되었습니다.`;
  850. $toast.success(message);
  851. }
  852. // 에러가 있으면 콘솔에 출력하고 토스트로 표시
  853. if (errors && errors.length > 0) {
  854. //console.warn('저장 중 일부 오류 발생:', errors);
  855. errors.forEach(error => {
  856. $toast.error(error);
  857. });
  858. }
  859. // 저장 후 페이지 새로고침
  860. window.location.reload();
  861. }
  862. } catch (error) {
  863. //console.error('주문 내역 저장 중 오류:', error);
  864. // 백엔드 에러 응답 확인
  865. if (error.response?.data) {
  866. const errorData = error.response.data;
  867. // 아무 작업 없이 저장 버튼 클릭 시
  868. if (errorData.messages?.error === "처리할 수 있는 데이터가 없습니다." ||
  869. errorData.message === "처리할 수 있는 데이터가 없습니다.") {
  870. $toast.warning("저장할 데이터가 없습니다.");
  871. return; // 새로고침하지 않음
  872. }
  873. // 다른 에러 메시지
  874. if (errorData.message) {
  875. $toast.error(errorData.message);
  876. } else if (errorData.messages?.error) {
  877. $toast.error(errorData.messages.error);
  878. } else {
  879. $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
  880. }
  881. } else {
  882. $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
  883. }
  884. }
  885. };
  886. /************************************************************************
  887. | 팝업 이벤트버스 정의
  888. ************************************************************************/
  889. $eventBus.off("FN_DELETE");
  890. $eventBus.on("FN_DELETE", () => {
  891. fnDelete();
  892. });
  893. $eventBus.off("FN_CLOSE");
  894. $eventBus.on("FN_CLOSE", () => {
  895. fnClose();
  896. });
  897. $eventBus.off("FN_INSERT");
  898. $eventBus.on("FN_INSERT", () => {
  899. fnInsert();
  900. });
  901. $eventBus.off("FN_DELETE_SELECTED");
  902. $eventBus.on("FN_DELETE_SELECTED", (selectedRows) => {
  903. fnDeleteSelected(selectedRows);
  904. });
  905. /************************************************************************
  906. | 라이프사이클
  907. ************************************************************************/
  908. onMounted(() => {
  909. pageType.value = "D";
  910. if(pageType.value == "I"){
  911. if(itemType == "G"){
  912. pageId.value = "공동구매 등록"
  913. } else {
  914. pageId.value = "제품 등록"
  915. }
  916. } else if(pageType.value == "U"){
  917. if(itemType == "G"){
  918. pageId.value = "공동구매 수정"
  919. } else {
  920. pageId.value = "제품 수정"
  921. }
  922. } else {
  923. if(itemType == "G"){
  924. pageId.value = "공동구매 상세"
  925. } else {
  926. pageId.value = "제품 상세"
  927. }
  928. }
  929. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  930. if (pageType.value !== "I") {
  931. fnDetail();
  932. }
  933. });
  934. </script>
  935. <style scoped>
  936. .cursor-pointer {
  937. cursor: pointer;
  938. }
  939. .cursor-pointer:hover {
  940. background-color: #f5f5f5;
  941. }
  942. .order-link {
  943. color: #1976d2;
  944. text-decoration: none;
  945. display: inline-flex;
  946. align-items: center;
  947. transition: color 0.2s;
  948. }
  949. .order-link:hover {
  950. color: #1565c0;
  951. text-decoration: underline;
  952. }
  953. .no-link {
  954. color: #999;
  955. font-style: italic;
  956. }
  957. </style>