Преглед на файлове

+ api테스트 컴포넌튼 setup방식으로 업데이트

송용우 преди 2 месеца
родител
ревизия
27324f3b12
променени са 9 файла, в които са добавени 1514 реда и са изтрити 935 реда
  1. 4 0
      .env.example
  2. 21 0
      .htaccess
  3. 130 0
      app/components/QuillEditor.vue
  4. 202 0
      app/components/SummernoteEditor.vue
  5. 612 106
      app/pages/contact/support.vue
  6. 9 0
      nuxt.config.ts
  7. 530 829
      package-lock.json
  8. 5 0
      package.json
  9. 1 0
      server/api/contact.post.ts

+ 4 - 0
.env.example

@@ -0,0 +1,4 @@
+# Nuxt 환경변수 설정 예시
+
+# API 기본 URL (백엔드 서버 주소)
+NUXT_PUBLIC_API_BASE=NUXT_PUBLIC_API_BASE=http://green.interscope.co.kr

+ 21 - 0
.htaccess

@@ -0,0 +1,21 @@
+# CORS 설정
+Header always set Access-Control-Allow-Origin "*"
+Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, Origin, Accept"
+Header always set Access-Control-Max-Age "86400"
+
+# OPTIONS 요청에 대한 응답
+RewriteEngine On
+RewriteCond %{REQUEST_METHOD} OPTIONS
+RewriteRule ^(.*)$ $1 [R=200,L]
+
+# /api/ 경로는 CodeIgniter로 라우팅
+RewriteCond %{REQUEST_URI} ^/api/(.*)$
+RewriteRule ^api/(.*)$ /api/index.php/$1 [L,QSA]
+
+# /api/ 경로의 정적 파일들 (images, assets, upload 등)
+RewriteCond %{REQUEST_URI} ^/api/(images|asset|upload)/(.*)$
+RewriteRule ^api/(.*)$ /api/$1 [L]
+
+# 나머지는 Nuxt.js로 (실제 운영에서는 Nginx/Apache가 처리)
+# 개발 환경에서는 npm run dev가 처리함

