Przeglądaj źródła

+ 닉네입 예외처리

송용우 4 miesięcy temu
rodzic
commit
093a91099d

+ 4 - 1
.env.development

@@ -6,4 +6,7 @@ VITE_APP_API_DOMAIN="http://localhost:3000"
 VITE_APP_DEBUG_LEVEL=trace
 VITE_APP_MODE=development
 VITE_APP_KAKAO_APP_KEY=""
-VITE_APP_DEV_TOPKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImludGVyc2NvcGVyb3VsZXR0ZSJ9.eyJpYXQiOjE3NDg0MDcyNzYsImV4cCI6OS4wZSs1NSwic3ViIjoiYWRtaW4iLCJuYW1lIjoiXHVhY2UwXHVjNTkxXHVjNzc0In0.gbceCSjAUaYOmuOvnMhgLTYJZOiD8WYpUs-S2kaY3ng"
+VITE_APP_DEV_TOPKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImludGVyc2NvcGVyb3VsZXR0ZSJ9.eyJpYXQiOjE3NDg0MDcyNzYsImV4cCI6OS4wZSs1NSwic3ViIjoiYWRtaW4iLCJuYW1lIjoiXHVhY2UwXHVjNTkxXHVjNzc0In0.gbceCSjAUaYOmuOvnMhgLTYJZOiD8WYpUs-S2kaY3ng"
+
+# Claude API 설정
+ANTHROPIC_API_KEY="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"

+ 288 - 0
components/chat/ClaudeChat.vue

