From 86fb78e20110ab0d98dfea604acda71c934d0fd3 Mon Sep 17 00:00:00 2001 From: smilevictory Date: Wed, 10 Sep 2025 14:15:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B6=80=20=EB=8F=99?= =?UTF-8?q?=EC=9D=98=EC=84=9C,=20=EC=9D=80=ED=96=89=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=84=B0=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Register/BankLinkPage.tsx | 8 ++++ .../Features/Register/RegisterPage.tsx | 8 ++++ .../sections/Features/Steps/index.tsx | 8 ++-- ssh/src/routes/routing.tsx | 40 ++++++------------- 4 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 ssh/src/pages/Donation/sections/Features/Register/BankLinkPage.tsx create mode 100644 ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx diff --git a/ssh/src/pages/Donation/sections/Features/Register/BankLinkPage.tsx b/ssh/src/pages/Donation/sections/Features/Register/BankLinkPage.tsx new file mode 100644 index 0000000..1aba81d --- /dev/null +++ b/ssh/src/pages/Donation/sections/Features/Register/BankLinkPage.tsx @@ -0,0 +1,8 @@ +export default function BankLinkPage() { + return ( +
+

은행 연동 안내

+

여기에 은행 연동 설명을 넣으세요.

+
+ ); +} diff --git a/ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx b/ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx new file mode 100644 index 0000000..6e4a19b --- /dev/null +++ b/ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx @@ -0,0 +1,8 @@ +export default function RegisterPage() { + return ( +
+

기부 의사 등록

+

여기에 등록 폼/안내 내용을 넣으세요.

+
+ ); +} diff --git a/ssh/src/pages/Donation/sections/Features/Steps/index.tsx b/ssh/src/pages/Donation/sections/Features/Steps/index.tsx index 6a448b9..41e8701 100644 --- a/ssh/src/pages/Donation/sections/Features/Steps/index.tsx +++ b/ssh/src/pages/Donation/sections/Features/Steps/index.tsx @@ -3,9 +3,6 @@ import { Button } from '@/components/ui/button'; import { ShieldCheck, Building2, HandCoins, FileSignature, ReceiptText } from 'lucide-react'; import { Link } from 'react-router-dom'; -const REGISTER_PATH = '/donation/register'; -const BANK_LINK_PATH = '/donation/banks'; - type Step = { title: string; desc: string; @@ -95,10 +92,11 @@ export default function Steps() {
+
diff --git a/ssh/src/routes/routing.tsx b/ssh/src/routes/routing.tsx index 703fbe5..6089236 100644 --- a/ssh/src/routes/routing.tsx +++ b/ssh/src/routes/routing.tsx @@ -10,39 +10,23 @@ import SettingPage from '@/pages/Setting'; import DonationPage from '@/pages/Donation/DonationPage'; import RootLayout from '@/components/Layout/RootLayout'; +import RegisterPage from '@/pages/Donation/sections/Features/Register/RegisterPage'; +import BankLinkPage from '@/pages/Donation/sections/Features/Register/BankLinkPage'; + export const routers = createBrowserRouter([ { path: '/', element: , children: [ - { - path: ROUTING_PATH.home, - element: , - }, - { - path: ROUTING_PATH.landing, - element: , - }, - { - path: ROUTING_PATH.atmmap, - element: , - }, - { - path: ROUTING_PATH.voicephishing, - element: , - }, - { - path: ROUTING_PATH.donation, - element: , - }, - { - path: ROUTING_PATH.setting, - element: , - }, + { path: ROUTING_PATH.home, element: }, + { path: ROUTING_PATH.landing, element: }, + { path: ROUTING_PATH.atmmap, element: }, + { path: ROUTING_PATH.voicephishing, element: }, + { path: ROUTING_PATH.donation, element: }, + { path: `${ROUTING_PATH.donation}/register`, element: }, + { path: `${ROUTING_PATH.donation}/banks`, element: }, + { path: ROUTING_PATH.setting, element: }, ], }, - { - path: ROUTING_PATH.chatbot, - element: , - }, + { path: ROUTING_PATH.chatbot, element: }, ]); From b8afac4287d93effc0b370ec141d75be0e0da349 Mon Sep 17 00:00:00 2001 From: smilevictory Date: Wed, 10 Sep 2025 16:49:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B6=80=20=EC=9D=98?= =?UTF-8?q?=EC=82=AC=20=EB=93=B1=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssh/package-lock.json | 109 ++++ ssh/package.json | 3 + ssh/src/components/Layout/RootLayout.tsx | 2 + ssh/src/components/common/ScrollToTop.tsx | 12 + ssh/src/components/ui/checkbox.tsx | 28 + ssh/src/components/ui/radio-group.tsx | 42 ++ ssh/src/components/ui/switch.tsx | 27 + .../Features/Register/RegisterPage.tsx | 606 +++++++++++++++++- 8 files changed, 826 insertions(+), 3 deletions(-) create mode 100644 ssh/src/components/common/ScrollToTop.tsx create mode 100644 ssh/src/components/ui/checkbox.tsx create mode 100644 ssh/src/components/ui/radio-group.tsx create mode 100644 ssh/src/components/ui/switch.tsx diff --git a/ssh/package-lock.json b/ssh/package-lock.json index 3c6a258..b0b0f84 100644 --- a/ssh/package-lock.json +++ b/ssh/package-lock.json @@ -9,13 +9,16 @@ "version": "0.0.0", "dependencies": { "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1240,6 +1243,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -1659,6 +1692,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -1762,6 +1827,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -1899,6 +1993,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", diff --git a/ssh/package.json b/ssh/package.json index f1ebbd3..18b0a74 100644 --- a/ssh/package.json +++ b/ssh/package.json @@ -11,13 +11,16 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/ssh/src/components/Layout/RootLayout.tsx b/ssh/src/components/Layout/RootLayout.tsx index 10376af..bcc9501 100644 --- a/ssh/src/components/Layout/RootLayout.tsx +++ b/ssh/src/components/Layout/RootLayout.tsx @@ -1,11 +1,13 @@ import { Outlet } from 'react-router-dom'; import Header from '@/components/Layout/Header'; import Footer from '@/components/Layout/Footer'; +import ScrollToTop from '@/components/common/ScrollToTop'; export default function RootLayout() { return ( <>
+
diff --git a/ssh/src/components/common/ScrollToTop.tsx b/ssh/src/components/common/ScrollToTop.tsx new file mode 100644 index 0000000..7d8c6f1 --- /dev/null +++ b/ssh/src/components/common/ScrollToTop.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +export default function ScrollToTop() { + const { pathname } = useLocation(); + + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + + return null; +} diff --git a/ssh/src/components/ui/checkbox.tsx b/ssh/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..0a6a9a5 --- /dev/null +++ b/ssh/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/ssh/src/components/ui/radio-group.tsx b/ssh/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..9d3a26e --- /dev/null +++ b/ssh/src/components/ui/radio-group.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/ssh/src/components/ui/switch.tsx b/ssh/src/components/ui/switch.tsx new file mode 100644 index 0000000..455c23b --- /dev/null +++ b/ssh/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx b/ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx index 6e4a19b..2c4b0c3 100644 --- a/ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx +++ b/ssh/src/pages/Donation/sections/Features/Register/RegisterPage.tsx @@ -1,8 +1,608 @@ +import { useMemo, useState, useRef, useEffect } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Switch } from '@/components/ui/switch'; +import { ShieldCheck, HandCoins, FileSignature, Building2, ReceiptText } from 'lucide-react'; + +type AmountType = 'fixed' | 'ratio'; + export default function RegisterPage() { + const navigate = useNavigate(); + + const [agreed, setAgreed] = useState(false); + const [amountType, setAmountType] = useState('fixed'); + const [fixedAmount, setFixedAmount] = useState(''); + const [ratio, setRatio] = useState(''); + const [bankLinked, setBankLinked] = useState(false); + const [sandboxMode] = useState(true); + const [destinations, setDestinations] = useState([]); + const [memo, setMemo] = useState(''); + + const [esignOpen, setEsignOpen] = useState(false); + const [signatureDataUrl, setSignatureDataUrl] = useState(null); + + const [step, setStep] = useState(1); + const maxStep = 5; + + const canNext = useMemo(() => { + if (step === 1) return agreed; + if (step === 2) { + if (amountType === 'fixed') return !!Number(fixedAmount); + return !!Number(ratio) && Number(ratio) > 0 && Number(ratio) <= 100; + } + if (step === 3) return bankLinked && !!signatureDataUrl; + if (step === 4) return destinations.length > 0; + return true; + }, [step, agreed, amountType, fixedAmount, ratio, bankLinked, destinations, signatureDataUrl]); + + const next = () => setStep((s) => Math.min(maxStep, s + 1)); + const prev = () => setStep((s) => Math.max(1, s - 1)); + + const submit = () => { + const payload = { + agreed, + amount: + amountType === 'fixed' + ? { type: 'fixed', value: Number(fixedAmount) } + : { type: 'ratio', value: Number(ratio) }, + bankLinked, + sandboxMode, + destinations, + memo, + }; + console.log('submit payload', payload); + alert('기부 의사 등록이 완료되었습니다.'); + navigate('/donation'); + }; + + const StepBadge = ({ n, active }: { n: number; active: boolean }) => ( +
+ {n} +
+ ); + + const StepHeader = ({ + n, + icon: Icon, + title, + desc, + active, + }: { + n: number; + icon: any; + title: string; + desc: string; + active: boolean; + }) => ( +
+ +
+ +
+

{title}

+

{desc}

+
+
+
+ ); + + const PRESET_AMOUNTS = [ + { label: '만원', value: 10_000 }, + { label: '십만원', value: 100_000 }, + { label: '백만원', value: 1_000_000 }, + { label: '천만원', value: 10_000_000 }, + { label: '억원', value: 100_000_000 }, + ]; + + const MAX_AMOUNT = 1_000_000_000_000; + const clamp = (n: number) => Math.max(0, Math.min(n, MAX_AMOUNT)); + const toNum = (s: string) => Number(s || '0'); + const fmt = (v: number) => v.toLocaleString('ko-KR') + '원'; + + function SignaturePad({ + height = 200, + lineWidth = 3, + onSave, + }: { + height?: number; + lineWidth?: number; + onSave: (dataUrl: string) => void; + }) { + const containerRef = useRef(null); + const canvasRef = useRef(null); + const ctxRef = useRef(null); + const drawingRef = useRef(false); + + useEffect(() => { + const canvas = canvasRef.current!; + const container = containerRef.current!; + const ctx = canvas.getContext('2d')!; + ctxRef.current = ctx; + + const resize = () => { + const dpr = Math.max(1, window.devicePixelRatio || 1); + const width = container.clientWidth; + const cssHeight = height; + + canvas.width = Math.floor(width * dpr); + canvas.height = Math.floor(cssHeight * dpr); + + canvas.style.width = `${width}px`; + canvas.style.height = `${cssHeight}px`; + + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(dpr, dpr); + + ctx.lineWidth = lineWidth; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = '#111827'; + }; + + resize(); + const ro = new ResizeObserver(resize); + ro.observe(container); + return () => ro.disconnect(); + }, [height, lineWidth]); + + const getPos = (e: React.PointerEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + return { x: e.clientX - rect.left, y: e.clientY - rect.top }; + }; + + const onPointerDown = (e: React.PointerEvent) => { + const ctx = ctxRef.current!; + const { x, y } = getPos(e); + drawingRef.current = true; + e.currentTarget.setPointerCapture(e.pointerId); + ctx.beginPath(); + ctx.moveTo(x, y); + }; + + const onPointerMove = (e: React.PointerEvent) => { + if (!drawingRef.current) return; + const ctx = ctxRef.current!; + const { x, y } = getPos(e); + ctx.lineTo(x, y); + ctx.stroke(); + }; + + const onPointerUp = (e: React.PointerEvent) => { + if (!drawingRef.current) return; + drawingRef.current = false; + const ctx = ctxRef.current!; + ctx.closePath(); + e.currentTarget.releasePointerCapture(e.pointerId); + }; + + const clear = () => { + const canvas = canvasRef.current!; + const ctx = ctxRef.current!; + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + }; + + const save = () => { + const dataUrl = canvasRef.current!.toDataURL('image/png'); + onSave(dataUrl); + }; + + return ( +
+
+ 서명을 입력해 주세요. +
+ + +
+
+ +
+ +
+

팁: 마우스/트랙패드/터치 모두 지원합니다.

+
+ ); + } + return ( -
-

기부 의사 등록

-

여기에 등록 폼/안내 내용을 넣으세요.

+
+
+

기부 의사 등록

+ + ← 기부 소개로 돌아가기 + +
+ +
+ {[1, 2, 3, 4, 5].map((n) => ( +
+
+
+ ))} +
+ + {/* STEP 1 */} + {step === 1 && ( + + + + + + + +

+ 약관과 처리방침은{' '} + + 은행 연동 안내 + + 에서 확인할 수 있습니다. +

+
+
+ )} + + {/* STEP 2 */} + {step === 2 && ( + + + + + + + setAmountType(v as AmountType)} + className="grid gap-4 md:grid-cols-2" + > + + + + + {amountType === 'fixed' ? ( +
+ + +
+ {PRESET_AMOUNTS.map((p) => ( + + ))} +
+ + { + const raw = e.target.value.replace(/[^0-9]/g, ''); + setFixedAmount(String(clamp(Number(raw)))); + }} + /> + +
+ + + 팁: 버튼을 누를 때마다 금액이 누적됩니다. + +
+ +

+ 현재 금액: {fmt(toNum(fixedAmount))} +

+
+ ) : ( +
+ + setRatio(e.target.value.replace(/[^0-9]/g, ''))} + /> +

1–100 사이의 정수를 권장합니다.

+
+ )} +
+
+ )} + + {/* STEP 3 */} + {step === 3 && ( + + + + + +
+

+ 실제 환경에서는 KB국민은행의 공식 연동 절차(로그인/전자서명)를 진행합니다. +

+ +
+ + {bankLinked && ✔ 연결되었습니다.} +
+ +
+ + +
+ + {esignOpen && ( +
+ { + setSignatureDataUrl(dataUrl); + }} + /> +
+ + {signatureDataUrl && ( + <> + 서명 미리보기 + + + )} +
+
+ )} + +

+ 자세한 절차는{' '} + + 은행 연동 안내 + + 를 참고하세요. +

+
+
+
+ )} + + {/* STEP 4 */} + {step === 4 && ( + + + + + +
+ {[ + '국내 병원', + '장학재단/학교', + '사회복지/비영리', + '국제구호', + '환경/동물보호', + '문화/예술', + ].map((name) => ( + + ))} +
+ +
+ + setMemo(e.target.value)} + maxLength={120} + placeholder="예: ○○대학교 장학금, ○○병원 소아암센터 등" + className=" + h-20 px-6 + text-3xl md:text-4xl font-semibold tracking-tight leading-tight + placeholder:text-3xl md:placeholder:text-4xl placeholder:font-semibold placeholder:tracking-tight placeholder:leading-tight + " + /> +
+
+
+ )} + + {/* STEP 5 */} + {step === 5 && ( + + + + + +
+
요약
+
    +
  • 동의 여부: {agreed ? '동의함' : '미동의'}
  • +
  • + 지정 방식:{' '} + {amountType === 'fixed' + ? `정액 ${Number(fixedAmount || 0).toLocaleString()}원` + : `비율 ${ratio || 0}%`} +
  • +
  • + 은행 연동: {bankLinked ? '완료' : '미완료'} ({sandboxMode ? '샌드박스' : '실거래'} + ) +
  • +
  • 기부처: {destinations.length ? destinations.join(', ') : '선택 없음'}
  • +
  • 메모: {memo || '미등록'}
  • +
+

+ 제출 후에도 생전에는 언제든 수정·철회가 + 가능합니다. +

+
+
+
+ )} + +
+ +
+ {step < maxStep ? ( + + ) : ( + + )} +
+
); }