Files
dognation_PT/src/stores/matching.ts
T
sb c8a8681a61
CI / build (push) Successful in 27s
feat: 찰떡지수 피드백 UI (우리/동네 궁합도)
- FeedbackDialog: 잘 놀았어요/안 맞았어요 + BAD 사유 선택·추가입력
- 채팅 '산책 완료' 버튼 + 2시간 FEEDBACK_REQUEST 알림 → 피드백 팝업
- AffinityBadges: 동네 찰떡지수(전체), 우리 찰떡지수(매칭 쌍·유료 게이팅)
  스팟 메이트/AI 추천 카드 및 우리 강아지 프로필에 노출

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:55:16 +09:00

132 lines
3.8 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { Notify } from 'quasar'
import { matchesApi, type MatchNotification, type ReceivedMatch } from '@/api/matches'
import { createUserNotifications } from '@/lib/chatSocket'
/**
* 매칭 상태/알림 스토어.
* - 로그인 시 connect(userId, myDogId): 실시간 알림 구독 + 받은/보낸 매칭 로드
* - received: 내 개가 받은 대기중 신청 (수락/거절 대상)
* - sentPendingTargetIds: 내가 신청한 진행중 대상 dogId (버튼 "진행중" 표시)
* - openChatMatchId: 수락된 매칭 → 1:1 채팅 열기 신호
*/
export const useMatchingStore = defineStore('matching', () => {
const received = ref<ReceivedMatch[]>([])
const sentPendingTargetIds = ref<Set<number>>(new Set())
const openChatMatchId = ref<number | null>(null)
/** 산책 후 피드백(찰떡지수) 팝업을 띄울 매칭 id — 알림 수신 시 세팅. */
const feedbackMatchId = ref<number | null>(null)
let socket: ReturnType<typeof createUserNotifications> | null = null
let myDogId: number | null = null
async function refresh(dogId: number): Promise<void> {
myDogId = dogId
try {
const [rec, snt] = await Promise.all([matchesApi.received(dogId), matchesApi.sent(dogId)])
received.value = rec
sentPendingTargetIds.value = new Set(snt.map((m) => m.targetDogId))
} catch {
// 무시 (미로그인/네트워크)
}
}
function connect(userId: number, dogId: number): void {
disconnect()
myDogId = dogId
void refresh(dogId)
socket = createUserNotifications(userId, onNotification)
socket.activate()
}
function disconnect(): void {
socket?.deactivate()
socket = null
received.value = []
sentPendingTargetIds.value = new Set()
}
function onNotification(n: MatchNotification): void {
Notify.create({
message: n.message,
caption: n.title,
color: colorFor(n.type),
icon: iconFor(n.type),
position: 'top',
timeout: 4000,
})
if (n.type === 'ACCEPTED') {
openChatMatchId.value = n.matchId // 신청자: 수락됨 → 채팅 열기
}
if (n.type === 'FEEDBACK_REQUEST') {
feedbackMatchId.value = n.matchId // 2시간 후: 찰떡지수 피드백 팝업
}
if (myDogId != null) void refresh(myDogId) // 목록/진행중 동기화
}
async function accept(id: number): Promise<void> {
await matchesApi.accept(id)
received.value = received.value.filter((m) => m.id !== id)
openChatMatchId.value = id // 수락자도 바로 채팅 열기
}
async function reject(id: number): Promise<void> {
await matchesApi.reject(id)
received.value = received.value.filter((m) => m.id !== id)
}
/** 매칭 신청 직후 낙관적으로 진행중 표시. */
function markSent(targetDogId: number): void {
const next = new Set(sentPendingTargetIds.value)
next.add(targetDogId)
sentPendingTargetIds.value = next
}
function isPending(targetDogId: number): boolean {
return sentPendingTargetIds.value.has(targetDogId)
}
return {
received,
sentPendingTargetIds,
openChatMatchId,
feedbackMatchId,
connect,
disconnect,
refresh,
accept,
reject,
markSent,
isPending,
}
})
function colorFor(type: MatchNotification['type']): string {
switch (type) {
case 'ACCEPTED':
return 'positive'
case 'REJECTED':
case 'EXPIRED':
return 'grey-8'
case 'FEEDBACK_REQUEST':
return 'teal'
default:
return 'primary'
}
}
function iconFor(type: MatchNotification['type']): string {
switch (type) {
case 'NEW_REQUEST':
return 'favorite'
case 'ACCEPTED':
return 'celebration'
case 'EXPIRED':
return 'schedule'
case 'FEEDBACK_REQUEST':
return 'rate_review'
default:
return 'heart_broken'
}
}