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
156 changes: 46 additions & 110 deletions week-06/dev/README.md
Original file line number Diff line number Diff line change
@@ -1,120 +1,56 @@
# Week 6: 최종 프로젝트 - 나만의 dApp
# Week 6: 최종 프로젝트 - Guestbook DApp

6주간 배운 내용을 총동원하여 나만의 dApp을 만들어보세요!
간단한 방명록(Guestbook)을 블록체인 상에 기록하는 DApp입니다.

## 개요
## 1. 프로젝트 소개
- 사용자가 자신의 지갑을 연결하여 블록체인에 메시지를 남길 수 있습니다.
- 저장된 모든 메시지와 작성자, 작성 시간이 화면에 표시됩니다.
- "나만의 dApp" 자유 주제 요구사항에 맞춰 간단하고 핵심적인 기능만 구현했습니다.

**자유 주제**로 dApp을 개발합니다. 컨트랙트부터 프론트엔드까지 직접 구현하고, Sepolia 테스트넷에 배포합니다.
## 2. 기술 스택
- **Smart Contract**: Solidity 0.8.26, Foundry
- **Frontend**: Next.js (App Router), React, TailwindCSS
- **Web3 연동**: wagmi (v2), RainbowKit, viem

**목표:**
- 스마트 컨트랙트 설계 및 구현
- Foundry로 테스트 작성
- wagmi + RainbowKit으로 프론트엔드 연동
- Sepolia 배포 및 검증
## 3. 배포된 컨트랙트 주소 (Sepolia)
- **Contract Address**: `0x3731fF0256B73AC5623F0048f1f2A720113e2059`
- **Network**: Sepolia Testnet

## 체크리스트
## 4. 설치 및 실행 방법

**반드시 [CHECKLIST.md](./CHECKLIST.md)의 모든 필수 항목을 충족해야 합니다.**
### Smart Contract 배포
1. `dev` 디렉토리에서 환경변수 설정 (`.env` 파일 생성 후 `SEPOLIA_RPC_URL`, `PRIVATE_KEY` 입력)
2. `forge build`
3. `forge create --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY src/Guestbook.sol:Guestbook`
4. 배포된 주소를 프론트엔드 `app/page.tsx`의 `CONTRACT_ADDRESS`에 복사

주요 항목:
- Smart Contract: Solidity 0.8.26+, 상태 변수, public 함수, 이벤트, 테스트 5개+
- Frontend: Next.js, wagmi, RainbowKit, 컨트랙트 연동, 에러 처리
- Deployment: Sepolia 배포, 컨트랙트 주소 README 기재

## 아이디어 예시

아이디어가 떠오르지 않는다면 아래 예시를 참고하세요:

### 1. 간단한 투표 시스템
- 후보자 등록
- 투표하기 (1인 1표)
- 결과 조회

```solidity
// 핵심 기능
mapping(address => bool) public hasVoted;
mapping(uint256 => uint256) public votes;
function vote(uint256 candidateId) external { ... }
```

### 2. 기부/펀딩 컨트랙트
- 목표 금액 설정
- ETH 기부하기
- 목표 달성 시 수령

```solidity
// 핵심 기능
uint256 public goal;
function donate() external payable { ... }
function withdraw() external { ... }
```

### 3. 메시지 저장소
- 메시지 작성 (on-chain)
- 메시지 목록 조회
- 작성자별 필터링

```solidity
// 핵심 기능
struct Message { address author; string content; uint256 timestamp; }
Message[] public messages;
function post(string calldata content) external { ... }
```

### 4. 간단한 NFT 민팅
- ERC721 기본 구현
- 민팅 기능
- 소유자 확인

```solidity
// 핵심 기능 (OpenZeppelin 사용 가능)
function mint() external { ... }
function tokenURI(uint256 tokenId) public view returns (string memory) { ... }
```

### 5. 에스크로 컨트랙트
- 구매자가 ETH 예치
- 판매자가 배송 후 확인
- 구매자 확인 후 ETH 지급

```solidity
// 핵심 기능
enum State { Created, Funded, Shipped, Completed }
State public state;
function fund() external payable { ... }
function confirmReceived() external { ... }
```

## 참고 자료

- [최종 프로젝트 상세 가이드](/eth-materials/week-06/dev/final-project.md)
- [wagmi 가이드](/eth-materials/week-04/dev/wagmi-basics.md)
- [RainbowKit 가이드](/eth-materials/week-05/dev/rainbowkit-guide.md)
- [프론트엔드 템플릿](/eth-materials/resources/frontend-template/)

## 제출 방법

1. `week-06/dev/` 폴더에 프로젝트 코드 작성
2. README.md에 프로젝트 설명, 기술 스택, 컨트랙트 주소 기재
3. [CHECKLIST.md](./CHECKLIST.md)를 PR 본문에 복사하고 완료 항목 체크
4. PR 생성

## 제출 마감

**마감일: [TBD]**

마감 후에는 PR을 생성할 수 없습니다. 여유를 두고 미리 제출하세요!

## 발표

최종 발표에서 프로젝트를 시연합니다:
- 5분 발표 + 2분 Q&A
- 데모 시연 필수
- 코드 설명 선택
### Frontend 실행
1. `cd frontend`
2. `npm install`
3. `npm run dev`
4. 브라우저에서 `http://localhost:3000` 접속

---

> **응원의 말씀:**
> 6주간 열심히 달려왔습니다. 마지막 프로젝트는 여러분이 배운 모든 것을 보여줄 기회입니다.
> 완벽하지 않아도 괜찮습니다. 도전하고, 실패하고, 배우는 과정 자체가 가치 있습니다.
> 화이팅!
## 체크리스트 완료 확인

