-
Notifications
You must be signed in to change notification settings - Fork 0
지갑 페이지 및 실시간 변동사항 반영 구현 #7
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
3f0724e
fdbc52b
4cc62b2
0fd6538
97a765d
35d8062
4f970c2
d5126df
ca99a96
b8810b2
801b289
a0bc9fb
32866b6
930ffe0
5ee4e1f
6ca6823
c05196a
0f2233b
c8b8950
f1e1027
a0a50f4
cb7306c
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 |
|---|---|---|
|
|
@@ -31,6 +31,4 @@ jobs: | |
|
|
||
| - name: Lint Code | ||
| run: pnpm lint | ||
|
|
||
| - name: Build Test | ||
| run: pnpm run build | ||
|
|
||
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,27 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| export async function GET(request: Request) { | ||
| try { | ||
| const { searchParams } = new URL(request.url); | ||
| const markets = searchParams.get("markets") || "KRW-WAXP"; | ||
|
|
||
| // Upbit API로 현재가 조회 | ||
| const response = await fetch(`https://api.upbit.com/v1/ticker?markets=${markets}`, { | ||
| headers: { | ||
| Accept: "application/json", | ||
| }, | ||
| cache: "no-store", | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error("Failed to fetch ticker from Upbit"); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
|
|
||
| return NextResponse.json({ success: true, data }, { status: 200 }); | ||
| } catch (error) { | ||
| console.error("Ticker API error:", error); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,11 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cookies } from "next/headers"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createClient } from "@/shared"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createClient as createServerClient } from "@/shared/utils/supabase/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(request: Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const supabase = await createClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const supabase = createServerClient(cookies()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: userData, error: userError } = await supabase.auth.getUser(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (userError || !userData.user) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -26,12 +27,140 @@ export async function POST(request: Request) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 총 거래 금액 계산 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const total_krw = price * amount; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userId = userData.user.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
26
to
+30
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 매수/매도에 따른 지갑 잔고 확인 및 업데이트 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (trade_type === "buy") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 매수: KRW 차감, 코인 증액 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 1. KRW 잔고 확인 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: krwWallet } = await supabase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("wallet") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select("*") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("user_id", userId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("coin_id", "KRW") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .single(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!krwWallet || krwWallet.amount < total_krw) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: "KRW 잔고가 부족합니다." }, { status: 400 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 2. KRW 차감 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error: krwUpdateError } = await supabase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("wallet") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .update({ amount: krwWallet.amount - total_krw }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("id", krwWallet.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (krwUpdateError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("KRW update error:", krwUpdateError); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: krwUpdateError.message }, { status: 500 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+57
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: krwWallet } = await supabase | |
| .from("wallet") | |
| .select("*") | |
| .eq("user_id", userId) | |
| .eq("coin_id", "KRW") | |
| .single(); | |
| if (!krwWallet || krwWallet.amount < total_krw) { | |
| return NextResponse.json({ error: "KRW 잔고가 부족합니다." }, { status: 400 }); | |
| } | |
| // 2. KRW 차감 | |
| const { error: krwUpdateError } = await supabase | |
| .from("wallet") | |
| .update({ amount: krwWallet.amount - total_krw }) | |
| .eq("id", krwWallet.id); | |
| if (krwUpdateError) { | |
| console.error("KRW update error:", krwUpdateError); | |
| return NextResponse.json({ error: krwUpdateError.message }, { status: 500 }); | |
| } | |
| // Atomic KRW balance check and update using a Postgres function | |
| // You must create the following function in your database: | |
| // CREATE OR REPLACE FUNCTION atomic_wallet_update(user_id uuid, coin_id text, amount numeric) | |
| // RETURNS BOOLEAN AS $$ | |
| // DECLARE | |
| // current_amount numeric; | |
| // BEGIN | |
| // SELECT amount INTO current_amount FROM wallet WHERE user_id = atomic_wallet_update.user_id AND coin_id = atomic_wallet_update.coin_id FOR UPDATE; | |
| // IF current_amount < atomic_wallet_update.amount THEN | |
| // RETURN FALSE; | |
| // END IF; | |
| // UPDATE wallet SET amount = current_amount - atomic_wallet_update.amount WHERE user_id = atomic_wallet_update.user_id AND coin_id = atomic_wallet_update.coin_id; | |
| // RETURN TRUE; | |
| // END; | |
| // $$ LANGUAGE plpgsql; | |
| const { data: krwUpdateResult, error: krwUpdateError } = await supabase | |
| .rpc("atomic_wallet_update", { | |
| user_id: userId, | |
| coin_id: "KRW", | |
| amount: total_krw, | |
| }); | |
| if (krwUpdateError) { | |
| console.error("KRW update error:", krwUpdateError); | |
| return NextResponse.json({ error: krwUpdateError.message }, { status: 500 }); | |
| } | |
| if (!krwUpdateResult) { | |
| return NextResponse.json({ error: "KRW 잔고가 부족합니다." }, { status: 400 }); | |
| } |
Copilot
AI
Nov 17, 2025
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 object is being cast to check for the code property using (coinSelectError as { code?: string }). This approach is not type-safe and could lead to runtime errors. Consider using a proper type guard or checking the error structure more safely with optional chaining, e.g., coinSelectError && 'code' in coinSelectError && coinSelectError.code === "PGRST116".
| if ((coinSelectError as { code?: string }).code === "PGRST116") { | |
| if (coinSelectError && 'code' in coinSelectError && coinSelectError.code === "PGRST116") { |
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.
새로운 코인을 coins 테이블에 추가할 때, korean_name과 english_name에 coin_id를 그대로 사용하고 있습니다. 이는 부정확한 데이터를 생성할 수 있습니다. Upbit API 등에서 정확한 코인 이름을 조회하여 채워 넣거나, 이것이 어렵다면 임시 데이터임을 명확히 알리는 주석과 함께 추후 수정할 수 있도록 TODO를 남겨두는 것이 좋습니다.
| if ((coinSelectError as { code?: string }).code === "PGRST116") { | |
| const { error: coinInsertError } = await supabase | |
| .from("coins") | |
| .insert({ market_id: coin_id, korean_name: coin_id, english_name: coin_id }); | |
| if (coinInsertError) { | |
| console.error("Coin insert error:", coinInsertError); | |
| return NextResponse.json({ error: coinInsertError.message }, { status: 500 }); | |
| } | |
| } | |
| if ((coinSelectError as { code?: string }).code === "PGRST116") { | |
| // TODO: 추후 Upbit API 등을 통해 정확한 코인 이름 조회 로직 추가 필요 | |
| const { error: coinInsertError } = await supabase | |
| .from("coins") | |
| .insert({ market_id: coin_id, korean_name: coin_id, english_name: coin_id }); | |
| if (coinInsertError) { | |
| console.error("Coin insert error:", coinInsertError); | |
| return NextResponse.json({ error: coinInsertError.message }, { status: 500 }); | |
| } | |
| } |
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.
Copilot
AI
Nov 17, 2025
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 buy/sell operations lack transaction safety. If any step fails after KRW is deducted (e.g., coin wallet update fails), the user loses money without receiving coins. These operations should be wrapped in a database transaction to ensure atomicity, or implement proper rollback logic on errors.
Copilot
AI
Nov 17, 2025
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 trade insertion occurs after all wallet updates, meaning if it fails, the wallet balances have already been modified but no trade record exists. This creates data inconsistency. The trade record insertion should either be part of the same transaction or handled before wallet updates to maintain data integrity.
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.
API 응답 형식을 일관성 있게 유지하는 것이 좋습니다. 성공 시에는
{ success: true, ... }를 반환하지만, 에러 발생 시에는{ error: "..." }를 반환하고 있습니다. 다른 API와 마찬가지로{ success: false, error: "..." }와 같은 형태로 통일하는 것을 권장합니다.