product-register.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>제품 관리</span>
  8. <span>{{ pageId }}</span>
  9. </div>
  10. </div>
  11. <div class="product-register--form">
  12. <div class="form--container">
  13. <div class="form--section">
  14. <h3>기본 정보</h3>
  15. <div class="form--row">
  16. <div class="form--field">
  17. <label class="form--label">제품명 <span class="required">*</span></label>
  18. <v-text-field
  19. v-model="form.productName"
  20. placeholder="제품명을 입력하세요"
  21. class="custom-input"
  22. :rules="[rules.required]"
  23. ></v-text-field>
  24. </div>
  25. </div>
  26. <div class="form--row">
  27. <div class="form--field">
  28. <label class="form--label">공급가 <span class="required">*</span></label>
  29. <v-text-field
  30. v-model="form.supplyPrice"
  31. type="number"
  32. placeholder="공급가를 입력하세요"
  33. class="custom-input"
  34. :rules="[rules.required]"
  35. ></v-text-field>
  36. </div>
  37. <div class="form--field">
  38. <label class="form--label">판매가 <span class="required">*</span></label>
  39. <v-text-field
  40. v-model="form.sellPrice"
  41. type="number"
  42. placeholder="판매가를 입력하세요"
  43. class="custom-input"
  44. :rules="[rules.required]"
  45. ></v-text-field>
  46. </div>
  47. </div>
  48. <div class="form--row">
  49. <div class="form--field">
  50. <label class="form--label">배송비 <span class="required">*</span></label>
  51. <v-text-field
  52. v-model="form.shippingCost"
  53. placeholder="배송비 정보를 입력하세요 (예: 3,000원, 무료배송)"
  54. class="custom-input"
  55. :rules="[rules.required]"
  56. ></v-text-field>
  57. </div>
  58. </div>
  59. <div class="form--row">
  60. <div class="form--field">
  61. <label class="form--label">소타이틀</label>
  62. <v-text-field
  63. v-model="form.subtitle"
  64. placeholder="소타이틀을 입력하세요"
  65. class="custom-input"
  66. ></v-text-field>
  67. </div>
  68. </div>
  69. </div>
  70. <div class="form--section">
  71. <h3>상세 정보</h3>
  72. <div class="form--row">
  73. <div class="form--field full-width">
  74. <label class="form--label">상세내용</label>
  75. <div class="editor--container">
  76. <div class="editor--toolbar">
  77. <v-btn-toggle v-model="editorMode" density="compact" class="editor--mode-toggle">
  78. <v-btn value="html" size="small">HTML</v-btn>
  79. <v-btn value="text" size="small">TEXT</v-btn>
  80. </v-btn-toggle>
  81. </div>
  82. <div v-show="editorMode === 'html'" class="html-editor">
  83. <textarea
  84. v-model="form.detailContent"
  85. placeholder="HTML 내용을 입력하세요"
  86. class="html-textarea"
  87. rows="15"
  88. ></textarea>
  89. </div>
  90. <div v-show="editorMode === 'text'" class="text-editor">
  91. <v-textarea
  92. v-model="form.detailContent"
  93. placeholder="텍스트 내용을 입력하세요"
  94. class="custom-input"
  95. rows="15"
  96. ></v-textarea>
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. </div>
  102. <div class="form--section">
  103. <h3>첨부파일</h3>
  104. <div class="form--row">
  105. <div class="form--field">
  106. <label class="form--label">상세다운로드</label>
  107. <div class="file-upload--container">
  108. <input
  109. type="file"
  110. ref="fileInput"
  111. @change="handleFileUpload"
  112. accept=".zip"
  113. class="file-input"
  114. style="display: none"
  115. />
  116. <div class="file-upload--area" @click="triggerFileUpload">
  117. <div v-if="!form.detailFile" class="file-upload--placeholder">
  118. <i class="upload-icon">📎</i>
  119. <p>ZIP 파일을 선택하세요</p>
  120. <small>클릭하여 파일 선택</small>
  121. </div>
  122. <div v-else class="file-upload--selected">
  123. <i class="file-icon">📁</i>
  124. <div class="file-info">
  125. <p class="file-name">{{ form.detailFile.name }}</p>
  126. <small class="file-size">{{ formatFileSize(form.detailFile.size) }}</small>
  127. </div>
  128. <v-btn
  129. @click.stop="removeFile"
  130. class="file-remove"
  131. size="small"
  132. color="error"
  133. icon="mdi-close"
  134. ></v-btn>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. <div class="form--section">
  142. <h3>상태 설정</h3>
  143. <div class="form--row">
  144. <div class="form--field">
  145. <label class="form--label">상태 <span class="required">*</span></label>
  146. <v-select
  147. v-model="form.status"
  148. :items="statusOptions"
  149. variant="outlined"
  150. class="custom-select"
  151. :rules="[rules.required]"
  152. ></v-select>
  153. </div>
  154. <div class="form--field">
  155. <label class="form--label">노출상태 <span class="required">*</span></label>
  156. <v-select
  157. v-model="form.displayStatus"
  158. :items="displayStatusOptions"
  159. variant="outlined"
  160. class="custom-select"
  161. :rules="[rules.required]"
  162. ></v-select>
  163. </div>
  164. </div>
  165. </div>
  166. <div class="form--section">
  167. <h3>업데이트 내역</h3>
  168. <div class="form--row">
  169. <div class="form--field full-width">
  170. <label class="form--label">업데이트 내역</label>
  171. <v-textarea
  172. v-model="form.updateHistory"
  173. placeholder="업데이트 내역을 입력하세요 (최대 500자)"
  174. class="custom-input"
  175. rows="5"
  176. :counter="500"
  177. :rules="[rules.maxLength(500)]"
  178. ></v-textarea>
  179. </div>
  180. </div>
  181. </div>
  182. <div class="form--actions">
  183. <v-btn
  184. class="custom-btn btn-white"
  185. @click="goBack"
  186. >
  187. 취소
  188. </v-btn>
  189. <v-btn
  190. class="custom-btn btn-blue"
  191. @click="saveProduct"
  192. :loading="loading"
  193. >
  194. 저장
  195. </v-btn>
  196. </div>
  197. </div>
  198. </div>
  199. </div>
  200. </template>
  201. <script setup>
  202. /************************************************************************
  203. | 레이아웃
  204. ************************************************************************/
  205. definePageMeta({
  206. layout: "default",
  207. });
  208. /************************************************************************
  209. | 스토어
  210. ************************************************************************/
  211. const useDtStore = useDetailStore();
  212. /************************************************************************
  213. | 전역 변수
  214. ************************************************************************/
  215. const { $toast, $log } = useNuxtApp();
  216. const router = useRouter();
  217. const pageId = ref("제품 등록");
  218. const loading = ref(false);
  219. const editorMode = ref("text");
  220. const fileInput = ref(null);
  221. /************************************************************************
  222. | 폼 데이터
  223. ************************************************************************/
  224. const form = ref({
  225. productName: "",
  226. supplyPrice: "",
  227. sellPrice: "",
  228. shippingCost: "",
  229. subtitle: "",
  230. detailContent: "",
  231. detailFile: null,
  232. status: "판매중",
  233. displayStatus: "노출",
  234. updateHistory: ""
  235. });
  236. /************************************************************************
  237. | 옵션 데이터
  238. ************************************************************************/
  239. const statusOptions = ref([
  240. { title: "판매중", value: "판매중" },
  241. { title: "품절", value: "품절" }
  242. ]);
  243. const displayStatusOptions = ref([
  244. { title: "노출", value: "노출" },
  245. { title: "비노출", value: "비노출" }
  246. ]);
  247. /************************************************************************
  248. | 유효성 검사
  249. ************************************************************************/
  250. const rules = {
  251. required: (value) => !!value || "필수 입력 항목입니다.",
  252. maxLength: (max) => (value) => {
  253. if (!value) return true;
  254. return value.length <= max || `최대 ${max}자까지 입력 가능합니다.`;
  255. }
  256. };
  257. /************************************************************************
  258. | 함수(METHODS)
  259. ************************************************************************/
  260. // 파일 업로드 트리거
  261. const triggerFileUpload = () => {
  262. fileInput.value.click();
  263. };
  264. // 파일 업로드 처리
  265. const handleFileUpload = (event) => {
  266. const file = event.target.files[0];
  267. if (file) {
  268. if (file.type !== 'application/zip' && !file.name.endsWith('.zip')) {
  269. $toast.error('ZIP 파일만 업로드 가능합니다.');
  270. return;
  271. }
  272. form.value.detailFile = file;
  273. $toast.success('파일이 선택되었습니다.');
  274. }
  275. };
  276. // 파일 제거
  277. const removeFile = () => {
  278. form.value.detailFile = null;
  279. if (fileInput.value) {
  280. fileInput.value.value = '';
  281. }
  282. };
  283. // 파일 크기 포맷팅
  284. const formatFileSize = (bytes) => {
  285. if (bytes === 0) return '0 Bytes';
  286. const k = 1024;
  287. const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  288. const i = Math.floor(Math.log(bytes) / Math.log(k));
  289. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  290. };
  291. // 뒤로가기
  292. const goBack = () => {
  293. router.push('/view/vendor');
  294. };
  295. // 제품 저장
  296. const saveProduct = async () => {
  297. // 유효성 검사
  298. if (!form.value.productName) {
  299. $toast.error('제품명을 입력하세요.');
  300. return;
  301. }
  302. if (!form.value.supplyPrice) {
  303. $toast.error('공급가를 입력하세요.');
  304. return;
  305. }
  306. if (!form.value.sellPrice) {
  307. $toast.error('판매가를 입력하세요.');
  308. return;
  309. }
  310. if (!form.value.shippingCost) {
  311. $toast.error('배송비를 입력하세요.');
  312. return;
  313. }
  314. loading.value = true;
  315. try {
  316. // FormData 생성 (파일 업로드용)
  317. const formData = new FormData();
  318. formData.append('productName', form.value.productName);
  319. formData.append('supplyPrice', form.value.supplyPrice);
  320. formData.append('sellPrice', form.value.sellPrice);
  321. formData.append('shippingCost', form.value.shippingCost);
  322. formData.append('subtitle', form.value.subtitle);
  323. formData.append('detailContent', form.value.detailContent);
  324. formData.append('status', form.value.status);
  325. formData.append('displayStatus', form.value.displayStatus);
  326. formData.append('updateHistory', form.value.updateHistory);
  327. formData.append('compId', useAuthStore().getCompanyId);
  328. if (form.value.detailFile) {
  329. formData.append('detailFile', form.value.detailFile);
  330. }
  331. await useAxios()
  332. .post("/product/register", formData, {
  333. headers: {
  334. 'Content-Type': 'multipart/form-data'
  335. }
  336. })
  337. .then((res) => {
  338. if (res.data.success) {
  339. $toast.success('제품이 등록되었습니다.');
  340. router.push('/view/vendor');
  341. } else {
  342. $toast.error('제품 등록에 실패했습니다.');
  343. }
  344. });
  345. } catch (error) {
  346. $log.error('제품 등록 오류:', error);
  347. $toast.error('제품 등록 중 오류가 발생했습니다.');
  348. } finally {
  349. loading.value = false;
  350. }
  351. };
  352. </script>
  353. <style scoped>
  354. .product-register--form {
  355. margin: 20px 0;
  356. }
  357. .form--container {
  358. background: white;
  359. border-radius: 8px;
  360. padding: 30px;
  361. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  362. }
  363. .form--section {
  364. margin-bottom: 40px;
  365. }
  366. .form--section h3 {
  367. font-size: 18px;
  368. font-weight: 600;
  369. color: #333;
  370. margin-bottom: 20px;
  371. padding-bottom: 10px;
  372. border-bottom: 2px solid #f0f0f0;
  373. }
  374. .form--row {
  375. display: flex;
  376. gap: 20px;
  377. margin-bottom: 20px;
  378. }
  379. .form--field {
  380. flex: 1;
  381. }
  382. .form--field.full-width {
  383. width: 100%;
  384. }
  385. .form--label {
  386. display: block;
  387. font-size: 14px;
  388. font-weight: 500;
  389. color: #333;
  390. margin-bottom: 8px;
  391. }
  392. .form--label .required {
  393. color: #f44336;
  394. }
  395. .custom-input {
  396. width: 100%;
  397. }
  398. .custom-select {
  399. width: 100%;
  400. }
  401. /* 에디터 스타일 */
  402. .editor--container {
  403. border: 1px solid #ddd;
  404. border-radius: 4px;
  405. overflow: hidden;
  406. }
  407. .editor--toolbar {
  408. background: #f8f9fa;
  409. padding: 10px;
  410. border-bottom: 1px solid #ddd;
  411. }
  412. .editor--mode-toggle {
  413. background: white;
  414. border-radius: 4px;
  415. }
  416. .html-textarea {
  417. width: 100%;
  418. border: none;
  419. outline: none;
  420. padding: 15px;
  421. font-family: 'Courier New', monospace;
  422. font-size: 14px;
  423. resize: vertical;
  424. min-height: 400px;
  425. }
  426. .text-editor {
  427. padding: 15px;
  428. }
  429. /* 파일 업로드 스타일 */
  430. .file-upload--container {
  431. width: 100%;
  432. }
  433. .file-upload--area {
  434. border: 2px dashed #ddd;
  435. border-radius: 8px;
  436. padding: 30px;
  437. text-align: center;
  438. cursor: pointer;
  439. transition: all 0.3s ease;
  440. }
  441. .file-upload--area:hover {
  442. border-color: #3f51b5;
  443. background: #f8f9ff;
  444. }
  445. .file-upload--placeholder {
  446. color: #666;
  447. }
  448. .file-upload--placeholder .upload-icon {
  449. font-size: 48px;
  450. display: block;
  451. margin-bottom: 10px;
  452. }
  453. .file-upload--placeholder p {
  454. font-size: 16px;
  455. margin: 10px 0;
  456. }
  457. .file-upload--placeholder small {
  458. color: #999;
  459. }
  460. .file-upload--selected {
  461. display: flex;
  462. align-items: center;
  463. gap: 15px;
  464. padding: 15px;
  465. background: #f8f9fa;
  466. border-radius: 4px;
  467. }
  468. .file-upload--selected .file-icon {
  469. font-size: 24px;
  470. }
  471. .file-info {
  472. flex: 1;
  473. text-align: left;
  474. }
  475. .file-name {
  476. font-weight: 500;
  477. margin: 0;
  478. }
  479. .file-size {
  480. color: #666;
  481. }
  482. .file-remove {
  483. margin-left: auto;
  484. }
  485. /* 액션 버튼 스타일 */
  486. .form--actions {
  487. display: flex;
  488. justify-content: center;
  489. gap: 15px;
  490. margin-top: 40px;
  491. padding-top: 20px;
  492. border-top: 1px solid #e0e0e0;
  493. }
  494. .custom-btn {
  495. padding: 12px 30px;
  496. font-size: 14px;
  497. font-weight: 500;
  498. border-radius: 4px;
  499. min-width: 120px;
  500. }
  501. .btn-white {
  502. background: white;
  503. color: #666;
  504. border: 1px solid #ddd;
  505. }
  506. .btn-blue {
  507. background: #3f51b5;
  508. color: white;
  509. }
  510. /* 반응형 */
  511. @media (max-width: 768px) {
  512. .form--row {
  513. flex-direction: column;
  514. gap: 15px;
  515. }
  516. .form--container {
  517. padding: 20px;
  518. }
  519. .form--actions {
  520. flex-direction: column;
  521. }
  522. .custom-btn {
  523. width: 100%;
  524. }
  525. }
  526. </style>