- FeedbackDialog: 잘 놀았어요/안 맞았어요 + BAD 사유 선택·추가입력 - 채팅 '산책 완료' 버튼 + 2시간 FEEDBACK_REQUEST 알림 → 피드백 팝업 - AffinityBadges: 동네 찰떡지수(전체), 우리 찰떡지수(매칭 쌍·유료 게이팅) 스팟 메이트/AI 추천 카드 및 우리 강아지 프로필에 노출 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
import { http } from './http'
|
||||||
|
|
||||||
|
export type FeedbackVerdict = 'GOOD' | 'BAD'
|
||||||
|
|
||||||
|
/** 동네 찰떡지수 (전체 노출). */
|
||||||
|
export interface NeighborhoodScore {
|
||||||
|
percent: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 우리 찰떡지수 (1회 이상 매칭된 쌍만).
|
||||||
|
* - locked=true : 무료회원 → "유료회원 전용"
|
||||||
|
* - matched=false: 매칭 이력 없음 → 미노출
|
||||||
|
*/
|
||||||
|
export interface PairScore {
|
||||||
|
matched: boolean
|
||||||
|
locked: boolean
|
||||||
|
percent: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const affinityApi = {
|
||||||
|
/** 산책 후 피드백 제출 → 우리/동네 찰떡지수 반영. */
|
||||||
|
feedback: (matchId: number, raterDogId: number, verdict: FeedbackVerdict, reasons?: string) =>
|
||||||
|
http.post<void>(`/api/affinity/matches/${matchId}/feedback`, { raterDogId, verdict, reasons }),
|
||||||
|
/** 동네 찰떡지수. */
|
||||||
|
neighborhood: (dogId: number) => http.get<NeighborhoodScore>(`/api/affinity/neighborhood/${dogId}`),
|
||||||
|
/** 우리 찰떡지수 — viewerDogId(내 강아지) 기준. */
|
||||||
|
pair: (viewerDogId: number, otherDogId: number) =>
|
||||||
|
http.get<PairScore>(`/api/affinity/pair?viewerDogId=${viewerDogId}&otherDogId=${otherDogId}`),
|
||||||
|
}
|
||||||
+1
-1
@@ -22,7 +22,7 @@ export interface ReceivedMatch {
|
|||||||
|
|
||||||
/** STOMP /topic/users/{userId} 로 오는 매칭 알림. */
|
/** STOMP /topic/users/{userId} 로 오는 매칭 알림. */
|
||||||
export interface MatchNotification {
|
export interface MatchNotification {
|
||||||
type: 'NEW_REQUEST' | 'ACCEPTED' | 'REJECTED' | 'EXPIRED'
|
type: 'NEW_REQUEST' | 'ACCEPTED' | 'REJECTED' | 'EXPIRED' | 'FEEDBACK_REQUEST'
|
||||||
matchId: number
|
matchId: number
|
||||||
title: string
|
title: string
|
||||||
message: string
|
message: string
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="column q-gutter-xs">
|
||||||
|
<!-- 동네 찰떡지수 (전체 노출) -->
|
||||||
|
<div v-if="hood" class="text-caption row items-center no-wrap">
|
||||||
|
<q-icon name="groups" size="14px" color="teal" class="q-mr-xs" />
|
||||||
|
<span class="text-weight-medium text-teal-8">동네 찰떡지수 {{ hood.percent }}%</span>
|
||||||
|
<span class="text-grey-6 q-ml-xs">({{ hood.label }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 우리 찰떡지수 (매칭 이력 있는 쌍만) -->
|
||||||
|
<div v-if="pair?.matched" class="text-caption row items-center no-wrap">
|
||||||
|
<q-icon name="favorite" size="14px" color="pink-5" class="q-mr-xs" />
|
||||||
|
<template v-if="pair.locked">
|
||||||
|
<span class="text-weight-medium text-pink-6">우리 찰떡지수</span>
|
||||||
|
<q-badge color="amber-7" class="q-ml-xs" text-color="white">유료회원 전용</q-badge>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-weight-medium text-pink-6">우리 찰떡지수 {{ pair.percent }}%</span>
|
||||||
|
<span class="text-grey-6 q-ml-xs">({{ pair.label }})</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { affinityApi, type NeighborhoodScore, type PairScore } from '@/api/affinity'
|
||||||
|
|
||||||
|
const props = defineProps<{ myDogId: number; otherDogId: number }>()
|
||||||
|
|
||||||
|
const hood = ref<NeighborhoodScore | null>(null)
|
||||||
|
const pair = ref<PairScore | null>(null)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.myDogId, props.otherDogId] as const,
|
||||||
|
() => void load(),
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
// 부가 정보 — 실패해도 카드 나머지는 정상 표시
|
||||||
|
try {
|
||||||
|
hood.value = await affinityApi.neighborhood(props.otherDogId)
|
||||||
|
} catch {
|
||||||
|
hood.value = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
pair.value = await affinityApi.pair(props.myDogId, props.otherDogId)
|
||||||
|
} catch {
|
||||||
|
pair.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog :model-value="modelValue" @update:model-value="onDialog" persistent>
|
||||||
|
<q-card class="feedback-card">
|
||||||
|
<q-card-section class="text-center q-pb-none">
|
||||||
|
<div class="text-h6 text-weight-bold">오늘 산책 어떠셨나요?</div>
|
||||||
|
<div v-if="otherDogName" class="text-caption text-grey-7 q-mt-xs">
|
||||||
|
{{ otherDogName }}와의 산책을 평가해 주세요
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- 1단계: 잘 놀았어요 / 안 맞았어요 -->
|
||||||
|
<q-card-section v-if="!showReasons" class="q-gutter-sm">
|
||||||
|
<q-btn
|
||||||
|
class="full-width verdict-btn"
|
||||||
|
unelevated
|
||||||
|
color="positive"
|
||||||
|
no-caps
|
||||||
|
:loading="submitting"
|
||||||
|
@click="submitGood"
|
||||||
|
>
|
||||||
|
<q-icon name="sentiment_very_satisfied" size="26px" class="q-mr-sm" />
|
||||||
|
잘 놀았어요
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
class="full-width verdict-btn"
|
||||||
|
outline
|
||||||
|
color="grey-8"
|
||||||
|
no-caps
|
||||||
|
:disable="submitting"
|
||||||
|
@click="showReasons = true"
|
||||||
|
>
|
||||||
|
<q-icon name="sentiment_dissatisfied" size="26px" class="q-mr-sm" />
|
||||||
|
안 맞았어요
|
||||||
|
</q-btn>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- 2단계: 안 맞았어요 사유 -->
|
||||||
|
<q-card-section v-else>
|
||||||
|
<div class="text-subtitle2 q-mb-sm">어떤 점이 아쉬웠나요? (선택)</div>
|
||||||
|
<q-option-group
|
||||||
|
v-model="selectedReasons"
|
||||||
|
:options="reasonOptions"
|
||||||
|
type="checkbox"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="extraReason"
|
||||||
|
class="q-mt-sm"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
maxlength="200"
|
||||||
|
placeholder="추가로 남기고 싶은 점이 있다면 적어주세요"
|
||||||
|
/>
|
||||||
|
<div class="row q-gutter-sm q-mt-md">
|
||||||
|
<q-btn flat color="grey-7" label="뒤로" no-caps :disable="submitting" @click="showReasons = false" />
|
||||||
|
<q-btn
|
||||||
|
class="col"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
label="제출"
|
||||||
|
no-caps
|
||||||
|
:loading="submitting"
|
||||||
|
@click="submitBad"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { affinityApi } from '@/api/affinity'
|
||||||
|
import { ApiError } from '@/api/http'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
matchId: number
|
||||||
|
raterDogId: number
|
||||||
|
otherDogName?: string
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', v: boolean): void
|
||||||
|
(e: 'submitted'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
|
|
||||||
|
const showReasons = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const selectedReasons = ref<string[]>([])
|
||||||
|
const extraReason = ref('')
|
||||||
|
|
||||||
|
const reasonOptions = [
|
||||||
|
{ label: '강아지 성향이 안 맞아요', value: '강아지 성향이 안 맞아요' },
|
||||||
|
{ label: '간식/장난감 때문에 으르릉거려요', value: '간식/장난감 때문에 으르릉거려요' },
|
||||||
|
{ label: '일방적으로 입질/공격성을 보였어요', value: '일방적으로 입질/공격성을 보였어요' },
|
||||||
|
{ label: '보호자 매너가 아쉬워요', value: '보호자 매너가 아쉬워요' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 열릴 때마다 초기화
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
showReasons.value = false
|
||||||
|
selectedReasons.value = []
|
||||||
|
extraReason.value = ''
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function onDialog(v: boolean) {
|
||||||
|
emit('update:modelValue', v)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitGood() {
|
||||||
|
await submit('GOOD')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitBad() {
|
||||||
|
const reasons = [...selectedReasons.value]
|
||||||
|
const extra = extraReason.value.trim()
|
||||||
|
if (extra) reasons.push(extra)
|
||||||
|
await submit('BAD', reasons.join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit(verdict: 'GOOD' | 'BAD', reasons?: string) {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await affinityApi.feedback(props.matchId, props.raterDogId, verdict, reasons)
|
||||||
|
$q.notify({
|
||||||
|
color: 'positive',
|
||||||
|
icon: 'favorite',
|
||||||
|
message: '소중한 피드백 고마워요! 찰떡지수에 반영했어요.',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 2500,
|
||||||
|
})
|
||||||
|
emit('submitted')
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
} catch (e) {
|
||||||
|
// 이미 제출한 경우(409) 등은 조용히 닫기
|
||||||
|
const message = e instanceof ApiError ? e.message : '피드백 전송에 실패했어요.'
|
||||||
|
$q.notify({ color: 'grey-8', icon: 'info', message, position: 'top', timeout: 2500 })
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feedback-card {
|
||||||
|
width: 340px;
|
||||||
|
max-width: 90vw;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
.verdict-btn {
|
||||||
|
height: 56px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
<q-chip dense :color="connected ? 'positive' : 'grey-5'" text-color="white">
|
<q-chip dense :color="connected ? 'positive' : 'grey-5'" text-color="white">
|
||||||
{{ connected ? '실시간' : '연결 중…' }}
|
{{ connected ? '실시간' : '연결 중…' }}
|
||||||
</q-chip>
|
</q-chip>
|
||||||
|
<q-btn flat dense no-caps icon="event_available" label="산책 완료" class="q-mr-xs" @click="onCheckout">
|
||||||
|
<q-tooltip>산책을 마치고 오늘 어땠는지 남겨주세요</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
<q-btn flat round dense icon="close" @click="close" />
|
<q-btn flat round dense icon="close" @click="close" />
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
|
|
||||||
@@ -55,7 +58,10 @@ import { createMatchChat } from '@/lib/chatSocket'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: boolean; matchId: number }>()
|
const props = defineProps<{ modelValue: boolean; matchId: number }>()
|
||||||
const emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void }>()
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', v: boolean): void
|
||||||
|
(e: 'checkout', matchId: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
@@ -111,6 +117,10 @@ function close() {
|
|||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCheckout() {
|
||||||
|
emit('checkout', props.matchId)
|
||||||
|
}
|
||||||
|
|
||||||
function isMine(m: ChatMessage) {
|
function isMine(m: ChatMessage) {
|
||||||
return m.senderId === auth.member?.id
|
return m.senderId === auth.member?.id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,20 @@
|
|||||||
</q-footer>
|
</q-footer>
|
||||||
|
|
||||||
<!-- 매칭 수락 시 1:1 채팅 -->
|
<!-- 매칭 수락 시 1:1 채팅 -->
|
||||||
<MatchChatDialog v-if="matchChatId != null" v-model="matchChatOpen" :match-id="matchChatId" />
|
<MatchChatDialog
|
||||||
|
v-if="matchChatId != null"
|
||||||
|
v-model="matchChatOpen"
|
||||||
|
:match-id="matchChatId"
|
||||||
|
@checkout="openFeedback"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 산책 후 찰떡지수 피드백 -->
|
||||||
|
<FeedbackDialog
|
||||||
|
v-if="feedbackMatchId != null && dogs.currentDogId != null"
|
||||||
|
v-model="feedbackOpen"
|
||||||
|
:match-id="feedbackMatchId"
|
||||||
|
:rater-dog-id="dogs.currentDogId"
|
||||||
|
/>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -88,6 +101,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import AdBanner from '@/components/AdBanner.vue'
|
import AdBanner from '@/components/AdBanner.vue'
|
||||||
import MatchChatDialog from '@/components/MatchChatDialog.vue'
|
import MatchChatDialog from '@/components/MatchChatDialog.vue'
|
||||||
|
import FeedbackDialog from '@/components/FeedbackDialog.vue'
|
||||||
import { useSubscriptionStore } from '@/stores/subscription'
|
import { useSubscriptionStore } from '@/stores/subscription'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useDogsStore } from '@/stores/dogs'
|
import { useDogsStore } from '@/stores/dogs'
|
||||||
@@ -105,6 +119,15 @@ const $q = useQuasar()
|
|||||||
const matchChatOpen = ref(false)
|
const matchChatOpen = ref(false)
|
||||||
const matchChatId = ref<number | null>(null)
|
const matchChatId = ref<number | null>(null)
|
||||||
|
|
||||||
|
const feedbackOpen = ref(false)
|
||||||
|
const feedbackMatchId = ref<number | null>(null)
|
||||||
|
|
||||||
|
/** 채팅 '산책 완료' 버튼 또는 2시간 알림 → 피드백 팝업 열기. */
|
||||||
|
function openFeedback(id: number) {
|
||||||
|
feedbackMatchId.value = id
|
||||||
|
feedbackOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await dogs.loadMyDogs()
|
await dogs.loadMyDogs()
|
||||||
if (auth.member && dogs.currentDogId != null) {
|
if (auth.member && dogs.currentDogId != null) {
|
||||||
@@ -125,6 +148,17 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 2시간 후 피드백 요청 알림 → 찰떡지수 피드백 팝업
|
||||||
|
watch(
|
||||||
|
() => matching.feedbackMatchId,
|
||||||
|
(id) => {
|
||||||
|
if (id != null) {
|
||||||
|
openFeedback(id)
|
||||||
|
matching.feedbackMatchId = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async function onAccept(id: number) {
|
async function onAccept(id: number) {
|
||||||
try {
|
try {
|
||||||
await matching.accept(id)
|
await matching.accept(id)
|
||||||
|
|||||||
@@ -76,6 +76,12 @@
|
|||||||
<q-badge color="primary" class="q-ml-xs">궁합 {{ m.score }}</q-badge>
|
<q-badge color="primary" class="q-ml-xs">궁합 {{ m.score }}</q-badge>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption>{{ m.breed || '견종 미상' }} · {{ m.reason }}</q-item-label>
|
<q-item-label caption>{{ m.breed || '견종 미상' }} · {{ m.reason }}</q-item-label>
|
||||||
|
<AffinityBadges
|
||||||
|
v-if="dogs.currentDogId != null"
|
||||||
|
class="q-mt-xs"
|
||||||
|
:my-dog-id="dogs.currentDogId"
|
||||||
|
:other-dog-id="m.dogId"
|
||||||
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -113,6 +119,12 @@
|
|||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-weight-medium">{{ mate.name }}</q-item-label>
|
<q-item-label class="text-weight-medium">{{ mate.name }}</q-item-label>
|
||||||
<q-item-label caption>{{ mate.breed || '견종 미상' }}</q-item-label>
|
<q-item-label caption>{{ mate.breed || '견종 미상' }}</q-item-label>
|
||||||
|
<AffinityBadges
|
||||||
|
v-if="dogs.currentDogId != null"
|
||||||
|
class="q-mt-xs"
|
||||||
|
:my-dog-id="dogs.currentDogId"
|
||||||
|
:other-dog-id="mate.dogId"
|
||||||
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -150,6 +162,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { useDogsStore } from '@/stores/dogs'
|
import { useDogsStore } from '@/stores/dogs'
|
||||||
import { useMatchingStore } from '@/stores/matching'
|
import { useMatchingStore } from '@/stores/matching'
|
||||||
import KakaoMap from '@/components/KakaoMap.vue'
|
import KakaoMap from '@/components/KakaoMap.vue'
|
||||||
|
import AffinityBadges from '@/components/AffinityBadges.vue'
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|||||||
@@ -55,6 +55,11 @@
|
|||||||
<div class="text-caption text-grey-7">
|
<div class="text-caption text-grey-7">
|
||||||
{{ current.breed || '견종 미상' }}<span v-if="current.size"> · {{ sizeLabel(current.size) }}</span>
|
{{ current.breed || '견종 미상' }}<span v-if="current.size"> · {{ sizeLabel(current.size) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="hoodScore" class="text-caption row items-center no-wrap q-mt-xs">
|
||||||
|
<q-icon name="groups" size="14px" color="teal" class="q-mr-xs" />
|
||||||
|
<span class="text-weight-medium text-teal-8">동네 찰떡지수 {{ hoodScore.percent }}%</span>
|
||||||
|
<span class="text-grey-6 q-ml-xs">({{ hoodScore.label }})</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn dense flat round color="grey-6" icon="delete_outline" @click="onDelete" />
|
<q-btn dense flat round color="grey-6" icon="delete_outline" @click="onDelete" />
|
||||||
@@ -140,6 +145,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useDogsStore } from '@/stores/dogs'
|
import { useDogsStore } from '@/stores/dogs'
|
||||||
|
import { affinityApi, type NeighborhoodScore } from '@/api/affinity'
|
||||||
import { ApiError } from '@/api/http'
|
import { ApiError } from '@/api/http'
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
@@ -147,6 +153,22 @@ const auth = useAuthStore()
|
|||||||
const dogs = useDogsStore()
|
const dogs = useDogsStore()
|
||||||
|
|
||||||
const current = computed(() => dogs.currentDog)
|
const current = computed(() => dogs.currentDog)
|
||||||
|
|
||||||
|
// 내 강아지 동네 찰떡지수 (전체 노출 지표)
|
||||||
|
const hoodScore = ref<NeighborhoodScore | null>(null)
|
||||||
|
watch(
|
||||||
|
() => current.value?.id,
|
||||||
|
async (id) => {
|
||||||
|
hoodScore.value = null
|
||||||
|
if (id == null) return
|
||||||
|
try {
|
||||||
|
hoodScore.value = await affinityApi.neighborhood(id)
|
||||||
|
} catch {
|
||||||
|
hoodScore.value = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
const selected = ref<Set<number>>(new Set())
|
const selected = ref<Set<number>>(new Set())
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const createOpen = ref(false)
|
const createOpen = ref(false)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const useMatchingStore = defineStore('matching', () => {
|
|||||||
const received = ref<ReceivedMatch[]>([])
|
const received = ref<ReceivedMatch[]>([])
|
||||||
const sentPendingTargetIds = ref<Set<number>>(new Set())
|
const sentPendingTargetIds = ref<Set<number>>(new Set())
|
||||||
const openChatMatchId = ref<number | null>(null)
|
const openChatMatchId = ref<number | null>(null)
|
||||||
|
/** 산책 후 피드백(찰떡지수) 팝업을 띄울 매칭 id — 알림 수신 시 세팅. */
|
||||||
|
const feedbackMatchId = ref<number | null>(null)
|
||||||
|
|
||||||
let socket: ReturnType<typeof createUserNotifications> | null = null
|
let socket: ReturnType<typeof createUserNotifications> | null = null
|
||||||
let myDogId: number | null = null
|
let myDogId: number | null = null
|
||||||
@@ -57,6 +59,9 @@ export const useMatchingStore = defineStore('matching', () => {
|
|||||||
if (n.type === 'ACCEPTED') {
|
if (n.type === 'ACCEPTED') {
|
||||||
openChatMatchId.value = n.matchId // 신청자: 수락됨 → 채팅 열기
|
openChatMatchId.value = n.matchId // 신청자: 수락됨 → 채팅 열기
|
||||||
}
|
}
|
||||||
|
if (n.type === 'FEEDBACK_REQUEST') {
|
||||||
|
feedbackMatchId.value = n.matchId // 2시간 후: 찰떡지수 피드백 팝업
|
||||||
|
}
|
||||||
if (myDogId != null) void refresh(myDogId) // 목록/진행중 동기화
|
if (myDogId != null) void refresh(myDogId) // 목록/진행중 동기화
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +91,7 @@ export const useMatchingStore = defineStore('matching', () => {
|
|||||||
received,
|
received,
|
||||||
sentPendingTargetIds,
|
sentPendingTargetIds,
|
||||||
openChatMatchId,
|
openChatMatchId,
|
||||||
|
feedbackMatchId,
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
refresh,
|
refresh,
|
||||||
@@ -103,6 +109,8 @@ function colorFor(type: MatchNotification['type']): string {
|
|||||||
case 'REJECTED':
|
case 'REJECTED':
|
||||||
case 'EXPIRED':
|
case 'EXPIRED':
|
||||||
return 'grey-8'
|
return 'grey-8'
|
||||||
|
case 'FEEDBACK_REQUEST':
|
||||||
|
return 'teal'
|
||||||
default:
|
default:
|
||||||
return 'primary'
|
return 'primary'
|
||||||
}
|
}
|
||||||
@@ -116,6 +124,8 @@ function iconFor(type: MatchNotification['type']): string {
|
|||||||
return 'celebration'
|
return 'celebration'
|
||||||
case 'EXPIRED':
|
case 'EXPIRED':
|
||||||
return 'schedule'
|
return 'schedule'
|
||||||
|
case 'FEEDBACK_REQUEST':
|
||||||
|
return 'rate_review'
|
||||||
default:
|
default:
|
||||||
return 'heart_broken'
|
return 'heart_broken'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user