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 (
+
+ );
+}