create.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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. <DatePicker
  44. v-model="formData.start_date"
  45. placeholder="시작일 선택"
  46. :max-date="formData.end_date"
  47. required
  48. />
  49. <span class="admin--date-separator">~</span>
  50. <DatePicker
  51. v-model="formData.end_date"
  52. placeholder="종료일 선택"
  53. :min-date="formData.start_date"
  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_setting"
  125. type="radio"
  126. value="today"
  127. name="cookie_setting"
  128. >
  129. <span>오늘 하루 창 띄우지 않음</span>
  130. </label>
  131. <label class="admin--radio-label">
  132. <input
  133. v-model="formData.cookie_setting"
  134. type="radio"
  135. value="forever"
  136. name="cookie_setting"
  137. >
  138. <span>다시는 창을 띄우지 않음</span>
  139. </label>
  140. <label class="admin--radio-label">
  141. <input
  142. v-model="formData.cookie_setting"
  143. type="radio"
  144. value="none"
  145. name="cookie_setting"
  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. import DatePicker from '~/components/admin/DatePicker.vue'
  222. definePageMeta({
  223. layout: 'admin',
  224. middleware: ['auth']
  225. })
  226. const router = useRouter()
  227. const { post, upload } = useApi()
  228. const isSaving = ref(false)
  229. const successMessage = ref('')
  230. const errorMessage = ref('')
  231. const imagePreview = ref(null)
  232. const imageFile = ref(null)
  233. const formData = ref({
  234. type: 'html',
  235. title: '',
  236. start_date: '',
  237. end_date: '',
  238. width: 800,
  239. height: 600,
  240. position_top: 100,
  241. position_left: 100,
  242. cookie_setting: 'none',
  243. content: '',
  244. image_url: '',
  245. link_url: ''
  246. })
  247. // 이미지 업로드
  248. const handleImageUpload = (event) => {
  249. const file = event.target.files[0]
  250. if (!file) return
  251. if (!file.type.startsWith('image/')) {
  252. alert('이미지 파일만 업로드 가능합니다.')
  253. return
  254. }
  255. imageFile.value = file
  256. // 미리보기
  257. const reader = new FileReader()
  258. reader.onload = (e) => {
  259. imagePreview.value = e.target.result
  260. }
  261. reader.readAsDataURL(file)
  262. }
  263. // 이미지 삭제
  264. const removeImage = () => {
  265. imagePreview.value = null
  266. imageFile.value = null
  267. formData.value.image_url = ''
  268. }
  269. // 폼 제출
  270. const handleSubmit = async () => {
  271. successMessage.value = ''
  272. errorMessage.value = ''
  273. // 유효성 검사
  274. if (!formData.value.title) {
  275. errorMessage.value = '제목을 입력하세요.'
  276. return
  277. }
  278. if (!formData.value.start_date || !formData.value.end_date) {
  279. errorMessage.value = '시작일과 종료일을 선택하세요.'
  280. return
  281. }
  282. if (formData.value.type === 'html' && !formData.value.content) {
  283. errorMessage.value = '출력내용을 입력하세요.'
  284. return
  285. }
  286. if (formData.value.type === 'image' && !imageFile.value && !formData.value.image_url) {
  287. errorMessage.value = '이미지를 첨부하세요.'
  288. return
  289. }
  290. isSaving.value = true
  291. try {
  292. let imageUrl = formData.value.image_url
  293. // 이미지 업로드 (단일페이지인 경우)
  294. if (formData.value.type === 'image' && imageFile.value) {
  295. const formDataImage = new FormData()
  296. formDataImage.append('file', imageFile.value)
  297. const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
  298. console.log('[Popup Create] 이미지 업로드 응답:', { uploadData, uploadError })
  299. if (uploadError || !uploadData?.success) {
  300. errorMessage.value = uploadError?.message || '이미지 업로드에 실패했습니다.'
  301. isSaving.value = false
  302. return
  303. }
  304. imageUrl = uploadData.data?.url || uploadData.data
  305. }
  306. // 팝업 등록
  307. const submitData = {
  308. ...formData.value,
  309. image_url: imageUrl
  310. }
  311. const { data, error } = await post('/basic/popup', submitData)
  312. if (error || !data?.success) {
  313. errorMessage.value = error?.message || data?.message || '등록에 실패했습니다.'
  314. } else {
  315. successMessage.value = data.message || '팝업이 등록되었습니다.'
  316. setTimeout(() => {
  317. router.push('/admin/basic/popup')
  318. }, 1000)
  319. }
  320. } catch (error) {
  321. errorMessage.value = '서버 오류가 발생했습니다.'
  322. console.error('Save error:', error)
  323. } finally {
  324. isSaving.value = false
  325. }
  326. }
  327. // 목록으로 이동
  328. const goToList = () => {
  329. router.push('/admin/basic/popup')
  330. }
  331. </script>