2026-07-11 07:50:47 +09:00
|
|
|
<template>
|
|
|
|
|
<q-page class="q-pa-md">
|
2026-07-11 13:26:42 +09:00
|
|
|
<!-- 카카오 지도: 스팟 마커 표시, 마커 클릭 시 해당 스팟 선택 -->
|
2026-07-11 08:20:19 +09:00
|
|
|
<q-card flat bordered class="q-mb-md overflow-hidden">
|
2026-07-11 13:26:42 +09:00
|
|
|
<KakaoMap :spots="spots" :selected-id="currentSpot?.id ?? null" @select="onSelectSpot" />
|
2026-07-11 07:50:47 +09:00
|
|
|
</q-card>
|
|
|
|
|
|
2026-07-11 08:07:53 +09:00
|
|
|
<!-- 로딩 / 에러 -->
|
|
|
|
|
<q-banner v-if="loadError" dense class="bg-red-1 text-red-8 q-mb-md rounded-borders">
|
|
|
|
|
<template #avatar><q-icon name="wifi_off" /></template>
|
|
|
|
|
백엔드에 연결하지 못했습니다. (서버 실행 여부를 확인하세요)
|
|
|
|
|
</q-banner>
|
|
|
|
|
|
2026-07-11 09:18:23 +09:00
|
|
|
<!-- 체크인 + 채팅 -->
|
|
|
|
|
<div class="row q-col-gutter-sm">
|
|
|
|
|
<div class="col">
|
|
|
|
|
<q-btn
|
|
|
|
|
color="primary"
|
|
|
|
|
class="full-width q-py-sm"
|
|
|
|
|
unelevated
|
|
|
|
|
:loading="checkingIn"
|
|
|
|
|
:disable="!currentSpot"
|
|
|
|
|
icon="place"
|
|
|
|
|
:label="checkedIn ? '체크인 완료' : '나 지금 여기 도착!'"
|
|
|
|
|
@click="onCheckIn"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-auto">
|
|
|
|
|
<q-btn
|
|
|
|
|
color="accent"
|
|
|
|
|
class="full-height q-px-md"
|
|
|
|
|
outline
|
|
|
|
|
:disable="!currentSpot"
|
|
|
|
|
icon="chat"
|
|
|
|
|
label="채팅"
|
|
|
|
|
@click="chatOpen = true"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 스팟 채팅 -->
|
|
|
|
|
<ChatDialog
|
|
|
|
|
v-if="currentSpot"
|
|
|
|
|
v-model="chatOpen"
|
|
|
|
|
:spot-id="currentSpot.id"
|
|
|
|
|
:spot-name="currentSpot.name"
|
2026-07-11 07:50:47 +09:00
|
|
|
/>
|
|
|
|
|
|
2026-07-11 13:44:46 +09:00
|
|
|
<!-- AI 궁합 추천 (체크인 후, 이 스팟의 강아지 중 사이좋게 지낼 만한 목록) -->
|
|
|
|
|
<div v-if="aiLoading || aiTried" class="q-mt-lg">
|
|
|
|
|
<div class="text-subtitle1 text-weight-bold q-mb-sm">
|
|
|
|
|
<q-icon name="auto_awesome" color="primary" class="q-mr-xs" />AI 궁합 추천
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="aiLoading" class="row items-center q-gutter-sm text-grey-6">
|
|
|
|
|
<q-spinner-dots size="24px" color="primary" />
|
|
|
|
|
<span class="text-caption">사이좋게 지낼 강아지를 찾는 중…</span>
|
|
|
|
|
</div>
|
|
|
|
|
<template v-else>
|
|
|
|
|
<q-card v-for="m in aiMates" :key="m.dogId" flat bordered class="q-mb-sm">
|
|
|
|
|
<q-item>
|
|
|
|
|
<q-item-section avatar>
|
|
|
|
|
<q-avatar color="primary" text-color="white" icon="favorite" />
|
|
|
|
|
</q-item-section>
|
|
|
|
|
<q-item-section>
|
|
|
|
|
<q-item-label class="text-weight-medium">
|
|
|
|
|
{{ m.name }}
|
|
|
|
|
<q-badge color="primary" class="q-ml-xs">궁합 {{ m.score }}</q-badge>
|
|
|
|
|
</q-item-label>
|
|
|
|
|
<q-item-label caption>{{ m.breed || '견종 미상' }} · {{ m.reason }}</q-item-label>
|
|
|
|
|
</q-item-section>
|
|
|
|
|
</q-item>
|
|
|
|
|
</q-card>
|
|
|
|
|
<div v-if="!aiMates.length" class="text-caption text-grey-5">
|
|
|
|
|
지금 이 스팟엔 잘 맞는 강아지가 없어요.
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-11 07:50:47 +09:00
|
|
|
<!-- 스팟 한줄평 (네이버 지도식 태그형) -->
|
|
|
|
|
<div class="text-subtitle1 text-weight-bold q-mt-lg q-mb-sm">
|
2026-07-11 08:07:53 +09:00
|
|
|
{{ currentSpot ? currentSpot.name : '스팟' }} · 오늘의 한줄평
|
2026-07-11 07:50:47 +09:00
|
|
|
</div>
|
2026-07-11 08:07:53 +09:00
|
|
|
<div v-if="reviews.length" class="row q-gutter-sm">
|
2026-07-11 07:50:47 +09:00
|
|
|
<q-chip
|
2026-07-11 08:07:53 +09:00
|
|
|
v-for="review in reviews"
|
|
|
|
|
:key="review.id"
|
2026-07-11 07:50:47 +09:00
|
|
|
color="secondary"
|
|
|
|
|
text-color="accent"
|
|
|
|
|
icon="tag"
|
|
|
|
|
>
|
2026-07-11 08:07:53 +09:00
|
|
|
{{ review.tag || review.content }}
|
2026-07-11 07:50:47 +09:00
|
|
|
</q-chip>
|
|
|
|
|
</div>
|
2026-07-11 08:07:53 +09:00
|
|
|
<div v-else class="text-caption text-grey-5">아직 한줄평이 없어요.</div>
|
2026-07-11 07:50:47 +09:00
|
|
|
|
|
|
|
|
<!-- 현재 체크인 중인 메이트 -->
|
|
|
|
|
<div class="text-subtitle1 text-weight-bold q-mt-lg q-mb-sm">지금 이 스팟의 산책 메이트</div>
|
2026-07-11 08:07:53 +09:00
|
|
|
<div v-if="!mates.length" class="text-caption text-grey-5">체크인 중인 메이트가 없어요.</div>
|
|
|
|
|
<q-card v-for="mate in mates" :key="mate.dogId" flat bordered class="q-mb-sm">
|
2026-07-11 07:50:47 +09:00
|
|
|
<q-item>
|
|
|
|
|
<q-item-section avatar>
|
|
|
|
|
<q-avatar color="secondary" text-color="accent" icon="pets" />
|
|
|
|
|
</q-item-section>
|
|
|
|
|
<q-item-section>
|
|
|
|
|
<q-item-label class="text-weight-medium">{{ mate.name }}</q-item-label>
|
2026-07-11 08:07:53 +09:00
|
|
|
<q-item-label caption>{{ mate.breed || '견종 미상' }}</q-item-label>
|
2026-07-11 07:50:47 +09:00
|
|
|
</q-item-section>
|
|
|
|
|
<q-item-section side>
|
2026-07-11 08:07:53 +09:00
|
|
|
<q-btn
|
|
|
|
|
dense
|
|
|
|
|
unelevated
|
|
|
|
|
color="primary"
|
|
|
|
|
icon="favorite"
|
|
|
|
|
label="매칭"
|
|
|
|
|
:loading="matchingDogId === mate.dogId"
|
|
|
|
|
@click="onMatch(mate)"
|
|
|
|
|
/>
|
2026-07-11 07:50:47 +09:00
|
|
|
</q-item-section>
|
|
|
|
|
</q-item>
|
|
|
|
|
</q-card>
|
|
|
|
|
</q-page>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-11 08:07:53 +09:00
|
|
|
import { onMounted, ref } from 'vue'
|
2026-07-11 07:50:47 +09:00
|
|
|
import { useQuasar } from 'quasar'
|
2026-07-11 13:44:46 +09:00
|
|
|
import { spotsApi, type AiMate, type Mate, type Review, type Spot } from '@/api/spots'
|
2026-07-11 08:07:53 +09:00
|
|
|
import { matchesApi } from '@/api/matches'
|
|
|
|
|
import { ApiError } from '@/api/http'
|
2026-07-11 08:35:00 +09:00
|
|
|
import { useAuthStore } from '@/stores/auth'
|
2026-07-11 09:00:19 +09:00
|
|
|
import { useDogsStore } from '@/stores/dogs'
|
2026-07-11 13:26:42 +09:00
|
|
|
import KakaoMap from '@/components/KakaoMap.vue'
|
2026-07-11 09:18:23 +09:00
|
|
|
import ChatDialog from '@/components/ChatDialog.vue'
|
2026-07-11 07:50:47 +09:00
|
|
|
|
|
|
|
|
const $q = useQuasar()
|
2026-07-11 08:35:00 +09:00
|
|
|
const auth = useAuthStore()
|
2026-07-11 09:00:19 +09:00
|
|
|
const dogs = useDogsStore()
|
2026-07-11 08:35:00 +09:00
|
|
|
|
2026-07-11 09:18:23 +09:00
|
|
|
const chatOpen = ref(false)
|
|
|
|
|
|
2026-07-11 09:00:19 +09:00
|
|
|
// 체크인/매칭 주체: 로그인 회원 + 대표 강아지
|
|
|
|
|
const currentUserId = () => auth.member?.id ?? 0
|
2026-07-11 08:07:53 +09:00
|
|
|
|
2026-07-11 08:20:19 +09:00
|
|
|
const spots = ref<Spot[]>([])
|
2026-07-11 08:07:53 +09:00
|
|
|
const currentSpot = ref<Spot | null>(null)
|
|
|
|
|
const reviews = ref<Review[]>([])
|
|
|
|
|
const mates = ref<Mate[]>([])
|
2026-07-11 07:50:47 +09:00
|
|
|
const checkedIn = ref(false)
|
2026-07-11 08:07:53 +09:00
|
|
|
const checkingIn = ref(false)
|
|
|
|
|
const matchingDogId = ref<number | null>(null)
|
|
|
|
|
const loadError = ref(false)
|
2026-07-11 07:50:47 +09:00
|
|
|
|
2026-07-11 13:44:46 +09:00
|
|
|
// AI 궁합 추천 (체크인 후 로드)
|
|
|
|
|
const aiMates = ref<AiMate[]>([])
|
|
|
|
|
const aiLoading = ref(false)
|
|
|
|
|
const aiTried = ref(false)
|
|
|
|
|
|
2026-07-11 08:07:53 +09:00
|
|
|
onMounted(load)
|
2026-07-11 07:50:47 +09:00
|
|
|
|
2026-07-11 08:07:53 +09:00
|
|
|
async function load() {
|
|
|
|
|
loadError.value = false
|
|
|
|
|
try {
|
2026-07-11 09:00:19 +09:00
|
|
|
await dogs.loadMyDogs()
|
2026-07-11 08:20:19 +09:00
|
|
|
spots.value = await spotsApi.list()
|
|
|
|
|
currentSpot.value = spots.value[0] ?? null
|
2026-07-11 08:07:53 +09:00
|
|
|
if (currentSpot.value) {
|
|
|
|
|
await refreshSpot(currentSpot.value.id)
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
loadError.value = true
|
|
|
|
|
console.error(e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 08:20:19 +09:00
|
|
|
async function onSelectSpot(spotId: number) {
|
|
|
|
|
const spot = spots.value.find((s) => s.id === spotId)
|
|
|
|
|
if (!spot) return
|
|
|
|
|
currentSpot.value = spot
|
|
|
|
|
checkedIn.value = false
|
2026-07-11 13:44:46 +09:00
|
|
|
aiMates.value = []
|
|
|
|
|
aiTried.value = false
|
2026-07-11 08:20:19 +09:00
|
|
|
await refreshSpot(spotId)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 08:07:53 +09:00
|
|
|
async function refreshSpot(spotId: number) {
|
2026-07-11 13:58:30 +09:00
|
|
|
const [r, m] = await Promise.all([
|
|
|
|
|
spotsApi.reviews(spotId),
|
|
|
|
|
spotsApi.mates(spotId, currentUserId()),
|
|
|
|
|
])
|
2026-07-11 08:07:53 +09:00
|
|
|
reviews.value = r
|
|
|
|
|
mates.value = m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onCheckIn() {
|
|
|
|
|
if (!currentSpot.value) return
|
2026-07-11 09:00:19 +09:00
|
|
|
if (dogs.currentDogId == null) {
|
|
|
|
|
$q.notify({ color: 'warning', message: '먼저 우리 개를 등록해 주세요.', icon: 'pets', position: 'top' })
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-11 08:07:53 +09:00
|
|
|
checkingIn.value = true
|
|
|
|
|
try {
|
2026-07-11 09:00:19 +09:00
|
|
|
await spotsApi.checkIn(currentSpot.value.id, currentUserId(), dogs.currentDogId)
|
2026-07-11 08:07:53 +09:00
|
|
|
checkedIn.value = true
|
|
|
|
|
await refreshSpot(currentSpot.value.id)
|
|
|
|
|
$q.notify({ color: 'primary', message: '체크인 완료! 스팟 채팅이 열렸어요.', icon: 'place', position: 'top' })
|
2026-07-11 13:44:46 +09:00
|
|
|
loadAiMates() // 체크인 후 AI 궁합 추천 (비동기)
|
2026-07-11 08:07:53 +09:00
|
|
|
} catch (e) {
|
|
|
|
|
notifyError(e)
|
|
|
|
|
} finally {
|
|
|
|
|
checkingIn.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 13:44:46 +09:00
|
|
|
/** 체크인 후 AI 궁합 추천 로드 — 이 스팟의 강아지 중 내 개와 사이좋게 지낼 만한 목록. */
|
|
|
|
|
async function loadAiMates() {
|
|
|
|
|
if (!currentSpot.value || dogs.currentDogId == null) return
|
|
|
|
|
aiLoading.value = true
|
|
|
|
|
aiTried.value = true
|
|
|
|
|
try {
|
|
|
|
|
aiMates.value = await spotsApi.aiMates(currentSpot.value.id, dogs.currentDogId)
|
|
|
|
|
} catch {
|
|
|
|
|
aiMates.value = []
|
|
|
|
|
} finally {
|
|
|
|
|
aiLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 08:07:53 +09:00
|
|
|
async function onMatch(mate: Mate) {
|
2026-07-11 09:00:19 +09:00
|
|
|
if (dogs.currentDogId == null) {
|
|
|
|
|
$q.notify({ color: 'warning', message: '먼저 우리 개를 등록해 주세요.', icon: 'pets', position: 'top' })
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-11 08:07:53 +09:00
|
|
|
matchingDogId.value = mate.dogId
|
|
|
|
|
try {
|
2026-07-11 09:00:19 +09:00
|
|
|
await matchesApi.create(dogs.currentDogId, mate.dogId)
|
2026-07-11 08:07:53 +09:00
|
|
|
$q.notify({ color: 'positive', message: `${mate.name}에게 매칭을 신청했어요!`, icon: 'favorite', position: 'top' })
|
|
|
|
|
} catch (e) {
|
|
|
|
|
notifyError(e)
|
|
|
|
|
} finally {
|
|
|
|
|
matchingDogId.value = null
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-11 07:50:47 +09:00
|
|
|
|
2026-07-11 08:07:53 +09:00
|
|
|
function notifyError(e: unknown) {
|
|
|
|
|
const message =
|
|
|
|
|
e instanceof ApiError ? e.message : '요청 중 오류가 발생했습니다. 서버 연결을 확인하세요.'
|
|
|
|
|
$q.notify({ color: 'negative', message, icon: 'error', position: 'top' })
|
2026-07-11 07:50:47 +09:00
|
|
|
}
|
|
|
|
|
</script>
|