Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
db28cdc
chore: 기능 정리 문서 제외
Dobbymin Nov 9, 2025
693bd53
chore: tanstack query 라이브러리 설치
Dobbymin Nov 9, 2025
4f6fb70
feat: tanstack query 설정 파일 추가
Dobbymin Nov 9, 2025
7dcebe5
chore: 상승 및 하락 색상 추가
Dobbymin Nov 9, 2025
74e20fa
feat: tanstack query 설정 파일 추가
Dobbymin Nov 9, 2025
889807f
refactor: entities 폴더로 api 코드 이동
Dobbymin Nov 9, 2025
c006b2d
feat: tanstack query 설정 파일 추가
Dobbymin Nov 9, 2025
cbcf544
feat: 필요한 마켓 정보만 가져오는 route 구현
Dobbymin Nov 9, 2025
1257543
feat: 필요한 마켓 정보만 가져오기 handler 구현
Dobbymin Nov 9, 2025
287f82d
feat: 마켓 관련 type 추가
Dobbymin Nov 9, 2025
0b67846
feat: 내부 마켓 api 호출 코드 구현
Dobbymin Nov 9, 2025
1bb5003
feat: api response type 추가
Dobbymin Nov 9, 2025
0dc6ffa
fix(home): 코인 정보 컴포넌트 너비 수정
Dobbymin Nov 9, 2025
e7272a4
feat: 10초마다 api 요청할 수 있도록 query hook 구현
Dobbymin Nov 9, 2025
7cbc26e
feat: 마켓 데이터 적용
Dobbymin Nov 9, 2025
cc94964
fix: 마켓 정보 수집 로직 성능 수정
Dobbymin Nov 9, 2025
2170e58
feat: API 응답 타입에 에러 타입 추가
Dobbymin Nov 9, 2025
5237250
fix: 사용하지 않는 주석 제거
Dobbymin Nov 9, 2025
9e134e1
feat: react-query 설정 코드 정리
Dobbymin Nov 9, 2025
db075c9
feat: sonner 컴포넌트 설정 추가
Dobbymin Nov 9, 2025
7cff7da
deploy: ci 테스트 코드 추가
Dobbymin Nov 9, 2025
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
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ next-env.d.ts

# local env files
.env.local
/.vscode
/.vscode
/docs
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.90.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.553.0",
Expand All @@ -26,6 +27,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@tanstack/react-query-devtools": "^5.90.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"@types/node": "^20",
"@types/react": "^19",
Expand Down
38 changes: 38 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/app/api/market/route.ts
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 });
Copy link

Copilot AI Nov 9, 2025

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 has status and data fields. The response returns an error field that doesn't exist in the type. Consider updating the type to include an optional error field or adjust the response structure to match the defined type.

Suggested change
return NextResponse.json({ status: "error", error: "Failed to fetch market data" }, { status: 500 });
return NextResponse.json({ status: "error", data: "Failed to fetch market data" }, { status: 500 });

Copilot uses AI. Check for mistakes.
}
Comment on lines +9 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

catch 블록에서 error를 직접 로깅하고 있습니다. errorunknown 타입일 수 있으므로, Error 인스턴스인지 확인하여 더 구체적인 오류 메시지를 로깅하면 프로덕션 환경에서 디버깅할 때 더 유용합니다.

  } 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 });
  }

}
4 changes: 4 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@
--color-text-light: var(--text-light);
--color-negative: var(--negative);
--color-positive: var(--positive);
--color-increase: var(--increase);
--color-decrease: var(--decrease);
}

:root {
--decrease: #1375ec;
--increase: #dd3c44;
--positive: #00b15d;
--negative: #ff4136;
--text-dark: #e0e0e0;
Expand Down
7 changes: 5 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Geist_Mono, Noto_Sans, Roboto } from "next/font/google";
import { Noto_Sans, Roboto } from "next/font/google";

import { AppProvider } from "@/shared";
import { AppLayout } from "@/widgets";
import type { Metadata } from "next";

