Browse Source

+ axios 미들웨어 형태로 교체하여 모듈화

송용우 2 months ago
parent
commit
42286d6e3b
5 changed files with 409 additions and 256 deletions
  1. 20 0
      README.md
  2. 195 3
      app/assets/scss/sub.scss
  3. 60 253
      app/pages/contact/support.vue
  4. 119 0
      composables/useApi.ts
  5. 15 0
      plugins/api.client.ts

+ 20 - 0
README.md

@@ -2,6 +2,26 @@
 
 Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
 
+## axios 호출 방식
+
+```
+// GET 요청
+const data = await $api.get('/api/endpoint', { param1: 'value1' })
+
+// POST 요청 (JSON)
+const result = await $api.post('/api/endpoint', { data: 'value' })
+
+// POST 요청 (FormData)
+const result = await $api.postForm('/board_proc', formData)
+
+// PUT 요청
+const result = await $api.put('/api/endpoint', { data: 'value' })
+
+// DELETE 요청
+const result = await $api.delete('/api/endpoint', { id: 123 })
+
+```
+
 ## Setup
 
 Make sure to install dependencies:

+ 195 - 3
app/assets/scss/sub.scss

@@ -13,8 +13,7 @@
     background-attachment: fixed;  
     background-size: contain;
   }
-
-  
+ 
 
   .inner--content{
     max-height: 440px;
@@ -96,7 +95,8 @@
             top:60px;
             left:50%;
             transform: translateX(-50%);
-            background: #fff;
+            background: #fff;            
+            box-shadow: 0 8px 16px 0 rgba(31, 33, 40, 0.12);
             padding:16px;
             max-height: 0;
             overflow: hidden;
@@ -135,4 +135,196 @@
       }
     }
   }
+}
+
+
+/** 중앙 컨텐츠 **/
+#out--container{
+  width:100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding-top:100px;
+  .out--container{
+    max-width:844px;    
+
+    .m--title{
+      text-align: center;
+      color:#1F2128;
+      text-align: center;
+      font-family: Pretendard;
+      font-size: 48px;
+      font-style: normal;
+      font-weight: 700;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-bottom:60px;
+    }
+
+
+    .form--contents--wrap {
+      margin-top: 40px;
+      border-top:1px solid #1F2128;
+      padding-top:40px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap:20px;
+
+      .form--contents {
+        margin-bottom: 30px;        
+        &.half--cont{
+          max-width:100%;
+        }
+      }
+    }
+
+
+    .form--contents h3 {
+      font-size: 16px;
+      font-weight: 600;
+      margin-bottom: 10px;
+    }
+
+    .form--contents h3 .required {
+      color: #ff0000;
+      margin-left: 4px;
+    }
+
+    .form--contents input[type="text"],
+    .form--contents input[type="email"],
+    .form--contents select,
+    .form--contents textarea {
+      width: 100%;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      font-size: 14px;
+    }
+
+    .tel-group {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .tel-group select {
+      width: 100px;
+    }
+
+    .tel-group input {
+      flex: 1;
+    }
+
+    .tel-group span {
+      font-weight: 500;
+    }
+
+    .radio-group {
+      display: flex;
+      gap: 20px;
+      align-items: center;
+    }
+
+    .radio-group label {
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+    }
+
+    .radio-group input[type="radio"] {
+      width: auto;
+      margin-right: 5px;
+    }
+
+    .privacy-box {
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      padding: 20px;
+      background-color: #f9f9f9;
+    }
+
+    .privacy-content {
+      height: 300px;
+      overflow-y: auto;
+      padding: 15px;
+      background-color: #fff;
+      border: 1px solid #e0e0e0;
+      border-radius: 4px;
+      margin-bottom: 20px;
+      line-height: 1.8;
+    }
+
+    .agree-check {
+      padding: 15px;
+      background-color: #fff;
+      border: 1px solid #e0e0e0;
+      border-radius: 4px;
+    }
+
+    .agree-check h4 {
+      font-size: 14px;
+      font-weight: 600;
+      margin-bottom: 10px;
+    }
+
+    .btn--wrap {
+      text-align: center;
+      margin-top: 40px;
+    }
+
+    .btn-submit {
+      padding: 12px 40px;
+      background-color: #00a651;
+      color: #fff;
+      border: none;
+      border-radius: 4px;
+      font-size: 16px;
+      font-weight: 600;
+      cursor: pointer;
+      transition: background-color 0.3s;
+    }
+
+    .btn-submit:hover {
+      background-color: #008840;
+    }
+
+    .btn-submit:disabled {
+      background-color: #ccc;
+      cursor: not-allowed;
+    }
+
+    .map-section {
+      margin-top: 80px;
+      padding-top: 40px;
+      border-top: 1px solid #ddd;
+    }
+
+    .map-section h3 {
+      font-size: 24px;
+      font-weight: 600;
+      margin-bottom: 20px;
+    }
+
+    .info-list,
+    .contact-list {
+      list-style: none;
+      padding: 0;
+      margin: 0 0 20px 0;
+    }
+
+    .info-list li,
+    .contact-list li {
+      font-size: 14px;
+      line-height: 1.8;
+    }
+
+    .map-container {
+      height: 450px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      overflow: hidden;
+    }
+  }
 }

