diff --git a/apps/web/.storybook/preview.ts b/apps/web/.storybook/preview.ts index 490a614..59013d4 100644 --- a/apps/web/.storybook/preview.ts +++ b/apps/web/.storybook/preview.ts @@ -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; diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e75d674..97cd818 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,9 +1,7 @@ +import OAuthLoginPage from './features/auth/ui/OAuthLoginPage'; + function App() { - return ( -
-

Locus

-
- ); + return ; } export default App; diff --git a/apps/web/src/features/auth/data/oauthLogin.ts b/apps/web/src/features/auth/data/oauthLogin.ts new file mode 100644 index 0000000..8eed548 --- /dev/null +++ b/apps/web/src/features/auth/data/oauthLogin.ts @@ -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; +}; diff --git a/apps/web/src/features/auth/ui/OAuthLoginPage.stories.tsx b/apps/web/src/features/auth/ui/OAuthLoginPage.stories.tsx new file mode 100644 index 0000000..b8eee12 --- /dev/null +++ b/apps/web/src/features/auth/ui/OAuthLoginPage.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +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 }, + }, +}; diff --git a/apps/web/src/features/auth/ui/OAuthLoginPage.tsx b/apps/web/src/features/auth/ui/OAuthLoginPage.tsx new file mode 100644 index 0000000..a01c9da --- /dev/null +++ b/apps/web/src/features/auth/ui/OAuthLoginPage.tsx @@ -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 ( +
+ +

+ 기억을 장소로 기록하고, 연결하다 +

+
+ ); +} + +function OAuthLoginSection() { + return ( +
+
+ + 소셜 계정으로 로그인 + +
+
+ ); +} + +export default function OAuthLoginPage() { + return ( +
+
+ + + + {/* OAuth Login Buttons */} +
+ { + handleOAuthLogin('google'); + }} + /> + { + handleOAuthLogin('naver'); + }} + /> + { + handleOAuthLogin('kakao'); + }} + /> +
+
+
+ ); +} diff --git a/apps/web/src/shared/icons/ErrorIcon.stories.tsx b/apps/web/src/shared/icons/ErrorIcon.stories.tsx deleted file mode 100644 index 543c820..0000000 --- a/apps/web/src/shared/icons/ErrorIcon.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import ErrorIcon from './ErrorIcon'; - -const meta = { - title: 'Shared/Icons/ErrorIcon', - component: ErrorIcon, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], - argTypes: { - className: { - control: 'text', - description: 'Tailwind CSS classes for styling (size, color, etc.)', - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; - -export const WithDifferentColors: Story = { - render: () => ( -
- - - - -
- ), -}; - -export const InContext: Story = { - render: () => ( -
-
- -
-
-

- 일시적인 문제가 발생했어요 -

-

- 잠시 후 다시 시도해 주세요 -

-
-
- ), -}; diff --git a/apps/web/src/shared/icons/GoogleIcon.tsx b/apps/web/src/shared/icons/GoogleIcon.tsx new file mode 100644 index 0000000..125fef5 --- /dev/null +++ b/apps/web/src/shared/icons/GoogleIcon.tsx @@ -0,0 +1,33 @@ +import type { IconProps } from '../types'; + +export default function GoogleIcon({ className, ...props }: IconProps) { + return ( + + ); +} diff --git a/apps/web/src/shared/icons/Icons.stories.tsx b/apps/web/src/shared/icons/Icons.stories.tsx new file mode 100644 index 0000000..1eb667e --- /dev/null +++ b/apps/web/src/shared/icons/Icons.stories.tsx @@ -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; + +interface IconCardProps { + name: string; + icon: React.ReactNode; + description?: string; +} + +function IconCard({ name, icon, description }: IconCardProps) { + return ( +
+
+ {icon} +
+
+

{name}

+ {description && ( +

{description}

+ )} +
+
+ ); +} + +export const IconCatalog: Story = { + render: () => ( +
+
+

+ 아이콘 카탈로그 +

+

+ 프로젝트에서 사용 가능한 모든 아이콘을 확인할 수 있습니다. +

+
+ +
+

+ 시스템 아이콘 +

+
+ } + description="에러 상태 표시용 아이콘" + /> + } + description="새로고침/재시도 아이콘" + /> +
+
+ +
+

+ 소셜 로그인 아이콘 +

+
+ } + description="Google 로그인 아이콘" + /> + } + description="Naver 로그인 아이콘" + /> + } + description="Kakao 로그인 아이콘" + /> +
+
+ +
+

