Skip to content
Open
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
2 changes: 2 additions & 0 deletions week-05/dev/my-dapp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.next/
.env.local
141 changes: 141 additions & 0 deletions week-05/dev/my-dapp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Bay-17th dApp 프론트엔드 템플릿

Web3 dApp 개발을 위한 Next.js + wagmi + RainbowKit 스타터 템플릿입니다.

## 시작하기

### 1. 의존성 설치

```bash
npm install
```

### 2. WalletConnect Project ID 설정

1. [WalletConnect Cloud](https://cloud.walletconnect.com)에 접속
2. 회원가입 후 새 프로젝트 생성
3. `config/wagmi.ts` 파일에서 `YOUR_PROJECT_ID`를 발급받은 ID로 교체

```typescript
const WALLETCONNECT_PROJECT_ID = 'your-actual-project-id';
```

> **참고:** Project ID 없이도 개발 서버에서는 동작하지만, 프로덕션 배포 시 반드시 필요합니다.

### 3. 개발 서버 실행

```bash
npm run dev
```

브라우저에서 [http://localhost:3000](http://localhost:3000)을 열어 확인하세요.

## 파일 구조

```
frontend-template/
├── app/
│ ├── layout.tsx # 루트 레이아웃 (Provider 설정)
│ └── page.tsx # 메인 페이지
├── components/
│ └── WalletConnect.tsx # 지갑 연결 컴포넌트
├── config/
│ └── wagmi.ts # wagmi + RainbowKit 설정
├── package.json
├── tsconfig.json
└── next.config.mjs
```

## 주요 라이브러리

| 라이브러리 | 버전 | 설명 |
|-----------|------|------|
| wagmi | ^2.0.0 | React hooks for Ethereum |
| viem | ^2.0.0 | TypeScript Ethereum 라이브러리 |
| @rainbow-me/rainbowkit | ^2.0.0 | 지갑 연결 UI |
| @tanstack/react-query | ^5.0.0 | 데이터 페칭 상태 관리 |
| next | ^14.0.0 | React 프레임워크 |

## 컨트랙트 연동하기

### 1. 컨트랙트 읽기 (Read)

```typescript
import { useReadContract } from 'wagmi';

const { data, isLoading } = useReadContract({
address: '0x...', // 컨트랙트 주소
abi: contractABI, // ABI
functionName: 'getValue',
});
```

### 2. 컨트랙트 쓰기 (Write)

```typescript
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';

const { writeContract, data: hash } = useWriteContract();
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });

// 함수 호출
writeContract({
address: '0x...',
abi: contractABI,
functionName: 'setValue',
args: [42],
});
```

### 3. ABI 타입 안전하게 사용하기

```typescript
// ABI를 const로 선언하면 타입 추론이 자동으로 됩니다
const contractABI = [
{
name: 'getValue',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ type: 'uint256' }],
},
] as const; // 중요: as const
```

## 참고 자료

- [wagmi-basics.md](/week-04/dev/wagmi-basics.md) - wagmi 상세 가이드
- [rainbowkit-guide.md](/week-05/dev/rainbowkit-guide.md) - RainbowKit 상세 가이드
- [wagmi 공식 문서](https://wagmi.sh)
- [RainbowKit 공식 문서](https://www.rainbowkit.com)
- [viem 공식 문서](https://viem.sh)

## Sepolia 테스트넷 ETH 받기

개발 및 테스트에 필요한 테스트넷 ETH는 아래 Faucet에서 받을 수 있습니다:

- [Alchemy Sepolia Faucet](https://sepoliafaucet.com)
- [Infura Sepolia Faucet](https://www.infura.io/faucet/sepolia)
- [QuickNode Sepolia Faucet](https://faucet.quicknode.com/ethereum/sepolia)

## 문제 해결

### Hydration 오류

`config/wagmi.ts`에서 `ssr: true` 옵션이 설정되어 있는지 확인하세요.

### WalletConnect 연결 안 됨

Project ID가 올바르게 설정되어 있는지 확인하세요.

### BigInt 타입 오류

컨트랙트에서 반환된 숫자는 BigInt입니다. 문자열로 변환하려면:

```typescript
const valueString = data?.toString();
```

## 라이선스

Bay-17th 학회 교육용
91 changes: 91 additions & 0 deletions week-05/dev/my-dapp/app/api/transactions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from 'next/server';

const ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api';
const SEPOLIA_CHAIN_ID = '11155111';

interface EtherscanTx {
blockNumber: string;
hash: string;
timeStamp: string;
from: string;
to: string;
value: string;
isError: string;
}

interface EtherscanResponse {
status: string;
message: string;
result: EtherscanTx[] | string;
}

export async function GET(request: NextRequest) {
const address = request.nextUrl.searchParams.get('address');
const apiKey = process.env.ETHERSCAN_API_KEY;

if (!apiKey) {
return NextResponse.json(
{ error: 'ETHERSCAN_API_KEY가 설정되어 있지 않습니다.' },
{ status: 500 },
);
}

if (!address) {
return NextResponse.json(
{ error: 'address 쿼리 파라미터가 필요합니다.' },
{ status: 400 },
);
}

const params = new URLSearchParams({
chainid: SEPOLIA_CHAIN_ID,
module: 'account',
action: 'txlist',
address,
startblock: '0',
endblock: '99999999',
page: '1',
offset: '10',
sort: 'desc',
apikey: apiKey,
});

const response = await fetch(`${ETHERSCAN_API_URL}?${params.toString()}`, {
cache: 'no-store',
});

if (!response.ok) {
return NextResponse.json(
{ error: 'Etherscan API 호출에 실패했습니다.' },
{ status: 502 },
);
}

const rawBody = await response.text();

let data: EtherscanResponse;

try {
data = JSON.parse(rawBody) as EtherscanResponse;
} catch {
return NextResponse.json(
{ error: 'Etherscan API가 JSON이 아닌 응답을 반환했습니다.' },
{ status: 502 },
);
}

if (data.status !== '1') {
const message =
typeof data.result === 'string' ? data.result : data.message || '트랜잭션을 불러오지 못했습니다.';

if (message === 'No transactions found') {
return NextResponse.json({ items: [] });
}

return NextResponse.json({ error: message }, { status: 502 });
}

return NextResponse.json({
items: Array.isArray(data.result) ? data.result : [],
});
}
8 changes: 8 additions & 0 deletions week-05/dev/my-dapp/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
background: #ffffff;
color: #111827;
}
58 changes: 58 additions & 0 deletions week-05/dev/my-dapp/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

// ============================================================
// RainbowKit 스타일 import
// ============================================================
// RainbowKit의 UI 컴포넌트가 제대로 표시되려면
// 반드시 이 스타일시트를 import해야 합니다.
import '@rainbow-me/rainbowkit/styles.css';
import './globals.css';

import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { config } from '@/config/wagmi';

// ============================================================
// React Query 클라이언트 설정
// ============================================================
// wagmi v2는 내부적으로 TanStack Query를 사용합니다.
// 이 클라이언트가 데이터 캐싱, 리페칭, 동기화를 담당합니다.
const queryClient = new QueryClient();

// ============================================================
// Root Layout
// ============================================================
// Next.js App Router의 루트 레이아웃입니다.
// 모든 페이지가 이 레이아웃을 공유합니다.
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
{/* ============================================================
Provider 순서가 중요합니다!
============================================================

WagmiProvider > QueryClientProvider > RainbowKitProvider

1. WagmiProvider: 가장 바깥. wagmi config를 전체 앱에 제공
2. QueryClientProvider: 데이터 페칭 상태 관리
3. RainbowKitProvider: 지갑 UI 및 연결 상태 관리

순서가 바뀌면 "Cannot find WagmiContext" 같은 오류 발생!
============================================================ */}
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
</body>
</html>
);
}
28 changes: 28 additions & 0 deletions week-05/dev/my-dapp/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { WalletConnect } from '@/components/WalletConnect';

// ============================================================
// 메인 페이지
// ============================================================
// 이 페이지는 서버 컴포넌트입니다.
// 클라이언트 전용 기능(지갑 연결 등)은 WalletConnect 컴포넌트에서 처리합니다.
export default function Home() {
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.18),_transparent_28%),linear-gradient(180deg,_#fffaf0_0%,_#f8fafc_42%,_#eef2ff_100%)] px-5 py-8 md:px-8 md:py-10">
<div className="mx-auto max-w-6xl">
<div className="mb-8 overflow-hidden rounded-[2rem] border border-amber-200/60 bg-white/80 px-6 py-7 shadow-[0_20px_80px_rgba(15,23,42,0.08)] backdrop-blur md:px-8">
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.28em] text-amber-700">
Bay-17th • Week 05
</p>
<h1 className="max-w-3xl text-3xl font-black tracking-tight text-slate-900 md:text-5xl">
Wallet connect, ETH transfer, and onchain activity
</h1>
<p className="mt-3 max-w-2xl text-sm leading-7 text-slate-600 md:text-base">
RainbowKit 커스텀 지갑 연결, Sepolia ETH 전송, 계정 트랜잭션, 카운트 이벤트 정보를 제공하는 dApp
</p>
</div>

<WalletConnect />
</div>
</main>
);
}
Loading