c8a8681a61
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>
132 lines
3.8 KiB
TypeScript
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'
|
|
}
|
|
} |