-
Notifications
You must be signed in to change notification settings - Fork 0
Upbit Market API 연동 및 실시간 시세 조회 기능 구현 #2
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
Changes from all commits
db28cdc
693bd53
4f6fb70
7dcebe5
74e20fa
889807f
c006b2d
cbcf544
1257543
287f82d
0b67846
1bb5003
0dc6ffa
e7272a4
7cbc26e
cc94964
2170e58
5237250
9e134e1
db075c9
7cff7da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| name: lint & Build Test | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - main | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout Code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Install pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10 | ||
| run_install: false | ||
|
|
||
| - name: Install Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 | ||
| cache: 'pnpm' | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Lint Code | ||
| run: pnpm lint | ||
|
|
||
| - name: Build Test | ||
| run: pnpm run build |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,4 +42,5 @@ next-env.d.ts | |
|
|
||
| # local env files | ||
| .env.local | ||
| /.vscode | ||
| /.vscode | ||
| /docs | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { marketInfoHandler } from "@/entities"; | ||
|
|
||
| export async function GET() { | ||
| try { | ||
| const data = await marketInfoHandler(); | ||
| return NextResponse.json({ status: "success", data }); | ||
| } catch (error) { | ||
| console.error("Market API error:", error); | ||
| return NextResponse.json({ status: "error", error: "Failed to fetch market data" }, { status: 500 }); | ||
| } | ||
|
Comment on lines
+9
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Market API error:", errorMessage, error);
return NextResponse.json({ status: "error", error: "Failed to fetch market data" }, { status: 500 });
} |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./market"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./market-info.handler"; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||
| import { marketAllAPI, tickerAPI } from "../model"; | ||||||
|
|
||||||
| export const marketInfoHandler = async () => { | ||||||
| const marketData = await marketAllAPI(); | ||||||
|
|
||||||
| const krwMarketMap = new Map<string, { korean_name: string }>(); | ||||||
| marketData.forEach((item) => { | ||||||
| if (item.market.startsWith("KRW-")) { | ||||||
| krwMarketMap.set(item.market, { korean_name: item.korean_name }); | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| const krwMarketCodes = Array.from(krwMarketMap.keys()); | ||||||
| if (krwMarketCodes.length === 0) { | ||||||
| return []; | ||||||
| } | ||||||
|
|
||||||
| const tickerData = await tickerAPI(krwMarketCodes); | ||||||
|
|
||||||
| const result = tickerData.map((ticker) => { | ||||||
| const marketInfo = krwMarketMap.get(ticker.market); | ||||||
|
|
||||||
| return { | ||||||
| market: ticker.market, | ||||||
| koreanName: marketInfo?.korean_name || "", | ||||||
| tradePrice: ticker.trade_price, | ||||||
| changeRate: parseFloat((ticker.signed_change_rate * 100).toFixed(2)), // 퍼센트로 변환 & 소수 둘째자리 | ||||||
|
||||||
| changeRate: parseFloat((ticker.signed_change_rate * 100).toFixed(2)), // 퍼센트로 변환 & 소수 둘째자리 | |
| changeRate: ticker.signed_change_rate * 100, // 퍼센트로 변환, 소수 둘째자리 포맷은 컴포넌트에서 처리 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from "./handler"; | ||
| export * from "./model"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from "./market-all.api"; | ||
| export * from "./ticker.api"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from "./apis"; | ||
| export * from "./types"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./market-info.type"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export type MarketInfoType = { | ||
| market: string; | ||
| koreanName: string; | ||
| tradePrice: number; | ||
| changeRate: number; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1 @@ | ||
| export * from "./market-all.api"; | ||
| export * from "./ticker.api"; | ||
| export * from "./market.api"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { MarketInfoType } from "@/entities"; | ||
| import { APIResponse } from "@/shared"; | ||
|
|
||
| export const marketAPI = async (): Promise<MarketInfoType[]> => { | ||
| const response = await fetch("/api/market", { cache: "no-store" }); | ||
| if (!response.ok) { | ||
| throw new Error(`Market API failed: ${response.status} ${response.statusText}`); | ||
| } | ||
| const json: APIResponse<MarketInfoType[]> = await response.json(); | ||
|
|
||
| return json.data; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,34 @@ | ||
| import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared"; | ||
| "use client"; | ||
|
|
||
| import { marketAllAPI, tickerAPI } from "../../../apis"; | ||
| import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn } from "@/shared"; | ||
|
|
||
| export const CoinInfoTable = async () => { | ||
| const marketData = await marketAllAPI(); | ||
| const marketList = marketData.map((item) => item.market); | ||
| // console.log(marketData); | ||
| const tickerData = await tickerAPI(marketList); | ||
| import { useGetMarketInfo } from "../../../hooks"; | ||
|
|
||
| export const CoinInfoTable = () => { | ||
| const { data: marketInfoData } = useGetMarketInfo(); | ||
|
||
| const rateColor = (rate: number) => { | ||
| return rate >= 0 ? "text-increase" : "text-decrease"; | ||
| }; | ||
| return ( | ||
| <div className='flex-1 overflow-y-auto'> | ||
| <Table> | ||
| <TableHeader> | ||
| <TableRow className='sticky top-0 border-none bg-surface-dark/30'> | ||
| <TableHead className='text-text-muted-dark'>마켓</TableHead> | ||
| <TableHead className='text-text-muted-dark'>가격</TableHead> | ||
| <TableHead className='text-text-muted-dark'>24시간 변동률</TableHead> | ||
| <TableHead className='text-text-muted-dark'>현재가</TableHead> | ||
| <TableHead className='text-text-muted-dark'>변동률</TableHead> | ||
| </TableRow> | ||
| </TableHeader> | ||
| <TableBody> | ||
| {/* <tr className='cursor-pointer border-l-2 border-primary bg-primary/10 hover:bg-primary/20'> | ||
| <td className='px-4 py-3 font-medium whitespace-nowrap text-text-dark'>BTC/USD</td> | ||
| <td className='px-4 py-3 text-right font-mono whitespace-nowrap text-text-dark'>68,450.25</td> | ||
| <td className='px-4 py-3 text-right font-mono whitespace-nowrap text-positive'>+2.15%</td> | ||
| </tr> | ||
| <tr className='cursor-pointer border-l-2 border-transparent hover:bg-white/5'> | ||
| <td className='px-4 py-3 font-medium whitespace-nowrap text-text-dark'>ETH/USD</td> | ||
| <td className='px-4 py-3 text-right font-mono whitespace-nowrap text-text-dark'>3,550.78</td> | ||
| <td className='px-4 py-3 text-right font-mono whitespace-nowrap text-positive'>+1.80%</td> | ||
| </tr> */} | ||
| {tickerData.map((ticker) => ( | ||
| {marketInfoData?.map((ticker) => ( | ||
| <TableRow key={ticker.market} className='cursor-pointer border-l-2 border-transparent hover:bg-white/5'> | ||
| <TableCell className='font-medium'>{ticker.market}</TableCell> | ||
| <TableCell>{ticker.trade_price.toLocaleString()}</TableCell> | ||
| <TableCell className={`${ticker.signed_change_rate >= 0 ? "text-positive" : "text-destructive"}`}> | ||
| {(ticker.signed_change_rate * 100).toFixed(2)}% | ||
| <TableCell className='flex flex-col font-medium'> | ||
| <p className='font-sans text-xs font-semibold'>{ticker.koreanName}</p> | ||
| <p className='font-roboto text-[10px] font-light'>{ticker.market}</p> | ||
| </TableCell> | ||
| <TableCell className='text-xs'>{Number(ticker.tradePrice).toLocaleString("ko-KR")}</TableCell> | ||
| <TableCell className={cn(rateColor(ticker.changeRate), "font-roboto text-xs")}> | ||
| {ticker.changeRate.toFixed(2)}% | ||
| </TableCell> | ||
| </TableRow> | ||
| ))} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./useGetMarketInfo"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { useQuery } from "@tanstack/react-query"; | ||
|
|
||
| import { marketAPI } from "../apis"; | ||
|
|
||
| export const useGetMarketInfo = () => { | ||
| return useQuery({ | ||
| queryKey: ["market-info"], | ||
| queryFn: marketAPI, | ||
| refetchInterval: 10000, // 10초 폴링 | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| export * from "./components"; | ||
| export * from "./constants"; | ||
| export * from "./utils"; | ||
| export * from "./provider"; | ||
| export * from "./types"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./query-client"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { QueryClient } from "@tanstack/react-query"; | ||
|
|
||
| export const queryClient = new QueryClient({ | ||
| defaultOptions: { | ||
| queries: { | ||
| retry: 1, | ||
| staleTime: 5000, | ||
|
|
||
| refetchOnWindowFocus: false, | ||
| }, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { Toaster } from "../components"; | ||
|
|
||
| import { QueryProvider } from "./components"; | ||
|
|
||
| type Props = { | ||
| children: React.ReactNode; | ||
| }; | ||
|
|
||
| export const AppProvider = ({ children }: Props) => { | ||
| return ( | ||
| <QueryProvider> | ||
| {children} | ||
| <Toaster /> | ||
| </QueryProvider> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error response structure is inconsistent with the
APIResponse<T>type definition which only hasstatusanddatafields. The response returns anerrorfield that doesn't exist in the type. Consider updating the type to include an optionalerrorfield or adjust the response structure to match the defined type.