Forráskód Böngészése

Merge remote-tracking branch 'origin/master'

interscope_003\interscope 2 hónapja
szülő
commit
a9bdfa7d2f

+ 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:

+ 2 - 0
app/assets/scss/style.scss

@@ -1,4 +1,6 @@
 @import "pretendard/dist/web/static/pretendard.css";
+
+// SCSS watching test
 * {
     font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
     word-break: keep-all;

+ 198 - 2
app/assets/scss/sub.scss

@@ -1,3 +1,4 @@
+
 /** 탑 비쥬얼 공통 모듈 **/
 #top--visual{
   display: flex;
@@ -12,6 +13,7 @@
     background-attachment: fixed;  
     background-size: contain;
   }
+ 
 
   .inner--content{
     max-height: 440px;
@@ -19,7 +21,8 @@
     display: flex;
     align-items: center;
     justify-content: center;
-    position: relative;
+    position: relative;    
+  
     > h1{
       color:#FFF;
       text-align: center;      
@@ -92,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;
@@ -131,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;
+    }
+  }
 }

+ 64 - 303
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,30 +223,20 @@
             </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>
 
-        <!-- 오시는 길 섹션 -->
-        <div class="map-section">
-          <h3>오시는 길</h3>
-          <ul class="info-list">
-            <li>서울시 강남구 언주로 650 한국건설기술인협회 신관 2층</li>
-          </ul>
-          <ul class="contact-list">
-            <li>Tel : 02-3447-8801~8802</li>
-            <li>E-Mail : green@greenwhaleglobal.com</li>
-          </ul>
-          <div id="map" class="map-container"></div>
-        </div>
+        
       </div>
     </section>
   </main>
@@ -282,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)
@@ -360,75 +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 내용 출력
-    console.log('=== Form Data 전송 내용 ===')
-    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`
-    
-    console.log('=== API 호출 정보 ===')
-    console.log('API URL:', fullUrl)
-    console.log('Request headers:', {
-      'Content-Type': 'multipart/form-data',
-      'X-Requested-With': 'XMLHttpRequest'
-    })
-    
-    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)
 
-    console.log('=== API 응답 ===')
-    console.log('Response status:', response.status)
-    console.log('Response headers:', response.headers)
-    console.log('Response data:', response.data)
-
-    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) {
-    console.error('=== 전체 에러 정보 ===')
-    console.error('Error object:', error)
-    console.error('Error message:', error.message)
-    console.error('Error response:', error.response)
-    
     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}`)
@@ -455,186 +391,11 @@ const resetForm = () => {
   }
 }
 
-// 네이버 지도 초기화
-onMounted(() => {
-  if (typeof naver !== 'undefined' && naver.maps) {
-    const position = new naver.maps.LatLng(37.51490691373259, 127.03574342661345)
-    
-    const map = new naver.maps.Map('map', {
-      center: position,
-      zoom: 16
-    })
-
-    new naver.maps.Marker({
-      position: position,
-      map: map
-    })
-  }
-})
 
-// 메타 정보 설정
-useHead({
-  script: [
-    {
-      src: 'https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=5qsxjdotgi',
-      async: true
-    }
-  ]
+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;
-}
+</script>
 
-.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
+})

+ 11 - 2
nuxt.config.ts

@@ -30,9 +30,18 @@ export default defineNuxtConfig({
     colorMode: false
   },
   css: [
-    '~/assets/styles/style.css',
-    '~/assets/styles/sub.css'
+    '~/assets/scss/style.scss',
+    '~/assets/scss/sub.scss'
   ],
+  vite: {
+    css: {
+      preprocessorOptions: {
+        scss: {
+          additionalData: ''
+        }
+      }
+    }
+  },
   compatibilityDate: '2025-07-15',
   devtools: { enabled: false },
   devServer: {

+ 31 - 0
package-lock.json

@@ -20,6 +20,9 @@
         "vue": "^3.5.21",
         "vue-router": "^4.5.1",
         "vue3-marquee": "^4.2.2"
+      },
+      "devDependencies": {
+        "sass": "^1.93.2"
       }
     },
     "node_modules/@alloc/quick-lru": {
@@ -6818,6 +6821,13 @@
       "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==",
       "license": "MIT"
     },
+    "node_modules/immutable": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
+      "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
+      "devOptional": true,
+      "license": "MIT"
+    },
     "node_modules/impound": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/impound/-/impound-1.0.0.tgz",
@@ -9692,6 +9702,27 @@
       ],
       "license": "MIT"
     },
+    "node_modules/sass": {
+      "version": "1.93.2",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
+      "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.0",
+        "immutable": "^5.0.2",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
     "node_modules/sax": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",

+ 5 - 2
package.json

@@ -11,10 +11,10 @@
   },
   "dependencies": {
     "@nuxt/ui": "^3.3.4",
-    "gsap": "^3.13.0",
     "@vueup/vue-quill": "^1.2.0",
     "axios": "^1.12.2",
     "bootstrap": "^4.6.2",
+    "gsap": "^3.13.0",
     "jquery": "^3.7.1",
     "nuxt": "^4.1.2",
     "pretendard": "^1.3.9",
@@ -24,5 +24,8 @@
     "vue-router": "^4.5.1",
     "vue3-marquee": "^4.2.2"
   },
-  "packageManager": "pnpm@9.13.0+sha512.beb9e2a803db336c10c9af682b58ad7181ca0fbd0d4119f2b33d5f2582e96d6c0d93c85b23869295b765170fbdaa92890c0da6ada457415039769edf3c959efe"
+  "packageManager": "pnpm@9.13.0+sha512.beb9e2a803db336c10c9af682b58ad7181ca0fbd0d4119f2b33d5f2582e96d6c0d93c85b23869295b765170fbdaa92890c0da6ada457415039769edf3c959efe",
+  "devDependencies": {
+    "sass": "^1.93.2"
+  }
 }

+ 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
+      }
+    }
+  }
+})

+ 11 - 0
public/.htaccess

@@ -0,0 +1,11 @@
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteBase /
+
+  # 실제 파일이나 디렉토리가 아닌 경우 index.html로 리다이렉트
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteCond %{REQUEST_URI} !^/_nuxt
+  RewriteCond %{REQUEST_URI} !^/img
+  RewriteRule ^.*$ /index.html [L]
+</IfModule>