Skip to content
36 changes: 22 additions & 14 deletions apps/web/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,30 @@ import type { Preview } from '@storybook/react-vite';
// @ts-expect-error
import '../src/index.css';

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
const customViewports = {
mobile1: {
name: 'Mobile',
styles: { width: '375px', height: '812px' },
type: 'mobile',
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
type: 'tablet',
},
desktop: {
name: 'Desktop',
styles: { width: '1280px', height: '800px' },
type: 'desktop',
},
};

a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
const preview: Preview = {
parameters: {
viewport: {
options: customViewports,
},
},
};

export default preview;
8 changes: 3 additions & 5 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import OAuthLoginPage from './features/auth/ui/OAuthLoginPage';

function App() {
return (
<div>
<h1>Locus</h1>
</div>
);
return <OAuthLoginPage />;
}

export default App;
13 changes: 13 additions & 0 deletions apps/web/src/features/auth/data/oauthLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { OAuthProvider } from '@/shared/types';

/**
* OAuth 로그인 처리 함수
* @param provider - OAuth 제공자 (google, naver, kakao)
*/
export const handleOAuthLogin = (_provider: OAuthProvider) => {
// TODO: OAuth 로그인 로직 구현
// 1. OAuth 제공자별 인증 URL 생성
// 2. 사용자를 인증 페이지로 리다이렉트
// 3. 콜백 처리 및 토큰 저장
void _provider;
};
34 changes: 34 additions & 0 deletions apps/web/src/features/auth/ui/OAuthLoginPage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import OAuthLoginPage from './OAuthLoginPage';

const meta = {
title: 'Features/Auth/OAuthLoginPage',
component: OAuthLoginPage,
parameters: {
layout: 'fullscreen',
},
tags: ['autodocs'],
} satisfies Meta<typeof OAuthLoginPage>;

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

export const Default: Story = {};

export const Mobile: Story = {
globals: {
viewport: { value: 'mobile1', isRotated: false },
},
};

export const Tablet: Story = {
globals: {
viewport: { value: 'tablet', isRotated: false },
},
};

export const Desktop: Story = {
globals: {
viewport: { value: 'desktop', isRotated: false },
},
};
59 changes: 59 additions & 0 deletions apps/web/src/features/auth/ui/OAuthLoginPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Logo from '@/shared/icons/Logo';
import OAuthLoginButton from '@/shared/ui/button/OAuthLoginButton';
import { handleOAuthLogin } from '../data/oauthLogin';

function LoginHeader() {
return (
<div className="flex flex-col items-center gap-3 sm:gap-4">
<Logo className="w-24 h-24 sm:w-[140px] sm:h-[140px]" />
<p className="text-gray-500 text-xs sm:text-sm px-4 text-center">
기억을 장소로 기록하고, 연결하다
</p>
</div>
);
}

function OAuthLoginSection() {
return (
<div className="relative flex items-center py-3 sm:py-4">
<div className="grow border-t border-gray-300"></div>
<span className="shrink mx-3 sm:mx-4 text-xs sm:text-sm text-gray-500 bg-white">
소셜 계정으로 로그인
</span>
<div className="grow border-t border-gray-300"></div>
</div>
);
}

export default function OAuthLoginPage() {
return (
<div className="min-h-screen bg-white flex flex-col items-center justify-center px-4 py-8 sm:py-12">
<div className="w-full max-w-md space-y-6 sm:space-y-8">
<LoginHeader />
<OAuthLoginSection />

{/* OAuth Login Buttons */}
<div className="space-y-2.5 sm:space-y-3">
<OAuthLoginButton
provider="google"
onClick={() => {
handleOAuthLogin('google');
}}
/>
<OAuthLoginButton
provider="naver"
onClick={() => {
handleOAuthLogin('naver');
}}
/>
<OAuthLoginButton
provider="kakao"
onClick={() => {
handleOAuthLogin('kakao');
}}
/>
</div>
</div>
</div>
);
}
53 changes: 0 additions & 53 deletions apps/web/src/shared/icons/ErrorIcon.stories.tsx

This file was deleted.

33 changes: 33 additions & 0 deletions apps/web/src/shared/icons/GoogleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { IconProps } from '../types';

