Skip to content

Commit

Permalink
Merge pull request #2 from TEAM-ALOM/feature/config
Browse files Browse the repository at this point in the history
config: 폴더 구조 세팅, style 세팅 및 react query jotai 설치 등
  • Loading branch information
GHooN99 authored Oct 10, 2024
2 parents f3bb492 + a93ce1a commit b25866b
Show file tree
Hide file tree
Showing 53 changed files with 1,053 additions and 165 deletions.
334 changes: 219 additions & 115 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@
"npm": ">=10"
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.8",
"@tanstack/react-query": "^5.59.8",
"@tanstack/react-query-devtools": "^5.59.8",
"axios": "^1.7.7",
"jotai": "^2.10.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.2",
"redux-persist": "^6.0.0",
"styled-components": "^6.1.13",
"vite-plugin-svgr": "^4.2.0"
"styled-components": "^6.1.13"
},
"devDependencies": {
"vite-plugin-svgr": "^4.2.0",
"@eslint/js": "^9.11.1",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^22.7.5",
"@types/node": "^20",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/styled-components": "^5.1.34",
Expand Down
95 changes: 95 additions & 0 deletions src/configs/http-client/axios.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, { AxiosInstance, type AxiosResponse } from 'axios';
import { type CommonHttpClient, type CommonHttpResponse } from './httpClient';
import {
CommonHttpException,
SpecificHttpException,
UnauthorizedException,
UnknownHttpException,
isAuthenticationErrorCode,
isCommonErrorCode,
} from './httpErrors';

/** axios config */
export const axiosClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true,
timeout: 15000,
});

/** interceptor config */
axiosClient.interceptors.response.use(
response => {
return response.data;
},
error => {
if (axios.isAxiosError(error) && error.response) {
const response = error.response as AxiosResponse<
CommonHttpResponse<unknown>
>;
const errors = response.data.errors;

// 전역 에러는 첫 에러를 중점으로 처리하기
const errorCodeOfFisrtError = errors?.[0].errorCode;

// 인증 에러
if (isAuthenticationErrorCode(errorCodeOfFisrtError)) {
return Promise.reject(
new UnauthorizedException('UnauthorizedException'),
);
}

// 글로벌 에러 처리
if (isCommonErrorCode(errorCodeOfFisrtError)) {
return Promise.reject(new CommonHttpException('CommonHttpException'));
}

// 각 도메인의 에러
if (errors) {
return Promise.reject(
new SpecificHttpException('SpecificHttpException', errors),
);
}
}

// 진짜 알 수 없는 에러
console.error(error);
return Promise.reject(new UnknownHttpException('UnknownHttpException'));
},
);

class CommonHttpAxiosClientImpl implements CommonHttpClient {
constructor(private readonly client: AxiosInstance) {}

async get<T = any>(url: string, config?: any): Promise<T> {
const response = await this.client.get<T>(url, config);
return response.data;
}

async post<T = any, D = any>(
url: string,
data?: D,
config?: any,
): Promise<T> {
const response = await this.client.post<T>(url, data, config);
return response.data;
}

async patch<T = any, D = any>(
url: string,
data?: D,
config?: any,
): Promise<T> {
const response = await this.client.patch<T>(url, data, config);
return response.data;
}

async delete<T = any>(url: string, config?: any): Promise<T> {
const response = await this.client.delete<T>(url, config);
return response.data;
}
}

export const httpClient: CommonHttpClient = new CommonHttpAxiosClientImpl(
axiosClient,
);
23 changes: 23 additions & 0 deletions src/configs/http-client/httpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** 넓은 타입 정의를 위해 */

/* eslint-disable @typescript-eslint/no-explicit-any */
import { CommonHttpErrorScheme } from './httpErrors';

export type CommonHttpResponseStatus = 'success' | 'fail' | 'error';

