create.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <template>
  2. <div class="admin--news-form">
  3. <form @submit.prevent="handleSubmit" class="admin--form">
  4. <!-- 댓글허용 -->
  5. <div class="admin--form-group">
  6. <label class="admin--form-label">댓글허용</label>
  7. <div class="admin--checkbox-group">
  8. <label class="admin--checkbox-label">
  9. <input v-model="formData.allow_comment" type="checkbox" />
  10. <span>댓글 허용</span>
  11. </label>
  12. </div>
  13. </div>
  14. <!-- 공지 -->
  15. <div class="admin--form-group">
  16. <label class="admin--form-label">공지</label>
  17. <div class="admin--checkbox-group">
  18. <label class="admin--checkbox-label">
  19. <input v-model="formData.is_notice" type="checkbox" />
  20. <span>공지글로 등록</span>
  21. </label>
  22. </div>
  23. </div>
  24. <!-- 이름 -->
  25. <div class="admin--form-group">
  26. <label class="admin--form-label"
  27. >이름 <span class="admin--required">*</span></label
  28. >
  29. <input
  30. v-model="formData.name"
  31. type="text"
  32. class="admin--form-input"
  33. placeholder="이름을 입력하세요"
  34. required
  35. />
  36. </div>
  37. <!-- 이메일 -->
  38. <div class="admin--form-group">
  39. <label class="admin--form-label"
  40. >이메일 <span class="admin--required">*</span></label
  41. >
  42. <input
  43. v-model="formData.email"
  44. type="email"
  45. class="admin--form-input"
  46. placeholder="이메일을 입력하세요"
  47. required
  48. />
  49. </div>
  50. <!-- URL -->
  51. <div class="admin--form-group">
  52. <label class="admin--form-label">URL</label>
  53. <input
  54. v-model="formData.url"
  55. type="url"
  56. class="admin--form-input"
  57. placeholder="https://example.com"
  58. />
  59. </div>
  60. <!-- 제목 -->
  61. <div class="admin--form-group">
  62. <label class="admin--form-label"
  63. >제목 <span class="admin--required">*</span></label
  64. >
  65. <input
  66. v-model="formData.title"
  67. type="text"
  68. class="admin--form-input"
  69. placeholder="제목을 입력하세요"
  70. required
  71. />
  72. </div>
  73. <!-- 내용 -->
  74. <div class="admin--form-group">
  75. <label class="admin--form-label"
  76. >내용 <span class="admin--required">*</span></label
  77. >
  78. <SunEditor v-model="formData.content" />
  79. </div>
  80. <!-- 파일첨부 -->
  81. <div class="admin--form-group">
  82. <label class="admin--form-label">파일첨부</label>
  83. <div class="admin--file-list">
  84. <div
  85. v-for="(file, index) in attachedFiles"
  86. :key="index"
  87. class="admin--file-item"
  88. >
  89. <span class="admin--file-name">{{ file.name }}</span>
  90. <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
  91. <button
  92. type="button"
  93. class="admin--btn-remove-file"
  94. @click="removeFile(index)"
  95. >
  96. 삭제
  97. </button>
  98. </div>
  99. </div>
  100. <input
  101. ref="fileInput"
  102. type="file"
  103. multiple
  104. class="admin--form-file-hidden"
  105. @change="handleFileAdd"
  106. />
  107. <button
  108. type="button"
  109. class="admin--btn admin--btn-secondary"
  110. @click="triggerFileInput"
  111. >
  112. 파일 추가
  113. </button>
  114. </div>
  115. <!-- 버튼 영역 -->
  116. <div class="admin--form-actions">
  117. <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
  118. {{ isSaving ? "저장 중..." : "확인" }}
  119. </button>
  120. <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
  121. 목록
  122. </button>
  123. </div>
  124. <!-- 성공/에러 메시지 -->
  125. <div v-if="successMessage" class="admin--alert admin--alert-success">
  126. {{ successMessage }}
  127. </div>
  128. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  129. {{ errorMessage }}
  130. </div>
  131. </form>
  132. </div>
  133. </template>
  134. <script setup>
  135. import { ref } from "vue";
  136. import { useRouter } from "vue-router";
  137. import SunEditor from "~/components/admin/SunEditor.vue";
  138. definePageMeta({
  139. layout: "admin",
  140. middleware: ["auth"],
  141. });
  142. const router = useRouter();
  143. const { post, upload } = useApi();
  144. const isSaving = ref(false);
  145. const successMessage = ref("");
  146. const errorMessage = ref("");
  147. const attachedFiles = ref([]);
  148. const fileInput = ref(null);
  149. const formData = ref({
  150. allow_comment: false,
  151. is_notice: false,
  152. name: "고진",
  153. email: "admin@admin.kr",
  154. url: "",
  155. title: "",
  156. content: "",
  157. file_urls: [],
  158. });
  159. const triggerFileInput = () => {
  160. fileInput.value?.click();
  161. };
  162. const handleFileAdd = (event) => {
  163. const files = Array.from(event.target.files);
  164. attachedFiles.value.push(...files);
  165. event.target.value = "";
  166. };
  167. const removeFile = (index) => {
  168. attachedFiles.value.splice(index, 1);
  169. };
  170. const formatFileSize = (bytes) => {
  171. if (bytes === 0) return "0 Bytes";
  172. const k = 1024;
  173. const sizes = ["Bytes", "KB", "MB", "GB"];
  174. const i = Math.floor(Math.log(bytes) / Math.log(k));
  175. return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
  176. };
  177. const handleSubmit = async () => {
  178. successMessage.value = "";
  179. errorMessage.value = "";
  180. if (!formData.value.title) {
  181. errorMessage.value = "제목을 입력하세요.";
  182. return;
  183. }
  184. if (!formData.value.content) {
  185. errorMessage.value = "내용을 입력하세요.";
  186. return;
  187. }
  188. isSaving.value = true;
  189. try {
  190. let fileUrls = [];
  191. // 파일 업로드
  192. if (attachedFiles.value.length > 0) {
  193. for (const file of attachedFiles.value) {
  194. const formDataFile = new FormData();
  195. formDataFile.append("file", file);
  196. const { data: uploadData, error: uploadError } = await upload(
  197. "/upload/news-file",
  198. formDataFile
  199. );
  200. if (uploadError) {
  201. errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`;
  202. isSaving.value = false;
  203. return;
  204. }
  205. fileUrls.push({
  206. name: file.name,
  207. url: uploadData.data.url,
  208. size: file.size,
  209. });
  210. }
  211. }
  212. // content에서 도메인 제거
  213. let contentToSave = formData.value.content;
  214. if (contentToSave) {
  215. // http://도메인 또는 https://도메인 제거
  216. contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, "");
  217. }
  218. const submitData = {
  219. ...formData.value,
  220. allow_comment: formData.value.allow_comment ? 1 : 0,
  221. is_notice: formData.value.is_notice ? 1 : 0,
  222. content: contentToSave,
  223. file_urls: fileUrls,
  224. };
  225. const { data, error } = await post("/board/notice", submitData);
  226. if (error) {
  227. errorMessage.value = error.message || "등록에 실패했습니다.";
  228. } else {
  229. successMessage.value = "뉴스가 등록되었습니다.";
  230. setTimeout(() => {
  231. router.push("/site-manager/board/notice");
  232. }, 1000);
  233. }
  234. } catch (error) {
  235. errorMessage.value = "서버 오류가 발생했습니다.";
  236. console.error("Save error:", error);
  237. } finally {
  238. isSaving.value = false;
  239. }
  240. };
  241. const goToList = () => {
  242. router.push("/site-manager/board/notice");
  243. };
  244. </script>