+ 60 - 253
app/pages/contact/support.vue

@@ -1,87 +1,70 @@
 <template>
   <main>
     <TopVisual :className="className" :title="title" :navigation="navigation" />
-    <section>
-      <div class="inn--container">
-        <h2>고객센터</h2>
+    <section id="out--container">
+      <div class="out--container">
+        <h2 class="m--title">고객센터</h2>
         <div class="form--contents--wrap">
-          <form @submit.prevent="submitForm">
-            <div class="form--contents">
+          <div class="contact-form">
+            <div class="form--contents half--cont">
               <h3>작성자 (성명) <span class="required">*</span></h3>
               <div>
-                <input 
-                  type="text" 
+                <UInput 
                   v-model="formData.name" 
                   placeholder="성명을 입력해주세요" 
-                  required 
                 />
               </div>
             </div>
-            <div class="form--contents">
+            <div class="form--contents half--cont">
               <h3>직책</h3>
               <div>
-                <input 
-                  type="text" 
+                <UInput 
                   v-model="formData.etc1" 
                   placeholder="직책을 입력해주세요" 
                 />
               </div>
             </div>
-            <div class="form--contents">
+            <div class="form--contents half--cont">
               <h3>회사명</h3>
               <div>
-                <input 
-                  type="text" 
+                <UInput 
                   v-model="formData.etc2" 
                   placeholder="회사명을 입력해주세요" 
                 />
               </div>
             </div>
-            <div class="form--contents">
+            <div class="form--contents half--cont">
               <h3>연락처 <span class="required">*</span></h3>
               <div class="tel-group">
-                <select v-model="formData.tel_1" required>
-                  <option value="010">010</option>
-                  <option value="011">011</option>
-                  <option value="016">016</option>
-                  <option value="017">017</option>
-                  <option value="018">018</option>
-                  <option value="019">019</option>
-                  <option value="02">02</option>
-                  <option value="031">031</option>
-                  <option value="032">032</option>
-                  <option value="033">033</option>
-                </select>
+                <USelect 
+                  v-model="formData.tel_1"
+                  :options="phoneOptions"
+                />
                 <span>-</span>
-                <input 
-                  type="text" 
+                <UInput 
                   v-model="formData.tel_2" 
                   maxlength="4" 
                   pattern="[0-9]{3,4}" 
-                  required 
                 />
                 <span>-</span>
-                <input 
-                  type="text" 
+                <UInput 
                   v-model="formData.tel_3" 
                   maxlength="4" 
                   pattern="[0-9]{4}" 
-                  required 
                 />
               </div>
             </div>
-            <div class="form--contents">
+            <div class="form--contents half--cont">
               <h3>이메일 <span class="required">*</span></h3>
               <div>
-                <input 
+                <UInput 
                   type="email" 
                   v-model="formData.email" 
                   placeholder="이메일을 입력해주세요" 
-                  required 
                 />
               </div>
             </div>