export interface CommonHttpResponse<T> {
apiVersion: string;
timestamp: string;
status: CommonHttpResponseStatus;
statusCode: number;
message?: string;
errors?: Array<CommonHttpErrorScheme>;
data: T;
}

export interface CommonHttpClient {
get<T = any>(url: string, config?: any): Promise<T>;
post<T = any, D = any>(url: string, data?: D, config?: any): Promise<T>;
patch<T = any, D = any>(url: string, data?: D, config?: any): Promise<T>;
delete<T = any>(url: string, config?: any): Promise<T>;
}
69 changes: 69 additions & 0 deletions src/configs/http-client/httpErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { type ValueOf } from '@/utils/types/utilTypes';

export type CommonHttpErrorScheme = {
field?: string;
errorCode: CommonHttpErrorCodeType;
message: string;
};

export const CommonHttpErrorCode = {
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR',
INVALID_REQUEST: 'INVALID_REQUEST',
RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
} as const;

export type CommonHttpErrorCodeType = ValueOf<typeof CommonHttpErrorCode>;

export type AuthenticationErrorCode = Extract<
CommonHttpErrorCodeType,
'AUTHENTICATION_ERROR' | 'AUTHORIZATION_ERROR'
>;

export const isCommonErrorCode = (
errorCode: string | undefined,
): errorCode is CommonHttpErrorCodeType => {
return Object.values(CommonHttpErrorCode).includes(errorCode as never);
};

export const isAuthenticationErrorCode = (
errorCode: string | undefined,
): errorCode is AuthenticationErrorCode => {
return ['AUTHENTICATION_ERROR', 'AUTHORIZATION_ERROR'].includes(
errorCode as never,
);
};

// 각 기능에서 사용 될 구분 가능한 http 에러
export class SpecificHttpException extends Error {
constructor(
message: string,
public errors: Array<CommonHttpErrorScheme>,
) {
super(message);
this.name = 'SpecificHttpException';
}
}

export class UnauthorizedException extends Error {
constructor(message: string) {
super(message);
this.name = 'UnauthorizedException';
}
}

export class CommonHttpException extends Error {
constructor(message: string) {
super(message);
this.name = 'CommonHttpException';
}
}

export class UnknownHttpException extends Error {
constructor(message: string) {
super(message);
this.name = 'UnknownHttpException';
}
}
1 change: 1 addition & 0 deletions src/configs/http-client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { httpClient } from './axios.config';
8 changes: 8 additions & 0 deletions src/configs/http-client/pageParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type PageParam = {
page: number;
size: number;
};

export type PageResponse<T> = {
list: T[];
} & PageParam;
3 changes: 3 additions & 0 deletions src/configs/react/libs/ReactToastify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const Toast = () => {
return;
};
45 changes: 45 additions & 0 deletions src/configs/react/providers/QueryProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { type ReactNode, useRef } from 'react';
import {
QueryCache,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import {
SpecificHttpException,
UnauthorizedException,
} from '@/configs/http-client/httpErrors';

export const QueryProvider = ({ children }: { children: ReactNode }) => {
const queryClient = useRef(
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
retryOnMount: false,
},
},
queryCache: new QueryCache({
onError: (error: unknown) => {
if (error instanceof UnauthorizedException) {
return;
}
if (error instanceof SpecificHttpException) {
return;
}
},
}),
}),
);

return (
<QueryClientProvider client={queryClient.current}>
<ReactQueryDevtools initialIsOpen={false} />
{children}
</QueryClientProvider>
);
};
4 changes: 4 additions & 0 deletions src/configs/react/stores/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Store<T> {
get: () => T;
set: (value: T) => void;
}
5 changes: 5 additions & 0 deletions src/features/users/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type User = {
userName: string;
userId: string;
isAdmin: boolean;
};
4 changes: 4 additions & 0 deletions src/features/users/models/userLoginDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UserLoginDto = {
userId: string;
password: string;
};
5 changes: 5 additions & 0 deletions src/features/users/models/userRegisterDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type UserRegisterDto = {
userId: string;
password: string;
userName: string;
};
5 changes: 5 additions & 0 deletions src/features/users/models/validationErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class UserValidationException extends Error {
constructor(message: string) {
super(message);
}
}
3 changes: 3 additions & 0 deletions src/features/users/models/validationPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const MAX_USER_NAME_LENGTH = 10;
export const MAX_USER_ID_LENGTH = 10;
export const MIN_USER_PASSWORD_LENGTH = 8;
5 changes: 5 additions & 0 deletions src/features/users/services/remotes/checkLoginRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { httpClient } from '@/configs/http-client';
import { User } from '../../models/user';

