Skip to content

[Feat] XRP 결제 생성 3-step 플로우 구현 및 API 연동#16

Merged
BaeSeong-min merged 24 commits into
developfrom
feat/#14
May 11, 2026
Merged

[Feat] XRP 결제 생성 3-step 플로우 구현 및 API 연동#16
BaeSeong-min merged 24 commits into
developfrom
feat/#14

Conversation

@BaeSeong-min

@BaeSeong-min BaeSeong-min commented May 11, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 요약

  • XRP 에스크로 결제 생성 플로우 3단계 구현 (파트너 선택 → 정보 확인 → 결제 요청)
  • 결제 관련 UI 컴포넌트 구현 및 API 연동
  • 결제 페이지 한/영 i18n 적용

🧾 관련 이슈

🛠️ 주요 변경 사항

  • CreatePayment 페이지 및 /payments/create 라우팅 추가
  • StepProgressBar 컴포넌트 구현
  • Step1 파트너 선택 — PaymentSelectPartner, 페이지네이션, shimmer 스켈레톤
  • Step2 파트너 정보 확인 — WalletConfirmModal, 지갑 주소 기반 파트너 조회 API 연동
  • Step3 결제 요청 폼 — EscrowInfoCard, 마일스톤/이벤트 트리거 추가·삭제
  • WalletAddressCard 컴포넌트 및 useMyProfile 훅 구현
  • 에스크로 결제 생성 API 연동 (POST /escrow-payments)
  • RowLabel 컴포넌트 분리
  • PartnerTableRow 텍스트 truncate 항목 hover 툴팁 추가
  • 결제 Step 전환 슬라이드 애니메이션 추가
  • 결제 페이지 전체 한/영 i18n 적용
  • payment 전용 컴포넌트 디렉토리 분리 (src/components/payment/)

🧠 의도 및 배경

  • XRP 에스크로 기반 결제 생성 기능이 없어 핵심 플로우 구현
  • 3단계 스텝 구조로 사용자가 파트너 선택 → 지갑 주소 확인 → 결제 조건 설정을 순차적으로 진행할 수 있도록 구성
  • 지갑 주소 직접 입력도 가능하도록 fallback 처리

BaeSeong-min and others added 23 commits May 10, 2026 16:04
Co-Authored-By: seongmin <baeseongmin000@gmail.com>
Co-Authored-By: seongmin <baeseongmin000@gmail.com>
@BaeSeong-min BaeSeong-min self-assigned this May 11, 2026
@vercel

vercel Bot commented May 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
k-statra-frontend Ready Ready Preview, Comment May 11, 2026 1:03am

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@BaeSeong-min has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 13 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 762fdc2a-a24a-4ccb-b955-98b8709ef1ff

📥 Commits

Reviewing files that changed from the base of the PR and between 5abf820 and baa0556.

📒 Files selected for processing (1)
  • src/pages/payment/PaymentSelectPartner.jsx
📝 Walkthrough

요약

결제 마일스톤 기반의 다단계 결제 생성 흐름을 구현한다. 파트너 선택, 지갑 주소 확인, 결제 정보 입력, 완료 페이지로 구성되며, 새로운 Escrow Payments API, React Query 훅, 재사용 가능한 UI 컴포넌트를 추가한다.

변경 사항

결제 생성 흐름 (Payment Flow)

계층 / 파일 요약
API 계약
src/apis/endpoints.ts, src/apis/index.ts, src/apis/modules/escrowPayments.ts
Escrow 결제 엔드포인트(/escrow-payments) 정의, EscrowItemDto/CreateEscrowPaymentDto DTO, escrowPaymentsApi로 생성/조회/승인/결제 및 지갑 조회 메서드 제공.
데이터 페칭 훅
src/hooks/myBusiness/useMyProfile.js, src/hooks/payments/useCreateEscrowPayment.js, src/hooks/payments/useMyPartners.js, src/hooks/payments/useWalletUser.js
프로필 조회, 파트너 목록(페이지네이션), 지갑 사용자 조회, 결제 생성 변경을 처리하는 React Query 훅.
기본 UI 컴포넌트
src/components/LabeledInput.jsx, src/components/Tooltip.jsx, src/components/payment/RowLabel.jsx, src/components/payment/PartnerTableSkeleton.jsx, src/components/payment/WalletAddressField.jsx
입력 필드 재이름 지정, 호버 툴팁, 필드 레이블, 스켈레톤 로더, 지갑 주소 필드 컴포넌트.
파트너 표 개선
src/components/PartnerTable.jsx, src/components/PartnerTableRow.jsx
파트너 행에서 아바타 제거, 이름/국가/산업/프로필을 Tooltip으로 감싸 텍스트 오버플로우 처리.
결제 입력 폼
src/components/payment/EscrowInfoCard.jsx, src/components/payment/WalletAddressCard.jsx, src/components/payment/WalletConfirmModal.jsx
마일스톤 편집 카드, 지갑 주소 표시, 지갑 소유자 확인 애니메이션 모달.
다단계 페이지
src/components/payment/StepProgressBar.jsx, src/pages/payment/PaymentSelectPartner.jsx, src/pages/payment/PaymentVerifyInfo.jsx, src/pages/payment/PaymentRequestForm.jsx, src/pages/payment/PaymentComplete.jsx, src/pages/payment/CreatePayment.jsx
진행률 표시기, 파트너 선택/정보 확인/요청 폼/완료 페이지, 다단계 상태 및 애니메이션 관리.
라우팅 및 네비게이션
src/router.jsx, src/components/Header.jsx
/payments/create 경로 등록, 헤더 네비게이션 링크 업데이트.
인증 페이지 업데이트
src/pages/LoginPage.jsx, src/pages/RegisterPage.jsx
AuthInputLabeledInput으로 교체.
국제화
src/lib/i18n/dict.js
결제 단계, 지갑 확인, 마일스톤 레이블에 대한 영문/한국어 문자열 추가(46개 키).