Expand Down Expand Up @@ -28,7 +29,9 @@ export default function RootLayout({
return (
<html lang='en'>
<body className={`${roboto.variable} ${notoSans.variable} antialiased`}>
<AppLayout>{children}</AppLayout>
<AppProvider>
<AppLayout>{children}</AppLayout>
</AppProvider>
</body>
</html>
);
Expand Down
1 change: 1 addition & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./market";
1 change: 1 addition & 0 deletions src/entities/market/handler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./market-info.handler";
32 changes: 32 additions & 0 deletions src/entities/market/handler/market-info.handler.ts
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)), // 퍼센트로 변환 & 소수 둘째자리
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using parseFloat after toFixed is redundant since toFixed returns a string. The value is then parsed back to a number and later used with .toFixed(2) again in the component. Consider returning the number directly as ticker.signed_change_rate * 100 and let the component handle formatting, avoiding unnecessary precision loss.

Suggested change
changeRate: parseFloat((ticker.signed_change_rate * 100).toFixed(2)), // 퍼센트로 변환 & 소수 둘째자리
changeRate: ticker.signed_change_rate * 100, // 퍼센트로 변환, 소수 둘째자리 포맷은 컴포넌트에서 처리

Copilot uses AI. Check for mistakes.
};
});

return result;
};
2 changes: 2 additions & 0 deletions src/entities/market/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./handler";
export * from "./model";
2 changes: 2 additions & 0 deletions src/entities/market/model/apis/index.ts
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
Expand Up @@ -8,7 +8,7 @@ export interface MarketAllItem {
}

export const marketAllAPI = async (): Promise<MarketAllItem[]> => {
const response = await fetch(`${UPBIT_URL}/market/all?isDetails=true`, {
const response = await fetch(`${UPBIT_URL}/market/all?isDetails=false`, {
next: { revalidate: 60 }, // 60초 캐시 (서버 컴포넌트 환경에서)
});

Expand Down
2 changes: 2 additions & 0 deletions src/entities/market/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./apis";
export * from "./types";
1 change: 1 addition & 0 deletions src/entities/market/model/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./market-info.type";
6 changes: 6 additions & 0 deletions src/entities/market/model/types/market-info.type.ts
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;
};
3 changes: 1 addition & 2 deletions src/features/home/apis/index.ts
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";
12 changes: 12 additions & 0 deletions src/features/home/apis/market.api.ts
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;
};
43 changes: 19 additions & 24 deletions src/features/home/components/features/coin-info/CoinInfoTable.tsx
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();
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component doesn't handle loading and error states from the React Query hook. When marketInfoData is undefined during loading or after an error, the table will render empty. Consider destructuring isLoading and error from the hook and displaying appropriate UI feedback for these states.

Copilot uses AI. Check for mistakes.
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>
))}
Expand Down
1 change: 1 addition & 0 deletions src/features/home/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useGetMarketInfo";
11 changes: 11 additions & 0 deletions src/features/home/hooks/useGetMarketInfo.ts
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초 폴링
});
};
2 changes: 1 addition & 1 deletion src/features/home/ui/CoinInfoSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CoinInfoTable, CoinSearchInput } from "../components";

export const CoinInfoSection = () => {
return (
<aside className='flex w-[20%] min-w-64 flex-col border-r border-white/10 bg-surface-dark/20'>
<aside className='flex w-[30%] min-w-64 flex-col border-r border-white/10 bg-surface-dark/20'>
<CoinSearchInput />
<CoinInfoTable />
</aside>
Expand Down
1 change: 1 addition & 0 deletions src/shared/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./select";
export * from "./separator";
export * from "./table";
export * from "./textarea";
export * from "./sonner";
2 changes: 2 additions & 0 deletions src/shared/index.ts
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";
1 change: 1 addition & 0 deletions src/shared/libs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./query-client";
12 changes: 12 additions & 0 deletions src/shared/libs/query-client.ts
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,
},
},
});
16 changes: 16 additions & 0 deletions src/shared/provider/AppProvider.tsx
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>
);
};
Loading