- 매칭 버튼: 신청 후 "진행중" 표시, 무료 소진(429) 안내
- 헤더 알림 벨: 받은 매칭 신청 목록 + 수락/거절
- 실시간 알림: STOMP /topic/users/{id} 구독(신청/수락/거절/만료 토스트)
- 수락 시 1:1 매칭 채팅(MatchChatDialog, /topic/matches/{id})
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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)
|
||||
|
||||
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 (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,
|
||||
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'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
}
|
||||
|
||||
function iconFor(type: MatchNotification['type']): string {
|
||||
switch (type) {
|
||||
case 'NEW_REQUEST':
|
||||
return 'favorite'
|
||||
case 'ACCEPTED':
|
||||
return 'celebration'
|
||||
case 'EXPIRED':
|
||||
return 'schedule'
|
||||
default:
|
||||
return 'heart_broken'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user