diff --git a/src/api/dogs.ts b/src/api/dogs.ts new file mode 100644 index 0000000..c1fd356 --- /dev/null +++ b/src/api/dogs.ts @@ -0,0 +1,42 @@ +import { http } from './http' + +export interface TraitTag { + id: number + code: string + label: string + category: string +} + +export interface Dog { + id: number + name: string + breed: string | null + birthDate: string | null + size: string | null + gender: string | null + neutered: boolean + imageUrl: string | null + traits: TraitTag[] +} + +export interface DogSaveRequest { + name: string + breed?: string | null + birthDate?: string | null + size?: string | null + gender?: string | null + neutered?: boolean + imageUrl?: string | null + traitIds?: number[] +} + +export const dogsApi = { + list: () => http.get('/api/dogs'), + create: (body: DogSaveRequest) => http.post('/api/dogs', body), + update: (id: number, body: DogSaveRequest) => http.put(`/api/dogs/${id}`, body), + remove: (id: number) => http.del(`/api/dogs/${id}`), +} + +export const traitTagsApi = { + list: () => http.get('/api/trait-tags'), +} diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 10d3bdb..aea6b4a 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -55,10 +55,12 @@ import { useRouter } from 'vue-router' import AdBanner from '@/components/AdBanner.vue' import { useSubscriptionStore } from '@/stores/subscription' import { useAuthStore } from '@/stores/auth' +import { useDogsStore } from '@/stores/dogs' const tab = ref('home') const subscription = useSubscriptionStore() const auth = useAuthStore() +const dogs = useDogsStore() const router = useRouter() const tierLabel = computed(() => { @@ -74,6 +76,7 @@ const tierLabel = computed(() => { async function onLogout() { await auth.logout() + dogs.reset() router.replace({ name: 'login' }) } diff --git a/src/pages/HomePage.vue b/src/pages/HomePage.vue index 800ccc9..bbaf36b 100644 --- a/src/pages/HomePage.vue +++ b/src/pages/HomePage.vue @@ -74,16 +74,16 @@ import { useQuasar } from 'quasar' import { spotsApi, type Mate, type Review, type Spot } from '@/api/spots' import { matchesApi } from '@/api/matches' import { ApiError } from '@/api/http' -import { useSessionStore } from '@/stores/session' import { useAuthStore } from '@/stores/auth' +import { useDogsStore } from '@/stores/dogs' import NaverMap from '@/components/NaverMap.vue' const $q = useQuasar() -const session = useSessionStore() const auth = useAuthStore() +const dogs = useDogsStore() -// 체크인/매칭 주체: 로그인한 회원. 강아지 ID 는 아직 견 관리 API 전이라 세션 기본값 사용. -const currentUserId = () => auth.member?.id ?? session.userId +// 체크인/매칭 주체: 로그인 회원 + 대표 강아지 +const currentUserId = () => auth.member?.id ?? 0 const spots = ref([]) const currentSpot = ref(null) @@ -99,6 +99,7 @@ onMounted(load) async function load() { loadError.value = false try { + await dogs.loadMyDogs() spots.value = await spotsApi.list() currentSpot.value = spots.value[0] ?? null if (currentSpot.value) { @@ -126,9 +127,13 @@ async function refreshSpot(spotId: number) { async function onCheckIn() { if (!currentSpot.value) return + if (dogs.currentDogId == null) { + $q.notify({ color: 'warning', message: '먼저 우리 개를 등록해 주세요.', icon: 'pets', position: 'top' }) + return + } checkingIn.value = true try { - await spotsApi.checkIn(currentSpot.value.id, currentUserId(), session.dogId) + await spotsApi.checkIn(currentSpot.value.id, currentUserId(), dogs.currentDogId) checkedIn.value = true await refreshSpot(currentSpot.value.id) $q.notify({ color: 'primary', message: '체크인 완료! 스팟 채팅이 열렸어요.', icon: 'place', position: 'top' }) @@ -140,9 +145,13 @@ async function onCheckIn() { } async function onMatch(mate: Mate) { + if (dogs.currentDogId == null) { + $q.notify({ color: 'warning', message: '먼저 우리 개를 등록해 주세요.', icon: 'pets', position: 'top' }) + return + } matchingDogId.value = mate.dogId try { - await matchesApi.create(session.dogId, mate.dogId) + await matchesApi.create(dogs.currentDogId, mate.dogId) $q.notify({ color: 'positive', message: `${mate.name}에게 매칭을 신청했어요!`, icon: 'favorite', position: 'top' }) } catch (e) { notifyError(e) diff --git a/src/pages/ProfilePage.vue b/src/pages/ProfilePage.vue index f9e81f7..1b2cfe3 100644 --- a/src/pages/ProfilePage.vue +++ b/src/pages/ProfilePage.vue @@ -1,73 +1,248 @@ diff --git a/src/stores/dogs.ts b/src/stores/dogs.ts new file mode 100644 index 0000000..97f29c2 --- /dev/null +++ b/src/stores/dogs.ts @@ -0,0 +1,64 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { dogsApi, traitTagsApi, type Dog, type DogSaveRequest, type TraitTag } from '@/api/dogs' + +/** + * 내 강아지 상태. 로그인 후 loadMyDogs 로 채운다. + * currentDogId 는 체크인/매칭의 주체(대표 강아지). + */ +export const useDogsStore = defineStore('dogs', () => { + const myDogs = ref([]) + const traitTags = ref([]) + const currentDogId = ref(null) + + const currentDog = computed(() => myDogs.value.find((d) => d.id === currentDogId.value) ?? null) + + async function loadMyDogs(): Promise { + myDogs.value = await dogsApi.list() + if (currentDogId.value == null || !myDogs.value.some((d) => d.id === currentDogId.value)) { + currentDogId.value = myDogs.value[0]?.id ?? null + } + } + + async function loadTraitTags(): Promise { + if (traitTags.value.length === 0) { + traitTags.value = await traitTagsApi.list() + } + } + + async function createDog(body: DogSaveRequest): Promise { + const dog = await dogsApi.create(body) + await loadMyDogs() + currentDogId.value = dog.id + return dog + } + + async function updateDog(id: number, body: DogSaveRequest): Promise { + const dog = await dogsApi.update(id, body) + await loadMyDogs() + return dog + } + + async function removeDog(id: number): Promise { + await dogsApi.remove(id) + await loadMyDogs() + } + + function reset(): void { + myDogs.value = [] + currentDogId.value = null + } + + return { + myDogs, + traitTags, + currentDogId, + currentDog, + loadMyDogs, + loadTraitTags, + createDog, + updateDog, + removeDog, + reset, + } +}) diff --git a/src/stores/session.ts b/src/stores/session.ts deleted file mode 100644 index 5257eef..0000000 --- a/src/stores/session.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineStore } from 'pinia' -import { ref } from 'vue' - -/** - * 임시 세션 스토어 (인증 도입 전까지 사용). - * local 시드 데이터 기준: 내 유저=1, 내 강아지(몽이)=1. - * 인증(4번) 도입 시 로그인 응답으로 대체. - */ -export const useSessionStore = defineStore('session', () => { - const userId = ref(1) - const dogId = ref(1) - return { userId, dogId } -})