create.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <template>
  2. <div class="admin--sales-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. <select v-model="formData.showroom_id" class="admin--form-select" required>
  8. <option value="">전시장을 선택하세요</option>
  9. <option v-for="showroom in showrooms" :key="showroom.id" :value="showroom.id">
  10. {{ showroom.name }}
  11. </option>
  12. </select>
  13. </div>
  14. <!-- 지점 -->
  15. <div class="admin--form-group">
  16. <label class="admin--form-label">지점 <span class="admin--required">*</span></label>
  17. <select v-model="formData.branch_id" class="admin--form-select" required>
  18. <option value="">지점을 선택하세요</option>
  19. <option v-for="branch in branches" :key="branch.id" :value="branch.id">
  20. {{ branch.name }}
  21. </option>
  22. </select>
  23. </div>
  24. <!-- 영업팀 -->
  25. <div class="admin--form-group">
  26. <label class="admin--form-label">영업팀 <span class="admin--required">*</span></label>
  27. <select v-model="formData.team_id" class="admin--form-select" required>
  28. <option value="">영업팀을 선택하세요</option>
  29. <option v-for="team in teams" :key="team.id" :value="team.id">
  30. {{ team.name }}
  31. </option>
  32. </select>
  33. </div>
  34. <!-- 이름 -->
  35. <div class="admin--form-group">
  36. <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
  37. <input
  38. v-model="formData.name"
  39. type="text"
  40. class="admin--form-input"
  41. placeholder="이름을 입력하세요"
  42. required
  43. >
  44. </div>
  45. <!-- 직책 -->
  46. <div class="admin--form-group">
  47. <label class="admin--form-label">직책 <span class="admin--required">*</span></label>
  48. <select v-model="formData.position" class="admin--form-select" required>
  49. <option value="">직책을 선택하세요</option>
  50. <option value="팀장">팀장</option>
  51. <option value="마스터">마스터</option>
  52. <option value="차장">차장</option>
  53. <option value="과장">과장</option>
  54. <option value="대리">대리</option>
  55. <option value="주임">주임</option>
  56. <option value="사원">사원</option>
  57. </select>
  58. </div>
  59. <!-- 대표번호 -->
  60. <div class="admin--form-group">
  61. <label class="admin--form-label">대표번호 <span class="admin--required">*</span></label>
  62. <input
  63. v-model="formData.main_phone"
  64. type="tel"
  65. class="admin--form-input"
  66. placeholder="02-1234-5678"
  67. required
  68. >
  69. </div>
  70. <!-- 직통번호 -->
  71. <div class="admin--form-group">
  72. <label class="admin--form-label">직통번호</label>
  73. <input
  74. v-model="formData.direct_phone"
  75. type="tel"
  76. class="admin--form-input"
  77. placeholder="02-1234-5679"
  78. >
  79. </div>
  80. <!-- 핸드폰 -->
  81. <div class="admin--form-group">
  82. <label class="admin--form-label">핸드폰</label>
  83. <input
  84. v-model="formData.mobile"
  85. type="tel"
  86. class="admin--form-input"
  87. placeholder="010-1234-5678"
  88. >
  89. </div>
  90. <!-- 이메일 -->
  91. <div class="admin--form-group">
  92. <label class="admin--form-label">이메일</label>
  93. <input
  94. v-model="formData.email"
  95. type="email"
  96. class="admin--form-input"
  97. placeholder="email@example.com"
  98. >
  99. </div>
  100. <!-- 사진 -->
  101. <div class="admin--form-group">
  102. <label class="admin--form-label">사진</label>
  103. <input
  104. type="file"
  105. accept="image/*"
  106. class="admin--form-file"
  107. @change="handlePhotoUpload"
  108. >
  109. <div v-if="photoPreview" class="admin--image-preview">
  110. <img :src="photoPreview" alt="미리보기">
  111. <button type="button" class="admin--btn-remove-image" @click="removePhoto">
  112. 삭제
  113. </button>
  114. </div>
  115. </div>
  116. <!-- SACT -->
  117. <div class="admin--form-group">
  118. <label class="admin--form-label">SACT</label>
  119. <div class="admin--radio-group">
  120. <label class="admin--radio-label">
  121. <input v-model="formData.is_sact" type="radio" :value="true" name="is_sact">
  122. <span>예</span>
  123. </label>
  124. <label class="admin--radio-label">
  125. <input v-model="formData.is_sact" type="radio" :value="false" name="is_sact">
  126. <span>아니오</span>
  127. </label>
  128. </div>
  129. </div>
  130. <!-- TOP30 -->
  131. <div class="admin--form-group">
  132. <label class="admin--form-label">TOP30</label>
  133. <div class="admin--radio-group">
  134. <label class="admin--radio-label">
  135. <input v-model="formData.is_top30" type="radio" :value="true" name="is_top30">
  136. <span>예</span>
  137. </label>
  138. <label class="admin--radio-label">
  139. <input v-model="formData.is_top30" type="radio" :value="false" name="is_top30">
  140. <span>아니오</span>
  141. </label>
  142. </div>
  143. </div>
  144. <!-- 노출순서 -->
  145. <div class="admin--form-group">
  146. <label class="admin--form-label">노출순서</label>
  147. <input
  148. v-model.number="formData.display_order"
  149. type="number"
  150. class="admin--form-input"
  151. placeholder="숫자만 입력"
  152. min="0"
  153. >
  154. <p class="admin--form-help">숫자가 작을수록 먼저 노출됩니다.</p>
  155. </div>
  156. <!-- 버튼 영역 -->
  157. <div class="admin--form-actions">
  158. <button
  159. type="submit"
  160. class="admin--btn admin--btn-primary"
  161. :disabled="isSaving"
  162. >
  163. {{ isSaving ? '저장 중...' : '확인' }}
  164. </button>
  165. <button
  166. type="button"
  167. class="admin--btn admin--btn-secondary"
  168. @click="goToList"
  169. >
  170. 목록
  171. </button>
  172. </div>
  173. <!-- 성공/에러 메시지 -->
  174. <div v-if="successMessage" class="admin--alert admin--alert-success">
  175. {{ successMessage }}
  176. </div>
  177. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  178. {{ errorMessage }}
  179. </div>
  180. </form>
  181. </div>
  182. </template>
  183. <script setup>
  184. import { ref, onMounted } from 'vue'
  185. import { useRouter } from 'vue-router'
  186. definePageMeta({
  187. layout: 'admin',
  188. middleware: ['auth']
  189. })
  190. const router = useRouter()
  191. const { get, post, upload } = useApi()
  192. const isSaving = ref(false)
  193. const successMessage = ref('')
  194. const errorMessage = ref('')
  195. const photoPreview = ref(null)
  196. const photoFile = ref(null)
  197. const showrooms = ref([])
  198. const branches = ref([])
  199. const teams = ref([])
  200. const formData = ref({
  201. showroom_id: '',
  202. branch_id: '',
  203. team_id: '',
  204. name: '',
  205. position: '',
  206. main_phone: '',
  207. direct_phone: '',
  208. mobile: '',
  209. email: '',
  210. photo_url: '',
  211. is_sact: false,
  212. is_top30: false,
  213. display_order: 0
  214. })
  215. // 필터 데이터 로드
  216. const loadFilters = async () => {
  217. const { data: showroomData } = await get('/staff/showrooms')
  218. if (showroomData) showrooms.value = showroomData
  219. const { data: branchData } = await get('/branch/list', { per_page: 1000 })
  220. if (branchData) branches.value = branchData.items || []
  221. const { data: teamData } = await get('/staff/teams')
  222. if (teamData) teams.value = teamData
  223. }
  224. // 사진 업로드
  225. const handlePhotoUpload = (event) => {
  226. const file = event.target.files[0]
  227. if (!file) return
  228. if (!file.type.startsWith('image/')) {
  229. alert('이미지 파일만 업로드 가능합니다.')
  230. return
  231. }
  232. photoFile.value = file
  233. const reader = new FileReader()
  234. reader.onload = (e) => {
  235. photoPreview.value = e.target.result
  236. }
  237. reader.readAsDataURL(file)
  238. }
  239. // 사진 삭제
  240. const removePhoto = () => {
  241. photoPreview.value = null
  242. photoFile.value = null
  243. formData.value.photo_url = ''
  244. }
  245. // 폼 제출
  246. const handleSubmit = async () => {
  247. successMessage.value = ''
  248. errorMessage.value = ''
  249. if (!formData.value.showroom_id || !formData.value.branch_id || !formData.value.team_id) {
  250. errorMessage.value = '전시장, 지점, 영업팀을 선택하세요.'
  251. return
  252. }
  253. if (!formData.value.name) {
  254. errorMessage.value = '이름을 입력하세요.'
  255. return
  256. }
  257. isSaving.value = true
  258. try {
  259. let photoUrl = formData.value.photo_url
  260. // 사진 업로드
  261. if (photoFile.value) {
  262. const formDataImage = new FormData()
  263. formDataImage.append('image', photoFile.value)
  264. const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
  265. if (uploadError) {
  266. errorMessage.value = '사진 업로드에 실패했습니다.'
  267. isSaving.value = false
  268. return
  269. }
  270. photoUrl = uploadData.url
  271. }
  272. const submitData = {
  273. ...formData.value,
  274. photo_url: photoUrl
  275. }
  276. const { data, error } = await post('/staff/sales', submitData)
  277. if (error) {
  278. errorMessage.value = error.message || '등록에 실패했습니다.'
  279. } else {
  280. successMessage.value = '영업사원이 등록되었습니다.'
  281. setTimeout(() => {
  282. router.push('/admin/staff/sales')
  283. }, 1000)
  284. }
  285. } catch (error) {
  286. errorMessage.value = '서버 오류가 발생했습니다.'
  287. console.error('Save error:', error)
  288. } finally {
  289. isSaving.value = false
  290. }
  291. }
  292. const goToList = () => {
  293. router.push('/admin/staff/sales')
  294. }
  295. onMounted(() => {
  296. loadFilters()
  297. })
  298. </script>