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
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ module.exports = {
},
],
settings: {
react: { version: 'detect' }, // react 설치 후 버전 명시
react: { version: '18.3.1' },
'import/resolver': {
typescript: { project: './tsconfig.json' },
},
},
ignorePatterns: [
'node_modules/',
Expand Down
12 changes: 12 additions & 0 deletions src/context/appProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReactNode } from 'react';
import AuthProvider from './authProvider';
import ToastProvider from './toastContext/toastContext';

const AppProvider = ({ children }: { children: ReactNode }) => {
return (
<AuthProvider>
<ToastProvider>{children}</ToastProvider>
</AuthProvider>
);
};
export default AppProvider;
79 changes: 79 additions & 0 deletions src/context/authProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { LoginRequest, User, UserRequest, UserRole } from '@/types/user';
import { createContext, ReactNode, useState } from 'react';

/**
* @TODO
* token은 AuthProvider 내부에서만 관리하고 외부에는 파생된 상태(isLogin, user 같은 권한)만 소비하는 설계
* 컴포넌트에서 토큰 문자열을 직접 다룰일을 막을 수 있음 (보안/ 유지보수측면)
* 토큰 저장 위치(Storage, cookie 등)를 변경해야 할때도 컨텍스트 안에서만 토큰이 사용되기 때문에 외부 코드를 건드릴 일이 없음
* token을 간접적으로 사용하는 저장,삭제,업데이트와 같은 액션은 내부 함수로 작성하고 Provider 외부로 노출하면 관리에 용이함
* 외부는 파생된 상태값만 받아서 적절한 권한 처리만 하면 된다.
*
* AuthContextValue의 함수는 전부 void 값으로 지정했으나 구현에 따라
* return 값이 필요할 경우 type/user 에서 맞는 타입 지정
*
* 초반 설계는 AuthContext에서 구현을 하다가
* 추후 AuthContext (토큰,로그인 상태 관리) / UserContext (프로필 전용) 로 관심사 분리 리팩토링 고려
*
*/

type AuthState = {
user: User | null;
isPending: boolean;
};
interface AuthContextValue extends AuthState {
isLogin: boolean;
role: UserRole;
login: (credentials: LoginRequest) => Promise<void>;
logout: () => void;
signup: (data: UserRequest) => Promise<void>;
getUser: () => Promise<void>;
updateUser: (data: Partial<User>) => Promise<void>;
}
const initialState: AuthState = {
user: null,
isPending: true,
};
export const AuthContext = createContext<AuthContextValue | undefined>(undefined);

const AuthProvider = ({ children }: { children: ReactNode }) => {
const [values, setValues] = useState<AuthState>(initialState);
const [token, setToken] = useState<string | null>(null);

const isLogin = !!token;
const role: UserRole = !isLogin
? 'guest'
: values.user?.type === 'employer'
? 'employer'
: 'employee';

const login: AuthContextValue['login'] = async credentials => {
// TODO: 로그인 구현 (API 요청 후 setValues, setToken)
};
const logout: AuthContextValue['logout'] = () => {
// TODO: 로그아웃 구현 (setValues, setToken 초기화)
};
const signup: AuthContextValue['signup'] = async data => {
// TODO: 회원가입 구현
};
const getUser: AuthContextValue['getUser'] = async () => {
// TODO: 유저 조회 구현
};
const updateUser: AuthContextValue['updateUser'] = async data => {
// TODO: 유저 업데이트 구현
};

const value: AuthContextValue = {
...values,
isLogin,
role,
login,
logout,
signup,
getUser,
updateUser,
};

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export default AuthProvider;
38 changes: 38 additions & 0 deletions src/context/mockAuthProvider/authRolePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import useAuth from '@/hooks/useAuth';

export default function AuthRolePreview() {
const { role, user } = useAuth();

if (role === 'employer') {
return (
<div style={{ padding: 12, border: '1px solid #ccc', borderRadius: 8 }}>
<h2>사장님 전용 화면</h2>
<p>role === 'employer'</p>
<p>
<strong>{user?.name}</strong>님의 가게 관리 화면
</p>
</div>
);
}

if (role === 'employee') {
return (
<div style={{ padding: 12, border: '1px solid #ccc', borderRadius: 8 }}>
<h2>알바생 전용 화면</h2>
<p>role === 'employee'</p>
<p>
<strong>{user?.name}</strong>님의 프로필 화면
</p>
</div>
);
}

return (
<div>
<div style={{ padding: 12, border: '1px solid #ccc', borderRadius: 8 }}>
<h2>비로그인 상태 (게스트 UI)</h2>
<p>role === 'guest'</p>
</div>
</div>
);
}
69 changes: 69 additions & 0 deletions src/context/mockAuthProvider/authState.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { User } from '@/types/user';
import type { Meta, StoryObj } from '@storybook/react';
import AuthRolePreview from './authRolePreview';
import { MockAuthProvider } from './mockAuthProvider';

const employerUser: User = {
id: '1',
email: '[email protected]',
type: 'employer',
name: '김사장',
shop: {
item: {
id: 'shop-1',
name: '김사장 카페',
category: '카페',
address1: '서울',
address2: '101호',
description: '테스트 카페',
imageUrl: 'https://picsum.photos/200',
originalHourlyPay: 10000,
},
},
};

const employeeUser: User = {
id: '2',
email: '[email protected]',
type: 'employee',
name: '이알바',
shop: null,
};

interface PlaygroundArgs {
role: 'guest' | 'employer' | 'employee';
}

const meta: Meta<typeof AuthRolePreview> = {
title: 'Auth/AuthRolePreview',
component: AuthRolePreview,
tags: ['autodocs'],
argTypes: {
role: {
control: 'select',
options: ['guest', 'employer', 'employee'],
},
},
};
export default meta;

type Story = StoryObj<PlaygroundArgs>;

export const Playground: Story = {
args: {
role: 'guest', // ✅ 기본값 지정
},
decorators: [
(Story, context) => {
const { role } = context.args as PlaygroundArgs;

const user = role === 'employer' ? employerUser : role === 'employee' ? employeeUser : null;

return (
<MockAuthProvider role={role} user={user}>
<Story />
</MockAuthProvider>
);
},
],
};
25 changes: 25 additions & 0 deletions src/context/mockAuthProvider/mockAuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AuthContext } from '@/context/authProvider';
import type { User, UserRole } from '@/types/user';
import { ReactNode } from 'react';

