오류 처리 및 문제 해결

AnySpend을 사용하여 최상의 사용자 경험을 제공하기 위한 오류 처리 및 일반적인 문제 디버깅 방법에 대한 종합적인 가이드입니다.

📊 주문 상태 수명주기

적절한 오류 처리 및 사용자 경험을 위해 주문 상태를 이해하는 것이 중요합니다.

주문 상태 유형

주문 상태 Enum
enum OrderStatus {
  // 초기 상태
  SCANNING_DEPOSIT_TRANSACTION = "scanning_deposit_transaction",
  WAITING_STRIPE_PAYMENT = "waiting_stripe_payment",
  EXPIRED = "expired",

  // 처리 중 상태
  SENDING_TOKEN_FROM_VAULT = "sending_token_from_vault",
  RELAY = "relay",

  // 성공 상태
  EXECUTED = "executed",

  // 실패 상태
  REFUNDING = "refunding",
  REFUNDED = "refunded",
  FAILURE = "failure",
}

상태 설명

상태설명사용자 조치 필요
scanning_deposit_transaction결제 확인 대기 중없음 - 블록체인 확인 대기
waiting_stripe_payment신용카드 결제 처리 중3D Secure를 완료해야 할 수 있음
sending_token_from_vault스왑을 위한 토큰 전송 중없음 - 자동 처리
relay크로스체인 거래 진행 중없음 - 완료 대기
executed거래 성공적으로 완료없음 - 성공!
expired완료 전 주문 만료새 주문 생성
refunding자동 환불 진행 중없음 - 환불 대기
refunded환불 완료지갑에서 환불된 토큰 확인
failure거래 실패오류 세부 정보 검토, 재시도

⚠️ 일반적인 오류 코드

결제 오류

네트워크 오류

계약 오류

🛠️ 오류 처리 패턴

컴포넌트 수준 오류 처리

오류 처리가 포함된 결제 컴포넌트
import { useAnyspendCreateOrder } from "@b3dotfun/sdk/anyspend";

function PaymentComponent() {
  const [error, setError] = useState<string | null>(null);
  const [retryCount, setRetryCount] = useState(0);

  const { createOrder, isCreatingOrder } = useAnyspendCreateOrder({
    onError: error => {
      console.error("결제 실패:", error);

      // 특정 오류 처리
      switch (error.message) {
        case "INSUFFICIENT_BALANCE":
          setError("잔액이 부족합니다. 지갑에 자금을 추가하세요.");
          break;

        case "SLIPPAGE":
          if (retryCount < 3) {
            setError("가격이 불리하게 움직였습니다. 재시도 중...");
            setTimeout(() => {
              setRetryCount(prev => prev + 1);
              retryPayment();
            }, 2000);
          } else {
            setError("가격이 너무 변동적입니다. 나중에 다시 시도하세요.");
          }
          break;

        case "NETWORK_ERROR":
          setError("네트워크 문제. 연결을 확인하고 다시 시도하세요.");
          break;

        case "QUOTE_EXPIRED":
          setError("가격 견적이 만료되었습니다. 새로운 견적을 받는 중...");
          refreshQuote();
          break;

        default:
          setError("결제 실패. 다시 시도하거나 지원팀에 문의하세요.");
      }

      // 모니터링을 위한 오류 추적
      analytics.track("payment_error", {
        error: error.message,
        retryCount,
        timestamp: new Date().toISOString(),
      });
    },

    onSuccess: () => {
      setError(null);
      setRetryCount(0);
    },
  });

  return (
    <div className="payment-component">
      {error && (
        <div className="error-banner">
          <span className="error-icon">⚠️</span>
          <span>{error}</span>
          <button onClick={() => setError(null)}>닫기</button>
        </div>
      )}

      <button onClick={handlePayment} disabled={isCreatingOrder}>
        {isCreatingOrder ? "처리 중..." : "지금 결제"}
      </button>
    </div>
  );
}

주문 상태 모니터링