-            <div class="form--contents">
+            <div class="form--contents half--cont">
               <h3>문의항목 <span class="required">*</span></h3>
               <div class="radio-group">
                 <label>
@@ -89,7 +72,6 @@
                     type="radio" 
                     v-model="formData.category" 
                     value="원료" 
-                    required 
                   />
                   <span>원료</span>
                 </label>
@@ -98,7 +80,6 @@
                     type="radio" 
                     v-model="formData.category" 
                     value="제품" 
-                    required 
                   />
                   <span>제품</span>
                 </label>
@@ -107,7 +88,6 @@
                     type="radio" 
                     v-model="formData.category" 
                     value="기술" 
-                    required 
                   />
                   <span>기술</span>
                 </label>
@@ -116,7 +96,6 @@
                     type="radio" 
                     v-model="formData.category" 
                     value="기타" 
-                    required 
                   />
                   <span>기타</span>
                 </label>
@@ -125,11 +104,9 @@
             <div class="form--contents">
               <h3>제목 <span class="required">*</span></h3>
               <div>
-                <input 
-                  type="text" 
+                <UInput 
                   v-model="formData.title" 
                   placeholder="제목을 입력해주세요" 
-                  required 
                 />
               </div>
             </div>
@@ -147,7 +124,6 @@
                       v-model="formData.contents" 
                       rows="10" 
                       placeholder="문의 내용을 입력해주세요"
-                      required
                     ></textarea>
                   </template>
                 </client-only>
@@ -230,7 +206,6 @@
                         type="radio" 
                         v-model="formData.agree" 
                         value="Y" 
-                        required 
                       />
                       <span>동의합니다.</span>
                     </label>
@@ -239,7 +214,6 @@
                         type="radio" 
                         v-model="formData.agree" 
                         value="N" 
-                        required 
                       />
                       <span>동의하지 않습니다.</span>
                     </label>
@@ -249,16 +223,17 @@
             </div>
             <div class="form--contents">
               <div class="btn--wrap">
-                <button 
-                  type="submit" 
-                  class="btn-submit"
-                  :disabled="isSubmitting"
+                <UButton 
+                  :loading="isSubmitting"
+                  @click="submitForm"
+                  size="lg"
+                  color="primary"
                 >
                   {{ isSubmitting ? '전송중...' : '보내기' }}
-                </button>
+                </UButton>
               </div>
             </div>
-          </form>
+          </div>
         </div>
 
         
@@ -271,7 +246,20 @@
 import { ref, onMounted } from 'vue'
 import TopVisual from '~/components/topVisual.vue'
 import SummernoteEditor from '~/components/SummernoteEditor.vue'
-import axios from 'axios'
+
+// 전화번호 옵션
+const phoneOptions = [
+  { value: '010', label: '010' },
+  { value: '011', label: '011' },
+  { value: '016', label: '016' },
+  { value: '017', label: '017' },
+  { value: '018', label: '018' },
+  { value: '019', label: '019' },
+  { value: '02', label: '02' },
+  { value: '031', label: '031' },
+  { value: '032', label: '032' },
+  { value: '033', label: '033' }
+]
 
 // 반응형 데이터
 const isSubmitting = ref(false)
