create.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <template>
  2. <div class="admin--popup-form">
  3. <form @submit.prevent="handleSubmit" class="admin--form">
  4. <!-- 형태 -->
  5. <div class="admin--form-group">
  6. <label class="admin--form-label">형태 <span class="admin--required">*</span></label>
  7. <div class="admin--radio-group">
  8. <label class="admin--radio-label">
  9. <input
  10. v-model="formData.type"
  11. type="radio"
  12. value="html"
  13. name="type"
  14. >
  15. <span>HTML</span>
  16. </label>
  17. <label class="admin--radio-label">
  18. <input
  19. v-model="formData.type"
  20. type="radio"
  21. value="image"
  22. name="type"
  23. >
  24. <span>단일페이지</span>
  25. </label>
  26. </div>
  27. </div>
  28. <!-- 제목 -->
  29. <div class="admin--form-group">
  30. <label class="admin--form-label">제목 <span class="admin--required">*</span></label>
  31. <input
  32. v-model="formData.title"
  33. type="text"
  34. class="admin--form-input"
  35. placeholder="팝업 제목을 입력하세요"
  36. required
  37. >
  38. </div>
  39. <!-- 시작일/종료일 -->
  40. <div class="admin--form-group">
  41. <label class="admin--form-label">시작일/종료일 <span class="admin--required">*</span></label>
  42. <div class="admin--date-range">
  43. <input
  44. v-model="formData.start_date"
  45. type="date"
  46. class="admin--form-input"
  47. required
  48. >
  49. <span class="admin--date-separator">~</span>
  50. <input
  51. v-model="formData.end_date"
  52. type="date"
  53. class="admin--form-input"
  54. required
  55. >
  56. </div>
  57. </div>
  58. <!-- 팝업창 사이즈 -->
  59. <div class="admin--form-group">
  60. <label class="admin--form-label">팝업창 사이즈 <span class="admin--required">*</span></label>
  61. <div class="admin--size-group">
  62. <div class="admin--size-item">
  63. <label>가로</label>
  64. <input
  65. v-model.number="formData.width"
  66. type="number"
  67. class="admin--form-input"
  68. placeholder="800"
  69. min="100"
  70. required
  71. >
  72. <span>px</span>
  73. </div>
  74. <div class="admin--size-item">
  75. <label>세로</label>
  76. <input
  77. v-model.number="formData.height"
  78. type="number"
  79. class="admin--form-input"
  80. placeholder="600"
  81. min="100"
  82. required
  83. >
  84. <span>px</span>
  85. </div>
  86. </div>
  87. </div>
  88. <!-- 팝업창 위치 -->
  89. <div class="admin--form-group">
  90. <label class="admin--form-label">팝업창 위치 <span class="admin--required">*</span></label>
  91. <div class="admin--size-group">
  92. <div class="admin--size-item">
  93. <label>TOP</label>
  94. <input
  95. v-model.number="formData.position_top"
  96. type="number"
  97. class="admin--form-input"
  98. placeholder="100"
  99. min="0"
  100. required
  101. >
  102. <span>px</span>
  103. </div>
  104. <div class="admin--size-item">
  105. <label>LEFT</label>
  106. <input
  107. v-model.number="formData.position_left"
  108. type="number"
  109. class="admin--form-input"
  110. placeholder="100"
  111. min="0"
  112. required
  113. >
  114. <span>px</span>
  115. </div>
  116. </div>
  117. </div>
  118. <!-- 쿠키설정 -->
  119. <div class="admin--form-group">
  120. <label class="admin--form-label">쿠키설정</label>
  121. <div class="admin--radio-group">
  122. <label class="admin--radio-label">
  123. <input
  124. v-model="formData.cookie_type"
  125. type="radio"
  126. value="today"
  127. name="cookie_type"
  128. >
  129. <span>오늘 하루 창 띄우지 않음</span>
  130. </label>
  131. <label class="admin--radio-label">
  132. <input
  133. v-model="formData.cookie_type"
  134. type="radio"
  135. value="forever"
  136. name="cookie_type"
  137. >
  138. <span>다시는 창을 띄우지 않음</span>
  139. </label>
  140. <label class="admin--radio-label">
  141. <input
  142. v-model="formData.cookie_type"
  143. type="radio"
  144. value="none"
  145. name="cookie_type"
  146. >
  147. <span>사용 안 함</span>
  148. </label>
  149. </div>
  150. </div>
  151. <!-- 출력내용 (HTML) -->
  152. <div v-if="formData.type === 'html'" class="admin--form-group">
  153. <label class="admin--form-label">출력내용 <span class="admin--required">*</span></label>
  154. <SunEditor
  155. v-model="formData.content"
  156. height="400px"
  157. placeholder="팝업 내용을 입력하세요"
  158. />
  159. </div>
  160. <!-- 출력내용 (이미지) -->
  161. <div v-if="formData.type === 'image'" class="admin--form-group">
  162. <label class="admin--form-label">이미지 첨부 <span class="admin--required">*</span></label>
  163. <input
  164. type="file"
  165. accept="image/*"
  166. class="admin--form-file"
  167. @change="handleImageUpload"
  168. >
  169. <div v-if="imagePreview" class="admin--image-preview">
  170. <img :src="imagePreview" alt="미리보기">
  171. <button
  172. type="button"
  173. class="admin--btn-remove-image"
  174. @click="removeImage"
  175. >
  176. 삭제
  177. </button>
  178. </div>
  179. </div>
  180. <!-- 링크 URL (단일페이지일 경우) -->
  181. <div v-if="formData.type === 'image'" class="admin--form-group">
  182. <label class="admin--form-label">링크 URL</label>
  183. <input
  184. v-model="formData.link_url"
  185. type="url"
  186. class="admin--form-input"
  187. placeholder="https://example.com"
  188. >
  189. </div>
  190. <!-- 버튼 영역 -->
  191. <div class="admin--form-actions">
  192. <button
  193. type="submit"
  194. class="admin--btn admin--btn-primary"
  195. :disabled="isSaving"
  196. >
  197. {{ isSaving ? '저장 중...' : '확인' }}
  198. </button>
  199. <button
  200. type="button"
  201. class="admin--btn admin--btn-secondary"
  202. @click="goToList"
  203. >
  204. 목록
  205. </button>
  206. </div>
  207. <!-- 성공/에러 메시지 -->
  208. <div v-if="successMessage" class="admin--alert admin--alert-success">
  209. {{ successMessage }}
  210. </div>
  211. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  212. {{ errorMessage }}
  213. </div>
  214. </form>
  215. </div>
  216. </template>
  217. <script setup>
  218. import { ref } from 'vue'
  219. import { useRouter } from 'vue-router'
  220. import SunEditor from '~/components/admin/SunEditor.vue'
  221. definePageMeta({
  222. layout: 'admin',
  223. middleware: ['auth']
  224. })
  225. const router = useRouter()
  226. const { post, upload } = useApi()
  227. const isSaving = ref(false)
  228. const successMessage = ref('')
  229. const errorMessage = ref('')
  230. const imagePreview = ref(null)
  231. const imageFile = ref(null)
  232. const formData = ref({
  233. type: 'html',
  234. title: '',
  235. start_date: '',
  236. end_date: '',
  237. width: 800,
  238. height: 600,
  239. position_top: 100,
  240. position_left: 100,
  241. cookie_type: 'none',
  242. content: '',
  243. image_url: '',
  244. link_url: ''
  245. })
  246. // 이미지 업로드
  247. const handleImageUpload = (event) => {
  248. const file = event.target.files[0]
  249. if (!file) return
  250. if (!file.type.startsWith('image/')) {
  251. alert('이미지 파일만 업로드 가능합니다.')
  252. return
  253. }
  254. imageFile.value = file
  255. // 미리보기
  256. const reader = new FileReader()
  257. reader.onload = (e) => {
  258. imagePreview.value = e.target.result
  259. }
  260. reader.readAsDataURL(file)
  261. }
  262. // 이미지 삭제
  263. const removeImage = () => {
  264. imagePreview.value = null
  265. imageFile.value = null
  266. formData.value.image_url = ''
  267. }
  268. // 폼 제출
  269. const handleSubmit = async () => {
  270. successMessage.value = ''
  271. errorMessage.value = ''
  272. // 유효성 검사
  273. if (!formData.value.title) {
  274. errorMessage.value = '제목을 입력하세요.'
  275. return
  276. }
  277. if (!formData.value.start_date || !formData.value.end_date) {
  278. errorMessage.value = '시작일과 종료일을 선택하세요.'
  279. return
  280. }
  281. if (formData.value.type === 'html' && !formData.value.content) {
  282. errorMessage.value = '출력내용을 입력하세요.'
  283. return
  284. }
  285. if (formData.value.type === 'image' && !imageFile.value && !formData.value.image_url) {
  286. errorMessage.value = '이미지를 첨부하세요.'
  287. return
  288. }
  289. isSaving.value = true
  290. try {
  291. let imageUrl = formData.value.image_url
  292. // 이미지 업로드 (단일페이지인 경우)
  293. if (formData.value.type === 'image' && imageFile.value) {
  294. const formDataImage = new FormData()
  295. formDataImage.append('image', imageFile.value)
  296. const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
  297. if (uploadError) {
  298. errorMessage.value = '이미지 업로드에 실패했습니다.'
  299. isSaving.value = false
  300. return
  301. }
  302. imageUrl = uploadData.url
  303. }
  304. // 팝업 등록
  305. const submitData = {
  306. ...formData.value,
  307. image_url: imageUrl
  308. }
  309. const { data, error } = await post('/basic/popup', submitData)
  310. if (error) {
  311. errorMessage.value = error.message || '등록에 실패했습니다.'
  312. } else {
  313. successMessage.value = '팝업이 등록되었습니다.'
  314. setTimeout(() => {
  315. router.push('/admin/basic/popup')
  316. }, 1000)
  317. }
  318. } catch (error) {
  319. errorMessage.value = '서버 오류가 발생했습니다.'
  320. console.error('Save error:', error)
  321. } finally {
  322. isSaving.value = false
  323. }
  324. }
  325. // 목록으로 이동
  326. const goToList = () => {
  327. router.push('/admin/basic/popup')
  328. }
  329. </script>