feat: nuxt-claude 프로젝트 초기 커밋
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 25m52s

Made-with: Cursor
This commit is contained in:
2026-03-08 16:36:13 +09:00
commit e66321386a
44 changed files with 13058 additions and 0 deletions

View 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
}
}

View 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
}
}

View 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
}
}