Skip to content

Commit

Permalink
Feature/#231 - zod 적용 (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
baegyeong authored Nov 25, 2024
2 parents 7c40223 + f97cf06 commit 346830f
Show file tree
Hide file tree
Showing 24 changed files with 280 additions and 126 deletions.
3 changes: 2 additions & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^2.5.4"
"tailwind-merge": "^2.5.4",
"zod": "^3.23.8"
},
"devDependencies": {
"@chromatic-com/storybook": "3.2.2",
Expand Down
7 changes: 7 additions & 0 deletions packages/frontend/src/apis/queries/auth/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const GetLoginStatusSchema = z.object({
message: z.enum(['Authenticated', 'Not Authenticated']),
});

export type GetLoginStatus = z.infer<typeof GetLoginStatusSchema>;
3 changes: 0 additions & 3 deletions packages/frontend/src/apis/queries/auth/types.ts

This file was deleted.

17 changes: 9 additions & 8 deletions packages/frontend/src/apis/queries/auth/useGetLoginStatus.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import { GetLoginStatusResponse } from './types';
import { instance } from '@/apis/config';
import { GetLoginStatusSchema, type GetLoginStatus } from './schema';
import { get } from '@/apis/utils/get';

const getLoginStatus = async (): Promise<GetLoginStatusResponse> => {
const { data } = await instance.get('/api/auth/google/status');
return data;
};
const getLoginStatus = () =>
get<GetLoginStatus>({
schema: GetLoginStatusSchema,
url: '/api/auth/google/status',
});

export const useGetLoginStatus = () => {
return useQuery<GetLoginStatusResponse, Error>({
queryKey: ['login_status'],
return useQuery({
queryKey: ['loginStatus'],
queryFn: getLoginStatus,
});
};
1 change: 1 addition & 0 deletions packages/frontend/src/apis/queries/stock-detail/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './useGetStockDetail';
export * from './usePostStockView';
32 changes: 32 additions & 0 deletions packages/frontend/src/apis/queries/stock-detail/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from 'zod';

export const GetStockRequestSchema = z.object({
stockId: z.string(),
});

export type GetStockRequest = z.infer<typeof GetStockRequestSchema>;

export const GetStockResponseSchema = z.object({
marketCap: z.number(),
name: z.string(),
eps: z.number(),
per: z.number(),
high52w: z.number(),
low52w: z.number(),
});

export type GetStockResponse = z.infer<typeof GetStockResponseSchema>;

export const PostStockViewRequestSchema = z.object({
stockId: z.string(),
});

export type PostStockViewRequest = z.infer<typeof PostStockViewRequestSchema>;

export const PostViewResponseSchema = z.object({
id: z.string(),
message: z.string(),
date: z.date(),
});

export type PostViewResponse = z.infer<typeof PostViewResponseSchema>;
9 changes: 0 additions & 9 deletions packages/frontend/src/apis/queries/stock-detail/types.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useQuery } from '@tanstack/react-query';
import {
GetStockResponseSchema,
type GetStockRequest,
type GetStockResponse,
} from './schema';
import { get } from '@/apis/utils/get';

const getStockDetail = ({ stockId }: GetStockRequest) =>
get<GetStockResponse>({
schema: GetStockResponseSchema,
url: `/api/stock/${stockId}/detail`,
});

export const useGetStockDetail = ({ stockId }: GetStockRequest) => {
return useQuery({
queryKey: ['stockDetail'],
queryFn: () => getStockDetail({ stockId }),
enabled: !!stockId,
});
};
34 changes: 18 additions & 16 deletions packages/frontend/src/apis/queries/stock-detail/usePostStockView.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import type { PostStockViewRequest, PostStockViewResponse } from './types';
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import { instance } from '@/apis/config';
import { useMutation } from '@tanstack/react-query';
import {
PostViewResponseSchema,
type PostStockViewRequest,
type PostViewResponse,
} from './schema';
import { post } from '@/apis/utils/post';

const postStockView = async ({
stockId,
}: PostStockViewRequest): Promise<PostStockViewResponse> => {
const { data } = await instance.post('/api/stock/view', { stockId });
return data;
};
const postStockView = async ({ stockId }: PostStockViewRequest) =>
post<PostViewResponse>({
params: stockId,
schema: PostViewResponseSchema,
url: 'api/stock/view',
});

export const usePostStockView = (
options?: UseMutationOptions<PostStockViewResponse, Error, string>,
) => {
return useMutation<PostStockViewResponse, Error, string>({
mutationKey: ['stock_view'],
mutationFn: (stockId) => postStockView({ stockId }),
...options,
export const usePostStockView = () => {
return useMutation({
mutationKey: ['stockView'],
mutationFn: ({ stockId }: PostStockViewRequest) =>
postStockView({ stockId }),
});
};
2 changes: 1 addition & 1 deletion packages/frontend/src/apis/queries/stocks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './types';
export * from './schema';
export * from './useGetTopViews';
export * from './useGetStocksByPrice';
19 changes: 19 additions & 0 deletions packages/frontend/src/apis/queries/stocks/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { z } from 'zod';

export const GetStockListRequestSchema = z.object({
limit: z.number(),
sortType: z.enum(['increase', 'decrease']),
});

export type GetStockListRequest = z.infer<typeof GetStockListRequestSchema>;

export const GetStockListResponseSchema = z.object({
id: z.string(),
name: z.string(),
currentPrice: z.number(),
changeRate: z.number(),
volume: z.number(),
marketCap: z.string(),
});

export type GetStockListResponse = z.infer<typeof GetStockListResponseSchema>;
12 changes: 0 additions & 12 deletions packages/frontend/src/apis/queries/stocks/types.ts

This file was deleted.

45 changes: 24 additions & 21 deletions packages/frontend/src/apis/queries/stocks/useGetStocksByPrice.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import type { GetStockListRequest, GetStockListResponse } from './types';
import { useQuery } from '@tanstack/react-query';
import { instance } from '@/apis/config';
import {
GetStockListResponseSchema,
type GetStockListRequest,
type GetStockListResponse,
} from './schema';
import { get } from '@/apis/utils/get';

const getTopGainers = async ({
limit,
}: GetStockListRequest): Promise<GetStockListResponse[]> => {
const { data } = await instance.get(`/api/stock/topGainers?limit=${limit}`);
return data;
};
const getTopGainers = ({ limit }: Partial<GetStockListRequest>) =>
get<GetStockListResponse[]>({
schema: GetStockListResponseSchema,
url: `/api/stock/topGainers?limit=${limit}`,
});

const getTopLosers = async ({
limit,
}: GetStockListRequest): Promise<GetStockListResponse[]> => {
const { data } = await instance.get(`/api/stock/topLosers?limit=${limit}`);
return data;
};
const getTopLosers = ({ limit }: Partial<GetStockListRequest>) =>
get<GetStockListResponse[]>({
schema: GetStockListResponseSchema,
url: `/api/stock/topLosers?limit=${limit}`,
});

export const useGetStocksByPrice = ({
limit,
isGaining,
}: GetStockListRequest & { isGaining: boolean }) => {
return useQuery<GetStockListResponse[], Error>({
queryKey: ['stocks', isGaining],
queryFn: isGaining
? () => getTopGainers({ limit })
: () => getTopLosers({ limit }),
sortType,
}: GetStockListRequest) => {
return useQuery({
queryKey: ['stocks', sortType],
queryFn:
sortType === 'increase'
? () => getTopGainers({ limit })
: () => getTopLosers({ limit }),
});
};
23 changes: 13 additions & 10 deletions packages/frontend/src/apis/queries/stocks/useGetTopViews.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import type { GetStockListRequest, GetStockListResponse } from './types';
import { useQuery } from '@tanstack/react-query';
import { instance } from '@/apis/config';
import {
GetStockListResponseSchema,
type GetStockListRequest,
type GetStockListResponse,
} from './schema';
import { get } from '@/apis/utils/get';

const getTopViews = async ({
limit,
}: GetStockListRequest): Promise<GetStockListResponse[]> => {
const { data } = await instance.get(`/api/stock/topViews?limit=${limit}`);
return data;
};
const getTopViews = ({ limit }: Partial<GetStockListRequest>) =>
get<GetStockListResponse[]>({
schema: GetStockListResponseSchema,
url: `/api/stock/topViews?limit=${limit}`,
});

export const useGetTopViews = ({ limit }: GetStockListRequest) => {
return useQuery<GetStockListResponse[], Error>({
export const useGetTopViews = ({ limit }: Partial<GetStockListRequest>) => {
return useQuery({
queryKey: ['stocks', 'topViews'],
queryFn: () => getTopViews({ limit }),
});
Expand Down
7 changes: 7 additions & 0 deletions packages/frontend/src/apis/utils/formatZodError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const formatZodError = (error: z.ZodError): string => {
return error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
};
26 changes: 26 additions & 0 deletions packages/frontend/src/apis/utils/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from 'zod';
import { instance } from '../config';
import { formatZodError } from './formatZodError';

interface GetParams {
schema: z.ZodType;
url: string;
}

export const get = async <T>({ schema, url }: GetParams): Promise<T | null> => {
try {
const { data } = await instance.get(url);
const result = schema.safeParse(data);

if (!result.success) {
throw new Error(formatZodError(result.error));
}

return data;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('API error:', error);
}
return null;
}
};
32 changes: 32 additions & 0 deletions packages/frontend/src/apis/utils/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AxiosRequestConfig } from 'axios';
import { z } from 'zod';
import { instance } from '../config';
import { formatZodError } from './formatZodError';

interface PostParams {
params: AxiosRequestConfig['params'];
schema: z.ZodType;
url: string;
}

export const post = async <T>({
params,
schema,
url,
}: PostParams): Promise<T | null> => {
try {
const { data } = await instance.post(url, { params });
const result = schema.safeParse(data);

if (!result.success) {
throw new Error(formatZodError(result.error));
}

return data;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('API error:', error);
}
return null;
}
};
12 changes: 9 additions & 3 deletions packages/frontend/src/pages/stock-detail/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { ChatDataType, ChatDataResponse } from '@/sockets/types';
import { useEffect, useMemo, useState } from 'react';
import { TextArea } from './components';
import { ChatMessage } from './components/ChatMessage';
import DownArrow from '@/assets/down-arrow.svg?react';
import { socketChat } from '@/sockets/config';
import {
ChatDataSchema,
type ChatData,
type ChatDataResponse,
} from '@/sockets/schema';
import { useWebsocket } from '@/sockets/useWebsocket';

export const ChatPanel = () => {
const STOCK_ID = '005930';
const [chatData, setChatData] = useState<ChatDataType[]>();
const [chatData, setChatData] = useState<ChatData[]>();

const socket = useMemo(() => {
return socketChat({ stockId: STOCK_ID });
Expand All @@ -27,7 +31,9 @@ export const ChatPanel = () => {

useEffect(() => {
const handleChat = (message: ChatDataResponse) => {
if (message?.chats) {
const validatedChatData = ChatDataSchema.safeParse(message?.chats);

if (validatedChatData.success && message?.chats) {
setChatData(message.chats);
}
};
Expand Down
7 changes: 4 additions & 3 deletions packages/frontend/src/pages/stock-detail/StockDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import {
NotificationPanel,
StockMetricsPanel,
} from '.';
import { useGetStockDetail } from '@/apis/queries/stock-detail';
import Plus from '@/assets/plus.svg?react';
import { Button } from '@/components/ui/button';
import stockData from '@/mocks/stock.json';

export const StockDetail = () => {
const { stockId = '' } = useParams();
const { stockId } = useParams();
const { data } = useGetStockDetail({ stockId: stockId ?? '' });

const detailData = stockData.data.find((data) => data.id === +stockId);

return (
<div className="flex flex-col gap-7">
<header className="flex gap-7">
<h1 className="display-bold24">{detailData?.name}</h1>
<h1 className="display-bold24">{data?.name}</h1>
<Button className="flex items-center justify-center gap-1">
<Plus /> 내 주식 추가
</Button>
Expand Down
Loading

0 comments on commit 346830f

Please sign in to comment.