From 057b723b3dc1e0cde5ec758c97f267a6e96586e0 Mon Sep 17 00:00:00 2001 From: Dodaji Date: Wed, 29 Apr 2026 21:31:32 +0900 Subject: [PATCH] feat(week-06): Guestbook DApp --- week-06/dev/README.md | 156 +- week-06/dev/frontend/.gitignore | 30 + week-06/dev/frontend/app/Providers.tsx | 21 + week-06/dev/frontend/app/globals.css | 3 + week-06/dev/frontend/app/layout.tsx | 23 + week-06/dev/frontend/app/page.tsx | 119 + week-06/dev/frontend/config/wagmi.ts | 13 + week-06/dev/frontend/next-env.d.ts | 5 + week-06/dev/frontend/next.config.js | 6 + week-06/dev/frontend/package-lock.json | 13290 ++++++++++++++++++++++ week-06/dev/frontend/package.json | 31 + week-06/dev/frontend/postcss.config.js | 6 + week-06/dev/frontend/tailwind.config.ts | 14 + week-06/dev/frontend/tsconfig.json | 27 + week-06/dev/src/Guestbook.sol | 41 + week-06/dev/test/Guestbook.t.sol | 66 + 16 files changed, 13741 insertions(+), 110 deletions(-) create mode 100644 week-06/dev/frontend/.gitignore create mode 100644 week-06/dev/frontend/app/Providers.tsx create mode 100644 week-06/dev/frontend/app/globals.css create mode 100644 week-06/dev/frontend/app/layout.tsx create mode 100644 week-06/dev/frontend/app/page.tsx create mode 100644 week-06/dev/frontend/config/wagmi.ts create mode 100644 week-06/dev/frontend/next-env.d.ts create mode 100644 week-06/dev/frontend/next.config.js create mode 100644 week-06/dev/frontend/package-lock.json create mode 100644 week-06/dev/frontend/package.json create mode 100644 week-06/dev/frontend/postcss.config.js create mode 100644 week-06/dev/frontend/tailwind.config.ts create mode 100644 week-06/dev/frontend/tsconfig.json create mode 100644 week-06/dev/src/Guestbook.sol create mode 100644 week-06/dev/test/Guestbook.t.sol diff --git a/week-06/dev/README.md b/week-06/dev/README.md index 2bbad5cf..a6aa6a10 100644 --- a/week-06/dev/README.md +++ b/week-06/dev/README.md @@ -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] 에러 메시지 표시 diff --git a/week-06/dev/frontend/.gitignore b/week-06/dev/frontend/.gitignore new file mode 100644 index 00000000..fd024467 --- /dev/null +++ b/week-06/dev/frontend/.gitignore @@ -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 diff --git a/week-06/dev/frontend/app/Providers.tsx b/week-06/dev/frontend/app/Providers.tsx new file mode 100644 index 00000000..48576919 --- /dev/null +++ b/week-06/dev/frontend/app/Providers.tsx @@ -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 ( + + + + {children} + + + + ); +} diff --git a/week-06/dev/frontend/app/globals.css b/week-06/dev/frontend/app/globals.css new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/week-06/dev/frontend/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/week-06/dev/frontend/app/layout.tsx b/week-06/dev/frontend/app/layout.tsx new file mode 100644 index 00000000..2adf0413 --- /dev/null +++ b/week-06/dev/frontend/app/layout.tsx @@ -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 ( + + + + {children} + + + + ); +} diff --git a/week-06/dev/frontend/app/page.tsx b/week-06/dev/frontend/app/page.tsx new file mode 100644 index 00000000..1f654b2e --- /dev/null +++ b/week-06/dev/frontend/app/page.tsx @@ -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 ( +
+
+
+

📖 Guestbook DApp

+ +
+ +
+

Leave a message

+
+