-
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary of ChangesHello @Dobbymin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 Pull Request는 지갑 페이지의 핵심 기능을 대폭 개선하여 사용자에게 더욱 정확하고 실시간적인 거래 경험을 제공합니다. 실시간 데이터 동기화, 정교한 손익 계산 로직, 그리고 다중 코인 거래 지원을 통해 사용자는 자신의 자산 현황을 보다 투명하게 파악하고 효율적으로 관리할 수 있게 됩니다. 또한, API 구조를 간소화하고 빌드 안정성을 확보하여 전반적인 시스템의 견고함을 향상시켰습니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
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.
Code Review
이번 PR은 지갑 페이지의 실시간 평가/손익 계산 도입, 다중 코인 거래 지원 등 중요한 기능 개선을 포함하고 있습니다. 전반적으로 Zustand와 React Query를 활용한 상태 관리 및 데이터 동기화 구조가 훌륭합니다. 다만, 백엔드 API의 데이터 정합성과 관련된 몇 가지 치명적인 문제점(거래 및 입금 처리의 원자성 부족)이 발견되어 수정이 반드시 필요합니다. 또한 API 응답 형식의 일관성, 프론트엔드의 데이터 갱신 방식 등에서 개선할 점들을 제안드렸습니다. 이 부분들을 보완한다면 더욱 안정적이고 완성도 높은 기능이 될 것입니다.
| // 매수/매도에 따른 지갑 잔고 확인 및 업데이트 | ||
| 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 }); | ||
| } | ||
|
|
||
| // 3. 코인 증액 (없으면 생성) | ||
| const { data: coinWallet } = await supabase | ||
| .from("wallet") | ||
| .select("*") | ||
| .eq("user_id", userId) | ||
| .eq("coin_id", coin_id) | ||
| .single(); | ||
|
|
||
| if (coinWallet) { | ||
| // 기존 코인 지갑 증액 | ||
| const { error: coinUpdateError } = await supabase | ||
| .from("wallet") | ||
| .update({ amount: coinWallet.amount + amount }) | ||
| .eq("id", coinWallet.id); | ||
|
|
||
| if (coinUpdateError) { | ||
| console.error("Coin update error:", coinUpdateError); | ||
| return NextResponse.json({ error: coinUpdateError.message }, { status: 500 }); | ||
| } | ||
| } else { | ||
| // 새 코인 지갑 생성 | ||
| // coins 테이블에 코인이 없으면 생성 | ||
| const { error: coinSelectError } = await supabase | ||
| .from("coins") | ||
| .select("market_id") | ||
| .eq("market_id", coin_id) | ||
| .single(); | ||
|
|
||
| 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 }); | ||
| } | ||
| } | ||
|
|
||
| const { error: walletInsertError } = await supabase.from("wallet").insert({ user_id: userId, coin_id, amount }); | ||
|
|
||
| if (walletInsertError) { | ||
| console.error("Wallet insert error:", walletInsertError); | ||
| return NextResponse.json({ error: walletInsertError.message }, { status: 500 }); | ||
| } | ||
| } | ||
| } else if (trade_type === "sell") { | ||
| // 매도: 코인 차감, KRW 증액 | ||
| // 1. 코인 잔고 확인 | ||
| const { data: coinWallet } = await supabase | ||
| .from("wallet") | ||
| .select("*") | ||
| .eq("user_id", userId) | ||
| .eq("coin_id", coin_id) | ||
| .single(); | ||
|
|
||
| if (!coinWallet || coinWallet.amount < amount) { | ||
| return NextResponse.json({ error: "코인 잔고가 부족합니다." }, { status: 400 }); | ||
| } | ||
|
|
||
| // 2. 코인 차감 | ||
| const { error: coinUpdateError } = await supabase | ||
| .from("wallet") | ||
| .update({ amount: coinWallet.amount - amount }) | ||
| .eq("id", coinWallet.id); | ||
|
|
||
| if (coinUpdateError) { | ||
| console.error("Coin update error:", coinUpdateError); | ||
| return NextResponse.json({ error: coinUpdateError.message }, { status: 500 }); | ||
| } | ||
|
|
||
| // 3. KRW 증액 | ||
| const { data: krwWallet } = await supabase | ||
| .from("wallet") | ||
| .select("*") | ||
| .eq("user_id", userId) | ||
| .eq("coin_id", "KRW") | ||
| .single(); | ||
|
|
||
| if (krwWallet) { | ||
| 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 }); | ||
| } | ||
| } else { | ||
| // KRW 지갑이 없으면 생성 (일반적으로 있어야 하지만 예외 처리) | ||
| const { error: krwInsertError } = await supabase | ||
| .from("wallet") | ||
| .insert({ user_id: userId, coin_id: "KRW", amount: total_krw }); | ||
|
|
||
| if (krwInsertError) { | ||
| console.error("KRW insert error:", krwInsertError); | ||
| return NextResponse.json({ error: krwInsertError.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.
| if (existingWallet) { | ||
| // 기존 지갑이 있으면 금액 증가 | ||
| const newAmount = existingWallet.amount + amount; | ||
| const { data, error } = await supabase | ||
| .from("wallet") | ||
| .update({ amount: newAmount }) | ||
| .eq("id", existingWallet.id) | ||
| .select() | ||
| .single(); |
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.
입금 처리 로직에 경쟁 상태(Race Condition)가 발생할 수 있습니다. 기존 지갑 잔액을 조회(select)한 후, 애플리케이션 코드에서 새로운 잔액을 계산하여 갱신(update)하고 있습니다. 만약 두 개의 입금 요청이 동시에 들어오면, 하나의 요청 결과가 다른 요청에 의해 덮어쓰여 입금이 누락될 수 있습니다. 이는 데이터 정합성을 깨뜨리는 심각한 문제입니다. update wallet set amount = amount + <입금액> where ... 와 같이 데이터베이스에서 원자적으로 덧셈 연산을 수행하도록 수정해야 합니다. Supabase에서는 RPC를 통해 이를 구현할 수 있습니다.
| setAmount(""); | ||
| setIsOpen(false); | ||
| // 페이지 새로고침으로 지갑 데이터 업데이트 | ||
| window.location.reload(); |
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.
입금 성공 후 window.location.reload()를 호출하여 데이터를 갱신하는 방식은 전체 페이지를 새로고침하기 때문에 사용자 경험을 저해하며, SPA(Single Page Application)의 장점을 활용하지 못하는 방식입니다. useQueryClient를 사용하여 관련 쿼리를 무효화(invalidate)하여 필요한 데이터만 다시 가져오도록 수정하는 것이 좋습니다.
// 1. useQueryClient import
import { useQueryClient } from "@tanstack/react-query";
export const CashSection = () => {
// 2. 컴포넌트 상단에 queryClient 선언
const queryClient = useQueryClient();
// ...
const { mutate: deposit, isPending } = useMutation({
mutationFn: (amount: number) => depositAPI(amount),
onSuccess: () => {
toast.success(`${amount.toLocaleString()}원이 입금되었습니다!`);
setAmount("");
setIsOpen(false);
// 3. 'wallet'과 'trades' 쿼리를 무효화하여 데이터 리프레시
queryClient.invalidateQueries({ queryKey: ["wallet"] });
queryClient.invalidateQueries({ queryKey: ["trades"] });
},
// ...
});
// ...
}| 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 }); |
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: "..." }와 같은 형태로 통일하는 것을 권장합니다.
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | |
| return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); |
| 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 }); | ||
| } | ||
| } |
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 }); | |
| } | |
| } |
| export const holdDepositAPI = async () => { | ||
| const response = await fetch("/api/wallet/deposit", { cache: "no-store" }); | ||
| if (!response.ok) { | ||
| const error = await response.json(); | ||
| throw new Error(error.error || "Hold deposit fetch failed"); | ||
| } | ||
| return response.json(); | ||
| }; |
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.
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.
Pull Request Overview
This PR implements a real-time wallet system with multi-coin trading support, profit/loss tracking using moving average cost basis calculation, and consolidates market API endpoints. The implementation introduces Supabase Realtime subscriptions for instant wallet updates and comprehensive trade tracking.
- Adds real-time wallet synchronization using Supabase Realtime and React Query polling
- Implements moving average cost basis calculation for accurate profit/loss tracking across trades
- Introduces multi-coin trading support with dynamic coin selection and real-time price updates
- Consolidates market APIs by merging
/api/marketsfunctionality into/api/marketwith query parameters
Reviewed Changes
Copilot reviewed 42 out of 44 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/components/ui/table.tsx | Updates CSS selectors for checkbox styling in table components |
| src/shared/components/ui/dialog.tsx | Adds new Dialog component using Radix UI primitives |
| src/shared/components/ui/index.ts | Exports new Dialog component |
| src/features/wallet/utils/aggregate-trades.ts | Implements moving average cost basis calculation algorithm for trade aggregation |
| src/features/wallet/types/wallet.type.ts | Defines WalletEntity type based on database schema |
| src/features/wallet/store/useWalletStore.ts | Creates Zustand store for wallet state management with profit/loss calculations |
| src/features/wallet/hooks/useRealtimeWallet.ts | Implements real-time wallet synchronization with Supabase Realtime and polling |
| src/features/wallet/hooks/useGetHoldDeposit.ts | Adds hook for fetching deposit holdings with 15-second refresh interval |
| src/features/wallet/apis/*.ts | Adds API client functions for wallet, deposits, and trades |
| src/features/wallet/ui/StatsAreaSection.tsx | Updates stats display to show real-time calculated values from store |
| src/features/wallet/ui/HeldAssetsListSection.tsx | Replaces mock data with real wallet holdings and profit/loss calculations |
| src/features/wallet/ui/CashSection.tsx | Adds KRW deposit dialog with input validation |
| src/features/home/hooks/useRealtimeTicker.ts | Creates hook for subscribing to real-time ticker price updates |
| src/features/home/components/features/order/OrderForm.tsx | Enhances order form with coin selection, balance display, and reactive price calculations |
| src/features/home/components/common/field/*.tsx | Updates form fields to support value change callbacks and dynamic calculations |
| src/features/home/apis/*.ts | Adds API clients for ticker and markets data |
| src/features/signup/ui/SignupForm.tsx | Integrates profile creation during signup flow |
| src/entities/user/model/apis/create-profile.api.ts | Implements profile creation API for new users |
| src/app/wallet/page.tsx | Adds CashSection to wallet page layout |
| src/app/api/wallet/deposit/route.ts | Creates deposit endpoint with GET/POST for KRW deposits and wallet queries |
| src/app/api/trade/route.ts | Extends trade endpoint to handle wallet balance updates for buy/sell operations |
| src/app/api/ticker/route.ts | Adds ticker proxy endpoint for Upbit real-time prices |
| src/app/api/market/route.ts | Consolidates market APIs with type parameter for list vs detailed views |
| package.json, pnpm-lock.yaml | Adds @radix-ui/react-dialog dependency |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| if (coinSelectError) { | ||
| // 데이터 없음(PGRST116)이면 KRW를 생성 시도 | ||
| if ((coinSelectError as { code?: string }).code === "PGRST116") { |
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.
| if ((coinSelectError as { code?: string }).code === "PGRST116") { | |
| if ( | |
| coinSelectError && | |
| typeof coinSelectError === "object" && | |
| "code" in coinSelectError && | |
| typeof (coinSelectError as any).code === "string" && | |
| (coinSelectError as any).code === "PGRST116" | |
| ) { |
| // 거래 데이터 삽입 | ||
| const { data: tradeData, error: tradeError } = await supabase | ||
| .from("trade") | ||
| .insert({ | ||
| user_id: userData.user.id, | ||
| user_id: userId, | ||
| coin_id, | ||
| price, | ||
| amount, |
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.
|
|
||
| form.reset(); | ||
| handleReset(); | ||
| refetchWallet(); // 지갑 정보 재조회 |
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 refetchWallet call after successful order is unnecessary because the component already uses useRealtimeWallet() hook which subscribes to Supabase Realtime changes and automatically updates when the wallet changes. This creates redundant network requests.
| refetchWallet(); // 지갑 정보 재조회 |
| // 즉시 실행 | ||
| syncData(); | ||
|
|
||
| // TODO: 구현 후 다시 1000 으로 수정 |
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 TODO comment mentions changing back to 1000ms after implementation, but the interval setting code is not visible in the diff. If this interval needs to be changed, please ensure it's updated or remove the TODO comment if no longer applicable.
| // TODO: 구현 후 다시 1000 으로 수정 |
| 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 }); | ||
| } | ||
|
|
||
| // 3. 코인 증액 (없으면 생성) | ||
| const { data: coinWallet } = await supabase | ||
| .from("wallet") | ||
| .select("*") | ||
| .eq("user_id", userId) | ||
| .eq("coin_id", coin_id) | ||
| .single(); | ||
|
|
||
| if (coinWallet) { | ||
| // 기존 코인 지갑 증액 | ||
| const { error: coinUpdateError } = await supabase | ||
| .from("wallet") | ||
| .update({ amount: coinWallet.amount + amount }) | ||
| .eq("id", coinWallet.id); | ||
|
|
||
| if (coinUpdateError) { | ||
| console.error("Coin update error:", coinUpdateError); | ||
| return NextResponse.json({ error: coinUpdateError.message }, { status: 500 }); | ||
| } | ||
| } else { | ||
| // 새 코인 지갑 생성 | ||
| // coins 테이블에 코인이 없으면 생성 | ||
| const { error: coinSelectError } = await supabase | ||
| .from("coins") | ||
| .select("market_id") | ||
| .eq("market_id", coin_id) | ||
| .single(); | ||
|
|
||
| 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 }); | ||
| } | ||
| } | ||
|
|
||
| const { error: walletInsertError } = await supabase.from("wallet").insert({ user_id: userId, coin_id, amount }); | ||
|
|
||
| if (walletInsertError) { | ||
| console.error("Wallet insert error:", walletInsertError); | ||
| return NextResponse.json({ error: walletInsertError.message }, { status: 500 }); | ||
| } | ||
| } | ||
| } else if (trade_type === "sell") { | ||
| // 매도: 코인 차감, KRW 증액 | ||
| // 1. 코인 잔고 확인 | ||
| const { data: coinWallet } = await supabase | ||
| .from("wallet") | ||
| .select("*") | ||
| .eq("user_id", userId) | ||
| .eq("coin_id", coin_id) | ||
| .single(); | ||
|
|
||
| if (!coinWallet || coinWallet.amount < amount) { | ||
| return NextResponse.json({ error: "코인 잔고가 부족합니다." }, { status: 400 }); | ||
| } | ||
|
|
||
| // 2. 코인 차감 | ||
| const { error: coinUpdateError } = await supabase | ||
| .from("wallet") | ||
| .update({ amount: coinWallet.amount - amount }) | ||
| .eq("id", coinWallet.id); | ||
|
|
||
| if (coinUpdateError) { | ||
| console.error("Coin update error:", coinUpdateError); | ||
| return NextResponse.json({ error: coinUpdateError.message }, { status: 500 }); | ||
| } | ||
|
|
||
| // 3. KRW 증액 | ||
| const { data: krwWallet } = await supabase | ||
| .from("wallet") | ||
| .select("*") | ||
| .eq("user_id", userId) | ||
| .eq("coin_id", "KRW") | ||
| .single(); | ||
|
|
||
| if (krwWallet) { | ||
| 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 }); | ||
| } | ||
| } else { | ||
| // KRW 지갑이 없으면 생성 (일반적으로 있어야 하지만 예외 처리) | ||
| const { error: krwInsertError } = await supabase | ||
| .from("wallet") | ||
| .insert({ user_id: userId, coin_id: "KRW", amount: total_krw }); | ||
|
|
||
| if (krwInsertError) { | ||
| console.error("KRW insert error:", krwInsertError); | ||
| return NextResponse.json({ error: krwInsertError.message }, { status: 500 }); | ||
| } | ||
| } | ||
| } |
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.
| data-slot='table-head' | ||
| className={cn( | ||
| "h-10 p-2 text-left align-middle text-xs font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", | ||
| "h-10 p-2 text-left align-middle text-xs font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-0.5", |
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 CSS selector syntax *:[[role=checkbox]] appears to be incorrect. It should be [&>[role=checkbox]] to properly select child elements with role="checkbox". The current syntax with *:[[role=checkbox]] is invalid and will not apply the intended styles.
| router.push(ROUTER_PATH.LOGIN); | ||
| form.reset(); | ||
| } catch (err: unknown) { | ||
| const message = err instanceof Error ? err.message : "프로필 생성 중 오류가 발생했습니다."; |
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 handling for createProfileAPI displays "프로필 생성 중 오류가 발생했습니다" (Error occurred during profile creation), but this is too generic. The error message should provide more context about what went wrong, especially since the user has already completed signup. Consider including the actual error message from the API or providing actionable guidance.
| const message = err instanceof Error ? err.message : "프로필 생성 중 오류가 발생했습니다."; | |
| let message = "프로필 생성 중 오류가 발생했습니다."; | |
| // Try to extract a more specific error message from API error responses | |
| if (err && typeof err === "object") { | |
| // For Axios or fetch errors, check for response.data.message or response.message | |
| const anyErr = err as any; | |
| if (anyErr.response?.data?.message) { | |
| message = anyErr.response.data.message; | |
| } else if (anyErr.response?.message) { | |
| message = anyErr.response.message; | |
| } else if (anyErr.message) { | |
| message = anyErr.message; | |
| } | |
| } |
| // 페이지 새로고침으로 지갑 데이터 업데이트 | ||
| window.location.reload(); |
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.
Using window.location.reload() causes a full page refresh, which defeats the purpose of using React Query for data management. Instead, use the refetch function from the wallet query or rely on the Supabase Realtime subscription already implemented in useRealtimeWallet to automatically update the UI after deposit.
| showCloseButton?: boolean; | ||
| }) { | ||
| return ( | ||
| <DialogPortal data-slot='dialog-portal'> |
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 data-slot attribute is being set on DialogPortal but DialogPrimitive.Portal doesn't accept this prop. This will cause a React warning about unknown DOM properties. The data-slot attribute should only be applied to actual DOM elements, not to Portal components.
| <FormItem className='flex w-full gap-1'> | ||
| <FormLabel className='text-md w-25 gap-1'> | ||
| <p>수량</p> | ||
| <p className='text-[10px] font-bold text-text-dark'>(WAXP)</p> |
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 hardcoded coin label "(WAXP)" in the Amount field doesn't match the dynamically selected coin. This should be updated to display the actual selected coin symbol, similar to how it's done for the "주문가능" display on line 137.
| <p className='text-[10px] font-bold text-text-dark'>(WAXP)</p> | |
| <p className='text-[10px] font-bold text-text-dark'>({form.watch("coin")?.symbol})</p> |
📝 요약 (Summary)
지갑 페이지에 실시간 평가/손익 계산을 도입하고, 다중 코인 거래를 지원하도록 주문/지갑 흐름을 확장했습니다. 또한 중복된 마켓 라우트를 통합해 API 사용성을 정리했습니다.
✅ 주요 변경 사항 (Key Changes)
wallet테이블 변경 즉시 UI 갱신/api/market단일 엔드포인트로 통합 (?type=list로 목록 제공),/api/markets제거costBasisMap및 합산 유틸 추가 (총 매수/손익/수익률 정확화)"use client"), Next 캐시 이슈 정리(.next삭제)💻 상세 구현 내용 (Implementation Details)
1) 데이터 수집 & 실시간 동기화
wallet변경 시 React Queryrefetch수행wallet-changes-{userId}schema=public, table=wallet, user_id=eq.{userId}, 이벤트:*2) 평균매수가/총원가 집계 (이동평균법)
computeCostBasis(trades)→{ [coin_id]: { avgPrice, quantity, totalCost } }avgPrice × 남은수량으로 조정totalCost현재 평가액 − totalCost평가손익 ÷ totalCost × 1003) 합산 지표 계산 정의
totalCost합 (KRW 제외)4) API 정리
/api/market(통합)GET /api/market?type=list→ KRW 마켓 목록만 반환/api/markets제거 및 클라이언트 호출 경로 전면 교체5) 주요 파일
src/features/wallet/hooks/useRealtimeWallet.ts: 지갑/시세/거래내역 실시간 동기화 + Realtime 구독src/features/wallet/store/useWalletStore.ts:costBasisMap,getTotalPurchase,getTotalProfitLoss,getTotalProfitRate,getTotalCoinEvaluation추가/수정src/features/wallet/utils/aggregate-trades.ts: 이동평균법 집계기src/features/wallet/apis/trades.api.ts: 사용자 거래내역 조회src/features/home/apis/markets.api.ts:/api/market?type=list로 경로 수정src/features/wallet/ui/StatsAreaSection.tsx: 총계 지표 계산 정확화src/features/wallet/ui/HeldAssetsListSection.tsx: 자산별 매수금액/손익(%)·KRW 실시간 표시🚀 트러블 슈팅 (Trouble Shooting)
useEffect) 사용 파일에"use client"누락 → 지시어 추가로 해결/api/markets제거 후.next캐시 참조로 컴파일 실패 →.next삭제 후 재빌드로 해결📸 스크린샷 (Screenshots)
(필요 시 이미지 추가)
#️⃣ 관련 이슈 (Related Issues)