@@ -0,0 +1,288 @@
+<template>
+  <div class="claude-chat">
+    <div class="chat-container">
+      <div class="messages" ref="messagesContainer">
+        <div 
+          v-for="(message, index) in messages" 
+          :key="index"
+          :class="['message', message.role]"
+        >
+          <div class="message-content">
+            <div class="message-role">{{ message.role === 'user' ? '사용자' : 'Claude' }}</div>
+            <div class="message-text">{{ message.content }}</div>
+          </div>
+        </div>
+        <div v-if="isLoading" class="message assistant loading">
+          <div class="message-content">
+            <div class="message-role">Claude</div>
+            <div class="message-text">
+              <div class="typing-indicator">
+                <span></span>
+                <span></span>
+                <span></span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <div class="input-container">
+        <div class="input-wrapper">
+          <textarea
+            v-model="newMessage"
+            @keydown.enter.prevent="handleEnter"
+            placeholder="메시지를 입력하세요..."
+            rows="1"
+            ref="messageInput"
+          ></textarea>
+          <button 
+            @click="sendMessage" 
+            :disabled="!newMessage.trim() || isLoading"
+            class="send-button"
+          >
+            <Icon name="mdi:send" />
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, nextTick, onMounted } from 'vue'
+
+const { sendStreamMessage } = useClaude()
+
+const messages = ref([])
+const newMessage = ref('')
+const isLoading = ref(false)
+const messagesContainer = ref(null)
+const messageInput = ref(null)
+
+const scrollToBottom = () => {
+  nextTick(() => {
+    if (messagesContainer.value) {
+      messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
+    }
+  })
+}
+
+const handleEnter = (event) => {
+  if (!event.shiftKey) {
+    sendMessage()
+  } else {
+    newMessage.value += '\n'
+  }
+}
+
+const sendMessage = async () => {
+  if (!newMessage.value.trim() || isLoading.value) return
+
+  const userMessage = newMessage.value.trim()
+  newMessage.value = ''
+  
+  messages.value.push({
+    role: 'user',
+    content: userMessage
+  })
+  
+  scrollToBottom()
+  isLoading.value = true
+  
+  const assistantMessage = {
+    role: 'assistant',
+    content: ''
+  }
+  
+  messages.value.push(assistantMessage)
+  scrollToBottom()
+
+  try {
+    const result = await sendStreamMessage(
+      userMessage,
+      messages.value.slice(0, -1),
+      (chunk, fullResponse) => {
+        assistantMessage.content = fullResponse
+        scrollToBottom()
+      }
+    )
+
+    if (!result.success) {
+      assistantMessage.content = `오류가 발생했습니다: ${result.error}`
+    }
+  } catch (error) {
+    assistantMessage.content = `오류가 발생했습니다: ${error.message}`
+  } finally {
+    isLoading.value = false
+    scrollToBottom()
+  }
+}
+
+onMounted(() => {
+  if (messageInput.value) {
+    messageInput.value.focus()
+  }
+})
+</script>
+
+<style scoped>
+.claude-chat {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.chat-container {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  height: 100%;
+}
+
+.messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px 0;
+  scroll-behavior: smooth;
+}
+
+.message {
+  margin-bottom: 20px;
+  display: flex;
+}
+
+.message.user {
+  justify-content: flex-end;
+}
+
+.message.assistant {
+  justify-content: flex-start;
+}
+
+.message-content {
+  max-width: 70%;
+  padding: 15px;
+  border-radius: 15px;
+  word-wrap: break-word;
+}
+
+.message.user .message-content {
+  background-color: #007bff;
+  color: white;
+  border-bottom-right-radius: 5px;
+}
+
+.message.assistant .message-content {
+  background-color: #f8f9fa;
+  color: #333;
+  border-bottom-left-radius: 5px;
+  border: 1px solid #e9ecef;
+}
+
+.message-role {
+  font-size: 12px;
+  font-weight: 600;
+  margin-bottom: 5px;
+  opacity: 0.7;
+}
+
+.message-text {
+  white-space: pre-wrap;
+  line-height: 1.5;
+}
+
+.input-container {
+  padding: 20px 0;
+  border-top: 1px solid #e9ecef;
+}
+
+.input-wrapper {
+  display: flex;
+  gap: 10px;
+  align-items: flex-end;
+}
+
+textarea {
+  flex: 1;
+  min-height: 40px;
+  max-height: 120px;
+  padding: 12px 15px;
+  border: 1px solid #ddd;
+  border-radius: 20px;
+  resize: none;
+  font-family: inherit;
+  font-size: 14px;
+  line-height: 1.4;
+  outline: none;
+  transition: border-color 0.2s;
+}
+
+textarea:focus {
+  border-color: #007bff;
+}
+
+.send-button {
+  width: 40px;
+  height: 40px;
+  border: none;
+  border-radius: 50%;
+  background-color: #007bff;
+  color: white;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: background-color 0.2s;
+}
+
+.send-button:hover:not(:disabled) {
+  background-color: #0056b3;
+}
+
+.send-button:disabled {
+  background-color: #ccc;
+  cursor: not-allowed;
+}
+
+.typing-indicator {
+  display: flex;
+  gap: 4px;
+  align-items: center;
+}
+
+.typing-indicator span {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background-color: #007bff;
+  animation: typing 1.4s infinite ease-in-out;
+}
+
+.typing-indicator span:nth-child(1) {
+  animation-delay: -0.32s;
+}
+
+.typing-indicator span:nth-child(2) {
+  animation-delay: -0.16s;
+}
+
+@keyframes typing {
+  0%, 80%, 100% {
+    transform: scale(0);
+    opacity: 0.5;
+  }
+  40% {
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+.loading .message-text {
+  display: flex;
+  align-items: center;
+  min-height: 20px;
+}
+</style>

+ 8 - 1
components/common/header.vue

@@ -3,7 +3,8 @@
     <div class="pro--wrap">
       <div class="pro--img"></div>
       <div class="pro--id" @click="proOn ? (proOn = false) : (proOn = true)">
-        {{ useStoreAuth.getSnsTempData.user.NICK_NAME }} <i class="ico" :class="[proOn ? 'on' : '']">></i>
+        {{ useStoreAuth.getSnsTempData?.user?.NICK_NAME || "사용자" }}
+        <i class="ico" :class="[proOn ? 'on' : '']">></i>
         <div class="id--box" v-show="proOn">
           <button type="button" class="btn-profile" @click="myPage(userId)">
             마이페이지
@@ -88,6 +89,12 @@
         menuName: "고객센터",
         linkType: "/view/cs",
       }
+      // {
+      //   menuId: "menu06",
+      //   parentMenuId: "menu06",
+      //   menuName: "AI 채팅",
+      //   linkType: "/view/chat",
+      // }
     );
 
     arrMenuInfo.value = info;

+ 81 - 0
composables/useClaude.js

