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
65 changes: 65 additions & 0 deletions public/assets/ajou-logo-text.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions src/shared/ui/footer/footer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { Footer } from "./footer";

const meta = {
title: "Shared/UI/Footer",
component: Footer,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
} satisfies Meta<typeof Footer>;

export default meta;
type Story = StoryObj<typeof meta>;

// 1. 기본 (default props)
export const Default: Story = {};

// 2. 커스텀 링크
export const CustomLinks: Story = {
args: {
links: [
{ label: "이용약관", href: "/terms" },
{ label: "개인정보처리방침", href: "/privacy" },
],
},
};

// 3. 커스텀 주소 / 연락처
export const CustomInfo: Story = {
args: {
address: "서울특별시 강남구 테헤란로 123",
phone: "T. 02-000-0000",
copyright: "© 2025 AIM AJOU. All rights reserved.",
},
};

// 4. 화이트 배경 위에서 확인
export const OnWhiteBackground: Story = {
parameters: {
backgrounds: { default: "white" },
},
};
97 changes: 97 additions & 0 deletions src/shared/ui/footer/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Image from "next/image";
import Link from "next/link";

// --- Types ---
export type FooterLink = {
label: string;
href: string;
};

export type FooterProps = {
links?: FooterLink[];
address?: string;
phone?: string;
copyright?: string;
className?: string;
};

// --- Styles ---
const footerBaseClasses =
"w-full bg-[color:var(--color-gray-50,#f9f9f9)] border-t border-[color:var(--color-gray-200,#e5e5ec)]";

const getFooterClasses = (className?: string) =>
[footerBaseClasses, className].filter(Boolean).join(" ");

const contentClasses = "mx-auto max-w-[1440px] py-5";

const infoTextClasses =
"text-[14px] leading-[1.6] tracking-[-0.35px] text-[color:var(--color-gray-500,#808080)]";

const linkClasses =
"underline text-[14px] font-bold leading-[1.6] tracking-[-0.35px] text-[color:var(--color-gray-500,#808080)] hover:text-[color:var(--color-gray-700,#666)] transition-colors";

const copyrightClasses = "text-[12px] leading-[1.6] text-[color:var(--color-gray-400,#b3b3b3)]";

// --- Default Values ---
const DEFAULT_LINKS: FooterLink[] = [
{ label: "이용약관", href: "/terms" },
{ label: "개인정보처리방침", href: "/privacy" },
{ label: "사이트맵", href: "/sitemap" },
];

const DEFAULT_ADDRESS = "16499 경기도 수원시 영통구 월드컵로 206 아주대학교";
const DEFAULT_PHONE = "T. 031-219-2114";
const DEFAULT_COPYRIGHT = "© 2025 AJOU University. All rights reserved.";

// --- Component ---
export const Footer = ({
links = DEFAULT_LINKS,
address = DEFAULT_ADDRESS,
phone = DEFAULT_PHONE,
copyright = DEFAULT_COPYRIGHT,
className,
}: FooterProps) => {
const footerClasses = getFooterClasses(className);

return (
<footer className={footerClasses}>
<div className={contentClasses}>
<div className="flex items-end justify-between gap-6 flex-col sm:flex-row">
{/* Left: Logo + Info */}
<div className="flex items-center gap-10 flex-col sm:flex-row">
{/* Ajou Logo */}
<div className="relative shrink-0 h-15 w-58">
<Image
src="/assets/ajou-logo-text.svg"
alt="Ajou University Logo"
fill
className="object-contain"
/>
</div>

{/* Info */}
<div className="flex flex-col gap-4">
{/* Policy Links */}
<div className="flex items-center gap-4 flex-wrap">
{links.map((link) => (
<Link key={link.href} href={link.href} className={linkClasses}>
{link.label}
</Link>
))}
</div>

{/* Address & Copyright */}
<div className={infoTextClasses}>
<p>{address}</p>
<p>{phone}</p>
<p className={copyrightClasses}>{copyright}</p>
</div>
</div>
</div>
</div>
</div>
</footer>
);
};

Footer.displayName = "Footer";
2 changes: 2 additions & 0 deletions src/shared/ui/footer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Footer } from "./footer";
export type { FooterProps, FooterLink } from "./footer";
10 changes: 10 additions & 0 deletions src/shared/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,15 @@ export { Pagination } from "./pagination/pagination";
export type { PaginationProps } from "./pagination/pagination";
export { Navigation } from "./navigation/navigation";
export type { NavigationProps, NavItem, NavUser } from "./navigation/navigation";
export { Footer } from "./footer/footer";
export type { FooterProps, FooterLink } from "./footer/footer";

// Form components
export * from "./form";
export * from "./checkbox";
export * from "./file-uploader";

// Modal components
export * from "./modal";
export { Tabs } from "./tabs/tabs";
export type { TabsProps, TabItem, TabVariant } from "./tabs/tabs";
8 changes: 4 additions & 4 deletions src/shared/ui/modal/modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const FilterModalExample = ({
isOpen: propsIsOpen,
onClose: propsOnClose,
}: {
isOpen?: boolean;
onClose?: () => void;
isOpen: boolean;
onClose: () => void;
}) => {
const [internalIsOpen, setInternalIsOpen] = useState(false);
const isControlled = propsIsOpen !== undefined;
Expand Down Expand Up @@ -92,8 +92,8 @@ const ProfileEditModalExample = ({
isOpen: propsIsOpen,
onClose: propsOnClose,
}: {
isOpen?: boolean;
onClose?: () => void;
isOpen: boolean;
onClose: () => void;
}) => {
const [internalIsOpen, setInternalIsOpen] = useState(false);
const isControlled = propsIsOpen !== undefined;
Expand Down