export const API_CHECK_LOGIN = 'v1/auth/check-login';
export const checkLoginRequest = () => httpClient.post<User>(API_CHECK_LOGIN);
8 changes: 8 additions & 0 deletions src/features/users/services/remotes/sendLoginRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { httpClient } from '@/configs/http-client';
import { User } from '../../models/user';
import { UserLoginDto } from '../../models/userLoginDto';

export const API_LOGIN = 'v1/auth/login';

export const sendLoginRequest = async (userLoginDto: UserLoginDto) =>
httpClient.post<User>(API_LOGIN, userLoginDto);
5 changes: 5 additions & 0 deletions src/features/users/services/remotes/sendLogoutRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { httpClient } from '@/configs/http-client';

export const API_LOGOUT = 'v1/auth/logout';

export const sendLogoutRequest = async () => httpClient.post(API_LOGOUT);
12 changes: 12 additions & 0 deletions src/features/users/services/validatePasswordInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MIN_USER_PASSWORD_LENGTH } from '../models/validationPolicy';

export const validatePasswordInput = (password: string) => {
return password.length >= MIN_USER_PASSWORD_LENGTH;
};

export const validateRePasswordInput = (
password: string,
rePassword: string,
) => {
return validatePasswordInput(password) && password === rePassword;
};
5 changes: 5 additions & 0 deletions src/features/users/services/validateUserIdInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MAX_USER_ID_LENGTH } from '../models/validationPolicy';

export const validateUserIdInput = (userId: string) => {
return userId.length > 0 && userId.length <= MAX_USER_ID_LENGTH;
};
5 changes: 5 additions & 0 deletions src/features/users/services/validateUserNameInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MAX_USER_NAME_LENGTH } from '../models/validationPolicy';

export const validateUserNameInput = (userName: string) => {
return userName.length > 0 && userName.length <= MAX_USER_NAME_LENGTH;
};
9 changes: 9 additions & 0 deletions src/features/users/ui/components/CardLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type ReactNode } from 'react';

export const CardLayout = ({ children }: { children: ReactNode }) => {
return (
<div className='flex items-center justify-center min-h-screen'>
{children}
</div>
);
};
33 changes: 33 additions & 0 deletions src/features/users/ui/hooks/useLoginFormState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState } from 'react';
import { useInputState } from '@/utils/react/hooks/useInputState';
import { validatePasswordInput } from '../../services/validatePasswordInput';
import { validateUserIdInput } from '../../services/validateUserIdInput';

export const useLoginFormState = () => {
const [userId, handleUserIdChange] = useInputState('');
const [password, handlePasswordChange] = useInputState('');

const isUserIdValid = validateUserIdInput(userId);
const isPasswordValid = validatePasswordInput(password);

const isFormValid = isUserIdValid && isPasswordValid;
const [isFormDirty, setIsFormDirty] = useState<boolean>(false);

return {
userId: {
value: userId,
onChange: handleUserIdChange,
isValid: isUserIdValid,
},
password: {
value: password,
onChange: handlePasswordChange,
isValid: isPasswordValid,
},
form: {
isValid: isFormValid,
isDirty: isFormDirty,
setIsDirty: setIsFormDirty,
},
} as const;
};
Loading

0 comments on commit b25866b

Please sign in to comment.