detail.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  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. width: 120,
  290. },
  291. {
  292. headerName: "연락처",
  293. field: "PHONE",
  294. editable: true,
  295. width: 150,
  296. },
  297. {
  298. headerName: "주소",
  299. field: "ADDRESS",
  300. editable: true,
  301. resizable: true,
  302. },
  303. {
  304. headerName: "수량",
  305. field: "QTY",
  306. width: 100,
  307. editable: true,
  308. valueFormatter: (params) => {
  309. return params.value ? Number(params.value).toLocaleString() : '0';
  310. }
  311. },
  312. {
  313. headerName: "배송업체",
  314. field: "DELI_COMP",
  315. editable: true,
  316. width: 150,
  317. },
  318. {
  319. headerName: "송장번호",
  320. field: "DELI_NUMB",
  321. editable: true,
  322. },
  323. ],
  324. autoSizeStrategy: {
  325. type: "fitGridWidth", // width맞춤
  326. },
  327. suppressHorizontalScroll: false, // 가로 스크롤 제거
  328. defaultColDef: {
  329. sortable: true,
  330. filter: true,
  331. resizable: false,
  332. },
  333. suppressMovableColumns: true,
  334. suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
  335. rowMultiSelectWithClick: true,
  336. rowSelection: {
  337. checkboxes: true,
  338. headerCheckbox: true,
  339. enableClickSelection: false,
  340. mode: "multiRow",
  341. },
  342. localeText: {
  343. noRowsToShow: '주문 내역이 없습니다.'
  344. }
  345. });
  346. /************************************************************************
  347. | 함수(METHODS)
  348. ************************************************************************/
  349. // 동적 높이 계산
  350. const gridHeight = computed(() => {
  351. const rowCount = tblItems.value.length;
  352. const minRows = 3; // 최소 5줄 높이
  353. const maxRows = 10; // 최대 15줄 높이 (스크롤 시작점)
  354. const rowHeight = 2.94; // rem 단위
  355. if (rowCount <= minRows) {
  356. return `calc(${minRows} * ${rowHeight}rem)`;
  357. } else if (rowCount > maxRows) {
  358. return `calc(${maxRows} * ${rowHeight}rem)`;
  359. } else {
  360. return `calc(${rowCount} * ${rowHeight}rem)`;
  361. }
  362. });
  363. const addEmptyRow = () => {
  364. const newRow = {
  365. BUYER_NAME: "",
  366. ADDRESS: "",
  367. PHONE: "",
  368. EMAIL: "",
  369. QTY: "",
  370. TOTAL: "",
  371. DELI_COMP: "",
  372. DELI_ADDR: "",
  373. DELI_NUMB: "",
  374. ORDER_DATE: "",
  375. };
  376. // 맨 앞에 추가 (unshift 사용)
  377. tblItems.value.unshift(newRow);
  378. pageObj.value.totalCnt = tblItems.value.length;
  379. // ag-grid 데이터 갱신
  380. if (gridApi.value) {
  381. gridApi.value.setGridOption("rowData", tblItems.value);
  382. }
  383. $toast.success("새 항목이 추가되었습니다.");
  384. };
  385. const deleteSelectedRows = () => {
  386. if (!gridApi.value) return;
  387. const selectedRows = gridApi.value.getSelectedRows();
  388. if (selectedRows.length === 0) {
  389. $toast.warning("삭제할 항목을 선택해주세요.");
  390. return;
  391. }
  392. let param = {
  393. id: pageId,
  394. title: pageId,
  395. content: `선택된 ${selectedRows.length}개 항목을 삭제하시겠습니까?`,
  396. yes: {
  397. text: "삭제",
  398. isProc: true,
  399. event: "FN_DELETE_SELECTED",
  400. param: selectedRows,
  401. },
  402. no: {
  403. text: "취소",
  404. isProc: false,
  405. },
  406. };
  407. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  408. };
  409. const fnDeleteSelected = (selectedRows) => {
  410. // 선택된 행들을 tblItems에서 제거
  411. selectedRows.forEach((selectedRow) => {
  412. const index = tblItems.value.findIndex(
  413. (item) =>
  414. item.BUYER_NAME === selectedRow.BUYER_NAME &&
  415. item.ADDRESS === selectedRow.ADDRESS &&
  416. item.PHONE === selectedRow.PHONE &&
  417. item.EMAIL === selectedRow.EMAIL
  418. );
  419. if (index > -1) {
  420. tblItems.value.splice(index, 1);
  421. }
  422. });
  423. pageObj.value.totalCnt = tblItems.value.length;
  424. // ag-grid 데이터 갱신
  425. if (gridApi.value) {
  426. gridApi.value.setGridOption("rowData", tblItems.value);
  427. }
  428. $toast.success(`${selectedRows.length}개 항목이 삭제되었습니다.`);
  429. };
  430. const handleExcelUpload = async (event) => {
  431. const file = event.target.files[0];
  432. if (!file) return;
  433. const errorHandler = useErrorHandler();
  434. // 파일 크기 검증 (10MB)
  435. const maxSize = 10 * 1024 * 1024;
  436. if (file.size > maxSize) {
  437. const sizeError = new Error("파일 크기 초과");
  438. sizeError.name = "FileSizeError";
  439. errorHandler.handleFileError(sizeError, file.name);
  440. event.target.value = "";
  441. return;
  442. }
  443. // 파일 형식 검증
  444. const allowedTypes = [
  445. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  446. "application/vnd.ms-excel",
  447. ];
  448. if (!allowedTypes.includes(file.type)) {
  449. const typeError = new Error("지원하지 않는 파일 형식");
  450. typeError.name = "FileTypeError";
  451. errorHandler.handleFileError(typeError, file.name);
  452. event.target.value = "";
  453. return;
  454. }
  455. const reader = new FileReader();
  456. reader.onload = async (e) => {
  457. try {
  458. const data = new Uint8Array(e.target.result);
  459. const workbook = XLSX.read(data, { type: "array", cellDates: true });
  460. const sheetName = workbook.SheetNames[0];
  461. const worksheet = workbook.Sheets[sheetName];
  462. const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
  463. if (jsonData.length < 2) {
  464. $toast.error("엑셀 파일에 데이터가 없습니다. 헤더와 최소 1개 이상의 데이터 행이 필요합니다.");
  465. return;
  466. }
  467. const headers = jsonData[0];
  468. const rows = jsonData.slice(1);
  469. // 헤더 매핑 (다양한 형태의 헤더명 지원)
  470. const headerMapping = {
  471. "주문번호": "ORDER_NUMB",
  472. "구매자명": "BUYER_NAME",
  473. "구매자이름": "BUYER_NAME",
  474. "구매자성명": "BUYER_NAME",
  475. "연락처": "PHONE",
  476. "수량": "QTY",
  477. "배송업체": "DELI_COMP",
  478. "택배사": "DELI_COMP",
  479. "주소": "ADDRESS",
  480. "배송주소": "ADDRESS",
  481. "배송지": "ADDRESS",
  482. "수령주소": "ADDRESS",
  483. "수령지": "ADDRESS",
  484. "받는주소": "ADDRESS",
  485. "수취주소": "ADDRESS",
  486. "송장번호": "DELI_NUMB"
  487. };
  488. // 필수 헤더 검증 (like 검색으로 변경)
  489. const requiredHeaders = ["주문번호", "구매자명"];
  490. const foundHeaders = headers.filter(header =>
  491. requiredHeaders.some(required =>
  492. (required === "주문번호" && header.includes("주문번호")) ||
  493. (required === "구매자명" && (
  494. header === "구매자" || header === "수취인" ||
  495. (header.includes("구매자") && (header.includes("이름") || header.includes("성함") || header.includes("명"))) ||
  496. (header.includes("수취인") && (header.includes("이름") || header.includes("성함") || header.includes("명")))
  497. ))
  498. )
  499. );
  500. if (foundHeaders.length < requiredHeaders.length) {
  501. $toast.error(`필수 헤더가 누락되었습니다. 필요한 헤더: ${requiredHeaders.join(", ")}`);
  502. return;
  503. }
  504. // 데이터 변환
  505. const mappedData = rows
  506. .map((row, rowIndex) => {
  507. const mappedRow = {};
  508. let hasValidData = false;
  509. headers.forEach((header, index) => {
  510. let fieldName = null;
  511. // 헤더 매핑 로직
  512. if (header.includes("주문번호")) fieldName = "ORDER_NUMB";
  513. else if (header === "구매자" || header === "수취인" ||
  514. (header.includes("구매자") && (header.includes("이름") || header.includes("성함") || header.includes("명"))) ||
  515. (header.includes("수취인") && (header.includes("이름") || header.includes("성함") || header.includes("명")))) {
  516. fieldName = "BUYER_NAME";
  517. }
  518. else if (header.includes("연락처")) fieldName = "PHONE";
  519. else if (header.includes("수량")) fieldName = "QTY";
  520. else if (header.includes("배송업체") || header.includes("택배사")) fieldName = "DELI_COMP";
  521. else if (header === "주소" || header.includes("배송주소") || header.includes("배송지") ||
  522. header.includes("수령주소") || header.includes("수령지") || header.includes("받는주소") ||
  523. header.includes("수취주소")) fieldName = "ADDRESS";
  524. else if (header.includes("송장")) fieldName = "DELI_NUMB";
  525. if (fieldName && row[index] !== undefined && row[index] !== "") {
  526. mappedRow[fieldName] = row[index].toString().trim();
  527. hasValidData = true;
  528. }
  529. });
  530. // 필수 필드 검증
  531. if (hasValidData) {
  532. const missingFields = [];
  533. if (!mappedRow.ORDER_NUMB) missingFields.push("주문번호");
  534. if (!mappedRow.BUYER_NAME) missingFields.push("구매자명");
  535. if (missingFields.length > 0) {
  536. console.warn(`${rowIndex + 2}행: 필수 필드 누락 - ${missingFields.join(", ")}`);
  537. return null;
  538. }
  539. }
  540. return hasValidData ? mappedRow : null;
  541. })
  542. .filter((row) => row !== null);
  543. if (mappedData.length === 0) {
  544. $toast.error("매핑 가능한 데이터가 없습니다. 엑셀 헤더명과 데이터를 확인해주세요.");
  545. return;
  546. }
  547. // 기존 데이터 업데이트 및 신규 데이터 추가 처리
  548. let updatedCount = 0;
  549. let newCount = 0;
  550. mappedData.forEach(newItem => {
  551. // 기존 데이터에서 주문번호 + 구매자명이 일치하는 항목 찾기
  552. const existingIndex = tblItems.value.findIndex(existingItem =>
  553. existingItem.ORDER_NUMB === newItem.ORDER_NUMB &&
  554. existingItem.BUYER_NAME === newItem.BUYER_NAME
  555. );
  556. if (existingIndex !== -1) {
  557. // 업데이트 메타데이터 추가
  558. newItem._metadata = {
  559. isUpdated: true,
  560. isNew: false,
  561. originalCreatedAt: tblItems.value[existingIndex].created_at || tblItems.value[existingIndex]._metadata?.originalCreatedAt,
  562. lastModifiedAt: new Date().toISOString()
  563. };
  564. // 기존 데이터 업데이트
  565. tblItems.value[existingIndex] = { ...tblItems.value[existingIndex], ...newItem };
  566. updatedCount++;
  567. } else {
  568. // 신규 메타데이터 추가
  569. newItem._metadata = {
  570. isUpdated: false,
  571. isNew: true,
  572. originalCreatedAt: new Date().toISOString(),
  573. lastModifiedAt: new Date().toISOString()
  574. };
  575. // 신규 데이터 추가
  576. tblItems.value.push(newItem);
  577. newCount++;
  578. }
  579. });
  580. pageObj.value.totalCnt = tblItems.value.length;
  581. // ag-grid 데이터 갱신
  582. if (gridApi.value) {
  583. gridApi.value.setGridOption("rowData", tblItems.value);
  584. }
  585. // 결과 메시지 표시
  586. if (updatedCount > 0 && newCount > 0) {
  587. $toast.success(`총 ${mappedData.length}건 처리완료 (업데이트: ${updatedCount}건, 신규추가: ${newCount}건)`);
  588. } else if (updatedCount > 0) {
  589. $toast.success(`${updatedCount}건의 기존 주문이 업데이트되었습니다.`);
  590. } else {
  591. $toast.success(`${newCount}건의 주문 내역이 추가되었습니다.`);
  592. }
  593. // 파일 입력 초기화
  594. event.target.value = "";
  595. } catch (error) {
  596. console.error("엑셀 파일 처리 중 오류:", error);
  597. $toast.error("엑셀 파일을 읽는 중 오류가 발생했습니다. 파일 형식을 확인해주세요.");
  598. }
  599. };
  600. reader.onerror = () => {
  601. $toast.error("파일을 읽는 중 오류가 발생했습니다.");
  602. };
  603. reader.readAsArrayBuffer(file);
  604. }
  605. const downloadExcel = () => {
  606. if (!tblItems.value || tblItems.value.length === 0) {
  607. $toast.warning("다운로드할 데이터가 없습니다.");
  608. return;
  609. }
  610. // 한글 헤더명 배열
  611. const headers = [
  612. "주문번호",
  613. "구매자명",
  614. "연락처",
  615. "주소",
  616. "구매수량",
  617. "배송업체",
  618. "송장번호",
  619. ];
  620. // 데이터를 엑셀 형식으로 변환
  621. const excelData = tblItems.value.map((item) => [
  622. item.ORDER_NUMB || "",
  623. item.BUYER_NAME || "",
  624. item.PHONE || "",
  625. item.ADDRESS || "",
  626. item.QTY || "",
  627. item.DELI_COMP || "",
  628. item.DELI_NUMB || "",
  629. ]);
  630. // 헤더를 첫 번째 행에 추가
  631. const worksheetData = [headers, ...excelData];
  632. // 워크시트 생성
  633. const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
  634. // 워크북 생성
  635. const workbook = XLSX.utils.book_new();
  636. XLSX.utils.book_append_sheet(workbook, worksheet, "배송관리");
  637. // 파일명 생성 (현재 날짜 포함)
  638. const today = new Date();
  639. const dateString =
  640. today.getFullYear() +
  641. String(today.getMonth() + 1).padStart(2, "0") +
  642. String(today.getDate()).padStart(2, "0");
  643. const fileName = `배송관리_${dateString}.xlsx`;
  644. // 엑셀 파일 다운로드
  645. XLSX.writeFile(workbook, fileName);
  646. $toast.success("엑셀 파일이 다운로드되었습니다.");
  647. };
  648. const listLocated = () => {
  649. router.push({
  650. path: "/view/common/item",
  651. });
  652. useDtStore.boardInfo.itemType = itemType;
  653. };
  654. // 인플루언서 이름 조회
  655. const getInfName = async (contact_inf) => {
  656. try {
  657. // contact_inf 값이 있을 때만 API 호출
  658. if (!contact_inf) {
  659. return null;
  660. }
  661. const response = await useAxios().get(`/user/getInfName/${contact_inf}`);
  662. if (response.data?.status === 'success') {
  663. return response.data.data;
  664. } else {
  665. return null;
  666. }
  667. } catch (error) {
  668. return null;
  669. }
  670. };
  671. // 브랜드사 조회
  672. const getBrdName = async (contact_brd) => {
  673. try {
  674. if (!contact_brd) {
  675. return null;
  676. }
  677. const response = await useAxios().get(`/user/getBrdName/${contact_brd}`);
  678. if (response.data?.status === 'success') {
  679. return response.data.data;
  680. } else {
  681. return null;
  682. }
  683. } catch (error) {
  684. return null;
  685. }
  686. };
  687. /*======================================================================
  688. | 작성 시퀀스
  689. | 1. 작성 컨펌
  690. | 2. 버튼 체크
  691. | 3. 등록시 -> 등록 API 호출
  692. ======================================================================*/
  693. const fnBtnEvt = () => {
  694. //await editorContent();
  695. router.push({
  696. path: "/view/common/item/add",
  697. });
  698. useDtStore.boardInfo.itemType = itemType;
  699. useDtStore.boardInfo.pageType = "U";
  700. };
  701. const fnDelEvt = () => {
  702. let param = {
  703. id: pageId,
  704. title: pageId,
  705. content: "공동구매를 종료하시겠습니까?",
  706. yes: {
  707. text: "확인",
  708. isProc: true,
  709. event: "FN_DELETE",
  710. param: "",
  711. },
  712. no: {
  713. text: "취소",
  714. isProc: false,
  715. },
  716. };
  717. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  718. };
  719. const fnCloseEvt = () => {
  720. let param = {
  721. id: pageId,
  722. title: pageId,
  723. content: "마감하시겠습니까?",
  724. yes: {
  725. text: "확인",
  726. isProc: true,
  727. event: "FN_CLOSE",
  728. param: "",
  729. },
  730. no: {
  731. text: "취소",
  732. isProc: false,
  733. },
  734. };
  735. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  736. };
  737. const fnClose = () => {
  738. let req = {
  739. seq: useDtStore.boardInfo.seq,
  740. };
  741. useAxios()
  742. .post(`/item/close/${req.seq}`)
  743. .then((res) => {
  744. router.push("/view/common/item");
  745. })
  746. .catch((error) => {
  747. })
  748. .finally(() => {
  749. });
  750. };
  751. const fnDelete = () => {
  752. let req = {
  753. seq: useDtStore.boardInfo.seq,
  754. };
  755. useAxios()
  756. .post(`/item/delete/${req.seq}`)
  757. .then((res) => {
  758. router.push("/view/common/item");
  759. })
  760. .catch((error) => {
  761. })
  762. .finally(() => {
  763. });
  764. };
  765. const getOrderList = () => {
  766. let req = {
  767. MEMBER_TYPE: memberType,
  768. COMPANY_NUMBER: useAtStore.auth.companyNumber || "1",
  769. INF_SEQ: useAtStore.auth.seq,
  770. TYPE: itemType,
  771. ITEM_SEQ: useDtStore.boardInfo.seq // 특정 아이템의 주문만 조회하려면 추가
  772. };
  773. useAxios()
  774. .get(`/deli/orderList/${req.ITEM_SEQ}`, req)
  775. .then(async (res) => {
  776. // 특정 아이템의 주문만 필터링
  777. const filteredData = res.data.filter(item => item.ITEM_SEQ == useDtStore.boardInfo.seq);
  778. tblItems.value = filteredData;
  779. pageObj.value.totalCnt = filteredData.length;
  780. })
  781. .catch((error) => {
  782. })
  783. .finally(() => {
  784. });
  785. };
  786. // ag-grid 관련 함수들
  787. const onGridReady = (params) => {
  788. gridApi.value = params.api;
  789. };
  790. const onCellValueChanged = (params) => {
  791. console.log('셀 값 변경:', params);
  792. };
  793. const chgPage = (page) => {
  794. pageObj.value.page = page;
  795. // 페이징이 필요한 경우 여기에 추가 로직 구현
  796. };
  797. const fnRegEvt = () => {
  798. let param = {
  799. id: pageId,
  800. title: pageId,
  801. content: "주문 내역을 저장하시겠습니까?",
  802. yes: {
  803. text: "확인",
  804. isProc: true,
  805. event: "FN_INSERT",
  806. param: "",
  807. },
  808. no: {
  809. text: "취소",
  810. isProc: false,
  811. },
  812. };
  813. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  814. };
  815. const fnDetail = () => {
  816. let req = {
  817. seq: useDtStore.boardInfo.seq,
  818. };
  819. useAxios()
  820. .get(`/item/detail/${req.seq}`)
  821. .then(async (res) => {
  822. form.value.formValue1 = res.data.NAME;
  823. form.value.formValue8 = res.data.STATUS;
  824. form.value.formValue9 = res.data.SHOW_YN;
  825. form.value.formValue10 = res.data.ADD_INFO;
  826. form.value.order_link = res.data.ORDER_LINK;
  827. form.value.order_start_date = res.data.ORDER_START_DATE;
  828. form.value.order_end_date = res.data.ORDER_END_DATE;
  829. form.value.contact_inf = res.data.CONTACT_INF;
  830. form.value.contact_brd = res.data.CONTACT_BRD;
  831. // contact_inf 값이 있으면 인플루언서 이름 조회
  832. if (res.data.CONTACT_INF) {
  833. const infData = await getInfName(res.data.CONTACT_INF);
  834. if (infData) {
  835. form.value.contact_inf_display = infData.NICK_NAME
  836. ? `${infData.NICK_NAME} (${infData.NAME})`
  837. : infData.NAME;
  838. }
  839. }
  840. if (res.data.CONTACT_BRD) {
  841. const brdData = await getBrdName(res.data.CONTACT_BRD);
  842. if (brdData) {
  843. form.value.contact_brd_display = brdData.NAME;
  844. }
  845. }
  846. getOrderList();
  847. })
  848. .catch((error) => {
  849. })
  850. .finally(() => {
  851. });
  852. };
  853. const fnInsert = async () => {
  854. try {
  855. const req = {
  856. item_seq: useDtStore.boardInfo.seq,
  857. inf_seq: form.value.contact_inf,
  858. orderList: tblItems.value || []
  859. };
  860. const response = await useAxios().post('/deli/reg', req);
  861. if (response.data) {
  862. const { message, updated_count, new_count, deleted_count, errors } = response.data;
  863. // 결과 메시지 표시
  864. const totalProcessed = updated_count + new_count + deleted_count;
  865. if (totalProcessed === 0) {
  866. $toast.success("주문 내역이 저장되었습니다.");
  867. } else {
  868. let message = `주문 내역이 저장되었습니다.`;
  869. $toast.success(message);
  870. }
  871. // 에러가 있으면 콘솔에 출력하고 토스트로 표시
  872. if (errors && errors.length > 0) {
  873. //console.warn('저장 중 일부 오류 발생:', errors);
  874. errors.forEach(error => {
  875. $toast.error(error);
  876. });
  877. }
  878. // 저장 후 페이지 새로고침
  879. window.location.reload();
  880. }
  881. } catch (error) {
  882. //console.error('주문 내역 저장 중 오류:', error);
  883. // 백엔드 에러 응답 확인
  884. if (error.response?.data) {
  885. const errorData = error.response.data;
  886. // 아무 작업 없이 저장 버튼 클릭 시
  887. if (errorData.messages?.error === "처리할 수 있는 데이터가 없습니다." ||
  888. errorData.message === "처리할 수 있는 데이터가 없습니다.") {
  889. $toast.warning("저장할 데이터가 없습니다.");
  890. return; // 새로고침하지 않음
  891. }
  892. // 다른 에러 메시지
  893. if (errorData.message) {
  894. $toast.error(errorData.message);
  895. } else if (errorData.messages?.error) {
  896. $toast.error(errorData.messages.error);
  897. } else {
  898. $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
  899. }
  900. } else {
  901. $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
  902. }
  903. }
  904. };
  905. /************************************************************************
  906. | 팝업 이벤트버스 정의
  907. ************************************************************************/
  908. $eventBus.off("FN_DELETE");
  909. $eventBus.on("FN_DELETE", () => {
  910. fnDelete();
  911. });
  912. $eventBus.off("FN_CLOSE");
  913. $eventBus.on("FN_CLOSE", () => {
  914. fnClose();
  915. });
  916. $eventBus.off("FN_INSERT");
  917. $eventBus.on("FN_INSERT", () => {
  918. fnInsert();
  919. });
  920. $eventBus.off("FN_DELETE_SELECTED");
  921. $eventBus.on("FN_DELETE_SELECTED", (selectedRows) => {
  922. fnDeleteSelected(selectedRows);
  923. });
  924. /************************************************************************
  925. | 라이프사이클
  926. ************************************************************************/
  927. onMounted(() => {
  928. pageType.value = "D";
  929. if(pageType.value == "I"){
  930. if(itemType == "G"){
  931. pageId.value = "공동구매 등록"
  932. } else {
  933. pageId.value = "제품 등록"
  934. }
  935. } else if(pageType.value == "U"){
  936. if(itemType == "G"){
  937. pageId.value = "공동구매 수정"
  938. } else {
  939. pageId.value = "제품 수정"
  940. }
  941. } else {
  942. if(itemType == "G"){
  943. pageId.value = "공동구매 상세"
  944. } else {
  945. pageId.value = "제품 상세"
  946. }
  947. }
  948. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  949. if (pageType.value !== "I") {
  950. fnDetail();
  951. }
  952. });
  953. </script>
  954. <style scoped>
  955. .cursor-pointer {
  956. cursor: pointer;
  957. }
  958. .cursor-pointer:hover {
  959. background-color: #f5f5f5;
  960. }
  961. .order-link {
  962. color: #1976d2;
  963. text-decoration: none;
  964. display: inline-flex;
  965. align-items: center;
  966. transition: color 0.2s;
  967. }
  968. .order-link:hover {
  969. color: #1565c0;
  970. text-decoration: underline;
  971. }
  972. .no-link {
  973. color: #999;
  974. font-style: italic;
  975. }
  976. </style>