Sequence 다이어그램

sequenceDiagram
    participant User
    participant PaymentFlow as CreatePayment<br/>(다단계 진행)
    participant PartnerAPI as useMyPartners
    participant WalletAPI as useWalletUser
    participant EscrowAPI as useCreateEscrowPayment
    
    User->>PaymentFlow: 결제 생성 시작
    activate PaymentFlow
    
    PaymentFlow->>PartnerAPI: 파트너 목록 조회
    activate PartnerAPI
    PartnerAPI-->>PaymentFlow: 파트너 배열(페이지네이션)
    deactivate PartnerAPI
    
    User->>PaymentFlow: 파트너 선택 → Next
    
    PaymentFlow->>WalletAPI: 지갑 사용자 확인
    activate WalletAPI
    WalletAPI-->>PaymentFlow: 지갑 데이터
    deactivate WalletAPI
    
    User->>PaymentFlow: 정보 확인 → 마일스톤 입력 → Next
    
    PaymentFlow->>EscrowAPI: 결제 생성 요청(escrows 배열)
    activate EscrowAPI
    EscrowAPI-->>PaymentFlow: 성공 또는 에러
    deactivate EscrowAPI
    
    alt 성공
        PaymentFlow->>PaymentFlow: 완료 페이지 표시
        User->>PaymentFlow: 확인
    else 실패
        PaymentFlow->>User: 에러 토스트 메시지
    end
    
    deactivate PaymentFlow
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes


관련 PR

  • K-Statra/frontend#8: API 계층(axios 기반 HTTP 클라이언트)이 이 PR의 escrowPaymentsApi 구현의 기초를 제공한다.
  • K-Statra/frontend#13: 동일한 ENDPOINTS 및 API 내보내기 패턴(myBusinessApi 추가)을 확장하므로 API 설계에서 관련이 있다.

🐰 결제 마일스톤의 마법