export default function GoogleIcon({ className, ...props }: IconProps) {
return (
<svg
{...props}
className={className}
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
>
<path
d="M18.162 10.2225C18.162 9.59678 18.108 9.13199 17.9911 8.6532H9.99561V11.5009H14.6925C14.5976 12.2806 14.0808 13.4631 12.9353 14.2547L12.9193 14.3607L15.4722 16.3368L15.6491 16.3548C17.2734 14.8555 18.211 12.6494 18.211 10.2225"
fill="#4285F4"
/>
<path
d="M9.99545 18.1737C12.3174 18.1737 14.2655 17.41 15.6889 16.1016L12.9751 14.0015C12.2454 14.5053 11.2629 14.8592 9.99545 14.8592C7.71946 14.8592 5.78731 13.3598 5.09961 11.2977L4.99865 11.3067L2.34382 13.3608L2.30884 13.4578C3.72821 16.3625 6.61494 18.1737 9.99545 18.1737Z"
fill="#34A853"
/>
<path
d="M5.09971 10.2985C4.91679 9.75576 4.81283 9.17502 4.81283 8.56928C4.81283 7.96455 4.91679 7.38381 5.09171 6.84105L5.08671 6.7281L2.3979 4.64001L2.30993 4.682C1.46331 6.17934 0.999512 7.76964 0.999512 9.56884C0.999512 11.3681 1.46331 12.9574 2.30993 14.4557L5.09971 12.2966"
fill="#FBBC05"
/>
<path
d="M9.99555 4.27915C11.6118 4.27915 12.7024 4.97784 13.3241 5.56158L15.753 3.19063C14.2577 1.87121 12.3175 0.90863 9.99555 0.90863C6.61504 0.90863 3.72831 2.71883 2.30994 5.62156L5.09171 7.77961C5.78741 5.77849 7.71956 4.27915 9.99555 4.27915Z"
fill="#EB4335"
/>
</svg>
);
}
122 changes: 122 additions & 0 deletions apps/web/src/shared/icons/Icons.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import ErrorIcon from './ErrorIcon';
import RefreshIcon from './RefreshIcon';
import GoogleIcon from './GoogleIcon';
import NaverIcon from './NaverIcon';
import KakaoIcon from './KakaoIcon';
import Logo from './Logo';

const meta = {
title: 'Shared/Icons',
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
} satisfies Meta;

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

interface IconCardProps {
name: string;
icon: React.ReactNode;
description?: string;
}

function IconCard({ name, icon, description }: IconCardProps) {
return (
<div className="flex flex-col items-center gap-3 p-6 border border-gray-200 rounded-lg bg-white hover:shadow-md transition-shadow">
<div className="flex items-center justify-center w-16 h-16 bg-gray-50 rounded-lg">
{icon}
</div>
<div className="text-center">
<h3 className="text-sm font-semibold text-gray-900">{name}</h3>
{description && (
<p className="text-xs text-gray-500 mt-1">{description}</p>
)}
</div>
</div>
);
}

export const IconCatalog: Story = {
render: () => (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
아이콘 카탈로그
</h2>
<p className="text-gray-600">
프로젝트에서 사용 가능한 모든 아이콘을 확인할 수 있습니다.
</p>
</div>

<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">
시스템 아이콘
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<IconCard
name="ErrorIcon"
icon={<ErrorIcon className="w-8 h-8 text-red-500" />}
description="에러 상태 표시용 아이콘"
/>
<IconCard
name="RefreshIcon"
icon={<RefreshIcon className="w-8 h-8 text-blue-500" />}
description="새로고침/재시도 아이콘"
/>
</div>
</div>

<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">
소셜 로그인 아이콘
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<IconCard
name="GoogleIcon"
icon={<GoogleIcon className="w-8 h-8" />}
description="Google 로그인 아이콘"
/>
<IconCard
name="NaverIcon"
icon={<NaverIcon className="w-8 h-8" />}
description="Naver 로그인 아이콘"
/>
<IconCard
name="KakaoIcon"
icon={<KakaoIcon className="w-8 h-8" />}
description="Kakao 로그인 아이콘"
/>
</div>
</div>

<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">
브랜드 아이콘
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<IconCard
name="Logo"
icon={<Logo className="w-16 h-16" />}
description="Locus 로고"
/>
</div>
</div>
</div>
),
};

export const AllIcons: Story = {
render: () => (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<IconCard name="ErrorIcon" icon={<ErrorIcon className="w-8 h-8" />} />
<IconCard name="RefreshIcon" icon={<RefreshIcon className="w-8 h-8" />} />
<IconCard name="GoogleIcon" icon={<GoogleIcon className="w-8 h-8" />} />
<IconCard name="NaverIcon" icon={<NaverIcon className="w-8 h-8" />} />
<IconCard name="KakaoIcon" icon={<KakaoIcon className="w-8 h-8" />} />
<IconCard name="Logo" icon={<Logo className="w-16 h-16" />} />
</div>
),
};
21 changes: 21 additions & 0 deletions apps/web/src/shared/icons/KakaoIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { IconProps } from '../types';

export default function KakaoIcon({ className, ...props }: IconProps) {
return (
<svg
{...props}
className={className}
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
>
<path
d="M9.99563 2.99866C5.57757 2.99866 1.99915 5.90738 1.99915 9.4958C1.99915 11.5749 3.27858 13.4141 5.26771 14.5536L4.53803 17.3024C4.48805 17.4823 4.66797 17.6322 4.8279 17.5423L8.08647 15.6531C8.70619 15.763 9.34591 15.823 9.99563 15.823C14.4137 15.823 17.9921 12.9143 17.9921 9.32587C17.9921 5.73745 14.4137 2.99866 9.99563 2.99866Z"
fill="#3C1E1E"
/>
</svg>
);
}
Loading