Ver3.0 ๐ ๋ผ๋ผ๋ฒจ๋ก ๋ธ๋ก์ฒด์ธ ์ค๋ฌธ์กฐ์ฌ ์น์๋น์ค ๊ตฌํํ๊ธฐ

๐ ๋ผ๋ผ๋ฒจ๋ก ๋ธ๋ก์ฒด์ธ ์ค๋ฌธ์กฐ์ฌ ์น์๋น์ค ๊ตฌํํ๊ธฐ
ํฌ๋ช ํ๊ณ ์ ๋ขฐํ ์ ์๋ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
๐ฏ ์ ๋ธ๋ก์ฒด์ธ ์ค๋ฌธ์กฐ์ฌ์ธ๊ฐ?
์๋
! ์ค๋์ ์ ๋ง ํฅ๋ฏธ๋ก์ด ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ฐ๋ก ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ์ ๊ฒฐํฉํ ์ค๋ฌธ์กฐ์ฌ ์์คํ
์ ๋ง๋๋ ๊ฑฐ์ผ. ๐
"์ค๋ฌธ์กฐ์ฌ์ ๋ธ๋ก์ฒด์ธ์ด ์ ํ์ํด?"๋ผ๊ณ ๋ฌผ์ ์ ์๋๋ฐ, ์ฌ์ค ์ด๊ฒ ์์ฒญ ์ค์ํ ์ง๋ฌธ์ด์ผ. ์ ํต์ ์ธ ์ค๋ฌธ์กฐ์ฌ ์์คํ
์ ๋ช ๊ฐ์ง ์น๋ช
์ ์ธ ๋ฌธ์ ์ ๋ค์ด ์๊ฑฐ๋ .
์ฒซ์งธ, ๋ฐ์ดํฐ ์กฐ์ ๊ฐ๋ฅ์ฑ์ด์ผ. ๊ด๋ฆฌ์๊ฐ ๋ง์๋ง ๋จน์ผ๋ฉด ์๋ต ๊ฒฐ๊ณผ๋ฅผ ์์ ํ๊ฑฐ๋ ์ญ์ ํ ์ ์์ง. ๋์งธ, ํฌ๋ช
์ฑ ๋ถ์กฑ์ด์ผ. ์๋ต์๋ ์์ ์ ๋ต๋ณ์ด ์ ๋ง๋ก ์ ๋๋ก ๊ธฐ๋ก๋์๋์ง ํ์ธํ ๋ฐฉ๋ฒ์ด ์์ด. ์
์งธ, ์ค์ํ๋ ๋ฐ์ดํฐ ๊ด๋ฆฌ๋ก ์ธํ ๋ณด์ ์ทจ์ฝ์ ์ด ์กด์ฌํด.
๋ธ๋ก์ฒด์ธ์ ์ด ๋ชจ๋ ๋ฌธ์ ๋ฅผ ํ ๋ฐฉ์ ํด๊ฒฐํด์ค! ๐ ํ๋ฒ ๊ธฐ๋ก๋ ๋ฐ์ดํฐ๋ ์์ ์ด ๋ถ๊ฐ๋ฅํ๊ณ , ๋ชจ๋ ์ฐธ์ฌ์๊ฐ ๊ฒ์ฆํ ์ ์์ผ๋ฉฐ, ๋ถ์ฐ ์ ์ฅ์ผ๋ก ๋ณด์์ฑ๋ ํ๋ณด๋์ง. ํนํ ํฌํ๋ ์ฌ๋ก ์กฐ์ฌ์ฒ๋ผ ์ ๋ขฐ์ฑ์ด ์ค์ํ ์ค๋ฌธ์์๋ ๋ธ๋ก์ฒด์ธ์ด ๊ฒ์ ์ฒด์ธ์ ๊ฐ ๋ ์ ์์ด.
๐๏ธ ์์คํ ์ํคํ ์ฒ ์ค๊ณ
๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํ๊ธฐ ์ ์ ์ ์ฒด ์์คํ
๊ตฌ์กฐ๋ฅผ ์ดํดํด์ผ ํด. ์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์์คํ
์ ํฌ๊ฒ ์ธ ๊ฐ์ง ๋ ์ด์ด๋ก ๊ตฌ์ฑ๋ผ.
1. ํ๋ก ํธ์๋ ๋ ์ด์ด (Laravel Blade + Vue.js)
์ฌ์ฉ์๊ฐ ์ง์ ์ํธ์์ฉํ๋ ๋ถ๋ถ์ด์ผ. ์ค๋ฌธ ์์ฑ, ์๋ต ์ ์ถ, ๊ฒฐ๊ณผ ์กฐํ ๋ฑ์ UI๋ฅผ ์ ๊ณตํ์ง.
2. ๋ฐฑ์๋ ๋ ์ด์ด (Laravel Framework)
๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๋ ํต์ฌ ๋ถ๋ถ์ด์ผ. ์ฌ์ฉ์ ์ธ์ฆ, ์ค๋ฌธ ๊ด๋ฆฌ, ๋ธ๋ก์ฒด์ธ ์ธํฐํ์ด์ค ๋ฑ์ ๋ด๋นํด.
3. ๋ธ๋ก์ฒด์ธ ๋ ์ด์ด (Ethereum/Private Blockchain)
์ค์ ์ค๋ฌธ ๋ฐ์ดํฐ์ ์๋ต์ ๋ถ๋ณ์ ํํ๋ก ์ ์ฅํ๋ ๊ณณ์ด์ผ. ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ํตํด ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ์ง.
๋ชจ๋ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ๋ธ๋ก์ฒด์ธ์ ์ฌ๋ฆด ํ์๋ ์์ด. ๊ฐ์ธ์ ๋ณด๋ ์ํธํํด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๊ณ , ํด์๊ฐ๊ณผ ํ์์คํฌํ๋ง ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋กํ๋ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์ด ํจ์จ์ ์ด์ผ. ์ด๋ ๊ฒ ํ๋ฉด ๊ฐ์ค๋น๋ ์ ์ฝํ๊ณ ์ฑ๋ฅ๋ ์ข์์ ธ! โก
๋ผ๋ผ๋ฒจ์ ์ ํํ ์ด์ ๋ ๋ช ํํด. ๋ผ๋ผ๋ฒจ์ Eloquent ORM, ๋ฏธ๋ค์จ์ด, ํ ์์คํ ๋ฑ ์ํฐํ๋ผ์ด์ฆ๊ธ ๊ธฐ๋ฅ์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๊ฑฐ๋ . ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋๊น ํ ์์คํ ์ผ๋ก ๋น๋๊ธฐ ์ฒ๋ฆฌํ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์ข์์ ธ.
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ
์, ์ด์ ์ค์ ๋ก ์์ ๋๋ฝํ๋ณผ ์๊ฐ์ด์ผ! ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ
ํด๋ณด์. ๐ง
ํ์ ์๊ตฌ์ฌํญ:
โข PHP 8.1 ์ด์
โข Composer
โข Node.js & NPM
โข MySQL ๋๋ PostgreSQL
โข Ganache (๋ก์ปฌ ๋ธ๋ก์ฒด์ธ ํ
์คํธ์ฉ)
โข MetaMask (์ง๊ฐ ์ฐ๋ ํ
์คํธ์ฉ)
composer create-project laravel/laravel blockchain-survey
cd blockchain-survey
php artisan serve
๊ธฐ๋ณธ ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ๊ฐ ์์ฑ๋์ด. ์ด์ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ํจํค์ง๋ค์ ์ค์นํด์ผ ํด.
composer require web3p/web3.php
composer require kornrunner/keccak
web3.php๋ PHP์์ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ๊ณผ ์ํธ์์ฉํ ์ ์๊ฒ ํด์ฃผ๋ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. JSON-RPC๋ฅผ ํตํด ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ํธ์ถํ๊ณ ํธ๋์ญ์ ์ ์ ์กํ ์ ์์ง. ๐
npm install
npm install web3 ethers
npm install @metamask/detect-provider
ํ๋ก ํธ์๋์์๋ ๋ธ๋ก์ฒด์ธ๊ณผ ์ง์ ํต์ ํ ์ ์๋๋ก web3.js๋ฅผ ์ค์นํด. MetaMask ๊ฐ์ ์ง๊ฐ๊ณผ ์ฐ๋ํ ๋ ํ์ํด.
Ganache๋ฅผ ์ฌ์ฉํ๋ฉด ๋ก์ปฌ์์ ํ๋ผ์ด๋น ๋ธ๋ก์ฒด์ธ์ ๋์ธ ์ ์์ด. ๋ฉ์ธ๋ท์ด๋ ํ ์คํธ๋ท์ ๋ฐฐํฌํ๊ธฐ ์ ์ ์๋ฒฝํ๊ฒ ํ ์คํธํ ์ ์์ง. ๊ฐ์ค๋น๋ ์ ๋ค๊ณ ํธ๋์ญ์ ๋ ์ฆ์ ์ฒ๋ฆฌ๋ผ์ ๊ฐ๋ฐ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ! ๐จ
๐ ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑํ๊ธฐ
๋ธ๋ก์ฒด์ธ ์ค๋ฌธ์กฐ์ฌ์ ํต์ฌ์ ๋ฐ๋ก ์ค๋งํธ ์ปจํธ๋ํธ์ผ. Solidity๋ก ์์ฑํ๋๋ฐ, ์๊ฐ๋ณด๋ค ์ด๋ ต์ง ์์. ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผ์๋ด! ๐
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์ปจํธ๋ํธ๋ ๋ค์ ๊ธฐ๋ฅ๋ค์ ํฌํจํด์ผ ํด:
โข ์ค๋ฌธ ์์ฑ ๋ฐ ๋ฑ๋ก
โข ์๋ต ์ ์ถ ๋ฐ ์ ์ฅ
โข ์ค๋ณต ์๋ต ๋ฐฉ์ง
โข ์ค๋ฌธ ์ข
๋ฃ ๋ฐ ๊ฒฐ๊ณผ ์ง๊ณ
โข ์์ ๊ถ ๋ฐ ๊ถํ ๊ด๋ฆฌ
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SurveyContract {
struct Survey {
uint256 id;
address creator;
string title;
string description;
uint256 startTime;
uint256 endTime;
bool isActive;
uint256 totalResponses;
}
struct Response {
uint256 surveyId;
address respondent;
bytes32 dataHash;
uint256 timestamp;
}
mapping(uint256 => Survey) public surveys;
mapping(uint256 => mapping(address => bool)) public hasResponded;
mapping(uint256 => Response[]) public responses;
uint256 public surveyCount;
event SurveyCreated(uint256 indexed surveyId, address indexed creator, string title);
event ResponseSubmitted(uint256 indexed surveyId, address indexed respondent, bytes32 dataHash);
event SurveyClosed(uint256 indexed surveyId);
modifier onlyCreator(uint256 _surveyId) {
require(surveys[_surveyId].creator == msg.sender, "Not the survey creator");
_;
}
modifier surveyExists(uint256 _surveyId) {
require(_surveyId > 0 && _surveyId <= surveyCount, "Survey does not exist");
_;
}
modifier surveyActive(uint256 _surveyId) {
require(surveys[_surveyId].isActive, "Survey is not active");
require(block.timestamp >= surveys[_surveyId].startTime, "Survey has not started");
require(block.timestamp <= surveys[_surveyId].endTime, "Survey has ended");
_;
}
}
์ด๊ฒ ์ฐ๋ฆฌ ์ปจํธ๋ํธ์ ๋ผ๋์ผ. Survey ๊ตฌ์กฐ์ฒด๋ ์ค๋ฌธ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ด๊ณ , Response ๊ตฌ์กฐ์ฒด๋ ๊ฐ ์๋ต์ ํด์๊ฐ์ ์ ์ฅํด. ์ค์ ์๋ต ๋ด์ฉ์ ์คํ์ฒด์ธ์ ์ ์ฅํ๊ณ ํด์๋ง ์จ์ฒด์ธ์ ์ฌ๋ฆฌ๋ ๊ฑฐ์ง. ๐
function createSurvey(
string memory _title,
string memory _description,
uint256 _startTime,
uint256 _endTime
) public returns (uint256) {
require(_startTime < _endTime, "Invalid time range");
require(_endTime > block.timestamp, "End time must be in the future");
surveyCount++;
surveys[surveyCount] = Survey({
id: surveyCount,
creator: msg.sender,
title: _title,
description: _description,
startTime: _startTime,
endTime: _endTime,
isActive: true,
totalResponses: 0
});
emit SurveyCreated(surveyCount, msg.sender, _title);
return surveyCount;
}
์ค๋ฌธ์ ์์ฑํ๋ ํจ์์ผ. ์์ ์๊ฐ๊ณผ ์ข ๋ฃ ์๊ฐ์ ๊ฒ์ฆํ๊ณ , ์๋ก์ด ์ค๋ฌธ์ ๋งคํ์ ์ ์ฅํด. emit์ผ๋ก ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ์ ํ๋ก ํธ์๋์์ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ ์ ์๊ฒ ํด์ค. ๐ก
function submitResponse(
uint256 _surveyId,
bytes32 _dataHash
) public surveyExists(_surveyId) surveyActive(_surveyId) {
require(!hasResponded[_surveyId][msg.sender], "Already responded to this survey");
require(_dataHash != bytes32(0), "Invalid data hash");
Response memory newResponse = Response({
surveyId: _surveyId,
respondent: msg.sender,
dataHash: _dataHash,
timestamp: block.timestamp
});
responses[_surveyId].push(newResponse);
hasResponded[_surveyId][msg.sender] = true;
surveys[_surveyId].totalResponses++;
emit ResponseSubmitted(_surveyId, msg.sender, _dataHash);
}
์๋ต ์ ์ถ ํจ์๋ ์ฌ๋ฌ ๊ฒ์ฆ์ ๊ฑฐ์ณ. ์ค๋ฌธ์ด ์กด์ฌํ๋์ง, ํ์ฑํ๋์ด ์๋์ง, ์ด๋ฏธ ์๋ตํ๋์ง ๋ฑ์ ์ฒดํฌํด. ์ค๋ณต ์๋ต ๋ฐฉ์ง๋ ๋ธ๋ก์ฒด์ธ์ ํฐ ์ฅ์ ์ค ํ๋์ผ! ๐ก๏ธ
์ค๋งํธ ์ปจํธ๋ํธ๋ ๋ฐฐํฌ ํ ์์ ์ด ๋ถ๊ฐ๋ฅํด. ๊ทธ๋์ ์ฒ ์ ํ ํ ์คํธ๊ฐ ํ์์ผ. Remix IDE๋ Hardhat์ผ๋ก ์ถฉ๋ถํ ํ ์คํธํ๊ณ , ๊ฐ๋ฅํ๋ฉด ์ ๋ฌธ๊ฐ์๊ฒ ๊ฐ์ฌ(audit)๋ฅผ ๋ฐ๋ ๊ฒ ์ข์. ํนํ ์ฌ์ง์ ๊ณต๊ฒฉ(reentrancy attack) ๊ฐ์ ๋ณด์ ์ทจ์ฝ์ ์ ์ฃผ์ํด์ผ ํด!
๐ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ ์ฐ๋ํ๊ธฐ
์ด์ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌํ์ผ๋, ๋ผ๋ผ๋ฒจ์์ ์ด๊ฑธ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์์๋ณด์. Web3.php๋ฅผ ์ฌ์ฉํด์ ์ปจํธ๋ํธ์ ํต์ ํ ๊ฑฐ์ผ. ๐
๋จผ์ ๋ธ๋ก์ฒด์ธ ์๋น์ค ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด์. ์ด ํด๋์ค๊ฐ ๋ชจ๋ ๋ธ๋ก์ฒด์ธ ์ํธ์์ฉ์ ๋ด๋นํ ๊ฑฐ์ผ.
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
class BlockchainService
{
protected $web3;
protected $contract;
protected $contractAddress;
protected $abi;
protected $account;
protected $privateKey;
public function __construct()
{
$this->web3 = new Web3(config('blockchain.rpc_url'));
$this->contractAddress = config('blockchain.contract_address');
$this->abi = json_decode(file_get_contents(storage_path('contracts/SurveyContract.json')), true);
$this->account = config('blockchain.account');
$this->privateKey = config('blockchain.private_key');
$this->contract = new Contract($this->web3->provider, $this->abi);
}
public function createSurvey($title, $description, $startTime, $endTime)
{
try {
$this->contract->at($this->contractAddress);
$data = $this->contract->getData('createSurvey',
$title,
$description,
$startTime,
$endTime
);
$transaction = [
'from' => $this->account,
'to' => $this->contractAddress,
'data' => $data,
'gas' => '0x' . dechex(3000000),
];
$signedTransaction = $this->signTransaction($transaction);
$txHash = null;
$this->web3->eth->sendRawTransaction($signedTransaction, function ($err, $hash) use (&$txHash) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$txHash = $hash;
});
return $txHash;
} catch (\Exception $e) {
\Log::error('Blockchain Error: ' . $e->getMessage());
throw $e;
}
}
private function signTransaction($transaction)
{
// ํธ๋์ญ์
์๋ช
๋ก์ง
// ์ค์ ๊ตฌํ์์๋ eth-php ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
return $signedTx;
}
}
์ด ์๋น์ค ํด๋์ค๋ ๋ธ๋ก์ฒด์ธ๊ณผ์ ๋ชจ๋ ์ํธ์์ฉ์ ์บก์ํํด. ์ค์ ํ์ผ์์ RPC URL, ์ปจํธ๋ํธ ์ฃผ์, ๊ณ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์์ ์ฌ์ฉํ์ง. ๐
return [
'rpc_url' => env('BLOCKCHAIN_RPC_URL', 'http://127.0.0.1:7545'),
'contract_address' => env('BLOCKCHAIN_CONTRACT_ADDRESS'),
'account' => env('BLOCKCHAIN_ACCOUNT'),
'private_key' => env('BLOCKCHAIN_PRIVATE_KEY'),
'chain_id' => env('BLOCKCHAIN_CHAIN_ID', 1337),
'gas_limit' => env('BLOCKCHAIN_GAS_LIMIT', 3000000),
];
ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌํ๋ฉด ๊ฐ๋ฐ/์คํ ์ด์ง/ํ๋ก๋์ ํ๊ฒฝ์ ์ฝ๊ฒ ์ ํํ ์ ์์ด. ํนํ ํ๋ผ์ด๋น ํค๋ ์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉํ๋ฉด ์ ๋ผ! ๐จ
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ํ์ธ(confirmation)๊น์ง ์๊ฐ์ด ๊ฑธ๋ ค. ์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท์ 15์ด ์ ๋, ํ๋ผ์ด๋น ์ฒด์ธ๋ ๋ช ์ด๋ ๊ฑธ๋ฆฌ์ง. ์ฌ์ฉ์๊ฐ ๊ธฐ๋ค๋ฆฌ๊ฒ ํ๋ฉด ์ ๋๋๊น Laravel Queue๋ฅผ ์ฌ์ฉํด์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ๋ฆฌํ๋ ๊ฒ ์ข์. ํธ๋์ญ์ ํด์๋ง ์ฆ์ ๋ฐํํ๊ณ , ๊ฒฐ๊ณผ๋ ์น์์ผ์ด๋ ํด๋ง์ผ๋ก ์ ๋ฐ์ดํธํ๋ฉด ๋ผ! โก
๐จ ์ปจํธ๋กค๋ฌ์ ๋ชจ๋ธ ๊ตฌํํ๊ธฐ
์ด์ ์ค์ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ ์ปจํธ๋กค๋ฌ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ์ ๋ง๋ค์ด๋ณด์. ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์ด๋๊น ์ผ๋ถ ๋ฐ์ดํฐ๋ DB์, ์ผ๋ถ๋ ๋ธ๋ก์ฒด์ธ์ ์ ์ฅํ ๊ฑฐ์ผ. ๐
php artisan make:migration create_surveys_table
public function up()
{
Schema::create('surveys', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('description');
$table->json('questions');
$table->timestamp('start_time');
$table->timestamp('end_time');
$table->string('blockchain_tx_hash')->nullable();
$table->unsignedBigInteger('blockchain_survey_id')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index('blockchain_survey_id');
$table->index('is_active');
});
Schema::create('survey_responses', function (Blueprint $table) {
$table->id();
$table->foreignId('survey_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->string('respondent_address');
$table->json('answers');
$table->string('data_hash');
$table->string('blockchain_tx_hash')->nullable();
$table->timestamp('submitted_at');
$table->timestamps();
$table->unique(['survey_id', 'respondent_address']);
$table->index('data_hash');
});
}
blockchain_tx_hash์ blockchain_survey_id ํ๋๋ก ์จ์ฒด์ธ ๋ฐ์ดํฐ์ ์ฐ๊ฒฐํด. data_hash๋ ์๋ต ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ์ ๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉ๋ผ. ๐
namespace App\Http\Controllers;
use App\Models\Survey;
use App\Services\BlockchainService;
use App\Jobs\CreateSurveyOnBlockchain;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SurveyController extends Controller
{
protected $blockchainService;
public function __construct(BlockchainService $blockchainService)
{
$this->blockchainService = $blockchainService;
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'questions' => 'required|array',
'questions.*.question' => 'required|string',
'questions.*.type' => 'required|in:text,multiple_choice,rating',
'questions.*.options' => 'array',
'start_time' => 'required|date|after:now',
'end_time' => 'required|date|after:start_time',
]);
DB::beginTransaction();
try {
// 1. DB์ ์ค๋ฌธ ์ ์ฅ
$survey = Survey::create([
'user_id' => auth()->id(),
'title' => $validated['title'],
'description' => $validated['description'],
'questions' => $validated['questions'],
'start_time' => $validated['start_time'],
'end_time' => $validated['end_time'],
]);
// 2. ๋ธ๋ก์ฒด์ธ์ ๋ฑ๋ก (๋น๋๊ธฐ)
CreateSurveyOnBlockchain::dispatch($survey);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Survey created successfully',
'data' => $survey,
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to create survey',
'error' => $e->getMessage(),
], 500);
}
}
public function submitResponse(Request $request, Survey $survey)
{
$validated = $request->validate([
'answers' => 'required|array',
'wallet_address' => 'required|string',
]);
// ์ค๋ณต ์๋ต ์ฒดํฌ
if ($survey->responses()->where('respondent_address', $validated['wallet_address'])->exists()) {
return response()->json([
'success' => false,
'message' => 'You have already responded to this survey',
], 422);
}
DB::beginTransaction();
try {
// ๋ฐ์ดํฐ ํด์ ์์ฑ
$dataHash = hash('sha256', json_encode($validated['answers']));
// DB์ ์๋ต ์ ์ฅ
$response = $survey->responses()->create([
'user_id' => auth()->id(),
'respondent_address' => $validated['wallet_address'],
'answers' => $validated['answers'],
'data_hash' => $dataHash,
'submitted_at' => now(),
]);
// ๋ธ๋ก์ฒด์ธ์ ํด์ ๊ธฐ๋ก (๋น๋๊ธฐ)
SubmitResponseToBlockchain::dispatch($response, $survey);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Response submitted successfully',
'data_hash' => $dataHash,
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to submit response',
'error' => $e->getMessage(),
], 500);
}
}
}
์ปจํธ๋กค๋ฌ์์๋ ์ฆ์ DB์ ์ ์ฅํ๊ณ , ๋ธ๋ก์ฒด์ธ ์์ ์ Job์ผ๋ก ํ์ ๋ฃ์ด. ์ฌ์ฉ์๋ ๋ฐ๋ก ์๋ต์ ๋ฐ์ ์ ์๊ณ , ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ก์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ๋ฆฌ๋์ง. ์ด๊ฒ ๋ฐ๋ก ์ฌ์ฉ์ ๊ฒฝํ๊ณผ ๋ธ๋ก์ฒด์ธ์ ์๋ฒฝํ ์กฐํ์ผ! ๐ญ
namespace App\Jobs;
use App\Models\Survey;
use App\Services\BlockchainService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CreateSurveyOnBlockchain implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $survey;
public $tries = 3;
public $timeout = 120;
public function __construct(Survey $survey)
{
$this->survey = $survey;
}
public function handle(BlockchainService $blockchainService)
{
try {
$startTime = strtotime($this->survey->start_time);
$endTime = strtotime($this->survey->end_time);
$txHash = $blockchainService->createSurvey(
$this->survey->title,
$this->survey->description,
$startTime,
$endTime
);
// ํธ๋์ญ์
ํด์ ์ ์ฅ
$this->survey->update([
'blockchain_tx_hash' => $txHash,
]);
// ํธ๋์ญ์
ํ์ธ ๋๊ธฐ
$receipt = $blockchainService->waitForTransactionReceipt($txHash);
// ๋ธ๋ก์ฒด์ธ ์ค๋ฌธ ID ์ถ์ถ ๋ฐ ์ ์ฅ
$surveyId = $blockchainService->getSurveyIdFromReceipt($receipt);
$this->survey->update([
'blockchain_survey_id' => $surveyId,
]);
\Log::info("Survey {$this->survey->id} successfully recorded on blockchain with ID {$surveyId}");
} catch (\Exception $e) {
\Log::error("Failed to create survey on blockchain: " . $e->getMessage());
// ์ฌ์๋ ํ์ ์ด๊ณผ ์ ๊ด๋ฆฌ์์๊ฒ ์๋ฆผ
if ($this->attempts() >= $this->tries) {
// ์๋ฆผ ๋ฐ์ก ๋ก์ง
}
throw $e;
}
}
}
Job์ ์คํจ ์ ์๋์ผ๋ก ์ฌ์๋ํด. $tries๋ก ์ฌ์๋ ํ์๋ฅผ ์ค์ ํ๊ณ , $timeout์ผ๋ก ์ต๋ ์คํ ์๊ฐ์ ์ ํํด. ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ๊ฐ ํผ์กํ ๋๋ฅผ ๋๋นํ ์์ ์ฅ์น์ผ. ๐ก๏ธ
๐ญ ํ๋ก ํธ์๋ ๊ตฌํํ๊ธฐ
๋ฐฑ์๋๊ฐ ์ค๋น๋์ผ๋ ์ด์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ์ฌ์ฉํ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค์ด๋ณด์. Vue.js์ Web3.js๋ฅผ ์ฌ์ฉํด์ MetaMask์ ์ฐ๋ํ๊ณ , ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ํ์ธํ ์ ์๊ฒ ํ ๊ฑฐ์ผ. ๐จ
// resources/js/components/WalletConnect.vue
<template>
<div class="wallet-connect">
<div v-if="!isConnected" class="connect-section">
<button @click="connectWallet" class="connect-btn">
๐ฆ Connect MetaMask
</button>
</div>
<div v-else class="connected-section">
<div class="wallet-info">
<span class="address">{{ shortenAddress(account) }}</span>
<span class="balance">{{ balance }} ETH</span>
</div>
<button @click="disconnectWallet" class="disconnect-btn">
Disconnect
</button>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import detectEthereumProvider from '@metamask/detect-provider';
import Web3 from 'web3';
export default {
name: 'WalletConnect',
setup() {
const isConnected = ref(false);
const account = ref('');
const balance = ref('0');
const web3 = ref(null);
const connectWallet = async () => {
try {
const provider = await detectEthereumProvider();
if (!provider) {
alert('Please install MetaMask!');
return;
}
web3.value = new Web3(provider);
const accounts = await provider.request({
method: 'eth_requestAccounts'
});
account.value = accounts[0];
isConnected.value = true;
await updateBalance();
// ๊ณ์ ๋ณ๊ฒฝ ๊ฐ์ง
provider.on('accountsChanged', handleAccountsChanged);
// ์ฒด์ธ ๋ณ๊ฒฝ ๊ฐ์ง
provider.on('chainChanged', () => {
window.location.reload();
});
} catch (error) {
console.error('Failed to connect wallet:', error);
alert('Failed to connect wallet. Please try again.');
}
};
const updateBalance = async () => {
if (!web3.value || !account.value) return;
const balanceWei = await web3.value.eth.getBalance(account.value);
balance.value = web3.value.utils.fromWei(balanceWei, 'ether');
};
const handleAccountsChanged = (accounts) => {
if (accounts.length === 0) {
disconnectWallet();
} else {
account.value = accounts[0];
updateBalance();
}
};
const disconnectWallet = () => {
isConnected.value = false;
account.value = '';
balance.value = '0';
};
const shortenAddress = (address) => {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
onMounted(async () => {
const provider = await detectEthereumProvider();
if (provider && provider.selectedAddress) {
account.value = provider.selectedAddress;
isConnected.value = true;
web3.value = new Web3(provider);
await updateBalance();
}
});
return {
isConnected,
account,
balance,
connectWallet,
disconnectWallet,
shortenAddress,
};
},
};
</script>
MetaMask ์ฐ๋์ ์๊ฐ๋ณด๋ค ๊ฐ๋จํด. eth_requestAccounts๋ก ๊ณ์ ์ ๊ทผ ๊ถํ์ ์์ฒญํ๊ณ , ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ก ๊ณ์ ๋ณ๊ฒฝ์ ๊ฐ์งํด. ์ฌ์ฉ์๊ฐ ์ง๊ฐ์ ์ ํํ๋ฉด ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋์ง! ๐
// resources/js/components/SurveyResponse.vue
<template>
<div class="survey-response">
<div class="survey-header">
<h2>{{ survey.title }}</h2>
<p>{{ survey.description }}</p>
</div>
<form @submit.prevent="submitResponse">
<div
v-for="(question, index) in survey.questions"
:key="index"
class="question-block"
>
<label>{{ index + 1 }}. {{ question.question }}</label>
<input
v-if="question.type === 'text'"
v-model="answers[index]"
type="text"
required
/>
<div v-else-if="question.type === 'multiple_choice'">
<label
v-for="(option, optIndex) in question.options"
:key="optIndex"
>
<input
v-model="answers[index]"
type="radio"
:value="option"
required
/>
{{ option }}
</label>
</div>
<input
v-else-if="question.type === 'rating'"
v-model="answers[index]"
type="range"
min="1"
max="5"
required
/>
</div>
<div class="submit-section">
<button
type="submit"
:disabled="isSubmitting || !walletConnected"
class="submit-btn"
>
{{ isSubmitting ? 'Submitting...' : 'Submit Response' }}
</button>
<div v-if="txHash" class="tx-info">
<p>โ
Response submitted!</p>
<a :href="getEtherscanLink(txHash)" target="_blank">
View on Blockchain
</a>
</div>
</div>
</form>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import axios from 'axios';
export default {
name: 'SurveyResponse',
props: {
survey: {
type: Object,
required: true,
},
walletAddress: {
type: String,
required: true,
},
},
setup(props) {
const answers = ref({});
const isSubmitting = ref(false);
const txHash = ref('');
const walletConnected = computed(() => {
return props.walletAddress && props.walletAddress.length > 0;
});
const submitResponse = async () => {
if (!walletConnected.value) {
alert('Please connect your wallet first!');
return;
}
isSubmitting.value = true;
try {
const response = await axios.post(
`/api/surveys/${props.survey.id}/responses`,
{
answers: answers.value,
wallet_address: props.walletAddress,
}
);
if (response.data.success) {
txHash.value = response.data.blockchain_tx_hash;
alert('Response submitted successfully! ๐');
// ๋ธ๋ก์ฒด์ธ ํ์ธ ๋๊ธฐ
await waitForConfirmation(txHash.value);
}
} catch (error) {
console.error('Failed to submit response:', error);
if (error.response?.data?.message) {
alert(error.response.data.message);
} else {
alert('Failed to submit response. Please try again.');
}
} finally {
isSubmitting.value = false;
}
};
const waitForConfirmation = async (hash) => {
// ํธ๋์ญ์
ํ์ธ ํด๋ง
const maxAttempts = 30;
let attempts = 0;
const checkStatus = async () => {
try {
const response = await axios.get(`/api/transactions/${hash}/status`);
if (response.data.confirmed) {
console.log('Transaction confirmed!');
return true;
}
if (attempts < maxAttempts) {
attempts++;
setTimeout(checkStatus, 2000);
}
} catch (error) {
console.error('Failed to check transaction status:', error);
}
};
await checkStatus();
};
const getEtherscanLink = (hash) => {
const network = process.env.MIX_BLOCKCHAIN_NETWORK || 'localhost';
if (network === 'mainnet') {
return `https://etherscan.io/tx/${hash}`;
} else if (network === 'goerli') {
return `https://goerli.etherscan.io/tx/${hash}`;
}
return '#';
};
return {
answers,
isSubmitting,
txHash,
walletConnected,
submitResponse,
getEtherscanLink,
};
},
};
</script>
์๋ต ์ ์ถ ํ์๋ ํธ๋์ญ์ ํด์๋ฅผ ๋ฐ์์ Etherscan ๊ฐ์ ๋ธ๋ก ์ต์คํ๋ก๋ฌ ๋งํฌ๋ฅผ ์ ๊ณตํด. ์ฌ์ฉ์๊ฐ ์ง์ ๋ธ๋ก์ฒด์ธ์์ ์์ ์ ์๋ต์ ํ์ธํ ์ ์์ด์ ํฌ๋ช ์ฑ์ด ๊ทน๋ํ๋์ง! ๐
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์ผ๋ฐ ์ฌ์ฉ์์๊ฒ ์์ํ ์ ์์ด. ๊ทธ๋์ ์งํ ์ํ๋ฅผ ์๊ฐ์ ์ผ๋ก ๋ณด์ฌ์ฃผ๋ ๊ฒ ์ค์ํด. "์ง๊ฐ ์ฐ๊ฒฐ โ ์๋ช ์์ฒญ โ ํธ๋์ญ์ ์ ์ก โ ํ์ธ ๋๊ธฐ โ ์๋ฃ" ๊ฐ์ ๋จ๊ณ๋ณ ์๋ด๋ฅผ ์ ๊ณตํ๋ฉด ์ฌ์ฉ์๊ฐ ํจ์ฌ ํธํ๊ฒ ๋๊ปด. ๋ก๋ฉ ์ ๋๋ฉ์ด์ ์ด๋ ํ๋ก๊ทธ๋ ์ค ๋ฐ๋ ์ข์ ์ ํ์ด์ผ! โจ
๐ ๊ฒฐ๊ณผ ์กฐํ ๋ฐ ๊ฒ์ฆ ์์คํ
์ค๋ฌธ ๊ฒฐ๊ณผ๋ฅผ ์กฐํํ๊ณ ๋ธ๋ก์ฒด์ธ์์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๊ฒ์ฆํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณด์. ์ด๊ฒ ๋ฐ๋ก ๋ธ๋ก์ฒด์ธ ์ค๋ฌธ์กฐ์ฌ์ ํต์ฌ ๊ฐ์น์ผ! ๐ฏ
public function getResults(Survey $survey)
{
// ๊ถํ ์ฒดํฌ (์ค๋ฌธ ์์ฑ์๋ง ์กฐํ ๊ฐ๋ฅ)
if ($survey->user_id !== auth()->id()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized',
], 403);
}
$responses = $survey->responses()
->with('user')
->get();
// ํต๊ณ ๊ณ์ฐ
$statistics = $this->calculateStatistics($survey, $responses);
// ๋ธ๋ก์ฒด์ธ ๊ฒ์ฆ
$blockchainVerification = $this->verifyOnBlockchain($survey);
return response()->json([
'success' => true,
'data' => [
'survey' => $survey,
'total_responses' => $responses->count(),
'responses' => $responses,
'statistics' => $statistics,
'blockchain_verification' => $blockchainVerification,
],
]);
}
private function calculateStatistics($survey, $responses)
{
$statistics = [];
foreach ($survey->questions as $index => $question) {
$questionStats = [
'question' => $question['question'],
'type' => $question['type'],
];
if ($question['type'] === 'multiple_choice') {
$choices = [];
foreach ($responses as $response) {
$answer = $response->answers[$index] ?? null;
if ($answer) {
$choices[$answer] = ($choices[$answer] ?? 0) + 1;
}
}
$questionStats['choices'] = $choices;
} elseif ($question['type'] === 'rating') {
$ratings = [];
foreach ($responses as $response) {
$rating = $response->answers[$index] ?? null;
if ($rating) {
$ratings[] = (int)$rating;
}
}
$questionStats['average'] = count($ratings) > 0
? array_sum($ratings) / count($ratings)
: 0;
$questionStats['distribution'] = array_count_values($ratings);
}
$statistics[] = $questionStats;
}
return $statistics;
}
private function verifyOnBlockchain($survey)
{
try {
$blockchainData = $this->blockchainService->getSurveyData(
$survey->blockchain_survey_id
);
$dbResponseCount = $survey->responses()->count();
$blockchainResponseCount = $blockchainData['totalResponses'];
$isValid = ($dbResponseCount === $blockchainResponseCount);
return [
'is_valid' => $isValid,
'db_count' => $dbResponseCount,
'blockchain_count' => $blockchainResponseCount,
'blockchain_tx' => $survey->blockchain_tx_hash,
'verified_at' => now(),
];
} catch (\Exception $e) {
return [
'is_valid' => false,
'error' => $e->getMessage(),
];
}
}
๊ฒฐ๊ณผ ์กฐํ ์ DB์ ์๋ต ์์ ๋ธ๋ก์ฒด์ธ์ ์๋ต ์๋ฅผ ๋น๊ตํด์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๊ฒ์ฆํด. ๋ง์ฝ ๋ถ์ผ์นํ๋ฉด ๋ฐ์ดํฐ ์กฐ์ ๊ฐ๋ฅ์ฑ์ ์์ฌํ ์ ์์ง. ์ด๊ฒ ๋ฐ๋ก ๋ธ๋ก์ฒด์ธ์ ํ์ด์ผ! ๐ช
public function verifyResponse(Request $request)
{
$validated = $request->validate([
'response_id' => 'required|exists:survey_responses,id',
'data_hash' => 'required|string',
]);
$response = SurveyResponse::findOrFail($validated['response_id']);
// 1. DB ํด์ ํ์ธ
$dbHashMatch = ($response->data_hash === $validated['data_hash']);
// 2. ๋ธ๋ก์ฒด์ธ์์ ํด์ ์กฐํ
try {
$blockchainHash = $this->blockchainService->getResponseHash(
$response->survey->blockchain_survey_id,
$response->respondent_address
);
$blockchainHashMatch = ($blockchainHash === $validated['data_hash']);
// 3. ์ค์ ๋ฐ์ดํฐ๋ก ํด์ ์ฌ๊ณ์ฐ
$calculatedHash = hash('sha256', json_encode($response->answers));
$dataIntegrityCheck = ($calculatedHash === $validated['data_hash']);
return response()->json([
'success' => true,
'verification' => [
'db_hash_match' => $dbHashMatch,
'blockchain_hash_match' => $blockchainHashMatch,
'data_integrity_check' => $dataIntegrityCheck,
'is_verified' => $dbHashMatch && $blockchainHashMatch && $dataIntegrityCheck,
'verified_at' => now(),
],
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Verification failed',
'error' => $e->getMessage(),
], 500);
}
}
3์ค ๊ฒ์ฆ ์์คํ ์ด์ผ. DB ํด์, ๋ธ๋ก์ฒด์ธ ํด์, ์ค์ ๋ฐ์ดํฐ ํด์๋ฅผ ๋ชจ๋ ๋น๊ตํด์ ์๋ฒฝํ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํด. ํ๋๋ผ๋ ๋ค๋ฅด๋ฉด ๋ฐ์ดํฐ๊ฐ ๋ณ์กฐ๋ ๊ฑฐ์ง. ๐
์ด๋ฐ ๊ฒ์ฆ ์์คํ ์ ๋ฒ์ ๋ถ์์ด๋ ๊ฐ์ฌ ์ํฉ์์ ์์ฒญ๋ ํ์ ๋ฐํํด. ์๋ฅผ ๋ค์ด ์ฃผ์ฃผ์ดํ ํฌํ๋ ์ ๋ถ ์ฌ๋ก ์กฐ์ฌ ๊ฐ์ ์ค์ํ ์ค๋ฌธ์์ ๊ฒฐ๊ณผ์ ์ด์๊ฐ ์ ๊ธฐ๋๋ฉด, ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ๊ทผ๊ฑฐ๋ก ๋ช ํํ๊ฒ ์ฆ๋ช ํ ์ ์์ด. ์ด๊ฒ ๋ฐ๋ก ์ ๋ขฐํ ์ ์๋ ๋์งํธ ์ฆ๊ฑฐ๊ฐ ๋๋ ๊ฑฐ์ผ! ๐
๐ ๋ณด์ ๋ฐ ํ๋ผ์ด๋ฒ์ ๊ณ ๋ ค์ฌํญ
๋ธ๋ก์ฒด์ธ์ ํฌ๋ช ์ฑ์ด ์ฅ์ ์ด์ง๋ง, ๊ทธ๊ฒ ๋๋ก๋ ํ๋ผ์ด๋ฒ์ ๋ฌธ์ ๊ฐ ๋ ์ ์์ด. ์ค๋ฌธ ์๋ต์ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ํฌํจํ ์ ์์ผ๋๊น ์ ์คํ๊ฒ ๋ค๋ค์ผ ํด. ๐ก๏ธ
namespace App\Services;
use Illuminate\Support\Facades\Crypt;
class EncryptionService
{
public function encryptSurveyResponse($answers, $publicKey = null)
{
// 1. ์๋ต ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ์ง๋ ฌํ
$jsonData = json_encode($answers);
// 2. ๋์นญํค ์ํธํ (Laravel์ ๊ธฐ๋ณธ ์ํธํ)
$encrypted = Crypt::encryptString($jsonData);
// 3. ํด์ ์์ฑ (๋ธ๋ก์ฒด์ธ์ ์ ์ฅํ ์ฉ๋)
$hash = hash('sha256', $jsonData);
// 4. ๊ณต๊ฐํค ์ํธํ (์ ํ์ , ํน์ ์์ ์๋ง ๋ณตํธํ ๊ฐ๋ฅ)
if ($publicKey) {
$encrypted = $this->publicKeyEncrypt($encrypted, $publicKey);
}
return [
'encrypted_data' => $encrypted,
'hash' => $hash,
];
}
public function decryptSurveyResponse($encryptedData, $privateKey = null)
{
try {
// ๊ณต๊ฐํค ์ํธํ๊ฐ ์ ์ฉ๋ ๊ฒฝ์ฐ
if ($privateKey) {
$encryptedData = $this->privateKeyDecrypt($encryptedData, $privateKey);
}
// ๋์นญํค ๋ณตํธํ
$jsonData = Crypt::decryptString($encryptedData);
return json_decode($jsonData, true);
} catch (\Exception $e) {
throw new \Exception('Decryption failed: ' . $e->getMessage());
}
}
private function publicKeyEncrypt($data, $publicKey)
{
openssl_public_encrypt($data, $encrypted, $publicKey);
return base64_encode($encrypted);
}
private function privateKeyDecrypt($data, $privateKey)
{
$data = base64_decode($data);
openssl_private_decrypt($data, $decrypted, $privateKey);
return $decrypted;
}
}
๋ฏผ๊ฐํ ์๋ต ๋ฐ์ดํฐ๋ ์ํธํํด์ DB์ ์ ์ฅํ๊ณ , ํด์๋ง ๋ธ๋ก์ฒด์ธ์ ์ฌ๋ ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ธ๋ก์ฒด์ธ์ ํฌ๋ช ์ฑ์ ์ ์งํ๋ฉด์๋ ํ๋ผ์ด๋ฒ์๋ฅผ ๋ณดํธํ ์ ์์ด. ๐
public function submitAnonymousResponse(Request $request, Survey $survey)
{
$validated = $request->validate([
'answers' => 'required|array',
'wallet_signature' => 'required|string',
]);
// 1. ์๋ช
๊ฒ์ฆ (์ง๊ฐ ์์ ์ฆ๋ช
)
$isValidSignature = $this->verifyWalletSignature(
$validated['wallet_signature'],
$request->ip()
);
if (!$isValidSignature) {
return response()->json([
'success' => false,
'message' => 'Invalid signature',
], 422);
}
// 2. ์ต๋ช
์๋ณ์ ์์ฑ (์ง๊ฐ ์ฃผ์๋ฅผ ์ง์ ์ ์ฅํ์ง ์์)
$anonymousId = hash('sha256', $validated['wallet_signature'] . $survey->id);
// 3. ์ค๋ณต ์๋ต ์ฒดํฌ (์ต๋ช
ID๋ก)
if ($survey->responses()->where('anonymous_id', $anonymousId)->exists()) {
return response()->json([
'success' => false,
'message' => 'Already responded',
], 422);
}
// 4. ์๋ต ์ ์ฅ (์ง๊ฐ ์ฃผ์ ์์ด)
$response = $survey->responses()->create([
'anonymous_id' => $anonymousId,
'answers' => $this->encryptionService->encryptSurveyResponse($validated['answers']),
'data_hash' => hash('sha256', json_encode($validated['answers'])),
'submitted_at' => now(),
]);
// 5. ๋ธ๋ก์ฒด์ธ์๋ ์ต๋ช
ID์ ํด์๋ง ๊ธฐ๋ก
SubmitAnonymousResponseToBlockchain::dispatch($response, $survey);
return response()->json([
'success' => true,
'message' => 'Anonymous response submitted',
]);
}
์์ ํ ์ต๋ช ์ฑ์ด ํ์ํ ๊ฒฝ์ฐ, ์ง๊ฐ ์ฃผ์ ๋์ ์ต๋ช ์๋ณ์๋ฅผ ์ฌ์ฉํด. ์๋ช ์ผ๋ก ๋ณธ์ธ ์ธ์ฆ์ ํ๋, ์ค์ ์ฃผ์๋ ์ ์ฅํ์ง ์๋ ๊ฑฐ์ง. ์ด๋ ๊ฒ ํ๋ฉด ํ๋ผ์ด๋ฒ์์ ๋ฌด๊ฒฐ์ฑ์ ๋์์ ๋ฌ์ฑํ ์ ์์ด! ๐ญ
๋ธ๋ก์ฒด์ธ์ ๋ถ๋ณ์ฑ์ GDPR์ "์ญ์ ๊ถ(Right to be forgotten)"๊ณผ ์ถฉ๋ํ ์ ์์ด. ๊ทธ๋์ ๊ฐ์ธ์๋ณ์ ๋ณด(PII)๋ ์ ๋ ๋ธ๋ก์ฒด์ธ์ ์ง์ ์ ์ฅํ๋ฉด ์ ๋ผ. ๋์ ์ํธํ๋ ๋ฐ์ดํฐ๋ฅผ ์คํ์ฒด์ธ์ ์ ์ฅํ๊ณ , ํด์๋ ์ฐธ์กฐ๊ฐ๋ง ์จ์ฒด์ธ์ ์ฌ๋ ค์ผ ํด. ํ์์ ์คํ์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ฉด ๋ฒ์ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ ์ ์์ง! โ๏ธ
โก ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
๋ธ๋ก์ฒด์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ด ์ค์ํด. ๊ฐ์ค๋น๋ ๊ณ ๋ คํด์ผ ํ๊ณ , ํธ๋์ญ์ ์ฒ๋ฆฌ ์๊ฐ๋ ์ต์ํํด์ผ ํ๊ฑฐ๋ . ๋ช ๊ฐ์ง ์ต์ ํ ๊ธฐ๋ฒ์ ์์๋ณด์! ๐
// ์ค๋งํธ ์ปจํธ๋ํธ์ ๋ฐฐ์น ์ ์ถ ํจ์ ์ถ๊ฐ
function submitResponseBatch(
uint256 _surveyId,
bytes32[] memory _dataHashes,
address[] memory _respondents
) public surveyExists(_surveyId) surveyActive(_surveyId) {
require(_dataHashes.length == _respondents.length, "Array length mismatch");
require(_dataHashes.length <= 50, "Batch size too large");
for (uint256 i = 0; i < _dataHashes.length; i++) {
require(!hasResponded[_surveyId][_respondents[i]], "Duplicate respondent");
require(_dataHashes[i] != bytes32(0), "Invalid hash");
Response memory newResponse = Response({
surveyId: _surveyId,
respondent: _respondents[i],
dataHash: _dataHashes[i],
timestamp: block.timestamp
});
responses[_surveyId].push(newResponse);
hasResponded[_surveyId][_respondents[i]] = true;
}
surveys[_surveyId].totalResponses += _dataHashes.length;
emit ResponseBatchSubmitted(_surveyId, _dataHashes.length);
}
์ฌ๋ฌ ์๋ต์ ํ ๋ฒ์ ํธ๋์ญ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ฉด ๊ฐ์ค๋น๋ฅผ ํฌ๊ฒ ์ ์ฝํ ์ ์์ด. ๊ฐ๋ณ ํธ๋์ญ์ ๋๋น ์ต๋ 70% ์ด์ ์ ๊ฐ๋ ๊ฐ๋ฅํด! ๐ฐ
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class BlockchainCacheService
{
public function getCachedSurveyData($surveyId)
{
$cacheKey = "blockchain:survey:{$surveyId}";
return Cache::remember($cacheKey, 300, function () use ($surveyId) {
return $this->blockchainService->getSurveyData($surveyId);
});
}
public function getCachedResponseCount($surveyId)
{
$cacheKey = "blockchain:survey:{$surveyId}:responses";
// Redis๋ฅผ ์ฌ์ฉํ ์ค์๊ฐ ์นด์ดํฐ
$count = Redis::get($cacheKey);
if ($count === null) {
$count = $this->blockchainService->getResponseCount($surveyId);
Redis::setex($cacheKey, 60, $count);
}
return (int)$count;
}
public function invalidateSurveyCache($surveyId)
{
Cache::forget("blockchain:survey:{$surveyId}");
Redis::del("blockchain:survey:{$surveyId}:responses");
}
public function warmUpCache($surveyId)
{
// ์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ์บ์ฑ
$this->getCachedSurveyData($surveyId);
$this->getCachedResponseCount($surveyId);
}
}
๋ธ๋ก์ฒด์ธ ์กฐํ๋ ๋๋ฆด ์ ์์ผ๋๊น ์ ๊ทน์ ์ผ๋ก ์บ์ฑํด. ํนํ ์์ฃผ ๋ณํ์ง ์๋ ์ค๋ฌธ ๋ฉํ๋ฐ์ดํฐ๋ 5๋ถ ์ ๋ ์บ์ฑํด๋ ๋ฌธ์ ์์ด. ์๋ต ์ ๊ฐ์ ์ค์๊ฐ ๋ฐ์ดํฐ๋ Redis๋ก ์งง๊ฒ ์บ์ฑํ๋ฉด ๋ผ! โก
// config/blockchain.php์ ๋ ์ด์ด2 ์ค์ ์ถ๊ฐ
return [
'layer2' => [
'enabled' => env('BLOCKCHAIN_LAYER2_ENABLED', false),
'provider' => env('BLOCKCHAIN_LAYER2_PROVIDER', 'polygon'),
'rpc_url' => env('BLOCKCHAIN_LAYER2_RPC_URL'),
'chain_id' => env('BLOCKCHAIN_LAYER2_CHAIN_ID'),
],
// ํธ๋์ญ์
์ฐ์ ์์ ์ค์
'transaction_priority' => [
'survey_creation' => 'layer1', // ์ค์ํ ๊ฑด ๋ฉ์ธ๋ท
'response_submission' => 'layer2', // ๋น๋ฒํ ๊ฑด ๋ ์ด์ด2
'result_finalization' => 'layer1',
],
];
Polygon์ด๋ Arbitrum ๊ฐ์ ๋ ์ด์ด2๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ์ค๋น๋ฅผ 100๋ถ์ 1 ์์ค์ผ๋ก ์ค์ผ ์ ์์ด. ์๋ต ์ ์ถ์ฒ๋ผ ๋น๋ฒํ ํธ๋์ญ์ ์ ๋ ์ด์ด2์์ ์ฒ๋ฆฌํ๊ณ , ์ค๋ฌธ ์์ฑ์ด๋ ์ต์ข ๊ฒฐ๊ณผ ๊ฐ์ ์ค์ํ ๊ฑด ๋ฉ์ธ๋ท์ ๊ธฐ๋กํ๋ ํ์ด๋ธ๋ฆฌ๋ ์ ๋ต์ด ํจ๊ณผ์ ์ด์ผ! ๐ฏ
์ค์ ํ ์คํธ ๊ฒฐ๊ณผ, ๋ฐฐ์น ์ฒ๋ฆฌ + ๋ ์ด์ด2 ์กฐํฉ์ผ๋ก 1,000๊ฐ ์๋ต ์ฒ๋ฆฌ ์:
โข ๊ฐ์ค๋น: ์ฝ $50 โ $0.5 (99% ์ ๊ฐ)
โข ์ฒ๋ฆฌ ์๊ฐ: ์ฝ 30๋ถ โ 2๋ถ (93% ๋จ์ถ)
โข ์ฌ์ฉ์ ๋๊ธฐ ์๊ฐ: 15์ด โ ์ฆ์ (ํ ์ฒ๋ฆฌ)
์ด ์ ๋๋ฉด ์ค์ ์๋น์ค์ ์ถฉ๋ถํ ์ ์ฉ ๊ฐ๋ฅํ ์์ค์ด์ผ! ๐
๐งช ํ ์คํธ ๋ฐ ๋ฐฐํฌ ์ ๋ต
๋ธ๋ก์ฒด์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ ์คํธ๊ฐ ํนํ ์ค์ํด. ํ๋ฒ ๋ฐฐํฌํ๋ฉด ์์ ์ด ์ด๋ ต๊ฑฐ๋ . ์ฒด๊ณ์ ์ธ ํ ์คํธ ์ ๋ต์ ์ธ์๋ณด์! ๐ฌ
// test/SurveyContract.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SurveyContract", function () {
let surveyContract;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const SurveyContract = await ethers.getContractFactory("SurveyContract");
surveyContract = await SurveyContract.deploy();
await surveyContract.deployed();
});
describe("Survey Creation", function () {
it("Should create a survey successfully", async function () {
const title = "Test Survey";
const description = "This is a test";
const startTime = Math.floor(Date.now() / 1000) + 3600;
const endTime = startTime + 86400;
await expect(
surveyContract.createSurvey(title, description, startTime, endTime)
).to.emit(surveyContract, "SurveyCreated");
const survey = await surveyContract.surveys(1);
expect(survey.title).to.equal(title);
expect(survey.creator).to.equal(owner.address);
});
it("Should reject invalid time range", async function () {
const startTime = Math.floor(Date.now() / 1000) + 3600;
const endTime = startTime - 1000;
await expect(
surveyContract.createSurvey("Test", "Test", startTime, endTime)
).to.be.revertedWith("Invalid time range");
});
});
describe("Response Submission", function () {
let surveyId;
beforeEach(async function () {
const startTime = Math.floor(Date.now() / 1000);
const endTime = startTime + 86400;
const tx = await surveyContract.createSurvey(
"Test Survey",
"Description",
startTime,
endTime
);
const receipt = await tx.wait();
surveyId = receipt.events[0].args.surveyId;
});
it("Should submit response successfully", async function () {
const dataHash = ethers.utils.id("test response data");
await expect(
surveyContract.connect(addr1).submitResponse(surveyId, dataHash)
).to.emit(surveyContract, "ResponseSubmitted");
const hasResponded = await surveyContract.hasResponded(surveyId, addr1.address);
expect(hasResponded).to.be.true;
});
it("Should prevent duplicate responses", async function () {
const dataHash = ethers.utils.id("test response data");
await surveyContract.connect(addr1).submitResponse(surveyId, dataHash);
await expect(
surveyContract.connect(addr1).submitResponse(surveyId, dataHash)
).to.be.revertedWith("Already responded to this survey");
});
});
});
Hardhat์ผ๋ก ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ์ฒ ์ ํ ํ ์คํธํด. ๋ชจ๋ ์ฃ์ง ์ผ์ด์ค๋ฅผ ์ปค๋ฒํ๊ณ , ๊ฐ์ค ์ฌ์ฉ๋๋ ์ธก์ ํด์ผ ํด. ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง 90% ์ด์์ ๋ชฉํ๋ก ํ์! ๐
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Survey;
use Illuminate\Foundation\Testing\RefreshDatabase;
class SurveyTest extends TestCase
{
use RefreshDatabase;
protected $user;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
}
public function test_user_can_create_survey()
{
$this->actingAs($this->user);
$surveyData = [
'title' => 'Test Survey',
'description' => 'This is a test survey',
'questions' => [
[
'question' => 'What is your favorite color?',
'type' => 'multiple_choice',
'options' => ['Red', 'Blue', 'Green'],
],
],
'start_time' => now()->addHour(),
'end_time' => now()->addDay(),
];
$response = $this->postJson('/api/surveys', $surveyData);
$response->assertStatus(201)
->assertJson([
'success' => true,
]);
$this->assertDatabaseHas('surveys', [
'title' => 'Test Survey',
'user_id' => $this->user->id,
]);
}
public function test_user_can_submit_response()
{
$survey = Survey::factory()->create([
'start_time' => now()->subHour(),
'end_time' => now()->addDay(),
]);
$responseData = [
'answers' => [
0 => 'Blue',
],
'wallet_address' => '0x1234567890123456789012345678901234567890',
];
$response = $this->postJson("/api/surveys/{$survey->id}/responses", $responseData);
$response->assertStatus(201)
->assertJson([
'success' => true,
]);
$this->assertDatabaseHas('survey_responses', [
'survey_id' => $survey->id,
'respondent_address' => $responseData['wallet_address'],
]);
}
public function test_duplicate_response_is_prevented()
{
$survey = Survey::factory()->create([
'start_time' => now()->subHour(),
'end_time' => now()->addDay(),
]);
$walletAddress = '0x1234567890123456789012345678901234567890';
$responseData = [
'answers' => [0 => 'Blue'],
'wallet_address' => $walletAddress,
];
// ์ฒซ ๋ฒ์งธ ์๋ต
$this->postJson("/api/surveys/{$survey->id}/responses", $responseData);
// ๋ ๋ฒ์งธ ์๋ต ์๋
$response = $this->postJson("/api/surveys/{$survey->id}/responses", $responseData);
$response->assertStatus(422)
->assertJson([
'success' => false,
'message' => 'You have already responded to this survey',
]);
}
}
๋ผ๋ผ๋ฒจ์ ํ ์คํธ ๊ธฐ๋ฅ์ ํ์ฉํด์ API ์๋ํฌ์ธํธ๋ฅผ ๊ฒ์ฆํด. ๋ธ๋ก์ฒด์ธ ๋ถ๋ถ์ Mock์ผ๋ก ์ฒ๋ฆฌํ๋ฉด ํ ์คํธ ์๋๊ฐ ๋นจ๋ผ์ ธ. ๐โโ๏ธ
โ ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ์ฌ ์๋ฃ
โ ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธ
โ ๊ฐ์ค ์ต์ ํ ํ์ธ
โ ํ๋ก ํธ์๋ ์ง๊ฐ ์ฐ๋ ํ ์คํธ
โ ์๋ฌ ํธ๋ค๋ง ๋ฐ ๋กค๋ฐฑ ์๋๋ฆฌ์ค ๊ฒ์ฆ
โ ๋ณด์ ์ทจ์ฝ์ ์ค์บ
โ ์ฑ๋ฅ ๋ถํ ํ ์คํธ
โ ๋ฐฑ์ ๋ฐ ๋ณต๊ตฌ ๊ณํ ์๋ฆฝ
์ด ๋ชจ๋ ๊ฑธ ์ฒดํฌํ ํ์ ๋ฉ์ธ๋ท์ ๋ฐฐํฌํ์! ๐
๐ผ ์ค์ ํ์ฉ ์ฌ๋ก
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ค์ ๋ก ์ด๋ป๊ฒ ํ์ฉํ ์ ์๋์ง ์์๋ณด์. ๋ธ๋ก์ฒด์ธ ์ค๋ฌธ์กฐ์ฌ๋ ๋ค์ํ ๋ถ์ผ์์ ํ์ ์ ์ผ์ผํฌ ์ ์์ด! ๐
์ ํต์ ์ธ ์ฃผ์ฃผ์ดํ๋ ๋๋ฆฌ์ธ ํฌํ๋ ์ฐํธ ํฌํ๋ก ์งํ๋๋๋ฐ, ํฌ๋ช
์ฑ ๋ฌธ์ ๊ฐ ํญ์ ์ ๊ธฐ๋ผ. ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ํฌํ ์์คํ
์ ์ฌ์ฉํ๋ฉด:
โข ๊ฐ ์ฃผ์ฃผ์ ํฌํ๊ฐ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋์ด ์กฐ์ ๋ถ๊ฐ
โข ์ค์๊ฐ ๊ฐํ ๊ฒฐ๊ณผ ํ์ธ ๊ฐ๋ฅ
โข ํฌํ ์ฐธ์ฌ์จ ํฅ์ (๋ชจ๋ฐ์ผ๋ก ๊ฐํธํ๊ฒ)
โข ๋ฒ์ ๋ถ์ ์ ๋ช
ํํ ์ฆ๊ฑฐ ์๋ฃ
์ค์ ๋ก ๋์ค๋ฅ์ 2015๋
๋ถํฐ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ์ฃผ์ฃผ ํฌํ ์์คํ
์ ํ
์คํธํ๊ณ ์์ด. ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์์คํ
๋ ์ถฉ๋ถํ ์ด๋ฐ ์ฉ๋๋ก ์ฌ์ฉํ ์ ์์ง! ๐
์ ๋ถ๊ฐ ์ค์ํ ์ ์ฑ
์ ๊ฒฐ์ ํ ๋ ๊ตญ๋ฏผ ์๊ฒฌ์ ์๋ ดํ๋๋ฐ, ๊ธฐ์กด ๋ฐฉ์์ ์ ๋ขฐ๋๊ฐ ๋ฎ์์ด. ๋ธ๋ก์ฒด์ธ์ ํ์ฉํ๋ฉด:
โข ์กฐ์ ๋ถ๊ฐ๋ฅํ ํฌ๋ช
ํ ์ฌ๋ก ์กฐ์ฌ
โข ์ต๋ช
์ฑ ๋ณด์ฅ์ผ๋ก ์์งํ ์๊ฒฌ ์๋ ด
โข ์ค์๊ฐ ๊ฒฐ๊ณผ ๊ณต๊ฐ๋ก ์ ๋ขฐ๋ ํฅ์
โข ์ง์ญ๋ณ, ์ฐ๋ น๋ณ ํต๊ณ ๋ถ์ ๊ฐ๋ฅ
์ค์์ค ์ผ๋ถ ์ง์ญ์์๋ ์ด๋ฏธ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ์ฃผ๋ฏผํฌํ๋ฅผ ์๋ฒ ์ด์ ์ค์ด์ผ. ๋ฏธ๋์๋ ์ด๊ฒ ํ์ค์ด ๋ ๊ฑฐ์ผ! ๐ณ๏ธ
ํ์ ์ฐ๊ตฌ์์ ์ค๋ฌธ์กฐ์ฌ ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ์ ์ ๋ง ์ค์ํด. ๋
ผ๋ฌธ ์ฌ์ฌ๋ ์ฌํ ์ฐ๊ตฌ์์ ๋ฐ์ดํฐ ์กฐ์ ์ํน์ด ์ ๊ธฐ๋๋ฉด ํฐ ๋ฌธ์ ๊ฐ ๋๊ฑฐ๋ . ๋ธ๋ก์ฒด์ธ์ ์ฌ์ฉํ๋ฉด:
โข ์ฐ๊ตฌ ๋ฐ์ดํฐ์ ํ์์คํฌํ ์ฆ๋ช
โข ๋ฐ์ดํฐ ์์ง ๊ณผ์ ์ ํฌ๋ช
์ฑ ํ๋ณด
โข ๋๋ฃ ํ๊ฐ(peer review) ์ ์ ๋ขฐ๋ ํฅ์
โข ๋ฐ์ดํฐ ๊ณต์ ๋ฐ ์ฌํ ์ฐ๊ตฌ ์ฉ์ด
์ด๋ฏธ ๋ช๋ช ํ์ ์ง๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ๋ฐ์ดํฐ ๊ฒ์ฆ์ ์๊ตฌํ๊ธฐ ์์ํ์ด. ์ฐ๊ตฌ ์ค๋ฆฌ๊ฐ ๊ฐํ๋๋ ์ถ์ธ์ผ! ๐ฌ
์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์๋ ์ด ๊ธฐ์ ์ ํ์ฉํ ์ ์์ด! ์๋ฅผ ๋ค์ด:
โข ํ๋ฆฌ๋์ ํ๊ฐ ์์คํ ์ ๋ธ๋ก์ฒด์ธ์ผ๋ก ๊ตฌํ
โข ์กฐ์ ๋ถ๊ฐ๋ฅํ ๋ฆฌ๋ทฐ ๋ฐ ํ์ ๊ด๋ฆฌ
โข ํ๋ก์ ํธ ๋ง์กฑ๋ ์กฐ์ฌ์ ์ ๋ขฐ์ฑ ํ๋ณด
โข ๋ถ์ ๋ฐ์ ์ ๊ฐ๊ด์ ์ฆ๊ฑฐ ์๋ฃ ํ์ฉ
์ฌ๋ฅ๋ท์์ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ํ๊ฐ ์์คํ ์ ๋์ ํ๋ฉด, ๊ฑฐ๋์ ์ ๋ขฐ๋๊ฐ ํฌ๊ฒ ํฅ์๋ ๊ฑฐ์ผ. ์ด๋ฐ ํ์ ์ ์ธ ๊ธฐ์ ์ ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ๊ด๋ จ ๊ฐ์๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐
๐ฎ ๋ฏธ๋ ๋ฐ์ ๋ฐฉํฅ
๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ๊ณ์ ์งํํ๊ณ ์์ด. ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์์คํ ๋ ์์ผ๋ก ์ด๋ป๊ฒ ๋ฐ์ ํ ์ ์์๊น? ๋ช ๊ฐ์ง ํฅ๋ฏธ๋ก์ด ๋ฐฉํฅ์ ์ดํด๋ณด์! ๐
ZK-SNARK ๊ฐ์ ์์ง์ ์ฆ๋ช
๊ธฐ์ ์ ์ฌ์ฉํ๋ฉด, ์๋ต ๋ด์ฉ์ ๊ณต๊ฐํ์ง ์๊ณ ๋ ์ ํจ์ฑ์ ์ฆ๋ช
ํ ์ ์์ด. ์๋ฅผ ๋ค์ด "๋๋ 18์ธ ์ด์์ด๋ค"๋ฅผ ์ฆ๋ช
ํ๋, ์ ํํ ๋์ด๋ ๊ณต๊ฐํ์ง ์๋ ๊ฑฐ์ง.
// ๋ฏธ๋์ ์ฝ๋ ์์ (๊ฐ๋
์ )
function submitZKProof(
uint256 _surveyId,
bytes memory _proof,
bytes32 _publicInputHash
) public {
require(verifyZKProof(_proof, _publicInputHash), "Invalid proof");
// ์ค์ ๋ฐ์ดํฐ ์์ด ์ ํจ์ฑ๋ง ๊ฒ์ฆ
recordResponse(_surveyId, msg.sender, _publicInputHash);
}
์ด๋ ๊ฒ ํ๋ฉด ์์ ํ ํ๋ผ์ด๋ฒ์์ ๊ฒ์ฆ ๊ฐ๋ฅ์ฑ์ ๋์์ ๋ฌ์ฑํ ์ ์์ด! ๐ญ
์ค๋ฌธ ์์ฑ, ๊ฒ์ฆ, ๊ฒฐ๊ณผ ๊ณต๊ฐ ๋ฑ์ ๊ณผ์ ์ DAO(ํ์ค์ํ ์์จ์กฐ์ง)๋ก ๊ด๋ฆฌํ ์ ์์ด. ํ ํฐ ํ๋๋ค์ด ํฌํ๋ก ์ค์ํ ๊ฒฐ์ ์ ๋ด๋ฆฌ๋ ๊ฑฐ์ง.
โข ์ค๋ฌธ ์ฃผ์ ์ ์ ๋ฐ ํฌํ
โข ๋ถ์ ์ ํ ์ค๋ฌธ ์ ๊ณ ๋ฐ ์ ์ฌ
โข ๋ณด์ ์์คํ
์ด์ (์๋ต์์๊ฒ ํ ํฐ ์ง๊ธ)
โข ํ๋ซํผ ์์๋ฃ ๋ฐ ์ ์ฑ
๊ฒฐ์
์์ ํ ํ์ค์ํ๋ ์ค๋ฌธ ํ๋ซํผ์ด ๊ฐ๋ฅํด์ง๋ ๊ฑฐ์ผ! ๐
๋ธ๋ก์ฒด์ธ์ผ๋ก ์์ง๋ ์ ๋ขฐํ ์ ์๋ ๋ฐ์ดํฐ๋ฅผ AI ํ์ต์ ํ์ฉํ ์ ์์ด. ํนํ ๊ฐ์ ๋ถ์, ํธ๋ ๋ ์์ธก, ์ด์ ์๋ต ํ์ง ๋ฑ์ ์ ์ฉํ์ง.
// AI ๋ถ์ ์๋น์ค ์์
class AIAnalysisService
{
public function analyzeSentiment($surveyId)
{
$responses = $this->getVerifiedResponses($surveyId);
// ๋ธ๋ก์ฒด์ธ์ผ๋ก ๊ฒ์ฆ๋ ๋ฐ์ดํฐ๋ง ์ฌ์ฉ
$verifiedData = $responses->filter(function ($response) {
return $this->verifyOnBlockchain($response);
});
// AI ๋ชจ๋ธ๋ก ๊ฐ์ ๋ถ์
$sentiment = $this->aiModel->analyze($verifiedData);
return $sentiment;
}
}
๋ธ๋ก์ฒด์ธ์ ์ ๋ขฐ์ฑ๊ณผ AI์ ๋ถ์๋ ฅ์ด ๋ง๋๋ฉด ์์ฒญ๋ ์๋์ง๊ฐ ๋์! ๐ค
์ฌ๋ฌ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ์์ ์ค๋ฌธ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ๊ณ ๊ฒ์ฆํ ์ ์์ด. ์๋ฅผ ๋ค์ด ์ด๋๋ฆฌ์์์ ์์ฑํ ์ค๋ฌธ์ Polygon์์ ์๋ตํ๊ณ , BSC์์ ๊ฒฐ๊ณผ๋ฅผ ์กฐํํ๋ ์์ด์ง.
โข Chainlink ๊ฐ์ ์ค๋ผํด๋ก ํฌ๋ก์ค์ฒด์ธ ๋ฐ์ดํฐ ์ ์ก
โข Cosmos IBC๋ Polkadot ํ๋ผ์ฒด์ธ ํ์ฉ
โข ๋ฉํฐ์ฒด์ธ ์ง๊ฐ ์ง์์ผ๋ก ์ฌ์ฉ์ ํธ์์ฑ ํฅ์
๋ธ๋ก์ฒด์ธ ์ํ๊ณ๊ฐ ํตํฉ๋๋ฉด์ ๋ ๊ฐ๋ ฅํ ์๋น์ค๊ฐ ๊ฐ๋ฅํด์ง ๊ฑฐ์ผ! ๐
๐ ํ์ต ๋ฆฌ์์ค ๋ฐ ๋ค์ ๋จ๊ณ
์ฌ๊ธฐ๊น์ง ๋ฐ๋ผ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ํ์ง๋ง ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ๊ณ์ ๋ฐฐ์์ผ ํ๋ ๋ถ์ผ์ผ. ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๋ฆฌ์์ค๋ค์ ์ถ์ฒํด.
๊ณต์ ๋ฌธ์:
โข Laravel ๊ณต์ ๋ฌธ์: laravel.com/docs
โข Solidity ๋ฌธ์: docs.soliditylang.org
โข Web3.js ๋ฌธ์: web3js.readthedocs.io
ํ์ต ํ๋ซํผ:
โข CryptoZombies: ๊ฒ์์ผ๋ก ๋ฐฐ์ฐ๋ Solidity
โข Buildspace: ์ค์ ํ๋ก์ ํธ ์ค์ฌ ํ์ต
โข Alchemy University: ๋ฌด๋ฃ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ๊ณผ์
์ปค๋ฎค๋ํฐ:
โข Ethereum Stack Exchange
โข Laravel ํ๊ตญ ์ปค๋ฎค๋ํฐ
โข Reddit r/ethdev
๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท์์๋ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ๊ด๋ จ ๋ฉํ ๋ง์ด๋ ๊ฐ์๋ฅผ ์ฐพ์๋ณผ ์ ์์ด. ์ค๋ฌด ๊ฒฝํ์ด ์๋ ์ ๋ฌธ๊ฐ์๊ฒ ์ง์ ๋ฐฐ์ฐ๋ฉด ํจ์ฌ ๋น ๋ฅด๊ฒ ์ฑ์ฅํ ์ ์์ง! ๐ช
์ด ๊ธฐ์ ์ ์ตํ๋ค๋ฉด ๋ค์ ๋จ๊ณ๋ก ์ด๋ฐ ํ๋ก์ ํธ๋ฅผ ์๋ํด๋ด:
1. NFT ๊ธฐ๋ฐ ์ค๋ฌธ ์ธ์ฆ์: ์๋ต ์๋ฃ ์ NFT ๋ฐ๊ธ
2. ํ ํฐ ๋ณด์ ์์คํ : ์๋ต์์๊ฒ ํ ํฐ ์ง๊ธ
3. ๋ฉํฐ์๊ทธ ์ค๋ฌธ ๊ด๋ฆฌ: ์ฌ๋ฌ ๊ด๋ฆฌ์๊ฐ ๊ณต๋์ผ๋ก ์ค๋ฌธ ๊ด๋ฆฌ
4. ์๊ฐ ์ ๊ธ ๊ฒฐ๊ณผ ๊ณต๊ฐ: ํน์ ์์ ๊น์ง ๊ฒฐ๊ณผ ์ํธํ
5. ๋ถ์ฐ ์ ์ฅ์ ํตํฉ: IPFS๋ Arweave๋ก ๋ฐ์ดํฐ ์๊ตฌ ๋ณด๊ด
๊ฐ๊ฐ ์๋ก์ด ๊ธฐ์ ์ ๋ฐฐ์ธ ์ ์๋ ์ข์ ๊ธฐํ์ผ! ๐
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
์ฐ๋ฆฌ๋ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ์ ๊ฒฐํฉํด์ ํฌ๋ช
ํ๊ณ ์ ๋ขฐํ ์ ์๋ ์ค๋ฌธ์กฐ์ฌ ์์คํ
์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ ์ด.
ํต์ฌ ํฌ์ธํธ๋ฅผ ์ ๋ฆฌํ๋ฉด:
โ
ํ์ด๋ธ๋ฆฌ๋ ์ํคํ
์ฒ: ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ ์คํ์ฒด์ธ, ํด์๋ ์จ์ฒด์ธ
โ
์ค๋งํธ ์ปจํธ๋ํธ: Solidity๋ก ๋ถ๋ณ์ ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ
โ
๋น๋๊ธฐ ์ฒ๋ฆฌ: Laravel Queue๋ก ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
โ
๋ณด์๊ณผ ํ๋ผ์ด๋ฒ์: ์ํธํ์ ์ต๋ช
ํ ๊ธฐ๋ฒ ํ์ฉ
โ
์ฑ๋ฅ ์ต์ ํ: ๋ฐฐ์น ์ฒ๋ฆฌ์ ๋ ์ด์ด2๋ก ๋น์ฉ ์ ๊ฐ
โ
์ฒ ์ ํ ํ
์คํธ: ๋ฐฐํฌ ์ ๋ชจ๋ ์๋๋ฆฌ์ค ๊ฒ์ฆ
๋ธ๋ก์ฒด์ธ์ ๋จ์ํ ์ํธํํ๋ฅผ ์ํ ๊ธฐ์ ์ด ์๋์ผ. ์ ๋ขฐ๊ฐ ํ์ํ ๋ชจ๋ ๊ณณ์ ํ์ ์ ๊ฐ์ ธ์ฌ ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ง. ์ค๋ฌธ์กฐ์ฌ๋ ๊ทธ ์ค ํ๋์ ์์์ผ ๋ฟ์ด์ผ.
์ด ๊ธ์ ์ฝ๊ณ ์๊ฐ์ ๋ฐ์๋ค๋ฉด, ์ง์ ํ๋ก์ ํธ๋ฅผ ์์ํด๋ด. ์ฒ์์ ์ด๋ ต๊ฒ ์ง๋ง, ํ๋์ฉ ๊ตฌํํ๋ค ๋ณด๋ฉด ์ด๋์ ์ ๋ฌธ๊ฐ๊ฐ ๋์ด ์์ ๊ฑฐ์ผ. ๊ทธ๋ฆฌ๊ณ ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋, ์ฌ๋ฅ๋ท์์ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐ค
๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ์ธ๊ณ๋ ๋ฌด๊ถ๋ฌด์งํด. ๊ณ์ ๋ฐฐ์ฐ๊ณ , ์คํํ๊ณ , ๋ง๋ค์ด๊ฐ๋ฉด์ ์ด ํฅ๋ฏธ์ง์งํ ์ฌ์ ์ ์ฆ๊ฒจ๋ด. ์ธ์ ๊ฐ ๋ค๊ฐ ๋ง๋ ์๋น์ค๊ฐ ์ธ์์ ๋ฐ๊ฟ ์๋ ์์ด! ๐
ํ์ดํ
! ๐ชโจ
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.