From b9a9422562b502fe45ee7f3ea46b7ffc4e789ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:14:55 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=20=ED=91=9C=EC=8B=9C=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/ScreenToast.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/ui/ScreenToast.tsx b/src/components/ui/ScreenToast.tsx index 0a11ca22..742a979d 100644 --- a/src/components/ui/ScreenToast.tsx +++ b/src/components/ui/ScreenToast.tsx @@ -14,6 +14,11 @@ interface ScreenToastProps { * positive·cautionary는 WDS 기본 아이콘이 Figma와 이미 일치해서 따로 넘기지 않는다. */ variant?: "negative" | "positive" | "cautionary"; + /** + * 떠 있는 시간 — WDS Toast 기본값 short(3초), long은 5초, 숫자는 ms다. + * 읽고 옮겨 적어야 하는 문구(복사가 막혔을 때의 학생회비 계좌번호 등)는 숫자로 길게 준다. + */ + duration?: "short" | "long" | number; } // Figma: Toast/Toast (nodeId 1450:93026 negative / 1410:50080 positive / 2212:160680 cautionary). @@ -27,6 +32,7 @@ function ScreenToast({ message, onOpenChange, variant = "negative", + duration = "short", }: ScreenToastProps) { const portalEl = useScreenSheetPortal(); @@ -38,6 +44,7 @@ function ScreenToast({
Date: Fri, 6 Nov 2026 12:14:56 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=ED=81=B4=EB=A6=BD=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EB=B3=B5=EC=82=AC=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/copyText.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/lib/copyText.ts diff --git a/src/lib/copyText.ts b/src/lib/copyText.ts new file mode 100644 index 00000000..2df78956 --- /dev/null +++ b/src/lib/copyText.ts @@ -0,0 +1,31 @@ +// 글자를 클립보드에 복사하고 성공했는지 돌려준다(학생회비 계좌번호 복사 등). +// `navigator.clipboard`는 보안 컨텍스트와 권한이 있어야 해서 앱 WebView처럼 막히는 곳이 있다 — +// 막히면 숨긴 textarea를 골라 `document.execCommand("copy")`로 한 번 더 시도한다. +// 둘 다 실패하면 false라서, 부르는 쪽이 복사할 값을 화면에 직접 보여 줘야 한다. +// 사용자가 누른 그 이벤트 안에서 불러야 한다 — 다른 요청을 기다린 뒤에 부르면 브라우저가 막는다. +export async function copyText(text: string): Promise { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + return copyWithTextarea(text); + } +} + +function copyWithTextarea(text: string): boolean { + const textarea = document.createElement("textarea"); + textarea.value = text; + // 모바일에서 키보드가 올라오거나 화면이 움직이지 않게 읽기 전용으로 화면 밖에 둔다 + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.append(textarea); + textarea.select(); + try { + return document.execCommand("copy"); + } catch { + return false; + } finally { + textarea.remove(); + } +} From 157f83b33b98e869d382dc966d3d0102f18739f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:14:56 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=ED=95=99=EC=83=9D=ED=9A=8C?= =?UTF-8?q?=EB=B9=84=20=EC=86=A1=EA=B8=88=20=EB=8B=A8=EA=B3=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=86=A0=EC=8A=A4=C2=B7=EA=B3=84=EC=A2=8C=EC=9D=B4?= =?UTF-8?q?=EC=B2=B4=20=EC=A4=91=20=EC=86=A1=EA=B8=88=20=EB=B0=A9=EB=B2=95?= =?UTF-8?q?=20=EA=B3=A0=EB=A5=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/router.tsx | 2 +- src/assets/lottie/fee/won.json | 1 - src/features/fee/FeeTransferScreen.tsx | 140 ++++++++++++++---- .../components/FeeTransferMethodOption.tsx | 57 +++++++ .../fee/components/FeeTransferStepLayout.tsx | 2 +- src/features/fee/constants/fee.ts | 7 +- .../fee/constants/feeTransferFunnel.ts | 40 +++-- .../fee/steps/FeeTransferMethodStep.tsx | 125 ++++++++++++++++ .../fee/steps/FeeTransferTossStep.tsx | 89 ----------- .../fee/steps/FeeTransferTranscriptStep.tsx | 2 +- 10 files changed, 331 insertions(+), 134 deletions(-) delete mode 100644 src/assets/lottie/fee/won.json create mode 100644 src/features/fee/components/FeeTransferMethodOption.tsx create mode 100644 src/features/fee/steps/FeeTransferMethodStep.tsx delete mode 100644 src/features/fee/steps/FeeTransferTossStep.tsx diff --git a/src/app/router.tsx b/src/app/router.tsx index 7fd013ed..7cdc46c0 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -255,7 +255,7 @@ const routes = [ }, { element: , - // 학생회비 계좌 송금 퍼널 하나가 모든 단계(남은 학기 수, 학점이수현황 캡처, 토스 이동, + // 학생회비 계좌 송금 퍼널 하나가 모든 단계(남은 학기 수, 학점이수현황 캡처, 송금 방법, // 송금 확인, 확인요청 완료)를 그린다 — 단계는 `?fee.step=`으로 나뉜다. // 전부 하단 고정 버튼(Action Area)이 있는 흰 배경 화면이라 Bottom Nav가 없다. handle: { diff --git a/src/assets/lottie/fee/won.json b/src/assets/lottie/fee/won.json deleted file mode 100644 index a4b4afe0..00000000 --- a/src/assets/lottie/fee/won.json +++ /dev/null @@ -1 +0,0 @@ -{"nm":"Won Icon","ddd":0,"h":48,"w":48,"meta":{"g":"LottieFiles Figma (Figma Motion)"},"layers":[{"ty":4,"nm":"Won Icon","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Icon 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[20,20]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[24,24]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[],"ind":1},{"ty":4,"nm":"Won Symbol","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Symbol 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[9.62,7.13]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[19.62,21.15]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[],"ind":2,"parent":1},{"ty":4,"nm":"Won Symbol Stroke 2","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Symbol Stroke 2 1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[9.62,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[9.62,4.96]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[19.23,0]]}}},{"ty":"tm","bm":0,"hd":false,"nm":"","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"o":{"x":0.25,"y":0.65},"i":{"x":0.35,"y":1},"s":[0],"t":25},{"s":[100],"t":66}]},"o":{"a":0,"k":0},"s":{"a":0,"k":0},"m":1},{"ty":"st","bm":0,"hd":false,"nm":"","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":2.7295289039611816},"c":{"a":0,"k":[1,1,1]}}],"ind":3,"parent":2},{"ty":4,"nm":"Won Symbol Stroke 1","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Symbol Stroke 1 1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[7.83,7.13]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[9.69,7.13]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0,0],[3.72,14.27],[8.06,0],[11.88,14.27],[15.65,0]]}}},{"ty":"tm","bm":0,"hd":false,"nm":"","e":{"a":1,"k":[{"o":{"x":0.25,"y":0.65},"i":{"x":0.35,"y":1},"s":[0],"t":0},{"s":[100],"t":66}]},"o":{"a":0,"k":0},"s":{"a":0,"k":0},"m":1},{"ty":"st","bm":0,"hd":false,"nm":"","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":2.7295289039611816},"c":{"a":0,"k":[1,1,1]}}],"ind":4,"parent":2},{"ty":4,"nm":"Won Icon Background 1","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Icon Background 1 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[20,20]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[20,20]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[11.05,0],[0,11.05],[-11.05,0],[0,-11.05]],"o":[[0,11.05],[-11.05,0],[0,-11.05],[11.05,0],[0,0]],"v":[[40,20],[20,40],[0,20],[20,0],[40,20]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[0.9961,0.8118,0.3608]},"r":1,"o":{"a":0,"k":100}}],"ind":5,"parent":1}],"v":"5.7.0","fr":60,"op":120,"ip":0,"assets":[]} \ No newline at end of file diff --git a/src/features/fee/FeeTransferScreen.tsx b/src/features/fee/FeeTransferScreen.tsx index c5a4f3f2..8e0ca357 100644 --- a/src/features/fee/FeeTransferScreen.tsx +++ b/src/features/fee/FeeTransferScreen.tsx @@ -6,19 +6,22 @@ import ScreenToast from "@/components/ui/ScreenToast"; import FeeTransferSkeleton from "@/features/fee/components/FeeTransferSkeleton"; import { FEE_PAYMENTS_PATH, + formatWon, isAcceptableTranscript, } from "@/features/fee/constants/fee"; import { FEE_TRANSFER_STEP_OPTIONS, + type FeeTransferMethod, type FeeTransferSteps, } from "@/features/fee/constants/feeTransferFunnel"; import FeeTransferCompleteStep from "@/features/fee/steps/FeeTransferCompleteStep"; import FeeTransferConfirmStep from "@/features/fee/steps/FeeTransferConfirmStep"; +import FeeTransferMethodStep from "@/features/fee/steps/FeeTransferMethodStep"; import FeeTransferSemesterStep from "@/features/fee/steps/FeeTransferSemesterStep"; -import FeeTransferTossStep from "@/features/fee/steps/FeeTransferTossStep"; import FeeTransferTranscriptStep from "@/features/fee/steps/FeeTransferTranscriptStep"; import { HOME_PATH } from "@/features/home/constants/home"; import { useBackNavigation } from "@/hooks/useBackNavigation"; +import { copyText } from "@/lib/copyText"; import { openExternalLink } from "@/lib/openExternalLink"; import { useFunnel, useFunnelExit } from "@/lib/useFunnel"; import { isApiError } from "@/shared/api"; @@ -35,13 +38,13 @@ import { } from "@/shared/entities/service-links/serviceLinksQueries"; import type { ServiceLinkResponse } from "@/shared/entities/service-links/serviceLinksTypes"; -// 학생회비 계좌 송금(남은 학기 수 → 학점이수현황 캡처 → 토스 이동 → 송금 확인 → 확인요청 완료)을 +// 학생회비 계좌 송금(남은 학기 수 → 학점이수현황 캡처 → 송금 방법 → 송금 확인 → 확인요청 완료)을 // useFunnel 하나로 묶은 화면이다. 단계 화면은 `steps/`에 하나씩 나눠 두고, 이 파일은 단계 전환 // (어느 단계에서 어디로, 무슨 값을 넘기는지)만 맡는다 — 로그인·온보딩 퍼널(AuthSignUpScreen)과 같은 구성이다. // 단계는 `/fee/transfer?fee.step=...`으로 나뉘고 단계마다 브라우저 뒤로가기가 한 단계씩 돌아간다. -// 단계 사이에 넘기는 값(남은 학기 수·올린 캡처의 파일 키·송금 링크)은 퍼널 context가 들고 있어서 -// 뒤로 돌아와도 유지된다. 고른 캡처 파일만은 context(history.state)에 담지 못해서 이 화면이 메모리에 들고 -// 있는다(feeTransferFunnel.ts 참고) — 새로고침하면 캡처만 다시 고르면 된다. +// 단계 사이에 넘기는 값(남은 학기 수·올린 캡처의 파일 키·송금 안내)은 퍼널 context가 들고 있어서 +// 뒤로 돌아와도 유지된다. 고른 캡처 파일과 송금 방법은 이 화면이 들고 있는다 — 캡처는 context(history.state)에 +// 담지 못해서다(feeTransferFunnel.ts 참고). 새로고침하면 캡처만 다시 고르면 된다. // 단계가 바뀌는 이동에만 슬라이드를 켠다 — 학기 수 입력처럼 같은 단계의 context만 // 갈아 끼우는 replace에는 켜지 않는다. 뒤로가기는 React Router가 앞선 이동을 기억해 반대 방향으로 @@ -57,10 +60,31 @@ const INITIAL_STATE = { context: {}, step: "semester" } as const; const TOAST_MESSAGES = { invalidProofImage: "캡처를 확인하지 못했어요. 다시 올려 주세요.", invalidTranscript: "10MB 이하의 PNG·JPG·WEBP 이미지만 올릴 수 있어요.", - prepareTossFailed: "다음 단계로 넘어가지 못했어요. 다시 시도해 주세요.", + prepareTransferFailed: "다음 단계로 넘어가지 못했어요. 다시 시도해 주세요.", submitFailed: "납부 확인 요청에 실패했어요. 다시 시도해 주세요.", } as const; +// 계좌이체 안내 — 계좌번호는 화면에 보여 주지 않아서 토스트로만 알린다. 은행 앱에서 금액을 직접 넣어야 해서 +// 보낼 금액을 같이 적는다. 복사가 막히면(앱 WebView 등) 계좌번호를 토스트에 담아 길게 띄워 보고 보낼 수 있게 한다. +// 계좌번호 끝 숫자에 따라 조사(로/으로)가 달라져서 "계좌로"로 끝맺는다. 은행 이름과 계좌번호 사이는 +// 줄바꿈 없는 공백(U+00A0)이라 토스트에서 둘이 다른 줄로 갈라지지 않는다. +// 복사가 막혔을 때는 계좌번호를 읽고 옮겨 적어야 해서 15초 띄운다(WDS Toast long은 5초뿐이다). +const ACCOUNT_COPY_FAILED_TOAST_DURATION_MS = 15_000; + +const ACCOUNT_TOAST_MESSAGES = { + copied: (amount: string) => `계좌번호를 복사했어요. ${amount}을 보내 주세요.`, + copyFailed: (bank: string, accountNo: string, amount: string) => + `복사하지 못했어요. ${bank}\u00a0${accountNo} 계좌로 ${amount}을 보내 주세요.`, +}; + +interface ToastState { + isOpen: boolean; + key: number; + message: string; + variant?: "negative" | "positive" | "cautionary"; + duration?: "short" | "long" | number; +} + function FeeTransferFunnel() { // 내 납부 상태와 캡처 단계의 K스마트멘토 주소(운영진이 등록하는 서비스 링크)를 한 번에 기다린다 — // 요청이 줄줄이 이어지지 않게 하고, 캡처 단계에 닿았을 때 링크가 이미 준비돼 있게 한다. @@ -97,18 +121,33 @@ function FeeTransferFunnel() { const queryClient = useQueryClient(); const { mutateAsync: uploadFile } = useUploadFileMutation(); const { mutate: submitFee, isPending: isSubmitting } = useSubmitFeeMutation(); - // 고른 캡처 — 단계를 오가도 이 화면은 그대로 떠 있어서 토스에서 돌아와도 남는다 + // 고른 캡처 — 단계를 오가도 이 화면은 그대로 떠 있어서 송금 단계에서 돌아와도 남는다 const [transcript, setTranscript] = useState(); - // 캡처 단계의 "다음" — 캡처를 올리고 송금 링크를 받는 동안 버튼을 막는다 - const [isPreparingToss, setPreparingToss] = useState(false); - // 마지막으로 올린 캡처와 그 키 — 토스에서 돌아와 같은 파일로 다시 "다음"을 누르면 또 올리지 않는다 + // 고른 송금 방법 — 캡처처럼 이 화면이 들고 있어서 송금 확인에서 뒤로 돌아와도 남는다 + const [transferMethod, setTransferMethod] = + useState("toss"); + // 캡처 단계의 "다음" — 캡처를 올리고 송금 안내를 받는 동안 버튼을 막는다 + const [isPreparingTransfer, setPreparingTransfer] = useState(false); + // 마지막으로 올린 캡처와 그 키 — 송금 단계에서 돌아와 같은 파일로 다시 "다음"을 누르면 또 올리지 않는다 const lastUploadRef = useRef<{ file: File; fileKey: string } | null>(null); // 닫힌 뒤에도 마지막 문구를 들고 있어서 닫히는 동안 문구가 바뀌지 않는다. // 떠 있는 동안 다시 띄워도 새로 안내되게 key를 올린다(ScreenToast의 key) - const [toast, setToast] = useState({ isOpen: false, key: 0, message: "" }); + const [toast, setToast] = useState({ + isOpen: false, + key: 0, + message: "", + }); - const showToast = (message: string) => - setToast((prev) => ({ isOpen: true, key: prev.key + 1, message })); + const showToast = ( + message: string, + options: Pick = {}, + ) => + setToast((prev) => ({ + isOpen: true, + key: prev.key + 1, + message, + ...options, + })); const uploadTranscript = async (file: File) => { const lastUpload = lastUploadRef.current; @@ -190,10 +229,40 @@ function FeeTransferFunnel() { } /> )} - toss={({ context, history }) => ( - ( + history.back()} + onChangeMethod={setTransferMethod} + // 은행 앱에서 보내고 돌아오면 송금 확인 단계가 기다리게 바로 넘긴다(토스와 같다). + // 복사가 막혀도 토스트에 담긴 계좌로 보낼 수 있어서 같이 넘긴다 — 여기서 멈추면 다음으로 갈 길이 없다 + onCopyAccount={() => { + const amount = formatWon(context.transferAmount); + // 은행 앱이 붙여 넣은 계좌를 알아보기 쉽게 은행 이름을 붙여 복사한다 + copyText(`${context.bank} ${context.accountNo}`).then( + (isCopied) => { + if (isCopied) { + showToast(ACCOUNT_TOAST_MESSAGES.copied(amount), { + variant: "positive", + }); + } else { + showToast( + ACCOUNT_TOAST_MESSAGES.copyFailed( + context.bank, + context.accountNo, + amount, + ), + { + duration: ACCOUNT_COPY_FAILED_TOAST_DURATION_MS, + variant: "cautionary", + }, + ); + } + history.push("confirm", {}, SLIDE); + }, + ); + }} onOpenToss={() => { if (!context.transferLinkUrl) { return; @@ -203,11 +272,13 @@ function FeeTransferFunnel() { // 알 수 없어서 열기만 하고 넘긴다 — 송금하지 못했으면 그 단계에서 빠져나간다. history.push("confirm", {}, SLIDE); }} + remainingSemesters={context.semester} + transferAmount={context.transferAmount} /> )} transcript={({ context, history }) => ( history.back()} onChangeTranscript={(nextTranscript) => { @@ -218,27 +289,42 @@ function FeeTransferFunnel() { setTranscript(nextTranscript); }} // 송금하러 가기 전에 캡처를 올려 둔다 — 업로드가 실패하면 송금 전에 알 수 있다. - // 송금 링크는 남은 학기 수마다 금액이 달라서 이때 같이 받는다 + // 송금 안내(금액·토스 링크·계좌)는 남은 학기 수마다 금액이 달라서 이때 같이 받는다 onNext={(file) => { - setPreparingToss(true); + setPreparingTransfer(true); Promise.all([ uploadTranscript(file), queryClient.fetchQuery( feeQueries.transferGuide(context.semester), ), ]) - .then(([proofImageKey, { transferLinkUrl }]) => - history.push( - "toss", - { proofImageKey, transferLinkUrl }, - SLIDE, - ), + .then( + ([ + proofImageKey, + { transferLinkUrl, bank, accountNo, transferAmount }, + ]) => { + // 금액은 지금 낼 수 있는 상태(미납·확인필요)면 서버가 항상 계산해 준다 — 없으면 넘기지 않는다 + if (transferAmount === null) { + throw new Error("송금 금액을 받지 못했어요"); + } + history.push( + "method", + { + accountNo, + bank, + proofImageKey, + transferAmount, + transferLinkUrl, + }, + SLIDE, + ); + }, ) .catch((error: unknown) => { console.error(error); - showToast(TOAST_MESSAGES.prepareTossFailed); + showToast(TOAST_MESSAGES.prepareTransferFailed); }) - .finally(() => setPreparingToss(false)); + .finally(() => setPreparingTransfer(false)); }} // 운영진이 링크를 등록하지 않았으면 넘기지 않는다 — 캡처 단계가 "K스마트멘토"를 일반 텍스트로 보여 준다 onOpenGuide={ @@ -251,6 +337,7 @@ function FeeTransferFunnel() { /> { @@ -259,6 +346,7 @@ function FeeTransferFunnel() { } }} open={toast.isOpen} + variant={toast.variant} /> ); diff --git a/src/features/fee/components/FeeTransferMethodOption.tsx b/src/features/fee/components/FeeTransferMethodOption.tsx new file mode 100644 index 00000000..3665f52f --- /dev/null +++ b/src/features/fee/components/FeeTransferMethodOption.tsx @@ -0,0 +1,57 @@ +import { RadioGroupItem, Typography } from "@wanteddev/wds"; + +import type { FeeTransferMethod } from "@/features/fee/constants/feeTransferFunnel"; + +interface FeeTransferMethodOptionProps { + value: FeeTransferMethod; + title: string; + /** 고르면 어떻게 보내는지 한 줄 안내 */ + caption: string; + selected: boolean; +} + +// 송금 방법 단계의 선택 카드 하나 — 부모의 WDS `RadioGroup` 안에 둔다. Figma에 없는 화면이다. +// `RadioGroupItem`은 label prop이 없어서(행사 신청 폼과 같은 사정) 카드 전체를 `