Skip to content

Conversation

@Dobbymin
Copy link
Owner

📝 요약 (Summary)

지갑 페이지에 실시간 평가/손익 계산을 도입하고, 다중 코인 거래를 지원하도록 주문/지갑 흐름을 확장했습니다. 또한 중복된 마켓 라우트를 통합해 API 사용성을 정리했습니다.

✅ 주요 변경 사항 (Key Changes)

  • 지갑 실시간 반영: Supabase Realtime(postgres_changes)로 wallet 테이블 변경 즉시 UI 갱신
  • 평가/손익 계산 고도화: 거래내역 집계(이동평균법) 기반 평균매수가/총원가 → 손익(%)·KRW 계산
  • 다중 코인 지원: 마켓 목록 + 코인 선택 컴포넌트, 주문 폼 연동, 실시간 시세 반영
  • 마켓 API 통합: /api/market 단일 엔드포인트로 통합 (?type=list로 목록 제공), /api/markets 제거
  • 상태 관리 확장: Zustand store에 costBasisMap 및 합산 유틸 추가 (총 매수/손익/수익률 정확화)
  • 안정성: 빌드 오류 수정("use client"), Next 캐시 이슈 정리(.next 삭제)

💻 상세 구현 내용 (Implementation Details)

1) 데이터 수집 & 실시간 동기화

  • 실시간 시세: Upbit Ticker를 1초 간격으로 조회하여 현재가 반영
  • 지갑 실시간: Supabase Realtime 구독으로 wallet 변경 시 React Query refetch 수행
    • 채널: wallet-changes-{userId}
    • 필터: schema=public, table=wallet, user_id=eq.{userId}, 이벤트: *
    • 결과: 입금/매수/매도 시 보유자산 목록이 즉시 업데이트

2) 평균매수가/총원가 집계 (이동평균법)

  • 유틸: computeCostBasis(trades){ [coin_id]: { avgPrice, quantity, totalCost } }
  • 로직
    • buy: (기존총원가 + 매수금액) / (기존수량 + 매수수량)
    • sell: 평균단가 유지, 수량 감소, 총원가는 avgPrice × 남은수량으로 조정
  • 사용처
    • 보유자산 목록: 매수금액(KRW) = totalCost
    • 평가손익(KRW) = 현재 평가액 − totalCost
    • 평가손익(%) = 평가손익 ÷ totalCost × 100

3) 합산 지표 계산 정의

  • 총 보유 자산 = 보유 KRW + 코인 평가액 합
  • 총 매수 = 현재 보유 코인들의 totalCost 합 (KRW 제외)
  • 총 평가 손익 = 코인 평가액 합 − 총 매수 (KRW 제외)
  • 총 평가 수익률 = (총 평가 손익 ÷ 총 매수) × 100

4) API 정리

  • /api/market (통합)
    • 기본: 마켓 정보 + 시세 요약
    • GET /api/market?type=list → KRW 마켓 목록만 반환
  • /api/markets 제거 및 클라이언트 호출 경로 전면 교체

5) 주요 파일

  • Hooks
    • src/features/wallet/hooks/useRealtimeWallet.ts: 지갑/시세/거래내역 실시간 동기화 + Realtime 구독
  • Store
    • src/features/wallet/store/useWalletStore.ts: costBasisMap, getTotalPurchase, getTotalProfitLoss, getTotalProfitRate, getTotalCoinEvaluation 추가/수정
  • Utils / APIs
    • 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로 경로 수정
  • UI
    • src/features/wallet/ui/StatsAreaSection.tsx: 총계 지표 계산 정확화
    • src/features/wallet/ui/HeldAssetsListSection.tsx: 자산별 매수금액/손익(%)·KRW 실시간 표시

🚀 트러블 슈팅 (Trouble Shooting)

  • Next 빌드 오류: Client Hook(useEffect) 사용 파일에 "use client" 누락 → 지시어 추가로 해결
  • 삭제된 라우트 참조로 타입 체커 실패: /api/markets 제거 후 .next 캐시 참조로 컴파일 실패 → .next 삭제 후 재빌드로 해결
  • React Compiler 경고: client 컴포넌트의 함수형 prop(serializable 아님) 경고 → 실제 런타임/빌드에는 영향 없음(현시점 false positive로 인지)

⚠️ 알려진 이슈 및 참고 사항 (Known Issues & Notes)

  • 현재 총 평가 손익과 수익률 부분의 데이터가 정확하게 반영되지 않는 문제가 보이고 있어 추후 수정해야합니다.