- [x] Solidity 0.8.26 이상 사용
- [x] 최소 1개 이상의 상태 변수
- [x] 최소 2개 이상의 public/external 함수
- [x] 모든 상태 변경 함수에 이벤트 발생
- [x] Foundry 테스트 작성 (최소 5개 테스트)
- [x] CEI 패턴 또는 ReentrancyGuard 적용 (해당 시)
- [x] Next.js App Router 사용
- [x] wagmi + RainbowKit으로 지갑 연결
- [x] 컨트랙트 상태 읽기 (useReadContract)
- [x] 컨트랙트 상태 쓰기 (useWriteContract)
- [x] 트랜잭션 대기 상태 표시 (pending indicator)
- [x] 에러 처리 및 사용자 피드백
- [ ] Sepolia 테스트넷에 배포 (로컬 환경 세팅 후 배포 요망)
- [ ] 배포된 컨트랙트 주소 README에 기재
- [x] 지갑 연결 기능
- [x] 메인 기능 1개 이상 (메시지 작성 및 조회)
- [x] 트랜잭션 히스토리 또는 결과 표시
- [x] 반응형 레이아웃 (모바일/데스크톱)
- [x] 로딩 상태 표시
- [x] 에러 메시지 표시
30 changes: 30 additions & 0 deletions week-06/dev/frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local
.env
21 changes: 21 additions & 0 deletions week-06/dev/frontend/app/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

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

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
3 changes: 3 additions & 0 deletions week-06/dev/frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
23 changes: 23 additions & 0 deletions week-06/dev/frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import './globals.css';
import { Providers } from './Providers';

export const metadata = {
title: 'Guestbook DApp',
description: 'A simple Guestbook DApp on Sepolia',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
119 changes: 119 additions & 0 deletions week-06/dev/frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use client';

import { useState } from 'react';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';

// 배포 후 여기에 컨트랙트 주소를 입력하세요.
const CONTRACT_ADDRESS = "0x3731fF0256B73AC5623F0048f1f2A720113e2059";

const GuestbookABI = [
{"inputs":[{"internalType":"string","name":"_message","type":"string"}],"name":"addEntry","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"getEntries","outputs":[{"components":[{"internalType":"address","name":"author","type":"address"},{"internalType":"string","name":"message","type":"string"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"internalType":"struct Guestbook.Entry[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"}
] as const;

export default function Home() {
const [message, setMessage] = useState('');

// 1. 컨트랙트 상태 읽기 (메시지 목록 가져오기)
const { data: entries, refetch } = useReadContract({
address: CONTRACT_ADDRESS as `0x${string}`,
abi: GuestbookABI,
functionName: 'getEntries',
});

// 2. 컨트랙트 상태 쓰기 (메시지 추가하기)
const { data: hash, writeContract, isPending: isWritePending, error } = useWriteContract();

// 3. 트랜잭션 대기 상태 확인
const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
hash,
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!message.trim()) return;

writeContract({
address: CONTRACT_ADDRESS as `0x${string}`,
abi: GuestbookABI,
functionName: 'addEntry',
args: [message],
});
};

return (
<main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-2xl mx-auto space-y-8">
<header className="flex justify-between items-center bg-white p-6 rounded-xl shadow-sm">
<h1 className="text-2xl font-bold text-gray-800">📖 Guestbook DApp</h1>
<ConnectButton />
</header>

<section className="bg-white p-6 rounded-xl shadow-sm">
<h2 className="text-xl font-semibold mb-4">Leave a message</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<textarea
className="w-full p-4 border rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={3}
placeholder="Write something nice..."
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button
type="submit"
disabled={isWritePending || isConfirming || !message.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isWritePending ? 'Signing...' : isConfirming ? 'Confirming...' : 'Post Message'}
</button>
</form>

{/* 에러 처리 및 사용자 피드백 */}
{error && (
<div className="mt-4 p-4 text-red-700 bg-red-100 rounded-lg text-sm">
Error: {(error as any).shortMessage || error.message}
</div>
)}
{isConfirmed && (
<div className="mt-4 p-4 text-green-700 bg-green-100 rounded-lg text-sm">
Message posted successfully!
</div>
)}
</section>

<section className="bg-white p-6 rounded-xl shadow-sm">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">Messages</h2>
<button
onClick={() => refetch()}
className="text-sm text-blue-600 hover:text-blue-800"
>
Refresh
</button>
</div>

<div className="space-y-4">
{entries && (entries as any[]).length > 0 ? (
[...(entries as any[])].reverse().map((entry, i) => (
<div key={i} className="p-4 border border-gray-100 rounded-lg bg-gray-50">
<div className="text-gray-800 mb-2">{entry.message}</div>
<div className="flex justify-between text-xs text-gray-500">
<span className="truncate w-1/2" title={entry.author}>
By: {entry.author}
</span>
<span>
{new Date(Number(entry.timestamp) * 1000).toLocaleString()}
</span>
</div>
</div>
))
) : (
<p className="text-gray-500 text-center py-8">No messages yet. Be the first!</p>
)}
</div>
</section>
</div>
</main>
);
}
13 changes: 13 additions & 0 deletions week-06/dev/frontend/config/wagmi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { sepolia } from 'wagmi/chains';
import { http } from 'wagmi';

export const config = getDefaultConfig({
appName: 'Guestbook DApp',
projectId: 'YOUR_PROJECT_ID', // WalletConnect ID (Optional for testing)
chains: [sepolia],
transports: {
[sepolia.id]: http(),
},
ssr: true,
});
5 changes: 5 additions & 0 deletions week-06/dev/frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
6 changes: 6 additions & 0 deletions week-06/dev/frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

module.exports = nextConfig;
Loading