Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature #84 auth #86

Merged
merged 6 commits into from
Nov 19, 2024
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
49 changes: 25 additions & 24 deletions apps/frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,91 +17,92 @@ export default tseslint.config(
languageOptions: {
parser: typescriptParser,
ecmaVersion: 2020,
globals: globals.browser,
globals: globals.browser
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'no-relative-import-paths': noRelativeImportPathsPlugin,
import: eslintImport,
prettier: prettierPlugin,
prettier: prettierPlugin
},
rules: {
...reactHooks.configs.recommended.rules,
'prettier/prettier': 'error', // Prettier 규칙을 ESLint에서 에러로 표시
'@typescript-eslint/no-unnecessary-type-constraint': 'off',
'import/order': [
'error',
{
groups: [
['builtin', 'external'], // 내장 모듈과 외부 모듈 그룹
['internal', 'parent', 'sibling', 'index'], // 내부 모듈 그룹
['internal', 'parent', 'sibling', 'index'] // 내부 모듈 그룹
],
pathGroups: [
{
pattern: 'react',
group: 'builtin',
position: 'before', // react를 최상위에 오도록 설정
position: 'before' // react를 최상위에 오도록 설정
},
{
pattern: 'react-dom',
group: 'builtin',
position: 'before',
},
position: 'before'
}
],
pathGroupsExcludedImportTypes: ['builtin'],
alphabetize: { order: 'asc', caseInsensitive: true }, // 알파벳 순 정렬
},
alphabetize: { order: 'asc', caseInsensitive: true } // 알파벳 순 정렬
}
],
'sort-imports': [
'error',
{
ignoreDeclarationSort: true, // `import` 자체의 정렬은 무시
ignoreMemberSort: false, // 세부 항목 정렬은 적용
},
ignoreMemberSort: false // 세부 항목 정렬은 적용
}
],
'no-relative-import-paths/no-relative-import-paths': [
'warn',
{ allowSameFolder: true, rootDir: 'src', prefix: '@' },
{ allowSameFolder: true, rootDir: 'src', prefix: '@' }
],
'@typescript-eslint/naming-convention': [
'warn',
{
selector: 'variable',
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
format: ['camelCase', 'PascalCase', 'UPPER_CASE']
}, // 변수명은 camelCase, PascalCase, UPPER_CASE 형식 중 하나여야 함
{
selector: 'function',
format: ['camelCase', 'PascalCase'],
format: ['camelCase', 'PascalCase']
}, // 함수명은 camelCase, PascalCase 형식 중 하나여야 함
{
selector: 'typeLike',
format: ['PascalCase'],
format: ['PascalCase']
}, // 타입명은 PascalCase 형식이어야 함
{
selector: 'interface',
format: ['PascalCase'],
custom: {
regex: '^I[A-Z]',
match: false,
},
match: false
}
}, // 인터페이스명은 PascalCase이고 I로 시작하면 안됨
{
selector: 'typeAlias',
format: ['PascalCase'],
custom: {
regex: '^T[A-Z]',
match: false,
},
match: false
}
}, // 타입 별칭명은 PascalCase이고 T로 시작하면 안됨
{
selector: 'typeParameter',
format: ['PascalCase'],
custom: {
regex: '^T[A-Z]',
match: false,
},
}, // 타입 매개변수명은 PascalCase이고 T로 시작하면 안됨
],
},
},
match: false
}
} // 타입 매개변수명은 PascalCase이고 T로 시작하면 안됨
]
}
}
);
34 changes: 20 additions & 14 deletions apps/frontend/src/app/mock/userResolvers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { DefaultBodyType, HttpResponse, StrictRequest } from 'msw';

const MOCK_UUID = 'mock-uuid';