📸 스크린샷 (Screenshots)

스크린샷 2025-11-17 오후 2 00 27

(필요 시 이미지 추가)

#️⃣ 관련 이슈 (Related Issues)

@Dobbymin Dobbymin requested a review from Copilot November 17, 2025 05:03
@Dobbymin Dobbymin self-assigned this Nov 17, 2025
@Dobbymin Dobbymin added ✨ Feature 새로운 기능 추가 및 구현하는 경우 📡 API 비동기 통신 코드를 짜는 경우 labels Nov 17, 2025
@Dobbymin Dobbymin linked an issue Nov 17, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Nov 17, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
dobbit Ready Ready Preview Comment Nov 17, 2025 5:11am

@gemini-code-assist
Copy link

Summary of Changes

Hello @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

  • 실시간 지갑 및 시세 동기화: Supabase Realtime(postgres_changes)을 활용하여 wallet 테이블 변경 사항을 즉시 UI에 반영하고, Upbit Ticker를 통해 1초 간격으로 실시간 시세를 업데이트합니다.
  • 고도화된 평가/손익 계산: 이동평균법 기반의 거래내역 집계를 통해 평균매수가, 총원가를 계산하고, 이를 바탕으로 손익(%) 및 KRW 평가액을 정확하게 산출합니다.
  • 다중 코인 거래 지원: 마켓 목록과 코인 선택 컴포넌트를 추가하고, 주문 폼과 실시간 시세를 연동하여 다양한 코인 거래를 지원하도록 확장했습니다.
  • API 엔드포인트 통합 및 정리: 기존 /api/markets 엔드포인트를 제거하고 /api/market 단일 엔드포인트로 통합하여 ?type=list 파라미터로 마켓 목록을 제공하도록 개선했습니다.
  • Zustand 상태 관리 확장: Zustand 스토어에 costBasisMap 및 총 매수, 총 손익, 총 수익률, 총 코인 평가액 등 합산 유틸리티를 추가하여 지갑 상태 관리를 강화했습니다.
  • 안정성 개선: Next.js 빌드 오류("use client" 누락) 및 캐시 참조 문제를 해결하여 애플리케이션의 안정성을 높였습니다.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a 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 응답 형식의 일관성, 프론트엔드의 데이터 갱신 방식 등에서 개선할 점들을 제안드렸습니다. 이 부분들을 보완한다면 더욱 안정적이고 완성도 높은 기능이 될 것입니다.

Comment on lines +32 to +157
// 매수/매도에 따른 지갑 잔고 확인 및 업데이트
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 });
}
}
}

Choose a reason for hiding this comment

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

critical

매수/매도 로직이 여러 개의 await를 포함한 DB 호출로 이루어져 있어 원자성(atomicity)을 보장하지 않습니다. 예를 들어, 매수 시 KRW 잔고를 차감한 후 코인 수량을 늘리는 데 실패하면 사용자의 자산에 불일치가 발생합니다. 이는 심각한 버그로 이어질 수 있습니다. 이와 같은 여러 단계의 DB 업데이트는 반드시 단일 트랜잭션으로 처리되어야 합니다. Supabase의 RPC(PostgreSQL 함수)를 사용하여 전체 거래 로직을 데이터베이스 내에서 원자적으로 실행하도록 리팩터링하는 것을 강력히 권장합니다.

Comment on lines +64 to +72
if (existingWallet) {
// 기존 지갑이 있으면 금액 증가
const newAmount = existingWallet.amount + amount;
const { data, error } = await supabase
.from("wallet")
.update({ amount: newAmount })
.eq("id", existingWallet.id)
.select()
.single();

Choose a reason for hiding this comment

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

critical

입금 처리 로직에 경쟁 상태(Race Condition)가 발생할 수 있습니다. 기존 지갑 잔액을 조회(select)한 후, 애플리케이션 코드에서 새로운 잔액을 계산하여 갱신(update)하고 있습니다. 만약 두 개의 입금 요청이 동시에 들어오면, 하나의 요청 결과가 다른 요청에 의해 덮어쓰여 입금이 누락될 수 있습니다. 이는 데이터 정합성을 깨뜨리는 심각한 문제입니다. update wallet set amount = amount + <입금액> where ... 와 같이 데이터베이스에서 원자적으로 덧셈 연산을 수행하도록 수정해야 합니다. Supabase에서는 RPC를 통해 이를 구현할 수 있습니다.

setAmount("");
setIsOpen(false);
// 페이지 새로고침으로 지갑 데이터 업데이트
window.location.reload();

Choose a reason for hiding this comment

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

high

입금 성공 후 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 });

