본문 바로가기

디지털기술

DApp 개발: 카운터에서 감사 트래커로

발환경ganache : 로컬 이더리움 환경

 

Remix : 솔리디티 작성 및 배포

 

VisualStudio : 프론트 엔드 웹

 

메타마스크 : 지갑

카운터 컨트랙트 소스코드

 

1. 솔리디티 스마트 컨트랙트 (AuditTracker.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract AuditTracker {
    // 상태 변수 (접근 제어자를 private으로 변경하여 안전성 확보)
    uint256 private _auditCount;

    // 블록체인에 로그를 남기기 위한 이벤트 선언
    event AuditRecorded(address indexed auditor, uint256 newTotal);

    // 점검 횟수 증가 로직 (기존 increment)
    function recordAudit() public {
        _auditCount += 1;
        emit AuditRecorded(msg.sender, _auditCount);
    }

    // 현재 점검 횟수 조회 (기존 getCount)
    function getAuditCount() public view returns (uint256) {
        return _auditCount;
    }
}

 

2. DApp 프론트엔드 웹 코드 (index.html)

 

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>보안 점검 트래커 DApp</title>
  <style>
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #f8f9fa;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      margin: 0;
    }
    .app-container {
      background: #ffffff;
      padding: 40px;
      border-radius: 12px;
      box-shadow: 0 8px 16px rgba(0,0,0,0.1);
      text-align: center;
      width: 100%;
      max-width: 400px;
    }
    h2 { color: #2c3e50; margin-bottom: 20px; }
    .display-panel {
      background: #eef2f5;
      padding: 20px;
      border-radius: 8px;
      font-size: 1.2rem;
      margin-bottom: 25px;
      color: #34495e;
    }
    .display-panel span {
      font-weight: bold;
      color: #e74c3c;
      font-size: 1.5rem;
    }
    .btn-group {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    button {
      padding: 12px;
      border: none;
      border-radius: 6px;
      font-size: 1rem;
      font-weight: bold;
      color: white;
      cursor: pointer;
      transition: background-color 0.2s;
    }
    .btn-connect { background-color: #f39c12; }
    .btn-connect:hover { background-color: #d68910; }
    .btn-read { background-color: #3498db; }
    .btn-read:hover { background-color: #2980b9; }
    .btn-write { background-color: #2ecc71; }
    .btn-write:hover { background-color: #27ae60; }
  </style>
</head>
<body>

  <div class="app-container">
    <h2>🛡️ 보안 점검 트래커</h2>
    
    <div class="display-panel">
      누적 점검 완료 횟수: <br><br>
      <span id="auditDisplay">-</span> 회
    </div>

    <div class="btn-group">
      <button class="btn-connect" onclick="initWallet()">🦊 메타마스크 지갑 연결</button>
      <button class="btn-read" onclick="fetchAuditCount()">🔄 최신 데이터 조회</button>
      <button class="btn-write" onclick="submitAudit()">📝 점검 기록 추가 (+1)</button>
    </div>
  </div>

  <!-- ethers v6 라이브러리 -->
  <script src="https://cdn.jsdelivr.net/npm/ethers@6.10.0/dist/ethers.umd.min.js"></script>
  
  <script>
    const { ethers } = window;
    let web3Provider;
    let userSigner;
    let trackerContract;

    // 🔥 Remix에서 배포 후 발급받은 Contract 주소로 반드시 변경하세요.
    const CONTRACT_ADDRESS = "0xYOUR_NEW_CONTRACT_ADDRESS_HERE"; 
    
    // 변경된 스마트 컨트랙트의 ABI
    const CONTRACT_ABI = [
      "function recordAudit()",
      "function getAuditCount() view returns (uint256)"
    ];

    // 1. 지갑 연결 및 초기화
    async function initWallet() {
      try {
        if (!window.ethereum) {
          alert("메타마스크(MetaMask) 확장 프로그램이 설치되어 있어야 합니다.");
          return;
        }

        web3Provider = new ethers.BrowserProvider(window.ethereum);
        await web3Provider.send("eth_requestAccounts", []);
        userSigner = await web3Provider.getSigner();

        // 컨트랙트 배포 여부 사전 검증
        const codeAtAddress = await web3Provider.getCode(CONTRACT_ADDRESS);
        if (codeAtAddress === "0x") {
          alert("⚠️ 설정된 주소에 컨트랙트가 존재하지 않습니다. Ganache 네트워크와 컨트랙트 주소를 다시 확인해주세요.");
          return;
        }

        trackerContract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, userSigner);
        alert("✅ 지갑이 성공적으로 연결되었습니다.");
        
        // 연결 후 즉시 데이터 조회
        fetchAuditCount();

      } catch (error) {
        console.error("지갑 연결 에러:", error);
        alert("지갑 연결에 실패했습니다.");
      }
    }

    // 2. 블록체인에서 데이터 읽어오기
    async function fetchAuditCount() {
      try {
        if (!trackerContract) {
          alert("먼저 지갑을 연결해주세요.");
          return;
        }
        const currentCount = await trackerContract.getAuditCount();
        document.getElementById("auditDisplay").innerText = currentCount.toString();
      } catch (error) {
        console.error("데이터 조회 에러:", error);
        alert("데이터를 불러오는 중 오류가 발생했습니다.");
      }
    }

    // 3. 블록체인에 데이터 쓰기 (트랜잭션 발생)
    async function submitAudit() {
      try {
        if (!trackerContract) {
          alert("먼저 지갑을 연결해주세요.");
          return;
        }
        
        // 트랜잭션 전송
        const transaction = await trackerContract.recordAudit();
        console.log("트랜잭션 대기 중... Hash:", transaction.hash);
        
        // 블록에 마이닝될 때까지 대기
        await transaction.wait();
        alert("🎉 점검 기록이 블록체인에 성공적으로 저장되었습니다!");
        
        // 화면 즉시 갱신
        fetchAuditCount();

      } catch (error) {
        console.error("트랜잭션 에러:", error);
        alert("트랜잭션 처리에 실패했습니다.");
      }
    }

    // 메타마스크 계정이나 네트워크 변경 시 페이지 자동 새로고침 방어코드
    if (window.ethereum) {
      window.ethereum.on("accountsChanged", () => window.location.reload());
      window.ethereum.on("chainChanged", () => window.location.reload());
    }
  </script>
</body>
</html>