[Feat] XRP 결제 생성 3-step 플로우 구현 및 API 연동#16
Conversation
Co-Authored-By: seongmin <baeseongmin000@gmail.com>
Co-Authored-By: seongmin <baeseongmin000@gmail.com>
Co-Authored-By: seongmin <baeseongmin000@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 Walkthrough요약결제 마일스톤 기반의 다단계 결제 생성 흐름을 구현한다. 파트너 선택, 지갑 주소 확인, 결제 정보 입력, 완료 페이지로 구성되며, 새로운 Escrow Payments API, React Query 훅, 재사용 가능한 UI 컴포넌트를 추가한다. 변경 사항결제 생성 흐름 (Payment Flow)
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
🎯 3 (Moderate) | ⏱️ ~25 minutes 관련 PR
🚥 Pre-merge checks | ✅ 1 | ❌ 4❌ Failed checks (4 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 valueSTEPS 배열 메모이제이션을 고려해보세요.
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 주석 추가를 고려해보세요.
EscrowItemDto와CreateEscrowPaymentDto는 외부에서 사용되는 퍼블릭 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 wini18n을 사용하여 에러 메시지를 다국어화하세요.
에러 메시지가 하드코딩되어 있어 다른 언어 사용자에게 적절한 메시지를 제공할 수 없습니다. 앱의 다른 부분과 일관성을 유지하기 위해
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 winXRP 금액 유효성 검사를 추가하세요.
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
📒 Files selected for processing (28)
src/apis/endpoints.tssrc/apis/index.tssrc/apis/modules/escrowPayments.tssrc/components/Header.jsxsrc/components/LabeledInput.jsxsrc/components/PartnerTable.jsxsrc/components/PartnerTableRow.jsxsrc/components/Tooltip.jsxsrc/components/payment/EscrowInfoCard.jsxsrc/components/payment/PartnerTableSkeleton.jsxsrc/components/payment/RowLabel.jsxsrc/components/payment/StepProgressBar.jsxsrc/components/payment/WalletAddressCard.jsxsrc/components/payment/WalletAddressField.jsxsrc/components/payment/WalletConfirmModal.jsxsrc/hooks/myBusiness/useMyProfile.jssrc/hooks/payments/useCreateEscrowPayment.jssrc/hooks/payments/useMyPartners.jssrc/hooks/payments/useWalletUser.jssrc/lib/i18n/dict.jssrc/pages/LoginPage.jsxsrc/pages/RegisterPage.jsxsrc/pages/payment/CreatePayment.jsxsrc/pages/payment/PaymentComplete.jsxsrc/pages/payment/PaymentRequestForm.jsxsrc/pages/payment/PaymentSelectPartner.jsxsrc/pages/payment/PaymentVerifyInfo.jsxsrc/router.jsx
💤 Files with no reviewable changes (1)
- src/components/PartnerTable.jsx
| <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> |
There was a problem hiding this comment.
웹사이트 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.
| 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} | ||
| /> |
There was a problem hiding this comment.
프로필 조회 실패 상태가 숨겨지고 있습니다.
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.
| 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.
| <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> |
There was a problem hiding this comment.
아이콘 전용 페이지네이션 버튼에 접근성 레이블이 필요합니다.
이전/다음 버튼이 아이콘만 있어 스크린리더에서 의미가 전달되지 않습니다. 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); |
There was a problem hiding this comment.
지갑 주소를 전달하기 전에 공백을 제거하세요.
94번 라인에서는 walletAddress.trim()으로 유효성 검사를 하지만, 106번 라인에서는 트림되지 않은 walletAddress를 onNext로 전달합니다. 사용자가 앞뒤 공백이 포함된 주소를 입력하면 검증은 통과하지만 공백이 포함된 값이 다음 단계로 전달되어 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.
| 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).
🔍 PR 요약
🧾 관련 이슈
🛠️ 주요 변경 사항
CreatePayment페이지 및/payments/create라우팅 추가StepProgressBar컴포넌트 구현PaymentSelectPartner, 페이지네이션, shimmer 스켈레톤WalletConfirmModal, 지갑 주소 기반 파트너 조회 API 연동EscrowInfoCard, 마일스톤/이벤트 트리거 추가·삭제WalletAddressCard컴포넌트 및useMyProfile훅 구현POST /escrow-payments)RowLabel컴포넌트 분리PartnerTableRow텍스트 truncate 항목 hover 툴팁 추가src/components/payment/)🧠 의도 및 배경