Files
dognation_PT/src/pages/ProfilePage.vue
T
sb 3bebe77cab
CI / build (push) Failing after 15m8s
chore: UI 문구 '우리 개' → '우리 강아지'
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:00:34 +09:00

249 lines
7.5 KiB
Vue

<template>
<q-page class="q-pa-md">
<!-- 회원 + 구독 티어 (실데이터) -->
<q-card flat bordered class="q-mb-md">
<q-item>
<q-item-section avatar>
<q-avatar color="primary" text-color="white" icon="person" />
</q-item-section>
<q-item-section>
<q-item-label class="text-weight-medium">{{ auth.member?.nickname }}</q-item-label>
<q-item-label caption>{{ auth.member?.email }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-chip dense color="primary" text-color="white" icon="bolt">
{{ auth.member?.subscriptionTier }}
</q-chip>
</q-item-section>
</q-item>
</q-card>
<!-- 강아지 없을 -->
<q-card v-if="!dogs.myDogs.length" flat bordered class="q-pa-lg text-center text-grey-6">
<q-icon name="pets" size="40px" color="secondary" />
<div class="q-mt-sm q-mb-md">아직 등록된 강아지가 없어요.</div>
<q-btn color="primary" unelevated icon="add" label="우리 강아지 등록" @click="openCreate" />
</q-card>
<template v-else>
<!-- 강아지 선택 (여러 마리) -->
<div class="row items-center q-mb-sm">
<div class="text-subtitle1 text-weight-bold">우리 강아지</div>
<q-space />
<q-btn dense flat color="primary" icon="add" label="추가" @click="openCreate" />
</div>
<div v-if="dogs.myDogs.length > 1" class="row q-gutter-sm q-mb-md">
<q-chip
v-for="d in dogs.myDogs"
:key="d.id"
clickable
:color="d.id === dogs.currentDogId ? 'primary' : 'grey-3'"
:text-color="d.id === dogs.currentDogId ? 'white' : 'grey-8'"
@click="dogs.currentDogId = d.id"
>
{{ d.name }}
</q-chip>
</div>
<!-- 대표 강아지 프로필 -->
<q-card v-if="current" flat bordered class="q-mb-md">
<q-card-section class="row items-center">
<q-avatar size="64px" color="secondary" text-color="accent" icon="pets" />
<div class="q-ml-md">
<div class="text-h6">{{ current.name }}</div>
<div class="text-caption text-grey-7">
{{ current.breed || '견종 미상' }}<span v-if="current.size"> · {{ sizeLabel(current.size) }}</span>
</div>
</div>
<q-space />
<q-btn dense flat round color="grey-6" icon="delete_outline" @click="onDelete" />
</q-card-section>
</q-card>
<!-- 댕비티아이 성향 태그 편집 -->
<div class="text-subtitle1 text-weight-bold q-mb-sm">댕비티아이 · 성향 태그</div>
<div class="row q-gutter-sm q-mb-md">
<q-chip
v-for="tag in dogs.traitTags"
:key="tag.id"
clickable
:color="selected.has(tag.id) ? 'primary' : 'grey-3'"
:text-color="selected.has(tag.id) ? 'white' : 'grey-7'"
@click="toggle(tag.id)"
>
{{ tag.label }}
</q-chip>
</div>
<q-btn
color="primary"
unelevated
icon="save"
label="성향 저장"
:loading="saving"
:disable="!dirty"
@click="saveTraits"
/>
</template>
<!-- 강아지 등록 다이얼로그 -->
<q-dialog v-model="createOpen">
<q-card style="min-width: 300px">
<q-card-section class="text-subtitle1 text-weight-bold">우리 강아지 등록</q-card-section>
<q-card-section class="q-gutter-sm">
<q-input v-model="form.name" label="이름 *" dense outlined />
<q-input v-model="form.breed" label="견종" dense outlined />
<q-select
v-model="form.size"
:options="sizeOptions"
label="크기"
dense
outlined
emit-value
map-options
/>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="취소" v-close-popup />
<q-btn
color="primary"
unelevated
label="등록"
:loading="creating"
:disable="!form.name"
@click="onCreate"
/>
</q-card-actions>
</q-card>
</q-dialog>
</q-page>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useQuasar } from 'quasar'
import { useAuthStore } from '@/stores/auth'
import { useDogsStore } from '@/stores/dogs'
import { ApiError } from '@/api/http'
const $q = useQuasar()
const auth = useAuthStore()
const dogs = useDogsStore()
const current = computed(() => dogs.currentDog)
const selected = ref<Set<number>>(new Set())
const saving = ref(false)
const createOpen = ref(false)
const creating = ref(false)
const sizeOptions = [
{ label: '소형견', value: 'SMALL' },
{ label: '중형견', value: 'MEDIUM' },
{ label: '대형견', value: 'LARGE' },
]
const form = reactive<{ name: string; breed: string; size: string | null }>({
name: '',
breed: '',
size: null,
})
onMounted(async () => {
try {
await Promise.all([dogs.loadMyDogs(), dogs.loadTraitTags()])
} catch (e) {
notifyError(e)
}
})
// 대표 강아지가 바뀌면 선택된 성향 동기화
watch(
current,
(d) => {
selected.value = new Set(d?.traits.map((t) => t.id) ?? [])
},
{ immediate: true },
)
// 변경 감지(저장 버튼 활성화)
const dirty = computed(() => {
const orig = new Set(current.value?.traits.map((t) => t.id) ?? [])
if (orig.size !== selected.value.size) return true
for (const id of selected.value) if (!orig.has(id)) return true
return false
})
function toggle(id: number) {
const next = new Set(selected.value)
next.has(id) ? next.delete(id) : next.add(id)
selected.value = next
}
function sizeLabel(size: string) {
return sizeOptions.find((o) => o.value === size)?.label ?? size
}
async function saveTraits() {
if (!current.value) return
saving.value = true
try {
await dogs.updateDog(current.value.id, {
name: current.value.name,
breed: current.value.breed,
birthDate: current.value.birthDate,
size: current.value.size,
gender: current.value.gender,
neutered: current.value.neutered,
imageUrl: current.value.imageUrl,
traitIds: [...selected.value],
})
$q.notify({ color: 'primary', message: '성향을 저장했어요.', icon: 'check', position: 'top' })
} catch (e) {
notifyError(e)
} finally {
saving.value = false
}
}
function openCreate() {
form.name = ''
form.breed = ''
form.size = null
createOpen.value = true
}
async function onCreate() {
creating.value = true
try {
await dogs.createDog({ name: form.name, breed: form.breed || null, size: form.size })
createOpen.value = false
$q.notify({ color: 'primary', message: `${form.name} 등록 완료!`, icon: 'pets', position: 'top' })
} catch (e) {
notifyError(e)
} finally {
creating.value = false
}
}
async function onDelete() {
if (!current.value) return
$q.dialog({
title: '삭제',
message: `${current.value.name}을(를) 삭제할까요?`,
cancel: true,
persistent: true,
}).onOk(async () => {
try {
await dogs.removeDog(current.value!.id)
$q.notify({ color: 'grey-8', message: '삭제되었습니다.', position: 'top' })
} catch (e) {
notifyError(e)
}
})
}
function notifyError(e: unknown) {
const message = e instanceof ApiError ? e.message : '요청 중 오류가 발생했습니다.'
$q.notify({ color: 'negative', message, icon: 'error', position: 'top' })
}
</script>