Choose a reason for hiding this comment

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

medium

API 응답 형식을 일관성 있게 유지하는 것이 좋습니다. 성공 시에는 { success: true, ... }를 반환하지만, 에러 발생 시에는 { error: "..." }를 반환하고 있습니다. 다른 API와 마찬가지로 { success: false, error: "..." }와 같은 형태로 통일하는 것을 권장합니다.

Suggested change
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 });

Comment on lines +86 to +94
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 });
}
}

Choose a reason for hiding this comment

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

medium

새로운 코인을 coins 테이블에 추가할 때, korean_nameenglish_namecoin_id를 그대로 사용하고 있습니다. 이는 부정확한 데이터를 생성할 수 있습니다. Upbit API 등에서 정확한 코인 이름을 조회하여 채워 넣거나, 이것이 어렵다면 임시 데이터임을 명확히 알리는 주석과 함께 추후 수정할 수 있도록 TODO를 남겨두는 것이 좋습니다.

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

Comment on lines +1 to +8
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();
};

Choose a reason for hiding this comment

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

medium

hold-deposit.api.ts 파일과 holdDepositAPI 함수의 목적이 불분명하며, wallet.api.tsgetWalletAPI와 기능적으로 중복되는 것으로 보입니다. 특히 hold-deposit이라는 이름은 지갑 정보를 조회하는 GET 요청의 역할을 명확하게 설명하지 못해 혼란을 줄 수 있습니다. 코드의 유지보수성을 위해 사용되지 않는다면 삭제하고, 사용 중이라면 역할을 명확히 알 수 있도록 파일과 함수의 이름을 변경하는 것을 권장합니다. (예: getWalletSummary.api.ts)

Copy link

Copilot AI left a 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/markets functionality into /api/market with 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") {
Copy link

Copilot AI Nov 17, 2025

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.

Suggested change
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"
) {

Copilot uses AI. Check for mistakes.
Comment on lines 159 to 166
// 거래 데이터 삽입
const { data: tradeData, error: tradeError } = await supabase
.from("trade")
.insert({
user_id: userData.user.id,
user_id: userId,
coin_id,
price,
amount,
Copy link

Copilot AI Nov 17, 2025

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.

Copilot uses AI. Check for mistakes.

form.reset();
handleReset();
refetchWallet(); // 지갑 정보 재조회
Copy link

Copilot AI Nov 17, 2025

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.

Suggested change
refetchWallet(); // 지갑 정보 재조회

Copilot uses AI. Check for mistakes.
// 즉시 실행
syncData();

// TODO: 구현 후 다시 1000 으로 수정
Copy link

Copilot AI Nov 17, 2025

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.

Suggested change
// TODO: 구현 후 다시 1000 으로 수정

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +157
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 });
}
}
}
Copy link

Copilot AI Nov 17, 2025

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 uses AI. Check for mistakes.
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",
Copy link

Copilot AI Nov 17, 2025

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.

Copilot uses AI. Check for mistakes.
router.push(ROUTER_PATH.LOGIN);
form.reset();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "프로필 생성 중 오류가 발생했습니다.";
Copy link

Copilot AI Nov 17, 2025

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.

Suggested change
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;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +22
// 페이지 새로고침으로 지갑 데이터 업데이트
window.location.reload();
Copy link

Copilot AI Nov 17, 2025

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.

Copilot uses AI. Check for mistakes.
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot='dialog-portal'>
Copy link

Copilot AI Nov 17, 2025

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.

Copilot uses AI. Check for mistakes.
<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>
Copy link

Copilot AI Nov 17, 2025

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.

Suggested change
<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>

Copilot uses AI. Check for mistakes.
@Dobbymin Dobbymin changed the title Feat#05 coin order 지갑 페이지 및 실시간 변동사항 반영 구현 Nov 17, 2025
@Dobbymin Dobbymin merged commit 83cd063 into main Nov 17, 2025
3 checks passed
@Dobbymin Dobbymin deleted the feat#05-coin-order branch November 17, 2025 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📡 API 비동기 통신 코드를 짜는 경우 ✨ Feature 새로운 기능 추가 및 구현하는 경우

Projects

None yet

Development

Successfully merging this pull request may close these issues.

코인 주문하기 기능 구현 밒 지갑 페이지 연동

2 participants