detail.vue 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  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. - 엑셀의 헤더는 20개를 초과할 수 없습니다.<br>
  113. - 고유번호는 시스템 관리용 번호입니다. 변경 시 오류가 발생할 수 있습니다.
  114. </div>
  115. </div>
  116. <v-btn v-if="hasData" class="custom-btn btn-white mini" @click="addEmptyRow"
  117. ><i class="ico"></i>항목 추가</v-btn
  118. >
  119. <v-btn v-if="hasData" class="custom-btn btn-white mini" @click="deleteSelectedRows"
  120. ><i class="ico"></i>항목 삭제</v-btn
  121. >
  122. <input
  123. ref="excelFileInput"
  124. type="file"
  125. accept=".xlsx,.xls"
  126. @change="handleExcelUpload"
  127. style="display: none"
  128. />
  129. <v-btn class="custom-btn btn-excel" @click="$refs.excelFileInput.click()"
  130. ><i class="ico"></i>엑셀 업로드</v-btn
  131. >
  132. <v-btn v-if="hasData" class="custom-btn btn-excel" @click="downloadExcel"
  133. ><i class="ico"></i>엑셀 다운로드</v-btn
  134. >
  135. <v-btn class="custom-btn btn-purple mini" @click="fnRegEvt"
  136. ><i class="ico"></i>저장</v-btn
  137. >
  138. </div>
  139. </div>
  140. <div class="tbl-wrapper">
  141. <div class="tbl-wrap">
  142. <!-- ag grid -->
  143. <ag-grid-vue
  144. :style="{ width: '100%', height: gridHeight }"
  145. class="ag-theme-quartz order--table"
  146. :gridOptions="gridOptions"
  147. rowSelection="multiple"
  148. :rowData="tblItems"
  149. :paginationPageSize="pageObj.pageSize"
  150. :suppressPaginationPanel="true"
  151. @grid-ready="onGridReady"
  152. @cell-value-changed="onCellValueChanged"
  153. >
  154. </ag-grid-vue>
  155. <!-- 페이징 -->
  156. <!-- <div class="ag-grid-custom-pagenations">
  157. <pagination @chg_page="chgPage" :pageObj="pageObj"></pagination>
  158. </div> -->
  159. </div>
  160. </div>
  161. </div>
  162. </div>
  163. </div>
  164. </v-form>
  165. </div>
  166. <!-- 액션 버튼 -->
  167. <div class="action-buttons">
  168. <div class="button-group left">
  169. <v-btn
  170. class="action-btn secondary"
  171. variant="outlined"
  172. @click="listLocated"
  173. >
  174. <i class="mdi mdi-format-list-bulleted"></i>
  175. 목록으로
  176. </v-btn>
  177. <v-btn
  178. v-if="memberType !== 'INFLUENCER'"
  179. class="action-btn danger"
  180. variant="outlined"
  181. color="error"
  182. @click="fnDelEvt"
  183. >
  184. <i class="mdi mdi-close-circle"></i>
  185. 삭제
  186. </v-btn>
  187. </div>
  188. <div class="button-group right">
  189. <v-btn
  190. v-if="memberType !== 'INFLUENCER'"
  191. class="action-btn primary"
  192. color="primary"
  193. @click="fnBtnEvt"
  194. >
  195. <i class="mdi mdi-pencil"></i>
  196. 수정하기
  197. </v-btn>
  198. </div>
  199. </div>
  200. </div>
  201. </div>
  202. </template>
  203. <script setup>
  204. import useAxios from "@/composables/useAxios";
  205. import "@vuepic/vue-datepicker/dist/main.css";
  206. import { AgGridVue } from "ag-grid-vue3";
  207. import * as XLSX from "xlsx";
  208. /************************************************************************
  209. | 레이아웃
  210. ************************************************************************/
  211. definePageMeta({
  212. layout: "default",
  213. });
  214. /************************************************************************
  215. | 스토어
  216. ************************************************************************/
  217. const useDtStore = useDetailStore();
  218. const useAtStore = useAuthStore();
  219. /************************************************************************
  220. | 전역
  221. ************************************************************************/
  222. const memberType = useAtStore.auth.memberType;
  223. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  224. const router = useRouter();
  225. const pageId = ref("");
  226. const itemType = useDtStore.boardInfo.itemType;
  227. let pageObj = ref({
  228. page: 1, // 현재 페이지
  229. pageMaxNumSize: 10, // 페이지 숫자 최대 표현 개수
  230. pageSize: 10, // 테이블 조회 데이터 개수
  231. totalCnt: 0, // 전체 페이지
  232. });
  233. const addForm = ref(null);
  234. const form = ref({
  235. formValue1: "",
  236. formValue2: "",
  237. formValue3: "",
  238. formValue4: "",
  239. formValue5: null,
  240. formValue6: "",
  241. formValue7: null,
  242. formValue8: "0",
  243. formValue8Arr: [
  244. { title: "판매중", value: "0" },
  245. { title: "품절", value: "1" },
  246. ],
  247. formValue9: "Y",
  248. formValue9Arr: [
  249. { title: "노출", value: "Y" },
  250. { title: "비노출", value: "N" },
  251. ],
  252. formValue10: "",
  253. contact_inf: "", // 실제 전송될 INFLUENCER_SEQ
  254. contact_inf_display: "", // 화면에 표시될 이름
  255. contact_brd: "", // 실제 전송될 contact_brd
  256. contact_brd_display: "", // 화면에 표시될 브랜드명
  257. order_link: "",
  258. order_start_date: "",
  259. order_end_date: "",
  260. });
  261. const apiUrl = ref("");
  262. apiUrl.value = import.meta.env.VITE_APP_API_URL;
  263. const objProc = ref({
  264. validErrorMessage: "",
  265. });
  266. const pageType = ref("");
  267. // ag-grid 관련 변수
  268. const tblItems = ref([]);
  269. const gridApi = ref(null);
  270. const uploadedHeaders = ref([]); // 엑셀에서 업로드한 헤더 저장
  271. // 데이터 존재 여부 확인 (컬럼이 있고 데이터도 있어야 함)
  272. const hasData = computed(() => {
  273. return gridOptions.value.columnDefs &&
  274. gridOptions.value.columnDefs.length > 0 &&
  275. tblItems.value &&
  276. tblItems.value.length > 0;
  277. });
  278. const gridOptions = ref({
  279. columnDefs: [], // 초기에는 빈 배열, ORDER_HEADER_LIST에서 동적으로 생성
  280. autoSizeStrategy: {
  281. type: "fitGridWidth", // width맞춤
  282. },
  283. suppressHorizontalScroll: false, // 가로 스크롤 제거
  284. defaultColDef: {
  285. sortable: true,
  286. filter: true,
  287. resizable: true, // 리사이즈 가능하게 변경
  288. minWidth: 100, // 최소 너비 설정
  289. },
  290. suppressMovableColumns: true,
  291. suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
  292. rowMultiSelectWithClick: true,
  293. rowSelection: {
  294. checkboxes: true,
  295. headerCheckbox: true,
  296. enableClickSelection: false,
  297. mode: "multiRow",
  298. },
  299. localeText: {
  300. noRowsToShow: '주문 내역이 없습니다.'
  301. },
  302. getRowStyle: (params) => {
  303. // 새로 추가되거나 수정된 행은 연두색 배경
  304. if (params.data && params.data._isNewOrModified) {
  305. return { backgroundColor: '#e8f5e8' }; // 연두색 배경
  306. }
  307. return null;
  308. }
  309. });
  310. /************************************************************************
  311. | 함수(METHODS)
  312. ************************************************************************/
  313. // 동적 높이 계산
  314. const gridHeight = computed(() => {
  315. const rowCount = tblItems.value.length;
  316. const minRows = 5; // 최소 5줄 높이
  317. const maxRows = 10; // 최대 15줄 높이 (스크롤 시작점)
  318. const rowHeight = 2.94; // rem 단위
  319. if (rowCount <= minRows) {
  320. return `calc(${minRows} * ${rowHeight}rem)`;
  321. } else if (rowCount > maxRows) {
  322. return `calc(${maxRows} * ${rowHeight}rem)`;
  323. } else {
  324. return `calc(${rowCount} * ${rowHeight}rem)`;
  325. }
  326. });
  327. const addEmptyRow = () => {
  328. const newRow = {};
  329. // 현재 시간 포맷팅
  330. const getCurrentTime = () => {
  331. const now = new Date();
  332. return now.getFullYear() + '-' +
  333. String(now.getMonth() + 1).padStart(2, '0') + '-' +
  334. String(now.getDate()).padStart(2, '0') + ' ' +
  335. String(now.getHours()).padStart(2, '0') + ':' +
  336. String(now.getMinutes()).padStart(2, '0') + ':' +
  337. String(now.getSeconds()).padStart(2, '0');
  338. };
  339. // 해시 생성 함수
  340. const generateHash = () => {
  341. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  342. let result = '';
  343. for (let i = 0; i < 50; i++) {
  344. result += chars.charAt(Math.floor(Math.random() * chars.length));
  345. }
  346. return result;
  347. };
  348. // 현재 컬럼 정의에 따라 빈 행 생성
  349. const currentColumns = gridOptions.value.columnDefs;
  350. currentColumns.forEach(col => {
  351. if (col.field && col.field.startsWith('CONTENT_')) {
  352. newRow[col.field] = "";
  353. }
  354. });
  355. // 업로드 일자와 고유번호 자동 설정
  356. newRow['CONTENT_REGDATE'] = getCurrentTime();
  357. newRow['COLUMN_CODE'] = generateHash();
  358. // 새로 추가된 행 표시
  359. newRow['_isNewOrModified'] = true;
  360. // 맨 앞에 추가 (unshift 사용)
  361. tblItems.value.unshift(newRow);
  362. pageObj.value.totalCnt = tblItems.value.length;
  363. // ag-grid 데이터 갱신
  364. if (gridApi.value) {
  365. gridApi.value.setGridOption("rowData", tblItems.value);
  366. }
  367. $toast.success("새 항목이 추가되었습니다.");
  368. };
  369. const deleteSelectedRows = () => {
  370. if (!gridApi.value) return;
  371. const selectedRows = gridApi.value.getSelectedRows();
  372. if (selectedRows.length === 0) {
  373. $toast.warning("삭제할 항목을 선택해주세요.");
  374. return;
  375. }
  376. let param = {
  377. id: pageId,
  378. title: pageId,
  379. content: `선택된 ${selectedRows.length}개 항목을 삭제하시겠습니까?`,
  380. yes: {
  381. text: "삭제",
  382. isProc: true,
  383. event: "FN_DELETE_SELECTED",
  384. param: selectedRows,
  385. },
  386. no: {
  387. text: "취소",
  388. isProc: false,
  389. },
  390. };
  391. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  392. };
  393. const fnDeleteSelected = (selectedRows) => {
  394. // 선택된 행들을 tblItems에서 제거 (고유번호 기준으로 찾기)
  395. selectedRows.forEach((selectedRow) => {
  396. const index = tblItems.value.findIndex((item) => {
  397. // 고유번호가 있으면 고유번호로 비교
  398. if (selectedRow.COLUMN_CODE && item.COLUMN_CODE) {
  399. return item.COLUMN_CODE === selectedRow.COLUMN_CODE;
  400. }
  401. // 고유번호가 없으면 모든 CONTENT_ 필드로 비교
  402. let isMatch = true;
  403. for (let i = 1; i <= 20; i++) {
  404. const contentKey = `CONTENT_${i}`;
  405. if (selectedRow[contentKey] !== item[contentKey]) {
  406. isMatch = false;
  407. break;
  408. }
  409. }
  410. return isMatch;
  411. });
  412. if (index > -1) {
  413. tblItems.value.splice(index, 1);
  414. }
  415. });
  416. pageObj.value.totalCnt = tblItems.value.length;
  417. // 데이터가 모두 삭제되었으면 헤더도 초기화
  418. if (tblItems.value.length === 0) {
  419. gridOptions.value.columnDefs = [];
  420. uploadedHeaders.value = [];
  421. if (gridApi.value) {
  422. gridApi.value.setGridOption("columnDefs", []);
  423. gridApi.value.setGridOption("rowData", []);
  424. }
  425. } else {
  426. // ag-grid 데이터 갱신
  427. if (gridApi.value) {
  428. gridApi.value.setGridOption("rowData", tblItems.value);
  429. }
  430. }
  431. $toast.success(`${selectedRows.length}개 항목이 삭제되었습니다.`);
  432. };
  433. const handleExcelUpload = async (event) => {
  434. const file = event.target.files[0];
  435. if (!file) return;
  436. const errorHandler = useErrorHandler();
  437. // 파일 크기 검증 (10MB)
  438. const maxSize = 10 * 1024 * 1024;
  439. if (file.size > maxSize) {
  440. const sizeError = new Error("파일 크기 초과");
  441. sizeError.name = "FileSizeError";
  442. errorHandler.handleFileError(sizeError, file.name);
  443. event.target.value = "";
  444. return;
  445. }
  446. // 파일 형식 검증
  447. const allowedTypes = [
  448. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  449. "application/vnd.ms-excel",
  450. ];
  451. if (!allowedTypes.includes(file.type)) {
  452. const typeError = new Error("지원하지 않는 파일 형식");
  453. typeError.name = "FileTypeError";
  454. errorHandler.handleFileError(typeError, file.name);
  455. event.target.value = "";
  456. return;
  457. }
  458. const reader = new FileReader();
  459. reader.onload = async (e) => {
  460. try {
  461. const data = new Uint8Array(e.target.result);
  462. const workbook = XLSX.read(data, { type: "array", cellDates: true });
  463. const sheetName = workbook.SheetNames[0];
  464. const worksheet = workbook.Sheets[sheetName];
  465. const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
  466. if (jsonData.length < 2) {
  467. $toast.error("엑셀 파일에 데이터가 없습니다. 헤더와 최소 1개 이상의 데이터 행이 필요합니다.");
  468. return;
  469. }
  470. const headers = jsonData[0];
  471. const rows = jsonData.slice(1);
  472. // 해시 생성 함수
  473. const generateHash = () => {
  474. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  475. let result = '';
  476. for (let i = 0; i < 50; i++) {
  477. result += chars.charAt(Math.floor(Math.random() * chars.length));
  478. }
  479. return result;
  480. };
  481. // 현재 시간 포맷팅
  482. const getCurrentTime = () => {
  483. const now = new Date();
  484. return now.getFullYear() + '-' +
  485. String(now.getMonth() + 1).padStart(2, '0') + '-' +
  486. String(now.getDate()).padStart(2, '0') + ' ' +
  487. String(now.getHours()).padStart(2, '0') + ':' +
  488. String(now.getMinutes()).padStart(2, '0') + ':' +
  489. String(now.getSeconds()).padStart(2, '0');
  490. };
  491. // 최초 업로드인지 확인 (기존 데이터가 없으면 최초 업로드)
  492. const isFirstUpload = tblItems.value.length === 0;
  493. // 엑셀에 업로드 일자와 고유번호 헤더가 있는지 확인
  494. const hasUploadDateHeader = headers.includes("업로드 일자");
  495. const hasUniqueIdHeader = headers.includes("고유번호");
  496. let uploadDateIndex = headers.findIndex(h => h === "업로드 일자");
  497. let uniqueIdIndex = headers.findIndex(h => h === "고유번호");
  498. // 업데이트할 행과 새로 추가할 행 분리
  499. let updatedItems = [];
  500. let updateCount = 0;
  501. let insertCount = 0;
  502. // 최초 업로드인 경우
  503. if (isFirstUpload) {
  504. rows.forEach((row) => {
  505. const mappedRow = {};
  506. let hasValidData = false;
  507. let uploadDateValue = null;
  508. let uniqueIdValue = null;
  509. // 각 셀을 CONTENT_1, CONTENT_2... 형태로 매핑 (업로드 일자, 고유번호 제외)
  510. let contentIndex = 0;
  511. headers.forEach((header, index) => {
  512. if (header === "업로드 일자") {
  513. uploadDateValue = row[index] ? row[index].toString().trim() : null;
  514. } else if (header === "고유번호") {
  515. uniqueIdValue = row[index] ? row[index].toString().trim() : null;
  516. } else {
  517. // 일반 데이터는 CONTENT_ 매핑
  518. contentIndex++;
  519. const colKey = `CONTENT_${contentIndex}`;
  520. if (row[index] !== undefined && row[index] !== "") {
  521. mappedRow[colKey] = row[index].toString().trim();
  522. hasValidData = true;
  523. }
  524. }
  525. });
  526. if (hasValidData) {
  527. // 업로드 일자 설정 (CONTENT_REGDATE에 저장)
  528. mappedRow['CONTENT_REGDATE'] = getCurrentTime();
  529. // 고유번호 설정 (COLUMN_CODE에 저장)
  530. if (!hasUniqueIdHeader || !uniqueIdValue) {
  531. mappedRow['COLUMN_CODE'] = generateHash();
  532. } else {
  533. mappedRow['COLUMN_CODE'] = uniqueIdValue;
  534. }
  535. // 새로 추가된 행 표시
  536. mappedRow['_isNewOrModified'] = true;
  537. updatedItems.unshift(mappedRow); // 새 데이터를 맨 앞에 추가
  538. insertCount++;
  539. }
  540. });
  541. } else if (hasUniqueIdHeader && uniqueIdIndex >= 0) {
  542. // 재업로드 + 고유번호 헤더가 있는 경우 - 업데이트/인서트 로직
  543. // 기존 데이터의 고유번호 맵 생성 (COLUMN_CODE 사용)
  544. const existingDataMap = {};
  545. tblItems.value.forEach((item, index) => {
  546. if (item['COLUMN_CODE']) {
  547. existingDataMap[item['COLUMN_CODE']] = { item, index };
  548. }
  549. });
  550. updatedItems = [...tblItems.value];
  551. const processedUniqueIds = new Set();
  552. // 데이터 처리
  553. rows.forEach((row) => {
  554. const mappedRow = {};
  555. let hasValidData = false;
  556. let existingUniqueId = null;
  557. // 각 셀을 CONTENT_1, CONTENT_2... 형태로 매핑 (업로드 일자, 고유번호 제외)
  558. let contentIndex = 0;
  559. headers.forEach((header, index) => {
  560. if (header === "업로드 일자") {
  561. // 업로드 일자는 매핑하지 않음 (기존 값 유지)
  562. return;
  563. } else if (header === "고유번호") {
  564. // 고유번호만 확인용으로 저장
  565. if (row[index] !== undefined && row[index] !== "") {
  566. existingUniqueId = row[index].toString().trim();
  567. }
  568. return;
  569. } else {
  570. // 일반 데이터만 CONTENT_ 매핑
  571. contentIndex++;
  572. const colKey = `CONTENT_${contentIndex}`;
  573. if (row[index] !== undefined && row[index] !== "") {
  574. mappedRow[colKey] = row[index].toString().trim();
  575. hasValidData = true;
  576. }
  577. }
  578. });
  579. if (hasValidData) {
  580. // 고유번호가 있고 기존 데이터에 존재하는 경우 - 업데이트
  581. if (existingUniqueId && existingDataMap[existingUniqueId]) {
  582. const { index } = existingDataMap[existingUniqueId];
  583. // 업데이트 시에는 업로드 일자와 고유번호를 변경하지 않음 (기존 값 완전 유지)
  584. const updatedItem = { ...updatedItems[index] };
  585. // 고유번호와 업로드 일자를 제외한 나머지 필드만 업데이트
  586. Object.keys(mappedRow).forEach(key => {
  587. updatedItem[key] = mappedRow[key];
  588. });
  589. // 업데이트된 행은 연두색 표시하지 않음 (기존 데이터이므로)
  590. updatedItems[index] = updatedItem;
  591. processedUniqueIds.add(existingUniqueId);
  592. updateCount++;
  593. } else {
  594. // 새로운 데이터 - 인서트 (고유번호가 없거나 일치하지 않는 경우)
  595. if (!existingUniqueId) {
  596. mappedRow['COLUMN_CODE'] = generateHash(); // 고유번호 없으면 생성
  597. } else {
  598. mappedRow['COLUMN_CODE'] = existingUniqueId; // 엑셀에 있던 고유번호 사용
  599. }
  600. // 새로운 데이터에만 현재 시간으로 업로드 일자 설정
  601. mappedRow['CONTENT_REGDATE'] = getCurrentTime(); // 업로드 일자
  602. // 새로 추가된 행 표시
  603. mappedRow['_isNewOrModified'] = true;
  604. updatedItems.unshift(mappedRow); // 새 데이터를 맨 앞에 추가
  605. insertCount++;
  606. }
  607. }
  608. });
  609. } else {
  610. // 재업로드 + 고유번호 헤더가 없는 경우 - 기존 데이터는 유지하고 새 데이터만 추가
  611. // 기존 데이터는 그대로 유지 (고유번호 변경하지 않음)
  612. updatedItems = [...tblItems.value];
  613. // 기존 헤더에서 업로드 일자와 고유번호 위치 찾기
  614. const existingHeaders = uploadedHeaders.value || [];
  615. const existingUploadDateIndex = existingHeaders.findIndex(h => h === "업로드 일자");
  616. const existingUniqueIdIndex = existingHeaders.findIndex(h => h === "고유번호");
  617. rows.forEach((row) => {
  618. const mappedRow = {};
  619. let hasValidData = false;
  620. // 각 셀을 CONTENT_1, CONTENT_2... 형태로 매핑 (업로드 일자, 고유번호 제외)
  621. let contentIndex = 0;
  622. headers.forEach((header, index) => {
  623. // 업로드 일자와 고유번호는 CONTENT_ 매핑에서 제외
  624. if (header !== "업로드 일자" && header !== "고유번호") {
  625. contentIndex++;
  626. const colKey = `CONTENT_${contentIndex}`;
  627. if (row[index] !== undefined && row[index] !== "") {
  628. mappedRow[colKey] = row[index].toString().trim();
  629. hasValidData = true;
  630. }
  631. }
  632. });
  633. if (hasValidData) {
  634. // 새로 추가되는 데이터에만 업로드 일자와 고유번호 부여
  635. mappedRow['CONTENT_REGDATE'] = getCurrentTime(); // 업로드 일자
  636. mappedRow['COLUMN_CODE'] = generateHash(); // 고유번호 (새 데이터만)
  637. // 새로 추가된 행 표시
  638. mappedRow['_isNewOrModified'] = true;
  639. updatedItems.unshift(mappedRow); // 새 데이터를 맨 앞에 추가
  640. insertCount++;
  641. }
  642. });
  643. }
  644. const mappedData = updatedItems;
  645. if (mappedData.length === 0) {
  646. $toast.error("매핑 가능한 데이터가 없습니다.");
  647. return;
  648. }
  649. // 먼저 ORDER_HEADER_LIST 업데이트를 위한 헤더 저장
  650. const headerData = {};
  651. headers.forEach((header, index) => {
  652. const headerKey = `HEADER_${index + 1}`;
  653. if (index < 20) { // 최대 20개 헤더만
  654. headerData[headerKey] = header || '';
  655. }
  656. });
  657. // ag-grid 컬럼을 엑셀 헤더로 동적 생성
  658. const newColumnDefs = [];
  659. // 체크박스 컬럼
  660. newColumnDefs.push({
  661. checkboxSelection: true,
  662. headerCheckboxSelection: true,
  663. width: 50,
  664. sortable: false,
  665. filter: false,
  666. });
  667. // No 컬럼
  668. newColumnDefs.push({
  669. headerName: "No",
  670. valueGetter: (params) => params.api.getDisplayedRowCount() - params.node.rowIndex,
  671. sortable: false,
  672. filter: false,
  673. width: 80,
  674. });
  675. // 헤더 처리: 업로드 일자와 고유번호를 제외한 순수 헤더만 먼저 추출
  676. let pureHeaders = headers.filter(header =>
  677. header !== "업로드 일자" && header !== "고유번호"
  678. );
  679. // 업로드 일자와 고유번호를 항상 맨 끝에 추가
  680. let enhancedHeaders = [...pureHeaders];
  681. if (isFirstUpload) {
  682. // 최초 업로드 시 항상 맨 끝에 추가
  683. enhancedHeaders.push("업로드 일자");
  684. enhancedHeaders.push("고유번호");
  685. } else {
  686. // 재업로드인 경우에도 항상 맨 끝에 추가
  687. const existingHeaders = uploadedHeaders.value || [];
  688. const hasExistingUploadDate = existingHeaders.includes("업로드 일자");
  689. const hasExistingUniqueId = existingHeaders.includes("고유번호");
  690. // 기존에 있었던 경우 맨 끝에 추가
  691. if (hasExistingUploadDate || !hasUploadDateHeader) {
  692. enhancedHeaders.push("업로드 일자");
  693. }
  694. if (hasExistingUniqueId || !hasUniqueIdHeader) {
  695. enhancedHeaders.push("고유번호");
  696. }
  697. }
  698. // 전역 변수에 헤더 저장 (저장 시 사용)
  699. uploadedHeaders.value = enhancedHeaders;
  700. // 엑셀 헤더를 기반으로 컬럼 생성
  701. enhancedHeaders.forEach((header, index) => {
  702. if (header) { // 헤더가 있으면 처리
  703. const colDef = {
  704. headerName: header,
  705. field: `CONTENT_${index + 1}`, // 기본적으로 인덱스 기반 필드명
  706. editable: header !== "업로드 일자" && header !== "고유번호", // 업로드 일자, 고유번호는 편집 불가
  707. width: 150,
  708. resizable: true
  709. };
  710. // 업로드 일자는 CONTENT_REGDATE 필드 참조
  711. if (header === "업로드 일자") {
  712. colDef.field = "CONTENT_REGDATE";
  713. //colDef.cellStyle = { backgroundColor: '#f5f5f5' };
  714. }
  715. // 고유번호는 COLUMN_CODE 필드 참조
  716. if (header === "고유번호") {
  717. colDef.field = "COLUMN_CODE";
  718. colDef.cellStyle = { backgroundColor: '#f5f5f5' };
  719. colDef.hide = true;
  720. }
  721. // 20개를 초과하지 않는 일반 컬럼이거나, 업로드 일자/고유번호인 경우 추가
  722. if (index < 20 || header === "업로드 일자" || header === "고유번호") {
  723. newColumnDefs.push(colDef);
  724. }
  725. }
  726. });
  727. // 데이터 설정
  728. tblItems.value = mappedData;
  729. pageObj.value.totalCnt = tblItems.value.length;
  730. // gridOptions와 ag-grid API 모두 업데이트
  731. gridOptions.value.columnDefs = newColumnDefs;
  732. if (gridApi.value) {
  733. // 컬럼과 데이터를 순차적으로 업데이트
  734. gridApi.value.setGridOption("columnDefs", newColumnDefs);
  735. gridApi.value.setGridOption("rowData", tblItems.value);
  736. // 강제로 그리드 새로고침
  737. gridApi.value.refreshCells({ force: true });
  738. }
  739. // 결과 메시지 표시
  740. let message = '';
  741. if (updateCount > 0 && insertCount > 0) {
  742. message = `${updateCount}건 업데이트, ${insertCount}건 추가되었습니다. (총 ${mappedData.length}건)`;
  743. } else if (updateCount > 0) {
  744. message = `${updateCount}건의 주문 내역이 업데이트되었습니다.`;
  745. } else if (insertCount > 0) {
  746. message = `${insertCount}건의 주문 내역이 추가되었습니다.`;
  747. } else {
  748. message = `처리된 데이터가 없습니다.`;
  749. }
  750. $toast.success(message);
  751. // 컬럼 너비를 그리드 전체 너비에 맞춤
  752. if (gridApi.value) {
  753. setTimeout(() => {
  754. gridApi.value.sizeColumnsToFit();
  755. }, 100);
  756. }
  757. // 파일 입력 초기화
  758. event.target.value = "";
  759. } catch (error) {
  760. $toast.error("엑셀 파일을 읽는 중 오류가 발생했습니다. 파일 형식을 확인해주세요.");
  761. }
  762. };
  763. reader.onerror = () => {
  764. $toast.error("파일을 읽는 중 오류가 발생했습니다.");
  765. };
  766. reader.readAsArrayBuffer(file);
  767. }
  768. const downloadExcel = () => {
  769. if (!tblItems.value || tblItems.value.length === 0) {
  770. $toast.warning("다운로드할 데이터가 없습니다.");
  771. return;
  772. }
  773. // 현재 ag-grid에서 사용 중인 컬럼 정의에서 헤더 추출
  774. const visibleColumns = gridOptions.value.columnDefs?.filter(col =>
  775. col.headerName &&
  776. col.headerName !== "" &&
  777. !col.checkboxSelection &&
  778. col.headerName !== "No"
  779. // 엑셀 다운로드에는 숨김 컬럼(고유번호)도 포함
  780. ) || [];
  781. if (visibleColumns.length === 0) {
  782. $toast.warning("다운로드할 컬럼이 없습니다.");
  783. return;
  784. }
  785. // 동적 헤더 생성
  786. const headers = visibleColumns.map(col => col.headerName);
  787. // 데이터를 엑셀 형식으로 변환
  788. const excelData = tblItems.value.map((item) => {
  789. return visibleColumns.map(col => {
  790. const fieldName = col.field;
  791. return item[fieldName] || "";
  792. });
  793. });
  794. // 헤더를 첫 번째 행에 추가
  795. const worksheetData = [headers, ...excelData];
  796. // 워크시트 생성
  797. const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
  798. // 워크북 생성
  799. const workbook = XLSX.utils.book_new();
  800. XLSX.utils.book_append_sheet(workbook, worksheet, "주문관리");
  801. // 파일명 생성 (제품명과 현재 날짜 포함)
  802. const today = new Date();
  803. const dateString =
  804. today.getFullYear() +
  805. String(today.getMonth() + 1).padStart(2, "0") +
  806. String(today.getDate()).padStart(2, "0");
  807. // 제품명에서 파일명에 사용할 수 없는 문자 제거
  808. const productName = form.value.formValue1 ?
  809. form.value.formValue1.replace(/[\\/:*?"<>|]/g, "_") : "제품";
  810. const fileName = `${productName}_주문 내역_${dateString}.xlsx`;
  811. // 엑셀 파일 다운로드
  812. XLSX.writeFile(workbook, fileName);
  813. $toast.success("엑셀 파일이 다운로드되었습니다.");
  814. };
  815. const listLocated = () => {
  816. router.push({
  817. path: "/view/common/item",
  818. });
  819. useDtStore.boardInfo.itemType = itemType;
  820. };
  821. // 인플루언서 이름 조회
  822. const getInfName = async (contact_inf) => {
  823. try {
  824. // contact_inf 값이 있을 때만 API 호출
  825. if (!contact_inf) {
  826. return null;
  827. }
  828. const response = await useAxios().get(`/user/getInfName/${contact_inf}`);
  829. if (response.data?.status === 'success') {
  830. return response.data.data;
  831. } else {
  832. return null;
  833. }
  834. } catch (error) {
  835. return null;
  836. }
  837. };
  838. // 브랜드사 조회
  839. const getBrdName = async (contact_brd) => {
  840. try {
  841. if (!contact_brd) {
  842. return null;
  843. }
  844. const response = await useAxios().get(`/user/getBrdName/${contact_brd}`);
  845. if (response.data?.status === 'success') {
  846. return response.data.data;
  847. } else {
  848. return null;
  849. }
  850. } catch (error) {
  851. return null;
  852. }
  853. };
  854. /*======================================================================
  855. | 작성 시퀀스
  856. | 1. 작성 컨펌
  857. | 2. 버튼 체크
  858. | 3. 등록시 -> 등록 API 호출
  859. ======================================================================*/
  860. const fnBtnEvt = () => {
  861. //await editorContent();
  862. router.push({
  863. path: "/view/common/item/add",
  864. });
  865. useDtStore.boardInfo.itemType = itemType;
  866. useDtStore.boardInfo.pageType = "U";
  867. };
  868. const fnDelEvt = () => {
  869. let param = {
  870. id: pageId,
  871. title: pageId,
  872. content: "공동구매를 종료하시겠습니까?",
  873. yes: {
  874. text: "확인",
  875. isProc: true,
  876. event: "FN_DELETE",
  877. param: "",
  878. },
  879. no: {
  880. text: "취소",
  881. isProc: false,
  882. },
  883. };
  884. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  885. };
  886. const fnCloseEvt = () => {
  887. let param = {
  888. id: pageId,
  889. title: pageId,
  890. content: "마감하시겠습니까?",
  891. yes: {
  892. text: "확인",
  893. isProc: true,
  894. event: "FN_CLOSE",
  895. param: "",
  896. },
  897. no: {
  898. text: "취소",
  899. isProc: false,
  900. },
  901. };
  902. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  903. };
  904. const fnClose = () => {
  905. let req = {
  906. seq: useDtStore.boardInfo.seq,
  907. };
  908. useAxios()
  909. .post(`/item/close/${req.seq}`)
  910. .then((res) => {
  911. router.push("/view/common/item");
  912. })
  913. .catch((error) => {
  914. })
  915. .finally(() => {
  916. });
  917. };
  918. const fnDelete = () => {
  919. let req = {
  920. seq: useDtStore.boardInfo.seq,
  921. };
  922. useAxios()
  923. .post(`/item/delete/${req.seq}`)
  924. .then((res) => {
  925. router.push("/view/common/item");
  926. })
  927. .catch((error) => {
  928. })
  929. .finally(() => {
  930. });
  931. };
  932. const getOrderList = () => {
  933. let req = {
  934. ITEM_SEQ: useDtStore.boardInfo.seq
  935. };
  936. useAxios()
  937. .get(`/order/orderList/${req.ITEM_SEQ}`, req)
  938. .then(async (res) => {
  939. // 특정 아이템의 주문만 필터링
  940. const filteredHeader = res.data.headerList.filter(item => item.ITEM_SEQ == useDtStore.boardInfo.seq);
  941. const filteredContent = res.data.orderList.filter(item => item.ITEM_SEQ == useDtStore.boardInfo.seq);
  942. // 헤더가 있으면 새로운 columnDefs 생성
  943. if (filteredHeader && filteredHeader.length > 0) {
  944. const headerRow = filteredHeader[0];
  945. // 새로운 columnDefs 배열 생성
  946. const newColumnDefs = [];
  947. // 체크박스 컬럼 (항상 첫 번째)
  948. newColumnDefs.push({
  949. checkboxSelection: true,
  950. headerCheckboxSelection: true,
  951. width: 50,
  952. sortable: false,
  953. filter: false,
  954. });
  955. // No 컬럼 (항상 두 번째)
  956. newColumnDefs.push({
  957. headerName: "No",
  958. valueGetter: (params) => params.api.getDisplayedRowCount() - params.node.rowIndex,
  959. sortable: false,
  960. filter: false,
  961. width: 80,
  962. });
  963. // HEADER_1 ~ HEADER_22에서 값이 있는 것만 컬럼으로 추가 (업로드 일자, 고유번호 포함)
  964. for (let i = 1; i <= 22; i++) {
  965. const headerKey = `HEADER_${i}`;
  966. const colKey = `CONTENT_${i}`; // ORDER_LIST 테이블의 실제 필드명
  967. if (headerRow[headerKey] && headerRow[headerKey].trim() !== '') {
  968. const headerName = headerRow[headerKey];
  969. const colDef = {
  970. headerName: headerName,
  971. field: colKey,
  972. editable: true,
  973. resizable: true
  974. };
  975. // 업로드 일자는 CONTENT_REGDATE 필드 참조
  976. if (headerName === '업로드 일자') {
  977. colDef.field = 'CONTENT_REGDATE';
  978. colDef.editable = false;
  979. //colDef.cellStyle = { backgroundColor: '#f5f5f5' };
  980. }
  981. // 고유번호는 COLUMN_CODE 필드 참조
  982. if (headerName === '고유번호') {
  983. colDef.field = 'COLUMN_CODE';
  984. colDef.editable = false;
  985. colDef.cellStyle = { backgroundColor: '#f5f5f5' };
  986. colDef.resizable = false;
  987. colDef.hide = true;
  988. colDef.sortable = false;
  989. }
  990. newColumnDefs.push(colDef);
  991. }
  992. }
  993. // ORDER_LIST 데이터 설정
  994. tblItems.value = filteredContent;
  995. pageObj.value.totalCnt = filteredContent.length;
  996. // gridOptions와 ag-grid API 모두 업데이트
  997. gridOptions.value.columnDefs = newColumnDefs;
  998. if (gridApi.value) {
  999. // 컬럼과 데이터를 순차적으로 업데이트
  1000. gridApi.value.setGridOption("columnDefs", newColumnDefs);
  1001. gridApi.value.setGridOption("rowData", tblItems.value);
  1002. // 강제로 그리드 새로고침
  1003. gridApi.value.refreshCells({ force: true });
  1004. // 컬럼 너비를 그리드 전체 너비에 맞춤
  1005. setTimeout(() => {
  1006. gridApi.value.sizeColumnsToFit();
  1007. }, 100);
  1008. }
  1009. // DB에서 불러온 헤더도 uploadedHeaders에 저장 (HEADER_21, 22 포함)
  1010. const extractedHeaders = [];
  1011. for (let i = 1; i <= 22; i++) {
  1012. const headerKey = `HEADER_${i}`;
  1013. if (headerRow[headerKey] && headerRow[headerKey].trim() !== '') {
  1014. extractedHeaders.push(headerRow[headerKey]);
  1015. }
  1016. }
  1017. uploadedHeaders.value = extractedHeaders;
  1018. } else {
  1019. // 헤더가 없으면 컬럼 정의를 완전히 비움
  1020. const emptyColumnDefs = [];
  1021. tblItems.value = [];
  1022. pageObj.value.totalCnt = 0;
  1023. gridOptions.value.columnDefs = emptyColumnDefs;
  1024. if (gridApi.value) {
  1025. gridApi.value.setGridOption("columnDefs", emptyColumnDefs);
  1026. gridApi.value.setGridOption("rowData", []);
  1027. }
  1028. }
  1029. })
  1030. .catch((error) => {
  1031. })
  1032. .finally(() => {
  1033. });
  1034. };
  1035. // ag-grid 관련 함수들
  1036. const onGridReady = (params) => {
  1037. gridApi.value = params.api;
  1038. // 그리드가 준비되면 컬럼 너비를 그리드 전체 너비에 맞춤
  1039. setTimeout(() => {
  1040. gridApi.value.sizeColumnsToFit();
  1041. }, 100);
  1042. };
  1043. const onCellValueChanged = (params) => {
  1044. // 셀 값 변경은 기존 데이터 수정이므로 연두색 표시하지 않음
  1045. // (새로 insert되는 행만 연두색 표시)
  1046. };
  1047. // 모든 행의 수정 플래그 제거
  1048. const clearModifiedFlags = () => {
  1049. if (tblItems.value) {
  1050. tblItems.value.forEach(item => {
  1051. delete item._isNewOrModified;
  1052. });
  1053. // 그리드 새로고침
  1054. if (gridApi.value) {
  1055. gridApi.value.refreshCells({ force: true });
  1056. }
  1057. }
  1058. };
  1059. const chgPage = (page) => {
  1060. pageObj.value.page = page;
  1061. // 페이징이 필요한 경우 여기에 추가 로직 구현
  1062. };
  1063. const fnRegEvt = () => {
  1064. let param = {
  1065. id: pageId,
  1066. title: pageId,
  1067. content: "주문 내역을 저장하시겠습니까?",
  1068. yes: {
  1069. text: "확인",
  1070. isProc: true,
  1071. event: "FN_INSERT",
  1072. param: "",
  1073. },
  1074. no: {
  1075. text: "취소",
  1076. isProc: false,
  1077. },
  1078. };
  1079. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  1080. };
  1081. const fnDetail = () => {
  1082. let req = {
  1083. seq: useDtStore.boardInfo.seq,
  1084. };
  1085. useAxios()
  1086. .get(`/item/detail/${req.seq}`)
  1087. .then(async (res) => {
  1088. form.value.formValue1 = res.data.NAME;
  1089. form.value.formValue8 = res.data.STATUS;
  1090. form.value.formValue9 = res.data.SHOW_YN;
  1091. form.value.formValue10 = res.data.ADD_INFO;
  1092. form.value.order_link = res.data.ORDER_LINK;
  1093. form.value.order_start_date = res.data.ORDER_START_DATE;
  1094. form.value.order_end_date = res.data.ORDER_END_DATE;
  1095. form.value.contact_inf = res.data.CONTACT_INF;
  1096. form.value.contact_brd = res.data.CONTACT_BRD;
  1097. // contact_inf 값이 있으면 인플루언서 이름 조회
  1098. if (res.data.CONTACT_INF) {
  1099. const infData = await getInfName(res.data.CONTACT_INF);
  1100. if (infData) {
  1101. form.value.contact_inf_display = infData.NICK_NAME
  1102. ? `${infData.NICK_NAME} (${infData.NAME})`
  1103. : infData.NAME;
  1104. }
  1105. }
  1106. if (res.data.CONTACT_BRD) {
  1107. const brdData = await getBrdName(res.data.CONTACT_BRD);
  1108. if (brdData) {
  1109. form.value.contact_brd_display = brdData.NAME;
  1110. }
  1111. }
  1112. getOrderList();
  1113. })
  1114. .catch((error) => {
  1115. })
  1116. .finally(() => {
  1117. });
  1118. };
  1119. const fnInsert = async () => {
  1120. gridApi.value.stopEditing();
  1121. try {
  1122. // 데이터가 비어있으면 전체 삭제 처리
  1123. if (!tblItems.value || tblItems.value.length === 0) {
  1124. const req = {
  1125. itemSeq: useDtStore.boardInfo.seq,
  1126. headers: [],
  1127. orderData: []
  1128. };
  1129. const response = await useAxios().post('/order/reg', req);
  1130. if (response.data && response.data.success) {
  1131. $toast.success('저장이 완료되었습니다.');
  1132. getOrderList(); // 데이터 다시 로드
  1133. } else if (response.success) {
  1134. $toast.success('저장이 완료되었습니다.');
  1135. getOrderList(); // 데이터 다시 로드
  1136. } else {
  1137. $toast.error('삭제에 실패했습니다.');
  1138. }
  1139. return;
  1140. }
  1141. // 데이터가 있는 경우 기존 로직
  1142. // uploadedHeaders가 있으면 사용, 없으면 columnDefs에서 추출
  1143. let headers = [];
  1144. if (uploadedHeaders.value && uploadedHeaders.value.length > 0) {
  1145. // 엑셀에서 업로드한 헤더 사용 (업로드 일자, 고유번호 제외)
  1146. headers = uploadedHeaders.value.filter(header =>
  1147. header !== '업로드 일자' && header !== '고유번호'
  1148. );
  1149. } else {
  1150. // 기존 데이터에서 헤더 추출 (DB에서 불러온 경우, 업로드 일자와 고유번호 제외)
  1151. const currentColumns = gridOptions.value.columnDefs;
  1152. currentColumns.forEach(col => {
  1153. if (col.field && col.field.startsWith('CONTENT_')) {
  1154. const headerName = col.headerName || '';
  1155. if (headerName !== '업로드 일자' && headerName !== '고유번호') {
  1156. headers.push(headerName);
  1157. }
  1158. }
  1159. });
  1160. }
  1161. // 헤더가 비어있는 경우 처리
  1162. if (headers.length === 0) {
  1163. $toast.warning('헤더 정보가 없습니다. 엑셀을 먼저 업로드해주세요.');
  1164. return;
  1165. }
  1166. const req = {
  1167. itemSeq: useDtStore.boardInfo.seq,
  1168. headers: headers,
  1169. orderData: tblItems.value || []
  1170. };
  1171. const response = await useAxios().post('/order/reg', req);
  1172. // axios 응답 데이터는 response.data에 있음
  1173. if (response.data && response.data.success) {
  1174. $toast.success(`${response.data.message} (주문: ${response.data.orderCount}건)`);
  1175. // 저장 성공 후 모든 행의 수정 플래그 제거
  1176. clearModifiedFlags();
  1177. // 저장 후 데이터 다시 로드
  1178. getOrderList();
  1179. } else if (response.success) {
  1180. // 만약 response에 직접 success가 있는 경우
  1181. $toast.success(`${response.message} (주문: ${response.orderCount}건)`);
  1182. // 저장 성공 후 모든 행의 수정 플래그 제거
  1183. clearModifiedFlags();
  1184. // 저장 후 데이터 다시 로드
  1185. getOrderList();
  1186. } else {
  1187. $toast.error('저장에 실패했습니다.');
  1188. }
  1189. } catch (error) {
  1190. // 백엔드 에러 응답 확인
  1191. if (error.response?.data) {
  1192. const errorData = error.response.data;
  1193. if (errorData.error) {
  1194. $toast.error(errorData.error);
  1195. } else if (errorData.message) {
  1196. $toast.error(errorData.message);
  1197. } else {
  1198. $toast.error('저장 중 오류가 발생했습니다.');
  1199. }
  1200. } else {
  1201. if (tblItems.value.length === 0) {
  1202. $toast.warning("저장할 데이터가 없습니다.");
  1203. } else {
  1204. $toast.error('저장 중 오류가 발생했습니다.');
  1205. }
  1206. }
  1207. }
  1208. };
  1209. /************************************************************************
  1210. | 팝업 이벤트버스 정의
  1211. ************************************************************************/
  1212. $eventBus.off("FN_DELETE");
  1213. $eventBus.on("FN_DELETE", () => {
  1214. fnDelete();
  1215. });
  1216. $eventBus.off("FN_CLOSE");
  1217. $eventBus.on("FN_CLOSE", () => {
  1218. fnClose();
  1219. });
  1220. $eventBus.off("FN_INSERT");
  1221. $eventBus.on("FN_INSERT", () => {
  1222. fnInsert();
  1223. });
  1224. $eventBus.off("FN_DELETE_SELECTED");
  1225. $eventBus.on("FN_DELETE_SELECTED", (selectedRows) => {
  1226. fnDeleteSelected(selectedRows);
  1227. });
  1228. /************************************************************************
  1229. | 라이프사이클
  1230. ************************************************************************/
  1231. onMounted(() => {
  1232. pageType.value = "D";
  1233. if(pageType.value == "I"){
  1234. if(itemType == "G"){
  1235. pageId.value = "공동구매 등록"
  1236. } else {
  1237. pageId.value = "제품 등록"
  1238. }
  1239. } else if(pageType.value == "U"){
  1240. if(itemType == "G"){
  1241. pageId.value = "공동구매 수정"
  1242. } else {
  1243. pageId.value = "제품 수정"
  1244. }
  1245. } else {
  1246. if(itemType == "G"){
  1247. pageId.value = "공동구매 상세"
  1248. } else {
  1249. pageId.value = "제품 상세"
  1250. }
  1251. }
  1252. //상세 등록 아니 리스트 클릭시 상세 정보로 접근
  1253. if (pageType.value !== "I") {
  1254. fnDetail();
  1255. }
  1256. });
  1257. </script>
  1258. <style scoped>
  1259. .cursor-pointer {
  1260. cursor: pointer;
  1261. }
  1262. .cursor-pointer:hover {
  1263. background-color: #f5f5f5;
  1264. }
  1265. .order-link {
  1266. color: #1976d2;
  1267. text-decoration: none;
  1268. display: inline-flex;
  1269. align-items: center;
  1270. transition: color 0.2s;
  1271. }
  1272. .order-link:hover {
  1273. color: #1565c0;
  1274. text-decoration: underline;
  1275. }
  1276. .no-link {
  1277. color: #999;
  1278. font-style: italic;
  1279. }
  1280. </style>