@@ -0,0 +1,81 @@
+import Anthropic from '@anthropic-ai/sdk'
+
+export const useClaude = () => {
+  const runtimeConfig = useRuntimeConfig()
+  
+  const client = new Anthropic({
+    apiKey: runtimeConfig.public.anthropicApiKey,
+    dangerouslyAllowBrowser: true
+  })
+
+  const sendMessage = async (message, conversation = []) => {
+    try {
+      const messages = [
+        ...conversation,
+        { role: 'user', content: message }
+      ]
+
+      const response = await client.messages.create({
+        model: 'claude-3-5-sonnet-20241022',
+        max_tokens: 1000,
+        messages: messages
+      })
+
+      return {
+        success: true,
+        content: response.content[0].text,
+        usage: response.usage
+      }
+    } catch (error) {
+      console.error('Claude API Error:', error)
+      return {
+        success: false,
+        error: error.message
+      }
+    }
+  }
+
+  const sendStreamMessage = async (message, conversation = [], onChunk) => {
+    try {
+      const messages = [
+        ...conversation,
+        { role: 'user', content: message }
+      ]
+
+      const stream = await client.messages.create({
+        model: 'claude-3-5-sonnet-20241022',
+        max_tokens: 1000,
+        messages: messages,
+        stream: true
+      })
+
+      let fullResponse = ''
+      
+      for await (const chunk of stream) {
+        if (chunk.type === 'content_block_delta') {
+          const text = chunk.delta.text
+          fullResponse += text
+          if (onChunk) {
+            onChunk(text, fullResponse)
+          }
+        }
+      }
+
+      return {
+        success: true,
+        content: fullResponse
+      }
+    } catch (error) {
+      console.error('Claude Stream API Error:', error)
+      return {
+        success: false,
+        error: error.message
+      }
+    }
+  }
+
+  return {
+    sendMessage,
+    sendStreamMessage
+  }
+}

+ 7 - 2
nuxt.config.ts

@@ -17,8 +17,7 @@ export default defineNuxtConfig({
         { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
       ],
       script: [
-        { type: 'text/javascript', src: '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js' },
-        { type: 'text/javascript', src: "/js/jquery-3.7.1.min.js" },
+        { type: 'text/javascript', src: '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js' }
       ]
     },
   },  
@@ -90,4 +89,10 @@ export default defineNuxtConfig({
   },
 
   compatibilityDate: '2024-08-23',
+  
+  runtimeConfig: {
+    public: {
+      anthropicApiKey: process.env.ANTHROPIC_API_KEY
+    }
+  }
 })

+ 9 - 0
package-lock.json

@@ -7,6 +7,7 @@
       "name": "nuxt-app",
       "hasInstallScript": true,
       "dependencies": {
+        "@anthropic-ai/sdk": "^0.57.0",
         "@fortawesome/fontawesome-free": "^6.7.2",
         "@fortawesome/fontawesome-svg-core": "^6.7.2",
         "@fortawesome/free-brands-svg-icons": "^6.7.2",
@@ -87,6 +88,14 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/@anthropic-ai/sdk": {
+      "version": "0.57.0",
+      "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.57.0.tgz",
+      "integrity": "sha512-z5LMy0MWu0+w2hflUgj4RlJr1R+0BxKXL7ldXTO8FasU8fu599STghO+QKwId2dAD0d464aHtU+ChWuRHw4FNw==",
+      "bin": {
+        "anthropic-ai-sdk": "bin/cli"
+      }
+    },
     "node_modules/@babel/code-frame": {
       "version": "7.26.2",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "vue-router": "^4.2.5"
   },
   "dependencies": {
+    "@anthropic-ai/sdk": "^0.57.0",
     "@fortawesome/fontawesome-free": "^6.7.2",
     "@fortawesome/fontawesome-svg-core": "^6.7.2",
     "@fortawesome/free-brands-svg-icons": "^6.7.2",

+ 20 - 0
pages/view/chat.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="chat-page">
+    <ClaudeChat />
+  </div>
+</template>
+
+<script setup>
+import ClaudeChat from '~/components/chat/ClaudeChat.vue'
+
+definePageMeta({
+  layout: 'default'
+})
+</script>
+
+<style scoped>
+.chat-page {
+  height: calc(100vh - 80px);
+  padding: 0;
+}
+</style>