-
Notifications
You must be signed in to change notification settings - Fork 0
지갑 자산 계산 로직 리팩토링 및 실시간 동기화 구현 #14
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
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
855c705
chore: baseline-browser-mapping 설치
Dobbymin 4fca6b8
feat: 사용자 지갑 총액 api 구현
Dobbymin c414229
feat: 지갑의 user type 추가
Dobbymin 63a4940
refactor: 입금하기 api 구조 수정
Dobbymin 7226204
refactor: 보유 자산 관리 api 구조 수정
Dobbymin 5e812c3
refactor: 쿼리무효화로 보유 자산 초기화
Dobbymin b31f8f6
feat: 쿼리무효화 사용을 위한 설정
Dobbymin ab34223
refactor: 코드 구조 변경
Dobbymin 5964bf5
refactor: 사용자 보유 자산 기능 수정
Dobbymin fc97875
refactor: 실시간 코인 시세 반영한 보유 자산 기능 수정
Dobbymin e4ab962
refactor: 코인 매매 기능 수정
Dobbymin ffa85d2
fix: 보유 자산 로직 수정
Dobbymin ae09c6a
refactor: 보유 자산 커스텀 훅 구현
Dobbymin 1d4d5bc
fix: 이전 코드로 롤백
Dobbymin 6dca65d
fix: 실시간 지갑 보유 자산 업데이트 기능 구현
Dobbymin 3f6d008
fix: 순환참조 오류 수정
Dobbymin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { cookies } from "next/headers"; | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { getUserBalanceService } from "@/entities"; | ||
| import { APIResponse } from "@/shared"; | ||
|
|
||
| import { createClient as createServerClient } from "@/shared/utils/supabase/server"; | ||
|
|
||
| export async function GET(request: Request) { | ||
| try { | ||
| const supabase = createServerClient(cookies()); | ||
|
|
||
| const { | ||
| data: { user }, | ||
| error: userError, | ||
| } = await supabase.auth.getUser(); | ||
|
|
||
| if (userError || !user) { | ||
| return NextResponse.json<APIResponse<null>>( | ||
| { status: "error", data: null, error: "Unauthorized" }, | ||
| { status: 401 }, | ||
| ); | ||
| } | ||
|
|
||
| const { searchParams } = new URL(request.url); | ||
| const coin_id = searchParams.get("coin_id"); | ||
|
|
||
| const walletData = await getUserBalanceService({ | ||
| client: supabase, | ||
| userId: user.id, | ||
| coinId: coin_id, | ||
| }); | ||
|
|
||
| return NextResponse.json<APIResponse<typeof walletData>>({ status: "success", data: walletData }, { status: 200 }); | ||
| } catch (error) { | ||
| console.error("Wallet GET API error:", error); | ||
|
|
||
| const errorMessage = error instanceof Error ? error.message : "Internal server error"; | ||
|
|
||
| return NextResponse.json<APIResponse<null>>({ status: "error", data: null, error: errorMessage }, { status: 500 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,158 +1,51 @@ | ||
| import { cookies } from "next/headers"; | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { depositService } from "@/entities"; | ||
| import { APIResponse } from "@/shared"; | ||
|
|
||
| import { createClient as createServerClient } from "@/shared/utils/supabase/server"; | ||
|
|
||
| export async function POST(request: Request) { | ||
| try { | ||
| const supabase = createServerClient(cookies()); | ||
| const { data: userData, error: userError } = await supabase.auth.getUser(); | ||
|
|
||
| if (userError || !userData.user) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| const { | ||
| data: { user }, | ||
| error: userError, | ||
| } = await supabase.auth.getUser(); | ||
| if (userError || !user) { | ||
| return NextResponse.json<APIResponse<null>>( | ||
| { status: "error", data: null, error: "Unauthorized" }, | ||
| { status: 401 }, | ||
| ); | ||
| } | ||
|
|
||
| const body = await request.json(); | ||
| const { amount } = body; | ||
|
|
||
| // 필수 파라미터 검증 | ||
| if (!amount || amount <= 0) { | ||
| return NextResponse.json({ error: "Invalid amount" }, { status: 400 }); | ||
| } | ||
|
|
||
| const coin_id = "KRW"; // KRW 입금 - coins.market_id를 참조 | ||
|
|
||
| // FK 제약사항 대응: coins 테이블에 KRW가 없으면 생성 | ||
| const { error: coinSelectError } = await supabase | ||
| .from("coins") | ||
| .select("market_id") | ||
| .eq("market_id", coin_id) | ||
| .single(); | ||
|
|
||
| if (coinSelectError) { | ||
| // 데이터 없음(PGRST116)이면 KRW를 생성 시도 | ||
| if ((coinSelectError as { code?: string }).code === "PGRST116") { | ||
| const { error: coinInsertError } = await supabase | ||
| .from("coins") | ||
| .insert({ market_id: coin_id, korean_name: "원화", english_name: "Korean Won" }); | ||
| if (coinInsertError) { | ||
| console.error("KRW coin insert error:", coinInsertError); | ||
| return NextResponse.json({ error: coinInsertError.message }, { status: 500 }); | ||
| } | ||
| } else { | ||
| console.error("KRW coin select error:", coinSelectError); | ||
| return NextResponse.json({ error: coinSelectError.message }, { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| // FK 제약사항 대응: users 테이블에 사용자가 없으면 생성 (Supabase Auth에는 있지만 public.users 테이블에 없을 경우) | ||
| const { error: userSelectError } = await supabase.from("profiles").select("id").eq("id", userData.user.id).single(); | ||
|
|
||
| if (userSelectError) { | ||
| if ((userSelectError as { code?: string }).code === "PGRST116") { | ||
| // users 테이블에 사용자 정보가 없으면 생성 | ||
| const { error: userInsertError } = await supabase.from("profiles").insert({ | ||
| id: userData.user.id, | ||
| email: userData.user.email || "", | ||
| nickname: userData.user.user_metadata?.nickname || "User", | ||
| user_name: userData.user.user_metadata?.full_name || "User", // profiles 테이블의 user_name 필드 추가 | ||
| }); | ||
| if (userInsertError) { | ||
| console.error("User insert error:", userInsertError); | ||
| return NextResponse.json({ error: `Failed to sync user: ${userInsertError.message}` }, { status: 500 }); | ||
| } | ||
| } else { | ||
| console.error("User check error:", userSelectError); | ||
| return NextResponse.json({ error: userSelectError.message }, { status: 500 }); | ||
| } | ||
| if (!amount || typeof amount !== "number" || amount <= 0) { | ||
| return NextResponse.json<APIResponse<null>>( | ||
| { status: "error", data: null, error: "Invalid amount" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| // 기존 KRW 지갑 조회 | ||
| const { data: existingWallet, error: walletError } = await supabase | ||
| .from("wallet") | ||
| .select("*") | ||
| .eq("user_id", userData.user.id) | ||
| .eq("coin_id", coin_id) | ||
| .single(); | ||
|
|
||
| if (walletError && walletError.code !== "PGRST116") { | ||
| // PGRST116은 데이터 없음 오류 | ||
| console.error("Wallet query error:", walletError); | ||
| return NextResponse.json({ error: walletError.message }, { status: 500 }); | ||
| } | ||
|
|
||
| let result; | ||
|
|
||
| if (existingWallet) { | ||
| // 기존 지갑이 있으면 금액 증가 | ||
| const newAmount = existingWallet.amount + amount; | ||
| const { data, error } = await supabase | ||
| .from("wallet") | ||
| .update({ amount: newAmount }) | ||
| .eq("id", existingWallet.id) | ||
| .select() | ||
| .single(); | ||
|
|
||
| if (error) { | ||
| console.error("Wallet update error:", error); | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| result = data; | ||
| } else { | ||
| // 새 지갑 생성 | ||
| const { data, error } = await supabase | ||
| .from("wallet") | ||
| .insert({ | ||
| user_id: userData.user.id, | ||
| coin_id, | ||
| amount, | ||
| }) | ||
| .select() | ||
| .single(); | ||
|
|
||
| if (error) { | ||
| console.error("Wallet insert error:", error); | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| result = data; | ||
| } | ||
| const result = await depositService({ | ||
| client: supabase, | ||
| user: { | ||
| id: user.id, | ||
| email: user.email, | ||
| user_metadata: user.user_metadata, | ||
| }, | ||
| amount, | ||
| }); | ||
|
|
||
| return NextResponse.json({ success: true, data: result }, { status: 200 }); | ||
| return NextResponse.json<APIResponse<typeof result>>({ status: "success", data: result }, { status: 200 }); | ||
| } catch (error) { | ||
| console.error("Deposit API error:", error); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| // 사용자의 지갑 조회 | ||
| export async function GET(request: Request) { | ||
| try { | ||
| const supabase = createServerClient(cookies()); | ||
| const { data: userData, error: userError } = await supabase.auth.getUser(); | ||
|
|
||
| if (userError || !userData.user) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { searchParams } = new URL(request.url); | ||
| const coin_id = searchParams.get("coin_id"); | ||
| const errorMessage = error instanceof Error ? error.message : "Internal server error"; | ||
|
|
||
| let query = supabase.from("wallet").select("*").eq("user_id", userData.user.id); | ||
|
|
||
| if (coin_id) { | ||
| query = query.eq("coin_id", coin_id); | ||
| } | ||
|
|
||
| const { data, error } = await query; | ||
|
|
||
| if (error) { | ||
| console.error("Wallet query error:", error); | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
|
|
||
| return NextResponse.json({ success: true, data }, { status: 200 }); | ||
| } catch (error) { | ||
| console.error("Wallet GET API error:", error); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| return NextResponse.json<APIResponse<null>>({ status: "error", data: null, error: errorMessage }, { status: 500 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export * from "./model"; | ||
| export * from "./server"; | ||
| export * from "./types"; |
2 changes: 1 addition & 1 deletion
2
src/features/wallet/apis/deposit.api.ts → ...entities/wallet/model/apis/deposit.api.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from "./user-balance.api"; | ||
| export * from "./deposit.api"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { APIResponse } from "@/shared"; | ||
|
|
||
| interface UserBalanceResponse { | ||
| id: number; | ||
| user_id: string; | ||
| coin_id: string; | ||
| amount: number; | ||
| } | ||
|
|
||
| export const userBalanceAPI = async (coinId?: string) => { | ||
| const queryParams = coinId ? `?coin_id=${coinId}` : ""; | ||
|
|
||
| const response = await fetch(`/api/wallet/balance${queryParams}`); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error("Failed to fetch user balance"); | ||
| } | ||
|
|
||
| const result: APIResponse<UserBalanceResponse[]> = await response.json(); | ||
|
|
||
| if (result.status === "error") { | ||
| throw new Error(result.error); | ||
| } | ||
|
|
||
| return result.data; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./apis"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| export interface WalletStatsEntity { | ||
| id: string; | ||
| user_id: string; | ||
| total_assets: number; | ||
| total_purchase: number; | ||
| total_evaluation: number; | ||
| total_profit_loss: number; | ||
| total_profit_rate: number; | ||
| held_krw: number; | ||
| updated_at: string; | ||
| created_at: string; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { Database } from "@/shared"; | ||
| import { SupabaseClient } from "@supabase/supabase-js"; | ||
|
|
||
| import { WalletUser } from "../types"; | ||
|
|
||
| export const ensureKrwCoinExists = async (client: SupabaseClient<Database>) => { | ||
| const COIN_ID = "KRW"; | ||
|
|
||
| const { error: selectError } = await client.from("coins").select("market_id").eq("market_id", COIN_ID).single(); | ||
|
|
||
| if (selectError?.code === "PGRST116") { | ||
| const { error: insertError } = await client | ||
| .from("coins") | ||
| .insert({ market_id: COIN_ID, korean_name: "원화", english_name: "Korean Won" }); | ||
|
|
||
| if (insertError) throw new Error(`Failed to create KRW coin: ${insertError.message}`); | ||
| } | ||
| }; | ||
|
|
||
| export const ensureProfileExists = async (client: SupabaseClient<Database>, user: WalletUser) => { | ||
| const { error: selectError } = await client.from("profiles").select("id").eq("id", user.id).single(); | ||
|
|
||
| if (selectError?.code === "PGRST116") { | ||
| const { error: insertError } = await client.from("profiles").insert({ | ||
| id: user.id, | ||
| email: user.email || "", | ||
| nickname: user.user_metadata?.nickname || "User", | ||
| user_name: user.user_metadata?.full_name || "User", | ||
| }); | ||
|
|
||
| if (insertError) throw new Error(`Failed to sync user profile: ${insertError.message}`); | ||
| } | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
[nitpick] The
--webpackflag was added to the dev script. This flag is typically used to force Next.js to use webpack instead of Turbopack (the default in Next.js 15+).While this may have been added to resolve a specific issue, it's worth noting that:
If this change is necessary due to compatibility issues with dependencies, please add a comment explaining why.