+ 브랜드 아이콘 +

+
+ } + description="Locus 로고" + /> +
+
+
+ ), +}; + +export const AllIcons: Story = { + render: () => ( +
+ } /> + } /> + } /> + } /> + } /> + } /> +
+ ), +}; diff --git a/apps/web/src/shared/icons/KakaoIcon.tsx b/apps/web/src/shared/icons/KakaoIcon.tsx new file mode 100644 index 0000000..e0d6a40 --- /dev/null +++ b/apps/web/src/shared/icons/KakaoIcon.tsx @@ -0,0 +1,21 @@ +import type { IconProps } from '../types'; + +export default function KakaoIcon({ className, ...props }: IconProps) { + return ( + + ); +} diff --git a/apps/web/src/shared/icons/Logo.tsx b/apps/web/src/shared/icons/Logo.tsx new file mode 100644 index 0000000..bba8a12 --- /dev/null +++ b/apps/web/src/shared/icons/Logo.tsx @@ -0,0 +1,95 @@ +import type { IconProps } from '../types'; + +export default function Logo({ className, ...props }: IconProps) { + return ( + + ); +} diff --git a/apps/web/src/shared/icons/NaverIcon.tsx b/apps/web/src/shared/icons/NaverIcon.tsx new file mode 100644 index 0000000..cda916b --- /dev/null +++ b/apps/web/src/shared/icons/NaverIcon.tsx @@ -0,0 +1,36 @@ +import type { IconProps } from '../types'; + +export default function NaverIcon({ className, ...props }: IconProps) { + return ( + + ); +} diff --git a/apps/web/src/shared/icons/RefreshIcon.stories.tsx b/apps/web/src/shared/icons/RefreshIcon.stories.tsx deleted file mode 100644 index 88429d9..0000000 --- a/apps/web/src/shared/icons/RefreshIcon.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import RefreshIcon from './RefreshIcon'; - -const meta = { - title: 'Shared/Icons/RefreshIcon', - component: RefreshIcon, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], - argTypes: { - className: { - control: 'text', - description: 'Tailwind CSS classes for styling (size, color, etc.)', - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; - -export const WithDifferentColors: Story = { - render: () => ( -
- - - - -
- ), -}; - -export const Interactive: Story = { - args: { - className: - 'w-8 h-8 text-blue-500 cursor-pointer hover:text-blue-700 transition-colors', - onClick: () => alert('Refresh clicked!'), - }, -}; diff --git a/apps/web/src/shared/icons/index.ts b/apps/web/src/shared/icons/index.ts index 5716f22..ed9da27 100644 --- a/apps/web/src/shared/icons/index.ts +++ b/apps/web/src/shared/icons/index.ts @@ -1,2 +1,6 @@ export { default as ErrorIcon } from './ErrorIcon'; export { default as RefreshIcon } from './RefreshIcon'; +export { default as GoogleIcon } from './GoogleIcon'; +export { default as NaverIcon } from './NaverIcon'; +export { default as KakaoIcon } from './KakaoIcon'; +export { default as Logo } from './Logo'; diff --git a/apps/web/src/shared/types/index.ts b/apps/web/src/shared/types/index.ts index b4d5216..2dca7a9 100644 --- a/apps/web/src/shared/types/index.ts +++ b/apps/web/src/shared/types/index.ts @@ -1,2 +1,3 @@ export type { IconProps } from './icon'; export type { LoadingPageProps, LoadingPageVersion } from './loading'; +export type { OAuthProvider } from './oauth'; diff --git a/apps/web/src/shared/types/oauth.ts b/apps/web/src/shared/types/oauth.ts new file mode 100644 index 0000000..e27696d --- /dev/null +++ b/apps/web/src/shared/types/oauth.ts @@ -0,0 +1 @@ +export type OAuthProvider = 'google' | 'naver' | 'kakao'; diff --git a/apps/web/src/shared/ui/button/OAuthLoginButton.stories.tsx b/apps/web/src/shared/ui/button/OAuthLoginButton.stories.tsx new file mode 100644 index 0000000..c48c98a --- /dev/null +++ b/apps/web/src/shared/ui/button/OAuthLoginButton.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import OAuthLoginButton from './OAuthLoginButton'; + +const meta = { + title: 'Shared/UI/Button/OAuthLoginButton', + component: OAuthLoginButton, + parameters: { + layout: 'centered', + actions: { argTypesRegex: undefined }, + }, + tags: ['autodocs'], + argTypes: { + provider: { + control: 'select', + options: ['google', 'naver', 'kakao'], + description: 'OAuth 제공자 선택', + }, + onClick: { + action: 'clicked', + description: '버튼 클릭 핸들러 (Storybook Actions 패널에 기록)', + }, + className: { + control: 'text', + description: '추가 CSS 클래스', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Google: Story = { + args: { + provider: 'google', + }, +}; + +export const Naver: Story = { + args: { + provider: 'naver', + }, +}; + +export const Kakao: Story = { + args: { + provider: 'kakao', + }, +}; + +export const AllProviders: Story = { + args: { + provider: 'google', // 타입 만족용 (render에서는 직접 provider 지정) + }, + render: (args) => ( +
+ + + +
+ ), + parameters: { + layout: 'padded', + controls: { disable: true }, + docs: { + description: { + story: + 'Google / Naver / Kakao OAuth 버튼을 한 화면에서 비교합니다. 클릭 이벤트는 Actions 패널에 기록됩니다.', + }, + }, + }, +}; diff --git a/apps/web/src/shared/ui/button/OAuthLoginButton.tsx b/apps/web/src/shared/ui/button/OAuthLoginButton.tsx new file mode 100644 index 0000000..de19a75 --- /dev/null +++ b/apps/web/src/shared/ui/button/OAuthLoginButton.tsx @@ -0,0 +1,63 @@ +import GoogleIcon from '@/shared/icons/GoogleIcon'; +import NaverIcon from '@/shared/icons/NaverIcon'; +import KakaoIcon from '@/shared/icons/KakaoIcon'; +import type { OAuthProvider } from '@/shared/types'; + +interface OAuthLoginButtonProps { + provider: OAuthProvider; + onClick?: () => void; + className?: string; +} + +const providerConfig = { + google: { + icon: GoogleIcon, + text: 'Google로 로그인', + bgColor: 'bg-[#F3F4F6]', + textColor: 'text-gray-900', + hoverBgColor: 'hover:bg-gray-200', + }, + naver: { + icon: NaverIcon, + text: 'Naver로 로그인', + bgColor: 'bg-[#02C75A]', + textColor: 'text-white', + hoverBgColor: 'hover:bg-[#02B350]', + }, + kakao: { + icon: KakaoIcon, + text: 'Kakao로 로그인', + bgColor: 'bg-[#FEE501]', + textColor: 'text-[#3C1E1E]', + hoverBgColor: 'hover:bg-[#FDD835]', + }, +} as const; + +export default function OAuthLoginButton({ + provider, + onClick, + className = '', +}: OAuthLoginButtonProps) { + const config = providerConfig[provider]; + const Icon = config.icon; + + return ( + + ); +}