@@ -349,60 +337,34 @@ const submitForm = async () => {
   isSubmitting.value = true
 
   try {
-    // FormData 객체 생성
-    const submitData = new FormData()
-    submitData.append('act', 'ins')
-    submitData.append('boardId', 'contact')
-    submitData.append('name', formData.value.name)
-    submitData.append('etc1', formData.value.etc1)
-    submitData.append('etc2', formData.value.etc2)
-    submitData.append('tel_1', formData.value.tel_1)
-    submitData.append('tel_2', formData.value.tel_2)
-    submitData.append('tel_3', formData.value.tel_3)
-    submitData.append('email', formData.value.email)
-    submitData.append('category', formData.value.category)
-    submitData.append('title', formData.value.title)
-    submitData.append('contents', formData.value.contents)
-
-    // 디버깅을 위한 FormData 내용 출력    
-    for (let [key, value] of submitData.entries()) {
-      console.log(`${key}:`, value)
+    const submitData = {
+      act: 'ins',
+      boardId: 'contact',
+      name: formData.value.name,
+      etc1: formData.value.etc1,
+      etc2: formData.value.etc2,
+      tel_1: formData.value.tel_1,
+      tel_2: formData.value.tel_2,
+      tel_3: formData.value.tel_3,
+      email: formData.value.email,
+      category: formData.value.category,
+      title: formData.value.title,
+      contents: formData.value.contents
     }
 
-    // 백엔드 API 호출
-    const config = useRuntimeConfig()
-    const apiUrl = config.public.apiBase || 'http://localhost'
-    const fullUrl = `${apiUrl}/board_proc`
-        
-    
-    const response = await axios.post(
-      fullUrl,
-      submitData,
-      {
-        headers: {
-          'Content-Type': 'multipart/form-data',
-          'X-Requested-With': 'XMLHttpRequest'
-        }
-      }
-    )
-    
+    const response = await $api.postForm('/board_proc', submitData)
 
-    if (response.data && response.data.success) {
+    if (response && response.success) {
       alert('문의가 정상적으로 접수되었습니다.')
       resetForm()
     } else {
-      console.error('API 응답 오류:', response.data)
-      alert(`문의 접수 중 오류가 발생했습니다. 응답: ${JSON.stringify(response.data)}`)
+      console.error('API 응답 오류:', response)
+      alert(`문의 접수 중 오류가 발생했습니다. 응답: ${JSON.stringify(response)}`)
     }
   } catch (error) {
-        
     if (error.response) {
-      console.error('Error response status:', error.response.status)
-      console.error('Error response data:', error.response.data)
-      console.error('Error response headers:', error.response.headers)
-      alert(`API 호출 오류: ${error.response.status} - ${error.response.statusText}\n응답: ${JSON.stringify(error.response.data)}`)
+      alert(`API 호출 오류: ${error.response.status} - ${error.response.statusText}`)
     } else if (error.request) {
-      console.error('Error request:', error.request)
       alert('서버에 연결할 수 없습니다. 네트워크 연결을 확인해주세요.')
     } else {
       alert(`요청 설정 오류: ${error.message}`)
@@ -437,158 +399,3 @@ onMounted(() => {
 
 </script>
 
-<style scoped>
-.form--contents--wrap {
-  margin-top: 40px;
-}
-
-.form--contents {
-  margin-bottom: 30px;
-}
-
-.form--contents h3 {
-  font-size: 16px;
-  font-weight: 600;
-  margin-bottom: 10px;
-}
-
-.form--contents h3 .required {
-  color: #ff0000;
-  margin-left: 4px;
-}
-
-.form--contents input[type="text"],
-.form--contents input[type="email"],
-.form--contents select,
-.form--contents textarea {
-  width: 100%;
-  padding: 10px;
-  border: 1px solid #ddd;
-  border-radius: 4px;
-  font-size: 14px;
-}
-
-.tel-group {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.tel-group select {
-  width: 100px;
-}
-
-.tel-group input {
-  flex: 1;
-}
-
-.tel-group span {
-  font-weight: 500;
-}
-
-.radio-group {
-  display: flex;
-  gap: 20px;
-  align-items: center;
-}
-
-.radio-group label {
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-}
-
-.radio-group input[type="radio"] {
-  width: auto;
-  margin-right: 5px;
-}
-
-.privacy-box {
-  border: 1px solid #ddd;
-  border-radius: 4px;
-  padding: 20px;
-  background-color: #f9f9f9;
-}
-
-.privacy-content {
-  height: 300px;
-  overflow-y: auto;
-  padding: 15px;
-  background-color: #fff;
-  border: 1px solid #e0e0e0;
-  border-radius: 4px;
-  margin-bottom: 20px;
-  line-height: 1.8;
-}
-
-.agree-check {
-  padding: 15px;
-  background-color: #fff;
-  border: 1px solid #e0e0e0;
-  border-radius: 4px;
-}
-
-.agree-check h4 {
-  font-size: 14px;
-  font-weight: 600;
-  margin-bottom: 10px;
-}
-
-.btn--wrap {
-  text-align: center;
-  margin-top: 40px;
-}
-
-.btn-submit {
-  padding: 12px 40px;
-  background-color: #00a651;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  font-size: 16px;
-  font-weight: 600;
-  cursor: pointer;
-  transition: background-color 0.3s;
-}
-
-.btn-submit:hover {
-  background-color: #008840;
-}
-
-.btn-submit:disabled {
-  background-color: #ccc;
-  cursor: not-allowed;
-}
-
-.map-section {
-  margin-top: 80px;
-  padding-top: 40px;
-  border-top: 1px solid #ddd;
-}
-
-.map-section h3 {
-  font-size: 24px;
-  font-weight: 600;
-  margin-bottom: 20px;
-}
-
-.info-list,
-.contact-list {
-  list-style: none;
-  padding: 0;
-  margin: 0 0 20px 0;
-}
-
-.info-list li,
-.contact-list li {
-  font-size: 14px;
-  line-height: 1.8;
-}
-
-.map-container {
-  height: 450px;
-  border: 1px solid #ddd;
-  border-radius: 4px;
-  overflow: hidden;
-}
-</style>

+ 119 - 0
composables/useApi.ts

@@ -0,0 +1,119 @@
+import axios from 'axios'
+
+// Axios 인스턴스 생성
+const createApiClient = () => {
+  const config = useRuntimeConfig()
+  const apiBase = config.public.apiBase || 'http://localhost'
+  
+  const client = axios.create({
+    baseURL: apiBase,
+    timeout: 10000,
+    headers: {
+      'Content-Type': 'application/json',
+      'X-Requested-With': 'XMLHttpRequest'
+    }
+  })
+
+  // 요청 인터셉터
+  client.interceptors.request.use(
+    (config) => {
+      console.log('API Request:', config.method?.toUpperCase(), config.url)
+      return config
+    },
+    (error) => {
+      return Promise.reject(error)
+    }
+  )
+
+  // 응답 인터셉터
+  client.interceptors.response.use(
+    (response) => {
+      return response
+    },
+    (error) => {
+      console.error('API Error:', error.response?.status, error.response?.data)
+      return Promise.reject(error)
+    }
+  )
+
+  return client
+}
+
+// API 클라이언트 싱글톤
+let apiClient = null
+
+const getApiClient = () => {
+  if (!apiClient) {
+    apiClient = createApiClient()
+  }
+  return apiClient
+}
+
+// 글로벌 API 메서드들
+export const get = async (url, params = {}) => {
+  try {
+    const client = getApiClient()
+    const response = await client.get(url, { params })
+    return response.data
+  } catch (error) {
+    throw error
+  }
+}
+
+export const post = async (url, data = {}) => {
+  try {
+    const client = getApiClient()
+    const response = await client.post(url, data)
+    return response.data
+  } catch (error) {
+    throw error
+  }
+}
+
+export const postForm = async (url, data = {}) => {
+  try {
+    const client = getApiClient()
+    const formData = new FormData()
+    Object.keys(data).forEach(key => {
+      formData.append(key, data[key])
+    })
+
+    const response = await client.post(url, formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    })
+    return response.data
+  } catch (error) {
+    throw error
+  }
+}
+
+export const put = async (url, data = {}) => {
+  try {
+    const client = getApiClient()
+    const response = await client.put(url, data)
+    return response.data
+  } catch (error) {
+    throw error
+  }
+}
+
+export const del = async (url, params = {}) => {
+  try {
+    const client = getApiClient()
+    const response = await client.delete(url, { params })
+    return response.data
+  } catch (error) {
+    throw error
+  }
+}
+
+// 기존 useApi 호환성을 위해 유지
+export const useApi = () => ({
+  get,
+  post,
+  postForm,
+  put,
+  delete: del
+})

+ 15 - 0
plugins/api.client.ts

@@ -0,0 +1,15 @@
+import { get, post, postForm, put, del } from '~/composables/useApi'
+
+export default defineNuxtPlugin(() => {
+  return {
+    provide: {
+      api: {
+        get,
+        post,
+        postForm,
+        put,
+        delete: del
+      }
+    }
+  }
+})