feat: 찰떡지수 피드백 UI (우리/동네 궁합도)
CI / build (push) Successful in 27s

- FeedbackDialog: 잘 놀았어요/안 맞았어요 + BAD 사유 선택·추가입력
- 채팅 '산책 완료' 버튼 + 2시간 FEEDBACK_REQUEST 알림 → 피드백 팝업
- AffinityBadges: 동네 찰떡지수(전체), 우리 찰떡지수(매칭 쌍·유료 게이팅)
  스팟 메이트/AI 추천 카드 및 우리 강아지 프로필에 노출

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-11 23:55:16 +09:00
parent eb51201669
commit c8a8681a61
9 changed files with 344 additions and 3 deletions
+167
View File
@@ -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>