feat: nuxt-claude 프로젝트 초기 커밋
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 25m52s
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 25m52s
Made-with: Cursor
This commit is contained in:
145
app/composables/useAiChat.ts
Normal file
145
app/composables/useAiChat.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { AiConversation, AiMessage } from '~/types/ai'
|
||||
|
||||
export function useAiChat() {
|
||||
const client = useSupabaseClient()
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const conversations = ref<AiConversation[]>([])
|
||||
const currentConversation = ref<AiConversation | null>(null)
|
||||
const messages = ref<AiMessage[]>([])
|
||||
const streamingContent = ref('')
|
||||
const isStreaming = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchConversations() {
|
||||
if (!user.value) return
|
||||
const { data } = await client
|
||||
.from('ai_conversations')
|
||||
.select('*')
|
||||
.order('updated_at', { ascending: false })
|
||||
conversations.value = (data as AiConversation[]) ?? []
|
||||
}
|
||||
|
||||
async function createConversation(title: string = '새 대화') {
|
||||
if (!user.value) return null
|
||||
const { data, error } = await client
|
||||
.from('ai_conversations')
|
||||
.insert({ user_id: user.value.id, title })
|
||||
.select()
|
||||
.single()
|
||||
if (error) return null
|
||||
const conv = data as AiConversation
|
||||
conversations.value.unshift(conv)
|
||||
return conv
|
||||
}
|
||||
|
||||
async function selectConversation(conv: AiConversation) {
|
||||
currentConversation.value = conv
|
||||
await fetchMessages(conv.id)
|
||||
}
|
||||
|
||||
async function fetchMessages(conversationId: string) {
|
||||
const { data } = await client
|
||||
.from('ai_messages')
|
||||
.select('*')
|
||||
.eq('conversation_id', conversationId)
|
||||
.order('created_at', { ascending: true })
|
||||
messages.value = (data as AiMessage[]) ?? []
|
||||
}
|
||||
|
||||
async function deleteConversation(id: string) {
|
||||
await client.from('ai_conversations').delete().eq('id', id)
|
||||
conversations.value = conversations.value.filter(c => c.id !== id)
|
||||
if (currentConversation.value?.id === id) {
|
||||
currentConversation.value = null
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(content: string) {
|
||||
if (!currentConversation.value || isStreaming.value) return
|
||||
|
||||
// Save user message to DB
|
||||
const { data: userMsg } = await client
|
||||
.from('ai_messages')
|
||||
.insert({
|
||||
conversation_id: currentConversation.value.id,
|
||||
role: 'user',
|
||||
content
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (userMsg) messages.value.push(userMsg as AiMessage)
|
||||
|
||||
// Start streaming
|
||||
isStreaming.value = true
|
||||
streamingContent.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ai/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
conversationId: currentConversation.value.id,
|
||||
messages: messages.value.map(m => ({ role: m.role, content: m.content }))
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('API 오류')
|
||||
|
||||
const reader = response.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
streamingContent.value += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
// Save assistant message to DB
|
||||
const finalContent = streamingContent.value
|
||||
const { data: assistantMsg } = await client
|
||||
.from('ai_messages')
|
||||
.insert({
|
||||
conversation_id: currentConversation.value.id,
|
||||
role: 'assistant',
|
||||
content: finalContent
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (assistantMsg) messages.value.push(assistantMsg as AiMessage)
|
||||
|
||||
// Update conversation title if it's the first message
|
||||
if (messages.value.length === 2) {
|
||||
const title = content.slice(0, 30) + (content.length > 30 ? '...' : '')
|
||||
await client
|
||||
.from('ai_conversations')
|
||||
.update({ title })
|
||||
.eq('id', currentConversation.value.id)
|
||||
const conv = conversations.value.find(c => c.id === currentConversation.value!.id)
|
||||
if (conv) conv.title = title
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Streaming error:', e)
|
||||
} finally {
|
||||
isStreaming.value = false
|
||||
streamingContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
conversations: readonly(conversations),
|
||||
currentConversation: readonly(currentConversation),
|
||||
messages: readonly(messages),
|
||||
streamingContent: readonly(streamingContent),
|
||||
isStreaming: readonly(isStreaming),
|
||||
loading: readonly(loading),
|
||||
fetchConversations,
|
||||
createConversation,
|
||||
selectConversation,
|
||||
deleteConversation,
|
||||
sendMessage
|
||||
}
|
||||
}
|
||||
125
app/composables/usePurchases.ts
Normal file
125
app/composables/usePurchases.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { Purchase, PurchaseInsert, EquipmentCategory } from '~/types/purchase'
|
||||
|
||||
export function usePurchases() {
|
||||
const client = useSupabaseClient()
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const purchases = ref<Purchase[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const totalSpent = computed(() =>
|
||||
purchases.value.reduce((sum, p) => sum + p.price, 0)
|
||||
)
|
||||
|
||||
const categoryBreakdown = computed(() => {
|
||||
const breakdown: Record<string, number> = {}
|
||||
for (const p of purchases.value) {
|
||||
breakdown[p.category] = (breakdown[p.category] ?? 0) + p.price
|
||||
}
|
||||
return breakdown
|
||||
})
|
||||
|
||||
async function fetchPurchases() {
|
||||
if (!user.value) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('purchases')
|
||||
.select('*')
|
||||
.order('purchase_date', { ascending: false })
|
||||
if (err) throw err
|
||||
purchases.value = data as Purchase[]
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '오류가 발생했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createPurchase(payload: PurchaseInsert) {
|
||||
if (!user.value) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('purchases')
|
||||
.insert({ ...payload, user_id: user.value.id })
|
||||
.select()
|
||||
.single()
|
||||
if (err) throw err
|
||||
purchases.value.unshift(data as Purchase)
|
||||
return data as Purchase
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '저장에 실패했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePurchase(id: string, payload: Partial<PurchaseInsert>) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('purchases')
|
||||
.update(payload)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single()
|
||||
if (err) throw err
|
||||
const idx = purchases.value.findIndex(p => p.id === id)
|
||||
if (idx !== -1) purchases.value[idx] = data as Purchase
|
||||
return data as Purchase
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '수정에 실패했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePurchase(id: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { error: err } = await client
|
||||
.from('purchases')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
if (err) throw err
|
||||
purchases.value = purchases.value.filter(p => p.id !== id)
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '삭제에 실패했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getPurchase(id: string): Promise<Purchase | null> {
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('purchases')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
if (err) throw err
|
||||
return data as Purchase
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
purchases: readonly(purchases),
|
||||
loading: readonly(loading),
|
||||
error: readonly(error),
|
||||
totalSpent,
|
||||
categoryBreakdown,
|
||||
fetchPurchases,
|
||||
createPurchase,
|
||||
updatePurchase,
|
||||
deletePurchase,
|
||||
getPurchase
|
||||
}
|
||||
}
|
||||
132
app/composables/useUsedSales.ts
Normal file
132
app/composables/useUsedSales.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { UsedSale, UsedSaleInsert, SaleStatus } from '~/types/used-sale'
|
||||
|
||||
export function useUsedSales() {
|
||||
const client = useSupabaseClient()
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const sales = ref<UsedSale[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const totalRevenue = computed(() =>
|
||||
sales.value
|
||||
.filter(s => s.status === 'sold')
|
||||
.reduce((sum, s) => sum + (s.final_price ?? s.sale_price), 0)
|
||||
)
|
||||
|
||||
const byStatus = computed(() => {
|
||||
const result: Record<SaleStatus, UsedSale[]> = {
|
||||
listing: [],
|
||||
reserved: [],
|
||||
sold: [],
|
||||
cancelled: []
|
||||
}
|
||||
for (const s of sales.value) {
|
||||
result[s.status].push(s)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
async function fetchSales() {
|
||||
if (!user.value) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('used_sales')
|
||||
.select('*')
|
||||
.order('listed_at', { ascending: false })
|
||||
if (err) throw err
|
||||
sales.value = data as UsedSale[]
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '오류가 발생했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createSale(payload: UsedSaleInsert) {
|
||||
if (!user.value) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('used_sales')
|
||||
.insert({ ...payload, user_id: user.value.id })
|
||||
.select()
|
||||
.single()
|
||||
if (err) throw err
|
||||
sales.value.unshift(data as UsedSale)
|
||||
return data as UsedSale
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '저장에 실패했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSale(id: string, payload: Partial<UsedSaleInsert>) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('used_sales')
|
||||
.update(payload)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single()
|
||||
if (err) throw err
|
||||
const idx = sales.value.findIndex(s => s.id === id)
|
||||
if (idx !== -1) sales.value[idx] = data as UsedSale
|
||||
return data as UsedSale
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '수정에 실패했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSale(id: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const { error: err } = await client
|
||||
.from('used_sales')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
if (err) throw err
|
||||
sales.value = sales.value.filter(s => s.id !== id)
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '삭제에 실패했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getSale(id: string): Promise<UsedSale | null> {
|
||||
try {
|
||||
const { data, error: err } = await client
|
||||
.from('used_sales')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
if (err) throw err
|
||||
return data as UsedSale
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sales: readonly(sales),
|
||||
loading: readonly(loading),
|
||||
error: readonly(error),
|
||||
totalRevenue,
|
||||
byStatus,
|
||||
fetchSales,
|
||||
createSale,
|
||||
updateSale,
|
||||
deleteSale,
|
||||
getSale
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user