+ 130 - 0
app/components/QuillEditor.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="quill-editor-wrapper">
+    <client-only>
+      <QuillEditor
+        ref="quillEditor"
+        v-model:content="content"
+        contentType="html"
+        :options="editorOptions"
+        :style="{ height: `${height + 50}px` }"
+        @update:content="onContentChange"
+      />
+      <template #fallback>
+        <textarea
+          :value="modelValue"
+          @input="$emit('update:modelValue', $event.target.value)"
+          :placeholder="placeholder"
+          :style="{ height: `${height}px` }"
+          class="fallback-textarea"
+        />
+      </template>
+    </client-only>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, computed } from 'vue'
+import { QuillEditor } from '@vueup/vue-quill'
+import '@vueup/vue-quill/dist/vue-quill.snow.css'
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  },
+  height: {
+    type: Number,
+    default: 400
+  },
+  placeholder: {
+    type: String,
+    default: '내용을 입력하세요'
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const quillEditor = ref(null)
+
+const content = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+const editorOptions = {
+  theme: 'snow',
+  placeholder: props.placeholder,
+  modules: {
+    toolbar: [
+      [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
+      ['bold', 'italic', 'underline', 'strike'],
+      [{ 'color': [] }, { 'background': [] }],
+      [{ 'font': [] }],
+      [{ 'align': [] }],
+      [{ 'list': 'ordered'}, { 'list': 'bullet' }],
+      [{ 'indent': '-1'}, { 'indent': '+1' }],
+      ['blockquote', 'code-block'],
+      ['link', 'image', 'video'],
+      ['clean']
+    ]
+  },
+  formats: [
+    'header', 'bold', 'italic', 'underline', 'strike',
+    'color', 'background', 'font', 'align',
+    'list', 'bullet', 'indent', 'blockquote', 'code-block',
+    'link', 'image', 'video'
+  ]
+}
+
+const onContentChange = (content) => {
+  emit('update:modelValue', content)
+}
+
+watch(() => props.modelValue, (newValue) => {
+  if (content.value !== newValue) {
+    content.value = newValue
+  }
+})
+</script>
+
+<style scoped>
+.quill-editor-wrapper {
+  width: 100%;
+}
+
+.fallback-textarea {
+  width: 100%;
+  padding: 15px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-family: inherit;
+  font-size: 14px;
+  line-height: 1.6;
+  resize: vertical;
+}
+
+:deep(.ql-editor) {
+  font-family: inherit;
+  font-size: 14px;
+  line-height: 1.6;
+  min-height: 300px;
+}
+
+:deep(.ql-toolbar) {
+  border-top: 1px solid #ddd;
+  border-left: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+  background-color: #f8f9fa;
+}
+
+:deep(.ql-container) {
+  border-bottom: 1px solid #ddd;
+  border-left: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+  border-radius: 0 0 4px 4px;
+}
+
+:deep(.ql-toolbar:first-child) {
+  border-radius: 4px 4px 0 0;
+}
+</style>

+ 202 - 0
app/components/SummernoteEditor.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="summernote-wrapper">
+    <div ref="editorContainer" class="editor-container">
+      <textarea 
+        ref="summernoteElement" 
+        :id="editorId" 
+        :placeholder="placeholder"
+      ></textarea>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  },
+  height: {
+    type: Number,
+    default: 400
+  },
+  placeholder: {
+    type: String,
+    default: '내용을 입력하세요'
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const summernoteElement = ref(null)
+const editorContainer = ref(null)
+const editorId = `summernote-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
+let isInitialized = false
+
+const initializeSummernote = async () => {
+  if (process.client && summernoteElement.value && !isInitialized) {
+    try {
+      // jQuery와 관련 라이브러리 로드
+      const { default: $ } = await import('jquery')
+      window.jQuery = window.$ = $
+
+      // Bootstrap CSS 로드
+      await import('bootstrap/dist/css/bootstrap.min.css')
+      
+      // Summernote CSS 로드
+      await import('summernote/dist/summernote-bs4.min.css')
+      
+      // Bootstrap JS 로드
+      await import('bootstrap')
+      
+      // Summernote JS 로드
+      await import('summernote/dist/summernote-bs4.min.js')
+      
+      // 한국어 언어팩 로드
+      try {
+        await import('summernote/dist/lang/summernote-ko-KR.min.js')
+      } catch (err) {
+        console.log('Korean language pack not loaded:', err)
+      }
+
+      // DOM 요소가 준비될 때까지 대기
+      await nextTick()
+
+      if (summernoteElement.value && $) {
+        const $editor = $(summernoteElement.value)
+        
+        // Summernote 초기화
+        $editor.summernote({
+          height: props.height,
+          placeholder: props.placeholder,
+          lang: 'ko-KR',
+          toolbar: [
+            ['style', ['style']],
+            ['font', ['bold', 'italic', 'underline', 'strikethrough', 'clear']],
+            ['fontname', ['fontname']],
+            ['fontsize', ['fontsize']],
+            ['color', ['color']],
+            ['para', ['ul', 'ol', 'paragraph']],
+            ['table', ['table']],
+            ['insert', ['link', 'picture', 'video']],
+            ['view', ['fullscreen', 'help']]
+          ],
+          callbacks: {
+            onChange: function(contents) {
+              emit('update:modelValue', contents)
+            },
+            onInit: function() {
+              console.log('Summernote initialized successfully')
+              if (props.modelValue) {
+                $editor.summernote('code', props.modelValue)
+              }
+              isInitialized = true
+            },
+            onImageUpload: function(files) {
+              console.log('Image upload:', files)
+              // 이미지 업로드 처리가 필요한 경우 여기에 구현
+            }
+          }
+        })
+        
+        console.log('Summernote setup completed')
+      }
+    } catch (error) {
+      console.error('Summernote initialization error:', error)
+      isInitialized = false
+    }
+  }
+}
+
+const destroySummernote = () => {
+  if (process.client && window.$ && summernoteElement.value && isInitialized) {
+    try {
+      const $editor = window.$(summernoteElement.value)
+      if ($editor.length && $editor.summernote) {
+        $editor.summernote('destroy')
+        isInitialized = false
+        console.log('Summernote destroyed')
+      }
+    } catch (error) {
+      console.error('Error destroying Summernote:', error)
+    }
+  }
+}
+
+// Props 변경 감지
+watch(() => props.modelValue, (newValue) => {
+  if (process.client && window.$ && summernoteElement.value && isInitialized) {
+    const $editor = window.$(summernoteElement.value)
+    if ($editor.length && $editor.summernote) {
+      const currentCode = $editor.summernote('code')
+      if (currentCode !== newValue) {
+        $editor.summernote('code', newValue || '')
+      }
+    }
+  }
+})
+
+onMounted(async () => {
+  // 약간의 지연을 두고 초기화
+  setTimeout(initializeSummernote, 100)
+})
+
+onBeforeUnmount(() => {
+  destroySummernote()
+})
+</script>
+
+<style scoped>
+.summernote-wrapper {
+  width: 100%;
+}
+
+.editor-container {
+  width: 100%;
+  min-height: 50px;
+}
+
+/* Summernote 에디터 스타일 커스터마이징 */
+:deep(.note-editor) {
+  border: 1px solid #ddd !important;
+  border-radius: 4px !important;
+}
+
+:deep(.note-editor.note-frame) {
+  margin-bottom: 0;
+}
+
+:deep(.note-editor .note-toolbar) {
+  background-color: #f8f9fa;
+  border-bottom: 1px solid #ddd;
+  padding: 8px;
+}
+
+:deep(.note-editor .note-editing-area .note-editable) {
+  font-family: inherit;
+  font-size: 14px;
+  line-height: 1.6;
+  padding: 15px;
+  min-height: 300px;
+}
+
+:deep(.note-editor.note-airframe .note-editing-area .note-editable),
+:deep(.note-editor.note-frame .note-editing-area .note-editable) {
+  padding: 15px;
+}
+
+/* 로딩 중일 때의 fallback 스타일 */
+textarea {
+  width: 100%;
+  min-height: 400px;
+  padding: 15px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-family: inherit;
+  font-size: 14px;
+  line-height: 1.6;
+  resize: vertical;
+}
+</style>

+ 612 - 106
app/pages/contact/support.vue

@@ -5,130 +5,636 @@
       <div class="inn--container">
         <h2>고객센터</h2>
         <div class="form--contents--wrap">
-          <div class="form--contents">
-            <h3>작성자 (성명)</h3>
-            <div>
-              <input type="text" placeholder="성명을 입력해주세요" />
-            </div>
-          </div>
-          <div class="form--contents">
-            <h3>직책</h3>
-            <div>
-              <input type="text" placeholder="연락처를 입력해주세요" />
-            </div>
-          </div>
-          <div class="form--contents">
-            <h3>회사명</h3>
-            <div>
-              <input type="text" placeholder="이메일을 입력해주세요" />
-            </div>
-          </div>
-          <div class="form--contents">
-            <h3>연락처</h3>
-            <div>
-              <select>
-                <option value="010">010</option>
-                <option value="010">010</option>
-                <option value="010">010</option>
-              </select>              
-              <input type="text" placeholder="" />
-              <input type="text" placeholder="" />
+          <form @submit.prevent="submitForm">
+            <div class="form--contents">
+              <h3>작성자 (성명) <span class="required">*</span></h3>
+              <div>
+                <input 
+                  type="text" 
+                  v-model="formData.name" 
+                  placeholder="성명을 입력해주세요" 
+                  required 
+                />
+              </div>
             </div>
-          </div>
-          <div class="form--contents">
-            <h3>아메일</h3>
-            <div>
-              <input type="text" placeholder="이메일을 입력해주세요" />
+            <div class="form--contents">
+              <h3>직책</h3>
+              <div>
+                <input 
+                  type="text" 
+                  v-model="formData.etc1" 
+                  placeholder="직책을 입력해주세요" 
+                />
+              </div>
             </div>
-          </div>
-          <div class="form--contents">
-            <h3>제목</h3>
-            <div>
-              <input type="text" placeholder="제목을 입력해주세요" />
+            <div class="form--contents">
+              <h3>회사명</h3>
+              <div>
+                <input 
+                  type="text" 
+                  v-model="formData.etc2" 
+                  placeholder="회사명을 입력해주세요" 
+                />
+              </div>
             </div>
-          </div>
-          <div class="form--contents">
-            <h3>문의항목</h3>
-            <div>
-              <input type="radio" id="01" name="01" />
-              <label for="01">원료</label>
-              <input type="radio" id="02" name="02" />
-              <label for="02">제품</label>
-              <input type="radio" id="03" name="03" />
-              <label for="03">기술</label>
-              <input type="radio" id="04" name="04" />
-              <label for="04">기타</label>              
+            <div class="form--contents">
+              <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>
+                <span>-</span>
+                <input 
+                  type="text" 
+                  v-model="formData.tel_2" 
+                  maxlength="4" 
+                  pattern="[0-9]{3,4}" 
+                  required 
+                />
+                <span>-</span>
+                <input 
+                  type="text" 
+                  v-model="formData.tel_3" 
+                  maxlength="4" 
+                  pattern="[0-9]{4}" 
+                  required 
+                />
+              </div>
             </div>
-          </div>
-          <div class="form--contents">
-            <h3>제목</h3>
-            <div>
-              <input type="text" placeholder="제목을 입력해주세요" />
+            <div class="form--contents">
+              <h3>이메일 <span class="required">*</span></h3>
+              <div>
+                <input 
+                  type="email" 
+                  v-model="formData.email" 
+                  placeholder="이메일을 입력해주세요" 
+                  required 
+                />
+              </div>
             </div>
-          </div>
-
-          <div class="form--contents">
-            <h3>내용</h3>
-            <div>
-              에디터 자리
+            <div class="form--contents">
+              <h3>문의항목 <span class="required">*</span></h3>
+              <div class="radio-group">
+                <label>
+                  <input 
+                    type="radio" 
+                    v-model="formData.category" 
+                    value="원료" 
+                    required 
+                  />
+                  <span>원료</span>
+                </label>
+                <label>
+                  <input 
+                    type="radio" 
+                    v-model="formData.category" 
+                    value="제품" 
+                    required 
+                  />
+                  <span>제품</span>
+                </label>
+                <label>
+                  <input 
+                    type="radio" 
+                    v-model="formData.category" 
+                    value="기술" 
+                    required 
+                  />
+                  <span>기술</span>
+                </label>
+                <label>
+                  <input 
+                    type="radio" 
+                    v-model="formData.category" 
+                    value="기타" 
+                    required 
+                  />
+                  <span>기타</span>
+                </label>
+              </div>
             </div>
-          </div>          
-          
-          <div class="form--contents">
-            <h3>개인 정보 수집 및 이용 동의</h3>
-            <div>
-              그린웨일 글로벌 주식회사 (‘www.greenwhaleglobal.com’이하 '그린웨일 글로벌')은(는) 「개인정보 보호법」 제30조에 따라 정부주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.
-이 개인정보처리방침은 2022년 11월 01일부터 적용됩니다.<br/><br/>
-제1조(개인정보의 처리 목적)<br/><br/>
-그린웨일 글로벌은 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.<br/><br/>
-재화 또는 서비스 제공: 맞춤 서비스 제공을 목적으로 개인정보를 처리합니다.
+            <div class="form--contents">
+              <h3>제목 <span class="required">*</span></h3>
+              <div>
+                <input 
+                  type="text" 
+                  v-model="formData.title" 
+                  placeholder="제목을 입력해주세요" 
+                  required 
+                />
+              </div>
             </div>
-            <div>
-              <h4>개인정보 수집 및 이용에 동의합니까?.</h4>
-
+            <div class="form--contents">
+              <h3>내용 <span class="required">*</span></h3>
               <div>
-                <input type="radio" id="01" name="01" />
-                <label for="01">동의합니다.</label>
-                <input type="radio" id="02" name="02" />
-                <label for="02">동의하지 않습니다.</label>">                
+                <client-only>
+                  <SummernoteEditor 
+                    v-model="formData.contents"
+                    :height="editorHeight"
+                    :placeholder="editorPlaceholder"
+                  />
+                  <template #fallback>
+                    <textarea 
+                      v-model="formData.contents" 
+                      rows="10" 
+                      placeholder="문의 내용을 입력해주세요"
+                      required
+                    ></textarea>
+                  </template>
+                </client-only>
               </div>
             </div>
-          </div>
-          <div class="form--contents">
-            <div class="btn--wrap">
-              "보내기"
+            <div class="form--contents">
+              <h3>개인 정보 수집 및 이용 동의</h3>
+              <div class="privacy-box">
+                <div class="privacy-content">
+                  그린웨일 글로벌 주식회사 ('www.greenwhaleglobal.com'이하 '그린웨일 글로벌')은(는) 「개인정보 보호법」 제30조에 따라 정부주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.<br/><br/>
+                  
+                  <strong>● 이 개인정보처리방침은 2022년 11월 01일부터 적용됩니다.</strong><br/><br/>
+                  
+                  <strong>제1조(개인정보의 처리 목적)</strong><br/>
+                  그린웨일 글로벌은 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.<br/>
+                  ▶ 재화 또는 서비스 제공: 맞춤 서비스 제공을 목적으로 개인정보를 처리합니다.<br/><br/>
+                  
+                  <strong>제2조(개인정보의 처리 및 보유 기간)</strong><br/>
+                  ① 그린웨일 글로벌은 법령에 따른 개인정보 보유·이용 기간 또는 정보 주체로부터 개인정보를 수집할 때 동의받은 개인정보 보유·이용 기간 내에서 개인정보를 처리·보유합니다.<br/>
+                  ② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다.<br/>
+                  • 재화 또는 서비스 제공과 관련한 개인정보는 수집.이용에 관한 동의일로부터 3년까지 위 이용목적을 위하여 보유.이용됩니다.<br/>
+                  • 보유근거 : 지속적인 피드백 관리를 위해<br/>
+                  • 관련법령 :<br/>
+                  1) 신용정보의 수집/처리 및 이용 등에 관한 기록 : 3년<br/>
+                  2) 소비자의 불만 또는 분쟁처리에 관한 기록 : 3년<br/>
+                  3) 대금결제 및 재화 등의 공급에 관한 기록 : 5년<br/>
+                  4) 계약 또는 청약철회 등에 관한 기록 : 5년<br/><br/>
+                  
+                  <strong>제3조(정보주체와 법정대리인의 권리·의무 및 그 행사방법)</strong><br/>
+                  정보 주체는 그린웨일 글로벌에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.<br/>
+                  ① 제1항에 따른 권리 행사는 그린웨일 글로벌에 대해 「개인정보 보호법」 시행령 제41조 제1항에 따라, 서면 또는 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 그린웨일 글로벌은 이에 대해 지체 없이 조치하겠습니다.<br/>
+                  ② 제1항에 따른 권리 행사는 정보 주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.<br/>
+                  ③ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보 주체의 권리가 제한될 수 있습니다.<br/>
+                  ④ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.<br/>
+                  ⑤ 그린웨일 글로벌은 정보 주체 권리에 따른 열람의 요구, 정정·삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.<br/><br/>
+                  
+                  <strong>제4조(처리하는 개인정보의 항목 작성)</strong><br/>
+                  그린웨일 글로벌은 다음의 개인정보 항목을 처리하고 있습니다.<br/>
+                  • 재화 또는 서비스 제공<br/>
+                  • 필수항목 : 이메일, 휴대 전화번호, 이름, 회사 전화번호, 회사명<br/>
+                  • 선택항목 : 직책, 부서<br/><br/>
+                  
+                  <strong>제5조(개인정보의 파기)</strong><br/>
+                  ① 그린웨일 글로벌은 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때는 지체없이 해당 개인정보를 파기합니다.<br/>
+                  ② 정보 주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.<br/>
+                  ③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.<br/>
+                  • 파기 절차: 그린웨일 글로벌은 파기 사유가 발생한 개인정보를 선정하고, 그린웨일 글로벌의 개인정보 보호 책임자의 승인을 받아 개인정보를 파기합니다.<br/>
+                  • 파기 방법: 전자적 파일 형태의 정보는 기록을 재생할 수 없는 기술적 방법을 사용합니다<br/><br/>
+                  
+                  <strong>제6조(개인정보의 안전성 확보 조치)</strong><br/>
+                  그린웨일글로벌은 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.<br/>
+                  ① 정기적인 자체 감사 실시<br/>
+                  ② 개인정보 취급 직원의 최소화 및 교육<br/>
+                  ③ 내부관리계획의 수립 및 시행<br/>
+                  ④ 개인정보에 대한 접근 제한<br/>
+                  ⑤ 비인가자에 대한 출입 통제<br/><br/>
+                  
+                  <strong>제7조(개인정보 자동 수집 장치의 설치•운영 및 거부에 관한 사항)</strong><br/>
+                  ① 그린웨일글로벌는 이용자에게 개별적인 맞춤서비스를 제공하기 위해 이용정보를 저장하고 수시로 불러오는 '쿠키(cookie)'를 사용합니다.<br/>
+                  ② 쿠키는 웹사이트를 운영하는데 이용되는 서버(http)가 이용자의 컴퓨터 브라우저에게 보내는 소량의 정보이며 이용자들의 PC 컴퓨터내의 하드디스크에 저장되기도 합니다.<br/><br/>
+                  
+                  <strong>제8조(개인정보 보호책임자 등)</strong><br/>
+                  ① 그린웨일 글로벌은 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만 처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.<br/>
+                  • 담당자 : 경영지원팀 이안나 대리<br/>
+                  • 전화번호 : 02-3447-8802<br/>
+                  • 이메일 : green@greenwhaleglobal.com<br/><br/>
+                  
+                  <strong>제9조(권익침해 구제방법)</strong><br/>
+                  정보 주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다.<br/>
+                  1. 개인정보분쟁조정위원회 : (국번없이) 1833-6972 (www.kopico.go.kr)<br/>
+                  2. 개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)<br/>
+                  3. 대검찰청 : (국번없이) 1301 (www.spo.go.kr)<br/>
+                  4. 경찰청 : (국번없이) 182 (cyberbureau.police.go.kr)<br/>
+                </div>
+                <div class="agree-check">
+                  <h4>개인정보 수집 및 이용에 동의합니까?</h4>
+                  <div class="radio-group">
+                    <label>
+                      <input 
+                        type="radio" 
+                        v-model="formData.agree" 
+                        value="Y" 
+                        required 
+                      />
+                      <span>동의합니다.</span>
+                    </label>
+                    <label>
+                      <input 
+                        type="radio" 
+                        v-model="formData.agree" 
+                        value="N" 
+                        required 
+                      />
+                      <span>동의하지 않습니다.</span>
+                    </label>
+                  </div>
+                </div>
+              </div>
             </div>
-          </div>
+            <div class="form--contents">
+              <div class="btn--wrap">
+                <button 
+                  type="submit" 
+                  class="btn-submit"
+                  :disabled="isSubmitting"
+                >
+                  {{ isSubmitting ? '전송중...' : '보내기' }}
+                </button>
+              </div>
+            </div>
+          </form>
+        </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>
 </template>
-<script>
+
+<script setup>
+import { ref, onMounted } from 'vue'
 import TopVisual from '~/components/topVisual.vue'
+import SummernoteEditor from '~/components/SummernoteEditor.vue'
+import axios from 'axios'
+
+// 반응형 데이터
+const isSubmitting = ref(false)
+const formData = ref({
+  name: '',
+  etc1: '', // 직책
+  etc2: '', // 회사명
+  tel_1: '010',
+  tel_2: '',
+  tel_3: '',
+  email: '',
+  category: '',
+  title: '',
+  contents: '',
+  agree: ''
+})
 
-export default {
-  name: "support",
-  components: {
-    TopVisual
+// Summernote 에디터 설정
+const editorHeight = 400
+const editorPlaceholder = '문의 내용을 입력해주세요'
+
+// 페이지 설정
+const className = "contact"
+const title = "Contact"
+const navigation = [
+  {
+    name: "Contact",
+    link: "/contact",
+    gnbList: [
+      { name: "공지사항", link: "/" },
+      { name: "FAQ", link: "/" },
+      { name: "고객센터", link: "/contact/support" }
+    ]
   },
-  data() {
-    return {
-      className: "contact",
-      title: "Contact",
-      navigation: [                
-        { name: "Contact", 
-          link: "/contact" ,
-          gnbList : [
-            { name: "공지사항", link: "/" },
-            { name: "FAQ", link: "/" },
-            { name: "고객센터", link: "/" }
-          ]
-        },        
-        { name: "오시는길", link: "/contact/support" }        
-      ],
-      
+  { name: "고객센터", link: "/contact/support" }
+]
+
+// 폼 유효성 검증
+const validateForm = () => {
+  if (!formData.value.name) {
+    alert('성명을 입력해주세요.')
+    return false
+  }
+  if (!formData.value.tel_2 || !formData.value.tel_3) {
+    alert('연락처를 입력해주세요.')
+    return false
+  }
+  if (!formData.value.email) {
+    alert('이메일을 입력해주세요.')
+    return false
+  }
+  if (!formData.value.category) {
+    alert('문의항목을 선택해주세요.')
+    return false
+  }
+  if (!formData.value.title) {
+    alert('제목을 입력해주세요.')
+    return false
+  }
+  if (!formData.value.contents || formData.value.contents === '<p><br></p>' || formData.value.contents === '') {
+    alert('내용을 입력해주세요.')
+    return false
+  }
+  if (formData.value.agree !== 'Y') {
+    alert('개인정보 수집 및 이용에 동의해주세요.')
+    return false
+  }
+  return true
+}
+
+// 폼 제출
+const submitForm = async () => {
+  if (!validateForm()) return
+
+  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)
     }
+
+    // 백엔드 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'
+        }
+      }
+    )
+
+    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) {
+      alert('문의가 정상적으로 접수되었습니다.')
+      resetForm()
+    } else {
+      console.error('API 응답 오류:', response.data)
+      alert(`문의 접수 중 오류가 발생했습니다. 응답: ${JSON.stringify(response.data)}`)
+    }
+  } 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)}`)
+    } else if (error.request) {
+      console.error('Error request:', error.request)
+      alert('서버에 연결할 수 없습니다. 네트워크 연결을 확인해주세요.')
+    } else {
+      alert(`요청 설정 오류: ${error.message}`)
+    }
+  } finally {
+    isSubmitting.value = false
+  }
+}
+
+// 폼 초기화
+const resetForm = () => {
+  formData.value = {
+    name: '',
+    etc1: '',
+    etc2: '',
+    tel_1: '010',
+    tel_2: '',
+    tel_3: '',
+    email: '',
+    category: '',
+    title: '',
+    contents: '',
+    agree: ''
+  }
+}
+
+// 네이버 지도 초기화
+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
+    })
   }
