Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev --webpack",
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

[nitpick] The --webpack flag 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:

  1. Turbopack is significantly faster for development
  2. This change should be documented in the PR description if it's intentional
  3. Consider if the underlying issue could be resolved without forcing webpack

If this change is necessary due to compatibility issues with dependencies, please add a comment explaining why.

Suggested change
"dev": "next dev --webpack",
"dev": "next dev",

Copilot uses AI. Check for mistakes.
"build": "next build",
"start": "next start",
"lint": "eslint",
Expand Down Expand Up @@ -45,6 +45,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"baseline-browser-mapping": "^2.8.32",
"eslint": "^9",
"eslint-config-next": "16.0.1",
"immer": "^10.2.0",
Expand Down
11 changes: 7 additions & 4 deletions pnpm-lock.yaml

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

5 changes: 3 additions & 2 deletions src/app/api/trade/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

import { toDisplayMarket, toUpbitMarket } from "@/entities";
import { toDisplayMarket } from "@/entities";

import { createClient as createServerClient } from "@/shared/utils/supabase/server";

Expand Down Expand Up @@ -97,7 +97,8 @@ export async function POST(request: Request) {
.eq("market_id", coin_id)
.single();

if ((coinSelectError as { code?: string }).code === "PGRST116") {
// PGRST116: 데이터가 없을 때 발생하는 에러 코드
if (coinSelectError && coinSelectError.code === "PGRST116") {
const { error: coinInsertError } = await supabase
.from("coins")
.insert({ market_id: coin_id, korean_name: coin_id, english_name: coin_id });
Expand Down
42 changes: 42 additions & 0 deletions src/app/api/wallet/balance/route.ts
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 });
}
}
165 changes: 29 additions & 136 deletions src/app/api/wallet/deposit/route.ts
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 });
}
}
1 change: 1 addition & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./auth";
export * from "./market";
export * from "./user";
export * from "./news";
export * from "./wallet";
3 changes: 3 additions & 0 deletions src/entities/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./model";
export * from "./server";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WalletEntity } from "../types";
import { WalletEntity } from "../../types";

export interface DepositParams {
amount: number;
Expand Down
2 changes: 2 additions & 0 deletions src/entities/wallet/model/apis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./user-balance.api";
export * from "./deposit.api";
26 changes: 26 additions & 0 deletions src/entities/wallet/model/apis/user-balance.api.ts
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;
};
1 change: 1 addition & 0 deletions src/entities/wallet/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./apis";
12 changes: 12 additions & 0 deletions src/entities/wallet/model/types/wallet-stats.type.ts
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;
}
33 changes: 33 additions & 0 deletions src/entities/wallet/server/deposit.helpers.ts
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}`);
}
};
Loading
Loading