주문 상태 모니터
import { useAnyspendOrderAndTransactions } from "@b3dotfun/sdk/anyspend";

function OrderStatusMonitor({ orderId }: { orderId: string }) {
  const { orderAndTransactions, getOrderAndTransactionsError } = useAnyspendOrderAndTransactions(orderId);

  if (getOrderAndTransactionsError) {
    return (
      <div className="error-state">
        <h3>주문 상태를 불러올 수 없습니다</h3>
        <p>연결을 확인하고 다시 시도하세요.</p>
        <button onClick={() => window.location.reload()}>재시도</button>
      </div>
    );
  }

  if (!orderAndTransactions) {
    return <div>주문 상태를 불러오는 중...</div>;
  }

  const { order, depositTxs, executeTx, refundTxs } = orderAndTransactions.data;

  const renderStatusMessage = () => {
    switch (order.status) {
      case "scanning_deposit_transaction":
        return (
          <div className="status-pending">
            <div className="spinner" />
            <div>
              <h3>⏳ 결제 확인 대기 중</h3>
              <p>이 작업은 보통 1-2분이 소요됩니다. 이 창을 닫지 마세요.</p>
              {depositTxs.length > 0 && (
                <a
                  href={getExplorerUrl(depositTxs[0].txHash, depositTxs[0].chainId)}
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  결제 거래 보기
                </a>
              )}
            </div>
          </div>
        );

      case "relay":
        return (
          <div className="status-processing">
            <div className="spinner" />
            <div>
              <h3>🔄 크로스체인 거래 처리 중</h3>
              <p>결제가 처리되고 있습니다. 몇 분이 소요될 수 있습니다.</p>
            </div>
          </div>
        );

      case "executed":
        return (
          <div className="status-success">
            <div className="success-icon"></div>
            <div>
              <h3>거래가 성공적으로 완료되었습니다!</h3>
              <p>주문이 처리되었습니다.</p>
              {executeTx && (
                <a href={getExplorerUrl(executeTx.txHash, executeTx.chainId)} target="_blank" rel="noopener noreferrer">
                  거래 보기
                </a>
              )}
            </div>
          </div>
        );

      case "failure":
      case "obtain_failed":
        return (
          <div className="status-error">
            <div className="error-icon"></div>
            <div>
              <h3>거래 실패</h3>
              <p>{order.errorDetails || "주문을 처리하는 동안 오류가 발생했습니다."}</p>
              <div className="error-actions">
                <button onClick={() => createNewOrder()}>다시 시도</button>
                <button onClick={() => contactSupport(orderId)}>지원팀에 문의</button>
              </div>
            </div>
          </div>
        );

      case "refunded":
        return (
          <div className="status-refunded">
            <div className="refund-icon">↩️</div>
            <div>
              <h3>환불 처리됨</h3>
              <p>결제가 자동으로 환불되었습니다.</p>
              {refundTxs.length > 0 && (
                <a
                  href={getExplorerUrl(refundTxs[0].txHash, refundTxs[0].chainId)}
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  환불 거래 보기
                </a>
              )}
            </div>
          </div>
        );

      case "expired":
        return (
          <div className="status-expired">
            <div className="expired-icon"></div>
            <div>
              <h3>주문 만료됨</h3>
              <p>결제가 수신되기 전에 이 주문이 만료되었습니다.</p>
              <button onClick={() => createNewOrder()}>새 주문 생성</button>
            </div>
          </div>
        );

      default:
        return (
          <div className="status-unknown">
            <div className="spinner" />
            <div>
              <h3>처리 중...</h3>
              <p>주문 상태: {order.status}</p>
            </div>
          </div>
        );
    }
  };

  return (
    <div className="order-status-monitor">
      <div className="order-header">
        <h2>주문 #{orderId.slice(0, 8)}</h2>
        <div className="order-meta">
          <span>생성됨: {new Date(order.createdAt).toLocaleString()}</span>
          <span>상태: {order.status}</span>
        </div>
      </div>