Ver3.0 ๐ ๋ผ๋ผ๋ฒจ๋ก ๋ธ๋ก์ฒด์ธ ํ์ต ์ธ์ฆ ํ๋ซํผ ๊ตฌ์ถํ๊ธฐ

๐ ๋ผ๋ผ๋ฒจ๋ก ๋ธ๋ก์ฒด์ธ ํ์ต ์ธ์ฆ ํ๋ซํผ ๊ตฌ์ถํ๊ธฐ
Web3 ์๋, ํ์ต ์ด๋ ฅ์ ๋ธ๋ก์ฒด์ธ์ ์๊ตฌ ๊ธฐ๋กํ๋ ํ์ ์ ์ธ ํ๋ซํผ ๋ง๋ค๊ธฐ
์๋ ! ๐ ์ค๋์ ์ ๋ง ํฅ๋ฏธ์ง์งํ ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ฐ๋ก ๋ผ๋ผ๋ฒจ(Laravel)๊ณผ ๋ธ๋ก์ฒด์ธ์ ๊ฒฐํฉํด์ ํ์ต ์ธ์ฆ ํ๋ซํผ์ ๋ง๋๋ ๊ฑฐ์ผ. ์์ฆ Web3๊ฐ ๋์ธ์์? ๊ทธ๋ฐ๋ฐ ๋ง์ "๋ธ๋ก์ฒด์ธ์ผ๋ก ๋ญ ๋ง๋ค์ง?"๋ผ๊ณ ์๊ฐํ๋ฉด ๋ง๋งํ ๊ฒฝ์ฐ๊ฐ ๋ง๋๋ผ๊ณ . ๊ทธ๋์ ์ค๋นํ์ด! ์ค์ ๋ก ๊ตฌํ ๊ฐ๋ฅํ๊ณ , ์ฌํ์ ์ผ๋ก๋ ์๋ฏธ ์๋ ํ๋ก์ ํธ๋ฅผ ํจ๊ป ๋ง๋ค์ด๋ณด์. ๐ฏ
์ด ๊ธ์ ๋ค ์ฝ๊ณ ๋๋ฉด, ๋๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ํ๋ซํผ์ ์ง์ ๊ตฌ์ถํ ์ ์์ ๊ฑฐ์ผ. ๋ผ๋ผ๋ฒจ์ ๊ฐ๋ ฅํ ๋ฐฑ์๋ ๊ธฐ๋ฅ๊ณผ ๋ธ๋ก์ฒด์ธ์ ํฌ๋ช ์ฑยท๋ถ๋ณ์ฑ์ ๊ฒฐํฉํ๋ฉด ์ ๋ง ๋ฉ์ง ๊ฒฐ๊ณผ๋ฌผ์ด ๋์จ๋ค๋๊น! ํนํ ์จ๋ผ์ธ ๊ต์ก์ด ํ์ฑํ๋ ์์ฆ, ์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์๋ ํ์ต ์ธ์ฆ ์์คํ ์ด ์๋ค๋ฉด ์๊ฐ์๋ค์ ์ ๋ขฐ๋๋ฅผ ํฌ๊ฒ ๋์ผ ์ ์๊ฒ ์ง? ๐
๐ฏ ์ ๋ธ๋ก์ฒด์ธ ํ์ต ์ธ์ฆ ํ๋ซํผ์ธ๊ฐ?
๋จผ์ ์ด ํ๋ก์ ํธ๊ฐ ์ ์๋ฏธ ์๋์ง๋ถํฐ ์ด์ผ๊ธฐํด๋ณผ๊น? ์์ฆ ์จ๋ผ์ธ ๊ต์ก ์์ฅ์ด ์์ฒญ๋๊ฒ ์ปค์ง๊ณ ์์์. ๊ทธ๋ฐ๋ฐ ๋ฌธ์ ๊ฐ ๋ญ๋๋ฉด, ์๋ฃ์ฆ์ด๋ ํ์ต ์ด๋ ฅ์ ์ฝ๊ฒ ์์กฐํ ์ ์๋ค๋ ๊ฑฐ์ผ. PDF ํ์ผ ํ๋ ๋ง๋ค๋ฉด ๋์ด๋๊น ๋ง์ด์ผ. ๐
๋ธ๋ก์ฒด์ธ์ ํ์ฉํ๋ฉด ์ด๋ฐ ๋ฌธ์ ๋ฅผ ์๋ฒฝํ๊ฒ ํด๊ฒฐํ ์ ์์ด. ํ๋ฒ ๊ธฐ๋ก๋ ๋ฐ์ดํฐ๋ ์ ๋ ๋ณ๊ฒฝํ ์ ์๊ณ , ๋๊ตฌ๋ ๊ฒ์ฆํ ์ ์๊ฑฐ๋ . ๊ฒ๋ค๊ฐ ์ค์ ์๋ฒ๊ฐ ๋ค์ด๋์ด๋ ๋ฐ์ดํฐ๋ ์์ ํ๊ฒ ๋ณด์กด๋ผ. ์ด๊ฒ ๋ฐ๋ก ํ์ค์ํ์ ํ์ด์ง! ๐ช
1. ์์กฐ ๋ถ๊ฐ๋ฅ์ฑ: ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋ ํ์ต ์ด๋ ฅ์ ์ํธํ์ ์ผ๋ก ๋ณดํธ๋์ด ์์กฐ๊ฐ ๋ถ๊ฐ๋ฅํด.
2. ํฌ๋ช ์ฑ: ๋๊ตฌ๋ ํ์ต ์ด๋ ฅ์ ๊ฒ์ฆํ ์ ์์ด์ ์ ๋ขฐ๋๊ฐ ๋์์ ธ.
3. ์๊ตฌ ๋ณด์กด: ์ค์ ์๋ฒ๊ฐ ์ฌ๋ผ์ ธ๋ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ์ ๋ฐ์ดํฐ๊ฐ ์๊ตฌ ๋ณด์กด๋ผ.
4. ์์ ๊ถ ๋ช ํํ: ํ์ต์ ๋ณธ์ธ์ด ์์ ์ ํ์ต ๋ฐ์ดํฐ๋ฅผ ์์ ํ ์์ ํ๊ณ ๊ด๋ฆฌํ ์ ์์ด.
5. ์ํธ์ด์ฉ์ฑ: ๋ค๋ฅธ ํ๋ซํผ์์๋ ๋์ผํ ํ์ต ์ด๋ ฅ์ ์ธ์ ๋ฐ์ ์ ์์ด.
ํนํ ์ฌ๋ฅ ๊ณต์ ๋ ๊ต์ก ํ๋ซํผ์์ ์ด๋ฐ ์์คํ ์ ๋์ ํ๋ฉด, ๊ฐ์ฌ์ ์๊ฐ์ ๋ชจ๋์๊ฒ ์์ฒญ๋ ์ด์ ์ด ์์ด. ๊ฐ์ฌ๋ ์์ ์ ๊ฐ์ ํ์ง์ ๊ฐ๊ด์ ์ผ๋ก ์ฆ๋ช ํ ์ ์๊ณ , ์๊ฐ์์ ์์ ์ ํ์ต ์ด๋ ฅ์ ํฌํธํด๋ฆฌ์ค๋ก ํ์ฉํ ์ ์๊ฑฐ๋ . ๐
๐๏ธ ํ๋ก์ ํธ ์ํคํ ์ฒ ์ค๊ณ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ์ ์ฒด ์์คํ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํด์ผ ํด. ์ฐ๋ฆฌ๊ฐ ๋ง๋ค ํ๋ซํผ์ ํฌ๊ฒ ์ธ ๊ฐ์ง ๋ ์ด์ด๋ก ๊ตฌ์ฑ๋ ๊ฑฐ์ผ.
์ฌ์ฉ์๊ฐ ์ง์ ์ํธ์์ฉํ๋ ๋ถ๋ถ์ด์ผ. ๋ผ๋ผ๋ฒจ์ Blade ํ ํ๋ฆฟ ์์ง์ ์ฌ์ฉํ ์๋ ์๊ณ , Vue.js๋ React ๊ฐ์ ๋ชจ๋ ํ๋ ์์ํฌ๋ฅผ ๊ฒฐํฉํ ์๋ ์์ด. ์ฌ๊ธฐ์๋ ํ์ต ์งํ ์ํฉ์ ๋ณด์ฌ์ฃผ๊ณ , ์๋ฃ์ฆ์ ๋ฐ๊ธ๋ฐ๊ณ , ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํด.
๋ผ๋ผ๋ฒจ์ด ๋ด๋นํ๋ ํต์ฌ ์์ญ์ด์ผ. ์ฌ์ฉ์ ์ธ์ฆ, ๊ฐ์ ๊ด๋ฆฌ, ํ์ต ์ง๋ ์ถ์ , ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์์ฑ ๋ฑ ๋ชจ๋ ๋น์ฆ๋์ค ๋ก์ง์ด ์ฌ๊ธฐ์ ์ฒ๋ฆฌ๋ผ. RESTful API๋ฅผ ๊ตฌ์ถํด์ ํ๋ก ํธ์๋์ ํต์ ํ๊ณ , ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ์๋ ์ฐ๊ฒฐ๋๋ ์ค๊ฐ ๋ค๋ฆฌ ์ญํ ์ ํด.
์ค์ ํ์ต ์ธ์ฆ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋๋ ๊ณณ์ด์ผ. ์ด๋๋ฆฌ์ ๊ฐ์ ํผ๋ธ๋ฆญ ๋ธ๋ก์ฒด์ธ์ ์ฌ์ฉํ ์๋ ์๊ณ , Hyperledger Fabric ๊ฐ์ ํ๋ผ์ด๋น ๋ธ๋ก์ฒด์ธ์ ๊ตฌ์ถํ ์๋ ์์ด. ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ํตํด ํ์ต ์ด๋ ฅ์ ๊ธฐ๋กํ๊ณ , ์๋ฃ์ฆ์ NFT๋ก ๋ฐํํ ์ ์์ง.
์ด ์ธ ๋ ์ด์ด๊ฐ ์ ๊ธฐ์ ์ผ๋ก ์ฐ๊ฒฐ๋์ด์ผ ์์ฑ๋ ๋์ ํ๋ซํผ์ด ๋ง๋ค์ด์ ธ. ๊ฐ ๋ ์ด์ด๋ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐํ๊ณ ํ ์คํธํ ์ ์์ด์ ์ ์ง๋ณด์๋ ํธํด. ๐
โ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ
์ด์ ์ค์ ๋ก ์ฝ๋ฉ์ ์์ํ๊ธฐ ์ ์ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ธํ ํด์ผ ํด. ํ์ํ ๋๊ตฌ๋ค์ด ๊ฝค ๋ง์๋ฐ, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ค์นํด๋ณด์!
๋ผ๋ผ๋ฒจ 10.x ๋ฒ์ ์ ์ฌ์ฉํ๋ ค๋ฉด PHP 8.1 ์ด์์ด ํ์ํด. ์ต์ ๋ฒ์ ์ผ์๋ก ์ฑ๋ฅ๋ ์ข๊ณ ๋ณด์๋ ๊ฐํ๋์ด ์์ผ๋๊น ๊ฐ๋ฅํ๋ฉด ์ต์ ๋ฒ์ ์ ์ค์นํ์.
2๏ธโฃ Composer
PHP ์์กด์ฑ ๊ด๋ฆฌ ๋๊ตฌ์ผ. ๋ผ๋ผ๋ฒจ ์ค์น์ ํจํค์ง ๊ด๋ฆฌ์ ํ์์ ์ด์ง.
3๏ธโฃ Node.js & NPM
ํ๋ก ํธ์๋ ์์ฐ์ ๋น๋ํ๊ณ ๊ด๋ฆฌํ๋ ๋ฐ ํ์ํด. ์ต์ Node.js 16.x ์ด์์ ๊ถ์ฅํด.
4๏ธโฃ MySQL ๋๋ PostgreSQL
์ฌ์ฉ์ ์ ๋ณด, ๊ฐ์ ๋ฐ์ดํฐ ๋ฑ ์คํ์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ผ.
5๏ธโฃ Ganache ๋๋ Hardhat
๋ก์ปฌ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ํ๊ฒฝ์ด์ผ. ์ค์ ์ด๋๋ฆฌ์ ๋คํธ์ํฌ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ํ ์คํธํ ์ ์์ด.
6๏ธโฃ MetaMask
๋ธ๋ก์ฒด์ธ ์ง๊ฐ์ด์ ๋ธ๋ผ์ฐ์ ํ์ฅ ํ๋ก๊ทธ๋จ์ด์ผ. ์ฌ์ฉ์๊ฐ ๋ธ๋ก์ฒด์ธ๊ณผ ์ํธ์์ฉํ๋ ๋ฐ ํ์ํด.
# ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์์ฑ
composer create-project laravel/laravel blockchain-learning-platform
# ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ๋ก ์ด๋
cd blockchain-learning-platform
# ํ์ํ ํจํค์ง ์ค์น
composer require web3p/web3.php
composer require simplito/elliptic-php
# ํ๋ก ํธ์๋ ์์กด์ฑ ์ค์น
npm install
npm install web3 ethers
# ๊ฐ๋ฐ ์๋ฒ ์คํ
php artisan serve
ํ๊ฒฝ ๊ตฌ์ถ์ด ์๋ฃ๋์๋ค๋ฉด, ์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ์ ํด์ผ ํด. .env ํ์ผ์ ์ด์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ ๋ณด๋ฅผ ์
๋ ฅํ์.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blockchain_learning
DB_USERNAME=root
DB_PASSWORD=your_password
# ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ ์ค์
BLOCKCHAIN_NETWORK=ganache
BLOCKCHAIN_RPC_URL=http://127.0.0.1:7545
CONTRACT_ADDRESS=0x...
PRIVATE_KEY=0x...
๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์ค๊ณ
๋ธ๋ก์ฒด์ธ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ฉด ์ข๊ฒ ์ง๋ง, ํ์ค์ ์ผ๋ก ๋น์ฉ์ด ๋๋ฌด ๋ง์ด ๋ค์ด. ๊ทธ๋์ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์ ์ฌ์ฉํ ๊ฑฐ์ผ. ์ค์ํ ์ธ์ฆ ๋ฐ์ดํฐ๋ ๋ธ๋ก์ฒด์ธ์, ๋๋จธ์ง ์์ธ ์ ๋ณด๋ ์ผ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ ๊ฑฐ์ง. ๐ฏ
์ฌ์ฉ์ ๊ธฐ๋ณธ ์ ๋ณด๋ฅผ ์ ์ฅํด. ๋ผ๋ผ๋ฒจ์ ๊ธฐ๋ณธ ์ธ์ฆ ์์คํ ์ ํ์ฉํ๋, ๋ธ๋ก์ฒด์ธ ์ง๊ฐ ์ฃผ์๋ฅผ ์ถ๊ฐ๋ก ์ ์ฅํด์ผ ํด.
php artisan make:migration create_users_table
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('wallet_address')->unique()->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->enum('role', ['student', 'instructor', 'admin'])->default('student');
$table->rememberToken();
$table->timestamps();
});
}
๊ฐ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ํ ์ด๋ธ์ด์ผ. ๊ฐ์ ์ ๋ชฉ, ์ค๋ช , ๊ฐ์ฌ ์ ๋ณด ๋ฑ์ด ๋ค์ด๊ฐ.
public function up()
{
Schema::create('courses', function (Blueprint $table) {
$table->id();
$table->foreignId('instructor_id')->constrained('users')->onDelete('cascade');
$table->string('title');
$table->text('description');
$table->string('thumbnail')->nullable();
$table->integer('duration_hours');
$table->decimal('price', 10, 2)->default(0);
$table->enum('difficulty', ['beginner', 'intermediate', 'advanced']);
$table->boolean('is_published')->default(false);
$table->string('contract_address')->nullable(); // ์ค๋งํธ ์ปจํธ๋ํธ ์ฃผ์
$table->timestamps();
});
}
์๊ฐ ์ ์ฒญ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํด. ํ์๊ณผ ๊ฐ์๋ฅผ ์ฐ๊ฒฐํ๋ ์ค๊ฐ ํ ์ด๋ธ์ด์ผ.
public function up()
{
Schema::create('enrollments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('course_id')->constrained()->onDelete('cascade');
$table->integer('progress_percentage')->default(0);
$table->timestamp('enrolled_at');
$table->timestamp('completed_at')->nullable();
$table->string('blockchain_tx_hash')->nullable(); // ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
ํด์
$table->boolean('certificate_issued')->default(false);
$table->string('certificate_token_id')->nullable(); // NFT ํ ํฐ ID
$table->timestamps();
$table->unique(['user_id', 'course_id']);
});
}
ํ์ต ํ๋์ ์์ธํ๊ฒ ๊ธฐ๋กํ๋ ํ ์ด๋ธ์ด์ผ. ์ด๋ค ๊ฐ์๋ฅผ ์ธ์ ์ผ๋ง๋ ๋ค์๋์ง ์ถ์ ํด.
public function up()
{
Schema::create('learning_records', function (Blueprint $table) {
$table->id();
$table->foreignId('enrollment_id')->constrained()->onDelete('cascade');
$table->foreignId('lesson_id')->constrained()->onDelete('cascade');
$table->integer('watch_duration_seconds');
$table->boolean('completed')->default(false);
$table->timestamp('completed_at')->nullable();
$table->string('blockchain_tx_hash')->nullable();
$table->timestamps();
});
}
๋ฐ๊ธ๋ ์๋ฃ์ฆ ์ ๋ณด๋ฅผ ์ ์ฅํด. ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋ NFT ์ ๋ณด์ ์ฐ๊ฒฐ๋ผ.
public function up()
{
Schema::create('certificates', function (Blueprint $table) {
$table->id();
$table->foreignId('enrollment_id')->constrained()->onDelete('cascade');
$table->string('certificate_number')->unique();
$table->string('token_id')->unique(); // NFT ํ ํฐ ID
$table->string('token_uri'); // ๋ฉํ๋ฐ์ดํฐ URI
$table->string('blockchain_tx_hash');
$table->string('ipfs_hash')->nullable(); // IPFS์ ์ ์ฅ๋ ์ฆ๋ช
์ ํด์
$table->timestamp('issued_at');
$table->timestamps();
});
}
์ด๋ ๊ฒ ํ ์ด๋ธ์ ์ค๊ณํ๋ฉด ์จ์ฒด์ธ(๋ธ๋ก์ฒด์ธ)๊ณผ ์คํ์ฒด์ธ(์ผ๋ฐ DB) ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด. ๋ธ๋ก์ฒด์ธ์๋ ์ฆ๋ช ์ด ํ์ํ ํต์ฌ ๋ฐ์ดํฐ๋ง ์ ์ฅํ๊ณ , ๋๋จธ์ง๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋น ๋ฅด๊ฒ ์กฐํํ ์ ์์ง. ๐พ
๐ ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ
์ด์ ๋ธ๋ก์ฒด์ธ์ ํต์ฌ์ธ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณผ ์ฐจ๋ก์ผ! Solidity ์ธ์ด๋ฅผ ์ฌ์ฉํด์ ์์ฑํ ๊ฑฐ๊ณ , ERC-721 ํ์ค์ ๋ฐ๋ฅด๋ NFT ์ปจํธ๋ํธ๋ฅผ ๊ตฌํํ ๊ฑฐ์ผ. ์๋ฃ์ฆ์ NFT๋ก ๋ฐํํ๋ฉด ์ ๋ง ๋ฉ์ง์ง ์์? ๐จ
๋จผ์ ํ๋ก์ ํธ ๋ฃจํธ์ contracts ๋๋ ํ ๋ฆฌ๋ฅผ ๋ง๋ค๊ณ , Hardhat์ ์ด๊ธฐํํ์.
# Hardhat ์ค์น ๋ฐ ์ด๊ธฐํ
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers
npx hardhat
# OpenZeppelin ์ปจํธ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
npm install @openzeppelin/contracts
์ด์ contracts/LearningCertificate.sol ํ์ผ์ ๋ง๋ค์ด์ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ์์ฑํด๋ณด์.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract LearningCertificate is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
// ์๋ฃ์ฆ ์ ๋ณด ๊ตฌ์กฐ์ฒด
struct Certificate {
uint256 tokenId;
address student;
uint256 courseId;
string courseName;
uint256 completionDate;
uint256 score;
bool isValid;
}
// ํ ํฐ ID๋ก ์๋ฃ์ฆ ์ ๋ณด ๋งคํ
mapping(uint256 => Certificate) public certificates;
// ํ์ ์ฃผ์๋ก ์๋ฃ์ฆ ๋ชฉ๋ก ๋งคํ
mapping(address => uint256[]) public studentCertificates;
// ๊ฐ์ ID๋ก ์๋ฃ์ฆ ๋ชฉ๋ก ๋งคํ
mapping(uint256 => uint256[]) public courseCertificates;
// ์ด๋ฒคํธ ์ ์
event CertificateIssued(
uint256 indexed tokenId,
address indexed student,
uint256 indexed courseId,
string courseName,
uint256 completionDate
);
event CertificateRevoked(uint256 indexed tokenId);
constructor() ERC721("Learning Certificate", "LCERT") {}
// ์๋ฃ์ฆ ๋ฐ๊ธ ํจ์
function issueCertificate(
address student,
uint256 courseId,
string memory courseName,
string memory tokenURI,
uint256 score
) public onlyOwner returns (uint256) {
require(student != address(0), "Invalid student address");
require(score <= 100, "Score must be between 0 and 100");
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(student, tokenId);
_setTokenURI(tokenId, tokenURI);
certificates[tokenId] = Certificate({
tokenId: tokenId,
student: student,
courseId: courseId,
courseName: courseName,
completionDate: block.timestamp,
score: score,
isValid: true
});
studentCertificates[student].push(tokenId);
courseCertificates[courseId].push(tokenId);
emit CertificateIssued(tokenId, student, courseId, courseName, block.timestamp);
return tokenId;
}
// ์๋ฃ์ฆ ๊ฒ์ฆ ํจ์
function verifyCertificate(uint256 tokenId) public view returns (
bool isValid,
address student,
uint256 courseId,
string memory courseName,
uint256 completionDate,
uint256 score
) {
require(_exists(tokenId), "Certificate does not exist");
Certificate memory cert = certificates[tokenId];
return (
cert.isValid,
cert.student,
cert.courseId,
cert.courseName,
cert.completionDate,
cert.score
);
}
// ํ์์ ๋ชจ๋ ์๋ฃ์ฆ ์กฐํ
function getStudentCertificates(address student) public view returns (uint256[] memory) {
return studentCertificates[student];
}
// ๊ฐ์์ ๋ชจ๋ ์๋ฃ์ฆ ์กฐํ
function getCourseCertificates(uint256 courseId) public view returns (uint256[] memory) {
return courseCertificates[courseId];
}
// ์๋ฃ์ฆ ๋ฌดํจํ (๊ด๋ฆฌ์๋ง ๊ฐ๋ฅ)
function revokeCertificate(uint256 tokenId) public onlyOwner {
require(_exists(tokenId), "Certificate does not exist");
certificates[tokenId].isValid = false;
emit CertificateRevoked(tokenId);
}
// ํ์ ์ค๋ฒ๋ผ์ด๋ ํจ์๋ค
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
์ด ์ปจํธ๋ํธ๋ ERC-721 NFT ํ์ค์ ๋ฐ๋ผ์ ์๋ฃ์ฆ์ ๋ฐํํด. ๊ฐ ์๋ฃ์ฆ์ ๊ณ ์ ํ ํ ํฐ ID๋ฅผ ๊ฐ์ง๊ณ ์๊ณ , ํ์ ์ฃผ์, ๊ฐ์ ์ ๋ณด, ์๋ฃ ๋ ์ง, ์ ์ ๋ฑ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ํฌํจํด.
issueCertificate ํจ์๋ก ์๋ฃ์ฆ์ ๋ฐ๊ธํ๊ณ , verifyCertificate ํจ์๋ก ์ง์๋ฅผ ๊ฒ์ฆํ ์ ์์ด. ๊ด๋ฆฌ์๋ ํ์์ ์๋ฃ์ฆ์ ๋ฌดํจํํ ์๋ ์์ง.
์ด์ ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌํ๊ธฐ ์ํ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํด๋ณด์. scripts/deploy.js ํ์ผ์ ๋ง๋ค์ด.
const hre = require("hardhat");
async function main() {
console.log("Deploying LearningCertificate contract...");
const LearningCertificate = await hre.ethers.getContractFactory("LearningCertificate");
const certificate = await LearningCertificate.deploy();
await certificate.deployed();
console.log("LearningCertificate deployed to:", certificate.address);
// ๋ฐฐํฌ ์ ๋ณด๋ฅผ ํ์ผ๋ก ์ ์ฅ
const fs = require('fs');
const deploymentInfo = {
contractAddress: certificate.address,
deployedAt: new Date().toISOString(),
network: hre.network.name
};
fs.writeFileSync(
'./deployment-info.json',
JSON.stringify(deploymentInfo, null, 2)
);
console.log("Deployment info saved to deployment-info.json");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
๋ก์ปฌ Hardhat ๋คํธ์ํฌ์ ๋ฐฐํฌํด๋ณด์.
# ๋ก์ปฌ ๋คํธ์ํฌ ์คํ (๋ณ๋ ํฐ๋ฏธ๋)
npx hardhat node
# ์ปจํธ๋ํธ ๋ฐฐํฌ
npx hardhat run scripts/deploy.js --network localhost
๐ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ ์ฐ๋
์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌํ์ผ๋, ์ด์ ๋ผ๋ผ๋ฒจ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ ์ ์๋๋ก ์ฐ๋ํด์ผ ํด. Web3.php ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ์ด๋๋ฆฌ์ ๋คํธ์ํฌ์ ์ํธ์์ฉํ ๊ฑฐ์ผ. ๐
๋จผ์ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ๋ก์ง์ ์บก์ํํ ์๋น์ค ํด๋์ค๋ฅผ ๋ง๋ค์. app/Services/BlockchainService.php ํ์ผ์ ์์ฑํด.
php artisan make:service BlockchainService
<?php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
use Exception;
use Illuminate\Support\Facades\Log;
class BlockchainService
{
protected $web3;
protected $contract;
protected $contractAddress;
protected $contractAbi;
protected $privateKey;
protected $fromAddress;
public function __construct()
{
$this->web3 = new Web3(config('blockchain.rpc_url'));
$this->contractAddress = config('blockchain.contract_address');
$this->privateKey = config('blockchain.private_key');
$this->fromAddress = config('blockchain.from_address');
// ABI ํ์ผ ๋ก๋
$abiPath = storage_path('blockchain/LearningCertificate.json');
$abiJson = json_decode(file_get_contents($abiPath), true);
$this->contractAbi = $abiJson['abi'];
$this->contract = new Contract($this->web3->provider, $this->contractAbi);
}
/**
* ์๋ฃ์ฆ NFT ๋ฐ๊ธ
*/
public function issueCertificate(
string $studentAddress,
int $courseId,
string $courseName,
string $tokenURI,
int $score
): array {
try {
$contract = $this->contract->at($this->contractAddress);
// ํธ๋์ญ์
๋ฐ์ดํฐ ์ค๋น
$data = $contract->getData('issueCertificate',
$studentAddress,
$courseId,
$courseName,
$tokenURI,
$score
);
// ๊ฐ์ค ์ถ์
$gasEstimate = $this->estimateGas($data);
// ํธ๋์ญ์
์ ์ก
$txHash = $this->sendTransaction([
'from' => $this->fromAddress,
'to' => $this->contractAddress,
'data' => $data,
'gas' => Utils::toHex($gasEstimate, true),
]);
// ํธ๋์ญ์
์์์ฆ ๋๊ธฐ
$receipt = $this->waitForReceipt($txHash);
// ์ด๋ฒคํธ ๋ก๊ทธ์์ ํ ํฐ ID ์ถ์ถ
$tokenId = $this->extractTokenIdFromReceipt($receipt);
Log::info('Certificate issued', [
'tx_hash' => $txHash,
'token_id' => $tokenId,
'student' => $studentAddress,
'course_id' => $courseId
]);
return [
'success' => true,
'tx_hash' => $txHash,
'token_id' => $tokenId,
'gas_used' => $receipt['gasUsed']
];
} catch (Exception $e) {
Log::error('Failed to issue certificate', [
'error' => $e->getMessage(),
'student' => $studentAddress,
'course_id' => $courseId
]);
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* ์๋ฃ์ฆ ๊ฒ์ฆ
*/
public function verifyCertificate(int $tokenId): array
{
try {
$contract = $this->contract->at($this->contractAddress);
$result = null;
$contract->call('verifyCertificate', $tokenId, function ($err, $res) use (&$result) {
if ($err !== null) {
throw new Exception($err->getMessage());
}
$result = $res;
});
return [
'success' => true,
'is_valid' => $result['isValid'],
'student' => $result['student'],
'course_id' => $result['courseId'],
'course_name' => $result['courseName'],
'completion_date' => $result['completionDate'],
'score' => $result['score']
];
} catch (Exception $e) {
Log::error('Failed to verify certificate', [
'error' => $e->getMessage(),
'token_id' => $tokenId
]);
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* ํ์์ ๋ชจ๋ ์๋ฃ์ฆ ์กฐํ
*/
public function getStudentCertificates(string $studentAddress): array
{
try {
$contract = $this->contract->at($this->contractAddress);
$tokenIds = null;
$contract->call('getStudentCertificates', $studentAddress, function ($err, $res) use (&$tokenIds) {
if ($err !== null) {
throw new Exception($err->getMessage());
}
$tokenIds = $res;
});
return [
'success' => true,
'token_ids' => $tokenIds
];
} catch (Exception $e) {
Log::error('Failed to get student certificates', [
'error' => $e->getMessage(),
'student' => $studentAddress
]);
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* ๊ฐ์ค ์ถ์
*/
protected function estimateGas(string $data): int
{
$gasEstimate = 0;
$this->web3->eth->estimateGas([
'from' => $this->fromAddress,
'to' => $this->contractAddress,
'data' => $data
], function ($err, $gas) use (&$gasEstimate) {
if ($err !== null) {
throw new Exception('Gas estimation failed: ' . $err->getMessage());
}
$gasEstimate = hexdec($gas);
});
// ์์ ๋ง์ง ์ถ๊ฐ (20%)
return (int)($gasEstimate * 1.2);
}
/**
* ํธ๋์ญ์
์ ์ก
*/
protected function sendTransaction(array $txParams): string
{
// ์ค์ ๊ตฌํ์์๋ ๊ฐ์ธํค๋ก ์๋ช
ํ๊ณ ์ ์ก
// ์ฌ๊ธฐ์๋ ๊ฐ์ํ๋ ๋ฒ์
$txHash = null;
$this->web3->eth->sendTransaction($txParams, function ($err, $hash) use (&$txHash) {
if ($err !== null) {
throw new Exception('Transaction failed: ' . $err->getMessage());
}
$txHash = $hash;
});
return $txHash;
}
/**
* ํธ๋์ญ์
์์์ฆ ๋๊ธฐ
*/
protected function waitForReceipt(string $txHash, int $maxAttempts = 30): array
{
$attempts = 0;
$receipt = null;
while ($attempts < $maxAttempts) {
$this->web3->eth->getTransactionReceipt($txHash, function ($err, $rec) use (&$receipt) {
if ($err === null && $rec !== null) {
$receipt = $rec;
}
});
if ($receipt !== null) {
return $receipt;
}
sleep(2);
$attempts++;
}
throw new Exception('Transaction receipt timeout');
}
/**
* ์์์ฆ์์ ํ ํฐ ID ์ถ์ถ
*/
protected function extractTokenIdFromReceipt(array $receipt): int
{
// ์ด๋ฒคํธ ๋ก๊ทธ ํ์ฑํ์ฌ ํ ํฐ ID ์ถ์ถ
// ์ค์ ๊ตฌํ์์๋ ABI ๋์ฝ๋ฉ ํ์
$logs = $receipt['logs'];
foreach ($logs as $log) {
// CertificateIssued ์ด๋ฒคํธ์ ํ ํฐ ID ์ถ์ถ
if (count($log['topics']) > 1) {
return hexdec($log['topics'][1]);
}
}
throw new Exception('Token ID not found in receipt');
}
}
์ค์ ํ์ผ๋ ๋ง๋ค์ด์ผ ํด. config/blockchain.php ํ์ผ์ ์์ฑํ์.
<?php
return [
'rpc_url' => env('BLOCKCHAIN_RPC_URL', 'http://127.0.0.1:7545'),
'contract_address' => env('CONTRACT_ADDRESS'),
'private_key' => env('PRIVATE_KEY'),
'from_address' => env('FROM_ADDRESS'),
'network_id' => env('BLOCKCHAIN_NETWORK_ID', 1337),
'gas_limit' => env('BLOCKCHAIN_GAS_LIMIT', 3000000),
'gas_price' => env('BLOCKCHAIN_GAS_PRICE', '20000000000'), // 20 Gwei
];
.env ํ์ผ์ ์ ์ฅํ๊ณ , ์ด ํ์ผ์ .gitignore์ ์ถ๊ฐํด์ ๋ฒ์ ๊ด๋ฆฌ์์ ์ ์ธํด์ผ ํด. ํ๋ก๋์
ํ๊ฒฝ์์๋ AWS Secrets Manager๋ HashiCorp Vault ๊ฐ์ ๋ณด์ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์.
๐ ์๋ฃ์ฆ ๋ฐ๊ธ ์์คํ ๊ตฌํ
์ด์ ์ค์ ๋ก ์๋ฃ์ฆ์ ๋ฐ๊ธํ๋ ๋ก์ง์ ๊ตฌํํด๋ณด์. ํ์์ด ๊ฐ์๋ฅผ ์๋ฃํ๋ฉด ์๋์ผ๋ก ์๋ฃ์ฆ NFT๊ฐ ๋ฐ๊ธ๋๋๋ก ๋ง๋ค ๊ฑฐ์ผ. ์ด ๊ณผ์ ์ ์ฌ๋ฌ ๋จ๊ณ๋ก ๋๋์ด์ ธ ์์ด์ ํ(Queue)๋ฅผ ํ์ฉํ๋ฉด ์ข์. ๐
๋จผ์ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋ค์. app/Http/Controllers/CertificateController.php ํ์ผ์ ์์ฑํด.
php artisan make:controller CertificateController
<?php
namespace App\Http\Controllers;
use App\Models\Enrollment;
use App\Models\Certificate;
use App\Services\BlockchainService;
use App\Jobs\IssueCertificateJob;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class CertificateController extends Controller
{
protected $blockchainService;
public function __construct(BlockchainService $blockchainService)
{
$this->blockchainService = $blockchainService;
}
/**
* ์๋ฃ์ฆ ๋ฐ๊ธ ์์ฒญ
*/
public function issue(Request $request, $enrollmentId)
{
$enrollment = Enrollment::with(['user', 'course'])->findOrFail($enrollmentId);
// ๊ถํ ํ์ธ
if ($request->user()->id !== $enrollment->user_id && !$request->user()->isAdmin()) {
return response()->json(['error' => 'Unauthorized'], 403);
}
// ์ด๋ฏธ ๋ฐ๊ธ๋์๋์ง ํ์ธ
if ($enrollment->certificate_issued) {
return response()->json([
'error' => 'Certificate already issued',
'certificate' => $enrollment->certificate
], 400);
}
// ์๋ฃ ์ฌ๋ถ ํ์ธ
if ($enrollment->progress_percentage < 100) {
return response()->json([
'error' => 'Course not completed',
'progress' => $enrollment->progress_percentage
], 400);
}
// ์ง๊ฐ ์ฃผ์ ํ์ธ
if (!$enrollment->user->wallet_address) {
return response()->json([
'error' => 'Wallet address not set'
], 400);
}
// ๋น๋๊ธฐ๋ก ์๋ฃ์ฆ ๋ฐ๊ธ ์์
ํ์ ์ถ๊ฐ
IssueCertificateJob::dispatch($enrollment);
return response()->json([
'message' => 'Certificate issuance started',
'enrollment_id' => $enrollment->id
]);
}
/**
* ์๋ฃ์ฆ ์กฐํ
*/
public function show($certificateId)
{
$certificate = Certificate::with(['enrollment.user', 'enrollment.course'])
->findOrFail($certificateId);
// ๋ธ๋ก์ฒด์ธ์์ ๊ฒ์ฆ
$verification = $this->blockchainService->verifyCertificate(
(int)$certificate->token_id
);
return response()->json([
'certificate' => $certificate,
'blockchain_verification' => $verification
]);
}
/**
* ์ฌ์ฉ์์ ๋ชจ๋ ์๋ฃ์ฆ ์กฐํ
*/
public function userCertificates(Request $request)
{
$user = $request->user();
$certificates = Certificate::whereHas('enrollment', function ($query) use ($user) {
$query->where('user_id', $user->id);
})->with(['enrollment.course'])->get();
// ๋ธ๋ก์ฒด์ธ์์๋ ์กฐํ
if ($user->wallet_address) {
$blockchainCerts = $this->blockchainService->getStudentCertificates(
$user->wallet_address
);
return response()->json([
'certificates' => $certificates,
'blockchain_token_ids' => $blockchainCerts['token_ids'] ?? []
]);
}
return response()->json([
'certificates' => $certificates
]);
}
/**
* ์๋ฃ์ฆ ๊ฒ์ฆ (๊ณต๊ฐ API)
*/
public function verify(Request $request)
{
$request->validate([
'certificate_number' => 'required_without:token_id|string',
'token_id' => 'required_without:certificate_number|integer'
]);
if ($request->has('certificate_number')) {
$certificate = Certificate::where('certificate_number', $request->certificate_number)
->with(['enrollment.user', 'enrollment.course'])
->first();
if (!$certificate) {
return response()->json([
'valid' => false,
'message' => 'Certificate not found'
], 404);
}
$tokenId = (int)$certificate->token_id;
} else {
$tokenId = $request->token_id;
}
// ๋ธ๋ก์ฒด์ธ์์ ๊ฒ์ฆ
$verification = $this->blockchainService->verifyCertificate($tokenId);
if (!$verification['success']) {
return response()->json([
'valid' => false,
'message' => 'Blockchain verification failed',
'error' => $verification['error']
], 400);
}
return response()->json([
'valid' => $verification['is_valid'],
'certificate' => $certificate ?? null,
'blockchain_data' => $verification
]);
}
/**
* ์๋ฃ์ฆ ๋ฉํ๋ฐ์ดํฐ ์์ฑ
*/
protected function generateMetadata(Enrollment $enrollment): string
{
$metadata = [
'name' => "Certificate of Completion - {$enrollment->course->title}",
'description' => "This certificate is awarded to {$enrollment->user->name} for successfully completing the course: {$enrollment->course->title}",
'image' => $this->generateCertificateImage($enrollment),
'attributes' => [
[
'trait_type' => 'Student Name',
'value' => $enrollment->user->name
],
[
'trait_type' => 'Course Title',
'value' => $enrollment->course->title
],
[
'trait_type' => 'Completion Date',
'value' => $enrollment->completed_at->format('Y-m-d')
],
[
'trait_type' => 'Progress',
'value' => $enrollment->progress_percentage
],
[
'trait_type' => 'Instructor',
'value' => $enrollment->course->instructor->name
]
]
];
// IPFS์ ์
๋ก๋ํ๊ฑฐ๋ ๋ก์ปฌ ์ ์ฅ
$filename = "metadata/{$enrollment->id}.json";
Storage::put($filename, json_encode($metadata, JSON_PRETTY_PRINT));
// ๊ณต๊ฐ URL ๋ฐํ
return Storage::url($filename);
}
/**
* ์๋ฃ์ฆ ์ด๋ฏธ์ง ์์ฑ
*/
protected function generateCertificateImage(Enrollment $enrollment): string
{
// ์ค์ ๋ก๋ ์ด๋ฏธ์ง ์์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉ
// ์ฌ๊ธฐ์๋ ๊ฐ๋จํ URL๋ง ๋ฐํ
return url("/certificates/image/{$enrollment->id}");
}
}
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ธฐ ๋๋ฌธ์ ๋น๋๊ธฐ ์์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ ์ข์. Job์ ๋ง๋ค์ด๋ณด์.
php artisan make:job IssueCertificateJob
<?php
namespace App\Jobs;
use App\Models\Enrollment;
use App\Models\Certificate;
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;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class IssueCertificateJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $enrollment;
public $tries = 3;
public $timeout = 300;
public function __construct(Enrollment $enrollment)
{
$this->enrollment = $enrollment;
}
public function handle(BlockchainService $blockchainService)
{
try {
Log::info('Starting certificate issuance', [
'enrollment_id' => $this->enrollment->id
]);
// ๋ฉํ๋ฐ์ดํฐ ์์ฑ
$tokenURI = $this->generateMetadata();
// ๋ธ๋ก์ฒด์ธ์ ์๋ฃ์ฆ ๋ฐ๊ธ
$result = $blockchainService->issueCertificate(
$this->enrollment->user->wallet_address,
$this->enrollment->course_id,
$this->enrollment->course->title,
$tokenURI,
$this->enrollment->progress_percentage
);
if (!$result['success']) {
throw new \Exception('Blockchain issuance failed: ' . $result['error']);
}
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ฃ์ฆ ์ ๋ณด ์ ์ฅ
$certificate = Certificate::create([
'enrollment_id' => $this->enrollment->id,
'certificate_number' => $this->generateCertificateNumber(),
'token_id' => $result['token_id'],
'token_uri' => $tokenURI,
'blockchain_tx_hash' => $result['tx_hash'],
'issued_at' => now()
]);
// Enrollment ์
๋ฐ์ดํธ
$this->enrollment->update([
'certificate_issued' => true,
'certificate_token_id' => $result['token_id'],
'blockchain_tx_hash' => $result['tx_hash']
]);
Log::info('Certificate issued successfully', [
'certificate_id' => $certificate->id,
'token_id' => $result['token_id'],
'tx_hash' => $result['tx_hash']
]);
// ์ฌ์ฉ์์๊ฒ ์๋ฆผ ์ ์ก (์ ํ์ฌํญ)
// $this->enrollment->user->notify(new CertificateIssuedNotification($certificate));
} catch (\Exception $e) {
Log::error('Certificate issuance failed', [
'enrollment_id' => $this->enrollment->id,
'error' => $e->getMessage()
]);
throw $e;
}
}
protected function generateMetadata(): string
{
$metadata = [
'name' => "Certificate - {$this->enrollment->course->title}",
'description' => "Awarded to {$this->enrollment->user->name}",
'image' => url("/api/certificates/image/{$this->enrollment->id}"),
'attributes' => [
['trait_type' => 'Student', 'value' => $this->enrollment->user->name],
['trait_type' => 'Course', 'value' => $this->enrollment->course->title],
['trait_type' => 'Date', 'value' => now()->format('Y-m-d')],
]
];
// ์ค์ ๋ก๋ IPFS์ ์
๋ก๋
$filename = "metadata/{$this->enrollment->id}.json";
\Storage::put($filename, json_encode($metadata));
return \Storage::url($filename);
}
protected function generateCertificateNumber(): string
{
return 'CERT-' . strtoupper(Str::random(12));
}
}
sync๋ฅผ ์ฌ์ฉํด๋ ๋์ง๋ง, ํ๋ก๋์
์์๋ Redis๋ Database ํ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์. .env ํ์ผ์์ QUEUE_CONNECTION=redis๋ก ์ค์ ํ๊ณ , php artisan queue:work ๋ช
๋ น์ด๋ก ์์ปค๋ฅผ ์คํํด์ผ ํด.
๐จ ํ๋ก ํธ์๋ ๊ตฌํ
๋ฐฑ์๋๊ฐ ์ค๋น๋์์ผ๋ ์ด์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ์ฌ์ฉํ ํ๋ก ํธ์๋๋ฅผ ๋ง๋ค์ด๋ณด์. 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>
<p class="info-text">
Connect your wallet to receive certificates as NFTs
</p>
</div>
<div v-else class="connected-section">
<div class="wallet-info">
<span class="status-badge">โ
Connected</span>
<p class="address">{{ shortenAddress(account) }}</p>
<p class="network">Network: {{ networkName }}</p>
</div>
<button @click="disconnectWallet" class="disconnect-btn">
Disconnect
</button>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue';
import Web3 from 'web3';
export default {
name: 'WalletConnect',
setup() {
const account = ref(null);
const chainId = ref(null);
const error = ref(null);
const web3 = ref(null);
const isConnected = computed(() => account.value !== null);
const networkName = computed(() => {
const networks = {
'0x1': 'Ethereum Mainnet',
'0x5': 'Goerli Testnet',
'0xaa36a7': 'Sepolia Testnet',
'0x539': 'Local Network'
};
return networks[chainId.value] || 'Unknown Network';
});
const connectWallet = async () => {
try {
error.value = null;
if (typeof window.ethereum === 'undefined') {
error.value = 'MetaMask is not installed. Please install it to continue.';
return;
}
web3.value = new Web3(window.ethereum);
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
account.value = accounts[0];
chainId.value = await window.ethereum.request({
method: 'eth_chainId'
});
// ๋ฐฑ์๋์ ์ง๊ฐ ์ฃผ์ ์ ์ฅ
await saveWalletAddress(account.value);
console.log('Wallet connected:', account.value);
} catch (err) {
console.error('Connection error:', err);
error.value = err.message;
}
};
const disconnectWallet = () => {
account.value = null;
chainId.value = null;
web3.value = null;
};
const saveWalletAddress = async (address) => {
try {
await axios.post('/api/user/wallet', {
wallet_address: address
});
} catch (err) {
console.error('Failed to save wallet address:', err);
}
};
const shortenAddress = (address) => {
if (!address) return '';
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
};
// ๊ณ์ ๋ณ๊ฒฝ ๊ฐ์ง
const handleAccountsChanged = (accounts) => {
if (accounts.length === 0) {
disconnectWallet();
} else {
account.value = accounts[0];
saveWalletAddress(accounts[0]);
}
};
// ๋คํธ์ํฌ ๋ณ๊ฒฝ ๊ฐ์ง
const handleChainChanged = (newChainId) => {
chainId.value = newChainId;
window.location.reload();
};
onMounted(() => {
if (typeof window.ethereum !== 'undefined') {
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainChanged);
// ์ด๋ฏธ ์ฐ๊ฒฐ๋์ด ์๋์ง ํ์ธ
window.ethereum.request({ method: 'eth_accounts' })
.then(accounts => {
if (accounts.length > 0) {
account.value = accounts[0];
window.ethereum.request({ method: 'eth_chainId' })
.then(id => chainId.value = id);
}
});
}
});
return {
account,
chainId,
error,
isConnected,
networkName,
connectWallet,
disconnectWallet,
shortenAddress
};
}
};
</script>
<style scoped>
.wallet-connect {
padding: 20px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.connect-btn {
background: white;
color: #667eea;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
.connect-btn:hover {
transform: scale(1.05);
}
.connected-section {
display: flex;
justify-content: space-between;
align-items: center;
}
.wallet-info {
flex: 1;
}
.status-badge {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
font-size: 14px;
}
.address {
font-family: monospace;
font-size: 18px;
margin: 8px 0;
}
.network {
font-size: 14px;
opacity: 0.9;
}
.disconnect-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid white;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
}
.error-message {
margin-top: 12px;
padding: 12px;
background: rgba(255, 0, 0, 0.2);
border-radius: 6px;
border-left: 4px solid #ff4444;
}
</style>
์ด์ ์ฌ์ฉ์์ ์๋ฃ์ฆ์ ๋ณด์ฌ์ฃผ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์. resources/js/components/CertificateList.vue ํ์ผ์ ์์ฑํด.
<template>
<div class="certificate-list">
<h2>๐ My Certificates</h2>
<div v-if="loading" class="loading">
Loading certificates...
</div>
<div v-else-if="certificates.length === 0" class="empty-state">
<p>You don't have any certificates yet.</p>
<p>Complete a course to earn your first certificate!</p>
</div>
<div v-else class="certificates-grid">
<div
v-for="cert in certificates"
:key="cert.id"
class="certificate-card"
@click="viewCertificate(cert)"
>
<div class="card-header">
<span class="nft-badge">NFT #{{ cert.token_id }}</span>
<span class="verified-badge" v-if="cert.is_verified">โ Verified</span>
</div>
<div class="card-body">
<h3>{{ cert.course_name }}</h3>
<p class="date">Issued: {{ formatDate(cert.issued_at) }}</p>
<p class="cert-number">{{ cert.certificate_number }}</p>
</div>
<div class="card-footer">
<button @click.stop="downloadCertificate(cert)" class="download-btn">
๐ฅ Download
</button>
<button @click.stop="viewOnBlockchain(cert)" class="blockchain-btn">
๐ View on Blockchain
</button>
</div>
</div>
</div>
<CertificateModal
v-if="selectedCertificate"
:certificate="selectedCertificate"
@close="selectedCertificate = null"
/>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import CertificateModal from './CertificateModal.vue';
export default {
name: 'CertificateList',
components: { CertificateModal },
setup() {
const certificates = ref([]);
const loading = ref(true);
const selectedCertificate = ref(null);
const loadCertificates = async () => {
try {
loading.value = true;
const response = await axios.get('/api/certificates/my');
certificates.value = response.data.certificates;
// ๊ฐ ์๋ฃ์ฆ์ ๋ธ๋ก์ฒด์ธ ๊ฒ์ฆ ์ํ ํ์ธ
for (let cert of certificates.value) {
await verifyCertificate(cert);
}
} catch (error) {
console.error('Failed to load certificates:', error);
} finally {
loading.value = false;
}
};
const verifyCertificate = async (cert) => {
try {
const response = await axios.post('/api/certificates/verify', {
token_id: cert.token_id
});
cert.is_verified = response.data.valid;
} catch (error) {
cert.is_verified = false;
}
};
const viewCertificate = (cert) => {
selectedCertificate.value = cert;
};
const downloadCertificate = async (cert) => {
try {
const response = await axios.get(`/api/certificates/${cert.id}/download`, {
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `certificate-${cert.certificate_number}.pdf`);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.error('Download failed:', error);
}
};
const viewOnBlockchain = (cert) => {
const explorerUrl = `https://etherscan.io/tx/${cert.blockchain_tx_hash}`;
window.open(explorerUrl, '_blank');
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
onMounted(() => {
loadCertificates();
});
return {
certificates,
loading,
selectedCertificate,
viewCertificate,
downloadCertificate,
viewOnBlockchain,
formatDate
};
}
};
</script>
<style scoped>
.certificate-list {
padding: 20px;
}
.certificates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.certificate-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.certificate-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.nft-badge {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.verified-badge {
background: #4caf50;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
}
.card-body h3 {
margin: 0 0 8px 0;
color: #2d3748;
}
.date {
color: #718096;
font-size: 14px;
}
.cert-number {
font-family: monospace;
color: #4a5568;
font-size: 12px;
margin-top: 8px;
}
.card-footer {
display: flex;
gap: 8px;
margin-top: 16px;
}
.download-btn, .blockchain-btn {
flex: 1;
padding: 8px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: opacity 0.2s;
}
.download-btn {
background: #667eea;
color: white;
}
.blockchain-btn {
background: #f7fafc;
color: #4a5568;
border: 1px solid #e2e8f0;
}
.download-btn:hover, .blockchain-btn:hover {
opacity: 0.8;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #718096;
}
.loading {
text-align: center;
padding: 40px;
color: #718096;
}
</style>
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๋ธ๋ก์ฒด์ธ ํ๋ซํผ์ ๊ตฌ์ถํ ๋๋ ๋ณด์์ด ์ ๋ง ์ค์ํด. ํ๋ฒ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋ ๋ฐ์ดํฐ๋ ๋๋๋ฆด ์ ์๊ณ , ๊ฐ์ธํค๊ฐ ์ ์ถ๋๋ฉด ์์ฐ์ ๋ชจ๋ ์์ ์ ์๊ฑฐ๋ . ์ฃผ์ ๋ณด์ ์ฌํญ๋ค์ ์ดํด๋ณด์. ๐
1. ๊ฐ์ธํค ๊ด๋ฆฌ
์ ๋๋ก ๊ฐ์ธํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง. ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌํ๊ณ , ํ๋ก๋์ ์์๋ AWS KMS๋ HashiCorp Vault ๊ฐ์ ๋ณด์ ์ ์ฅ์๋ฅผ ์ฌ์ฉํด. ๋ํ ์๋ฒ์์ ์ฌ์ฉํ๋ ์ง๊ฐ์ ์ต์ํ์ ๊ถํ๋ง ๊ฐ์ง๋๋ก ์ค์ ํ๊ณ , ์ ๊ธฐ์ ์ผ๋ก ํค๋ฅผ ๋กํ ์ด์ ํด์ผ ํด.
2. ์ค๋งํธ ์ปจํธ๋ํธ ๋ณด์
์ฌ์ง์ ๊ณต๊ฒฉ(Reentrancy Attack), ์ ์ ์ค๋ฒํ๋ก์ฐ, ๊ถํ ๊ด๋ฆฌ ์ทจ์ฝ์ ๋ฑ ๋ค์ํ ๊ณต๊ฒฉ ๋ฒกํฐ๊ฐ ์์ด. OpenZeppelin ๊ฐ์ ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ณ , ๋ฐฐํฌ ์ ์ ๋ฐ๋์ ์ ๋ฌธ๊ฐ์ ๊ฐ์ฌ๋ฅผ ๋ฐ์์ผ ํด. Slither๋ Mythril ๊ฐ์ ์๋ํ ๋๊ตฌ๋ก ์ทจ์ฝ์ ์ ์ค์บํ๋ ๊ฒ๋ ์ข์.
3. API ๋ณด์
Rate limiting์ ์ ์ฉํด์ DDoS ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ๊ณ , JWT ํ ํฐ์ผ๋ก ์ธ์ฆ์ ๊ตฌํํด. ๋ฏผ๊ฐํ ์์ (์๋ฃ์ฆ ๋ฐ๊ธ, ์ง๊ฐ ์ฃผ์ ๋ณ๊ฒฝ ๋ฑ)์ ์ถ๊ฐ ์ธ์ฆ(2FA, ์ด๋ฉ์ผ ํ์ธ ๋ฑ)์ ์๊ตฌํ๋ ๊ฒ ์ข์.
4. ํ๋ก ํธ์๋ ๋ณด์
์ฌ์ฉ์์ ๊ฐ์ธํค๋ ์ ๋ ์๋ฒ๋ก ์ ์กํ์ง ๋ง. MetaMask ๊ฐ์ ์ง๊ฐ์ด ์๋ช ์ ์ฒ๋ฆฌํ๋๋ก ํด์ผ ํด. XSS ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฌ์ฉ์ ์ ๋ ฅ์ ํญ์ ๊ฒ์ฆํ๊ณ ์ด์ค์ผ์ดํ ์ฒ๋ฆฌํด.
5. ํธ๋์ญ์ ๊ฒ์ฆ
๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋กํ๊ธฐ ์ ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์์ ๊ฒ์ฆํด. ํด๋ผ์ด์ธํธ ์ธก ๊ฒ์ฆ๋ง์ผ๋ก๋ ๋ถ์กฑํด. ๋ํ ๊ฐ์ค ๊ฐ๊ฒฉ์ ๋ชจ๋ํฐ๋งํด์ ๋น์ ์์ ์ผ๋ก ๋์ ์์๋ฃ๊ฐ ๋ฐ์ํ์ง ์๋๋ก ํด์ผ ํด.
// ๋ณด์ ๋ฏธ๋ค์จ์ด ์์
class BlockchainSecurityMiddleware
{
public function handle($request, Closure $next)
{
// Rate limiting ์ฒดํฌ
if ($this->isRateLimitExceeded($request)) {
return response()->json(['error' => 'Too many requests'], 429);
}
// ์ง๊ฐ ์ฃผ์ ๊ฒ์ฆ
if ($request->has('wallet_address')) {
if (!$this->isValidAddress($request->wallet_address)) {
return response()->json(['error' => 'Invalid wallet address'], 400);
}
}
// ํธ๋์ญ์
์๋ช
๊ฒ์ฆ (ํ์์)
if ($request->has('signature')) {
if (!$this->verifySignature($request)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
}
return $next($request);
}
protected function isValidAddress($address)
{
return preg_match('/^0x[a-fA-F0-9]{40}$/', $address);
}
protected function verifySignature($request)
{
// ์๋ช
๊ฒ์ฆ ๋ก์ง
// Web3๋ฅผ ์ฌ์ฉํด์ ๋ฉ์์ง์ ์๋ช
์ ๊ฒ์ฆ
return true;
}
}
๐ ์ฑ๋ฅ ์ต์ ํ
๋ธ๋ก์ฒด์ธ ํ๋ซํผ์ ์ผ๋ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด๋ค ์ฑ๋ฅ ์ต์ ํ๊ฐ ๋ ์ค์ํด. ๋ธ๋ก์ฒด์ธ ์กฐํ๋ ๋๋ฆฌ๊ณ , ํธ๋์ญ์ ์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ฑฐ๋ . ๋ช ๊ฐ์ง ์ต์ ํ ์ ๋ต์ ์์๋ณด์. โก
๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ ๋ถ๋ณ์ด๊ธฐ ๋๋ฌธ์ ์บ์ฑํ๊ธฐ ์๋ฒฝํด. Redis๋ฅผ ์ฌ์ฉํด์ ์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ(์๋ฃ์ฆ ์ ๋ณด, ๊ฒ์ฆ ๊ฒฐ๊ณผ ๋ฑ)๋ฅผ ์บ์ํ๋ฉด ์๋ต ์๋๋ฅผ ํฌ๊ฒ ๊ฐ์ ํ ์ ์์ด.
use Illuminate\Support\Facades\Cache;
class CertificateService
{
public function getCertificateWithCache($tokenId)
{
$cacheKey = "certificate:{$tokenId}";
return Cache::remember($cacheKey, 3600, function () use ($tokenId) {
return $this->blockchainService->verifyCertificate($tokenId);
});
}
public function invalidateCertificateCache($tokenId)
{
Cache::forget("certificate:{$tokenId}");
}
}
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ํด์, ํ ํฐ ID, ์ง๊ฐ ์ฃผ์ ๋ฑ ์์ฃผ ๊ฒ์๋๋ ํ๋์ ์ธ๋ฑ์ค๋ฅผ ์ถ๊ฐํด์ผ ํด.
Schema::table('certificates', function (Blueprint $table) {
$table->index('token_id');
$table->index('blockchain_tx_hash');
$table->index('certificate_number');
});
Schema::table('users', function (Blueprint $table) {
$table->index('wallet_address');
});
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ๋ฐ๋์ ํ๋ก ์ฒ๋ฆฌํด์ผ ํด. ์ฌ์ฉ์๊ฐ ๊ธฐ๋ค๋ฆฌ์ง ์๋๋ก ํ๊ณ , ์คํจ ์ ์๋ ์ฌ์๋ ๋ก์ง์ ๊ตฌํํด.
// Job์ ์ฌ์๋ ๋ก์ง ์ถ๊ฐ
class IssueCertificateJob implements ShouldQueue
{
public $tries = 5;
public $backoff = [60, 120, 300, 600, 1200]; // ์ง์ ๋ฐฑ์คํ
public function handle()
{
// ํธ๋์ญ์
์ฒ๋ฆฌ
}
public function failed(Exception $exception)
{
// ์คํจ ์๋ฆผ ์ ์ก
Log::error('Certificate issuance failed permanently', [
'enrollment_id' => $this->enrollment->id,
'error' => $exception->getMessage()
]);
// ์ฌ์ฉ์์๊ฒ ์๋ฆผ
$this->enrollment->user->notify(
new CertificateIssuanceFailedNotification($exception->getMessage())
);
}
}
์ฌ๋ฌ ์๋ฃ์ฆ์ ๋์์ ๋ฐ๊ธํด์ผ ํ ๋๋ ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ์ค๋น๋ฅผ ์ ์ฝํ ์ ์์ด.
// ์ค๋งํธ ์ปจํธ๋ํธ์ ๋ฐฐ์น ๋ฐ๊ธ ํจ์ ์ถ๊ฐ
function batchIssueCertificates(
address[] memory students,
uint256[] memory courseIds,
string[] memory courseNames,
string[] memory tokenURIs,
uint256[] memory scores
) public onlyOwner returns (uint256[] memory) {
require(students.length == courseIds.length, "Array length mismatch");
uint256[] memory tokenIds = new uint256[](students.length);
for (uint256 i = 0; i < students.length; i++) {
tokenIds[i] = issueCertificate(
students[i],
courseIds[i],
courseNames[i],
tokenURIs[i],
scores[i]
);
}
return tokenIds;
}
๐งช ํ ์คํธ ์ ๋ต
๋ธ๋ก์ฒด์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ ์คํธ๊ฐ ํนํ ์ค์ํด. ์ค์ ๋ธ๋ก์ฒด์ธ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ์ฒ ์ ํ๊ฒ ํ ์คํธํด์ผ ํ๊ฑฐ๋ . ์ฌ๋ฌ ๋ ๋ฒจ์ ํ ์คํธ๋ฅผ ๊ตฌํํด๋ณด์. ๐ฌ
๊ฐ๋ณ ํจ์์ ๋ฉ์๋๋ฅผ ํ ์คํธํด. ๋ผ๋ผ๋ฒจ์ PHPUnit์ ์ฌ์ฉํ๋ฉด ๋ผ.
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Services\BlockchainService;
use Mockery;
class BlockchainServiceTest extends TestCase
{
public function test_validates_ethereum_address()
{
$service = new BlockchainService();
$this->assertTrue($service->isValidAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'));
$this->assertFalse($service->isValidAddress('invalid_address'));
$this->assertFalse($service->isValidAddress('0x123')); // ๋๋ฌด ์งง์
}
public function test_generates_certificate_metadata()
{
$enrollment = Enrollment::factory()->create();
$service = new CertificateService();
$metadata = $service->generateMetadata($enrollment);
$this->assertArrayHasKey('name', $metadata);
$this->assertArrayHasKey('description', $metadata);
$this->assertArrayHasKey('attributes', $metadata);
$this->assertIsArray($metadata['attributes']);
}
}
์ฌ๋ฌ ์ปดํฌ๋ํธ๊ฐ ํจ๊ป ์๋ํ๋์ง ํ ์คํธํด. ์ค์ ๋ก์ปฌ ๋ธ๋ก์ฒด์ธ(Ganache)์ ์ฌ์ฉํ ์ ์์ด.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Course;
use App\Models\Enrollment;
use Illuminate\Foundation\Testing\RefreshDatabase;
class CertificateIssuanceTest extends TestCase
{
use RefreshDatabase;
public function test_issues_certificate_when_course_completed()
{
$user = User::factory()->create([
'wallet_address' => '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
]);
$course = Course::factory()->create();
$enrollment = Enrollment::factory()->create([
'user_id' => $user->id,
'course_id' => $course->id,
'progress_percentage' => 100,
'completed_at' => now()
]);
$response = $this->actingAs($user)
->postJson("/api/certificates/issue/{$enrollment->id}");
$response->assertStatus(200)
->assertJson([
'message' => 'Certificate issuance started'
]);
// ํ ์์
์ด ๋์คํจ์น๋์๋์ง ํ์ธ
$this->assertDatabaseHas('jobs', [
'queue' => 'default'
]);
}
public function test_cannot_issue_certificate_for_incomplete_course()
{
$user = User::factory()->create();
$enrollment = Enrollment::factory()->create([
'user_id' => $user->id,
'progress_percentage' => 50
]);
$response = $this->actingAs($user)
->postJson("/api/certificates/issue/{$enrollment->id}");
$response->assertStatus(400)
->assertJson([
'error' => 'Course not completed'
]);
}
}
Hardhat์ ์ฌ์ฉํด์ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ํ ์คํธํด. JavaScript๋ก ์์ฑํ ์ ์์ด.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("LearningCertificate", function () {
let certificate;
let owner;
let student;
beforeEach(async function () {
[owner, student] = await ethers.getSigners();
const LearningCertificate = await ethers.getContractFactory("LearningCertificate");
certificate = await LearningCertificate.deploy();
await certificate.deployed();
});
it("Should issue a certificate", async function () {
const tx = await certificate.issueCertificate(
student.address,
1,
"Blockchain Development",
"https://example.com/metadata/1.json",
95
);
const receipt = await tx.wait();
const event = receipt.events.find(e => e.event === "CertificateIssued");
expect(event).to.not.be.undefined;
expect(event.args.student).to.equal(student.address);
expect(event.args.courseId).to.equal(1);
});
it("Should verify a certificate", async function () {
await certificate.issueCertificate(
student.address,
1,
"Blockchain Development",
"https://example.com/metadata/1.json",
95
);
const result = await certificate.verifyCertificate(0);
expect(result.isValid).to.be.true;
expect(result.student).to.equal(student.address);
expect(result.courseId).to.equal(1);
expect(result.score).to.equal(95);
});
it("Should get student certificates", async function () {
await certificate.issueCertificate(
student.address,
1,
"Course 1",
"https://example.com/1.json",
90
);
await certificate.issueCertificate(
student.address,
2,
"Course 2",
"https://example.com/2.json",
85
);
const certs = await certificate.getStudentCertificates(student.address);
expect(certs.length).to.equal(2);
});
it("Should revoke a certificate", async function () {
await certificate.issueCertificate(
student.address,
1,
"Course 1",
"https://example.com/1.json",
90
);
await certificate.revokeCertificate(0);
const result = await certificate.verifyCertificate(0);
expect(result.isValid).to.be.false;
});
it("Should not allow non-owner to issue certificates", async function () {
await expect(
certificate.connect(student).issueCertificate(
student.address,
1,
"Course 1",
"https://example.com/1.json",
90
)
).to.be.revertedWith("Ownable: caller is not the owner");
});
});
๐ ๋ฐฐํฌ ๋ฐ ์ด์
๊ฐ๋ฐ์ด ์๋ฃ๋์๋ค๋ฉด ์ด์ ์ค์ ํ๊ฒฝ์ ๋ฐฐํฌํ ์ฐจ๋ก์ผ. ๋ธ๋ก์ฒด์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐฐํฌ๋ ์ผ๋ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด๋ค ์ ์คํด์ผ ํด. ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด์. ๐ข
๋จผ์ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌํด์ผ ํด. ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธํ ํ์ ๋ฉ์ธ๋ท์ ๋ฐฐํฌํ์.
// hardhat.config.js ์ค์
require("@nomiclabs/hardhat-ethers");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
goerli: {
url: process.env.GOERLI_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
},
mainnet: {
url: process.env.MAINNET_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
},
polygon: {
url: process.env.POLYGON_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
// ๋ฐฐํฌ ๋ช
๋ น์ด
// ํ
์คํธ๋ท: npx hardhat run scripts/deploy.js --network goerli
// ๋ฉ์ธ๋ท: npx hardhat run scripts/deploy.js --network mainnet
// Polygon: npx hardhat run scripts/deploy.js --network polygon
โ ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ์ฌ ์๋ฃ
โ ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธ ์๋ฃ
โ ๊ฐ์ค๋น ์์ฐ ํ๋ณด
โ ๋ฐฐํฌ ๊ณ์ ์ ๊ฐ์ธํค ์์ ํ๊ฒ ๋ณด๊ด
โ ์ปจํธ๋ํธ ์์ค์ฝ๋ ๊ฒ์ฆ ์ค๋น
โ ๋กค๋ฐฑ ๊ณํ ์๋ฆฝ (ํ๋ก์ ํจํด ์ฌ์ฉ ์)
๋ผ๋ผ๋ฒจ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ผ๋ฐ์ ์ธ ๋ฐฉ๋ฒ์ผ๋ก ๋ฐฐํฌํ๋ฉด ๋ผ. AWS, DigitalOcean, Heroku ๋ฑ ๋ค์ํ ํ๋ซํผ์ ์ฌ์ฉํ ์ ์์ด.
# ํ๋ก๋์
ํ๊ฒฝ ์ค์
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com
# ๋ฐ์ดํฐ๋ฒ ์ด์ค
DB_CONNECTION=mysql
DB_HOST=your-db-host
DB_DATABASE=production_db
DB_USERNAME=db_user
DB_PASSWORD=secure_password
# ๋ธ๋ก์ฒด์ธ ์ค์
BLOCKCHAIN_RPC_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
CONTRACT_ADDRESS=0x... # ๋ฐฐํฌ๋ ์ปจํธ๋ํธ ์ฃผ์
FROM_ADDRESS=0x... # ์๋ฒ ์ง๊ฐ ์ฃผ์
PRIVATE_KEY=0x... # ์ ๋ ๋
ธ์ถ๋๋ฉด ์ ๋จ!
# ํ ์ค์
QUEUE_CONNECTION=redis
REDIS_HOST=your-redis-host
# ์บ์ ์ค์
CACHE_DRIVER=redis
# ์ธ์
์ค์
SESSION_DRIVER=redis
# ๋ฐฐํฌ ์คํฌ๋ฆฝํธ (deploy.sh)
#!/bin/bash
echo "Starting deployment..."
# Git pull
git pull origin main
# Composer ์์กด์ฑ ์ค์น
composer install --no-dev --optimize-autoloader
# NPM ๋น๋
npm install
npm run production
# ์บ์ ํด๋ฆฌ์ด
php artisan config:clear
php artisan cache:clear
php artisan view:clear
# ์บ์ ์์ฑ
php artisan config:cache
php artisan route:cache
php artisan view:cache
# ๋ง์ด๊ทธ๋ ์ด์
์คํ
php artisan migrate --force
# ํ ์์ปค ์ฌ์์
php artisan queue:restart
# ๊ถํ ์ค์
chmod -R 755 storage bootstrap/cache
echo "Deployment completed!"
์ด์ ํ๊ฒฝ์์๋ ๋ชจ๋ํฐ๋ง์ด ํ์์ผ. ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ํ, ํ ์์ , ์๋ฌ ๋ก๊ทธ ๋ฑ์ ์ค์๊ฐ์ผ๋ก ์ถ์ ํด์ผ ํด.
# Laravel Telescope ์ค์น (๊ฐ๋ฐ/์คํ
์ด์ง ํ๊ฒฝ)
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
# Laravel Horizon ์ค์น (ํ ๋ชจ๋ํฐ๋ง)
composer require laravel/horizon
php artisan horizon:install
# Sentry ์ค์น (์๋ฌ ์ถ์ )
composer require sentry/sentry-laravel
php artisan sentry:publish --dsn=YOUR_DSN
๐ฏ ์ค์ ํ์ฉ ์ฌ๋ก
์ด์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ํ๋ซํผ์ ์ค์ ๋ก ์ด๋ป๊ฒ ํ์ฉํ ์ ์๋์ง ์ดํด๋ณด์. ๋ค์ํ ์๋๋ฆฌ์ค๋ฅผ ํตํด ์ค์ฉ์ฑ์ ํ์ธํด๋ณผ๊ฒ. ๐ผ
์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์ ์ด ์์คํ ์ ๋์ ํ๋ฉด ์ด๋จ๊น? ๊ฐ์ฌ๊ฐ ๊ฐ์๋ฅผ ๋ฑ๋กํ๊ณ , ์๊ฐ์์ด ์๋ฃํ๋ฉด ์๋์ผ๋ก NFT ์๋ฃ์ฆ์ด ๋ฐ๊ธ๋ผ. ์ด ์๋ฃ์ฆ์ ๋ค๋ฅธ ํ๋ซํผ์์๋ ์ธ์ ๋ฐ์ ์ ์๊ณ , ์ทจ์ ํ ๋ ํฌํธํด๋ฆฌ์ค๋ก ํ์ฉํ ์ ์์ด. ์์กฐ๊ฐ ๋ถ๊ฐ๋ฅํ๋๊น ๊ธฐ์ ์ ์ฅ์์๋ ์ ๋ขฐํ ์ ์์ง.
๊ตฌํ ํฌ์ธํธ:
- ๊ฐ์๋ณ๋ก ๋ณ๋์ NFT ์ปฌ๋ ์ ์์ฑ
- ์๋ฃ์ฆ ๋์์ธ ์ปค์คํฐ๋ง์ด์ง ๊ธฐ๋ฅ
- ์๊ฐ์ ์ฑ์ทจ๋์ ๋ฐ๋ฅธ ๋ฑ๊ธ๋ณ ์๋ฃ์ฆ (Gold, Silver, Bronze)
- ์๋ฃ์ฆ ์์ ์ ์ ์ฉ ์ปค๋ฎค๋ํฐ ์ ๊ทผ ๊ถํ
๋๊ธฐ์ ์ด๋ ๊ณต๊ณต๊ธฐ๊ด์์ ์ง์ ๊ต์ก ์ด๋ ฅ์ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋กํ๋ฉด ์ด๋จ๊น? ์ง์์ด ์ด์งํ๊ฑฐ๋ ํด์ฌํด๋ ์์ ์ ๊ต์ก ์ด๋ ฅ์ ์ฆ๋ช ํ ์ ์๊ณ , ํ์ฌ๋ ๊ต์ก ํฌ์๋ฅผ ํฌ๋ช ํ๊ฒ ๊ด๋ฆฌํ ์ ์์ด.
๊ตฌํ ํฌ์ธํธ:
- ํ๋ผ์ด๋น ๋ธ๋ก์ฒด์ธ ์ฌ์ฉ (Hyperledger Fabric)
- ๋ถ์๋ณ, ์ง๊ธ๋ณ ์ ๊ทผ ๊ถํ ๊ด๋ฆฌ
- ๊ต์ก ์ด์ ์๊ฐ ์๋ ์ง๊ณ
- ์ฐ๊ฐ ๊ต์ก ๋ชฉํ ๋ฌ์ฑ๋ฅ ๋์๋ณด๋
๋ํ๊ต์์ ํ์์ฆ๋ช ์๋ฅผ ๋ธ๋ก์ฒด์ธ์ผ๋ก ๋ฐ๊ธํ๋ฉด ์ด๋จ๊น? ์กธ์ ์์ ์ธ์ ์ด๋์๋ ์์ ์ ํ์๋ฅผ ์ฆ๋ช ํ ์ ์๊ณ , ๊ธฐ์ ์ ์ฆ์ ๊ฒ์ฆํ ์ ์์ด. ์์กฐ ํ์ ๋ฌธ์ ๋ ์์ ํ ํด๊ฒฐ๋์ง.
๊ตฌํ ํฌ์ธํธ:
- ํ์, ์ฑ์ ํ, ์ฌํ์ฆ๋ช ์ ๋ฑ ๋ค์ํ ์ฆ๋ช ์ ๋ฐ๊ธ
- ๊ตญ์ ํ์ค ์ค์ (W3C Verifiable Credentials)
- ๋ค๊ตญ์ด ์ง์
- ์ํฌ์คํฐ์ ์ธ์ฆ ์ฐ๋
IT ์๊ฒฉ์ฆ, ์ดํ ์ํ, ์ ๋ฌธ ๋ฉดํ ๋ฑ์ ๋ธ๋ก์ฒด์ธ์ผ๋ก ๊ด๋ฆฌํ๋ฉด ์ด๋จ๊น? ์๊ฒฉ์ฆ ๋ฐ๊ธ ๊ธฐ๊ด์ ์์กฐ๋ฅผ ๋ฐฉ์งํ ์ ์๊ณ , ์๊ฒฉ์ฆ ์์ง์๋ ๋์งํธ ์ง๊ฐ์ ๋ชจ๋ ์๊ฒฉ์ฆ์ ๋ณด๊ดํ ์ ์์ด.
๊ตฌํ ํฌ์ธํธ:
- ์๊ฒฉ์ฆ ๊ฐฑ์ ๋ฐ ๋ง๋ฃ ๊ด๋ฆฌ
- ๊ณ์ ๊ต์ก ์ด์ ์๊ฐ ์ถ์
- ์๊ฒฉ์ฆ ๋ฑ๊ธ ์ ๊ทธ๋ ์ด๋ ์๋ํ
- ๊ด๋ จ ์๊ฒฉ์ฆ ์ถ์ฒ ์์คํ
์ด๋ฐ ํ์ฉ ์ฌ๋ก๋ค์ ๋ณด๋ฉด ๋ธ๋ก์ฒด์ธ ํ์ต ์ธ์ฆ ํ๋ซํผ์ ๊ฐ๋ฅ์ฑ์ด ์ ๋ง ๋ฌด๊ถ๋ฌด์งํ๋ค๋ ๊ฑธ ์ ์ ์์ด. ํต์ฌ์ ์ ๋ขฐ์ ํฌ๋ช ์ฑ์ด์ผ. ๋ธ๋ก์ฒด์ธ์ด ์ ๊ณตํ๋ ์ด ๋ ๊ฐ์ง ํน์ฑ์ด ๊ต์ก ๋ถ์ผ์์ ์ผ๋ง๋ ํฐ ๊ฐ์น๋ฅผ ์ฐฝ์ถํ ์ ์๋์ง ๋ณด์ฌ์ฃผ๋ ๊ฑฐ์ง. ๐
๐ฎ ๋ฏธ๋ ๋ฐ์ ๋ฐฉํฅ
์ฐ๋ฆฌ๊ฐ ๋ง๋ ํ๋ซํผ์ ๋์ฑ ๋ฐ์ ์ํฌ ์ ์๋ ๋ฐฉํฅ๋ค์ ์ดํด๋ณด์. Web3 ๊ธฐ์ ์ ๊ณ์ ์งํํ๊ณ ์์ผ๋๊น, ์ต์ ํธ๋ ๋๋ฅผ ๋ฐ๋ผ๊ฐ๋ ๊ฒ๋ ์ค์ํด! ๐
1. DID (Decentralized Identity) ํตํฉ
์ฌ์ฉ์๊ฐ ์์ ์ ์ ์์ ์์ ํ ํต์ ํ ์ ์๋๋ก DID๋ฅผ ๋์ ํด. ์ฌ๋ฌ ํ๋ซํผ์์ ํ๋์ ์ ์์ผ๋ก ํ์ต ์ด๋ ฅ์ ๊ด๋ฆฌํ ์ ์์ด.
2. ํ์ต ํ ํฐ ์ด์ฝ๋ ธ๋ฏธ
๊ฐ์๋ฅผ ์๋ฃํ๋ฉด ํ ํฐ์ ๋ณด์์ผ๋ก ์ง๊ธํ๊ณ , ์ด ํ ํฐ์ผ๋ก ๋ค๋ฅธ ๊ฐ์๋ฅผ ๊ตฌ๋งคํ๊ฑฐ๋ ํ๋ฆฌ๋ฏธ์ ๊ธฐ๋ฅ์ ์ด์ฉํ ์ ์๊ฒ ํด. ํ์ต ๋๊ธฐ๋ฅผ ๋์ด๋ ํจ๊ณผ๊ฐ ์์ง.
3. DAO ๊ธฐ๋ฐ ๊ฑฐ๋ฒ๋์ค
ํ๋ซํผ ์ด์์ ์ปค๋ฎค๋ํฐ๊ฐ ๊ฒฐ์ ํ๋๋ก DAO๋ฅผ ๊ตฌ์ถํด. ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ, ์์๋ฃ ์กฐ์ ๋ฑ์ ํฌํ๋ก ๊ฒฐ์ ํ ์ ์์ด.
4. ํฌ๋ก์ค์ฒด์ธ ์ง์
์ฌ๋ฌ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ๋ฅผ ์ง์ํด์ ์ฌ์ฉ์๊ฐ ์ํ๋ ์ฒด์ธ์ ์ ํํ ์ ์๊ฒ ํด. ์ด๋๋ฆฌ์, ํด๋ฆฌ๊ณค, ๋ฐ์ด๋ธ์ค ์ค๋งํธ ์ฒด์ธ ๋ฑ์ ๋ชจ๋ ์ง์ํ๋ ๊ฑฐ์ง.
5. AI ๊ธฐ๋ฐ ํ์ต ์ถ์ฒ
๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋ ํ์ต ์ด๋ ฅ์ ๋ถ์ํด์ ๊ฐ์ธํ๋ ๊ฐ์๋ฅผ ์ถ์ฒํด. ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ์ ํ์ฉํ๋ฉด ๋ ํจ๊ณผ์ ์ธ ํ์ต ๊ฒฝ๋ก๋ฅผ ์ ์ํ ์ ์์ด.
6. ์์ ๊ธฐ๋ฅ ๊ฐํ
์๋ฃ์ฆ NFT๋ฅผ ์์ ๋ฏธ๋์ด์ ๊ณต์ ํ๊ณ , ๊ฐ์ ๊ฐ์๋ฅผ ์๊ฐํ ์ฌ๋๋ค๋ผ๋ฆฌ ๋คํธ์ํนํ ์ ์๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด.
7. ๋ฉํ๋ฒ์ค ํตํฉ
๊ฐ์ ์ธ๊ณ์์ ์๋ฃ์ฆ์ ์ ์ํ๊ณ , ๊ฐ์ ์กธ์ ์์ ์ด ์ ์์ด. NFT ์๋ฃ์ฆ์ ์๋ฐํ ๋ฐฐ์ง๋ก ์ฐฉ์ฉํ๋ ๊ฒ๋ ๊ฐ๋ฅํ์ง.
// ํ ํฐ ์ด์ฝ๋
ธ๋ฏธ ์์ - ERC-20 ํ ํฐ ์ปจํธ๋ํธ
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract LearningToken is ERC20, Ownable {
// ๊ฐ์ ์๋ฃ ์ ๋ณด์ ํ ํฐ ์
mapping(uint256 => uint256) public courseRewards;
// ์ฌ์ฉ์๋ณ ํ๋ ํ ํฐ ์ด๋ ฅ
mapping(address => uint256[]) public userRewardHistory;
constructor() ERC20("Learning Token", "LEARN") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function setCourseReward(uint256 courseId, uint256 reward) public onlyOwner {
courseRewards[courseId] = reward;
}
function rewardStudent(address student, uint256 courseId) public onlyOwner {
uint256 reward = courseRewards[courseId];
require(reward > 0, "No reward set for this course");
_mint(student, reward);
userRewardHistory[student].push(courseId);
emit StudentRewarded(student, courseId, reward);
}
event StudentRewarded(address indexed student, uint256 indexed courseId, uint256 amount);
}
์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ๋จ๊ณ์ ์ผ๋ก ์ถ๊ฐํ๋ฉด์ ํ๋ซํผ์ ๋ฐ์ ์์ผ ๋๊ฐ๋ฉด, ์ ๋ง ํ์ ์ ์ธ ๊ต์ก ์ํ๊ณ๋ฅผ ๋ง๋ค ์ ์์ด. ์ค์ํ ๊ฑด ์ฌ์ฉ์์ ํผ๋๋ฐฑ์ ์ง์์ ์ผ๋ก ์์งํ๊ณ , ์ค์ ๋ก ํ์ํ ๊ธฐ๋ฅ์ ์ฐ์ ์์์ ๋๋ ๊ฑฐ์ผ. ๐ฏ
๐ก ๋ง์น๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. ์ฐ๋ฆฌ๋ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ์ ๊ฒฐํฉํด์ ํ์ต ์ธ์ฆ ํ๋ซํผ์ ๊ตฌ์ถํ๋ ์ ๊ณผ์ ์ ์ดํด๋ดค์ด. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ๋ถํฐ ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ, ํ๋ก ํธ์๋ ๊ตฌํ, ๋ณด์, ํ ์คํธ, ๋ฐฐํฌ๊น์ง ์ ๋ง ๋ง์ ๋ด์ฉ์ ๋ค๋ค์ง. ๐
๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ์ฒ์์๋ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ด. ํ์ง๋ง ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐฐ์๋๊ฐ๋ฉด ์๊ฐ๋ณด๋ค ์ด๋ ต์ง ์์. ํนํ ๋ผ๋ผ๋ฒจ ๊ฐ์ ๊ฐ๋ ฅํ ํ๋ ์์ํฌ์ ๊ฒฐํฉํ๋ฉด, ๋ณต์กํ ๋ธ๋ก์ฒด์ธ ๋ก์ง๋ ๊น๋ํ๊ฒ ๊ตฌํํ ์ ์๊ฑฐ๋ .
1. ํ์ด๋ธ๋ฆฌ๋ ์ํคํ ์ฒ: ์ค์ํ ์ธ์ฆ ๋ฐ์ดํฐ๋ ๋ธ๋ก์ฒด์ธ์, ๋๋จธ์ง๋ ์ผ๋ฐ DB์ ์ ์ฅํด์ ๋น์ฉ๊ณผ ์ฑ๋ฅ์ ์ต์ ํํด.
2. ๋ณด์ ์ต์ฐ์ : ๊ฐ์ธํค ๊ด๋ฆฌ, ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ์ฌ, API ๋ณด์ ๋ฑ ๋ชจ๋ ๋ ์ด์ด์์ ๋ณด์์ ๊ณ ๋ คํด์ผ ํด.
3. ์ฌ์ฉ์ ๊ฒฝํ: ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋๊น, ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ช ํํ ํผ๋๋ฐฑ์ผ๋ก UX๋ฅผ ๊ฐ์ ํด.
4. ํ ์คํธ ์ฒ ์ ํ: ๋ธ๋ก์ฒด์ธ์ ๋๋๋ฆด ์ ์์ผ๋๊น, ๋ฐฐํฌ ์ ์ ์ถฉ๋ถํ ํ ์คํธํด์ผ ํด.
5. ์ง์์ ๊ฐ์ : Web3 ๊ธฐ์ ์ ๋น ๋ฅด๊ฒ ๋ฐ์ ํ๋๊น, ์ต์ ํธ๋ ๋๋ฅผ ๋ฐ๋ผ๊ฐ๋ฉด์ ํ๋ซํผ์ ๊ฐ์ ํด ๋๊ฐ์ผ ํด.
์ด ํ๋ซํผ์ ๋จ์ํ ๊ธฐ์ ์ ์ธ ๊ตฌํ์ ๋์ด์, ๊ต์ก์ ๋ฏธ๋๋ฅผ ๋ฐ๊ฟ ์ ์๋ ์ ์ฌ๋ ฅ์ ๊ฐ์ง๊ณ ์์ด. ํ์ต์๋ ์์ ์ ์ฑ์ทจ๋ฅผ ์๊ตฌ์ ์ผ๋ก ์ฆ๋ช ํ ์ ์๊ณ , ๊ต์ก ๊ธฐ๊ด์ ์ ๋ขฐ๋ฅผ ๊ตฌ์ถํ ์ ์์ผ๋ฉฐ, ๊ธฐ์ ์ ์ธ์ฌ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ฒ์ฆํ ์ ์์ง.
ํนํ ์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์ ์ด๋ฐ ์์คํ ์ ๋์ ํ๋ค๋ฉด, ๊ฐ์ฌ์ ์๊ฐ์ ๋ชจ๋์๊ฒ ํฐ ๊ฐ์น๋ฅผ ์ ๊ณตํ ์ ์์ ๊ฑฐ์ผ. ๊ฐ์ฌ๋ ์์ ์ ๊ฐ์ ํ์ง์ ๊ฐ๊ด์ ์ผ๋ก ์ฆ๋ช ํ ์ ์๊ณ , ์๊ฐ์์ ๊ฒ์ฆ ๊ฐ๋ฅํ ํ์ต ์ด๋ ฅ์ ์์ ์ ์์ผ๋๊น ๋ง์ด์ผ.
์, ์ด์ ๋๋ ์ง์ ๋ธ๋ก์ฒด์ธ ํ์ต ์ธ์ฆ ํ๋ซํผ์ ๋ง๋ค์ด๋ณผ ์ค๋น๊ฐ ๋์์ด! ์ฒ์์๋ ์๊ฒ ์์ํด๋ ๊ด์ฐฎ์. ๋ก์ปฌ ํ๊ฒฝ์์ ํ ์คํธํ๊ณ , ํ ์คํธ๋ท์ ๋ฐฐํฌํด๋ณด๊ณ , ์ ์ง์ ์ผ๋ก ๊ธฐ๋ฅ์ ์ถ๊ฐํด ๋๊ฐ๋ฉด ๋ผ. ์ค์ํ ๊ฑด ์์ํ๋ ๊ฑฐ์ผ! ๐
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๊ถ๊ธํ ๊ฒ ์๊ธฐ๋ฉด, ๋ผ๋ผ๋ฒจ ์ปค๋ฎค๋ํฐ๋ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ด. ๋ค๋ค ์น์ ํ๊ฒ ๋์์ค ๊ฑฐ์ผ. ๊ทธ๋ฆฌ๊ณ GitHub์ ์คํ์์ค๋ก ๊ณต๊ฐํ๋ฉด ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ํผ๋๋ฐฑ๋ ๋ฐ์ ์ ์๊ณ , ํจ๊ป ๋ฐ์ ์์ผ ๋๊ฐ ์ ์์ด.
Web3 ์๋๋ ์ด๋ฏธ ์์๋์์ด. ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ํ์ฉํ ํ์ ์ ์ธ ์๋น์ค๋ค์ด ๊ณ์ ๋ฑ์ฅํ๊ณ ์์ง. ๋๋ ์ด ํ๋ฆ์ ๋์ฐธํด์, ์ธ์์ ์กฐ๊ธ ๋ ํฌ๋ช ํ๊ณ ๊ณต์ ํ๊ฒ ๋ง๋๋ ๋ฐ ๊ธฐ์ฌํ ์ ์์ด. ํ์ดํ ! ๐ชโจ
1. ์ด ๊ธ์ ์ฝ๋๋ฅผ ์ง์ ํ์ดํํ๋ฉด์ ๋ฐ๋ผํด๋ด (๋ณต๋ถ๋ณด๋ค ํจ์ฌ ๋ง์ด ๋ฐฐ์)
2. ๋ก์ปฌ ํ๊ฒฝ์์ ์ ์ฒด ํ๋ก์ฐ๋ฅผ ํ ์คํธํด๋ด
3. ์์ ๋ง์ ๊ธฐ๋ฅ์ ํ๋ ์ถ๊ฐํด๋ด (์ฐฝ์๋ ฅ ๋ฐํ!)
4. GitHub์ ํ๋ก์ ํธ๋ฅผ ์ฌ๋ฆฌ๊ณ ํฌํธํด๋ฆฌ์ค๋ก ํ์ฉํด๋ด
5. ์ค์ ํ ์คํธ๋ท์ ๋ฐฐํฌํด์ ์น๊ตฌ๋ค์๊ฒ ๋ณด์ฌ์ค
6. ์ปค๋ฎค๋ํฐ์ ๊ณต์ ํ๊ณ ํผ๋๋ฐฑ์ ๋ฐ์๋ด
๋ง์ง๋ง์ผ๋ก, ์ด ๊ธ์ด ๋์์ด ๋์๋ค๋ฉด ์ ๋ง ๊ธฐ์ ๊ฒ ๊ฐ์. ๋ธ๋ก์ฒด์ธ๊ณผ ๋ผ๋ผ๋ฒจ์ ๊ฒฐํฉํ ํ๋ก์ ํธ๋ ์ ๋ง ์ฌ๋ฏธ์๊ณ ๋ณด๋์ฐฌ ๊ฒฝํ์ด ๋ ๊ฑฐ์ผ. ์์ผ๋ก๋ ๊ณ์ ๋ฐฐ์ฐ๊ณ ์ฑ์ฅํ๋ ๊ฐ๋ฐ์๊ฐ ๋๊ธธ ์์ํ ๊ฒ! ๐
Happy coding! ๊ทธ๋ฆฌ๊ณ ๋ธ๋ก์ฒด์ธ ์ธ๊ณ์์ ๋ง๋์! ๐๐
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

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