Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
dcae0cc
feature: next app init
six-standard Nov 19, 2024
b51a5ee
feature: 14로 다운그레이드
six-standard Nov 19, 2024
ed901ab
feature: init
six-standard Nov 21, 2024
17c92ab
feature: recovered eslint
six-standard Nov 22, 2024
2cfdfee
refactor: dependencies to devDependencies
six-standard Nov 22, 2024
20c3d9c
modify: pnpm peerdeps 자동 설치
six-standard Nov 22, 2024
fedb701
modify: env 무시 추가
six-standard Nov 22, 2024
589f90e
refactor: html lang en -> kr
six-standard Nov 23, 2024
7623ddd
feature: 폰트 적용
six-standard Nov 23, 2024
64101f3
modify: 테스트 일부 수정
six-standard Nov 23, 2024
c9eded9
feature: 컬러셋 추가
six-standard Nov 23, 2024
3354be8
modify: bold 폰트만 적용되는 오류 수정
six-standard Nov 23, 2024
8556144
modify: main 페이지 분리
six-standard Nov 23, 2024
4bf80bd
feature: 기본 입력 컴포넌트 추가
six-standard Nov 23, 2024
08f9cc3
feature: 로그인 페이지 퍼블리싱
six-standard Nov 23, 2024
362191e
refactor: gitignore new line 추가
six-standard Nov 24, 2024
b40b32e
refactor: 타입스크립트 규칙 추가
six-standard Nov 24, 2024
e1b3882
modify: 명령어 변경
six-standard Nov 24, 2024
4b038cc
feature: 필요한 라이브러리 추가
six-standard Nov 24, 2024
2d8affd
modify: readme 적용
six-standard Nov 24, 2024
bd43bb4
modify: strict 규칙 추가
six-standard Nov 24, 2024
226e2ff
feature: .env.simple 추가
six-standard Nov 24, 2024
8f7d9c1
feature: 필요한 api 추가 설치
six-standard Nov 24, 2024
6d9f957
modify: 일부 테스트 추가
six-standard Nov 24, 2024
170fa2c
modify: 로그인 페이지 이벤트 일부 적용
six-standard Nov 24, 2024
9c0faf6
refactor: 레이아웃 코드 정리
six-standard Nov 24, 2024
5ad1263
feature: api 인스턴스 정의
six-standard Nov 24, 2024
fe862c5
other: feature/init과 merge
six-standard Nov 24, 2024
762cc80
feature: 필요한 라이브러리 추가
six-standard Nov 26, 2024
a46e329
feature: Proxy 설정
six-standard Nov 26, 2024
dd783b2
modify: readme.md 수정
six-standard Nov 26, 2024
4eaab5c
feature: fetch mock 설정
six-standard Nov 28, 2024
f517971
feature: Login Test 제작
six-standard Nov 28, 2024
3517391
modify: Proxy 제거
six-standard Nov 28, 2024
aa17e50
feature: queryClient 관련 오류 수정
six-standard Nov 28, 2024
2b7af66
feature: 로그인 페이지 제작
six-standard Nov 28, 2024
5e7683e
modify: SSR / CSR 분리
six-standard Nov 28, 2024
3291549
Merge branch 'main' into feature/login
six-standard Nov 28, 2024
dce2bc3
modify: Input 마스킹 처리
six-standard Nov 28, 2024
bfa0bbc
modify: Metadata 추가
six-standard Nov 28, 2024
7225fec
modify: 토큰 전달 방식 변경
six-standard Nov 28, 2024
a185830
modify: 경로 관련 문제로 삭제
six-standard Nov 28, 2024
9da1f6c
modify: BASE_URL 안정성 강화
six-standard Nov 28, 2024
817b6ea
feature: 임시 요소 제작
six-standard Nov 28, 2024
25d9239
modify: queryClient 옵션 추가
six-standard Nov 28, 2024
8439aef
modify: 필요 없는 test 제거
six-standard Nov 28, 2024
f1d0f70
refactor: 네이밍 규칙 일관화
six-standard Nov 28, 2024
48ff0a7
refactor: 필요없는 텍스트 제거
six-standard Nov 28, 2024
1340245
modify: 일부 밀린 커밋 적용
six-standard Nov 28, 2024
360605b
modify: eslint-disable 제거
six-standard Nov 29, 2024
a76c825
modify: 네이밍 오타
six-standard Nov 29, 2024
bafe062
modify: new line 추가 자동화
six-standard Nov 29, 2024
97cba3f
refactor: 반복된 코드 정리
six-standard Nov 29, 2024
69ac83e
modify: 아직 타임아웃 기능이 없음
six-standard Nov 29, 2024
6959548
modify: include 오류 해결
six-standard Nov 29, 2024
ab6f0cf
modify: 컬러 팔레트 이름 변경
six-standard Nov 29, 2024
05310c0
refactor: 절대경로 설정
six-standard Nov 30, 2024
c1ee3c1
refactor: prop 타입 관련 오류 해결
six-standard Nov 30, 2024
69555fb
refactor: 메인 페이지 제거
six-standard Dec 4, 2024
69113fe
refactor: import 코드 정리
six-standard Dec 4, 2024
3eaf8a0
refactor: 적절한 경로로 이동
six-standard Dec 4, 2024
7516166
refactor: 코드 리팩토링
six-standard Dec 4, 2024
ba46cc1
feature: 서버가 응답하지 않는 케이스 추가
six-standard Dec 6, 2024
6405d65
modify: any 타입 규칙 변경
six-standard Dec 6, 2024
abfcf4e
feature: 서버가 응답하지 않을 경우의 처리 추가
six-standard Dec 6, 2024
1e63dc5
refactor: trunk 추가
six-standard Dec 6, 2024
9a7e354
refactor: mutation 오류 처리 방식 변경
six-standard Dec 6, 2024
566eee1
modify: 컬러 팔레트 관련 오류 해결
six-standard Dec 6, 2024
72d016f
modify: 오류 핸들링 관련 코드 추가
six-standard Dec 6, 2024
76e7ed0
feature: 커스텀 에러 추가
six-standard Dec 6, 2024
fbd255a
refactor: 네이밍 변경
six-standard Dec 6, 2024
9721a9a
Merge branch 'main' into refactor/all
six-standard Dec 6, 2024
12cd7b1
refactor: 시맨틱 태그로 변경
six-standard Dec 6, 2024
ca98243
refactor: ABORT_MS env 값으로 이동
six-standard Dec 7, 2024
930a016
refactor: 오류 명시 변경
six-standard Dec 8, 2024
e58f54b
refactor: 타입 정리
six-standard Dec 8, 2024
a3ac105
refactor: AbortController 인자 제거
six-standard Dec 8, 2024
577ca43
refactor: 중복 검사 코드 제거
six-standard Dec 8, 2024
7ea45b9
refactor: 코드 스타일과 new line
six-standard Dec 8, 2024
42f0211
refactor: 에러 코드 추가
six-standard Dec 8, 2024
3624118
refactor: import 정리
six-standard Dec 8, 2024
4e736f1
refactor: 커스텀 에러 적용
six-standard Dec 8, 2024
e9c338d
refactor: 네이밍 맞춤
six-standard Dec 8, 2024
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
3 changes: 2 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NEXT_PUBLIC_BASE_URL=<server's url here>
NEXT_PUBLIC_BASE_URL=<'server url here'>
NEXT_PUBLIC_ABORT_MS=<'abort time(ms) for fetch here'>
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default [
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
},
languageOptions: {
parserOptions: {
Expand Down
41 changes: 20 additions & 21 deletions src/__test__/login.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { act, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import Page from '@/app/(login)/page';
import fetchMock from 'jest-fetch-mock';
import { renderWithQueryClient } from '@/utils/test-util';
import { act, screen } from '@testing-library/react';
import { ToastContainer } from 'react-toastify';
import { useRouter } from 'next/navigation';
import fetchMock from 'jest-fetch-mock';
import { renderWithQueryClient } from '@/utils';
import { TimeoutError } from '@/errors';
import { Login } from '@/app';

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
Expand All @@ -26,7 +27,7 @@ const renderPage = () => {
renderWithQueryClient(
<>
<ToastContainer autoClose={2000} />
<Page />
<Login />
</>,
);
};
Expand Down Expand Up @@ -58,37 +59,36 @@ describe('로그인 화면에서', () => {
});

describe('API 요청에서', () => {
// it('서버가 응답하지 않으면 오류 토스트가 표기된다', async () => {
// renderPage();
it('서버가 응답하지 않으면 응답 없음 토스트가 표시된다', async () => {
renderPage();

// fetchMock.mockAbortOnce();
fetchMock.mockRejectedValueOnce(new TimeoutError());

// const { buttonEl, accessInputEl, refreshInputEl } = getElements();
const { buttonEl, accessInputEl, refreshInputEl } = getElements();

// await userEvent.type(accessInputEl, 'invalid_access');
// await userEvent.type(refreshInputEl, 'invalid_refresh');
// await act(async () => buttonEl.click());
await userEvent.type(accessInputEl, 'invalid_access');
await userEvent.type(refreshInputEl, 'invalid_refresh');
await act(async () => buttonEl.click());

// const toastEl = screen.getByText('유효하지 않은 토큰 (404)');
// expect(toastEl).toBeInTheDocument();
// });
const toastEl = screen.getByText('잠시 후 다시 시도해 주세요');
expect(toastEl).not.toBeUndefined();
});

it('액세스 토큰이 비정상적이면 오류 토스트가 표기된다', async () => {
renderPage();

fetchMock.mockRejectOnce(new Error());
fetchMock.mockResolvedValueOnce({ ok: false, status: 404 } as Response);

const { buttonEl, accessInputEl, refreshInputEl } = getElements();

await userEvent.type(accessInputEl, 'invalid_access');
await userEvent.type(refreshInputEl, 'invalid_refresh');
await act(async () => buttonEl.click());

const toastEl = screen.getByText('유효하지 않은 토큰 (404)');
expect(toastEl).toBeInTheDocument();
const toastEl = screen.getByText('일치하는 계정을 찾을 수 없습니다');
expect(toastEl).not.toBeUndefined();
});

it('액세스 토큰이 정상적이면 페이지를 대시보드로 이동시킨다', async () => {
it('요청이 성공하면 페이지를 대시보드로 이동시킨다', async () => {
renderPage();

const replace = jest.fn();
Expand All @@ -97,7 +97,6 @@ describe('로그인 화면에서', () => {
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
json: () => ({}),
} as Response);

const { buttonEl, accessInputEl, refreshInputEl } = getElements();
Expand Down
55 changes: 50 additions & 5 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
import returnFetch from 'return-fetch';
import returnFetch, { FetchArgs } from 'return-fetch';
import { ServerNotRespondingError } from '@/errors';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
const ABORT_MS = Number(process.env.NEXT_PUBLIC_ABORT_MS);

if (Number.isNaN(ABORT_MS)) {
throw new Error('ABORT_MS가 ENV에서 설정되지 않았습니다');
}

if (!BASE_URL) {
throw new Error('BASE_URL가 ENV에서 설정되지 않았습니다.');
throw new Error('BASE_URL이 ENV에서 설정되지 않았습니다.');
}

export const instance = returnFetch({
type ErrorObject = Record<string, Error>;

const abortPolyfill = (ms: number) => {
const controller = new AbortController();
setTimeout(() => controller.abort(), ms);
return controller.signal;
};

const fetch = returnFetch({
baseUrl: BASE_URL,
headers: { Accept: 'application/json' },
interceptors: {
response: async (response) => {
if (!response.ok) throw response;
return response;
if (!response.ok) {
throw response;
}
return {
...response,
body: response?.text ? JSON.parse(await response?.text()) : {},
};
},
},
});

export const instance = async (
input: URL | RequestInfo,
init?: Omit<NonNullable<FetchArgs[1]>, 'body'> & { body: object },
error?: ErrorObject,
) => {
try {
const data = await fetch(input, {
...init,
body: init?.body ? JSON.stringify(init.body) : undefined,
signal: AbortSignal.timeout
? AbortSignal.timeout(ABORT_MS)
: abortPolyfill(ABORT_MS),
});

return data as Awaited<ReturnType<typeof fetch>>;
} catch (err: any) {
if ((err as Error).name === 'TimeoutError')
throw new ServerNotRespondingError();
else {
if (!error || !(error && error[`${(err as Response).status}`]))
throw new Error(`서버에서 Z 오류가 발생했습니다. (${err.name})`);
throw error[`${(err as Response).status}`];
}
}
};
53 changes: 31 additions & 22 deletions src/app/(login)/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use client';

import { Button, Input } from '@/components';
import { useForm } from 'react-hook-form';
import { useMutation } from '@tanstack/react-query';
import { instance } from '../../api';
import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify';
import { useForm } from 'react-hook-form';
import { Button, Input } from '@/components';
import { NotFoundError } from '@/errors';
import { instance } from '@/api';

interface formVo {
interface FormVo {
access_token: string;
refresh_token: string;
}
Expand All @@ -19,51 +19,60 @@ export const Content = () => {
register,
handleSubmit,
formState: { isValid },
} = useForm<formVo>({ mode: 'onChange' });
} = useForm<FormVo>({ mode: 'onChange' });

const { mutate } = useMutation({
mutationFn: async (data: formVo) =>
await instance('/login', {
method: 'POST',
headers: {
cookie: `access_token=${data.access_token};refresh_token=${data.refresh_token}`,
mutationFn: async (body: FormVo) =>
await instance(
'/login',
{ method: 'POST', body },
{
'404': new NotFoundError(
'일치하는 계정을 찾을 수 없습니다',
'CannotFindAccount',
),
},
}),
),
onSuccess: () => replace('/main'),
onError: (res: Response) => {
toast.error(`${res.statusText} (${res.status})`);
},
});

const onSubmit = (data: formVo) => {
const onSubmit = (data: FormVo) => {
mutate(data);
};

return (
<div className="w-full h-full flex items-center justify-center">
<main className="w-full h-full flex items-center justify-center">
<form
onSubmit={handleSubmit(onSubmit)}
className="w-fit h-fit flex flex-col gap-[30px] items-center p-[30px] bg-bg-main rounded-[4px] shadow-[0_4px_16px_0_rgba(0,0,0,.04)]"
className="w-fit h-fit flex flex-col gap-[30px] items-center p-[30px] bg-bg-sub rounded-[4px] shadow-[0_4px_16px_0_rgba(0,0,0,.04)]"
>
<h1 className="font-medium text-[32px] text-text-main">
Velog Dashboard
</h1>
<Input
size="large"
id="access"
size="LARGE"
type="password"
placeholder="Access Token을 입력하세요"
{...register('access_token', { required: true })}
/>
<Input
size="large"
id="refresh"
size="LARGE"
type="password"
placeholder="Refresh Token을 입력하세요"
{...register('refresh_token', { required: true })}
/>
<Button size="large" form="large" type="submit" disabled={!isValid}>
<Button
size="LARGE"
form="LARGE"
type="submit"
disabled={!isValid}
id="login"
>
로그인
</Button>
</form>
</div>
</main>
);
};
1 change: 1 addition & 0 deletions src/app/(login)/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Login } from './page';
2 changes: 1 addition & 1 deletion src/app/(login)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Content } from './Content';
import { Metadata } from 'next';
import { Content } from './Content';

export const metadata: Metadata = {
title: '로그인',
Expand Down
1 change: 1 addition & 0 deletions src/app/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './(login)';
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ToastContainer } from 'react-toastify';
import { Noto_Sans_KR } from 'next/font/google';
import 'react-toastify/dist/ReactToastify.css';
import type { Metadata } from 'next';
import { QueryProvider } from '@/components';
import './globals.css';
import { QueryProvider } from '@/utils/QueryProvider';
import 'react-toastify/dist/ReactToastify.css';

export const metadata: Metadata = {
title: 'Velog Dashboard',
Expand Down
2 changes: 1 addition & 1 deletion src/app/main/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Content } from './Content';
import type { Metadata } from 'next';
import { Content } from './Content';

export const metadata: Metadata = {
title: '대시보드',
Expand Down
3 changes: 0 additions & 3 deletions src/app/page.tsx

This file was deleted.

21 changes: 12 additions & 9 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { HTMLProps } from 'react';
import { sizeStyle } from './size';
import { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
import { sizeStyle, sizeStyleType } from './size';

interface IProp extends Omit<HTMLProps<HTMLButtonElement>, 'size'> {
form?: 'large' | 'small';
size: 'large' | 'medium' | 'small';
type?: 'submit' | 'reset' | 'button';
interface IProp
extends DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
form?: keyof typeof formStyle;
size: sizeStyleType;
}

const formStyle = {
large: 'h-[55px] rounded-sm',
small: 'pl-[20px] pr-[20px] w-fit h-8 rounded-[4px]',
LARGE: 'h-[55px] rounded-sm',
SMALL: 'pl-[20px] pr-[20px] w-fit h-8 rounded-[4px]',
};

export const Button = ({ form = 'small', size, children, ...rest }: IProp) => (
export const Button = ({ form = 'SMALL', size, children, ...rest }: IProp) => (
<button
className={`bg-primary-main hover:bg-primary-sub disabled:bg-border-sub disabled:cursor-not-allowed text-bg-main font-bold ${sizeStyle[size]} ${formStyle[form]}`}
{...rest}
Expand Down
28 changes: 18 additions & 10 deletions src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import { HTMLProps } from 'react';
import { sizeStyle } from './size';
import { forwardRef, ForwardedRef } from 'react';
import {
DetailedHTMLProps,
InputHTMLAttributes,
forwardRef,
ForwardedRef,
} from 'react';
import { sizeStyle, sizeStyleType } from './size';

interface IProp extends Omit<HTMLProps<HTMLInputElement>, 'size'> {
form?: 'large' | 'small';
size: 'large' | 'medium' | 'small';
interface IProp
extends Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size'
> {
form?: keyof typeof formStyle;
size: sizeStyleType;
}

const formStyle = {
large: 'p-4 h-[48px] focus:border-primary-2 rounded-sm',
small: 'p-2 h-[38px] focus:border-border-3 rounded-[4px]',
LARGE: 'p-4 h-[48px] focus:border-primary-sub rounded-sm',
SMALL: 'p-2 h-[38px] focus:border-border-alt rounded-[4px]',
};

export const Input = forwardRef<HTMLInputElement, IProp>(
(
{ form = 'large', size, ...rest }: IProp,
{ form = 'LARGE', size, ...rest }: IProp,
ref?: ForwardedRef<HTMLInputElement> | undefined,
) => (
<input
ref={ref}
className={`bg-bg-sub border-[1px] border-border-sub placeholder:text-text-3 text-text-main text-[16px] font-light ${formStyle[form]} ${sizeStyle[size]} ${rest.className}`}
className={`bg-bg-sub border-[1px] border-border-sub placeholder:text-text-alt text-text-main text-[16px] font-light ${formStyle[form]} ${sizeStyle[size]} ${rest.className}`}
{...rest}
/>
),
Expand Down
20 changes: 20 additions & 0 deletions src/components/QueryProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { ReactNode } from 'react';

const client = new QueryClient({
defaultOptions: {
queries: { retry: 1, refetchOnWindowFocus: false },
mutations: { onError: (err) => toast.error(`${err.message}`) },
},
});

interface IProp {
children: ReactNode | ReactNode[];
}

export const QueryProvider = ({ children }: IProp) => {
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
};
3 changes: 2 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Input';
export * from './QueryProvider';
export * from './Button';
export * from './Input';
Loading