[fix] 채팅 진입 시 권한 없는/없는 세션 → '잘못된 접근' 토스트 후 목록 복귀
다른 공급사 소유(또는 없는) session_id 로 /chat 진입 시 일반 에러+재시도 대신 '잘못된 접근입니다.' 토스트를 띄우고 협상 목록(/list)으로 돌려보낸다. 로그인은 유효하므로 로그아웃하지 않는다(인증≠인가 — 로그아웃은 401/403/43x 토큰 오류에서만, 기존 인터셉터 동작 유지). - useChatController: 진입 로드(init/messages) 에러가 NEGO_FORBIDDEN(1300)/ NEGO_NOT_FOUND(1304) 이면 토스트 후 /list 로 replace 이동(ref 가드로 1회). 리다이렉트 동안 스피너 유지(ErrorPage 깜빡임 방지), 그 외 서버/네트워크 에러는 기존 ErrorPage(재시도) 유지. - chat.queries: init/messages 쿼리에 retry 추가 — 1300/1304 는 재시도해도 안 바뀌어 즉시 실패시켜 목록 복귀 지연 제거(그 외 에러는 기존 재시도 유지). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1d4e5816b2
commit
b2d53b133f
@ -1,9 +1,17 @@
|
|||||||
// 채팅 도메인의 조회(useQuery) 훅.
|
// 채팅 도메인의 조회(useQuery) 훅.
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { ErrorCode, isApiError } from '@/apis/types'
|
||||||
import { chatApi } from './chat.api'
|
import { chatApi } from './chat.api'
|
||||||
import { chatKeys } from './chat.keys'
|
import { chatKeys } from './chat.keys'
|
||||||
import { mapInit, mapMessage } from './chat.type'
|
import { mapInit, mapMessage } from './chat.type'
|
||||||
|
|
||||||
|
// 재시도해도 결과가 바뀌지 않는 클라이언트 에러(권한없음/없는 세션) → 즉시 실패시켜 목록 복귀를 지연 없이 처리.
|
||||||
|
const NON_RETRYABLE = new Set<number>([ErrorCode.NEGO_FORBIDDEN, ErrorCode.NEGO_NOT_FOUND])
|
||||||
|
function retryQuery(failureCount: number, error: unknown): boolean {
|
||||||
|
if (isApiError(error) && NON_RETRYABLE.has(error.code)) return false
|
||||||
|
return failureCount < 3
|
||||||
|
}
|
||||||
|
|
||||||
/** 채팅 진입 메타(상품·견적). 마감 시각은 거의 불변이라 오래 캐싱한다. */
|
/** 채팅 진입 메타(상품·견적). 마감 시각은 거의 불변이라 오래 캐싱한다. */
|
||||||
export function useChatInitQuery(sessionId: string) {
|
export function useChatInitQuery(sessionId: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -11,6 +19,7 @@ export function useChatInitQuery(sessionId: string) {
|
|||||||
queryFn: () => chatApi.getInit(sessionId).then(mapInit),
|
queryFn: () => chatApi.getInit(sessionId).then(mapInit),
|
||||||
enabled: !!sessionId,
|
enabled: !!sessionId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
|
retry: retryQuery,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,5 +34,6 @@ export function useChatMessagesQuery(sessionId: string) {
|
|||||||
enabled: !!sessionId,
|
enabled: !!sessionId,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
retry: retryQuery,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
@ -23,6 +23,10 @@ const TERMINAL_CODES = new Set<number>([
|
|||||||
ErrorCode.NEGO_NOT_FOUND,
|
ErrorCode.NEGO_NOT_FOUND,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 진입 로드(init/messages) 시 '잘못된 접근'으로 볼 코드 → 권한 없음 / 없는(또는 남의) 세션.
|
||||||
|
// 토스트 안내 후 목록으로 복귀시킨다. (로그인은 유효하므로 로그아웃하지 않는다 — 인증≠인가)
|
||||||
|
const INVALID_ACCESS_CODES = new Set<number>([ErrorCode.NEGO_FORBIDDEN, ErrorCode.NEGO_NOT_FOUND])
|
||||||
|
|
||||||
// 화면이 보는 대화 상태가 서버와 어긋났을 수 있는 코드 → 서버 기준으로 메시지를 다시 불러와 리싱크.
|
// 화면이 보는 대화 상태가 서버와 어긋났을 수 있는 코드 → 서버 기준으로 메시지를 다시 불러와 리싱크.
|
||||||
const RESYNC_CODES = new Set<number>([
|
const RESYNC_CODES = new Set<number>([
|
||||||
ErrorCode.CHAT_INPUT_MODE_MISMATCH, // 보낸 입력이 직전 봇이 요구한 모드와 불일치
|
ErrorCode.CHAT_INPUT_MODE_MISMATCH, // 보낸 입력이 직전 봇이 요구한 모드와 불일치
|
||||||
@ -64,6 +68,17 @@ export function useChatController(sessionId: string) {
|
|||||||
// staleTime:Infinity 인 메시지 쿼리는 재조회되지 않아 스토어가 빈 채로 남아 채팅이 사라진다.
|
// staleTime:Infinity 인 메시지 쿼리는 재조회되지 않아 스토어가 빈 채로 남아 채팅이 사라진다.
|
||||||
const { mutate: sendMutate } = useChatSendMutation(sessionId)
|
const { mutate: sendMutate } = useChatSendMutation(sessionId)
|
||||||
|
|
||||||
|
// 진입 로드 실패. 권한 없음/없는 세션(잘못된 접근)이면 토스트 후 목록으로 복귀시킨다.
|
||||||
|
const loadError = initQuery.error ?? messagesQuery.error
|
||||||
|
const isInvalidAccess = isApiError(loadError) && INVALID_ACCESS_CODES.has(loadError.code)
|
||||||
|
const redirectedRef = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInvalidAccess || redirectedRef.current) return
|
||||||
|
redirectedRef.current = true
|
||||||
|
toast.error('잘못된 접근입니다.')
|
||||||
|
navigate('/list', { replace: true })
|
||||||
|
}, [isInvalidAccess, navigate])
|
||||||
|
|
||||||
// init 메타 → 스토어
|
// init 메타 → 스토어
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initQuery.data) setInitData(initQuery.data)
|
if (initQuery.data) setInitData(initQuery.data)
|
||||||
@ -130,8 +145,10 @@ export function useChatController(sessionId: string) {
|
|||||||
}, [sessionId, sendMutate, navigate, queryClient])
|
}, [sessionId, sendMutate, navigate, queryClient])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isInitLoading: initQuery.isLoading || messagesQuery.isLoading,
|
// 잘못된 접근이면 목록으로 리다이렉트되는 동안 스피너를 유지(ErrorPage 깜빡임 방지).
|
||||||
initError: initQuery.error ?? messagesQuery.error,
|
isInitLoading: initQuery.isLoading || messagesQuery.isLoading || isInvalidAccess,
|
||||||
|
// 잘못된 접근은 토스트+리다이렉트로 처리하므로 ErrorPage(재시도)로 노출하지 않는다.
|
||||||
|
initError: isInvalidAccess ? null : loadError,
|
||||||
// 로드 실패(서버/네트워크) 시 init·messages 를 함께 재조회한다.
|
// 로드 실패(서버/네트워크) 시 init·messages 를 함께 재조회한다.
|
||||||
refetchInit: () => {
|
refetchInit: () => {
|
||||||
void initQuery.refetch()
|
void initQuery.refetch()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user