interface MockAuthProviderProps {
user: User | null;
role?: UserRole; // guest | employer | employee
children: ReactNode;
}

export const MockAuthProvider = ({ user, role, children }: MockAuthProviderProps) => {
const value = {
user,
isPending: false,
isLogin: role !== 'guest',
role: role ?? (user ? user.type : 'guest'),
login: async () => {},
logout: () => {},
signup: async () => {},
getUser: async () => {},
updateUser: async () => {},
};

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
Empty file removed src/hooks/index.ts
Empty file.
9 changes: 9 additions & 0 deletions src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AuthContext } from '@/context/authProvider';
import { useContext } from 'react';

const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth는 AuthProvider 안에서 사용해야 합니다.');
return context;
};
export default useAuth;
8 changes: 5 additions & 3 deletions src/lib/axios/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import baseAxios from 'axios';
import baseAxios, { AxiosInstance } from 'axios';

const axiosInstance = baseAxios.create({
const axiosInstance: AxiosInstance = baseAxios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
});
export default axiosInstance;
4 changes: 2 additions & 2 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Header, Wrapper } from '@/components/layout';
import Footer from '@/components/layout/footer/footer';
import AppProvider from '@/context/appProvider';

import ToastProvider from '@/context/toastContext/toastContext';
import '@/styles/fonts.css';
import '@/styles/globals.css';
import type { NextPage } from 'next';
Expand Down Expand Up @@ -31,7 +31,7 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
<link rel='icon' href='/favicon.ico' sizes='any' />
<link rel='icon' href='/favicon.png' type='image/png' sizes='192x192' />
</Head>
<ToastProvider>{getLayout(<Component {...pageProps} />)}</ToastProvider>
<AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>
</>
);
}
10 changes: 9 additions & 1 deletion src/stories/DesignTokens/ColorPalette.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ const colors = [
'gray-300',
'gray-400',
'gray-500',
'gray-600',
'gray-700',
'gray-800',
'gray-900',
'red-100',
'red-200',
'red-300',
'red-400',
'red-500',
'red-600',
'red-700',
'red-800',
'red-900',
'blue-100',
'blue-200',
'green-100',
Expand All @@ -31,7 +39,7 @@ type Story = StoryObj;

export const Palette: Story = {
render: () => (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '1rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: '1rem' }}>
{colors.map(color => (
<div key={color} style={{ textAlign: 'center' }}>
<div
Expand Down
24 changes: 14 additions & 10 deletions src/stories/DesignTokens/Typography.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type { Meta, StoryObj } from '@storybook/nextjs';

const texts = [
{ name: 'caption', label: 'Caption', size: 12 },
{ name: 'body-s', label: 'Body-2 regular', size: 14 },
{ name: 'body-m', label: 'Body-1 bold', size: 16 },
{ name: 'body-l', label: 'Body-1 regular', size: 16 },
{ name: 'modal', label: 'Modal text', size: 18 },
{ name: 'heading-s', label: 'h3', size: 20 },
{ name: 'heading-m', label: 'h2', size: 24 },
{ name: 'heading-l', label: 'h1', size: 28 },
{ name: 'text-caption', label: 'Caption', size: 12, mob: 12 },
{ name: 'text-body-s', label: 'Body-2 regular', size: 14, mob: 14 },
{ name: 'text-body-m', label: 'Body-1 bold', size: 16, mob: 14 },
{ name: 'text-body-l', label: 'Body-1 regular', size: 16, mob: 14 },
{ name: 'text-modal', label: 'Modal', size: 18, mob: 16 },
{ name: 'text-heading-s', label: 'h3', size: 20, mob: 16 },
{ name: 'text-heading-m', label: 'h2', size: 24, mob: 18 },
{ name: 'text-heading-l', label: 'h1', size: 28, mob: 20 },
];


const meta: Meta = {
title: 'Design Tokens/Typography',
};
Expand All @@ -22,8 +23,11 @@ export const TextStyles: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{texts.map(t => (
<p key={t.name} className={`text-${t.name}`}>
text-{t.name} : {t.size}px - 피그마 기준: {t.label}
<p key={t.name} className={`${t.name} flex gap-1`}>
<span className='w-60'>{t.name}</span>
<span className='w-60'>피그마 폰트명: {t.label}</span>
<span className='w-30'>pc:{t.size}px</span>
<span className='w-30'>mob:{t.size}px</span>
</p>
))}
</div>
Expand Down
Loading