-};  
-</script>
+})
+
+// 메타 정보 설정
+useHead({
+  script: [
+    {
+      src: 'https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=5qsxjdotgi',
+      async: true
+    }
+  ]
+})
+</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>

+ 9 - 0
nuxt.config.ts

@@ -11,6 +11,10 @@ export default defineNuxtConfig({
         //{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
       ],
       script: [
+        {
+          src: 'https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=5qsxjdotgi',
+          defer: true
+        }
       ]
     },
   },  
@@ -26,5 +30,10 @@ export default defineNuxtConfig({
   devtools: { enabled: false },
   devServer: {
     host: "0.0.0.0"
+  },
+  runtimeConfig: {
+    public: {
+      apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost'
+    }
   }
 })

Файловите разлики са ограничени, защото са твърде много
+ 530 - 829
package-lock.json


+ 5 - 0
package.json

@@ -11,8 +11,13 @@
   },
   "dependencies": {
     "@nuxt/ui": "^3.3.4",
+    "@vueup/vue-quill": "^1.2.0",
+    "axios": "^1.12.2",
+    "bootstrap": "^4.6.2",
+    "jquery": "^3.7.1",
     "nuxt": "^4.1.2",
     "pretendard": "^1.3.9",
+    "summernote": "^0.9.1",
     "swiper": "^12.0.2",
     "vue": "^3.5.21",
     "vue-router": "^4.5.1",

+ 1 - 0
server/api/contact.post.ts

@@ -0,0 +1 @@
+// 파일을 삭제하고 직접 CodeIgniter API를 호출하는 방식으로 변경됨

Някои файлове не бяха показани, защото твърде много файлове са промени