여러 단계를 춤을 추듯 나아가며,
지갑은 확인되고 파트너도 정해진다,
에스크로우의 신비로운 약속과 함께,
완료의 숨결이 전해진다. ✨💳

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (4 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목이 변경 내용과 불일치합니다. 제목은 'XRP 결제 생성 3-step 플로우 구현 및 API 연동'이지만, 실제 변경 사항은 MyBusiness 페이지 구현, 파트너 선택 관련 UI 변경, 그리고 결제 플로우 구현을 포함하고 있습니다. PR 제목을 '[Feat] MyBusiness 페이지 구현 및 XRP 결제 3-step 플로우 API 연동' 또는 유사한 형태로 변경하여 실제 변경 범위를 정확히 반영하세요.
Linked Issues check ⚠️ Warning PR이 이슈 #15의 요구사항을 부분적으로만 충족합니다. 결제 플로우 구현(결제 API, 지갑 확인)은 완료되었으나, MyBusiness 페이지의 주요 기능(프로필 수정, 파트너 삭제)이 구현되지 않았습니다. 이슈 #15의 모든 요구사항(프로필 수정 API 연동, 파트너 삭제 기능)을 구현하거나, 부분적 구현 사항을 명확히 기록하세요.
Out of Scope Changes check ⚠️ Warning Header.jsx의 네비게이션 링크 변경(/payments → /payments/create)과 LabeledInput 컴포넌트 이름 변경 등이 이슈 #15 범위를 벗어난 추가 변경사항입니다. 이슈 범위 외의 변경사항(Header 링크 수정, 컴포넌트 이름 변경)을 별도 이슈로 분리하거나, 이 PR에 포함된 모든 변경의 정당성을 설명하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션을 포함하고 있으며, 변경 사항이 명확하고 충분히 상세합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#14

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (10)
src/components/payment/WalletAddressField.jsx (1)

22-31: ⚡ Quick win

로딩 상태에 접근성 시맨틱을 추가해 주세요.

Line 30"..." 표시는 시각적으로는 충분하지만, 스크린리더에는 상태 변화가 전달되지 않습니다. aria-busy/aria-live(또는 role="status")를 추가해 로딩 상태를 보조기기가 인식할 수 있게 해주세요.

예시 변경안
-      <div
+      <div
+        aria-busy={isLoading}
+        aria-live="polite"
+        role="status"
         style={{
           background: "#f4f4f4",
           borderRadius: 12,
           height: 48,
           display: "flex",
           alignItems: "center",
           padding: "12px 20px",
         }}
       >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/WalletAddressField.jsx` around lines 22 - 31, The span
rendering the wallet value in WalletAddressField.jsx currently shows "..." when
isLoading but lacks accessibility semantics; update the span (the element that
contains {isLoading ? "..." : value || "-"}) to expose loading state to
assistive tech by adding appropriate attributes such as role="status" or
aria-live="polite" (and/or aria-busy={isLoading}) so screen readers announce the
change; ensure the attributes are bound to the isLoading prop and kept on the
same span used by the WalletAddressField component.
src/components/payment/StepProgressBar.jsx (1)

5-9: 💤 Low value

STEPS 배열 메모이제이션을 고려해보세요.

STEPS 배열이 매 렌더링마다 새로 생성되고 있습니다. t 함수가 안정적이라면 큰 문제는 아니지만, useMemo로 감싸면 불필요한 재생성을 방지할 수 있습니다.

♻️ 제안하는 개선 방안
+import { useMemo } from "react";
 import { useI18n } from "@/lib/i18n/I18nProvider";

 export default function StepProgressBar({ currentStep }) {
   const { t } = useI18n();
-  const STEPS = [
+  const STEPS = useMemo(() => [
     { label: t("payment_step_verify_info") },
     { label: t("payment_step_request_payment") },
     { label: t("payment_step_complete") },
-  ];
+  ], [t]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/StepProgressBar.jsx` around lines 5 - 9, STEPS is
recreated on every render causing unnecessary allocations; wrap its creation
with React's useMemo so the array is memoized around the translation function
(t) — e.g., memoize the array referenced as STEPS inside the StepProgressBar
component using useMemo(() => [...], [t]) so STEPS only rebuilds when t changes.
src/components/payment/WalletConfirmModal.jsx (1)

41-42: 💤 Low value

모달의 조건부 렌더링을 고려해보세요.

현재 AnimatePresence가 항상 렌더링되고 있습니다. address가 없을 때는 모달 자체를 렌더링하지 않는 것이 더 나은 패턴일 수 있습니다.

♻️ 제안하는 개선 방안
+  if (!address) return null;
+
   return (
     <AnimatePresence>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/WalletConfirmModal.jsx` around lines 41 - 42, The
component WalletConfirmModal always renders <AnimatePresence>, causing the modal
wrapper to exist even when no address is present; update the component to
conditionally render the modal by checking the `address` (prop or state) before
returning the AnimatePresence block — either return null early from
WalletConfirmModal when `address` is falsy or wrap <AnimatePresence> and its
children in a conditional (e.g., address && <AnimatePresence>...) so the modal
and its animations are not mounted when `address` is missing.
src/components/payment/EscrowInfoCard.jsx (1)

141-147: ⚡ Quick win

금액 입력 필드에 숫자 유효성 검사를 추가하는 것을 고려해보세요.

xrplAmount 입력 필드가 문자열을 허용하므로, 사용자가 숫자가 아닌 값을 입력할 수 있습니다. 입력 시 숫자만 허용하거나, 최소한 양수 검증을 추가하면 사용자 경험이 개선됩니다.

♻️ 제안하는 개선 방안
 <input
   style={{ ...inputStyle, width: 260 }}
   placeholder={t("payment_amount_placeholder")}
+  type="number"
+  min="0"
+  step="0.01"
   value={milestone.xrplAmount}
   onChange={(e) => update("xrplAmount", e.target.value)}
 />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/EscrowInfoCard.jsx` around lines 141 - 147, The
xrplAmount input currently binds milestone.xrplAmount and calls
update("xrplAmount", ...) without validation; change the input to enforce
numeric/positive values by using a numeric input and sanitizing/parsing in the
onChange handler: switch the input to type="number" (or add a numeric pattern),
trim non-numeric characters and parseFloat the value, only call update when the
parsed value is a finite number and >= 0, and optionally show/handle empty
string; update the onChange logic that references milestone.xrplAmount and the
update function to perform this validation and normalization before updating
state.
src/apis/modules/escrowPayments.ts (1)

4-17: 💤 Low value

퍼블릭 인터페이스에 JSDoc 주석 추가를 고려해보세요.

EscrowItemDtoCreateEscrowPaymentDto는 외부에서 사용되는 퍼블릭 API이므로, 각 필드의 목적과 제약사항을 설명하는 JSDoc 주석을 추가하면 개발자 경험이 향상됩니다. 특히 amountXrp의 범위 제약이나 requiredEventTypes에 들어갈 수 있는 값 등을 문서화하면 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/apis/modules/escrowPayments.ts` around lines 4 - 17, Add JSDoc comments
to the public interfaces EscrowItemDto and CreateEscrowPaymentDto and to each
field (label, amountXrp, order, requiredEventTypes, buyerId,
sellerWalletAddress, memo, currency, escrows) describing purpose and
constraints; specifically document amountXrp as an XRP amount with its valid
range/precision, enumerate allowed values for currency ("XRP" | "RLUSD") and
provide examples/allowed tokens for requiredEventTypes, and note optionality for
memo and currency and that escrows is an array of EscrowItemDto—use standard
JSDoc blocks (/** ... */) directly above the interface and property declarations
so IDEs and generated docs pick them up.
src/hooks/payments/useWalletUser.js (1)

4-11: ⚡ Quick win

지갑 조회 결과에 대한 캐싱 전략을 고려해보세요.

지갑 주소는 변경되지 않는 정적 데이터이므로, staleTime을 설정하면 동일한 지갑 주소를 여러 번 조회할 때 불필요한 네트워크 요청을 줄일 수 있습니다. 예를 들어:

♻️ 제안하는 개선 방안
 export function useWalletUser(address) {
   return useQuery({
     queryKey: ["wallet-user", address],
     queryFn: () => escrowPaymentsApi.findUserByWallet(address),
     enabled: !!address,
     retry: false,
+    staleTime: 5 * 60 * 1000, // 5분간 캐시 유지
   });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/payments/useWalletUser.js` around lines 4 - 11, The hook
useWalletUser currently calls useQuery without a caching duration; add a
staleTime to its useQuery options so wallet lookups (queryKey ["wallet-user",
address]) are considered fresh for a reasonable period (e.g., several minutes or
Infinity for effectively static addresses) to avoid repeated network requests to
escrowPaymentsApi.findUserByWallet(address); update the options object in
useWalletUser to include staleTime and optionally cacheTime if desired.
src/pages/payment/CreatePayment.jsx (1)

92-103: 💤 Low value

불필요한 조건부 렌더링을 단순화하세요.

step >= 1 조건은 step의 초기값이 1이고 최소값이 1로 유지되므로 항상 참입니다. 미래의 변경사항을 대비한 방어적 코딩일 수 있지만, 현재 로직상으로는 불필요한 조건입니다.

♻️ 간소화 옵션
-     {step >= 1 && (
-       <div
-         style={{
-           background: "#f4f7fc",
-           display: "flex",
-           justifyContent: "center",
-           padding: "40px 80px 80px",
-         }}
-       >
-         <StepProgressBar currentStep={step} />
-       </div>
-     )}
+     <div
+       style={{
+         background: "#f4f7fc",
+         display: "flex",
+         justifyContent: "center",
+         padding: "40px 80px 80px",
+       }}
+     >
+       <StepProgressBar currentStep={step} />
+     </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/payment/CreatePayment.jsx` around lines 92 - 103, The conditional
render using "step >= 1" is unnecessary because step's initial and minimum value
is 1; remove the conditional and always render the StepProgressBar block
instead: replace the "{step >= 1 && ( ... )}" wrapper with the inner <div> that
contains <StepProgressBar currentStep={step} /> so the progress bar is always
rendered, keeping the same styles and props; reference the step variable and
StepProgressBar component to locate and modify the JSX.
src/hooks/payments/useCreateEscrowPayment.js (1)

9-9: ⚡ Quick win

i18n을 사용하여 에러 메시지를 다국어화하세요.

에러 메시지가 하드코딩되어 있어 다른 언어 사용자에게 적절한 메시지를 제공할 수 없습니다. 앱의 다른 부분과 일관성을 유지하기 위해 useI18n 훅을 사용하여 메시지를 번역 키로 교체하는 것을 권장합니다.

참고: src/pages/payment/PaymentRequestForm.jsx의 66, 71, 73, 75번 라인에도 동일한 패턴이 있습니다.

♻️ 제안된 수정
+import { useI18n } from "@/lib/i18n/I18nProvider";
+
 export function useCreateEscrowPayment() {
+  const { t } = useI18n();
   return useMutation({
     mutationFn: (data) => escrowPaymentsApi.create(data),
     onError: () => {
-      toast.error("결제 요청에 실패했습니다.");
+      toast.error(t("payment_create_error"));
     },
   });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/payments/useCreateEscrowPayment.js` at line 9, Replace the
hardcoded Korean toast error in useCreateEscrowPayment (the toast.error call)
with a localized message obtained from the i18n hook: import and call useI18n
(or the project's translation hook) to get the t function, then pass a
translation key (e.g. t('payment.createEscrowFailed')) to toast.error; apply the
same change for the identical hardcoded messages in PaymentRequestForm.jsx so
all failures use consistent translation keys and the useI18n hook.
src/pages/payment/PaymentRequestForm.jsx (2)

33-44: 💤 Low value

더 안정적인 ID 생성 방식을 고려하세요.

Date.now()를 마일스톤 ID로 사용하면 사용자가 매우 빠르게 "Add Milestone" 버튼을 연속 클릭할 경우 동일한 밀리초 내에 중복 ID가 생성될 수 있습니다. 실제로는 드물지만, React의 키 충돌 경고나 예상치 못한 렌더링 동작을 초래할 수 있습니다.

♻️ 개선 방안

카운터 기반 ID 또는 UUID 라이브러리 사용:

+let milestoneIdCounter = 1;
+
 const handleAddMilestone = () => {
   onMilestonesChange([
     ...milestones,
     {
-      id: Date.now(),
+      id: milestoneIdCounter++,
       label: "",
       xrplAmount: "",
       currency: "XRP",
       memo: "",
       triggers: [""],
     },
   ]);
 };

또는 더 견고한 방법으로 crypto.randomUUID() 사용.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/payment/PaymentRequestForm.jsx` around lines 33 - 44, The current
handleAddMilestone uses Date.now() for the new milestone id which can collide on
rapid clicks; update handleAddMilestone to generate a more robust unique id
(e.g., use crypto.randomUUID() when available, falling back to a module-level
incremental counter) and pass that id into the object passed to
onMilestonesChange; locate handleAddMilestone, milestones and onMilestonesChange
in PaymentRequestForm.jsx and replace the Date.now() usage with the new
UUID/counter generation approach so each milestone id is reliably unique.

50-55: ⚡ Quick win

XRP 금액 유효성 검사를 추가하세요.

parseFloat(m.xrplAmount) || 0은 잘못된 입력에 대해 0으로 폴백하지만, 0 XRP 금액의 마일스톤이 API에서 거부되거나 비즈니스 로직상 유효하지 않을 수 있습니다. 제출 전에 모든 마일스톤의 xrplAmount가 유효한 양수인지 검증하여 더 명확한 오류 메시지를 사용자에게 제공하는 것을 권장합니다.

🛡️ 제안된 유효성 검사
 const handleSubmit = async () => {
   const buyerId = companyId;
+  
+  // Validate all milestones have valid amounts
+  const invalidMilestone = milestones.find(m => {
+    const amount = parseFloat(m.xrplAmount);
+    return isNaN(amount) || amount <= 0;
+  });
+  
+  if (invalidMilestone) {
+    toast.error(t("payment_invalid_amount"));
+    return;
+  }

   const escrows = milestones.map((m, index) => ({
     label: m.label,
-    amountXrp: parseFloat(m.xrplAmount) || 0,
+    amountXrp: parseFloat(m.xrplAmount),
     order: index,
     requiredEventTypes: m.triggers.filter((t) => t.trim() !== ""),
   }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/payment/PaymentRequestForm.jsx` around lines 50 - 55, The escrows
mapping currently sets amountXrp via parseFloat(m.xrplAmount) || 0 which
silently converts invalid or empty inputs to 0; add explicit validation before
submission to ensure every milestone in milestones has a numeric xrplAmount > 0
and surface a clear error if not. Locate the escrows creation and the form
submit handler (e.g., where escrows or milestones are used) and replace the
silent fallback with a validation step that checks Number.isFinite(parsed) &&
parsed > 0 for each m.xrplAmount, collect invalid indices/labels, prevent submit
if any fail, and show a user-facing error message mentioning the offending
milestone(s) so the user can correct them. Ensure the validation runs
client-side prior to building/sending the API payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/PartnerTableRow.jsx`:
- Around line 81-97: In PartnerTableRow, avoid rendering an <a> when websiteUrl
is empty: add a conditional around the current anchor (referencing websiteUrl
and name) so that when websiteUrl is falsy you render a non-clickable element
(e.g., a <span> or <div>) with the same styling and text content (name) but
without href/target/rel; keep the visual styles (fontSize, color,
textDecoration, overflow, textOverflow, whiteSpace, cursor should be adjusted
from "pointer" to default) so it no longer appears as a link.

In `@src/components/payment/WalletAddressCard.jsx`:
- Around line 8-34: The profile error state from useMyProfile is not handled
causing buyerWalletAddress to fall back to an indistinguishable empty string;
update the component to destructure error (or isError) from useMyProfile and
change how buyerWalletAddress is derived (e.g., const { data, isLoading, error }
= useMyProfile(); const buyerWalletAddress = error ? null :
data?.wallet?.address || ""); then pass the error/isLoading info to
WalletAddressField (or render an explicit error placeholder when error is
truthy) so WalletAddressField receives a clear signal (e.g., value null +
isError flag) to display an error state instead of the generic "-" placeholder.

In `@src/pages/payment/PaymentSelectPartner.jsx`:
- Around line 93-107: The pagination icon-only buttons in
PaymentSelectPartner.jsx lack accessible labels; update the left and right
pagination <button> elements (the one containing <ChevronLeft /> and the one
containing <ChevronRight /> around lines with page state handling) to include
descriptive aria-label attributes such as aria-label="Previous page" and
aria-label="Next page" respectively (also apply the same change to the other
pagination button pair noted at 129-143) so screen readers can convey their
purpose.
- Around line 18-81: The component hides API errors after loading because it
only checks isFetching and renders PartnerTable/PartnerTableSkeleton; update the
UI to handle and show the error returned by useMyPartners. Extract the error
(and refetch or retry function) from useMyPartners (e.g., const { data,
isFetching, error, refetch } = useMyPartners(page)) and when error is truthy
render a clear error state (message + a "Retry" button) instead of PartnerTable
or skeleton; wire the retry button to call refetch (or the hook's retry
function) so users can retry fetching partners. Ensure this logic is used where
PartnerTable/PartnerTableSkeleton are chosen and keep existing
handleToggleOne/onSelect behavior unchanged.

In `@src/pages/payment/PaymentVerifyInfo.jsx`:
- Line 106: Validation uses walletAddress.trim() but the value passed to onNext
is untrimmed; update the handler in PaymentVerifyInfo.jsx to compute a single
trimmed value (e.g., const trimmedAddress = walletAddress.trim()) use that for
the existing validation and pass trimmedAddress to onNext(walletAddress) call so
the next step receives the whitespace-trimmed address (reference symbols:
walletAddress, onNext, PaymentVerifyInfo.jsx).

---

Nitpick comments:
In `@src/apis/modules/escrowPayments.ts`:
- Around line 4-17: Add JSDoc comments to the public interfaces EscrowItemDto
and CreateEscrowPaymentDto and to each field (label, amountXrp, order,
requiredEventTypes, buyerId, sellerWalletAddress, memo, currency, escrows)
describing purpose and constraints; specifically document amountXrp as an XRP
amount with its valid range/precision, enumerate allowed values for currency
("XRP" | "RLUSD") and provide examples/allowed tokens for requiredEventTypes,
and note optionality for memo and currency and that escrows is an array of
EscrowItemDto—use standard JSDoc blocks (/** ... */) directly above the
interface and property declarations so IDEs and generated docs pick them up.

In `@src/components/payment/EscrowInfoCard.jsx`:
- Around line 141-147: The xrplAmount input currently binds milestone.xrplAmount
and calls update("xrplAmount", ...) without validation; change the input to
enforce numeric/positive values by using a numeric input and sanitizing/parsing
in the onChange handler: switch the input to type="number" (or add a numeric
pattern), trim non-numeric characters and parseFloat the value, only call update
when the parsed value is a finite number and >= 0, and optionally show/handle
empty string; update the onChange logic that references milestone.xrplAmount and
the update function to perform this validation and normalization before updating
state.

In `@src/components/payment/StepProgressBar.jsx`:
- Around line 5-9: STEPS is recreated on every render causing unnecessary
allocations; wrap its creation with React's useMemo so the array is memoized
around the translation function (t) — e.g., memoize the array referenced as
STEPS inside the StepProgressBar component using useMemo(() => [...], [t]) so
STEPS only rebuilds when t changes.

In `@src/components/payment/WalletAddressField.jsx`:
- Around line 22-31: The span rendering the wallet value in
WalletAddressField.jsx currently shows "..." when isLoading but lacks
accessibility semantics; update the span (the element that contains {isLoading ?
"..." : value || "-"}) to expose loading state to assistive tech by adding
appropriate attributes such as role="status" or aria-live="polite" (and/or
aria-busy={isLoading}) so screen readers announce the change; ensure the
attributes are bound to the isLoading prop and kept on the same span used by the
WalletAddressField component.

In `@src/components/payment/WalletConfirmModal.jsx`:
- Around line 41-42: The component WalletConfirmModal always renders
<AnimatePresence>, causing the modal wrapper to exist even when no address is
present; update the component to conditionally render the modal by checking the
`address` (prop or state) before returning the AnimatePresence block — either
return null early from WalletConfirmModal when `address` is falsy or wrap
<AnimatePresence> and its children in a conditional (e.g., address &&
<AnimatePresence>...) so the modal and its animations are not mounted when
`address` is missing.

In `@src/hooks/payments/useCreateEscrowPayment.js`:
- Line 9: Replace the hardcoded Korean toast error in useCreateEscrowPayment
(the toast.error call) with a localized message obtained from the i18n hook:
import and call useI18n (or the project's translation hook) to get the t
function, then pass a translation key (e.g. t('payment.createEscrowFailed')) to
toast.error; apply the same change for the identical hardcoded messages in
PaymentRequestForm.jsx so all failures use consistent translation keys and the
useI18n hook.

In `@src/hooks/payments/useWalletUser.js`:
- Around line 4-11: The hook useWalletUser currently calls useQuery without a
caching duration; add a staleTime to its useQuery options so wallet lookups
(queryKey ["wallet-user", address]) are considered fresh for a reasonable period
(e.g., several minutes or Infinity for effectively static addresses) to avoid
repeated network requests to escrowPaymentsApi.findUserByWallet(address); update
the options object in useWalletUser to include staleTime and optionally
cacheTime if desired.

In `@src/pages/payment/CreatePayment.jsx`:
- Around line 92-103: The conditional render using "step >= 1" is unnecessary
because step's initial and minimum value is 1; remove the conditional and always
render the StepProgressBar block instead: replace the "{step >= 1 && ( ... )}"
wrapper with the inner <div> that contains <StepProgressBar currentStep={step}
/> so the progress bar is always rendered, keeping the same styles and props;
reference the step variable and StepProgressBar component to locate and modify
the JSX.

In `@src/pages/payment/PaymentRequestForm.jsx`:
- Around line 33-44: The current handleAddMilestone uses Date.now() for the new
milestone id which can collide on rapid clicks; update handleAddMilestone to
generate a more robust unique id (e.g., use crypto.randomUUID() when available,
falling back to a module-level incremental counter) and pass that id into the
object passed to onMilestonesChange; locate handleAddMilestone, milestones and
onMilestonesChange in PaymentRequestForm.jsx and replace the Date.now() usage
with the new UUID/counter generation approach so each milestone id is reliably
unique.
- Around line 50-55: The escrows mapping currently sets amountXrp via
parseFloat(m.xrplAmount) || 0 which silently converts invalid or empty inputs to
0; add explicit validation before submission to ensure every milestone in
milestones has a numeric xrplAmount > 0 and surface a clear error if not. Locate
the escrows creation and the form submit handler (e.g., where escrows or
milestones are used) and replace the silent fallback with a validation step that
checks Number.isFinite(parsed) && parsed > 0 for each m.xrplAmount, collect
invalid indices/labels, prevent submit if any fail, and show a user-facing error
message mentioning the offending milestone(s) so the user can correct them.
Ensure the validation runs client-side prior to building/sending the API
payload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bb2dc332-1b3c-4ddc-97df-d4b83ed285b4

📥 Commits

Reviewing files that changed from the base of the PR and between 726d21e and 5abf820.

📒 Files selected for processing (28)
  • src/apis/endpoints.ts
  • src/apis/index.ts
  • src/apis/modules/escrowPayments.ts
  • src/components/Header.jsx
  • src/components/LabeledInput.jsx
  • src/components/PartnerTable.jsx
  • src/components/PartnerTableRow.jsx
  • src/components/Tooltip.jsx
  • src/components/payment/EscrowInfoCard.jsx
  • src/components/payment/PartnerTableSkeleton.jsx
  • src/components/payment/RowLabel.jsx
  • src/components/payment/StepProgressBar.jsx
  • src/components/payment/WalletAddressCard.jsx
  • src/components/payment/WalletAddressField.jsx
  • src/components/payment/WalletConfirmModal.jsx
  • src/hooks/myBusiness/useMyProfile.js
  • src/hooks/payments/useCreateEscrowPayment.js
  • src/hooks/payments/useMyPartners.js
  • src/hooks/payments/useWalletUser.js
  • src/lib/i18n/dict.js
  • src/pages/LoginPage.jsx
  • src/pages/RegisterPage.jsx
  • src/pages/payment/CreatePayment.jsx
  • src/pages/payment/PaymentComplete.jsx
  • src/pages/payment/PaymentRequestForm.jsx
  • src/pages/payment/PaymentSelectPartner.jsx
  • src/pages/payment/PaymentVerifyInfo.jsx
  • src/router.jsx
💤 Files with no reviewable changes (1)
  • src/components/PartnerTable.jsx

Comment on lines +81 to +97
<a
href={websiteUrl}
target="_blank"
rel="noopener noreferrer"
style={{
width: 24,
height: 24,
borderRadius: "50%",
flexShrink: 0,
objectFit: "cover",
fontSize: 12,
color: "#080616",
textDecoration: "underline",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
cursor: "pointer",
display: "block",
}}
/>
) : (
<div
style={{
width: 24,
height: 24,
borderRadius: "50%",
background: "#edf1f4",
flexShrink: 0,
}}
/>
)}
<a
href={websiteUrl}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: 12,
color: "#080616",
textDecoration: "underline",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
cursor: "pointer",
}}
>
{name}
</a>
>
{name}
</a>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

웹사이트 URL이 없을 때도 링크처럼 보여 오해를 유발합니다.

websiteUrl이 비어 있으면 <a> 대신 일반 텍스트로 렌더링하는 분기가 필요합니다.

수정 예시
-          <Tooltip content={name}>
-            <a
-              href={websiteUrl}
-              target="_blank"
-              rel="noopener noreferrer"
-              style={{
-                fontSize: 12,
-                color: "#080616",
-                textDecoration: "underline",
-                overflow: "hidden",
-                textOverflow: "ellipsis",
-                whiteSpace: "nowrap",
-                cursor: "pointer",
-                display: "block",
-              }}
-            >
-              {name}
-            </a>
-          </Tooltip>
+          <Tooltip content={name}>
+            {websiteUrl ? (
+              <a
+                href={websiteUrl}
+                target="_blank"
+                rel="noopener noreferrer"
+                style={{
+                  fontSize: 12,
+                  color: "#080616",
+                  textDecoration: "underline",
+                  overflow: "hidden",
+                  textOverflow: "ellipsis",
+                  whiteSpace: "nowrap",
+                  cursor: "pointer",
+                  display: "block",
+                }}
+              >
+                {name}
+              </a>
+            ) : (
+              <span
+                style={{
+                  fontSize: 12,
+                  color: "#080616",
+                  overflow: "hidden",
+                  textOverflow: "ellipsis",
+                  whiteSpace: "nowrap",
+                  display: "block",
+                }}
+              >
+                {name}
+              </span>
+            )}
+          </Tooltip>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PartnerTableRow.jsx` around lines 81 - 97, In PartnerTableRow,
avoid rendering an <a> when websiteUrl is empty: add a conditional around the
current anchor (referencing websiteUrl and name) so that when websiteUrl is
falsy you render a non-clickable element (e.g., a <span> or <div>) with the same
styling and text content (name) but without href/target/rel; keep the visual
styles (fontSize, color, textDecoration, overflow, textOverflow, whiteSpace,
cursor should be adjusted from "pointer" to default) so it no longer appears as
a link.

Comment on lines +8 to +34
const { data, isLoading } = useMyProfile();

const buyerWalletAddress = data?.wallet?.address || "";

return (
<div
style={{
borderBottom: "1px dashed #a2a0a0",
padding: "32px 20px",
display: "flex",
flexDirection: "column",
gap: 28,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<CreditCard size={24} color="#080616" strokeWidth={1.5} />
<span style={{ fontSize: 16, fontWeight: 500, color: "#080616" }}>
{t("payment_xrp_info_title")}
</span>
</div>

<div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
<WalletAddressField
label={t("payment_buyer_wallet_label")}
value={buyerWalletAddress}
isLoading={isLoading}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

프로필 조회 실패 상태가 숨겨지고 있습니다.

Line 8~Line 10에서 에러 상태를 분기하지 않아, 조회 실패 시에도 구매자 주소가 단순 "-"로 표시됩니다. 빈 값과 장애 상황이 구분되지 않아 결제 UX를 혼동시킬 수 있습니다.

예시 변경안
-  const { data, isLoading } = useMyProfile();
+  const { data, isLoading, isError } = useMyProfile();

   const buyerWalletAddress = data?.wallet?.address || "";
@@
         <WalletAddressField
           label={t("payment_buyer_wallet_label")}
-          value={buyerWalletAddress}
-          isLoading={isLoading}
+          value={isError ? t("payment_wallet_address_load_failed") : buyerWalletAddress}
+          isLoading={isLoading && !isError}
         />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data, isLoading } = useMyProfile();
const buyerWalletAddress = data?.wallet?.address || "";
return (
<div
style={{
borderBottom: "1px dashed #a2a0a0",
padding: "32px 20px",
display: "flex",
flexDirection: "column",
gap: 28,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<CreditCard size={24} color="#080616" strokeWidth={1.5} />
<span style={{ fontSize: 16, fontWeight: 500, color: "#080616" }}>
{t("payment_xrp_info_title")}
</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
<WalletAddressField
label={t("payment_buyer_wallet_label")}
value={buyerWalletAddress}
isLoading={isLoading}
/>
const { data, isLoading, isError } = useMyProfile();
const buyerWalletAddress = data?.wallet?.address || "";
return (
<div
style={{
borderBottom: "1px dashed `#a2a0a0`",
padding: "32px 20px",
display: "flex",
flexDirection: "column",
gap: 28,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<CreditCard size={24} color="#080616" strokeWidth={1.5} />
<span style={{ fontSize: 16, fontWeight: 500, color: "#080616" }}>
{t("payment_xrp_info_title")}
</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
<WalletAddressField
label={t("payment_buyer_wallet_label")}
value={isError ? t("payment_wallet_address_load_failed") : buyerWalletAddress}
isLoading={isLoading && !isError}
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/WalletAddressCard.jsx` around lines 8 - 34, The
profile error state from useMyProfile is not handled causing buyerWalletAddress
to fall back to an indistinguishable empty string; update the component to
destructure error (or isError) from useMyProfile and change how
buyerWalletAddress is derived (e.g., const { data, isLoading, error } =
useMyProfile(); const buyerWalletAddress = error ? null : data?.wallet?.address
|| ""); then pass the error/isLoading info to WalletAddressField (or render an
explicit error placeholder when error is truthy) so WalletAddressField receives
a clear signal (e.g., value null + isError flag) to display an error state
instead of the generic "-" placeholder.

Comment thread src/pages/payment/PaymentSelectPartner.jsx Outdated
Comment on lines +93 to +107
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
style={{
background: "none",
border: "none",
cursor: page === 1 ? "default" : "pointer",
display: "flex",
alignItems: "center",
color: page === 1 ? "#dadada" : "#080616",
padding: 4,
}}
>
<ChevronLeft size={18} />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

아이콘 전용 페이지네이션 버튼에 접근성 레이블이 필요합니다.

이전/다음 버튼이 아이콘만 있어 스크린리더에서 의미가 전달되지 않습니다. aria-label을 추가해 주세요.

수정 예시
-            <button
+            <button
+              type="button"
+              aria-label={t("common_previous_page")}
               onClick={() => setPage((p) => Math.max(1, p - 1))}
               disabled={page === 1}
               style={{
@@
-            <button
+            <button
+              type="button"
+              aria-label={t("common_next_page")}
               onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
               disabled={page === totalPages}
               style={{

Also applies to: 129-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/payment/PaymentSelectPartner.jsx` around lines 93 - 107, The
pagination icon-only buttons in PaymentSelectPartner.jsx lack accessible labels;
update the left and right pagination <button> elements (the one containing
<ChevronLeft /> and the one containing <ChevronRight /> around lines with page
state handling) to include descriptive aria-label attributes such as
aria-label="Previous page" and aria-label="Next page" respectively (also apply
the same change to the other pagination button pair noted at 129-143) so screen
readers can convey their purpose.

address={walletAddress}
onConfirm={() => {
setShowModal(false);
onNext(walletAddress);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

지갑 주소를 전달하기 전에 공백을 제거하세요.

94번 라인에서는 walletAddress.trim()으로 유효성 검사를 하지만, 106번 라인에서는 트림되지 않은 walletAddressonNext로 전달합니다. 사용자가 앞뒤 공백이 포함된 주소를 입력하면 검증은 통과하지만 공백이 포함된 값이 다음 단계로 전달되어 API 조회 실패나 예상치 못한 동작이 발생할 수 있습니다.

🔧 제안된 수정
          onConfirm={() => {
            setShowModal(false);
-           onNext(walletAddress);
+           onNext(walletAddress.trim());
          }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onNext(walletAddress);
onConfirm={() => {
setShowModal(false);
onNext(walletAddress.trim());
}}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/payment/PaymentVerifyInfo.jsx` at line 106, Validation uses
walletAddress.trim() but the value passed to onNext is untrimmed; update the
handler in PaymentVerifyInfo.jsx to compute a single trimmed value (e.g., const
trimmedAddress = walletAddress.trim()) use that for the existing validation and
pass trimmedAddress to onNext(walletAddress) call so the next step receives the
whitespace-trimmed address (reference symbols: walletAddress, onNext,
PaymentVerifyInfo.jsx).

@BaeSeong-min BaeSeong-min merged commit d815067 into develop May 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] XRP 결제 생성 3-step 플로우 구현 및 API 연동

1 participant