Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 51 additions & 53 deletions ssh/src/components/Layout/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,76 @@
import { useLocation, useNavigate } from "react-router-dom";
import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar";
import { Button } from "@/components/ui/button";
import { ROUTING_PATH } from "@/routes/path.constants";
import { MessageSquare, CircleUserRound } from "lucide-react";
import { Link, useLocation } from 'react-router-dom';
import { Menubar, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar';
import { Button } from '@/components/ui/button';
import { ROUTING_PATH } from '@/routes/path.constants';
import { MessageSquare, CircleUserRound } from 'lucide-react';

const toAbs = (seg: string) => (!seg ? '/' : seg.startsWith('/') ? seg : `/${seg}`);

export default function Header() {
const { pathname } = useLocation();
const navigate = useNavigate();

const NAV = [
{ label: "홈페이지", seg: ROUTING_PATH.home },
{ label: "채팅하기", seg: ROUTING_PATH.chatbot },
{ label: "보이스피싱 진단", seg: ROUTING_PATH.voicephishing },
{ label: "주변 ATM 찾기", seg: ROUTING_PATH.atmmap },
{ label: "기부", seg: ROUTING_PATH.donation },
{ label: '홈페이지', seg: ROUTING_PATH.home },
{ label: '채팅하기', seg: ROUTING_PATH.chatbot },
{ label: '보이스피싱 진단', seg: ROUTING_PATH.voicephishing },
{ label: '주변 ATM 찾기', seg: ROUTING_PATH.atmmap },
{ label: '기부', seg: ROUTING_PATH.donation },
] as const;

const toAbs = (seg: string) => (seg === "" ? "/" : `/${seg}`);

const handleNav = (seg: string) => {
navigate(toAbs(seg));
};

const isActive = (seg: string) => {
if (seg === "") return pathname === "/";
return pathname.startsWith(`/${seg}`);
const abs = toAbs(seg);
return abs === '/' ? pathname === '/' : pathname.startsWith(abs);
};

const loginActive = isActive(ROUTING_PATH.setting);

return (
<header className="fixed top-0 z-50 w-full bg-background/80 backdrop-blur">
<div className="mx-auto flex h-16 w-full max-w-screen-xl items-center justify-between px-6">
<Menubar className="border-transparent bg-transparent p-0 shadow-none">
{NAV.map((item) => (
<MenubarMenu key={item.label}>
<MenubarTrigger
onClick={() => handleNav(item.seg)}
aria-current={isActive(item.seg) ? "page" : undefined}
className={[
"cursor-pointer rounded-full px-4 py-2 text-sm font-medium transition-colors",
isActive(item.seg)
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted",
].join(" ")}
>
{item.label}
</MenubarTrigger>
</MenubarMenu>
))}
{NAV.map((item) => {
const href = toAbs(item.seg);
return (
<MenubarMenu key={item.label}>
<MenubarTrigger
asChild
aria-current={isActive(item.seg) ? 'page' : undefined}
className={[
'cursor-pointer rounded-full px-4 py-2 text-sm font-medium transition-colors',
isActive(item.seg)
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-muted',
].join(' ')}
>
<Link to={href}>{item.label}</Link>
</MenubarTrigger>
</MenubarMenu>
);
})}
</Menubar>

<div className="flex items-center gap-4">
<Button
variant="ghost"
onClick={() => handleNav(ROUTING_PATH.setting)}
className={[
"rounded-full px-4 py-2 text-sm font-medium transition-colors",
loginActive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted",
].join(" ")}
asChild
className="rounded-full px-4 py-2 text-sm font-medium transition-colors"
>
<CircleUserRound className="mr-2 h-4 w-4" />
로그인/회원가입
<Link
to={toAbs(ROUTING_PATH.setting)}
className={
isActive(ROUTING_PATH.setting)
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-muted'
}
>
<CircleUserRound className="mr-2 h-4 w-4" />
로그인/회원가입
</Link>
</Button>

<Button
size="sm"
className="rounded-full"
onClick={() => handleNav(ROUTING_PATH.chatbot)}
>
<MessageSquare className="mr-2 h-4 w-4" />
채팅 시작하기
<Button size="sm" className="rounded-full" asChild>
<Link to={toAbs(ROUTING_PATH.chatbot)}>
<MessageSquare className="mr-2 h-4 w-4" />
채팅 시작하기
</Link>
</Button>
</div>
</div>
Expand Down
109 changes: 109 additions & 0 deletions ssh/src/pages/Donation/components/fourthsection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
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;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
};

const STEPS: Step[] = [
{
title: '본인 확인 및 동의',
desc: '전용 로그인으로 본인 확인 후, 사전기부 안내에 동의합니다.',
icon: ShieldCheck,
},
{
title: '금액/비율 지정',
desc: '정액 또는 비율로 기부 범위를 설정합니다. 생전 언제든 수정·철회 가능합니다.',
icon: HandCoins,
},
{
title: '은행 기부 기능 연동',
desc: 'KB국민은행 ‘공식 기부 기능’과 연동되어 안전하게 등록됩니다.',
icon: FileSignature,
},
{
title: '기부처 선택',
desc: '병원·학교·비영리단체 등 은행에서 제공하는 원하는 곳을 고릅니다.',
icon: Building2,
},
{
title: '등록 완료 · 사후 집행',
desc: '사후에 지정한 내용대로 기부가 집행되고, 영수증이 발급됩니다.',
icon: ReceiptText,
},
];

export default function Steps() {
return (
<section id="steps" aria-labelledby="steps-title" className="w-full">
<div className="mx-auto grid max-w-screen-xl grid-cols-1 gap-10 px-6 py-14 lg:grid-cols-2">
<div>
<h3 id="steps-title" className="text-xl font-semibold md:text-2xl">
기부 신청 방법 (단계별 안내)
</h3>

<ol role="list" className="mt-8 space-y-8">
{STEPS.map((s, idx) => {
const Icon = s.icon;
return (
<li key={s.title} className="relative">
<div className="absolute left-3 top-6 bottom-0 w-px bg-border" aria-hidden />
<span className="absolute left-0 top-0 grid h-6 w-6 place-items-center rounded-full bg-primary text-xs font-bold text-primary-foreground">
{idx + 1}
</span>

<div className="pl-10">
<div className="flex items-start gap-3">
<Icon className="mt-0.5 h-5 w-5 text-primary" aria-hidden />
<div>
<h4 className="text-base font-medium">{s.title}</h4>
<p className="mt-1 text-sm leading-6 text-muted-foreground">{s.desc}</p>
</div>
</div>
</div>
</li>
);
})}
</ol>
</div>

<div className="flex">
<Card className="my-auto w-full rounded-xl shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base md:text-lg">신청하기</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm leading-6 text-muted-foreground md:text-base">
<p>
등록 내용은 생전 언제든 <b className="text-foreground">수정·철회</b>할 수 있습니다.
</p>
<p>
<b className="text-foreground">기부금 영수증</b> 발급 및 연말정산 연계가 가능합니다.
</p>
<p>
개인·계좌 정보는 은행 시스템에서 <b className="text-foreground">암호화</b>되어
처리됩니다.
</p>

<div className="pt-2 flex flex-col gap-3 sm:flex-row">
<Button asChild className="rounded-full">
<Link to={REGISTER_PATH}>기부 의사 등록하기</Link>
</Button>
<Button asChild variant="ghost" className="rounded-full">
<Link to={BANK_LINK_PATH}>은행 연동 안내</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</section>
);
}
16 changes: 16 additions & 0 deletions ssh/src/pages/Donation/components/herosection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default function Hero() {
return (
<section id="hero" className="w-full bg-blue-50">
<div className="mx-auto max-w-screen-xl px-6 py-10 md:py-14">
<h1 className="text-2xl font-bold tracking-tight md:text-3xl">
당신의 뜻, 사회에 이어집니다
</h1>
<p className="mt-4 text-base leading-7 text-muted-foreground md:text-lg">
평생 모은 재산을 원하는 곳에 기부하세요.
<br className="hidden sm:block" />
복잡한 절차 없이, 사전에 간단히 등록하면 사후에도 안전하게 뜻이 전달됩니다.
</p>
</div>
</section>
);
}
29 changes: 29 additions & 0 deletions ssh/src/pages/Donation/components/secondsection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default function Meaning() {
return (
<section id="meaning" className="w-full">
<div className="mx-auto grid max-w-screen-xl grid-cols-1 gap-8 px-6 py-14 md:grid-cols-2 md:gap-12">
{/* 이미지 교체 예정 */}
<div className="aspect-[4/3] w-full rounded-xl bg-muted shadow-sm" aria-hidden />
<div className="flex flex-col justify-center">
<h2 className="text-xl font-semibold md:text-2xl">
당신의 재산, 뜻깊은 나눔으로 이어집니다
</h2>
<div className="mt-4 space-y-4 leading-7 text-muted-foreground">
<p>
삶은 언제 끝날지 알 수 없지만, 그 뜻은 사라지지 않아야 합니다. 평생 모은 재산이 내
의도와 다르게 흘러가는 대신, 사회에 의미 있게 쓰이길 바라는 마음을 지켜드립니다.
</p>
<p>
이 서비스는 장기기증처럼{' '}
<b className="text-foreground">생전에 미리 기부 의사를 등록</b>하면, 사후에도{' '}
<b className="text-foreground">안전하고 투명하게</b> 나눔이 이어지도록 돕습니다.
</p>
<p className="text-foreground">
모든 기부 절차는 은행에서 제공하는 <b>공식 기부 기능</b>과 연동되어 처리됩니다.
</p>
</div>
</div>
</div>
</section>
);
}
39 changes: 39 additions & 0 deletions ssh/src/pages/Donation/components/thirdsection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

export default function BankServices() {
return (
<section id="bank-services" className="w-full bg-blue-50">
<div className="mx-auto max-w-screen-xl px-6 py-14">
<h3 className="text-xl font-semibold md:text-2xl">은행에서 제공하는 기부 서비스</h3>

<div className="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2">
<Card className="rounded-xl shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base md:text-lg">고향사랑기부 · KB 국민은행</CardTitle>
</CardHeader>
<CardContent className="text-sm leading-7 text-muted-foreground md:text-base">
<p>
고향과 지역사회를 응원하는 나눔. 거주지가 아닌 지역에도 기부할 수 있는 제도입니다.
</p>
<p className="mt-2">
세액공제 혜택과 답례품을 통해 기부의 기쁨을 두 배로 느껴보세요.
</p>
</CardContent>
</Card>

<Card className="rounded-xl shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base md:text-lg">학교사랑기부 · KB 국민은행</CardTitle>
</CardHeader>
<CardContent className="text-sm leading-7 text-muted-foreground md:text-base">
<p>
작은 참여로 이어지는 큰 나눔. 걸음수·좋아요 미션으로 모은 성과가 학생들을 위한
장학금으로 전달됩니다.
</p>
</CardContent>
</Card>
</div>
</div>
</section>
);
}
15 changes: 13 additions & 2 deletions ssh/src/pages/Donation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import Steps from './components/fourthsection';
import Hero from './components/herosection';
import Meaning from './components/secondsection';
import BankServices from './components/thirdsection';

const DonationPage = () => {
return <h1>재산 기부 페이지</h1>;
return (
<main className="pt-16">
<Hero />
<Meaning />
<BankServices />
<Steps />
</main>
);
};

export default DonationPage;