// github 사용자 기본 정보 조회 api
export const mockGetUserInfo = () => {
export const mockGetUserInfo = ({ request }: { request: StrictRequest<DefaultBodyType> }) => {
const authorization = request.headers.get('Authorization');

const [type, token] = authorization?.split(' ') || [];

console.log('type', type, 'token', token, 'MOCK_UUID', MOCK_UUID, token === MOCK_UUID);

if (token !== MOCK_UUID || type !== 'Bearer') {
return new HttpResponse('Unauthorized: Invalid or missing token', {
status: 401,
headers: {
'Content-Type': 'text/plain'
}
});
}

return HttpResponse.json({
id: '1',
nickname: 'mockUser',
Expand Down Expand Up @@ -48,20 +65,9 @@ export const mockPatchUserInfo = async ({ request }: { request: StrictRequest<De
};

// 로그인 api
export const mockLogin = ({ request }: { request: StrictRequest<DefaultBodyType> }) => {
const authorization = request.headers.get('Authorization');

if (!authorization || !authorization.startsWith('Bearer ')) {
return new HttpResponse('Unauthorized: Invalid or missing token', {
status: 401,
headers: {
'Content-Type': 'text/plain'
}
});
}

export const mockLogin = () => {
return HttpResponse.json({
token: '가짜 토큰'
token: MOCK_UUID
});
};

Expand Down
25 changes: 16 additions & 9 deletions apps/frontend/src/feature/User/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { UserType } from './type';
import { LotusType } from '@/feature/Lotus/type';

// github 사용자 기본 정보 조회
export const getUserInfo = async () => {
const res = await fetch('/api/user');

const user = await res.json();

return user as UserType;
};
import { api } from '@/shared/utils/api';

// 사용자의 Lotus 목록 조회
export const getUserLotusList = async ({ page = 1, size = 10 }: { page?: number; size?: number }) => {
Expand Down Expand Up @@ -59,3 +51,18 @@ export const getUserGistFile = async ({ gistId }: { gistId: string }) => {

return files as GistFileType[];
};

export const postLogin = async () => {
const res = await api.post<{ token: string }>('/api/user/login');

const data = res.data;

return data;
};

//사용자 기본 정보 조회
export const getUserInfo = async () => {
const res = await api.get<UserType>('/api/user');

return res.data;
};
21 changes: 19 additions & 2 deletions apps/frontend/src/feature/User/query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query';
import { getUserGistFile, getUserGistList, getUserInfo, getUserLotusList } from './api';
import { useMutation, useQuery, useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query';
import { getUserGistFile, getUserGistList, getUserInfo, getUserLotusList, postLogin } from './api';

export const useUserInfoSuspenseQuery = () => {
const query = useSuspenseQuery({
Expand Down Expand Up @@ -40,3 +40,20 @@ export const useUserGistFileSuspenseQuery = ({ gistId }: { gistId: string }) =>

return query;
};

export const useUserQuery = () => {
const query = useQuery({
queryKey: ['user'],
queryFn: getUserInfo
});

return query;
};

export const useLoginMutation = () => {
const mutation = useMutation({
mutationFn: postLogin
});

return mutation;
};
16 changes: 16 additions & 0 deletions apps/frontend/src/shared/hooks/useLocalStorage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';

//TODO : 간단한 로컬스토리지 훅이니까 추후 수정이 필요할 수 있음
export const useLocalStorage = <T extends unknown>({ key, initialValue }: { key: string; initialValue: T }) => {
const [storage, setStorage] = useState(() => {
const item = window.localStorage.getItem(key) || '';

return item ? JSON.parse(item) : initialValue;
});

useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(storage));
}, [key, storage]);

return [storage, setStorage];
};
17 changes: 17 additions & 0 deletions apps/frontend/src/shared/utils/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import axios from 'axios';

export const api = axios.create({
baseURL: '/'
});

api.interceptors.request.use((config) => {
const raw = localStorage.getItem('token');

const token = raw ? JSON.parse(raw) : null;

if (token) {
config.headers.Authorization = `Bearer ${token}`;
}

return config;
});
36 changes: 23 additions & 13 deletions apps/frontend/src/widget/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
import { Button, Heading, Text } from '@froxy/design/components';
import { Button, Heading } from '@froxy/design/components';
import { useNavigate } from '@tanstack/react-router';
import { CreateLotusButton, LoginButton, LogoutButton } from './Navigation';
import { useUserQuery } from '@/feature/User/query';

export function Header() {
// TODO: 로그인 여부 확인하는 로직 추가 필요
const isLogin = false;
const { data } = useUserQuery();

const navigate = useNavigate();

const handleClick = () => {
navigate({ to: '/lotus' });
};

return (
<div className="flex justify-center mb-7 w-full shadow-md">
<header className="flex justify-center mb-7 w-full shadow-md">
<div className="w-full max-w-screen-xl py-5 px-4 sm:px-6 lg:px-8 flex justify-between items-center">
<div className="flex items-center gap-4">
<Button className="flex items-center gap-4" variant={null} onClick={() => handleClick()}>
<img className="w-14 h-14" src="/image/logoIcon.svg" alt="로고" />
<Heading size="lg">Froxy</Heading>
</div>
<div className="flex items-center gap-4">
{isLogin ? (
</Button>
<div className="flex items-center gap-8">
{data ? (
<>
<Button variant={'ghost'}>
<Text variant="muted">create Lotus</Text>
</Button>
<div className="flex items-center">
<CreateLotusButton />
<LogoutButton />
</div>

<img className="w-10 h-10 rounded-full" src="/image/exampleImage.jpeg" alt="프로필 사진" />
</>
) : (
<Button variant={'default'}>Login</Button>
<LoginButton />
)}
</div>
</div>
</div>
</header>
);
}
12 changes: 12 additions & 0 deletions apps/frontend/src/widget/Navigation/CreateLotusButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Button, Text } from '@froxy/design/components';
import { Link } from '@tanstack/react-router';

export function CreateLotusButton() {
return (
<Button variant={'ghost'} asChild>
<Text size="sm" variant="muted" asChild>
<Link to="/lotus/create">Create Lotus</Link>
</Text>
</Button>
);
}
27 changes: 27 additions & 0 deletions apps/frontend/src/widget/Navigation/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Button } from '@froxy/design/components';
import { useQueryClient } from '@tanstack/react-query';
import { useLoginMutation } from '@/feature/User/query';
import { useLocalStorage } from '@/shared/hooks/useLocalStorage';

export function LoginButton() {
const [, set] = useLocalStorage({ key: 'token', initialValue: '' });

const { mutate, isPending } = useLoginMutation();

const queryClient = useQueryClient();

const handleClick = () => {
mutate(undefined, {
onSuccess: ({ token }) => {
set(token);
queryClient.invalidateQueries({ queryKey: ['user'] });
}
});
};

return (
<Button variant={'default'} onClick={handleClick} disabled={isPending}>
Login
</Button>
);
}
27 changes: 27 additions & 0 deletions apps/frontend/src/widget/Navigation/LogoutButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Button, Text } from '@froxy/design/components';
import { useQueryClient } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { useLocalStorage } from '@/shared/hooks/useLocalStorage';

export function LogoutButton() {
const [, set] = useLocalStorage({ key: 'token', initialValue: '' });

const queryClient = useQueryClient();

const navigate = useNavigate();

const handleClick = () => {
set('');
queryClient.invalidateQueries({ queryKey: ['user'] });

navigate({ to: '/' });
};

return (
<Button variant={'ghost'} onClick={handleClick}>
<Text size="sm" variant="muted">
Logout
</Text>
</Button>
);
}
3 changes: 3 additions & 0 deletions apps/frontend/src/widget/Navigation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './CreateLotusButton';
export * from './LoginButton';
export * from './LogoutButton';